Skip to content

Supervise confined agents and validate policy and export boundaries - #663

Open
imran-siddique wants to merge 2 commits into
mainfrom
agent/confinement-lifecycle
Open

imran-siddique wants to merge 2 commits into
mainfrom
agent/confinement-lifecycle

Conversation

@imran-siddique

@imran-siddique imran-siddique commented Sep 18, 2026

Copy link
Copy Markdown
Member

What

Add a separate lease watchdog that stops a plaintext-bearing agent container when its bridge dies or stops sending heartbeats. Add an operator restriction gate that serializes live revocation with tool dispatch without resetting session classification, and remove private exception text from audit/OTel failure logs.

Why

Continues #659 after #662 was approved and merged. The earlier reference relied on the bridge's cleanup path and did not exercise live operator restrictions or actual telemetry exports.

Security impact

The watchdog assumes a surviving trusted host, watcher and responsive Docker daemon. Tests kill/pause the real bridge after observed tool delivery, kill the watcher, and remove supervision as a causal control. The policy gate can only restrict or restore originally approved tools; update acknowledgment follows already-admitted calls, whose side effects cannot be undone. Invalid/stale revisions close admission until a newer valid update.

Audit sink representations and exporter exception tracebacks can contain private payloads. Two tests reproduced those leaks before removing the unsafe diagnostics. Real gateway probes inspect SQLite audit persistence, chain entries, SDK-exported spans and logs for successful responses, upstream errors, malformed frames and stderr echoing. No runtime confinement defaults, audit hashes or hardware claims change.

Test plan

  • Merged reference baseline: 29 passed, 12 Linux-only skips locally.
  • Focused follow-up suite: 40 passed, 17 Linux-only skips locally.
  • Export counterexample before the fix: two expected failures, four passing real-export probes.
  • Ruff, mypy and Bandit pass.
  • Full local suite including AARM telemetry tests: 1,992 passed, 25 skipped, 89.46% coverage.
  • Hosted Linux confinement run: all 57 tests passed in 22.34 seconds. Retained observations show protected containers stopped after bridge kill/pause and watcher kill; the unsupervised control remained running. The live-policy Docker case delivered two allowed calls and denied two calls.
  • Removing the live release gate makes the queued-call regression fail; restoring it passes all five policy lifecycle tests.
  • All 17 hosted checks passed at a56f5ab, including the Python/OS matrix, fuzzing, container build, docs and governance; two conditional checks skipped.

The reference does not cover simultaneous bridge/watcher loss, host-wide orphan recovery, arbitrary hot catalog/classifier replacement, or arbitrary exporter plugins. See docs/confinement.md.

DCO sign-off

  • Commits include the required DCO sign-off.

Signed-off-by: Imran Siddique <imran.siddique@opaque.co>

@carloshvp carloshvp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: LeaseWatchdog.pulse() treats a successful write/drain to the child stdin pipe as proof that the watchdog is alive. That is not a liveness acknowledgement: the pipe continues buffering heartbeats while the watchdog is hung or stopped. I suspended the watchdog after its ready line, waited 3.25 seconds (past the 2-second lease), and observed watchdog_returncode=None and pulse_done=False. The bridge therefore remains active with no responsive supervisor, and if the bridge subsequently dies the stopped watchdog cannot process EOF or stop the plaintext-bearing container.

This contradicts the documented claim that watchdog failure terminates the bridge exchange and leaves a fail-open supervision state untested. Please require a bounded acknowledgement from the watchdog (or another independent health signal) so missed watcher progress closes the bridge and invokes its own container-stop path, and add a causal Linux case that pauses the watchdog while the bridge remains active.

Qiang-Xu
Qiang-Xu previously approved these changes Sep 18, 2026

@Qiang-Xu Qiang-Xu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me over all.

@Qiang-Xu

Copy link
Copy Markdown
Collaborator

Blocking: LeaseWatchdog.pulse() treats a successful write/drain to the child stdin pipe as proof that the watchdog is alive. That is not a liveness acknowledgement: the pipe continues buffering heartbeats while the watchdog is hung or stopped. I suspended the watchdog after its ready line, waited 3.25 seconds (past the 2-second lease), and observed watchdog_returncode=None and pulse_done=False. The bridge therefore remains active with no responsive supervisor, and if the bridge subsequently dies the stopped watchdog cannot process EOF or stop the plaintext-bearing container.

That is a valid concern, we may want to check the buffer size, @imran-siddique thoughts?

Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
@imran-siddique

Copy link
Copy Markdown
Member Author

@Qiang-Xu buffer size would not establish that the watchdog is responsive. A write can succeed while the watchdog is stopped.

bad751b now requires a watchdog acknowledgement within one second. A missing or invalid reply ends the exchange. It also adds the paused-watchdog test and a control with supervision disabled.

The Linux run passed all 61 tests at that commit, including both new cases.

@carloshvp could you re-review the fix against your blocking finding?

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.

3 participants