Sandbox access
By default a seat's sandbox reaches only the platform: no internet, no cluster services, no credentials. Access profiles grant practical access: binaries, hosts, IP ranges, GitHub repositories and a browser. Each part of a profile is a plugin with its own enforcement, and a seat gets the union of the profiles named on the seat and on its teams.
access_profiles = {
github_engineer = {
tools = { binaries = ["git", "gh"] }
egress = { hosts = ["pypi.org", "files.pythonhosted.org"] }
github = { connection = "github", repos = ["acme/sandbox"],
permissions = { contents = "write", pull_requests = "write" }, delivery = "sandbox" }
}
internal_db = { network = { rules = [{ cidr = "10.0.5.0/24", ports = [5432] }] } }
web = { browser = { session = { connection = "github_web" } } }
}
seats = {
engineer = { …, access_profiles = ["github_engineer", "internal_db"] }
}
teams = {
engineering = { …, access_profiles = ["web"] } # every member
}
Plugins
| Plugin | What it grants | How it is enforced | Changes apply |
|---|---|---|---|
tools | Binaries the seat needs, e.g. git, gh, curl. | The seat runner checks they are on the image's PATH before the harness starts. A missing binary stops the seat with a reason; nothing is installed at run time. | New Pod |
egress | Hosts: example.com, *.example.com (subdomains only), or host:port. Without a port, 443 and 80. | The egress gateway matches the CONNECT host and port, and requires the TLS server name to equal the host. Plain HTTP is matched on its host. | Live: within seconds, open connections included |
network | Direct connections to IP ranges, optionally limited to ports and a protocol (tcp, udp, sctp). | NetworkPolicy ipBlock rules, which every NetworkPolicy implementation enforces. Host names cannot be expressed this way; use egress. | New Pod |
github | Repositories and permissions (contents, pull_requests, issues, metadata: read or write) through a github connection. | See GitHub. | Repositories and permissions live; switching delivery needs a new Pod |
browser | A headless Chromium as tools (browser_navigate, browser_click, …), optionally signed in. | Its traffic goes through the egress gateway, so the seat's egress hosts decide what it can open. The browser closes when the grant goes. | New Pod |
Readiness never claims access that is not in effect. A seat's AccessApplied condition is true only when its Pod runs the configuration revision that contains its tools, image, NetworkPolicy and proxy settings. Until then it says what it is waiting for, and if the seat cannot start, for example because a binary is missing, it says why.
The egress gateway
Hostname egress needs the gateway. It is off by default; enable it in the platform stage with enable_egress = true. A seat whose profiles need it stays blocked, with that reason, while it is off.
- Path. Harness processes get
HTTPS_PROXYandHTTP_PROXYpointing at a proxy in the seat runner. The runner forwards each connection to the gateway with the seat's projected token, read afresh for every connection.git,gh,curl, Python and Node tools honour these variables, and the browser is started with the proxy. - Identity. The gateway asks the platform for the seat's current access with that token. It holds no credentials and no Kubernetes permissions, and never trusts Pod IP addresses.
- Network. The seat's NetworkPolicy allows the gateway only for seats that use it. Everything else stays denied, so a process that ignores the proxy settings reaches nothing.
- Revocation. Removing a host or a profile applies within seconds without restarting the seat. The gateway re-checks open tunnels every few seconds and closes those no longer allowed, so revocation covers connections already open. A retired seat's token stops authenticating, so all its tunnels close.
- Denials. A refused request gets
403naming the host and the seat ("no access profile grants it"). It is recorded as anegress_deniedevent on the seat's run, visible in the console. Closed tunnels are recorded asegress_revoked.
GitHub
Declare a connection with adapter github. Its secret holds either a GitHub App (app_id, installation_id, private_key; recommended) or a fine-grained personal access token (token). endpoint_ref points at GitHub Enterprise's API, for example https://ghe.example.com/api/v3.
Platform delivery (default)
The seat calls repository operations with connections.invoke: repo.read, pull_request.read, pull_request.create, issue.read and issue.comment, as the permissions allow and only on the granted repositories. They go through the operation ledger like any connector operation, and the credential never enters the sandbox.
Sandbox delivery
With delivery = "sandbox", git and gh in the sandbox work with GitHub directly. GitHub's hosts are allowed through the egress gateway, or the connection's own endpoint for GitHub Enterprise. The seat runner sets git's credential helper to steadmesh-tools credential git, and gh runs through a wrapper that fetches a token for each invocation. Every fetch is recorded as a credential_issued event, with the repositories and permissions but never the token.
- With a GitHub App, each credential is an installation token limited to the granted repositories and permissions, valid for one hour.
- With a personal access token, the token itself is delivered. It is only as narrow as you made it.
The credential boundary. A credential delivered to the sandbox can be read by the agent and by any process in the seat's Pod. Until it expires or is revoked, it can be used against any destination the seat's egress allows. Steadmesh keeps credentials out of Terraform plans and state, CRDs, manifests, Pod specs, images, instruction text and logs, and records every delivery. Prefer platform delivery when the seat does not need git or gh itself.
Revocation
- Through Steadmesh, immediately: removing the grant stops new credentials (
credential_deniedevents), platform operations on those repositories are refused, and the egress gateway closes open connections to GitHub hosts that are no longer allowed. - Credentials already delivered: GitHub App installation tokens are revoked at GitHub when a sync removes the seat's grant. Steadmesh cannot invalidate a personal access token: rotate or revoke it at GitHub. Rotation is picked up without Terraform (see Rotating credentials).
Browser
The browser plugin needs a seat image with Chromium and Playwright MCP: the -browser variants. Releases publish ghcr.io/darcys22/steadmesh/seat-pi-browser (the platform module's seat_images["pi-browser"]); build one locally with build/build.sh seat-pi-browser. The seat's harness gets a browser MCP server, steadmesh-tools browser-mcp, which runs Playwright MCP headless and isolated through the egress gateway.
Signed-in sessions. A personal access token cannot create a browser session. To browse as an account, declare a connection with adapter browser_session whose secret key storage_state holds a Playwright storage state (cookies and local storage) for a dedicated test account, and name it in browser.session. Produce the state with a manual login, for example npx playwright codegen --save-storage=state.json https://github.com/login. Then store state.json in the Secret or Vault entry. It is delivered to the sandbox, with the boundary stated above.
Revocation. When the grant goes, the browser is killed within a few seconds and a tool call in progress fails. The gateway also closes its connections. Cookies already delivered stay valid at the website until you log the account out or replace the session. Rotating the secret is picked up without Terraform; the browser loads the new state when it next starts.
Seat images
| Image | Includes |
|---|---|
seat-claudecode, seat-codex, seat-pi | The harness; git 2.39, gh 2.23 (with the credential wrapper) and curl 7.88 from Debian bookworm |
seat-pi-browser (any <seat image>-browser) | The above, plus Chromium and Playwright MCP 0.0.83 |
seat-fake | The deterministic test harness only; no tools |
Adding a sandbox plugin
- Add the plugin's configuration to
spec.AccessProfileand to the provider schema; runmake generateandmake provider-docs. - Implement
access.Plugininpkg/access.Checkvalidates the configuration with exact paths;Applyadds the plugin's contribution to the seat'sSeatAccess. Decide which parts need a new Pod: they belong inPodPart, so they change the configuration revision. Live parts must be enforced where the platform or gateway reads them at request time. - Enforce it where it acts: the Pod or NetworkPolicy (
runtime/kube), the seat runner (cmd/seat-runner/access.go), the platform (/v1/access,/v1/credentials) or the gateway. If it cannot be enforced on a cluster, fail validation with a clear reason rather than claim it. - Record denials and deliveries as events, document the revocation behaviour and credential boundary, and add unit and e2e tests.