execute: add detach field for autonomous background processes

When detach: true is set on a CodeStep, the process is immediately
backgrounded after start. Its stdout/stderr are captured in a 64KB
ring buffer accessible via GetDetachedOutput. The agent can then
inspect and manage the process via process_list/process_output tools.
This commit is contained in:
ollie 2026-07-17 23:28:14 +02:00
parent b6d0653d0b
commit c1124b588d
3 changed files with 12 additions and 3 deletions

View File

@ -19,6 +19,7 @@ import (
// CodeStep is one stage in an execution pipeline.
// Set Code/Language for inline code, Tool/Args for a named script, or Parallel for concurrent fan-out.
// Set Elevated to run the step outside the sandbox via the configured elevation backend.
// Set Detach to immediately background the process and return its PID.
type CodeStep struct {
Code string `json:"code"`
Language string `json:"language"`
@ -26,6 +27,7 @@ type CodeStep struct {
Args []string `json:"args"`
Parallel []CodeStep `json:"parallel"`
Elevated bool `json:"elevated"`
Detach bool `json:"detach"`
}
// resolveCodeStep loads a CodeStep into executable (code, language, trusted).
@ -267,7 +269,7 @@ func (e *Server) dispatchSteps(ctx context.Context, steps []CodeStep, timeout in
if err != nil {
return "", err
}
return e.executeWithStdin(ctx, code, lang, timeout, sandboxName, trusted, "")
return e.executeWithStdin(ctx, code, lang, timeout, sandboxName, trusted, "", steps[0].Detach)
}
// Multiple steps: auto-batch consecutive read-safe steps in parallel;

View File

@ -55,7 +55,7 @@ func (e *Server) runStage(ctx context.Context, idx int, stage CodeStep, timeout
if err != nil {
return "", err
}
return e.executeWithStdin(ctx, code, lang, timeout, sandboxName, trusted, stdinData)
return e.executeWithStdin(ctx, code, lang, timeout, sandboxName, trusted, stdinData, stage.Detach)
}
return "", fmt.Errorf("stage %d: requires code, tool, or parallel", idx)

View File

@ -157,6 +157,7 @@ No stdout chaining between steps — use pipe for that.
Each step is one of:
- {code, language} — inline code (default language: bash)
- {elevated: true, code} — run outside sandbox via elevation backend (bash only)
- {detach: true, code} — start process and immediately background it; returns PID
- {parallel: [{code/language}...]} — concurrent fan-out; outputs concatenated in submission order
Supported languages: bash, python3, perl, lua, awk, sed, jq, ed, expect, bc.
@ -179,6 +180,7 @@ Examples:
"code": {"type": "string", "description": "Inline code to execute."},
"language": {"type": "string", "description": "Language interpreter (default: bash)."},
"elevated": {"type": "boolean", "description": "Run outside the sandbox via the elevation backend. Only bash is supported."},
"detach": {"type": "boolean", "description": "Start the process and immediately background it. Returns the PID. Use process_list/process_output to inspect."},
"parallel": {
"type": "array",
"description": "Fan-out: steps run concurrently, outputs concatenated in submission order.",
@ -486,7 +488,7 @@ func (e *Server) Execute(ctx context.Context, code, language string, timeout int
// executeWithStdin is like Execute but feeds stdinData to the command's stdin.
// For languages where code is itself passed via stdin (ed, expect, bc), stdinData is ignored.
func (e *Server) executeWithStdin(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool, stdinData string) (string, error) {
func (e *Server) executeWithStdin(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool, stdinData string, detach ...bool) (string, error) {
if timeout < 0 {
timeout = 30
}
@ -640,6 +642,11 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
return "", fmt.Errorf("execution failed: %v", err)
}
// If detach requested, immediately signal the detach channel
if len(detach) > 0 && detach[0] {
close(detachCh)
}
// Wait for completion, context cancellation, or detach signal
waitCh := make(chan error, 1)
go func() { waitCh <- cmd.Wait() }()