feat(core): add sandbox target configuration for observed-IO opt-out - #36853
Open
AgentEnder wants to merge 13 commits into
Open
feat(core): add sandbox target configuration for observed-IO opt-out#36853AgentEnder wants to merge 13 commits into
AgentEnder wants to merge 13 commits into
Conversation
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
View your CI Pipeline Execution ↗ for commit 9f1297c
☁️ Nx Cloud last updated this comment at |
AgentEnder
force-pushed
the
claude/nx-snapshot-opt-out-b6dgt1
branch
from
September 1, 2026 02:12
77aa600 to
b2f1714
Compare
Contributor
There was a problem hiding this comment.
Nx Cloud has identified a flaky task in your failed CI:
🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
🎓 Learn more about Self-Healing CI on nx.dev
Adds a `sandbox` property to target configuration with `enabled`, `ignoredReads`, and `ignoredWrites`. The task orchestrator registers each task's sandbox configuration with the TaskIOService, which suppresses PID reporting for tasks whose target sets `sandbox.enabled: false`, so no IO tracing signal (and therefore no sandbox report) is produced for them. checkFilesAreInputs/checkFilesAreOutputs treat paths matching the ignored globs as reconciled so sandbox-violation validation skips them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVrVQUAtU1aSQPuUxLkJsn
Review feedback: the target's sandbox configuration is copied onto each Task in createTaskGraph (Task['sandbox'] equals TargetConfiguration['sandbox']), so consumers read it off the task instead of resolving it through the project graph. The orchestrator registers it with the TaskIOService just-in-time in processTask, which both run paths await before spawning, replacing the upfront iteration over the task graph. The check-task-files sandbox matching is reverted: those functions answer whether a file is an input/output, and with ignored accesses excluded from reports at record time there is nothing left for them to reconcile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVrVQUAtU1aSQPuUxLkJsn
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVrVQUAtU1aSQPuUxLkJsn
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVrVQUAtU1aSQPuUxLkJsn
TargetSandboxConfiguration aliased the resolved napi shape, which has no '...' member, so authoring a sandbox spread in TypeScript failed to compile on a form the schema, the merge implementation and the PR's own test all accept. The test needed an `as any` to construct it. Adds Spreadable<T> and applies it to TargetConfiguration['sandbox']. Task['sandbox'] keeps the resolved type, since merging resolves the token away before a task is built.
sandbox rode the generic top-level merge, which assigns the object without recursing into its values. mergeObjectWithSpread does a raw per-key copy for the same reason, so a nested `'...'` was unresolved in BOTH the wholesale and the object-spread branches: an inherited ignoredReads was silently dropped and the literal '...' reached the task graph as a glob matching nothing. Nothing rejects it, since the schema types the items as plain strings. Gives sandbox its own merge, next to options and configurations. The object level keeps replace-unless-'...' semantics, which the schema documents and the existing specs pin; only the values now merge per key, so a nested spread expands against the inherited value. The merge copies before resolving. Without an object-level spread getMergeValueResult returns the incoming object itself, so writing through it would edit the caller's target in place and corrupt later merge layers.
The JSON schema is editor-only and nothing validated sandbox at runtime, so a malformed value reached the task graph verbatim. Downstream consumers are strict: a wrong-typed ignoredReads is rejected by the cloud runner's request validation, failing the run rather than the target. Validates the effective value during normalization, where the project, target and source file are all still in hand, and throws naming all three. This runs after every createNodes result is merged, so plugin-inferred targets are covered too. Checks shape only. A syntactically invalid glob is a well-formed string and is deliberately out of scope.
sandbox had no section in the project configuration reference, while every sibling target property does. The violation-fixing guide routed readers to .nx/workflows/sandboxing-config.yaml with no mention that a per-target property exists, so anyone following the official remediation path was steered to the workspace-wide mechanism. Adds the reference section, cross-links both directions, and states that the two mechanisms combine rather than override. Also corrects the spread token level table, which was presented as exhaustive and did not list sandbox.
isTaskSandboxDisabled had no production caller. Suppression happens inside notifyPidUpdate, and the only reads were two assertions in its own spec that sat alongside assertions on the observable behaviour they duplicated. Confirmed unused by Nx Cloud before removing, rather than inferred from nx alone: it consumes TaskIOService through getTaskIOService and the three subscribeTo* methods only, and reads the opt-out off task.sandbox rather than through the service.
Two regressions from the previous commits, both found by re-review. The merge gated on `target.sandbox` being truthy while 'sandbox' was also in skipForOwnMerge, so `"sandbox": false` was dropped from the merged target entirely and validation never saw the exact input it exists to reject. Before these commits that value survived to the napi boundary and threw there, so the fix had turned a loud error into a silent one: the opt-out quietly did nothing. Gating on key presence restores it. An array sandbox was also object-ified into a valid empty config by the copy, so the guard now excludes arrays. The per-key pass also keyed off Array.isArray alone and overwrote the object-level merge's decision even where that decision was base-wins, so the authored position of a glob array had no effect while `enabled` in the same object still honoured it. It now skips keys authored before the spread that the base already provides, matching how the target level computes the same set. The validation specs built root maps the pipeline cannot produce, which is why neither escaped there. They now also run through mergeCreateNodesResults.
isSupportedGlob rejects any pattern whose first segment contains '*', plus '?', '!', '[', ']' and extglobs, so '**/generated/**' and '**/*.log' are honoured while a task runs and then dropped from its snapshot. Those are the first forms a user reaches for, and neither the schema description nor the reference page said anything about it. Documents the constraint in both schemas and the reference page. The silent drop itself is a cloud-side fix; this is about the promise the schema makes.
The previous commit updated the two JSON schemas and missed the Rust doc comment on TaskSandboxConfiguration, which is what a plugin author sees on hover writing createNodes in TypeScript. That audience is the one most likely to build a pattern programmatically and never open a schema. index.d.ts is generated from the Rust and was regenerated. The reference page also said an unsupported pattern is 'rejected'. Nothing performs that action: nx validates shape only and never inspects glob syntax, so there is no error at authoring time, at graph construction, or at run. A reader who believes 'rejected' waits for a failure that never comes. It now matches the schemas' 'not supported' and says plainly that the pattern has no effect.
AgentEnder
force-pushed
the
claude/nx-snapshot-opt-out-b6dgt1
branch
from
September 2, 2026 16:33
7e1bbdd to
9f1297c
Compare
FrozenPandaz
added a commit
that referenced
this pull request
Sep 2, 2026
…tion Drops this branch's own `ioSnapshots: false` target property in favour of the `sandbox` configuration from #36853, which covers the same concept with a fuller shape (`enabled`, `ignoredReads`, `ignoredWrites`), spread token support, pinned merge semantics and glob validation. The eligibility walk now reads `task.sandbox.enabled` off the Task the planner already has in hand, so the project-graph lookup and the `Target.io_snapshots` field it existed to serve are both gone. `nx show target` and the run summary report the opt-out under the new key; the docs section now points at the Sandbox reference rather than describing a second mechanism.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
There is no way to opt a target out of observed-IO sandboxing or to declare reads/writes that should be excluded from sandboxing reports. Every task the runner tracks gets its PIDs reported through the
TaskIOService, so a sandbox report is produced regardless of whether the task can safely use one, and task instances carry no sandbox information for downstream consumers (the Nx Cloud runner and cloud api).Expected Behavior
Targets can declare a
sandboxconfiguration in project/target config (andtargetDefaults):{ "sandbox": { "enabled": false, "ignoredReads": ["tmp/cache/**"], "ignoredWrites": ["scratch/**"] } }TargetConfigurationgainssandbox(typed and in the JSON schemas), defined as the nativeTaskSandboxConfigurationsoTask['sandbox']andTargetConfiguration['sandbox']are the same type.createTaskGraphcopies the target'ssandboxconfig onto eachTaskinstance (likecache/parallelism), so consumers read it off the task itself instead of resolving it through the project graph.TaskIOServicejust-in-time inprocessTask(both run paths await it before spawning).notifyPidUpdatedrops PID updates for tasks withenabled: false, so no IO tracing signal — and therefore no sandbox report — is produced for opted-out tasks.ignoredReads/ignoredWritesare consumed by the Nx Cloud runner and io-trace daemon (nrwl/ocean), which exclude matching accesses from sandbox reports at record time; this PR only carries them on the task instances.Related Issue(s)
Part of the "Allow Nx plugins to declare tasks as non-snapshot-able" work (Linear). The
backpropagate/healingflag from that ticket is deliberately not included here.🤖 Generated with Claude Code
https://claude.ai/code/session_01YVrVQUAtU1aSQPuUxLkJsn
Generated by Claude Code