feat(harness): lease a pool sandbox for prompt leaves - #201
Merged
Conversation
A `kind:"prompt"` leaf resolved its own sandbox from the environment (ADR 0028: "prompt leaves inherit /turn's sandbox routing"), so it never consulted a lease. Two consequences: SH_REMOTE_SANDBOX/SH_RELAY_ADDR are read only on the lease path, making a remote sandbox unreachable from a prompt leaf; and on a pool-selector-only deployment like service.yaml's, resolveSandboxConfig returned null and the leaf's tool calls ran in the harness container itself -- silently, and indistinguishably from a sandbox that answered. runPromptLeaf now leases through selectPoolSandbox as the converge and solve paths do (heartbeat while the turn runs, release in a finally) and hands the leased sandbox to the turn. executeTurn gains an optional pre-leased `sandbox`; resolveTurnSandbox returns it verbatim when present and otherwise falls back to resolveSandboxConfig, so /turn is unchanged. This is a superset rather than a swap: with no pool selector configured, selectPoolSandbox performs that same single-pod resolution itself. Two contract changes, both recorded in the ADR amendment: saturation propagates as failed/saturated (the sync route bounded-waits then 503s; classifyOutcome already keeps it retryable for the async queue), and a throw from the turn returns failed/error instead of escaping as a 500 -- matching converge and solve. Drive the remote-sandbox demo with a free-form prompt, since the reply names the OS it read rather than handing over a flag to trust. lib-relay.sh gains dispatch_prompt and assert_reply_contains/_lacks alongside the verdict pair, which relay-leaf-smoke.sh keeps: its binary CLEAR/FLAGGED is the stronger gate for a live smoke that cannot run in CI. lib-relay-shared.test.sh locks both pairs and records why they diverge. The demo script also gains Act 3, which it previously stopped short of while the doc claimed every step was asserted: the planted marker (fail-closed if the pod unexpectedly has the file) and the presence-gone teardown, skipped under --keep since that flag promises the worker stays running. A workload-addressed prompt leaf still ignores the workload's own sandboxSelector; that remains deferred and is noted in the ADR. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
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.
Summary
A
kind:"prompt"leaf resolved its own sandbox from the environment — ADR-0028 deferred the lease with "prompt leaves inherit/turn's sandbox routing" — so it never consulted one. Two consequences:SH_REMOTE_SANDBOX/SH_RELAY_ADDRare read only on the lease path, so a remote sandbox was unreachable from a prompt leaf.deploy/knative/service.yamlis (aKAGENTI_SANDBOX_POOL_SELECTOR, noKAGENTI_SANDBOX_POD),resolveSandboxConfigreturnednulland the leaf's tool calls ran in the harness container's own filesystem — silently, and indistinguishably from a sandbox that answered. Since that container isnode:22-alpine, a prompt reading/etc/os-releasewould confidently report Alpine having touched no sandbox at all.The driver is the remote-sandbox demo, which needs a free-form prompt to execute its tool calls on a laptop container reached over the sandbox relay.
Changes
Harness
runPromptLeafleases throughselectPoolSandboxexactly as the converge and solve paths do — heartbeat while the turn runs, release and close the transport in afinally.executeTurngains an optional pre-leasedsandbox.resolveTurnSandboxreturns it verbatim when present and otherwise falls back toresolveSandboxConfig, so/turnis unchanged. It returns exactlyk8sSandboxExtension's argument and is passed through untransformed, so a leased transport cannot be dropped by a field-by-field rebuild.selectPoolSandboxperforms that same single-pod resolution itself and returnsnullwhen nothing is set.Two contract changes, both recorded in the ADR amendment:
{status:"failed", reason:"saturated"}— the sync/runspath already bounded-waits then 503s on it, andclassifyOutcomealready keeps it retryable for the async queue. The alternative ADR-0028 rejected ("imports the saturation/503 path") is accepted as a consequence rather than avoided; suppressing it would have taken extra code to be less consistent.failed/errorinstead of escaping as a500, matching converge and solve.Demo — driven by a free-form prompt, since the reply names the OS it read rather than handing over a flag to trust:
lib-relay.shgainsdispatch_promptandassert_reply_contains/_lacksalongside the verdict pair, whichrelay-leaf-smoke.shkeeps — its binaryCLEAR/FLAGGEDis the stronger gate for a live smoke that cannot run in CI.tests/lib-relay-shared.test.shlocks both pairs and records why they diverge.demo-remote-worker.shgains Act 3, which it previously stopped short of while the doc claimed every step was asserted: the planted marker (fail-closed if the pod unexpectedly has the file) and the presence-gone teardown, skipped under--keepsince that flag promises the worker stays running.Testing
resolveTurnSandbox, 6 for the lease (transport handed to the turn,remoteSandboxforwarded, request-scoped pool selector, saturation mapped, lease released on success and on a throw).make test— 686 pass, 0 fail (includes the deploy shell tests and Go tests).make typecheckclean.shellcheck -xclean on all three scripts apart fromSC2329/SC2015informational findings that predate this change.make demo-remote-sandbox DEMO_ARGS=--reuse-cluster): 12 assertions passed, 0 failed.Same prompt on both backends.
B/remote: reply names 'Red Hat'is the assertion that fails on the old code, where both runs report the harness container's Alpine.Not in scope
A workload-addressed prompt leaf (
workloadId) still ignores the workload's ownsandboxSelector, and the API boundary still logs that warning. The envelope'ssandboxPoolSelectoris honoured now, so only the workload-resolver special case remains — whether a workload's pool should bound its prompt leaves is a separate decision, noted as still deferred in the ADR.