Skip to content

feat(core): add sandbox target configuration for observed-IO opt-out - #36853

Open
AgentEnder wants to merge 13 commits into
masterfrom
claude/nx-snapshot-opt-out-b6dgt1
Open

feat(core): add sandbox target configuration for observed-IO opt-out#36853
AgentEnder wants to merge 13 commits into
masterfrom
claude/nx-snapshot-opt-out-b6dgt1

Conversation

@AgentEnder

Copy link
Copy Markdown
Member

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 sandbox configuration in project/target config (and targetDefaults):

{
  "sandbox": {
    "enabled": false,
    "ignoredReads": ["tmp/cache/**"],
    "ignoredWrites": ["scratch/**"]
  }
}
  • TargetConfiguration gains sandbox (typed and in the JSON schemas), defined as the native TaskSandboxConfiguration so Task['sandbox'] and TargetConfiguration['sandbox'] are the same type.
  • createTaskGraph copies the target's sandbox config onto each Task instance (like cache/parallelism), so consumers read it off the task itself instead of resolving it through the project graph.
  • The task orchestrator registers each task's config with the TaskIOService just-in-time in processTask (both run paths await it before spawning). notifyPidUpdate drops PID updates for tasks with enabled: false, so no IO tracing signal — and therefore no sandbox report — is produced for opted-out tasks.

ignoredReads/ignoredWrites are 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/healing flag from that ticket is deliberately not included here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YVrVQUAtU1aSQPuUxLkJsn


Generated by Claude Code

@AgentEnder
AgentEnder requested a review from a team as a code owner August 30, 2026 06:22
@netlify

netlify Bot commented Aug 30, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 9f1297c
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a984fe0cab46f0008fe03e3
😎 Deploy Preview https://deploy-preview-36853--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Aug 30, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 9f1297c
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a984fe123ef7f0008c4e8cc
😎 Deploy Preview https://deploy-preview-36853--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 9f1297c

Command Status Duration Result
nx affected --targets=lint,oxlint,test,build,e2... ✅ Succeeded 54m 28s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 4s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 58s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 18s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 17:32:54 UTC

@AgentEnder
AgentEnder force-pushed the claude/nx-snapshot-opt-out-b6dgt1 branch from 77aa600 to b2f1714 Compare September 1, 2026 02:12

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Nx Cloud View detailed reasoning in Nx Cloud ↗

🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.


🎓 Learn more about Self-Healing CI on nx.dev

claude and others added 13 commits September 2, 2026 12:30
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
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
AgentEnder force-pushed the claude/nx-snapshot-opt-out-b6dgt1 branch from 7e1bbdd to 9f1297c Compare September 2, 2026 16:33
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.
@AgentEnder AgentEnder added scope: core core nx functionality type: feature labels Sep 2, 2026
@AgentEnder AgentEnder self-assigned this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: core core nx functionality type: feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants