Skip to content

ltic-main: sync to upstream v0.0.81 (0.0.81-ltic.1) - #1

Closed
smolyn-ubc wants to merge 36 commits into
mainfrom
ltic-main
Closed

smolyn-ubc wants to merge 36 commits into
mainfrom
ltic-main

Conversation

@smolyn-ubc

Copy link
Copy Markdown
Collaborator

Summary

ltic-main is now upstream main at v0.0.81 + 4 commits (98a4016) plus the fork's patches. Version 0.0.81-ltic.1 (srt --version). Sync path: upstream → gs/allow-all-domains → ltic-main, as usual.

Fork deltas over upstream (what this diff contains)

  • network.allowAllDomains (src/sandbox/sandbox-config.ts, src/sandbox/sandbox-manager.ts): allow any hostname after deniedDomains are checked. IP literals and loopback names still need an allowedDomains entry. Credential-masking validation is allowAllDomains-aware. Upstream PR feat(network): add allowAllDomains flag to bypass allowlist anthropics/sandbox-runtime#283, still open.
  • Violations file (src/cli.ts): enables the log monitor and streams each violation to ~/.local/state/srt/violations-<pid>.log, advertised to the child as SRT_VIOLATIONS_FILE, so a PostToolUse hook can surface denial reasons to the agent. Logs older than a week are pruned.
  • Unrecognized-settings-key warning (src/utils/config-loader.ts): walks the parsed JSON against the zod schema and warns at startup about keys the schema would otherwise silently drop.
  • Version 0.0.81-ltic.1 in package.json / package-lock.json; test/proxy-cli.test.ts accepts a prerelease suffix.
  • Docs: 0.62_to_0.70_NEW_FEATURES.md, 0.70_to_0.77_NEW_FEATURES.md, 0.77_to_0.81_NEW_FEATURES.md, HANDOFF_sandbox-runtime-config_v0.0.81.md.

What changed in this sync (0.0.77-ltic.1 → 0.0.81-ltic.1)

Impact on the ccx configs

None required: both ~/.srt-claude-*.json load with no "unrecognized settings key" warning. New prerequisite: Node ≥ 22.12. Optional hardening (network.requireHostMatch) and other considerations are in 0.77_to_0.81_NEW_FEATURES.md, section 4.

Verification

  • npm install && npm run build && npm run typecheck clean.
  • npm test: 3400 pass, 1179 skipped (Linux/Windows only), 28 fail. Every failure also fails on pristine upstream/main on the same Mac (Bun 1.4.4 vs CI's 1.4.2 for the request-target tests; macOS 26's tcsh/csh for the shell tests). None touch fork code.
  • Smoke tests through the real _ccx_run wrapper under both configs: srt --version, HTTPS egress 200, IMDS 403, gist.github.com 403 with the configured reason in-band, gh api user, git ls-remote over SSH with socat off PATH, and the violations file carrying the same deny lines.

Details in 0.77_to_0.81_NEW_FEATURES.md and HANDOFF_sandbox-runtime-config_v0.0.81.md.

Follow-up (not in this PR)

smolyn and others added 30 commits June 30, 2026 11:39
When `network.allowAllDomains: true`, filterNetworkRequest short-circuits
to allow after the deniedDomains check runs, so explicit denies still take
effect. Intended for environments where filesystem restrictions are the
primary boundary and enumerating every reachable domain is impractical
(e.g. opening web docs lookups in an otherwise locked-down sandbox).

The flag is optional and additive: existing configs are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Read restrictions today have only two layers — denyRead and allowRead —
with allowRead unconditionally winning. That makes it impossible to
broadly allow a directory (e.g. ~/src for cross-project work) while
still keeping credential-style files (.env, *.pem, ~/.aws/credentials)
denied within it. The write side already has the symmetric capability
via denyWrite, so reads were the odd one out.

New optional `denyReadAlways: string[]` in FilesystemConfigSchema. On
macOS it emits Seatbelt (deny file-read*) rules AFTER the allowRead
loop so they take precedence (last-match-wins). On Linux it emits
bubblewrap binds (--tmpfs for dirs, --ro-bind /dev/null for files)
after the allowRead re-bind loop for the same effect.

denyReadAlways paths are also passed to generateMoveBlockingRules so
mv/rename cannot bypass the deny by relocating the file.

Glob handling reuses the existing helpers — patterns work the same as
denyRead. Patterns with a leading "/" match globally (e.g.
"/**/.env*"); without it they are CWD-relative, matching existing
denyRead behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	test/config-validation.test.ts
Make `srt --version` a reliable fork identifier. Upstream read the
version from `process.env.npm_package_version`, which is unset for a
globally-installed binary, so `--version` always fell back to a
hardcoded "1.0.0" — identical on upstream and this fork. Read the
version from package.json at runtime instead, and stamp this branch
with a distinctive `-ltic.1` suffix so users can confirm they are on
the patched fork (allowAllDomains + denyReadAlways).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When `network.allowAllDomains: true`, filterNetworkRequest short-circuits
to allow after the deniedDomains check runs, so explicit denies still take
effect. Intended for environments where filesystem restrictions are the
primary boundary and enumerating every reachable domain is impractical
(e.g. opening web docs lookups in an otherwise locked-down sandbox).

The flag is optional and additive: existing configs are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Read restrictions today have only two layers — denyRead and allowRead —
with allowRead unconditionally winning. That makes it impossible to
broadly allow a directory (e.g. ~/src for cross-project work) while
still keeping credential-style files (.env, *.pem, ~/.aws/credentials)
denied within it. The write side already has the symmetric capability
via denyWrite, so reads were the odd one out.

New optional `denyReadAlways: string[]` in FilesystemConfigSchema. On
macOS it emits Seatbelt (deny file-read*) rules AFTER the allowRead
loop so they take precedence (last-match-wins). On Linux it emits
bubblewrap binds (--tmpfs for dirs, --ro-bind /dev/null for files)
after the allowRead re-bind loop for the same effect.

denyReadAlways paths are also passed to generateMoveBlockingRules so
mv/rename cannot bypass the deny by relocating the file.

Glob handling reuses the existing helpers — patterns work the same as
denyRead. Patterns with a leading "/" match globally (e.g.
"/**/.env*"); without it they are CWD-relative, matching existing
denyRead behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve version to 0.0.66-ltic.1 and keep the denyReadAlways
config-validation test alongside upstream's new suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep both fork tests (allowAllDomains + denyReadAlways) in
config-validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve version to 0.0.70-ltic.1 in package.json and package-lock.json
(upstream's lockfile was still stamped 0.0.67).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enable the seatbelt/seccomp log monitor in CLI mode (previously only
proxy denials reached the store) and stream every violation line to
~/.local/state/srt/violations-<pid>.log, advertised to the child via
SRT_VIOLATIONS_FILE. A Claude Code PostToolUse hook inside the sandbox
tails the file and injects new lines as model context, so denials read
as policy (with deniedDomainReasons) instead of mysterious 403/EPERM
failures. The directory must be sandbox-readable but not writable so
sandboxed code cannot forge lines into the agent's context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zod strips unknown config keys silently, so a mistyped fork key
(allowAllDomainz) or a config/build mismatch validated cleanly while the
intended grant or restriction was silently lost — the exact footgun that
makes stock srt turn an allowAllDomains config into deny-all with no
feedback. The settings loader now walks the parsed JSON against the
schema and names every unrecognized key in a stderr warning at launch.

Tests pin both directions: all keys the ccx configs use (including fork
keys allowAllDomains and denyReadAlways) are recognized, and unknown
keys are reported with full dot-paths.
…ains

The injectHosts ⊆ allowedDomains reachability check predates
allowAllDomains: with the flag on, every host is reachable, but the
validator still demanded allowlist entries the flag makes meaningless —
the documented masking recipe (empty allowlist + injectHosts toward
GitHub) failed to load without padding allowedDomains.

Skip the subset check when allowAllDomains is set. Conversely, add the
missing guard for the trap that combination creates: a masked credential
with no injectHosts defaults to allowedDomains, which under
allowAllDomains is typically empty — masked but injected nowhere, a
silent auth failure. That spelling is now a validation error asking for
explicit injectHosts. The excludeDomains contradiction checks and the
explicit-empty-injectHosts rejection are unchanged.
The macOS GIT_SSH_COMMAND used BSD nc for its ProxyCommand, and nc only
speaks the SOCKS5 no-auth method. When SRT owns the proxy it always sets
a per-session auth token, so every sandboxed git-over-ssh attempt died
at the SOCKS handshake ("authentication method negotiation failed") —
the code comment acknowledged this and punted users to git-over-https.

Linux never had the problem because it emits
  socat - PROXY:localhost:%h:%p,proxyport=<port>,proxyauth=<user>:<token>
— an HTTP CONNECT tunnel that can present Proxy-Authorization. The mux
serves HTTP CONNECT on the same port on macOS, and the terminate proxy
already sniffs for a ClientHello before MITM-ing, so non-TLS SSH bytes
pass through opaquely (pinned by connect-non-tls.test.ts).

macOS now emits the identical socat spelling whenever socat is found on
PATH (whichSync, per generateProxyEnvVars call — the wrapper already
probes PATH per wrap for the shell) and httpProxyPort is set, keeping
the nc spelling as the fallback for hosts without socat. socat stays an
optional macOS dependency, unlike Linux where it is mandatory.

Tests pin all four macOS shapes: socat+auth, socat+per-command
attribution username (userRaw, not percent-encoded — the Basic header
takes it verbatim), socat without auth, and the nc fallback. Also fixes
stale "Linux-only" wording in http-proxy.ts / connect-non-tls.test.ts
and the SOCKS5 misnomer in the linux-sandbox-utils docblock.
…f credential globs

denyReadAlways globs are deliberately broad ("/**/.env*", "/**/id_*"),
which also catches files that are meant to be read: .env.example and
friends are templates with no secrets, and id_*.pub are public keys —
whose unreadability breaks ssh's IdentitiesOnly agent flow (ssh needs
the .pub to know which agent key to offer). Plain globs cannot express
"everything except this suffix".

New optional filesystem.denyReadAlwaysExcept: paths re-allowed for
reading even when they match denyReadAlways. Highest-priority read
rule: denyAlwaysExcept > denyAlways > allowWithinDeny > denyOnly.

- macOS: exception rules are emitted immediately after the denyAlways
  denies; Seatbelt is last-match-wins, so the allow fires when both
  match. No move-blocking changes needed: creation/rename inside
  write-allowed dirs is already re-allowed per-path by the
  writeAllowPaths pass, and that is the only place writes happen.
- Linux: the platform layer only sees concrete paths, so exceptions
  apply as set-subtraction in sandbox-manager (both getFsReadConfig and
  the per-exec path), comparing normalized paths.

Also fixes two pre-existing gaps in the denyReadAlways feature:
- The maskedFileBinds readConfig reconstruction on macOS dropped
  denyAlways, silently erasing every credential glob whenever
  credentials.files was configured.
- hasReadRestrictions (macOS and Linux) ignored denyAlways, so a config
  whose only read restriction was denyReadAlways ran unsandboxed.

Tests: new test/sandbox/deny-read-always.test.ts pins profile ordering,
the only-denyAlways wrapping fix, the mask-path regression, sandbox-exec
integration (.env denied / .env.example readable, including a file
created in-sandbox), and Linux set-subtraction; config-validation
covers the new schema key.
…k names

Upstream's resolved-address guard treats an allow-listed IP literal or
localhost name as an explicit choice and skips its checks. The flag's
short-circuit made every such destination count as allow-listed, so
169.254.169.254, 127.0.0.1 and localhost were reachable. The flag now
covers hostnames only; those destinations still need an allowedDomains
entry.
# Conflicts:
#	package-lock.json
#	package.json
#	src/cli.ts
#	src/sandbox/linux-sandbox-utils.ts
#	src/sandbox/macos-sandbox-utils.ts
#	src/sandbox/sandbox-manager.ts
#	src/sandbox/sandbox-schemas.ts
#	src/utils/config-loader.ts
…in-allow

Upstream v0.0.77 makes a denyRead glob beat an enclosing allowRead on
macOS (d106511) and Linux, and a narrower allowRead carves exceptions
back out of a covering deny glob. That covers what denyReadAlways and
denyReadAlwaysExcept were added for. The merge already took upstream's
rewritten rule generators; this drops the leftover schema fields and
tests. Leftover keys in a config are now reported as unrecognized.
smolyn and others added 6 commits September 24, 2026 16:40
Conflicts resolved:
- src/cli.ts: keep the violations file patch (startViolationsFile,
  initialize(cfg, undefined, true), SRT_VIOLATIONS_FILE); take upstream's
  static package.json import for --version (anthropics#630) and the interrupt
  handling added around initialize().
- src/sandbox/sandbox-utils.ts, test/sandbox/proxy-env-vars.test.ts: take
  upstream. The fork's macOS socat GIT_SSH_COMMAND (0759421, PR anthropics#452) is
  retired: upstream anthropics#681 authenticates to the proxy with stock /usr/bin/nc
  via SRT_SSH_PROXY_COMMAND, so socat is no longer needed on macOS.
- package.json, package-lock.json: version 0.0.81-ltic.1.
…suffix

config-loader.ts walked SandboxRuntimeConfigSchema with the root 'zod'
types while upstream anthropics#590 moved sandbox-config.ts to 'zod/v3', which
tsc rejected as excessively deep. The instanceof checks also need the
same module instance at runtime.

The srt-proxy --version test required a bare X.Y.Z; the fork ships
X.Y.Z-ltic.N, so allow a prerelease suffix.
0.77_to_0.81_NEW_FEATURES.md: fork patch outcomes (socat GIT_SSH_COMMAND
retired for upstream anthropics#681), macOS-relevant behaviour changes, options to
consider (requireHostMatch, SSH denies), library-level additions, Linux
and Windows summaries.

HANDOFF_sandbox-runtime-config_v0.0.81.md: self-contained brief for
updating ltic-ai-guidelines/sandbox-runtime-config — verified outputs,
file-by-file edits (README, DETAILS, justfile, configs) and an acceptance
checklist.
@smolyn-ubc

Copy link
Copy Markdown
Collaborator Author

Not needed: the guidelines repo installs straight from the ltic-main branch, and the fork's main stays a mirror of upstream.

@smolyn-ubc smolyn-ubc closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants