Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
223de24
feat(network): add allowAllDomains flag to bypass allowlist
smolyn May 27, 2026
4c57da4
feat(filesystem): add denyReadAlways for denies that beat allowRead
smolyn May 27, 2026
bcc1cf9
Merge branch 'gs/allow-all-domains' into ltic-main
smolyn Jun 30, 2026
7d2ec95
Merge branch 'gs/deny-read-always' into ltic-main
smolyn Jun 30, 2026
4cc90d7
chore(ltic): version 0.0.62-ltic.1 + read version from package.json
smolyn Jun 30, 2026
d20d944
feat(network): add allowAllDomains flag to bypass allowlist
smolyn May 27, 2026
963439c
feat(filesystem): add denyReadAlways for denies that beat allowRead
smolyn May 27, 2026
069e1d6
Merge branch 'main' into gs/deny-read-always
smolyn Jul 13, 2026
bccd443
Merge branch 'main' into gs/allow-all-domains
smolyn Jul 13, 2026
593d4cc
Merge upstream/main (v0.0.66) into gs/allow-all-domains
smolyn Jul 20, 2026
d10b1d6
Merge upstream/main (v0.0.66) into gs/deny-read-always
smolyn Jul 20, 2026
7f675d8
Merge gs/allow-all-domains (upstream v0.0.66) into ltic-main
smolyn Jul 20, 2026
dc973ea
Merge gs/deny-read-always (upstream v0.0.66) into ltic-main
smolyn Jul 20, 2026
8839f8d
Merge upstream/main (v0.0.70) into gs/deny-read-always
smolyn Aug 5, 2026
8320cc8
Merge upstream/main (v0.0.70) into gs/allow-all-domains
smolyn Aug 5, 2026
0b84730
Merge gs/allow-all-domains (upstream v0.0.70) into ltic-main
smolyn Aug 5, 2026
d1f0f7e
Merge gs/deny-read-always (upstream v0.0.70) into ltic-main
smolyn Aug 5, 2026
ff5e0fe
feat(ltic): surface sandbox violations to the sandboxed agent
smolyn Aug 5, 2026
0064f45
feat(ltic): warn on unrecognized settings keys; version 0.0.70-ltic.2
smolyn Aug 6, 2026
041568c
fix(network): make credential-masking validation aware of allowAllDom…
smolyn Aug 6, 2026
e1aa3f0
Merge gs/allow-all-domains (allowAllDomains-aware masking validation)…
smolyn Aug 6, 2026
ef70de6
chore(ltic): version 0.0.70-ltic.3
smolyn Aug 6, 2026
0759421
fix(macos): auth-capable GIT_SSH_COMMAND via socat when present
smolyn Aug 6, 2026
3f55b35
feat(filesystem): denyReadAlwaysExcept — carve known-safe names out o…
smolyn Aug 6, 2026
62a80ae
Merge gs/deny-read-always (denyReadAlwaysExcept + denyAlways fixes) i…
smolyn Aug 6, 2026
6d50702
chore(ltic): version 0.0.70-ltic.5; pin denyReadAlwaysExcept in unrec…
smolyn Aug 6, 2026
99dd236
Merge upstream/main (v0.0.77) into gs/allow-all-domains
smolyn Sep 24, 2026
9583642
fix(network): allowAllDomains no longer admits IP literals or loopbac…
smolyn Sep 24, 2026
fc58d63
Merge gs/allow-all-domains (upstream v0.0.77) into ltic-main
smolyn Sep 24, 2026
c711f61
chore(ltic): retire denyReadAlways — superseded by upstream deny-with…
smolyn Sep 24, 2026
9da617e
docs(ltic): v0.0.70 → v0.0.77 sync notes
smolyn Sep 24, 2026
bc9448d
docs(ltic): sync notes — the TMPDIR/SSH findings are bare-srt only
smolyn Sep 24, 2026
3a92210
Merge upstream/main (v0.0.81) into gs/allow-all-domains
smolyn-ubc Oct 27, 2026
de07cb3
Merge gs/allow-all-domains (upstream v0.0.81) into ltic-main
smolyn-ubc Oct 27, 2026
92c9e67
fix(ltic): follow upstream's zod/v3 import; accept the -ltic version …
smolyn-ubc Oct 27, 2026
c3611d4
docs(ltic): v0.0.77 → v0.0.81 sync notes and guidelines-repo handoff
smolyn-ubc Oct 27, 2026
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
304 changes: 304 additions & 0 deletions 0.62_to_0.70_NEW_FEATURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
# sandbox-runtime v0.0.62 → v0.0.70: What's New and What It Means for the ccx Configs

Status: the `ltic-main` fork is now merged up to upstream **v0.0.70** (version
`0.0.70-ltic.1`, verify with `srt --version`). Both fork features —
`network.allowAllDomains` (PR #283, still open upstream) and
`filesystem.denyReadAlways` (PR #284, still open upstream) — survived the
update intact; this sync had **zero code conflicts** (only the version
stamp), so upstream drift remains cheap to carry. Both
`~/.srt-claude-denyall.json` and `~/.srt-claude-allowall.json` were
validated against the new schema and pass. **No config changes are
required.** The rest of this doc is what's newly possible and recommended.

None of the v0.0.66 recommendations below have been adopted yet — they all
still apply, and the v0.0.70 additions compose with them (the biggest
payoffs come from combining the two, see "What this unlocks" at the end).

---

## Part 1 — upstream v0.0.62 → v0.0.66 (133 commits, 4 releases)

- **Credential masking, greatly expanded.** Sentinel substitution now covers
request **bodies** (JSON, form, multipart, gzip, chunked framing) and
**environment variables** (`credentials.envVars`), with optional structured
masking (`extract` regex), JWT decoding (`decode: "jwt"`), and per-claim
masking (`maskClaims`). The sandboxed process sees a structurally valid
fake credential; the TLS-terminating proxy swaps in the real one only on
egress to configured hosts.
- **Linux hardening** (not active on macOS): seccomp-notify violation
observer, forced `--unshare-user` + `--cap-drop ALL` in the secure bwrap
branch, symlink resolution for write-deny paths before computing bwrap
masks.
- **`network.tlsTerminate.extraCaCertPaths`** — new: PEM CA files appended to
the trust bundle srt injects into the sandbox (after the MITM CA and the
host roots). Only `CERTIFICATE` blocks are copied; missing files are
skipped safely.
- **Perf:** MITM CA/leaf signing moved from pure-JS node-forge to native
`crypto.sign()` — faster session startup when `tlsTerminate` is enabled.
- **Removed:** the `disableViolationMonitoring` config option. The schema
strips unknown keys silently, so configs that still set it won't error —
but drop it if present. (The ccx configs never used it.)
- **Misc:** `GRPC_PROXY` now advertises the HTTP CONNECT proxy instead of
socks5h; large Windows (srt-win) overhaul — both irrelevant to the macOS
ccx setup.

### Recommended: mask `GH_TOKEN` instead of passing it in plain

Today `_ccx_run` extracts the gh token from the keychain and exports it as
`GH_TOKEN` into the sandbox. Any sandboxed process can read it, and because
the configs run with `allowAllDomains: true`, it could be sent anywhere.
The env-var masking closes exactly this hole:

```json
"credentials": {
"envVars": [
{
"name": "GH_TOKEN",
"mode": "mask",
"injectHosts": ["github.com", "*.github.com"]
}
]
}
```

The sandbox sees a length-matched fake token; the proxy substitutes the real
one only for GitHub hosts — in headers *and* request bodies. A process that
exfiltrates `$GH_TOKEN` to any other host leaks a worthless sentinel.

#### Caveats — read before enabling

1. **`injectHosts` is mandatory in these configs.** When unset it defaults
to `network.allowedDomains`, which is `[]` here because the fork's
`allowAllDomains` flag does the allowing. Without an explicit
`injectHosts`, the real token is injected **nowhere** and `gh` silently
fails auth.
2. **Requires `network.tlsTerminate`.** Substitution happens inside the
TLS-terminated stream (plain-HTTP substitution is off by default via
`allowPlaintextInject`). Enabling it means the sandbox must trust srt's
MITM CA — see next section.
3. **`enableWeakerNetworkIsolation: true` must stay.** `gh` is a Go binary
and still needs `com.apple.trustd.agent` for TLS verification on macOS.
4. Tools that bypass the proxy send the fake token and fail auth — a
functionality caveat, not a leak (the sentinel is worthless).

### Enabling `tlsTerminate` may retire the CA-bundle staging hack

`_ccx_run` currently copies `/etc/ssl/cert.pem` to a `.crt` path and exports
`SSL_CERT_FILE`/`CURL_CA_BUNDLE`/`CARGO_HTTP_CAINFO`/`GIT_SSL_CAINFO`/
`REQUESTS_CA_BUNDLE`, because the `/**/*.pem` deny glob eats the system
bundle. With `tlsTerminate` enabled, srt **builds and injects its own trust
bundle** (MITM CA + host roots + any `extraCaCertPaths`) and sets those same
env vars itself, with the bundle at an allowed temp path the pem-glob
doesn't cover. The zshrc comment anticipating "srt's MITM CA must be
appended to this bundle" is thus handled natively — the manual `cp` and env
exports are likely removable. **Smoke-test before deleting**: run a
sandboxed `gh api user`, `curl https://example.com`, and a `cargo` fetch;
srt's env injection should win, but confirm the wrapper's own exports don't
shadow it.

`extraCaCertPaths` is also the supported way to trust site-local/internal
CAs inside the sandbox if that ever comes up on the managed machine.

### Explicitly *not* worth adopting from this range (macOS)

- **`credentials.files` masking** — whole-file masking is Linux-only and
degrades to plain `deny` on macOS (SBPL cannot redirect reads). The
existing `denyReadAlways` globs already provide everything it could offer
here. This also means the keychain→`~/.claude/.credentials.json` seeding
scheme in `_ccx_run` remains the right approach; file masking cannot
replace it on a Mac.
- **Violation-monitoring / seccomp options** — Linux-only.
- **`windows` / `srtWin` config blocks** — N/A.
- **Symlinked-deny-path resolution, native CA signing, `GRPC_PROXY`** —
automatic behavior changes with no config surface.

---

## Part 2 — upstream v0.0.66 → v0.0.70 (42 commits, 4 releases)

### `:port` suffixes on `allowedDomains` / `deniedDomains`

Domain entries may now carry an optional destination-port suffix:
`"api.example.com:443"`, `"github.com:22"`, and — in `deniedDomains` only —
port-scoped deny-alls like `"*:22"`. An entry without a port matches any
port, exactly as before. This is the first way to say "this host, but only
this protocol" (or "any host, except this protocol") in srt config.

This composes cleanly with the fork: **`allowAllDomains` short-circuits the
allowlist *after* `deniedDomains` is checked**
(`src/sandbox/sandbox-manager.ts` — the deny loop runs first, port-aware),
so port-scoped denies are fully enforced in the default-allow ccx setup.

### `deniedDomainReasons` — explain a deny to the model

New optional `network.deniedDomainReasons` map, keyed by the **exact**
`deniedDomains` entry string:

```json
"network": {
"deniedDomains": ["gist.github.com"],
"deniedDomainReasons": {
"gist.github.com": "Creating gists is blocked in this sandbox (exfiltration risk). Use a file in the repo instead."
}
}
```

When that entry denies a connection, the reason is recorded on the
violation line instead of a generic "host is on the deny list".

**Honest caveat for the ccx setup:** the reason travels in the violation
*store*, which is an embedder API — `annotateStderrWithSandboxFailures`
appends a `<sandbox_violations>` block to a wrapped command's stderr when
srt is embedded as a library (as Claude Code's native sandboxing does).
The `srt` CLI wrapper that ccx uses spawns the child directly and never
prints the store, and the sandboxed client itself still sees only a generic
`403 Connection blocked by network allowlist`. So today this is
self-documenting config + future-proofing, not model-visible feedback. Two
ways to make it pay off are in Part 3.

### Proxy denials are now real violations

Previously a network deny was only a debug-log line; now every proxy denial
(HTTP and SOCKS) is recorded in the `SandboxViolationStore` with the denied
`host:port` and reason. Hardening came with it:

- Violation lines **honor `ignoreViolations`** (the same command-pattern →
ignore map that already silenced filesystem noise), so an expected,
recurring deny can be kept out of the report.
- URLs on violation lines are **redacted**: query strings collapse to `?…`
and `user:pass@` userinfo is dropped — runtime-interpolated secrets
(api_key=, signed URLs) can't leak into a transcript via the violation
block. Lines are also forced single-line and `<`/`>`-free so they can't
break the block's framing.

### Per-exec `commandLabel` (embedder API)

`SandboxManager` exec options gained `commandLabel` — a display label used
for violation attribution instead of the raw command string (which may be
long or contain decoded proxy internals). N/A for the CLI wrapper today;
relevant if ccx ever embeds the library.

### AWS SigV4 re-signing (`credentials.awsPairs`, `credentials.sigv4`)

The masking story from 0.66 had a gap for AWS: SigV4 signatures are HMACs
computed *from* the secret key, so a client signing with a masked secret
produced signatures no header substitution could repair. The proxy can now
**re-sign** such requests with the real credentials. The conventional
`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN` trio pairs
automatically when masked whole-value; `awsPairs` names non-standard
variables; `sigv4` sets deny/passthrough policy for the un-re-signable
shapes (streaming uploads, presigned URLs, SigV4A). **N/A for ccx until an
AWS credential enters the sandbox** — but it means AWS creds can get the
same treatment as `GH_TOKEN` if that day comes.

### `git.safeDirectories`

Injects `safe.directory` entries via `GIT_CONFIG_*` env vars, without
granting write. Solves git's "dubious ownership" failures where the sandbox
runs as a different user — a Linux (bwrap user-namespace) and Windows
(sandbox user) problem. **macOS sandbox-exec keeps your uid, so ccx never
hits this**; nothing to adopt.

### Robustness fixes (automatic, no config surface)

- **Trailing slashes on filesystem entries are now normalized** at the
`normalizePathForSandbox` chokepoint: a non-glob spelling like
`"/Users/"` used to silently produce a weaker/non-matching rule; now it
equals `"/Users"`. The ccx configs were already slash-free, so this
changes nothing there — but hand-edits are now safer.
- **Client-abort hardening** in the proxy: a sandboxed client disconnecting
mid-request no longer risks wedged sockets/unhandled errors.
- Linux-only: creation stubs under a read-only denied directory are skipped
correctly; the sandbox now starts when the seccomp helper binary is
execute-only.
- Windows: another large srt-win overhaul; `windows.srtWin.path` is now
required to use the Windows sandbox. N/A.

---

## Part 3 — what this unlocks for the ccx configs

The denyall config's intent is: **default-allow network with targeted
exfiltration denies** (fork `allowAllDomains` + `gist.github.com`),
**secrets unreadable even under broad read grants** (fork `denyReadAlways`
globs), and minimal friction/false positives for everything else. Against
that intent, ranked by value:

### 1. Widen the exfiltration denylist cheaply (port suffixes)

`gist.github.com` blocks one paste-site; port-scoped denies let you cut
whole *protocols* without touching the default-allow posture:

```json
"deniedDomains": ["gist.github.com", "*:25", "*:587", "*:465"]
```

blocks direct SMTP from the sandbox (a classic quiet-exfil channel that
`allowAllDomains` currently permits). If/when `GH_TOKEN` masking is on,
`"github.com:22"` + `"ssh.github.com"` is worth considering too: it forces
GitHub traffic onto https where the proxy can see it and where the masked
token is the only credential — over SSH the proxy substitutes nothing, so
an agent quietly switching a remote to SSH bypasses the whole masking
story. (Leave SSH alone until then; today it's a legitimate path.)

### 2. Adopt the 0.66 combo — it just got better

`tlsTerminate` + `GH_TOKEN` masking (Part 1) remains **the single biggest
risk reduction and simplification available**: it retires the CA-staging
hack in `_ccx_run` (simpler wrapper, fewer env vars to keep in sync) and
turns the plaintext token into a sentinel. The 0.70 changes strengthen the
same path: the proxy that does the masking is now more robust under client
aborts, and its denials are observable instead of silent.

### 3. Make denials visible to the agent — IMPLEMENTED (2026-08-05)

Recording proxy denials in the violation store is the foundation the ccx
setup was missing: a blocked gist push used to look like a mysterious 403
to the agent, which reads as a *false positive* ("the network is broken")
rather than policy. This is now closed end-to-end:

- **srt side (fork patch in `src/cli.ts`):** the CLI enables the seatbelt
log monitor (previously off in CLI mode, so filesystem denials were
never collected) and streams every violation line to
`~/.local/state/srt/violations-<pid>.log`, advertised to the sandboxed
child via `SRT_VIOLATIONS_FILE`. That directory is readable but **not
writable** under both ccx configs — sandboxed code cannot forge
violation lines into an agent's context.
- **Claude Code side (no fork drift):** a PostToolUse(Bash) hook
(`~/.claude/hooks/srt-violations.mjs`, registered in
`~/.claude/settings.json`) reads new lines since its last offset and
injects them as `additionalContext` wrapped in `<sandbox_violations>`,
with a preamble explaining these are policy blocks. Outside an srt
sandbox the env var is unset and the hook is a no-op.
- **Config side:** both ccx configs now carry `deniedDomainReasons` for
the gist entry and an `ignoreViolations` block filtering benign macOS
noise (sysctl kern.*, GlobalPreferences plists, configd mach-lookup) —
proxy deny lines honor `ignoreViolations` as of 0.70.

Verified end-to-end: a denied `https://gist.github.com` fetch logs the
deny with the configured reason instantly; a denied `.env` read logs the
exact path after ~1–2s of `log stream` latency (a trailing violation can
therefore slip past a session that exits immediately — irrelevant for
interactive sessions, where the next Bash command's hook picks it up).

### 4. Nothing to simplify away yet

Nothing in 0.67–0.70 replaces an existing entry in either config, and
nothing upstream supersedes the fork features: `allowedDomains` still
rejects bare `*` (so PR #283 `allowAllDomains` is still needed for
default-allow) and deny-beats-allow read globs remain fork-only (PR #284;
upstream's credential-file masking is still Linux-only). Both PRs remain
open upstream. The configs as written are already minimal for what they
express — the simplification wins live in `_ccx_run` (CA staging, see #2),
not in the JSON.

### Suggested next step

Copy `.srt-claude-denyall.json`, add the `credentials` block +
`"tlsTerminate": {}` from Part 1 and the `deniedDomainReasons` +
port-scoped denies from Part 3, and run `ccx` against it. Verify: `gh api
user` succeeds, `env | grep GH_TOKEN` shows a fake value inside the
sandbox, TLS tools (curl, git, cargo, uv) still verify, and a deliberate
`curl https://gist.github.com` / SMTP connection is denied. If that passes,
apply to `.srt-claude-allowall.json`, drop the CA-staging lines from
`_ccx_run`, and decide on the CLI violation-dump patch.
Loading