toolsrv: kill all procs on shutdown, add Pdeathsig

- KillAll(): sends SIGKILL to all running proc groups on clean shutdown
- Pdeathsig: SIGKILL ensures child procs die if toolsrv crashes
- Shell tool: use subshell + set +e for streaming without exit propagation
- Fix integration test to match new error format

This is the connection-based ownership model: toolsrv death = proc death.
The session owns the toolsrv process, so session death cascades to all procs.
This commit is contained in:
Ollie Agent 2026-08-11 10:02:53 +02:00
parent 0d1eeaa46b
commit 97c208fa39
5 changed files with 36 additions and 4 deletions

View File

@ -368,11 +368,14 @@ func TestIntegration_ToolExecutionError(t *testing.T) {
t.Fatalf("failed to parse toolsrv.ToolResult: %v (raw: %s)", err, string(result))
}
// Should have exit code in output
// Should be an error result with exit code info
if len(toolResult.Content) == 0 {
t.Fatal("toolsrv.ToolResult.Content is empty")
}
if !strings.Contains(toolResult.Content[0].Text, "exit: 42") {
if !toolResult.IsError {
t.Error("expected IsError=true for non-zero exit")
}
if !strings.Contains(toolResult.Content[0].Text, "exit status 42") && !strings.Contains(toolResult.Content[0].Text, "exit: 42") {
t.Errorf("output doesn't contain exit code: %q", toolResult.Content[0].Text)
}

View File

@ -197,7 +197,7 @@ func executeSandboxed(ctx context.Context, toolPath, stdinData, cwd string, envE
cmd.Env = append(cmd.Env, k+"="+v)
}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGKILL}
cmd.Cancel = func() error {
if cmd.Process != nil {
syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)

View File

@ -336,6 +336,28 @@ func (st *State) ListProcs() []int {
return pids
}
// KillAll sends SIGKILL to all running procs and cancels their contexts.
// Called on shutdown to ensure no orphaned processes.
func (st *State) KillAll() {
st.procMu.Lock()
procs := make([]*Proc, 0, len(st.procs))
for _, p := range st.procs {
procs = append(procs, p)
}
st.procMu.Unlock()
for _, p := range procs {
p.mu.Lock()
proc := p.proc
exited := p.Exited
p.mu.Unlock()
if !exited && proc != nil {
syscall.Kill(-proc.Pid, syscall.SIGKILL)
}
p.cancel()
}
}
// DismissProc removes a process from the list.
func (st *State) DismissProc(pid int) bool {
st.procMu.Lock()

View File

@ -105,6 +105,9 @@ func main() {
cancel()
}()
// Kill all procs on shutdown
defer srv.Fs.KillAll()
// Start idle timeout monitor
if *idleTimeout > 0 {
go idleMonitor(runCtx, cancel, *idleTimeout)

View File

@ -31,7 +31,11 @@ fi
# Execute the command. The sandbox (landlock) is already applied by toolsrv.
# We use eval to handle pipes, redirects, and compound commands.
# Output streams directly to stdout/stderr for real-time capture.
eval "$cmd" 2>&1
# Run in a subshell so 'exit N' doesn't kill this script.
# Disable errexit for the subshell so we can capture the exit code.
set +e
(eval "$cmd") 2>&1
rc=$?
set -e
echo "exit: $rc"
exit $rc