Documentation
¶
Index ¶
- Constants
- func BuildToolPermissionRequest(req PermissionRequest, opts ToolPermissionOptions) (types.ToolPermissionRequest, error)
- func EnvSliceToMap(env []string) map[string]string
- func ErrorForDecision(result DecisionResult) error
- func ErrorForPermissionResult(result types.PermissionResult, fallbackReason string) error
- func RegisterRemoteExecutor(sessionID string, executor Executor)
- func ResolveToolPermission(ctx context.Context, checker types.CanUseToolFn, req PermissionRequest, ...) (types.PermissionResult, error)
- func UnregisterRemoteExecutor(sessionID string)
- type AccessKind
- type ApprovalRequiredError
- type ApprovalScope
- type CommandPolicy
- type Context
- type Decision
- type DecisionResult
- type DockerExecutor
- type DockerExecutorConfig
- type DockerRunOptions
- type EnvironmentKind
- type Executor
- type ExecutorConfig
- type FilesystemPolicy
- type NoopExecutor
- type PathDecision
- type PermissionDecision
- type PermissionDeniedError
- type PermissionRequest
- type RunRequest
- type RunResult
- type ToolAccessPreview
- type ToolPermissionOptions
Constants ¶
const ( MetadataRequestKey = "sandbox_request" MetadataPreviewKey = "sandbox_preview" )
Variables ¶
This section is empty.
Functions ¶
func BuildToolPermissionRequest ¶
func BuildToolPermissionRequest(req PermissionRequest, opts ToolPermissionOptions) (types.ToolPermissionRequest, error)
BuildToolPermissionRequest converts a normalized sandbox request into the runtime permission request expected by the engine.
func EnvSliceToMap ¶
enviroToMap converts an os.Environ()-style slice to a map. Exported for use in tests.
func ErrorForDecision ¶
func ErrorForDecision(result DecisionResult) error
ErrorForDecision converts a normalized decision into a conventional Go error.
func ErrorForPermissionResult ¶
func ErrorForPermissionResult(result types.PermissionResult, fallbackReason string) error
ErrorForPermissionResult converts a runtime permission result into a conventional Go error.
func RegisterRemoteExecutor ¶ added in v0.2.3
RegisterRemoteExecutor attaches executor as sessionID's remote execution backend. A re-registration (e.g. a reconnect) replaces any previous one.
func ResolveToolPermission ¶
func ResolveToolPermission( ctx context.Context, checker types.CanUseToolFn, req PermissionRequest, opts ToolPermissionOptions, ) (types.PermissionResult, error)
ResolveToolPermission runs the normalized request through the active permission resolver.
func UnregisterRemoteExecutor ¶ added in v0.2.3
func UnregisterRemoteExecutor(sessionID string)
UnregisterRemoteExecutor detaches sessionID's remote execution backend — subsequent bash calls for that session fall back to local execution.
Types ¶
type AccessKind ¶
type AccessKind string
AccessKind describes the resource access being requested.
const ( AccessRead AccessKind = "read" AccessWrite AccessKind = "write" AccessCreate AccessKind = "create" AccessDelete AccessKind = "delete" AccessSearch AccessKind = "search" AccessExecute AccessKind = "execute" AccessNetwork AccessKind = "network" AccessEscalate AccessKind = "escalate" )
type ApprovalRequiredError ¶
type ApprovalRequiredError struct {
Reason string
}
ApprovalRequiredError is returned when an action is valid but needs approval.
func (*ApprovalRequiredError) Error ¶
func (e *ApprovalRequiredError) Error() string
type ApprovalScope ¶
type ApprovalScope string
ApprovalScope controls how long an approval grant should remain valid.
const ( ApprovalScopeToolCall ApprovalScope = "tool_call" ApprovalScopeTurn ApprovalScope = "turn" ApprovalScopeSession ApprovalScope = "session" )
type CommandPolicy ¶
type CommandPolicy struct {
// contains filtered or unexported fields
}
CommandPolicy is the single authoritative command safety classifier. It replaces the scattered PermissionValidator in the bash package.
Two-tier model (mirrors Codex):
- IsKnownSafe: explicit allowlist → bypass approval entirely
- Evaluate: deny/ask/allow based on danger fragments and command type
func NewDefaultCommandPolicy ¶
func NewDefaultCommandPolicy() *CommandPolicy
func (*CommandPolicy) Evaluate ¶
func (p *CommandPolicy) Evaluate(command string) DecisionResult
Evaluate returns the policy decision for a command.
Order of evaluation:
- Known-safe allowlist → Allow (no approval prompt needed)
- Shell wrapper → evaluate composed inner
- Deny fragment match → Deny
- Ask-command list → Ask
- Command type classification: write/vcs/unknown → Ask, read/search/state → Allow
func (*CommandPolicy) IsKnownSafe ¶
func (p *CommandPolicy) IsKnownSafe(command string) bool
IsKnownSafe returns true when every sub-command in the expression is on the explicit safe allowlist, meaning the entire command can be executed without any approval prompt.
Mirrors Codex's is_known_safe_command. If the command is a shell wrapper (bash -c "…") the inner script is split into segments and each is checked.
type Context ¶
type Context struct {
WorkingDirectory string
WorkspaceRoot string
AdditionalRoots []string
Environment EnvironmentKind
SandboxEnabled bool
}
Context carries the execution boundary relevant to sandbox decisions.
func (Context) ResolvePath ¶
ResolvePath resolves a candidate path according to the current execution context. When a WorkspaceRoot is configured, relative paths are anchored to the root and validated to stay inside it. Absolute paths bypass workspace containment so the user can approve writes to /tmp or other out-of-workspace directories — the FilesystemPolicy dangerous-prefix check still applies downstream.
type DecisionResult ¶
DecisionResult is the normalized output of a policy decision.
type DockerExecutor ¶ added in v0.2.3
type DockerExecutor struct {
// contains filtered or unexported fields
}
DockerExecutor runs commands inside isolated Docker containers via the docker CLI (os/exec with structured argv — request text is passed as a single argv element to `sh -c`, never interpolated into a shell string; same safety pattern as NoopExecutor.Run).
Each environment (RunRequest.Docker.EnvironmentID) gets one long-lived container, created lazily on first use and reused via `docker exec` for every subsequent call — this is what makes state (installed packages, background processes, …) persist across calls within the same environment, without spinning a fresh container per command.
Isolation properties enforced: filesystem and process-namespace isolation via the container boundary, hard CPU/memory caps (--cpus/--memory), and optional full network isolation (--network=none). These are the two properties an earlier Dagger-SDK-based design could not provide — Dagger's public Go API (as of v0.21) exposes neither per-container resource limits nor a way to disable network access for a WithExec step.
Scope: this backs local, single-user desktop sandboxing — not a multi-tenant sandbox-as-a-service platform. Multi-tenant concerns (Kubernetes/distributed scheduling, an ingress gateway for exposing sandbox-hosted services to third parties, a standalone credential vault, a formal third-party-runtime lifecycle protocol) are deliberately NOT built here — if a use case ever needs that, the right move is a sandbox.Executor adapter for an existing open-source platform (e.g. OpenSandbox) rather than reimplementing a worse copy of it inside seshat.
Deliberately not attempted — --cap-drop=ALL: Docker's own default capability set is already reduced (not ALL), and dropping further risks breaking legitimate tools (e.g. CAP_SETUID/CAP_SETGID, which privilege-dropping install scripts rely on) for a security benefit with no articulated compromise scenario it would prevent that NET_RAW/NET_ADMIN (already dropped below) and runtime selection (below) don't already cover.
Done:
- Strong isolation runtime selection (DockerExecutorConfig.Runtime, e.g. "runsc" for gVisor) — plumbed and tested against `runc` as a stand-in (this machine has no gVisor installed to verify against for real: `docker info` lists only runc/nvidia-container-runtime). Actually installing and registering gVisor with the Docker daemon is an operator/host setup step outside what Go code here can do; Healthy(ctx) refuses to proceed if the configured Runtime isn't registered, rather than silently falling back to plain runc.
- Orphan container cleanup (labels + reaper on construction, see reapOrphanContainers) — a crashed host process's containers are cleaned up by the next one, not left running indefinitely.
- NET_RAW/NET_ADMIN dropped by default (--cap-drop) — narrows the capability set Docker already reduces further, with no realistic legitimate-tool breakage (package managers/builds/git all use ordinary TCP, not raw sockets or network device configuration).
- Port tunneling (DockerExecutorConfig.PublishPorts / RunResult.Endpoints) — useful even for local usage (e.g. previewing a dev server an agent started inside the sandbox). Declared once at the environment level, not per RunRequest, because Docker only allows publishing ports at container-creation time.
- Git-tracked history of file changes (DockerExecutorConfig. TrackFileChanges) — a product feature (undo/diff of what an agent did), not a security property, but implemented: snapshots RunRequest.WorkDir into a separate bare shadow repo (never the project's own .git, which is explicitly excluded) after any Run call that actually changed something, keyed by a hash of the absolute WorkDir path so history survives across environment/process restarts for the same project directory. Off by default (requires git on the host; Healthy(ctx) fails loudly rather than silently skipping tracking if TrackFileChanges is set but git can't be found).
Already true today, not a gap: RunRequest.Env / DockerExecutorConfig.Env are the only environment variables that reach the container — unlike NoopExecutor.Run (which inherits the full host process environment via cmd.Environ()), DockerExecutor never implicitly forwards os.Environ() into the sandbox, so host-side API keys and other secrets the seshat process holds are not exposed to a sandboxed command unless explicitly passed.
func (*DockerExecutor) Close ¶ added in v0.2.3
func (e *DockerExecutor) Close() error
Close stops and removes every environment container this executor has created (--rm at creation makes stop trigger automatic removal). Safe to call multiple times. Host applications should call this at shutdown — DockerExecutor is not wired into any automatic engine-wide shutdown hook yet, so the caller that owns the Executor is responsible for this today.
func (*DockerExecutor) Healthy ¶ added in v0.2.3
func (e *DockerExecutor) Healthy(ctx context.Context) error
Healthy runs `docker version` against the daemon. A non-nil error means Docker isn't installed, isn't running, or isn't reachable — the engine calls this once at startup so the host application can fall back (typically to NoopExecutor with a visible warning) instead of failing every subsequent bash call individually.
func (*DockerExecutor) Kind ¶ added in v0.2.3
func (e *DockerExecutor) Kind() EnvironmentKind
func (*DockerExecutor) Run ¶ added in v0.2.3
func (e *DockerExecutor) Run(ctx context.Context, req RunRequest) (RunResult, error)
type DockerExecutorConfig ¶ added in v0.2.3
type DockerExecutorConfig struct {
// BaseImage is the OCI image used for new environments.
// Default: "ubuntu:24.04"
BaseImage string
// SetupCommands are shell commands run once when an environment's
// container is first created.
// Example: ["apt-get update -y", "apt-get install -y python3 nodejs"]
SetupCommands []string
// WorkDir is the working directory inside the container that
// RunRequest.WorkDir (a host path) is bind-mounted to.
// Default: "/workdir"
WorkDir string
// Env is a set of KEY=VALUE pairs always injected into the container.
Env map[string]string
// NetworkAccess controls whether the container can reach the network.
// Default: true (default Docker bridge network).
// Set to false to run with --network=none.
NetworkAccess bool
// MemoryLimitMB caps container memory (docker run --memory).
// Default: 2048 (2 GiB). 0 means no additional default is applied
// (Docker's own daemon-level default, if any, still applies).
MemoryLimitMB int
// CPULimit caps container CPU (docker run --cpus). Fractional values
// are allowed (e.g. 1.5). Default: 2.0. 0 means no limit is passed.
CPULimit float64
// DockerBinary is the path to the docker CLI. Default: "docker"
// (resolved via PATH).
DockerBinary string
// Runtime selects a non-default OCI runtime (docker run --runtime=…),
// e.g. "runsc" for gVisor. Empty (default) uses Docker's own default
// runtime — plain runc, which shares the host kernel with the
// container. A stronger isolation runtime like gVisor must already be
// installed and registered with the Docker daemon separately (this is
// an operator/host setup step, not something DockerExecutor can do) —
// if Runtime is set but not registered, Healthy(ctx) fails rather than
// silently falling back to running without it, so an explicit request
// for stronger isolation never gets silently downgraded.
Runtime string
// PublishPorts lists container ports to publish to random host ports
// when an environment's container is created (docker run -p). Useful
// for e.g. previewing a dev server an agent starts inside the sandbox.
// Fixed for the lifetime of an environment — declared here rather than
// per RunRequest because Docker only allows publishing ports at
// container-creation time. The actual host-side ports are reported back
// via RunResult.Endpoints on every call to that environment.
PublishPorts []int
// TrackFileChanges enables git-tracked snapshots of RunRequest.WorkDir
// (the host directory bind-mounted into the environment) after every
// Run call that actually changed something. Default: false — this is an
// audit/undo convenience feature, not a security property, and requires
// git on the HOST (not inside the container). If true and git isn't
// found on the host, Healthy(ctx) fails rather than silently tracking
// nothing — same "don't silently downgrade a requested capability"
// contract as Runtime.
//
// Snapshots go into a separate, bare git repository — NOT the
// project's own .git, if it has one — keyed by a hash of the absolute
// WorkDir path (stable across environment/container restarts for the
// same directory, see HistoryDir). The project's own .git, if present,
// is excluded from what gets snapshotted.
TrackFileChanges bool
// HistoryDir is the base directory shadow tracking repos are stored
// under when TrackFileChanges is true. Default (DefaultDockerConfig):
// os.UserConfigDir()/seshat/sandbox-history.
HistoryDir string
// GitBinary is the path to the git CLI used for TrackFileChanges.
// Default: "git" (resolved via PATH).
GitBinary string
}
DockerExecutorConfig holds static configuration for the DockerExecutor. All fields have sensible defaults and can be overridden by the operator.
func DefaultDockerConfig ¶ added in v0.2.3
func DefaultDockerConfig() DockerExecutorConfig
DefaultDockerConfig returns a DockerExecutorConfig with sensible, security-conscious defaults — callers only need to override what they actually want different.
type DockerRunOptions ¶ added in v0.2.3
type DockerRunOptions struct {
// EnvironmentID selects a named, persistent environment (one long-lived
// container reused across calls via `docker exec`). Empty means the
// default session environment.
//
// Port publishing is NOT a per-call option here — Docker only allows
// publishing ports at container-creation time, not on an already-running
// container, and an environment's container may already exist by the
// time a given RunRequest is issued. See
// DockerExecutorConfig.PublishPorts, which is declared once for the
// environment instead.
EnvironmentID string
}
DockerRunOptions carries Docker-specific per-run parameters. These are only meaningful when the active Executor is a DockerExecutor.
type EnvironmentKind ¶
type EnvironmentKind string
EnvironmentKind describes where a tool executes. The policy layer should know this, even if the actual backend runtime (local process, docker, remote host) is implemented elsewhere.
const ( EnvironmentLocal EnvironmentKind = "local" EnvironmentWorktree EnvironmentKind = "worktree" EnvironmentDocker EnvironmentKind = "docker" EnvironmentRemote EnvironmentKind = "remote" EnvironmentUnknown EnvironmentKind = "unknown" )
type Executor ¶
type Executor interface {
// Run executes a command inside the sandbox and streams its output.
// Implementations must respect ctx cancellation and RunRequest.Timeout.
Run(ctx context.Context, req RunRequest) (RunResult, error)
// Kind identifies which backend this is.
Kind() EnvironmentKind
// Healthy returns nil when the executor is ready to accept work.
// A non-nil error means the backend is unavailable (Dagger engine down,
// Docker socket missing, etc). The engine calls this once at startup.
Healthy(ctx context.Context) error
}
Executor is the interface all sandbox backends implement. The engine routes bash/shell execution through the active Executor.
Backends:
- NoopExecutor — runs commands directly on the host OS (default, no isolation)
- DockerExecutor — runs commands inside an isolated Docker container
Selection at startup via ExecutorConfig.Kind. Tools that are read-only (file read, glob, grep) bypass the Executor and go directly to the host FS.
func NewExecutor ¶
func NewExecutor(cfg ExecutorConfig) (Executor, error)
NewExecutor creates the Executor selected by cfg.Kind. Returns an error when the requested backend is unavailable.
func RemoteExecutorFor ¶ added in v0.2.3
RemoteExecutorFor looks up sessionID's registered remote executor, if any.
type ExecutorConfig ¶
type ExecutorConfig struct {
// Kind selects the backend. Defaults to EnvironmentLocal (noop).
Kind EnvironmentKind
// Docker holds Docker-specific configuration.
// Only used when Kind == EnvironmentDocker.
Docker DockerExecutorConfig
}
ExecutorConfig is the configuration passed to NewExecutor at startup.
type FilesystemPolicy ¶
type FilesystemPolicy struct {
// contains filtered or unexported fields
}
FilesystemPolicy centralizes common filesystem access checks.
func NewDefaultFilesystemPolicy ¶
func NewDefaultFilesystemPolicy() *FilesystemPolicy
func (*FilesystemPolicy) EvaluatePath ¶
func (p *FilesystemPolicy) EvaluatePath(ctx Context, path string, access AccessKind) (PathDecision, error)
type NoopExecutor ¶
type NoopExecutor struct{}
NoopExecutor runs commands directly on the host OS with no additional isolation. This is the default executor and mirrors the current behavior of the bash tool before sandboxing was introduced.
Isolation: none. The CommandPolicy and FilesystemPolicy still apply, but there is no OS-level boundary between the agent and the host filesystem.
func NewNoopExecutor ¶
func NewNoopExecutor() *NoopExecutor
NewNoopExecutor returns a NoopExecutor ready to use.
func (*NoopExecutor) Kind ¶
func (e *NoopExecutor) Kind() EnvironmentKind
func (*NoopExecutor) Run ¶
func (e *NoopExecutor) Run(ctx context.Context, req RunRequest) (RunResult, error)
type PathDecision ¶
type PathDecision struct {
DecisionResult
ResolvedPath string
}
PathDecision includes the resolved path for filesystem checks.
type PermissionDecision ¶
type PermissionDecision struct {
Decision Decision
Reason string
Scope ApprovalScope
ApprovedPaths []string
Metadata map[string]any
}
PermissionDecision is the normalized response returned by the sandbox/approval layer.
func (PermissionDecision) IsAllowed ¶
func (d PermissionDecision) IsAllowed() bool
IsAllowed reports whether the request was approved.
type PermissionDeniedError ¶
type PermissionDeniedError struct {
Reason string
}
PermissionDeniedError is returned when the sandbox denies an action outright.
func (*PermissionDeniedError) Error ¶
func (e *PermissionDeniedError) Error() string
type PermissionRequest ¶
type PermissionRequest struct {
ToolName string
Description string
Environment EnvironmentKind
Access AccessKind
Command string
Paths []string
NetworkTargets []string
Justification string
Scope ApprovalScope
Metadata map[string]any
}
PermissionRequest is the normalized request emitted by tools and runtimes when they need a sandbox/approval decision.
func (PermissionRequest) DescriptionText ¶
func (r PermissionRequest) DescriptionText() string
DescriptionText returns a stable approval-friendly description.
func (PermissionRequest) MetadataMap ¶
func (r PermissionRequest) MetadataMap() map[string]any
MetadataMap returns stable metadata for the shared permission pipeline.
func (PermissionRequest) Validate ¶
func (r PermissionRequest) Validate() error
Validate ensures a permission request is structurally usable by the approval pipeline.
type RunRequest ¶
type RunRequest struct {
// Command is the shell command string to execute.
// Passed as-is to the interpreter defined by Shell.
Command string
// Shell specifies the interpreter (default: /bin/sh -c).
// Ignored by DockerExecutor which always uses sh inside the container.
Shell []string
// Env is a set of KEY=VALUE pairs injected into the command environment
// on top of the executor's base environment.
Env map[string]string
// WorkDir is the host working directory for the command. NoopExecutor
// and RemoteExecutor use it directly as the process's cwd. DockerExecutor
// additionally treats it as the bind-mount source for the environment's
// container-side working directory (see DockerExecutorConfig.WorkDir) —
// fixed at the environment's first Run call and reused on every
// subsequent call to the same Docker.EnvironmentID.
WorkDir string
// Stdin is connected to the command's standard input (may be nil).
Stdin io.Reader
// Timeout is the maximum duration for the command.
// Zero means no timeout beyond the parent context deadline.
Timeout time.Duration
// Background requests a fire-and-forget execution (no output captured).
// Used for long-running processes (servers, watchers).
Background bool
// Docker-specific extensions — ignored by NoopExecutor.
Docker *DockerRunOptions
}
RunRequest is the input to Executor.Run.
type RunResult ¶
type RunResult struct {
// Stdout and Stderr capture the command output.
// Empty when Background=true.
Stdout string
Stderr string
// ExitCode is the process exit code. Non-zero indicates failure.
ExitCode int
// Duration is the wall-clock time the command took.
Duration time.Duration
// Cwd is the working directory the command left the environment in.
// Only populated by executors backed by a persistent shell (e.g.
// RemoteExecutor) where a `cd` genuinely changes state for the next
// command; empty for one-shot-per-call executors like NoopExecutor/
// DockerExecutor where WorkDir never changes out from under the caller.
Cwd string
// Endpoints maps container port numbers to "host:port" strings — where
// on the host a service listening on that container port is reachable.
// Populated on every call by DockerExecutor when
// DockerExecutorConfig.PublishPorts is non-empty (the mapping is fixed
// at environment creation, so it's the same on every call for a given
// environment, not something a specific RunRequest controls).
Endpoints map[int]string
}
RunResult is the output of Executor.Run.
type ToolAccessPreview ¶
type ToolAccessPreview struct {
ToolName string `json:"tool_name"`
Environment string `json:"environment,omitempty"`
Access string `json:"access,omitempty"`
Command string `json:"command,omitempty"`
Paths []string `json:"paths,omitempty"`
NetworkTargets []string `json:"network_targets,omitempty"`
Justification string `json:"justification,omitempty"`
}
ToolAccessPreview is the normalized human-facing summary of what a tool is asking to do.
func BuildPreview ¶
func BuildPreview(req PermissionRequest) ToolAccessPreview
BuildPreview converts a permission request into a stable preview payload.
type ToolPermissionOptions ¶
type ToolPermissionOptions struct {
ToolInput map[string]any
ToolUseID string
SessionID types.SessionID
TurnID types.TurnID
PermissionMode types.PermissionMode
WorkingDirectory string
IsToolRunningInSandbox bool
Stage types.ToolPermissionStage
Intent types.ToolPermissionIntent
Metadata map[string]any
}
ToolPermissionOptions carries runtime-specific fields for the shared permission pipeline request.