Repository navigation
Add a parser and request builder for the sandbox step CLI - #279
Conversation
Pure, not-yet-wired modules for running sandbox steps over exec through the step CLI (python3 -P -m lightspeed_agentic.step), per the sandbox's step-cli.md contract: - spawner/step_protocol.py: incremental stdout JSONL reader with strict, bounded validation of event and result records (no NaN/Infinity, duplicate or extra keys, deep nesting; line, total and transcript caps), pid from the exec wrapper's first stderr line, a redacted stderr tail, and mapping of exit codes 0/1/2/3/4/124/signals plus result.json to step outcomes. - spawner/exec_request.py: builds the StepRequest stdin line with the same fields as /run, a derived stepId, a fresh runDir and traceparent. Omits allowedTools/deniedTools with a warning, rejects MCP servers with plain headers, and caps the request size. Golden fixtures were captured from the real step CLI at sandbox bca5ad0 using its fake providers; see fixtures/step_cli/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review of the step protocol modules: - Exit 0 without a valid result line is now a protocol violation. Exit 0 promises delivery on the CLI's private fd, and result.json can be replaced by a setsid'd agent descendant, so it is no longer a fallback there. - Results read from result.json (exit 4, transport loss) always taint the lease. After transport loss a file success is never trusted: the step raises so run_with_retries reruns it in a fresh sandbox. An invalid file after exit 4 is a protocol violation. - Exit 3 is a non-retryable failure that leaves the circuit breaker alone, since it is deterministic or agent-inducible. - The MCP plain-header check rejects any non-empty headers dict, including an empty header name. - The stderr tail is redacted on a wider window before truncation, so a secret cut at the window edge can't leak a fragment. - Document the stderr pid as best effort, and add check_exec_message_size() so the exec caller can check stdin, env and argv against the gateway's 1 MiB message limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
completed now comes only from a valid stdout result line. After exit 4 or a lost stream, result.json is replaceable by a same-uid descendant, so a valid file no longer decides the outcome. The step fails with reason result_unverified: no retry, no breaker, and the lease is tainted. The file's status and error summary go into a new, bounded diagnostic field. That field is kept out of error, because run_with_retries classifies error text and forged file text could otherwise make the step look transient. Align the docs with retry behaviour: error outcomes raise, but is_transient_failure only matches the 502 text, so lost stream, exit 124, killed and unexpected exits fail without a rerun. Tests pin that classification. Also add notes for the exec caller (run the CLI with python3 -I, and never reuse a tainted lease) and document the stderr redaction limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The diagnostic for an untrusted result.json held up to 200 chars of its error or summary text, truncated before the caller could redact it. A secret straddling the cut, or longer than the limit, left a fragment that redaction could not catch. The diagnostic now uses fixed wording and validated values only: the status code, the success flag on 200, has_<key> flags for a hardcoded key list, a count of other keys and the byte size. No string from the file appears in it, not even key names, so it needs no redaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit's test rewrite also deleted test_stdout_result_does_not_taint_lease and the seven cases of test_exit4_invalid_file_is_violation. Restore them verbatim; the current mapping still gives failed/protocol_violation with a tainted lease for an invalid result.json after exit 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
beesarmy
left a comment
There was a problem hiding this comment.
Reviewed head e456d06 against base 37a0840. No blocking findings remain in scope.
Approval covers only the new, currently unused protocol/parser and request-builder modules and their tests/fixtures. Inspected strict bounded parsing, request serialization and size limits, plain MCP-header rejection, stdout-only completion, diagnostic-only file outcomes, metadata-only diagnostics, and preservation of lease-taint and retry decisions. These modules do not wire in or enable exec transport.
Read-only source/test review: local unit and fixture-generation results are implementer evidence, not independently rerun here. Confirmed exact-head build, lint, unit-tests and e2e checks succeeded. Integration did not run because the workflow path filter excludes this diff; that is not an integration pass.
No live gateway, sandbox image, end-to-end transport, finite-tool enforcement, secret-bearing MCP activation, or warm-lease safety is established by this approval. Wiring and activation require separate review and evidence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
First piece of running sandbox steps through OpenShell exec instead of an exposed HTTP server. This adds two modules that are not wired into anything yet:
spawner/exec_request.pybuilds the JSON request the sandbox step CLI (python -m lightspeed_agentic.step) reads on stdin.spawner/step_protocol.pyreads the CLI's stdout/stderr and maps its exit code and result to the runner's existing step outcomes.The CLI contract is the sandbox's
.ai/spec/what/step-cli.md. The CLI runs as the same user as the agent's tools, so its output is treated as untrusted.Request builder
StepRequest:query,context,systemPrompt,outputSchema,timeout_ms,stepId,runDir,traceparent. Unknown keys are rejected by the CLI, soallowedTools/deniedToolsare never sent (a warning is logged if present) andtracestateis dropped.stepIdis derived from the step name: a slug plus a hash of workflow id, step name and attempt, always matching^[a-z0-9-]{1,40}$.check_exec_message_size(stdin, env, argv)checks the whole exec message against the gateway's 1 MiB limit, for the transport to call.headersvalues is refused, because exec mode has no safe way to pass them yet. The error names the server, never the values.Outcome rules
completedonly from a valid result line on stdout.result.jsonis not used as a fallback, because a process started by the agent can replace it.result.jsonis read only for a fixed-vocabulary diagnostic (status code, success flag, presence flags for a fixed list of fields, byte size). No text from the file is carried. The step fails, is not retried, and the sandbox is marked unsafe to reuse.Testing
tests/unit/spawner/fixtures/step_cli/were captured from the real step CLI (sandbox commitbca5ad01) using its own fake providers, with requests built by this builder.generate.pyand the README describe how; expected outcomes are written by hand from the spec.Not in this PR
The exec transport, the spawner changes and the runner wiring come in later PRs. Nothing here changes runtime behaviour.
🤖 Generated with Claude Code