Skip to content

[APPS-2792] Add: runtime network/subprocess guard for local execution - #484

Draft
tyffical wants to merge 8 commits into
tiffany.trinh/apps-2792-wire-into-dev-serverfrom
tiffany.trinh/apps-2792-runtime-network-guard
Draft

[APPS-2792] Add: runtime network/subprocess guard for local execution#484
tyffical wants to merge 8 commits into
tiffany.trinh/apps-2792-wire-into-dev-serverfrom
tiffany.trinh/apps-2792-runtime-network-guard

Conversation

@tyffical

@tyfficaltyffical commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Motivation

Architecture

net.Socket.prototype.connect, globalThis.fetch, and child_process's spawn/exec/execSync are real, process-wide singletons — network-guard.ts monkey-patches them directly rather than sandboxing the customer's module, since there's no process boundary to sandbox with. That makes the guard a single shared piece of mutable state (allowDepth + the saved originals) threaded through one execution's lifetime:

runScriptLocally
│
▼
┌───────────────────────────────┐
│ runBlocked(fn) │ applyPatches()
│ BLOCKED: net.Socket.connect, │ → net.Socket.connect throws
│ fetch, child_process all │ → fetch rejects
│ throw/reject │ → spawn/exec/execSync throw
└───────────────┬────────────────┘
│ customer's fn() runs
▼
fn() calls $.Actions.a() and $.Actions.b() concurrently (Promise.all)
│
┌─────────┴──────────┐
▼ ▼
runAllowed(a) runAllowed(b)
allowDepth 0→1 allowDepth 1→2
restorePatches() (already restored — no-op)
│ │
▼ ▼
┌────────────────────────────────────┐
│ ALLOWED (allowDepth > 0) │
│ real net/fetch/spawn restored — │
│ only inside executeAction │
└──────┬───────────────────────┬─────┘
│ b resolves first │ a still in flight
▼ │
allowDepth 2→1 │
(still > 0 → stays ALLOWED) ──┘
│
│ a resolves
▼
allowDepth 1→0 → applyPatches() → BLOCKED again
│
│ fn() returns
▼
runBlocked's finally: restorePatches()
│
▼
UNBLOCKED (real functions, for whatever
the dev server does next)

The ref-count (allowDepth), not a boolean, is what makes the overlap safe: two concurrent $.Actions calls each bump it on entry and drop it on exit, and the guard only re-blocks once the last one exits — a boolean would re-block the instant the faster of two overlapping calls finished, breaking the slower one mid-flight.

runBlocked/runAllowed's own try/finally only unwinds when fn actually settles. runScriptLocally's timeout wraps the whole thing in Promise.race([run(), timeout]), which abandons rather than cancels the loser — a customer function that never resolves means run() (and the runBlocked inside it) never reaches its finally, so without a separate backstop the block would stay applied for the rest of the process once the timeout fires. forceReset() is that backstop: called directly from the timer callback (unconditionally restoring the real functions and zeroing allowDepth) the moment the timeout fires, independently of whether the abandoned run() ever settles — alongside local-execution.ts's own epoch-gated poisonActionCatalogRegistration() call in the same timer (from #480), since both are closing the same class of "abandoned execution left shared state pointing the wrong way" gap. The same function is used as a Jest afterEach in network-guard.test.ts/local-execution.test.ts, for the identical reason at the test level — these are real Node singletons, not per-test-file sandboxed state, so a test that leaves them patched leaks into every test that runs after it in the same Jest worker, including unrelated test files.

Changes

What changedFile
New runBlocked(fn): monkey-patches net.Socket.prototype.connect, fetch, and child_process's spawn/exec/execSync to throw/reject for the duration of fn, restoring the real implementations in a finally regardless of how fn completes.network-guard.ts
New runAllowed(fn): temporarily restores real network access for the duration of fn, ref-counted (not a boolean) so two $.Actions calls overlapping within a single execution (e.g. inside a Promise.all) don't re-block network on each other mid-flight.network-guard.ts
New forceReset(): unconditionally restores the real functions and zeroes allowDepth, independent of runBlocked/runAllowed's own finally — the backstop for a fn that's abandoned (timeout) or a test that fails to clean up after itself.network-guard.ts
runScriptLocally now wraps the customer's function call (only — not the loadModule/registration calls before it, which need no network) in runBlocked, and calls forceReset() from the timeout timer itself so an abandoned, still-running hung function can't leave network/subprocess access blocked for the rest of the process.local-execution.ts
makeActionsProxy's apply trap now wraps its executeAction call in runAllowed — the one sanctioned network path, exempted from the block.local-execution.ts
Unit tests for every patched target and both directions (block + restore, restore-on-throw, no state leak across separate runBlocked calls, nested runAllowed exemption, concurrent-overlap ref-counting, re-block-on-throw). A Jest afterEach calls forceReset() unconditionally as a hard safety net, independent of any test's own cleanup.network-guard.test.ts
Integration tests confirming the guard is actually wired into executeScriptLocally: a customer function using raw net/fetch/child_process is rejected; a real $.Actions call still succeeds; network is restored after the execution finishes, including after a timeout abandons a hung function; two real $.Actions calls made concurrently via Promise.all keep network allowed through the entire overlap, exercised through the real executeScriptLocallymakeActionsProxy path (not just the unit-level runAllowed). Same afterEach safety net as above.local-execution.test.ts

QA Instructions

yarn install
yarn test:unit packages/plugins/apps/src/vite/network-guard.test.ts
# Expected: Test Suites: 1 passed / Tests: 11 passed ✅ VERIFIED
yarn test:unit packages/plugins/apps/src/vite/local-execution.test.ts
# Expected: Test Suites: 1 passed / Tests: 30 passed ✅ VERIFIED
yarn test:unit packages/plugins/apps
# Expected: Test Suites: 25 passed / Tests: 337 passed ✅ VERIFIED
yarn workspace @dd/apps-plugin run typecheck
# Expected: no output, clean exit ✅ VERIFIED
npx eslint 'packages/plugins/apps/**/*.ts' packages/tests/src/_jest/helpers/mocks.ts --quiet
# Expected: no output, clean exit ✅ VERIFIED

Coverage note: this repo's Jest collectCoverageFrom CLI flag didn't produce a usable per-file report for either new/changed file in this environment (pre-existing tooling quirk, not introduced by this change — the coverage table only ever listed _jest helper files regardless of the glob passed). Manually verified every branch in network-guard.ts is exercised by at least one test.

This module isn't independently reachable from a real npm run dev session on its own — that requires #481. It was exercised as part of a real, combined manual QA pass across the full stack (see #481's QA Instructions): a real scaffolded app, running with the full stack merged locally, confirmed the network guard correctly blocks raw net/fetch in a customer function while still letting a real $.Actions call through — end-to-end, not just at the unit-test level.

Blast Radius

  • No behavior change for any currently-shipping code path — same as [APPS-2792] Add: in-process local execution for backend functions #479/[APPS-2792] Add: harden the in-process local execution path #480, this stack isn't released yet.
  • Scoped precisely to the duration of a local execution's customer-function call; the dev server's own network use (before/after that window, and anything unrelated to local-execution.ts) is never touched.
  • forceReset() on timeout narrows, rather than eliminates, an existing gap: the abandoned (not cancelled) hung function keeps running with real network access restored early rather than staying blocked forever — bounded to that one already-abandoned execution, versus the alternative of leaving every future execution in the same dev server process permanently blocked until restart.
  • Risk: low. Additive, defense-in-depth only — closes a gap that only matters for local-dev-loop safety/prod-parity, not a new production security boundary (production's own Deno sandbox is unaffected and remains the real boundary).

Out of Scope / Follow-ups

ItemStatusNext step
Native addon bypassing Node's JS-level net stack entirelyAccepted residual gapNarrower and rarer than the pure-JS case this closes (most native modules are for CPU-bound work, not networking) — not worth the false-positive risk of blocking native addon loading outright
dns.lookup interceptionOut of scopeLow realistic benefit for this threat model (dev-loop safety, not defending against deliberate DNS-tunneling exfiltration) — would risk breaking legitimate hostname validation for no real gain
A hung customer function is abandoned, not cancelled, on timeout — it keeps running in the background with real network access restored (see Blast Radius)Accepted residual gapWould need real cancellation (e.g. an AbortSignal threaded through the customer's own function, which we don't control) or re-architecting local execution onto a worker thread that can be killed outright — bigger change than this PR's scope

Documentation

@datadog-prod-us1-6

datadog-prod-us1-6Bot commented Aug 8, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 063d85e | Docs | View more details | Give us feedback!

tyffical added a commit that referenced this pull request Aug 10, 2026
dev-server.test.ts and local-execution.test.ts (build-plugins#480/#484)
each defined their own near-identical LoadModule resolver double. Factor
the common resolve-or-throw logic into moduleResolverFor in the shared
mocks helper so both can build on it instead of duplicating it.
tyffical added a commit that referenced this pull request Aug 20, 2026
dev-server.test.ts and local-execution.test.ts (build-plugins#480/#484)
each defined their own near-identical LoadModule resolver double. Factor
the common resolve-or-throw logic into moduleResolverFor in the shared
mocks helper so both can build on it instead of duplicating it.
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-runtime-network-guard branch from e48778e to 2ad1ce9CompareAugust 20, 2026 22:19
@tyffical
tyffical changed the base branch from tiffany.trinh/apps-2792-harden-local-execution-v2 to tiffany.trinh/apps-2792-wire-into-dev-serverAugust 20, 2026 22:25
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-runtime-network-guard branch from 2ad1ce9 to b69e5f2CompareAugust 20, 2026 22:28
tyffical added a commit that referenced this pull request Aug 20, 2026
dev-server.test.ts and local-execution.test.ts (build-plugins#480/#484)
each defined their own near-identical LoadModule resolver double. Factor
the common resolve-or-throw logic into moduleResolverFor in the shared
mocks helper so both can build on it instead of duplicating it.
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from d976f85 to 1900a78CompareAugust 20, 2026 23:16
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-runtime-network-guard branch from b69e5f2 to 63539ebCompareAugust 20, 2026 23:31
tyffical added a commit that referenced this pull request Aug 20, 2026
dev-server.test.ts and local-execution.test.ts (build-plugins#480/#484)
each defined their own near-identical LoadModule resolver double. Factor
the common resolve-or-throw logic into moduleResolverFor in the shared
mocks helper so both can build on it instead of duplicating it.
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from 1900a78 to a0bcc4fCompareAugust 20, 2026 23:38
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-runtime-network-guard branch from 63539eb to 1b11592CompareAugust 20, 2026 23:40
tyffical added a commit that referenced this pull request Aug 21, 2026
dev-server.test.ts and local-execution.test.ts (build-plugins#480/#484)
each defined their own near-identical LoadModule resolver double. Factor
the common resolve-or-throw logic into moduleResolverFor in the shared
mocks helper so both can build on it instead of duplicating it.
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from a0bcc4f to dc33400CompareAugust 21, 2026 03:52
tyfficaland others added 5 commits August 20, 2026 23:53
Closes a gap the build-time checks in ast-parsing/ can't reach: those
only scan the customer's own .backend.ts file, so a third-party
dependency's own net/http/fetch usage (e.g. a Postgres/Redis client)
was invisible to them, and nothing else stopped it once local
execution moved in-process (no Deno, no process boundary).
Blocks net.Socket.prototype.connect, fetch, and child_process's
spawn/exec/execSync for the duration of a local execution, exempting
only the internal $.Actions call via a ref-counted allow scope (so
concurrent $.Actions calls within a single execution don't fight over
re-blocking).
Co-Authored-By: Claude <noreply@anthropic.com>
…xecutions
Promise.race in runScriptLocally abandons whichever of run()/timeout
loses without cancelling it. A hung customer function (its promise
never settling) meant runBlocked's own finally never ran, leaving
net.Socket.connect/fetch/child_process patched to throw for the rest
of the process — poisoning every later local execution, and in CI,
leaking into unrelated test files that happened to run afterward in
the same Jest worker (e.g. rollupConfig.test.ts's real esbuild spawn).
forceReset() is a hard backstop independent of runBlocked/runAllowed's
own try/finally: it unconditionally restores the real functions and
zeroes the ref-count. runScriptLocally calls it directly from the
timeout timer, and network-guard.test.ts/local-execution.test.ts now
call it in an afterEach regardless of test outcome, since these are
real process-wide Node singletons, not per-test-file sandboxed state.
…ocally
network-guard.test.ts already proves runAllowed's ref-counting at the
unit level, calling it directly. Adds the same proof through the real
path a customer's code takes: executeScriptLocally's Promise.all of
two $.Actions calls, through makeActionsProxy's apply trap, with the
mocked ExecuteAction making its own real fetch call to stand in for
the network call the dev server's own implementation makes — network
must stay allowed for the slower call the entire time the faster one
is finishing and re-blocking.
Also adds a regression test for the timeout/forceReset fix, and the
same afterEach safety net as network-guard.test.ts.
dgram (raw UDP) has its own socket implementation and doesn't route
through net.Socket.prototype.connect, so it isn't caught by this
guard's patching. Unlike the existing native-addon gap, this one is
pure JS and closeable the same way if it turns out to matter — call it
out explicitly rather than leaving it undisclosed alongside the
addon gap.
… found in review
network-guard.ts's applyPatches/restorePatches and allowDepth are shared,
module-level state with no per-execution identity. An abandoned (not
cancelled) execution's runBlocked/runAllowed call can settle well after
a newer execution has started its own — its late restore/decrement was
acting on whatever the newer execution's own snapshot/depth happened to
be, either prematurely unblocking the newer execution's active sandbox
or driving allowDepth permanently negative (breaking every future
$.Actions call in the process). Both runBlocked and runAllowed now
capture a generation counter at entry and skip their own restore/decrement
if a newer generation has since taken over; forceReset() bumps the
generation so an abandoned call's eventual settlement is reliably
recognized as stale.
registerActionCatalogIfInstalled's registered callback also didn't wrap
its call to the injected executeAction in runAllowed the way
makeActionsProxy's raw $.Actions path does — a real action-catalog
typed-wrapper call whose ExecuteAction implementation itself makes a
network call was incorrectly rejected as blocked, since it runs from
inside the customer function's still-active runBlocked scope.
@tyffical
tyfficalforce-pushed the tiffany.trinh/apps-2792-runtime-network-guard branch from 1b11592 to 9d591ecCompareAugust 21, 2026 03:54
@tyffical
tyffical requested a balanced review from CopilotAugust 21, 2026 16:24
@tyffical

Copy link
Copy Markdown
ContributorAuthor

@cursor review
@codex review

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Friend, this PR adds runtime restrictions for in-process local backend execution.

Changes:

  • Adds process-wide network and subprocess guards.
  • Exempts $.Actions calls and resets guards after timeouts.
  • Adds unit and integration coverage for guard behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

FileDescription
network-guard.tsImplements blocking, exemptions, and reset logic.
network-guard.test.tsTests guard state and concurrency.
local-execution.tsIntegrates guards into local execution.
local-execution.test.tsTests execution-path guard behavior.
Suppressed comments (1)

packages/plugins/apps/src/vite/network-guard.ts:165

  • runAllowed can run after its enclosing blocked scope has already been reset. In the existing abandoned-execution scenario, a late call through a captured $.Actions proxy increments from zero, the guarded action rejects, and this applyPatches() then leaves the whole process blocked even though no runBlocked is active; the test's afterEach(forceReset) masks the leak. Track whether this call actually entered from an active blocked scope and only reapply in that case, or perform the abandoned check before entering runAllowed.
 if (currentGeneration === myGeneration) {
allowDepth -= 1;
if (allowDepth === 0) {
applyPatches();
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadpackages/plugins/apps/src/vite/network-guard.ts Outdated
Comment on lines +428 to +433
// Blocks net/fetch/child_process for the duration of the customer's
// function call only — loadModule and the registration calls above
// (both Vite's own transform pipeline, no network) run unguarded.
// $.Actions calls made from inside fn are exempted via `runAllowed`
// in `makeActionsProxy`. See network-guard.ts.
const result = await runBlocked(() => fn(...args));

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried fixing this by moving runBlocked to wrap loadModule itself, but reverted it — Vite's real ssrLoadModule pipeline needs genuine network/fs access internally to transform and resolve the customer's module, and blocking that broke the real dev-server integration test outright (not just theoretical: a real @datadog/apps-backend import through a real Vite server started returning 500). Documented as an accepted residual gap in network-guard.ts's own doc comment, alongside the existing native-addon and dgram gaps, rather than engineered around further for now. Leaving unresolved to keep it tracked.

Comment threadpackages/plugins/apps/src/vite/network-guard.ts Outdated
Comment on lines +88 to +104
function restorePatches(): void {
if (savedConnect) {
net.Socket.prototype.connect = savedConnect;
}
if (savedFetch) {
globalThis.fetch = savedFetch;
}
if (savedSpawn) {
child_process.spawn = savedSpawn;
}
if (savedExec) {
child_process.exec = savedExec;
}
if (savedExecSync) {
child_process.execSync = savedExecSync;
}
}

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:9d591ecafc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/plugins/apps/src/vite/network-guard.ts Outdated
Comment threadpackages/plugins/apps/src/vite/network-guard.ts Outdated
Comment threadpackages/plugins/apps/src/vite/network-guard.ts Outdated
…etwork guard
execFile/execFileSync/fork weren't patched alongside spawn/exec/execSync, and
restorePatches() left stale references in the saved* variables after
consuming them, so an idle forceReset() could reinstall a snapshot from a
completed cycle and clobber whatever a later caller had installed since.
Also tried moving runBlocked to wrap loadModule itself, to close the gap
where a module-level side effect runs before the guard is active. Reverted:
it broke the real Vite dev-server integration test, since ssrLoadModule's
own transform pipeline needs real network/fs access internally. Documented
as an accepted residual gap instead, alongside the existing native-addon and
dgram gaps.
… blocked
spawnSync was missing from the patched subprocess API list alongside
spawn/exec/execSync/execFile/execFileSync/fork.
More significantly: runAllowed captured currentGeneration at entry, a
counter that keeps monotonically advancing regardless of whether any
runBlocked scope is actually active. An abandoned execution's $.Actions call
that only reaches runAllowed after forceReset already fired (rather than
being already in flight when it fires) would capture whatever generation
number is current at that point — a number that, with no newer execution
started yet, never changes again. Its own finally would then see that
generation still "matches" and call applyPatches() with no corresponding
runBlocked left alive to ever restorePatches() again, permanently blocking
net/fetch/child_process for every execution afterward.
Introduces activeGeneration, tracking which generation (if any) currently
owns a live runBlocked scope, separately from the ever-incrementing
currentGeneration counter. runAllowed now checks activeGeneration both at
entry (treating a call with no live scope to belong to as an inert no-op)
and at exit, closing the gap.
@tyffical
tyffical requested a balanced review from CopilotAugust 21, 2026 20:02
@tyffical

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:b36f55e0bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// (both Vite's own transform pipeline, no network) run unguarded.
// $.Actions calls made from inside fn are exempted via `runAllowed`
// in `makeActionsProxy`. See network-guard.ts.
const result = await runBlocked(() => fn(...args));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip guard entry after an execution already timed out

When the timeout fires while loadModule(...) is still pending, forceReset() clears the guard and releases the queue, but the abandoned run() continues and enters this runBlocked call once loading completes. If a newer execution is already blocked, the stale call overwrites its saved snapshots and generation; when either call finishes, the process can be left permanently patched, and a stale function that hangs leaves the same result. Check abandoned before invoking the function/entering a new guard scope.

Useful? React with 👍 / 👎.

savedExecFileSync = child_process.execFileSync;
savedFork = child_process.fork;

net.Socket.prototype.connect = throwNetworkBlocked as typeof net.Socket.prototype.connect;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block server-side socket creation too

When dependency code calls net.createServer().listen(...) inside a backend function, it never invokes Socket.prototype.connect, so this patch does not reject it (verified with the same prototype replacement on Node 20). The dependency can therefore open a real listening socket and leave it alive after the function returns, despite production running without network permission; patch the server/listen entry points as well as outbound connects.

Useful? React with 👍 / 👎.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

// (both Vite's own transform pipeline, no network) run unguarded.
// $.Actions calls made from inside fn are exempted via `runAllowed`
// in `makeActionsProxy`. See network-guard.ts.
const result = await runBlocked(() => fn(...args));
Comment on lines +263 to +288
const abandonedAction = runAllowed(
() =>
new Promise<void>((resolve) => {
resolveAbandonedAction = resolve;
}),
);

// Simulates the timeout handler abandoning this execution while
// the $.Actions call above is still in flight.
forceReset();

// A newer execution starts, and its own legitimate $.Actions call
// must be correctly allowed through and re-blocked afterward.
const result = await runBlocked(async () => {
await runAllowed(async () => 'newer allowed call');
await expect(fetch('https://example.com')).rejects.toThrow(
/Network access is not allowed/,
);
return 'newer execution result';
});
expect(result).toBe('newer execution result');

// The abandoned call's runAllowed finally now fires, well after
// being superseded — it must not touch allowDepth.
resolveAbandonedAction?.();
await abandonedAction;
runAllowed previously restored real net/fetch/child_process globally for
its duration (a shared allowDepth counter), so a sibling call made
concurrently by customer code — e.g.
Promise.all([$.Actions.foo(...), fetch(url)]) — got a free pass for the
entire window the legitimate $.Actions call was in flight, defeating the
guard for a common concurrent-call pattern.
Replaces the global toggle with an AsyncLocalStorage-scoped exemption: the
patched functions now stay installed for the whole runBlocked duration and
each checks whether its own specific call is running inside the async chain
runAllowed started, rather than whether any runAllowed call is active
anywhere. A sibling call outside that chain sees no store at all and stays
blocked, regardless of what else is concurrently allowed.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tyffical