sandbox

package
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Index

Constants

View Source
const (
	StreamTypeStdout = "stdout"
	StreamTypeStderr = "stderr"
	StreamTypeExit   = "exit"
	StreamTypeKill   = "kill"
)

StreamChunk is one unit in the streaming host-command protocol (Streaming=true).

Broker → Shim: type "stdout"/"stderr" carry Data; type "exit" carries ExitCode. Shim → Broker: type "kill" requests SIGTERM on the host process group.

Variables

This section is empty.

Functions

func CheckPolicy

func CheckPolicy(def CommandDef, args []string) bool

func CommandFromArgv0

func CommandFromArgv0(argv0 string) string

func RunGitShim

func RunGitShim(args []string) (int, error)

func ShellQuote

func ShellQuote(value string) string

ShellQuote is the exported version of shellQuote for use outside the package.

Types

type BindMount

type BindMount struct {
	Source   string
	Target   string // if empty, defaults to Source
	Mode     string // "rw" | "" (ro default)
	IsFile   bool   // if true, treat target as a file (touch before bind, skip type detection)
	Optional bool   // if true, skip mount when Source does not exist on the host
}

BindMount is the dispatcher-facing DTO for arbitrary bind-mount requests. It is used by the dispatcher boundary (via SandboxSpec.AdditionalBindings) and is converted into Mount entries at the edge. The sandbox layer itself works in terms of Mount only.

type BoidExecutor

type BoidExecutor interface {
	ExecuteBoidBuiltin(ctx context.Context, tokenCtx TokenContext, req *BoidRequest) *ExecResponse
}

BoidExecutor performs typed boid builtin operations after broker validation.

ctx is the broker-side request context. For most ops it carries no deadline, but for the blocking BoidOpTaskAsk it is cancelled when the broker shuts down or the sandbox connection closes, so the server-side handler can stop waiting for an answer and clean up.

type BoidOp

type BoidOp string
const (
	BoidOpJobDone    BoidOp = "job_done"
	BoidOpJobList    BoidOp = "job_list"
	BoidOpJobShow    BoidOp = "job_show"
	BoidOpJobLog     BoidOp = "job_log"
	BoidOpActionSend BoidOp = "action_send"
	BoidOpAgentStop  BoidOp = "agent_stop"
	BoidOpTaskCreate BoidOp = "task_create"
	BoidOpTaskGet    BoidOp = "task_get"
	BoidOpTaskUpdate BoidOp = "task_update"
	BoidOpTaskImport BoidOp = "task_import"
	BoidOpTaskReopen BoidOp = "task.reopen"
	BoidOpTaskList   BoidOp = "task_list"
	BoidOpTaskNotify BoidOp = "task_notify"
	BoidOpTaskAnswer BoidOp = "task_answer"
	BoidOpTaskAsk    BoidOp = "task_ask"
	BoidOpTaskDelete BoidOp = "task_delete"
)

type BoidRequest

type BoidRequest struct {
	Op        BoidOp `json:"op"`
	JobID     string `json:"job_id,omitempty"`
	TaskID    string `json:"task_id,omitempty"`
	TaskField string `json:"task_field,omitempty"`
	// ProjectID is extracted by the shim for broker authorization / project
	// resolver; it is also present inside CreatePatch when the YAML includes
	// project_id. The executor prefers createReq.ProjectID from CreatePatch
	// and falls back to this field, then to ctx.ProjectID.
	ProjectID string `json:"project_id,omitempty"`

	ExitCode int    `json:"exit_code,omitempty"`
	Output   string `json:"output,omitempty"`

	// CreatePatch is a JSON-serialised api.CreateTaskRequest. The shim builds
	// it from the full YAML map so that every field (including previously
	// dropped ones such as instructions, traits, readonly, worktree,
	// branch_prefix, id) is forwarded without enumeration.
	CreatePatch json.RawMessage `json:"create_patch,omitempty"`

	// UpdatePatch is a JSON-serialised api.UpdateTaskRequest. The shim
	// assembles it from --patch-file and/or individual flags (--title,
	// --description, --payload-file).
	UpdatePatch json.RawMessage `json:"update_patch,omitempty"`

	// task import fields
	ImportTasks           []json.RawMessage `json:"import_tasks,omitempty"`
	ImportProjectOverride string            `json:"import_project_override,omitempty"`

	// task list fields
	WorkspaceID string `json:"workspace_id,omitempty"`
	Status      string `json:"status,omitempty"`
	Limit       int    `json:"limit,omitempty"`

	// task notify fields
	Message    string `json:"message,omitempty"`
	Ask        string `json:"ask,omitempty"`
	QuestionID string `json:"question_id,omitempty"`
	Progress   string `json:"progress,omitempty"`
	Done       string `json:"done,omitempty"`
	Fail       string `json:"fail,omitempty"`

	// task answer fields
	Answer string `json:"answer,omitempty"`

	// task ask fields. Question carries the blocking-RPC question text for
	// `boid task ask <text>`. The broker holds the connection open until the
	// answer arrives; the answer is returned verbatim in ExecResponse.Stdout
	// (the boid builtin reply framing is ExecResponse, so no separate
	// BoidResponse type is needed).
	Question string `json:"question,omitempty"`

	// task delete fields
	Force bool `json:"force,omitempty"`

	// action send fields
	ActionType string `json:"action_type,omitempty"`
	Payload    []byte `json:"payload,omitempty"`
}

type Broker

type Broker struct {
	SocketPath      string
	BoidBinary      string
	BoidExecutor    BoidExecutor
	ProjectResolver ProjectResolver
	// contains filtered or unexported fields
}

func (*Broker) GetContext

func (b *Broker) GetContext(token string) (TokenContext, bool)

func (*Broker) Handle

func (b *Broker) Handle(req *ExecRequest) *ExecResponse

Handle dispatches a request using the broker's base context. Retained for callers (mainly tests) that drive the broker synchronously without a per-connection context; the live socket path uses handle with a request ctx.

func (*Broker) Register

func (b *Broker) Register(commands map[string]CommandDef, builtinPolicies map[string]BuiltinPolicy, ctx TokenContext) string

func (*Broker) RegisterWithSecrets

func (b *Broker) RegisterWithSecrets(commands map[string]CommandDef, builtinPolicies map[string]BuiltinPolicy, ctx TokenContext, resolver SecretResolver) string

func (*Broker) Start

func (b *Broker) Start(ctx context.Context) error

func (*Broker) Stop

func (b *Broker) Stop()

func (*Broker) Unregister

func (b *Broker) Unregister(token string)

type BuiltinPolicy

type BuiltinPolicy struct {
	AllowedOps map[string]struct{}
	// AllowedCwdRoots lists additional cwd roots permitted for this builtin
	// beyond the per-token entry root (project/worktree dir). Used so that
	// e.g. gate jobs can target /tmp or the host project dir without the
	// broker needing to know the role itself.
	AllowedCwdRoots []string
}

BuiltinPolicy defines which operations are permitted for a named builtin command. It is stamped at token registration time by the planner and checked at dispatch time by the broker, keeping all role-based authorization logic outside the broker itself.

func (BuiltinPolicy) Allows

func (p BuiltinPolicy) Allows(op string) bool

Allows reports whether op is in the allowed set.

func (BuiltinPolicy) AllowsCwd

func (p BuiltinPolicy) AllowsCwd(cwd string) bool

AllowsCwd reports whether cwd is within any of the policy's additional cwd roots.

type CommandDef

type CommandDef struct {
	Name               string
	Path               string
	AllowedPatterns    []string
	DeniedPatterns     []string
	AllowedSubcommands []string
	AllowStdin         bool
	Env                map[string]string
	// MissingSecrets lists "ENV_NAME (secret:key)" entries for env vars that
	// the kit declared via the "secret:" prefix but failed to resolve at
	// register time. When non-empty, the broker rejects exec requests for
	// this command (fail-closed) instead of silently letting host env /
	// host-side config fallbacks take over.
	MissingSecrets []string
}

CommandDef is the canonical sandbox-side policy for brokered host commands. Dispatcher and orchestrator mirror this shape as transport data, but the policy semantics live here.

func (CommandDef) MissingSecretsMessage added in v0.0.6

func (d CommandDef) MissingSecretsMessage() string

MissingSecretsMessage returns the human-readable rejection message used when a host_command is invoked despite unresolved declared secrets. Returns an empty string if MissingSecrets is empty.

type ExecRequest

type ExecRequest struct {
	Command   string        `json:"command"`
	Args      []string      `json:"args"`
	Cwd       string        `json:"cwd,omitempty"`
	Stdin     []byte        `json:"stdin,omitempty"`
	Token     string        `json:"token"`
	Boid      *BoidRequest  `json:"boid,omitempty"`
	Git       *GitRequest   `json:"git,omitempty"`
	Fetch     *FetchRequest `json:"fetch,omitempty"`
	Streaming bool          `json:"streaming,omitempty"`
}

type ExecResponse

type ExecResponse struct {
	ExitCode int    `json:"exit_code"`
	Stdout   string `json:"stdout"`
	Stderr   string `json:"stderr"`
}

func ExecFetch added in v0.0.5

func ExecFetch(req *FetchRequest) *ExecResponse

ExecFetch performs an HTTP GET on the host (no broker involved) and returns the result as an ExecResponse. Used by the `boid fetch` CLI subcommand.

func RunBoidShim

func RunBoidShim(args []string) (*ExecResponse, error)

func ShimExec

func ShimExec(brokerSocket, argv0 string, args []string, stdin []byte) (*ExecResponse, error)

ShimExec sends a host command request to the broker using the streaming protocol so that stdout/stderr is forwarded in real-time and signals (Ctrl-C) are propagated to the host process group.

type FetchRequest added in v0.0.5

type FetchRequest struct {
	URL string `json:"url"`
}

FetchRequest carries the parameters for a broker-mediated HTTP GET. Only GET is supported; the broker performs the request on the host and returns the response body (HTML converted to markdown, other types verbatim).

type FileWrite

type FileWrite struct {
	Path    string // absolute path inside sandbox
	Content string
}

FileWrite describes a file to materialize inside the sandbox. Content is written verbatim (shell-quoted at render time); the parent directory is created with mkdir -p beforehand.

type GitBinding

type GitBinding struct {
	ProjectDir     string
	WorktreeRoot   string
	SnapshotBranch string
	Upstream       GitUpstream
	Remotes        map[string]GitRemote
}

type GitOp

type GitOp string
const (
	GitOpFetch      GitOp = "fetch"
	GitOpPush       GitOp = "push"
	GitOpPushDelete GitOp = "push_delete"
	GitOpCloneLocal GitOp = "clone_local"
)

type GitRemote

type GitRemote struct {
	FetchURL string
	PushURL  string
}

type GitRequest

type GitRequest struct {
	Op             GitOp    `json:"op"`
	Remote         string   `json:"remote,omitempty"`
	Refspecs       []string `json:"refspecs,omitempty"`
	DryRun         bool     `json:"dry_run,omitempty"`
	Verbose        bool     `json:"verbose,omitempty"`
	Quiet          bool     `json:"quiet,omitempty"`
	Prune          bool     `json:"prune,omitempty"`
	Tags           bool     `json:"tags,omitempty"`
	Force          bool     `json:"force,omitempty"`
	Porcelain      bool     `json:"porcelain,omitempty"`
	ForceWithLease bool     `json:"force_with_lease,omitempty"`
	Delete         bool     `json:"delete,omitempty"`
	SetUpstream    bool     `json:"set_upstream,omitempty"`
	// Source and Dest are used exclusively for GitOpCloneLocal.
	// Source is the peer project path to clone from; Dest is the destination
	// directory within the current worktree. Both are validated by the broker.
	Source string `json:"source,omitempty"`
	Dest   string `json:"dest,omitempty"`
}

type GitUpstream

type GitUpstream struct {
	Remote   string
	MergeRef string
}

type HarnessType added in v0.0.6

type HarnessType string

HarnessType identifies which HarnessAdapter the runner should hand the process off to via adapter.Run(). Phase 3-d (PR1) made this field invariant non-empty for every dispatched job — hook / session / exec all resolve to shell / claude / codex / opencode. The empty string is no longer a valid value and the runner-inner-child rejects it; the legacy runExecArgv fallback was retired in the same change.

const (
	// HarnessShell routes through internal/adapters/shell.Adapter.Run() and
	// is the fall-through for hooks without an `agent:` declaration, every
	// `boid exec` job, and the default for unknown agent values. Shell
	// adapter forwards SIGUSR1 / SIGWINCH and normalises stop-signal exits
	// like the agent adapters do but performs no session resolution.
	HarnessShell HarnessType = "shell"
	// HarnessClaude routes through internal/adapters/claude.Adapter.Run().
	HarnessClaude HarnessType = "claude"
	// HarnessCodex routes through internal/adapters/codex.Adapter.Run().
	// Added in Phase 3-c as a prototype to validate the HarnessAdapter
	// abstraction beyond claude. Minimum implementation: 1-turn launch with
	// signal forwarding; session resume / payload patch are deliberately
	// left as no-ops (see docs/plans/agent-aware-boid.md Phase 3-c).
	HarnessCodex HarnessType = "codex"
	// HarnessOpenCode routes through internal/adapters/opencode.Adapter.Run().
	// Phase 3-c prototype, same scope as HarnessCodex.
	HarnessOpenCode HarnessType = "opencode"
)

type Mount

type Mount struct {
	Source     string // host path (empty for tmpfs)
	Target     string // absolute path inside sandbox
	Type       MountType
	ReadOnly   bool
	Slave      bool     // mount --make-rslave after mounting
	IsFile     bool     // target is a file, not a directory
	DetectType bool     // detect file vs dir at runtime (if/elif)
	Guard      string   // shell test expression; if non-empty, wrap in if [ $Guard ]; then
	NeedsDirs  []string // subdirs to create under Target before ro remount
}

Mount describes a single filesystem mount applied inside the sandbox. Types: bind, rbind, tmpfs. Flags cover read-only remount, file vs dir handling, slave propagation, guards, and required sub-directory creation.

type MountType

type MountType string

MountType represents the type of filesystem mount.

const (
	MountBind  MountType = "bind"
	MountRBind MountType = "rbind"
	MountTmpfs MountType = "tmpfs"
)

type NFTRule added in v0.0.6

type NFTRule struct {
	Args []string
}

NFTRule is a single nftables command expressed as an argv. The go-native sandbox runner applies it with exec.Command("nft", Args...). Replaces the former []string of pre-rendered shell lines so the runner can exec nft directly without a shell.

type Plan added in v0.0.6

type Plan struct {
	Mounts       []Mount
	Files        []FileWrite
	Symlinks     []Symlink
	NFTRules     []NFTRule
	CleanupPaths []string
}

Plan is the declarative description of the sandbox layout that the go-native runner materializes via syscalls. It starts with base mounts (system dirs, /dev, /proc, /tmp, DNS) plus optional nft rules and then appends everything the caller supplied in Spec.

func BuildPlan added in v0.0.6

func BuildPlan(spec Spec) *Plan

BuildPlan constructs the Plan from a Spec. The base mounts and nft rules are sandbox-package knowledge; the runner package consumes the result.

type ProjectResolver

type ProjectResolver func(ref string) (uuid string, err error)

ProjectResolver maps a project reference (UUID, exact name, or partial name) to a concrete project UUID. The broker calls it just before the UUID-based AllowsProject authorization check so that sandbox-side callers (e.g. plan agents) can use project names while the broker continues to enforce workspace isolation in UUID space. When nil, the broker passes refs through verbatim (tests and UUID-only callers).

type Proxy

type Proxy struct {
	// contains filtered or unexported fields
}

func NewProxy

func NewProxy(allowedDomains []string) *Proxy

func WireProxy

func WireProxy(allowedDomains []string) *Proxy

func (*Proxy) Start

func (p *Proxy) Start(ctx context.Context) (int, error)

func (*Proxy) Stop

func (p *Proxy) Stop()

type Sandbox

type Sandbox interface {
	Setup(cfg SandboxConfig) error
	Shell(windowName string, cmd string) (string, error)
	Cleanup() error
}

Sandbox provides an isolated execution environment.

type SandboxConfig

type SandboxConfig struct {
	ProjectDir    string
	WorkspaceDirs map[string]string // project-id -> dir (readonly mounts)
	TaskFile      string
	HostCommands  []string
	Bindings      []string
	Env           map[string]string
	BoidBinary    string
	BrokerSocket  string
	ServerSocket  string
}

SandboxConfig holds configuration for sandbox setup.

type SecretResolver

type SecretResolver func(key string) (string, error)

type Spec

type Spec struct {
	// ID is the job id; it namespaces the generated /tmp/boid-<ID>-* files.
	ID string

	// --- Filesystem primitives ---
	// Mounts are applied in order to compose the sandbox root filesystem.
	Mounts []Mount
	// Files are materialized inside the sandbox (after pivot_root) before the
	// entry command runs.
	Files []FileWrite
	// Symlinks are created inside the sandbox.
	Symlinks []Symlink

	// --- Network ---
	// ProxyPort, when > 0, engages the nft drop policy + HTTP proxy env vars.
	ProxyPort int

	// --- Process ---
	// Argv is the program and arguments to invoke (POSIX argv).
	Argv []string
	// WorkDir is the cwd for the entry process. Also used as the cwd reported to
	// the broker for the job-done call (must be within the project/worktree root).
	WorkDir string
	// Env is the environment for the entry process.
	Env map[string]string
	// StdinBytes, when non-empty, is piped into the entry's stdin.
	StdinBytes []byte
	// StdoutCaptureFile, when non-empty, redirects stdout to that sandbox-internal
	// path. Doubles as the job-done output fallback when no payload patch exists.
	StdoutCaptureFile string
	// TTY, when true, the entry process inherits the caller's PTY on stdio.
	TTY bool

	// --- Completion / broker ---
	// Foreground indicates a user-facing job (boid exec): stdout/stderr are
	// inherited and no broker job-done callback fires on exit. Hook jobs leave
	// this false so the runner posts `boid job done` through the broker.
	Foreground bool
	// PayloadPatchPath is the sandbox-internal path the agent writes its result
	// patch to (HOME/.boid/output/payload_patch.json). The runner reads it as the
	// job-done output. Broker socket path and token are carried in Env
	// (BOID_BROKER_SOCKET / BOID_BROKER_TOKEN).
	PayloadPatchPath string

	// --- Bookkeeping ---
	// RootDir, if non-empty, is the host directory used as the sandbox ROOT (a
	// tmpfs is mounted on it). Go-side cleanup removes it after exit.
	RootDir string
	// CleanupPaths are removed by runner-outer after the sandbox process tree
	// exits (used for staging dirs). Removal runs in the host mount namespace,
	// where the sandbox's bind mounts are already gone.
	CleanupPaths []string
	// HarnessType, when non-empty, directs runner-inner-child to hand the
	// agent process off to the matching HarnessAdapter.Run() instead of
	// exec-ing Argv verbatim. Empty preserves the legacy exec path used by
	// boid exec jobs and any non-agent hook job. See sandbox.HarnessType.
	HarnessType HarnessType

	// UserAnswer carries the Q&A reply that should be threaded into the
	// adapter's RunContext.UserAnswer. Empty for fresh starts and for resumes
	// without a Q&A reply.
	UserAnswer string
}

Spec describes a sandbox invocation in primitives only. The sandbox layer knows nothing about Role / Task / Job / Broker / Hook — all of those are the caller's concern. Everything needed to build and run the sandbox must already be present as mounts, files, env, argv, etc.

Spec is JSON-serialized by the dispatcher to /tmp/boid-<ID>-runner-spec.json and read back by the go-native runner subcommands (runner-outer / runner-inner / runner-inner-child). All fields are exported so the default encoding/json round-trip is exact.

type StreamChunk

type StreamChunk struct {
	Type     string `json:"type"`
	Data     string `json:"data,omitempty"`
	ExitCode int    `json:"exit_code,omitempty"`
}
type Symlink struct {
	LinkPath   string // absolute path inside sandbox (where the symlink is created)
	LinkTarget string // what the symlink points to (resolved relative to LinkPath)
}

Symlink describes a symlink to create inside the sandbox.

type TokenContext

type TokenContext struct {
	JobID             string
	TaskID            string
	ProjectID         string
	WorkspaceID       string
	AllowedProjectIDs []string
	Role              string
	// ProjectDir is the project's host-side work directory. Independent of
	// spec.Visibility.ProjectDir (which drives sandbox mount layout and is
	// intentionally empty for gate jobs): host-side operations the broker
	// performs on behalf of the sandbox (git binding, host-command cwd) have
	// their own notion of "which project are we operating on" that doesn't
	// care whether the sandbox itself can see the tree.
	ProjectDir  string
	WorktreeDir string
	// WorkspacePeers maps peer project IDs to their host-side work directories.
	// Used by the broker to validate git clone --local source paths: only paths
	// within a known peer project are permitted as clone sources.
	WorkspacePeers map[string]string
}

func (TokenContext) AllowsProject

func (c TokenContext) AllowsProject(projectID string) bool

Directories

Path Synopsis
Package brokerclient holds the low-level transport for talking to the boid broker over its UNIX socket, plus a self-contained JobDone helper.
Package brokerclient holds the low-level transport for talking to the boid broker over its UNIX socket, plus a self-contained JobDone helper.
Package runner is the go-native sandbox runner.
Package runner is the go-native sandbox runner.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL