Documentation
¶
Overview ¶
Package sandbox is the agent sandbox: the policy beekeeper renders into Claude Code's managed settings, which Anthropic's sandbox runtime enforces on every command of every session on the machine, and the allow lists beekeeper's PreToolUse hook holds the harness's own file tools to.
The home directory is denied and only what a session needs is re-allowed, so a credential path nobody listed stays unreadable. Egress goes through beekeeper's egress proxy alone, which holds it to an allow list and sets the GitHub token's header itself, so no command ever holds the token (egress.go, github.go).
Index ¶
- Constants
- Variables
- func Ask(dir string, req Request, timeout time.Duration) error
- func Authorization(host, token string) string
- func EgressDir(runtimeDir string) string
- func EnterScope(dir, unit string, cp platform.Cap) error
- func GPGProgram(exe string) []byte
- func Holders(procDir string, dev, ino uint64) []int
- func KeepGitHub(ctx context.Context, tok *Token, r GitHubRenewal, ...)
- func ManagedDir() string
- func Origin(procDir string, pid int, keys []string) (string, []string, error)
- func Path() string
- func ProxyKubeconfig(raw []byte, proxy string) ([]byte, bool, error)
- func RemoveMaskedGitHub(runtimeDir string) error
- func SOCKSProxy(getenv func(string) string) (string, error)
- func SameUser(procDir string) func(net.Conn) error
- func Sandboxed(procDir string, pid int) (bool, error)
- func Serve(ctx context.Context, dir, procDir string, tick time.Duration, h Handler) error
- func SpoolDir(stateDir string) string
- func Unconfine(egressDir string)
- func Unheld(getenv func(string) string) bool
- func Unset(egressDir string) []string
- func UnsetShell(egressDir string) string
- func WriteEgress(dir string, ca []byte, exe string) error
- type CA
- type Capper
- type Egress
- type GitHubRenewal
- type Handler
- type Paths
- type Policy
- type Reply
- type Request
- type Token
Constants ¶
const ( // EgressCA is the proxy's CA certificate. EgressCA = "ca.pem" // EgressBundle is the system's roots and the proxy's CA, for the // clients that take one file of roots (Go, OpenSSL, curl, git). EgressBundle = "bundle.pem" // EgressGH is gh's configuration directory: a login whose token is a // fixed word, since the proxy sets the header (GHLogin). EgressGH = "gh" // EgressGPG is git's gpg.program in the sandbox: it runs beekeeper // sandbox gpg, which has the broker sign with the person's key, out of // the sandbox's reach. EgressGPG = "gpg" )
The GitHub token of a sandboxed session is the devctl App's short-lived user token, which the broker holds in its memory alone and the egress proxy puts into the Authorization header of requests to GitHub's hosts (egress.go). The sandbox gets only what trusts and uses that proxy, in the egress directory under the user's runtime directory, which sandboxed commands read but never write:
const ( // OpPing answers whether a broker runs. OpPing = "ping" // OpCapSlot caps a build slot's slice. OpCapSlot = "cap-slot" // OpScope moves the requester into a capped scope. OpScope = "scope" // OpSecret runs a beekeeper secret call on the host, where sops and // op reach their keys, and answers its output and exit code. OpSecret = "secret" // OpKubeconfig writes a held lab lease's kubeconfig, which takes the // container runtime's socket the sandbox closes. OpKubeconfig = "kubeconfig" // OpVault answers whether the broker holds the vault session // (secret.session), never the session itself. OpVault = "vault" // OpGate runs a gated devctl command (beekeeper gate) on the host, // where devctl reads its keychain and the gate starts its units. OpGate = "gate" // OpAgents runs a beekeeper agents start, wake or resume on the host, // where the user's service manager starts the agent's unit. OpAgents = "agents" // OpWatch runs beekeeper watch on the host, where it reads the // installations through the person's kubeconfig, streamed for as long // as its requester runs. OpWatch = "watch" // OpLab creates or tears down a held lab's kind cluster on the host, // where kind reaches the container runtime. OpLab = "lab" // OpSign signs a payload with the person's key on the host, where gpg // reaches its agent: git's gpg.program in the sandbox. OpSign = "sign" // OpPerson runs beekeeper person on the host, where the person's own gh // login reads the org's roster. OpPerson = "person" )
The broker's operations.
const Brokered = "BEEKEEPER_SANDBOX_BROKERED"
Brokered is set, with Env, in a beekeeper secret call the broker runs on the host for a sandboxed session: the call holds its files to the policy's lists and asks no broker itself.
const DropIn = "beekeeper-sandbox.json"
DropIn is the policy's file in Claude Code's managed settings directory.
const Env = "BEEKEEPER_SANDBOX"
Env is set in every session the policy holds, for the hook to know it.
const GHLogin = "beekeeper-egress-proxy"
GHLogin is gh's login in the sandbox: gh needs one to call GitHub, and the proxy replaces the header it sends. It is no secret.
const Runtime = "SANDBOX_RUNTIME"
Runtime is set by the sandbox runtime in every command it holds; the host never has it.
Variables ¶
var ErrNoBroker = errors.New("no beekeeper sandbox broker answered")
ErrNoBroker is a request no broker answered in time.
var FileTools = []string{"Read", "Grep", "Glob", "Edit", "MultiEdit", "Write", "NotebookEdit"}
FileTools are the harness's tools that read or write files outside the sandbox, whose paths the hook holds to the policy.
Functions ¶
func Ask ¶ added in v0.84.0
Ask sends req to the broker serving dir and waits up to timeout for its answer: nil when done, the broker's refusal, or ErrNoBroker.
func Authorization ¶ added in v0.97.0
Authorization is the header value the token goes to host with: git's endpoint on github.com takes Basic only, the API a token.
func EgressDir ¶ added in v0.97.0
EgressDir is the egress directory under the runtime directory, "" without one.
func EnterScope ¶ added in v0.84.0
EnterScope asks the broker to move this process into the capped scope unit, in cp.Slice.
func GPGProgram ¶ added in v0.97.2
GPGProgram is git's gpg.program in the sandbox: git runs it by its path alone, so it is a script that runs exe's sandbox gpg.
func Holders ¶ added in v0.84.0
Holders are the processes of this user under procDir that hold the file dev:ino open.
func KeepGitHub ¶ added in v0.90.0
func KeepGitHub(ctx context.Context, tok *Token, r GitHubRenewal, token func(context.Context) (string, error), say func(string))
KeepGitHub sets tok to what token returns now and then as r says until ctx ends, telling say what happened, never the token. A failed read keeps the token it had, and say names its cause: the bound that cut it, the token func's error, or no token in devctl's answer.
func ManagedDir ¶
func ManagedDir() string
ManagedDir is Claude Code's managed settings directory on this system.
func Origin ¶ added in v0.85.0
Origin is where process pid runs: its working directory and, of its environment, the variables named in keys only.
func Path ¶
func Path() string
Path is where the policy is installed: a drop-in of the managed settings.
func ProxyKubeconfig ¶ added in v0.91.0
ProxyKubeconfig sets proxy on every cluster of the kubeconfig raw whose server is on loopback, and reports whether that changed it.
func RemoveMaskedGitHub ¶ added in v0.97.0
RemoveMaskedGitHub removes the masked token files earlier brokers kept in the runtime directory: the token now stays in the broker.
func SOCKSProxy ¶ added in v0.91.0
SOCKSProxy is the SOCKS5 proxy URL of the sandbox the calling process runs in. A kind lab's API server is on loopback, which every HTTP client exempts from the proxy environment, so the lab's kubeconfig names the sandbox's SOCKS proxy, which ends at beekeeper's egress proxy: an opaque tunnel, held to the allow list (127.0.0.1:<port>).
func SameUser ¶ added in v0.97.0
SameUser refuses a loopback connection whose peer socket another user owns: the proxy adds the GitHub token, so it serves the broker's own user (Claude Code's bridge to the sandbox) and no other account on the machine. The owner comes from the kernel's socket table in procDir.
func Sandboxed ¶ added in v0.91.0
Sandboxed reports whether process pid runs in another mount namespace than procDir's own process: the agent sandbox (bubblewrap) always mounts its own, which no process inside can leave or fake.
func Serve ¶ added in v0.84.0
Serve answers the requests in dir until ctx ends, polling every tick. Each is answered as the one process of this user that holds it open; a request no process or more than one holds is refused. Requests are answered side by side, so a long secret call holds up no capped run.
func SpoolDir ¶ added in v0.84.0
SpoolDir is the spool under beekeeper's state directory, which the policy lets sessions write.
func Unconfine ¶ added in v0.98.0
func Unconfine(egressDir string)
Unconfine drops Unset from this process's environment.
func Unheld ¶ added in v0.98.0
Unheld reports whether an environment carries the policy's variables while no sandbox holds it: Claude Code applied the policy's env but not its sandbox block, so no egress proxy completes the egress login. A brokered call is held by the broker instead.
func Unset ¶ added in v0.98.0
Unset are the variables an unheld environment drops to be the host's again: the policy's egress variables and Env.
func UnsetShell ¶ added in v0.98.0
UnsetShell is Unset as a POSIX shell line.
Types ¶
type CA ¶ added in v0.97.0
type CA struct {
// contains filtered or unexported fields
}
CA is the egress proxy's certificate authority: made in the broker's memory, its key never written anywhere, and name-constrained to the hosts it terminates TLS for, so a client that trusts it trusts it for GitHub's hosts alone.
type Capper ¶ added in v0.84.0
type Capper struct {
// Dir is the broker's spool.
Dir string
// Exe is this binary, whose "sandbox scope" asks the broker for the
// scope and then becomes the command.
Exe string
}
Capper caps the runs of a sandboxed session through the host's broker: the sandbox keeps the service manager out of reach, so the broker caps the slot's slice and moves the command into its scope. The command itself stays in the sandbox.
func (Capper) Adopt ¶ added in v0.84.0
Adopt is the host's: a sandboxed session asks for its scope instead.
func (Capper) Capped ¶ added in v0.84.0
Capped reports whether this process runs in a capped run's scope.
type Egress ¶ added in v0.97.0
type Egress struct {
// Allow are the hosts commands reach (the policy's Domains).
Allow []string
// Inject are the hosts whose requests get the token on port 443.
Inject []string
// Token is the GitHub token now, "" while there is none.
Token func() string
// CA issues the injected hosts' certificates.
CA *CA
// Peer reports whether a connection comes from a process the proxy
// serves; nil serves every one.
Peer func(net.Conn) error
// Say reports a refused or failed connection, never a header.
Say func(string)
// Upstream sends the injected hosts' requests; nil is the default
// transport, its connections held to the allow list's address rules.
Upstream http.RoundTripper
// contains filtered or unexported fields
}
Egress is the sandbox's egress proxy.
type GitHubRenewal ¶ added in v0.150.0
type GitHubRenewal struct {
Every, Bound, ColdBound time.Duration
// Backoff is the wait before the retry after each failed read in a
// row; once spent, the next read is at Every.
Backoff []time.Duration
}
GitHubRenewal is how KeepGitHub reads the token: every Every while reads succeed, a failed one tried again after Backoff's waits before the next tick. A read is cut at Bound while the proxy holds a token, at ColdBound while it holds none: a broker started at boot reads first while devctl may still wait on what the boot has not brought up (the keyring on the session bus, the network), and five minutes without a token is every sandboxed gh call failing.
type Handler ¶ added in v0.84.0
Handler acts on one request of process pid, the request's holder, and answers the reply's output; an error is the refusal.
type Paths ¶
type Paths struct {
Home, ConfigFile, StateDir, LeaseDir, SlotDir, Exe string
// ScanDir is the value scanner's key and index, which no session reads.
ScanDir string
// RuntimeDir is the user's runtime directory ($XDG_RUNTIME_DIR), where
// the broker keeps the egress proxy's CA.
RuntimeDir string
}
Paths are where beekeeper and its configuration live, and the build slots beekeeper run takes.
type Policy ¶
type Policy struct {
// Home is denied for reading, apart from Read and Write.
Home string
// Runtime is the user's runtime directory, denied for reading like
// Home (the container runtime's registry login, the agents' sockets),
// apart from the egress proxy's directory.
Runtime string
// Read are the paths under Home that may be read.
Read []string
// Write are the paths that may be written, and read.
Write []string
// Deny are paths inside Write that may be neither read nor written:
// the value scanner's key and index in beekeeper's state.
Deny []string
// Harness is the harness's config directory, which commands never
// write: Claude Code mounts it writable in the sandbox, so a command
// could leave a file there the harness reads outside it. The file
// tools write its memory and plans through ToolWrite.
Harness string
// ToolWrite are the paths only the harness's file tools may write:
// the sessions' memory and plans, never a command.
ToolWrite []string
// Domains are the hosts commands reach.
Domains []string
// ProxyPort is the egress proxy's port on the host's loopback.
ProxyPort int
// Exe is the beekeeper binary the policy's hook runs.
Exe string
// Vars are the environment variables the policy sets besides Env: a
// symlinked config's target, which the sandbox mounts without the link,
// the egress proxy's CA and gh's and git's way to GitHub.
Vars map[string]string
// Egress is the directory of the egress proxy's CA and gh's login
// (EgressDir), "" without a runtime directory.
Egress string
}
Policy is what an agent session's commands and file tools may read, write and reach. Paths are absolute and clean.
func (Policy) MountPoints ¶ added in v0.101.1
MountPoints creates every path of Deny that does not exist yet: the sandbox mounts each over a writable parent, which it remounts read-only first and cannot create one in, so a missing one fails every session's start.
func (Policy) Readable ¶
Readable reports whether a file tool may read path: outside the home directory, or under a path of Read or Write, or writable.
type Reply ¶ added in v0.85.0
type Reply struct {
Error string `json:"error,omitempty"`
Out string `json:"out,omitempty"`
Err string `json:"err,omitempty"`
Code int `json:"code,omitempty"`
}
Reply is the broker's answer: an empty Error is done, Out, Err and Code are a call's output and exit code (OpSecret).
type Request ¶ added in v0.84.0
type Request struct {
// Op is one of the broker's operations below.
Op string `json:"op"`
// Unit is the scope to put the requester into (OpScope).
Unit string `json:"unit,omitempty"`
// Slice, Max and Swap are the cap (OpCapSlot, OpScope).
Slice string `json:"slice,omitempty"`
Max string `json:"max,omitempty"`
Swap string `json:"swap,omitempty"`
// Args are the beekeeper secret call's arguments after "secret"
// (OpSecret), or the gated devctl command (OpGate).
Args []string `json:"args,omitempty"`
// Wait is how long a gated merge waits for its turn (OpGate).
Wait string `json:"wait,omitempty"`
// Resource is the lab lease whose kubeconfig to write (OpKubeconfig)
// or whose lab to create or tear down (OpLab).
Resource string `json:"resource,omitempty"`
// Input is the call's standard input: the payload to sign (OpSign).
Input []byte `json:"input,omitempty"`
// Stream asks for the call's output while it runs, and ties the call to
// its requester: once no process holds the request any more, the
// broker ends the call.
Stream bool `json:"stream,omitempty"`
// contains filtered or unexported fields
}
Request is one ask of the broker.