Skip to content

feat(agent): stop a sandboxed session the daemon has lost track of - #38

Draft
LutzLange wants to merge 4 commits into
containment/pr1-launcherfrom
containment/pr2a-kill
Draft

LutzLange wants to merge 4 commits into
containment/pr1-launcherfrom
containment/pr2a-kill

Conversation

@LutzLange

Copy link
Copy Markdown
Owner

feat(agent): stop a sandboxed session the daemon has lost track of

Builds on PR 1 and lands after it. When the daemon starts the MXC executor (wxc-exec.exe) but then
cannot record the launch pid, or loses track of the executor during the start-up watch or later in
the poller, it now stops the executor instead of dropping the handle. Stopping the executor ends
the box: in the lab every process with the box's AppContainer identity was gone within seconds.
Before this change such a box kept running, unrecorded and with a live credential file until the
next sweep.

What changes

  • LaunchChild::kill (Ok for an executor that has already exited); ProcessChild calls
    std::process::Child::kill (TerminateProcess on Windows).
  • One stop_executor helper at the three places that lose track of the executor. A kill error is
    logged as a warning and never replaces the original error or keeps the launch from being marked
    exited.
  • Behaviour change: a box whose launch fails after the executor started, or which the poller can
    no longer watch, is now stopped.
  • README: the 500 list names the watch failures; a sentence says a running sandboxed session the
    daemon can no longer watch is stopped.
  • No config, schema, core or provider change. Boxes still survive a daemon restart (the kill is
    explicit, not on drop).

Process evidence

(The spec, review and lab paths below live in the engineering workspace used to build this series,
not in this repository.)

  • Spec: specs/PR-2.md (v5 frozen after four Opus spec-review rounds; the scope was cut from the
    original policy/engine PR to this one change, the rest is listed under "Later").
  • Tests first: 6 tests by a separate agent (308c237) plus one Windows-only unit test that stops a
    real child process; strengthened and renamed during review, every change ledgered
    (specs/PR-2-test-changes.md). 6 on Linux, 7 on Windows.
  • Gate (lab/evidence/pr2a/gate-v2.log; v3 is a doc comment, fmt + clippy re-run): fmt, clippy
    -D warnings, unit tests, schema, frontend build + lint, Storybook, test guard: PASSED.
  • Reviews (reviews/PR-2a-*.md): conformance (Sonnet), adversarial (Opus, mutation probes),
    maintainer fit (Opus) on v1; delta review v2 (Opus) pass with minors; v3 wording-only.
    Fixed on the way: the missing README sentence, a test that did not check the launch directory was
    removed, a poller test that could not detect a kill after the directory removal, two test errors
    that could not be told apart.
  • Lab (lab/evidence/pr2a/first-run-92e99b1.md, Windows 11 VM, build = v3 + fix(agent): store Windows secrets as encrypted files keyed from Credential Manager agentdesktop-dev/agentdesktop#171): K0–K2 pass.
    • K1: terminating wxc-exec (the pid the daemon recorded) ended all nine descendants (Claude
      Code, cmd, conhost, all with the box's AppContainer token) within 10 s; the launch shows
      exited, exitCode -1, its directory removed.
    • K2: a normal launch still works: AppContainer token, a completed turn, gateway
      POST /v1/messages 200, normal exit exitCode 0.
    • The three daemon-initiated stop paths cannot be provoked in the lab without fault injection;
      unit tests cover them.

Known limits and follow-ups

  • No bounded wait after the kill: TerminateProcess is asynchronous, so the launch directory removal
    can race the teardown and leave the credential file until the next startup sweep.
  • No test pins the kill before the launch is marked exited (the code does it in that order; tests
    pin it before the directory removal).
  • cargo clippy --all-targets --target x86_64-pc-windows-gnu fails on an existing clone_on_copy
    in bin/agentdesktop-service.rs:83, a file this PR does not touch; separate fix.
  • Please squash-merge (the first commit carries the tests against todo!() stubs).

🤖 Generated with Claude Code

LutzLange and others added 4 commits October 18, 2026 12:35
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The daemon now ends the executor (and with it the box) when it cannot
record the launch pid after starting it, or when it loses track of the
executor during the start-up watch or in the poller. ProcessChild::kill
calls std::process::Child::kill (TerminateProcess on Windows) and is Ok
for an executor that has already exited. A kill error is logged as a
warning and never replaces the original error or keeps the launch from
being marked exited. The README's 500 list names the watch failure.

Behaviour change: a box whose launch fails after the executor started is
now ended; before, the daemon dropped the handle and left it running.
Terminating wxc-exec ends the box's processes (lab-observed).

Tests: 6 on Linux, 7 on Windows (the seventh is a Windows-only unit test
that stops a real child process). All are baseline tests; one baseline
test's early-exit case now expects the documented 500 (ledgered). No
writer tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The daemon now stops the executor (and with it the box) when it cannot
record the launch pid after starting it, or when it can no longer watch
the executor during the start-up watch or in the poller; before, it
dropped the handle and left the process running. A small stop_executor
helper does this at all three places. ProcessChild::kill calls
std::process::Child::kill (TerminateProcess on Windows) and is Ok for an
executor that has already exited. A kill error is logged as a warning
and never replaces the original error or keeps the launch from being
marked exited. The poller's log line and doc comment now say the
executor is stopped.

README: the 500 list names the watch failures, a new sentence says that
a running sandboxed session the daemon can no longer watch is stopped,
and "within three seconds" now clearly refers to the executor.
Terminating wxc-exec ends the box's processes (lab-observed).

Behaviour change: a box whose launch fails after the executor started,
or which the poller can no longer watch, is now stopped.

Tests: 6 on Linux, 7 on Windows (one Windows-only unit test that stops a
real child process). They check that the executor is stopped on each of
the three paths and not on spawn failure, early exit or normal exit,
that the poller stops it before the launch directory is removed, that
the launch directory is gone after a watch failure, and that a kill
error does not replace the original error.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Documentation only; no behaviour or test change (6 tests on Linux, 7 on
Windows).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI 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.

🟢 Approval recommended

The focused termination changes preserve error handling and cleanup behavior, with regression coverage and no identified blocking issues.

No open issues 🎉

What changed in this PR

Stops sandbox executors when the daemon cannot record or monitor them, preventing sessions from continuing untracked.

Changes:

  • Adds explicit termination to all three tracking-failure paths while preserving original errors.
  • Adds regression tests and a Windows process-termination test.
  • Documents the behavior without changing daemon-restart survival.
File Description
README.md Documents termination on tracking failures.
crates/​agent/​src/​launch/​writer_tests.rs Updates the test child with kill support.
crates/​agent/​src/​launch/​tests.rs Tests termination, error preservation, and cleanup.
crates/​agent/​src/​launch/​mod.rs Stops executors before tracking-failure cleanup.
crates/​agent/​src/​launch/​host.rs Implements process termination and tests it on Windows.

Review effort: Balanced

Sign up for free to 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