Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c34beb6
Define the background child run lifecycle contract
Aaronontheweb Oct 9, 2026
cd1e05d
Retain parent detector evidence for child continuations
Aaronontheweb Oct 9, 2026
b95ea58
Capture legacy storage bindings for child-run verification
Aaronontheweb Oct 9, 2026
1b86769
Remove the provider capacity gate from the child run contract
Aaronontheweb Oct 9, 2026
6d544bc
Implement durable background child runs and parent controls
Aaronontheweb Oct 9, 2026
bd1391b
Record independently verified background lifecycle tasks
Aaronontheweb Oct 9, 2026
3c7a27c
Align background child contracts with durable owner behavior
Aaronontheweb Oct 9, 2026
821b6d9
Sync background child run specifications
Aaronontheweb Oct 9, 2026
644e2cb
Use portable paths in the background context fixture
Aaronontheweb Oct 9, 2026
d38480b
Wait for provider cancellation in the child lifetime test
Aaronontheweb Oct 10, 2026
219a478
test(subagents): preserve child death and parent status handling
Aaronontheweb Oct 10, 2026
b37aa7e
test: await child model entry before the count assertion
Aaronontheweb Oct 10, 2026
6bcd13f
Rebalance the focused mutation gate groups
Aaronontheweb Oct 10, 2026
1c71e96
Clarify child finalization during daemon drain
Aaronontheweb Oct 10, 2026
b052736
Record relay and parent response verification
Aaronontheweb Oct 10, 2026
73a415f
Record hosted boundary and mutation proof
Aaronontheweb Oct 10, 2026
d00a691
Record targeted background instruction eval evidence
Aaronontheweb Oct 10, 2026
c7a73d3
Reset recurrence evidence at durable child result adoption
Aaronontheweb Oct 10, 2026
45a9f83
Align child prompt specifications with scoped instructions
Aaronontheweb Oct 10, 2026
00064d4
Preserve canonical child prompt scenarios across the stack
Aaronontheweb Oct 10, 2026
4ff4a44
fix(sessions): adapt child report to the renamed session-workspace sp…
Aaronontheweb Oct 11, 2026
2494a01
fix(subagents): deliver background child death as a BackgroundChildTe…
Aaronontheweb Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/pr_validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,9 @@ jobs:
matrix:
include:
- group: shell-analysis
gates: shell-command-analysis tool-authorizer-order mcp-artifact-admission tool-task-adoption
gates: shell-command-analysis tool-authorizer-order mcp-artifact-admission
- group: shell-assignment-and-paths
gates: shell-assignment shell-config-read path-access
gates: shell-assignment shell-config-read path-access tool-task-adoption
- group: verb-chain-and-reminder
gates: exact-verb-chain reminder-execution literal-twin stream-commit-gate
- group: approval-and-authorization
Expand Down Expand Up @@ -326,6 +326,7 @@ jobs:
bash -n evals/run-background-evals.sh
bash -n evals/cycle_evals.sh
python3 -m unittest discover -s evals -p test_background_evals.py -v
python3 -m unittest discover -s evals -p test_child_run_evals.py -v
python3 -m unittest discover -s evals -p test_cycle_evals.py -v
python3 -m unittest discover -s evals -p test_cli_detector_evals.py -v
python3 -m unittest discover -s evals -p test_spill_steer_evals.py -v
Expand Down
40 changes: 35 additions & 5 deletions TOOLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,19 @@
All focused gates run in one job definition, `mutation-gates` in `pr_validation.yml`, on each pull request and merge group. They do not run on `dev` pushes.
The job has four Linux matrix groups that run in parallel with the normal test matrix.
The baseline run at `2e6bc4f01` took 12m24s, 12m29s, 18m56s, and 20m14s across the four groups.
The new task-adoption gate uses the lightest group, `shell-analysis`. The job timeout remains 25 minutes.
The task-adoption gate uses `shell-assignment-and-paths`. The job timeout remains 25 minutes.
The prior `shell-analysis` job reached its deadline at `fa6aad73` after 342 reported mutants were killed.
The final child-control target produced no report.
The same mutation inputs passed the earlier `832ebff8` job in 20m25s.

The destination group took 13m13s to 16m49s across four retained hosted runs.
It took 14m53s in the `fa6aad73` run. The complete gate moves without target or test changes.
The [combined `153d9223` hosted run](https://github.com/netclaw-dev/netclaw/actions/runs/38045919693) verifies the new distribution.
Its four groups pass in 16m51s, 23m14s, 15m11s, and 21m01s.
Each group stays below the 25-minute limit. All six expected task-adoption and child-authority targets have `Killed` status.
The `8aba7cb1` assignment group passes in 24m52s, with eight seconds of margin.
See [the cancelled job](https://github.com/netclaw-dev/netclaw/actions/runs/38020934971/job/114121548608)
and [the prior successful job](https://github.com/netclaw-dev/netclaw/actions/runs/38014833252/job/114102687127).
Each group runs its gates in sequence after one checkout and tool restore, and it reports every failed gate.
To add a gate, add its script name (`scripts/run-<name>-mutations.sh`) to the lightest group. Do not add a new job.

Expand All @@ -43,31 +55,48 @@ coverage. They do not replace positive and negative behavior tests.
### Task Adoption Gate

Run `./scripts/run-tool-task-adoption-mutations.sh` from the repository root.
The gate uses the existing xUnit 2 harness and selects 13 tests through a temporary Stryker JSON configuration.
The gate uses the existing xUnit 2 harness and selects 14 tests through a temporary Stryker JSON configuration.
It runs the five prior targets with their 13 existing controls.
It runs the child-authority target separately with its one owner test.
An unrelated broken parent-input guard can prevent that owner test from starting a child.
Separate filters keep each mutation with the tests that reject it.
The script deletes that configuration on exit.

The gate targets five exact mutations:
The gate targets six exact mutations:

- Replace full task-authority equality.
- Remove the ordered-prefix rejection.
- Replace `Take` with `Skip` in the ordered prefix.
- Replace actual-outcome equality in the recurrence tracker.
- Change stale-callback rejection to acceptance.
- Remove the original-requester rejection for a child control call.

The callback target changes the final `false` result in `OwnsToolExecution` to `true`.
Its exact mutator name is `Boolean mutation`.
The mailbox test replays a captured prior result during a new dispatch with the same provider call ID.
The test verifies the current authority attempt, result pair, cancellation token, and confirmed disk effects.

The child control target removes the `!` from `TurnContext.HasSameAuthority(original, current)`.
Its exact mutator name is `LogicalNotExpression to un-LogicalNotExpression mutation`.
The test uses a real owner with a held child and the actual status tool.
It checks original-owner access, opaque foreign denial, canonical log paths, and normal file-policy rejection.

Every expected mutant must exist and have the `Killed` status.
The gate rejects expected compile errors, timeouts, survivors, and absent mutants.
It also rejects extra executable mutants.
Stryker can emit a non-target `Count`-to-`Sum` mutation that cannot compile for the typed input list.
The exact-name check excludes that invalid mutation; it does not exclude an expected target error.

The current independent run killed all five targets in 187.564 seconds.
The earlier independent run killed all five prior targets in 187.564 seconds.
The prior four-target run took 3m05s.
CI runs this gate in `shell-analysis` and uploads `artifacts/stryker/tool-task-adoption` with the group reports.
The first combined six-target run took 232.52 seconds and failed.
Four targets were killed. The two ordered-prefix targets timed out.
The gate correctly rejected those timeouts.
The separate-filter run killed all six targets in 339.85 seconds.
Its reports contain no tested timeout or survivor.
The separate build adds 152.286 seconds over the earlier five-target run.
CI runs this gate in `shell-assignment-and-paths` and uploads `artifacts/stryker/tool-task-adoption` with the group reports.
The `adoption` and `child-control` directories each retain their own mutation report.
The local duration does not establish the combined CI duration.

### Current Targets
Expand All @@ -76,6 +105,7 @@ The local duration does not establish the combined CI duration.
|--------|-----------------|------------------|---------|
| `SessionState` task adoption and `TurnStateTracker` actual outcomes | Full authority context and the ordered input prefix control adoption; changed actual outcomes do not trigger exact recurrence | 4 killed | `./scripts/run-tool-task-adoption-mutations.sh` |
| `LlmSessionActor.OwnsToolExecution` | A captured prior reply cannot affect the current dispatch, even when the provider call ID repeats | 1 killed | `./scripts/run-tool-task-adoption-mutations.sh` |
| `LlmSessionActor.ChildRuns` original-requester check | Child controls retain the original authority; foreign callers receive no state or paths | 1 killed | `./scripts/run-tool-task-adoption-mutations.sh` |
| `PathAccessPolicy.AddSessionRoots` and `PathAccessPolicy.IsReadableByAudience` | Only a Personal context receives shared session roots; a reviewed phrase uses the read authority of the audience, attended or not (D2), only for a fully qualified host path of the shell's own style that is not protected | 4 killed | `./scripts/run-path-access-mutations.sh` |
| `ToolAccessPolicy.AdmitMcpAudience` | Server and tool audience grants precede approval | 2 killed | `./scripts/run-tool-authorization-mutations.sh` |
| `ToolAccessPolicy.ScreenHardDeny` | A shell hard denial precedes approval | 1 killed | `./scripts/run-tool-authorization-mutations.sh` |
Expand Down
55 changes: 45 additions & 10 deletions docs/architecture/tool-authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,15 @@ Durable approval answers retain their canonical pending context and existing req
A recovered durable approval can use the existing redrive path after the old source ends.
That redrive preserves the original authority and authorization attempt and creates a new local dispatch source.

Foreground child spawn requests carry the original parent token before child creation.
The parent receives foreground child activity through a token-bound local mailbox envelope before public emission.
Routed foreground skills use the same existing source for their spawn, activity, result, and failure callbacks.
These checks change no public output format or transport protocol.
Child start requests carry the original parent token until the owner commits acceptance.
The accepted run then owns its lifetime and immutable original authority.
The pipeline validates the returned start key, authority, and source operation against the stamped request.
The slash adapter applies the same checks to its original activation.
The spawner rejects malformed identifiers, digests, profiles, and state before it emits success.
A duplicate returns the recorded run identity and its current state.
The owner binds child results to the accepted run, actor identity, and live dispatch gate.
The parent start call does not own later child approval prompts or terminal delivery.
Shell background job deliveries retain their separate durable origin and input-admission contract.
Future accepted background children need their separate run owner after parent foreground settlement.
See [TA-1](../../openspec/specs/tool-authorization/spec.md#requirement-ta-1-trust-context-is-explicit-and-fails-loud).

## Background job result authority
Expand Down Expand Up @@ -721,16 +724,16 @@ Leaks today:
`false`) need no consent.
- Grant match for one shell candidate can run up to 5 times in one gate run.

### 3.6 Consent delivery
### 3.6 Approval prompt delivery

| Item | Current state |
| --- | --- |
| Question | How does the operator see a request and answer it, now or after a restart? |
| Classes | Approval part of [`LlmSessionActor`](../../src/Netclaw.Actors/Sessions/LlmSessionActor.cs), [`ToolApprovalState`](../../src/Netclaw.Actors/Sessions/ToolApprovalState.cs), [`IApprovalChannel`](../../src/Netclaw.Actors/Sessions/IApprovalChannel.cs), [`ParentSessionApprovalBridge`](../../src/Netclaw.Actors/Sessions/ParentSessionApprovalBridge.cs), [`IParentApprovalBridge`](../../src/Netclaw.Tools.Abstractions/IParentApprovalBridge.cs), the approval loop in [`SubAgentActor`](../../src/Netclaw.Actors/SubAgents/SubAgentActor.cs), [`ApprovalResponseFlow`](../../src/Netclaw.Channels/ApprovalResponseFlow.cs), [`PendingApprovalLookup`](../../src/Netclaw.Channels/PendingApprovalLookup.cs), [`ApprovalButtonValueCodec`](../../src/Netclaw.Actors/Protocol/ApprovalButtonValueCodec.cs), [`ApprovalOptionKeys`](../../src/Netclaw.Actors/Protocol/ApprovalOptionKeys.cs), the Slack, Discord, and Mattermost prompt builders |
| Published contract | Output `ToolInteractionRequest` and input `ToolInteractionResponse` ([`SessionProtocol.Outputs.cs`](../../src/Netclaw.Actors/Sessions/SessionProtocol.Outputs.cs)). Journal events `ToolApprovalRequested` and `ToolApprovalResolved`. Option key strings such as `approve_once`. |
| Must not know | Policy rules. It renders the options that Consent offers. |
| Data | Durable session journal. Actor-local state for an unanswered request. A subagent request is live-only. |
| Rules | [TA-10](../../openspec/specs/tool-authorization/spec.md#requirement-ta-10-consent-prompts-offer-only-safe-options), [TA-11](../../openspec/specs/tool-authorization/spec.md#requirement-ta-11-an-unanswered-consent-request-survives-restart), [TA-12](../../openspec/specs/tool-authorization/spec.md#requirement-ta-12-subagent-consent-goes-through-the-parent-session) |
| Data | Durable session journal for prompt lifecycle and original requester facts. A child execution waiter remains run-local. |
| Rules | [TA-10](../../openspec/specs/tool-authorization/spec.md#requirement-ta-10-consent-prompts-offer-only-safe-options), [TA-11](../../openspec/specs/tool-authorization/spec.md#requirement-ta-11-an-unanswered-consent-request-survives-restart), [TA-12](../../openspec/specs/tool-authorization/spec.md#requirement-ta-12-subagent-approval-prompts-go-through-the-parent-session) |

Leaks today:

Expand Down Expand Up @@ -839,15 +842,47 @@ Facts behind the diagram (current code):
through one prompt, `ParentSessionApprovalBridge`, which waits with the
session's approval timeout. The daemon sets that timeout to infinite. The
request waits until the operator answers, the run is cancelled, or a new user
message abandons the parked batch.
message abandons the parked parent batch. An ordinary parent message does not abandon a live child prompt.
- A subagent request goes to the parent through `ParentSessionApprovalBridge`.
It is live-only. After a restart, Netclaw rejects the old prompt as expired.
Its lifecycle facts retain the original requester, exact call, authorization attempt, and accepted run.
`LlmSessionActor` stores its request and resolution in the durable `BackgroundChildRun.Approvals` ledger.
The ordinary parent-turn approval map does not own that child prompt.
Its execution waiter remains run-local. After restart, Netclaw rejects the old child prompt as expired.
A subagent without a parent bridge cannot ask. Its whole run fails with
`ToolExecutionFailed`; it does not return a per-tool denial.
- Child cancellation or loss closes that run's prompt. A late answer creates no grant or retry.
- The owner checks run liveness across any await before a broad grant or prompted retry can commit.
- The owner claims the exact live approval wait before it persists a reusable grant.
- A completed start call does not own the accepted child's prompt lifetime.
- A channel that cannot post a prompt sends `Deny` for that call
(`ChannelOutputEngine`, `SlackThreadBindingActor`). This is the only
automatic denial of a consent request.

The parent loads `check_agent_run` explicitly through `load_tool` for child status or cancellation.
Normal tool policy, the owner session, and the original eligible requester define access.
Children cannot discover, load, or dispatch that control. A foreign caller receives no target details.
The owner uses the original authority record. It does not copy requester or path authority into a second policy structure.
Cancellation creates no authorization grant. A report path still requires ordinary file access policy.

The owner commits cancellation admission before it reports `Cancelling`.
It closes the live dispatch gate, requests token cancellation, and waits for any synchronous dispatch prefix to exit.
It commits `DispatchClosed` before framework-only report finalization.
It commits any unresolved child prompt disposition before the terminal receipt.

Token cancellation can resolve a prompt earlier. Durable closure does not require prompt resolution to commit first.
See [the operation-health specification](../spec/SPEC-016-tool-liveness-and-stall-detection.md#cancellation-and-partial-evidence) for the full cancellation sequence.

After authorization, the control adapter obtains `log_path` and `artifact_directory` from the existing canonical child storage binding.
These call-local output paths are data. They create no grant or separate durable authority.
The parent uses normal file tools for log access. A denied control reveals neither path.

Positive example: the original parent requester answers its live child's exact approval prompt after the start call completes.
Negative example: a later speaker cannot answer that prompt with replacement requester authority.
Positive control example: the eligible parent cancels its accepted run and receives separate closure evidence.
Negative control example: a child that knows a sibling run ID cannot inspect or cancel it.
Positive log example: the original requester uses the returned live log path with `file_read` under normal file policy.
Negative log example: a returned path does not bypass a denied file read.

A non-shell call follows the same path, with two differences. `ToolAuthorizer`
applies the rules for other tools and one `StoredGrantCheck` in place of the
shell coverage. The launch step does not exist.
Expand Down
15 changes: 15 additions & 0 deletions docs/prd/PRD-001-netclaw-mvp.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,21 @@ the current daemon instance running. Valid changes SHALL trigger coordinated
daemon restart: close new ingress, drain active sessions, restart, relaunch the
sessions that were active, and resume from the last durable checkpoint.

### FR-017 Background Child Delegation

Netclaw shall return a durable accepted run identifier for explicit and routed child tasks before child completion.
The parent shall accept other inputs and retain status and cancellation controls for its accepted children.
Child results shall return later as attributed tool-origin content under the original requester authority.
Child approval prompts shall remain valid for the live run after its start call ends.
Cancellation shall stop new task dispatch and preserve recorded partial evidence through a five-second framework-only grace period.

Restart shall report interrupted children as lost, retain terminal delivery, and expire child approval prompts without automatic child relaunch.
The release shall prove a parent model response before a held child request completes on the selected real backend.
Actor acknowledgements alone do not prove model responsiveness. The inference backend owns capacity and request queues.
Private agent messages, peer discovery, questions, and live steering remain outside this delivery.

See `openspec/changes/background-child-runs/` for the second PR's review contract.

## Operational Requirements

- Daemon deploys as a user-level systemd service on `pi1` (no sudo required)
Expand Down
Loading
Loading