diff --git a/pkg/tools/execute/code.go b/pkg/tools/execute/code.go index b3a9ea5..b67d4dd 100644 --- a/pkg/tools/execute/code.go +++ b/pkg/tools/execute/code.go @@ -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; diff --git a/pkg/tools/execute/pipe.go b/pkg/tools/execute/pipe.go index cf1bf7a..17c0e1d 100644 --- a/pkg/tools/execute/pipe.go +++ b/pkg/tools/execute/pipe.go @@ -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) diff --git a/pkg/tools/execute/server.go b/pkg/tools/execute/server.go index 076f5da..66d9ff6 100644 --- a/pkg/tools/execute/server.go +++ b/pkg/tools/execute/server.go @@ -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() }()