Files
zero/internal/agent/member_auto_test.go
Vasanth T 46c44a8753 feat(swarm): let swarm members build (write + sandboxed shell) (#313)
* feat(swarm): let swarm members build (write + sandboxed shell)

Swarm subagents were effectively read-only: a member runs headless at
"--auto low" (PermissionModeAuto), and ToolAdvertised strips every
prompt-requiring tool from the model's list — so write_file/edit_file/
apply_patch/bash were never handed to the member. It only saw read/grep/
glob/plan tools, so it could not build and the orchestrator did all the
writes itself.

Add PermissionModeMemberAuto: a headless mode that ADVERTISES the
in-workspace mutators (SideEffectWrite + SideEffectShell) on top of the
Auto set, while behaving exactly like Auto everywhere else. The sandbox
engine still decides at call time — in-workspace writes and sandbox-backed
shell auto-allow; out-of-workspace writes, network, and destructive
commands prompt and so are denied headless. member-auto normalizes to Auto
in the sandbox layer, so no sandbox authority is widened beyond what an
interactive auto agent already has.

Scope: swarm members only. The launcher sets MemberAutonomy; the executor
emits "--auto member" for a non-unsafe member (unsafe still -> high, plain
specialists still -> low). Task-tool specialists are unchanged.

Windows note: shell only auto-runs when the OS sandbox is active; without
`zero sandbox setup` a member writes files but cannot run builds.

* style: gofmt member_auto_test.go
2026-06-25 11:36:40 +05:30

52 lines
2.1 KiB
Go

package agent
import (
"context"
"testing"
"github.com/Gitlawb/zero/internal/tools"
)
type safetyTool struct {
name string
safety tools.Safety
}
func (t safetyTool) Name() string { return t.name }
func (t safetyTool) Description() string { return "test tool" }
func (t safetyTool) Parameters() tools.Schema {
return tools.Schema{Type: "object", AdditionalProperties: false}
}
func (t safetyTool) Safety() tools.Safety { return t.safety }
func (t safetyTool) Run(context.Context, map[string]any) tools.Result {
return tools.Result{Status: tools.StatusOK}
}
// member-auto advertises the in-workspace mutators a headless member needs to
// build (write/edit + shell) that plain Auto hides, but NOT network or denied
// tools — the sandbox still gates the advertised ones at call time.
func TestToolAdvertisedMemberAuto(t *testing.T) {
write := safetyTool{name: "write_file", safety: tools.Safety{SideEffect: tools.SideEffectWrite, Permission: tools.PermissionPrompt}}
shell := safetyTool{name: "bash", safety: tools.Safety{SideEffect: tools.SideEffectShell, Permission: tools.PermissionPrompt}}
read := safetyTool{name: "read_file", safety: tools.Safety{SideEffect: tools.SideEffectRead, Permission: tools.PermissionAllow}}
network := safetyTool{name: "net_tool", safety: tools.Safety{SideEffect: tools.SideEffectNetwork, Permission: tools.PermissionPrompt}}
denied := safetyTool{name: "blocked", safety: tools.Safety{SideEffect: tools.SideEffectRead, Permission: tools.PermissionDeny}}
// Plain Auto hides prompt-requiring mutators (the read-only member problem).
if ToolAdvertised(write, PermissionModeAuto) || ToolAdvertised(shell, PermissionModeAuto) {
t.Fatal("Auto must NOT advertise write/shell prompt tools")
}
for _, tool := range []tools.Tool{write, shell, read} {
if !ToolAdvertised(tool, PermissionModeMemberAuto) {
t.Fatalf("member-auto must advertise %q", tool.Name())
}
}
if ToolAdvertised(network, PermissionModeMemberAuto) {
t.Fatal("member-auto must NOT advertise a network prompt tool")
}
if ToolAdvertised(denied, PermissionModeMemberAuto) {
t.Fatal("member-auto must NOT advertise a denied tool")
}
}