Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sbox

Multi-tenant credential-isolation sandbox. Each user gets their own container with an R2-backed /workspace, OAuth tokens stored in OpenBao and stamped onto outbound API calls by an MITM proxy, and a shared CLI toolbox the operator manages.

┌────────────┐         ┌───────────────────────────────────────┐
│   sbox     │  HTTPS  │              sboxd (daemon)            │
│   CLI      │◄───────►│   Bao auth │ FUSE mount │ TLS proxy   │
└────────────┘         │   per tenant: docker container         │
                       │     /workspace ← R2 (per-tenant)       │
                       │     /opt/sbox  ← R2 (operator-shared)  │
                       └───────────────────────────────────────┘
                                          ▲
                                          │ docker exec
                                          ▼
                            ┌─────────────────────────┐
                            │  agent (per-tenant)     │
                            │  shells out to gh,      │
                            │  linear, gws, ...       │
                            └─────────────────────────┘

What you get:

  • Per-tenant isolation — each tenant has its own docker container and its own R2 prefix. No tenant can see another's files via path traversal, the FUSE mount, or the HTTP API.
  • No credential exposure — OAuth tokens never leave the daemon. The proxy injects Authorization headers on outbound calls based on the tenant identity attached to the proxy listener.
  • Dynamic everything — providers (OAuth dialect), skills (markdown docs), and CLI binaries are all pushed live without touching the daemon code or restarting containers.

Codebase layout

Package What it does
cmd/sbox the sbox CLI binary (cobra command tree)
cmd/agent the per-tenant agent that runs in each container
internal/daemon HTTP API: auth, OAuth, files, skills, admin tools, sandbox lifecycle
internal/sboxfs abstract object-storage FS, R2 backend, Prefixed scoper, FUSE adapter
internal/skills catalog at _catalog/, per-tenant install state
internal/provider YAML provider definitions (OAuth + injection rules)
internal/proxy MITM TLS proxy that stamps creds onto outbound requests
internal/vault OpenBao client (auth, KV, policy)
internal/cli CLI command implementations

Setting up the daemon

The daemon (sboxd) needs:

  1. OpenBao running and unsealed (token in BAO_TOKEN, addr in BAO_ADDR)
  2. R2 bucket + access key (R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, optional R2_ENDPOINT)
  3. fuse3 installed and user_allow_other in /etc/fuse.conf
  4. docker for spawning per-tenant containers
  5. The sbox-agent:latest docker image built (./examples/agent/build.sh)

Drop a systemd unit (see deploy/sboxd.service) and start with:

systemctl start sboxd

Reach the daemon over a Cloudflare Tunnel or any public HTTPS:

SBOX_PUBLIC_URL=https://sbox.example.com sboxd ...

CLI tutorial

Authenticate

sbox auth login --remote https://sbox.example.com --user alice
# password prompt → token saved to ~/.config/sbox/auth.json
sbox whoami

Files (per tenant)

/workspace inside the container is the same R2 prefix the CLI manipulates here:

sbox files put notes/today.md ./notes.md
sbox files ls
sbox files get notes/today.md
sbox files rm notes/today.md

Skills

Catalog of markdown docs the agent reads to learn capabilities:

sbox skills list           # operator's catalog + your install state
sbox skills install gmail  # copy from catalog into your /workspace/skills/
sbox skills sync           # pull all catalog updates + new skills
sbox skills install my-thing ./local-dir   # custom upload (immune to sync)
sbox skills uninstall gmail

Login to a provider (any OAuth provider in the registry)

sbox login google
sbox login github
sbox login slack
sbox login notion
# whatever YAML the operator dropped in providers.d/ — just works

Chat with your agent

sbox chat "list my unread emails"
sbox chat -i              # interactive REPL
sbox chat --history       # dump conversation
sbox chat --reset         # clear history

Operator workflow: adding a new integration

Three artifacts. Three commands. No daemon code, no container restart.

mything/
├── mything.yaml          # OAuth provider (auth/inject config)
├── bin/mything           # binary: drops into /opt/sbox/bin/<name>
└── skills/mything/SKILL.md   # markdown the agent reads on demand
# 1. provider — daemon hot-loads it
sbox provider add ./mything/mything.yaml

# 2. binary — visible to every tenant container at /opt/sbox/bin/mything
sbox admin tools put bin/mything ./mything/bin/mything

# 3. skill — replicated into each tenant on `sbox skills sync`
sbox admin skills push ./mything/skills

Tenant side:

sbox skills sync     # pulls the new skill into /workspace/skills/
sbox login mything   # OAuth dance against the third party

The agent's system prompt is regenerated on every message — the skill + binary appear automatically the next time the agent sends a message. No daemon restart, no agent restart, no docker pull.

Provider YAML reference

name: <provider-name>             # also the URL path (sbox login <name>)
kind: oauth2                      # or "static" for API-key providers
auth:
  flow: loopback
  auth_url: https://example.com/oauth/authorize
  token_url: https://example.com/oauth/token
  client_id: "..."
  client_secret: "..."
  default_scopes: [scope_a, scope_b]
  extra_auth_params:              # provider quirks (Google offline, Slack user_scope, ...)
    access_type: offline
    prompt: consent
inject:
  destinations: ["api.example.com", "*.example.com"]
  mode: header                    # or "env"
  header: Authorization
  value: "Bearer {access_token}"

That's the only knob the operator needs to add a new OAuth provider — no Go code, no daemon restart. Hot-load via sbox provider add.

Admin commands

sbox admin tools put bin/<name> ./<name-linux>      # operator-shared binary
sbox admin tools ls
sbox admin tools rm bin/<name>

sbox admin skills push ./skills/                    # tarball of skills/<name>/
sbox admin skills default a,b,c                    # auto-install set for new tenants

Multi-tenancy guarantees

  • Auth — every HTTP call hits OpenBao lookup-self (cheap, ~1ms). Identity is attached to request context.
  • Tenant prefix — all storage paths are derived server-side from id.TenantName(). Clients can't name themselves.
  • Path traversalPrefixed.full() rejects any key containing .. or starting with / before any backend call.
  • FUSE per tenant — each tenant's container has its own /workspace mount; no inode resolves outside the prefix.
  • Container isolation — separate docker container per tenant; only this tenant's FUSE mount + the read-only operator-shared mount are bind-mounted in.
  • Proxy injectionInjectionsFor(sandboxName, host) looks up the cred for this tenant's sandbox before stamping headers.

The shared /opt/sbox mount carries only operator-pushed bytes that contain no tenant data.

Status

This is a working prototype. Not production-hardened. Things known to work: per-tenant FUSE, R2 backend, generic OAuth (google/slack/github tested), shared admin tools, dynamic system-prompt discovery, skill catalog with versioned sync.

Things deliberately punted: per-tenant R2 keys (one bucket key for the whole daemon — compromise it and you have everyone's data), quotas, audit logs, anything resembling fine-grained authorization beyond "is operator?".

About

Multi-tenant credential-isolation sandbox: per-tenant containers with R2-backed FUSE workspaces, OAuth tokens stamped onto outbound calls by an MITM proxy, and a dynamic skill+CLI catalog.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages