Documentation
¶
Overview ¶
Package broker carries command execution across the sandbox boundary. The in-sandbox client sends a command line over a unix socket; the server, which runs outside the agent's own sandbox, interprets the shell language itself and streams the output back.
The wire format is one request per connection, followed by length-prefixed frames in both directions. Frames keep stdout and stderr separate so a caller can route them independently.
Index ¶
- Constants
- Variables
- func WriteError(w io.Writer, msg string) error
- func WriteExit(w io.Writer, code int) error
- func WriteFrame(w io.Writer, ch Channel, payload []byte) error
- func WriteRequest(w io.Writer, req Request) error
- type Channel
- type Client
- type CommandRunner
- type Executor
- type Frame
- type Request
- type Server
- type ShellExecutor
Constants ¶
const ( PolicyLaunchBurst = 4 PolicyLaunchInterval = 500 * time.Millisecond )
PolicyLaunchBurst and PolicyLaunchInterval pace how fast policy-controlled commands may be launched: PolicyLaunchBurst of them may start with no wait at all, and the budget refills at one launch per PolicyLaunchInterval. See launchPacer for what is being rationed and how the numbers were arrived at.
const SandboxNotRunningHint = "command broker is not available; run Claude via `agent-sandbox claude`, which starts it automatically"
SandboxNotRunningHint is the actionable message shown when the broker is not reachable. It is exported because the situation is detected before any command runs: `agent-sandbox exec` and the MCP server both fail to build a client when AGENT_SANDBOX_BROKER_SOCKET is unset, and must print this rather than a raw dial error.
const SocketEnvVar = "AGENT_SANDBOX_BROKER_SOCKET"
SocketEnvVar names the environment variable that carries the broker socket path into the sandbox.
Variables ¶
ErrBrokerUnavailable signals that the broker socket could not be reached. Callers translate it into an actionable message instead of a raw dial error.
Functions ¶
func WriteError ¶
WriteError writes a terminal error frame carrying a human-readable message.
func WriteFrame ¶
WriteFrame writes one frame: 1 byte channel, 4 bytes big-endian length, payload.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client dials the broker socket. It is safe for concurrent use: every call opens its own connection, which is what lets a mixed pipeline run several sandboxed segments at once.
func NewClientFromEnv ¶
NewClientFromEnv builds a client from SocketEnvVar. It returns ErrBrokerUnavailable when the variable is unset, which happens whenever a command is routed to the sandbox outside an `agent-sandbox claude` session.
type CommandRunner ¶ added in v0.17.0
type CommandRunner interface {
RunCommand(ctx context.Context, command string, stdin io.Reader,
stdout, stderr io.Writer) (int, error)
}
CommandRunner executes one command line inside the sandbox. The broker client is the production implementation; tests substitute their own.
type Executor ¶
type Executor interface {
Execute(ctx context.Context, req Request, stdin io.Reader,
stdout, stderr io.Writer) (int, error)
}
Executor runs one command line. The production implementation is ShellExecutor; tests substitute a fake so the server can be exercised without spawning anything.
type Frame ¶
Frame is one decoded frame.
type Request ¶
type Request struct {
Command string `json:"command"`
// Cwd is client-controlled: it comes straight from the sandboxed agent's
// own working directory (see Client.RunCommand's workingDir helper). It
// reaches the interpreter's Dir option and, through it, --workdir of the
// commands the interpreter execs, but nothing in this package bounds it to
// any particular root — see ShellExecutor.Execute for where that bound
// actually lives.
Cwd string `json:"cwd"`
WithStdin bool `json:"with_stdin"`
}
Request is the first message on a connection: what to run and where.
It carries a command *line*, not an argv. The broker interprets the shell language itself and executes each simple command, so splitting it here would duplicate that work in the one place that cannot see the result.
There is deliberately no environment field. The command's environment is a policy decision owned by the command profile: the broker's own environment is filtered by nono before it starts, and each command's is decided by its entry. A request-supplied environment could not work — the agent's nono profile strips those variables long before they could be reported — and must not work, because the request originates inside the sandbox it would configure.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server accepts one command per connection on a unix socket. It runs in the dedicated broker process — outside the agent's own sandbox, but inside a nono session of its own — which is the point: a process cannot create a new sandbox boundary around itself, so the broker needs a boundary of its own rather than borrowing the agent's, or worse, running with the unsandboxed launcher's own reach.
That session is started by internal/claude.startCommandBroker, which runs `nono run --profile <command profile> -- agent-sandbox broker --socket <path>` (see claude.BrokerArgs) as a sibling of the agent's own sandbox, not a child of it — nono refuses to nest.
func NewServer ¶
NewServer creates the socket at sockPath with 0600 permissions. An existing stale socket at that path is removed first: a previous run that was killed leaves the file behind, and bind would otherwise fail forever.
func (*Server) Serve ¶
func (s *Server) Serve()
Serve accepts connections until Close is called. It is meant to run in its own goroutine.
func (*Server) SocketPath ¶
SocketPath returns the path clients dial.
type ShellExecutor ¶ added in v0.17.0
type ShellExecutor struct {
// contains filtered or unexported fields
}
ShellExecutor runs one command line. It parses and evaluates the shell language in this process with mvdan.cc/sh and executes every simple command itself, which is what keeps each execution mediated: the broker runs inside a nono session whose command policies decide what may be executed at all, and handing the line to a real shell would hand that decision to the shell.
Everything the shell language does with the filesystem — globbing, redirects, command substitution — is performed here and is therefore bounded by the broker's own sandbox, not by the agent's.
func NewShellExecutor ¶ added in v0.17.0
func NewShellExecutor() *ShellExecutor
NewShellExecutor returns a ShellExecutor. Its only state is the launch pacer below, which is deliberately per-executor rather than per-request: see the pacer field.
func (*ShellExecutor) Execute ¶ added in v0.17.0
func (e *ShellExecutor) Execute(ctx context.Context, req Request, stdin io.Reader, stdout, stderr io.Writer) (int, error)
Execute satisfies Executor so the server can run a request directly. The server owns the transport; ShellExecutor owns the shell language.
req.Cwd is client-controlled and flows straight into interp.Dir, and from there into every command's own working directory. The old router-based design (NonoExecutor.checkCwd) rejected a relative path or one outside the granted root itself; this executor does not reproduce that check, because the bound it enforced now comes from the broker's own nono session instead: the broker runs under --profile with no --allow-cwd (see BrokerArgs), so every filesystem access the interpreter or a child process makes — cwd included — is already confined to whatever that profile grants, whatever req.Cwd claims. What is checked here is only the request's shape, not its reach: a non-absolute Cwd (including the empty string a client sends when its own os.Getwd fails) is refused rather than silently resolved against this process's own working directory, which would not be the directory the agent thinks it is running commands in.
func (*ShellExecutor) Run ¶ added in v0.17.0
func (e *ShellExecutor) Run(ctx context.Context, command, cwd string, stdin io.Reader, stdout, stderr io.Writer) (int, error)
Run evaluates command with cwd as the working directory, streaming output to stdout and stderr, and returns the exit status of the last command.
The error is non-nil only for a failure of Run itself. A syntax error, a command that does not exist, and a command that fails are all reported through the exit status with a message on stderr, because they are outcomes of the agent's line rather than faults of the broker.