Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
# Development (optional)
# AGENT_VAULT_DEV_MODE=false # when true, allows internal/localhost hosts in proposals

# Sandbox mode for `agent-vault vault run` (optional)
# process (default, cooperative) | container (non-cooperative Docker sandbox with iptables egress lock)
# AGENT_VAULT_SANDBOX=process

# Observability (optional)
# AGENT_VAULT_LOG_LEVEL=info # info (default) | debug — debug emits one line per proxied request (no secret values)

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ make docker # Multi-stage Docker image; data persisted at /data/.agent-vau
- Vault role: `proxy` < `member` < `admin`. Proxy can use the proxy and raise proposals; member can manage credentials/services; admin can invite humans.
- **KEK/DEK key wrapping**: A random DEK (Data Encryption Key) encrypts credentials and the CA key at rest (AES-256-GCM). If a master password is set, Argon2id derives a KEK (Key Encryption Key) that wraps the DEK; changing the password re-wraps the DEK without re-encrypting credentials. If no password is set (passwordless mode), the DEK is stored in plaintext — suitable for PaaS deploys where volume security is the trust boundary. Login uses email+password or Google OAuth. The first user to register becomes the instance owner and is auto-granted vault admin on `default`.
- **Agent skills are the agent-facing contract.** [cmd/skill_cli.md](cmd/skill_cli.md) and [cmd/skill_http.md](cmd/skill_http.md) are embedded into the binary, installed by `vault run`, and served publicly at `/v1/skills/{cli,http}`. They are the authoritative reference for what agents can do.
- **Two sandbox modes for `vault run`** (selected via `--sandbox` or `AGENT_VAULT_SANDBOX`): `process` (default, cooperative — fork+exec with `HTTPS_PROXY` envvars) and `container` (non-cooperative — Docker container with iptables egress locked to the Agent Vault proxy). Container mode lives in [internal/sandbox/](internal/sandbox/) with an embedded Dockerfile + init-firewall.sh + entrypoint.sh, built on first use and cached by content hash.

## Where to look for details

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ agent-vault vault run -- claude

The agent calls APIs normally (e.g. `fetch("https://api.github.com/...")`). Agent Vault intercepts the request, injects the credential, and forwards it upstream. The agent never sees secrets.

For **non-cooperative** sandboxing — where the child physically cannot reach anything except the Agent Vault proxy, regardless of what it tries — launch it in a Docker container with egress locked down by iptables:

```bash
agent-vault vault run --sandbox=container -- claude
```

See [Container sandbox](https://docs.agent-vault.dev/guides/container-sandbox) for the threat model and flags.

### SDK — sandboxed agents (Docker, Daytona, E2B)

For agents running inside containers, use the SDK from your orchestrator to mint a session and pass proxy config into the sandbox:
Expand Down
94 changes: 55 additions & 39 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"
"syscall"

"github.com/Infisical/agent-vault/internal/sandbox"
"github.com/Infisical/agent-vault/internal/session"
"github.com/Infisical/agent-vault/internal/store"
"github.com/charmbracelet/huh"
Expand All @@ -25,6 +26,10 @@ var skillCLI string
//go:embed skill_http.md
var skillHTTP string

// sandboxMode is enum-typed so `--sandbox=foo` fails at flag-parse time
// with the allowed set, rather than deep inside RunE.
var sandboxMode SandboxMode

var runCmd = &cobra.Command{
Use: "run [flags] -- <command> [args...]",
Short: "Wrap an agent process with Agent Vault access",
Expand Down Expand Up @@ -53,6 +58,24 @@ Example:
Args: cobra.MinimumNArgs(1),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
// 0. Resolve sandbox mode and validate flag compatibility before any
// network I/O — the user sees conflicts immediately, not after
// a slow session-mint round-trip.
mode := sandboxMode
if mode == "" {
if v := os.Getenv("AGENT_VAULT_SANDBOX"); v != "" {
if err := mode.Set(v); err != nil {
return fmt.Errorf("AGENT_VAULT_SANDBOX: %w", err)
}
}
}
if mode == "" {
mode = SandboxProcess
}
if err := validateSandboxFlagConflicts(cmd, mode); err != nil {
return err
}

// 1. Load the admin session from agent-vault auth login.
sess, err := ensureSession()
if err != nil {
Expand All @@ -78,6 +101,10 @@ Example:
return err
}

if mode == SandboxContainer {
return runContainer(cmd, args, scopedToken, addr, vault)
}

// 4. Resolve the target binary.
binary, err := exec.LookPath(args[0])
if err != nil {
Expand Down Expand Up @@ -276,23 +303,19 @@ func fetchUserVaults(addr, token string) ([]string, error) {
return names, nil
}

// mitmInjectedKeys is the set of env keys augmentEnvWithMITM manages on
// the child. Any pre-existing occurrence inherited from os.Environ() must
// be stripped before the new values are appended — POSIX getenv returns
// the *first* match in C code paths (glibc, curl, libcurl-backed Python),
// so a stale corporate HTTPS_PROXY from the parent shell would otherwise
// silently win and the MITM route would be bypassed entirely.
var mitmInjectedKeys = map[string]struct{}{
"HTTPS_PROXY": {},
"NO_PROXY": {},
"NODE_USE_ENV_PROXY": {},
"SSL_CERT_FILE": {},
"NODE_EXTRA_CA_CERTS": {},
"REQUESTS_CA_BUNDLE": {},
"CURL_CA_BUNDLE": {},
"GIT_SSL_CAINFO": {},
"DENO_CERT": {},
}
// mitmInjectedKeys is the keyset that BuildProxyEnv emits. Any
// pre-existing occurrence inherited from os.Environ() must be stripped
// before the new values are appended — POSIX getenv returns the *first*
// match in C code paths (glibc, curl, libcurl-backed Python), so a stale
// corporate HTTPS_PROXY from the parent shell would otherwise silently
// win and the MITM route would be bypassed entirely.
var mitmInjectedKeys = func() map[string]struct{} {
m := make(map[string]struct{}, len(sandbox.ProxyEnvKeys))
for _, k := range sandbox.ProxyEnvKeys {
m[k] = struct{}{}
}
return m
}()

// stripEnvKeys returns env with every entry whose key (the part before
// '=') appears in keys removed. Case-sensitive, matching how the kernel
Expand Down Expand Up @@ -359,30 +382,16 @@ func augmentEnvWithMITM(env []string, addr, token, vault, caPath string) ([]stri
mitmHost = h
}
}
scheme := "http"
if mitmTLS {
scheme = "https"
}
proxyURL := (&url.URL{
Scheme: scheme,
User: url.UserPassword(token, vault),
Host: fmt.Sprintf("%s:%d", mitmHost, port),
}).String()

env = stripEnvKeys(env, mitmInjectedKeys)
// CA trust variables must stay in sync with buildProxyEnv() in
// sdks/sdk-typescript/src/resources/sessions.ts.
env = append(env,
"HTTPS_PROXY="+proxyURL,
"NO_PROXY=localhost,127.0.0.1",
"NODE_USE_ENV_PROXY=1",
"SSL_CERT_FILE="+caPath,
"NODE_EXTRA_CA_CERTS="+caPath,
"REQUESTS_CA_BUNDLE="+caPath,
"CURL_CA_BUNDLE="+caPath,
"GIT_SSL_CAINFO="+caPath,
"DENO_CERT="+caPath,
)
env = append(env, sandbox.BuildProxyEnv(sandbox.ProxyEnvParams{
Host: mitmHost,
Port: port,
Token: token,
Vault: vault,
CAPath: caPath,
MITMTLS: mitmTLS,
})...)
return env, port, true, nil
}

Expand Down Expand Up @@ -440,5 +449,12 @@ func init() {
runCmd.Flags().Int("ttl", 0, "Session TTL in seconds (300–604800; default: server default 24h)")
runCmd.Flags().Bool("no-mitm", false, "Skip HTTPS_PROXY/CA env injection for the child (explicit /proxy only)")

runCmd.Flags().Var(&sandboxMode, "sandbox", "Sandbox mode: process (default) or container")
runCmd.Flags().String("image", "", "Container image override (requires --sandbox=container)")
runCmd.Flags().StringArray("mount", nil, "Extra bind mount src:dst[:ro] (repeatable; requires --sandbox=container)")
runCmd.Flags().Bool("keep", false, "Don't pass --rm to docker (requires --sandbox=container)")
runCmd.Flags().Bool("no-firewall", false, "Skip iptables egress rules inside the container (requires --sandbox=container; debug only)")
runCmd.Flags().Bool("home-volume-shared", false, "Share /home/claude/.claude across invocations (requires --sandbox=container); default is a per-invocation volume, losing auth state but avoiding concurrency corruption")

vaultCmd.AddCommand(runCmd)
}
Loading