Repository navigation
Releases
Releases · carlostapiacl/tartaros
tartaros 0.1.0-alpha.25
Added
tartaros setup apparmor: the full Linux mode on a stock Ubuntu. Ubuntu
23.10 and later keep a user without root from making user namespaces, so
tartaros fell back to the Landlock carve; measured on Ubuntu 24.04 as a user,
the agent could not create a file at the top of a git repository andgit add
andgit commitfailed on.git/index.lock. The command writes
/etc/apparmor.d/tartaros, which grants user namespaces to this binary and
nothing else (the shape Ubuntu ships forlxc-usernsexec), and loads it.
It refuses a binary anyone but root can change, a path AppArmor would have to
quote, and an existing file it did not write;--printshows the profile. With
it, on the same host and as the same user: namespaces on, files at the top of
the repository andgit commitwork, and the agent is still refused a user
namespace of its own. The message given when namespaces are unavailable names it.
Fixed
- Linux: the agent saw the placeholders and committed them. With a mount
namespace, an absent top-level handoff name (.mcp.json,.claude,.bashrc,
.github…) was held by an empty read-only file. In a plain repository 18 of them
showed in the agent'sgit status, andgit add . && git commitcommitted them;
when the run ended they were removed, leaving a commit of empty dotfiles and as
many deletions. In the working tree the placeholder is now an empty read-only
directory (0555), which git does not list or add; it holds the name the same
way, since no file can be created where a directory is and it is bound
read-only. In a repository's git internals it is still an empty file. New
corpus scenariotrust-handoff-placeholders-invisible-to-git: a control that
commits in a repository with every handoff name absent, and must commit only
what it wrote.
tartaros 0.1.0-alpha.24
Fixed
- macOS: a program that reads preferences could not start. The profile denied
the shared memory CoreFoundation reads to tell whether its cached preferences
are current, soCFPreferencesAppSynchronizefailed once a value had been read,
and Codex CLI stopped with "Failed to synchronize managed preferences". Three
segments are now readable, by exact name and read only:apple.cfprefs.daemonv1,
the user'sapple.cfprefs.<uid>v1andapple.shm.notification_center. Writing,
creating and unlinking segments stay denied. It also letscfprefsdanswer the
program:defaults readnow returns values wherever the profile allows reading
the plist. A plist denied by the profile stays unanswered and writes to a domain
still fail. - macOS: the scan for symlinked handoff dirs said the wrong thing and could spend
itself on a cache. The warning said one writable root had more than 10000
directories; the limit is over all of them together, and it stopped at the first
root that used it up. Its 10000 directories now go first to the roots inside the
working directory and then to the others in turn, so a small root is read whatever
is listed before it, and the warning names the roots it did not finish. A run
costs what it did.
tartaros 0.1.0-alpha.23
Fixed
- Parallel runs on Linux: a protected path removed while a run was being set
up failed it. Measured in parallel seisin suites: another test's repo in a
shared/tmp(.git/config,.claude/settings.json) removed between plan
and fork, a path whose directory had become a file (ENOTDIR), and a directory
that went while a placeholder was made in it (handoff placeholder ...: No such file or directory) failed the spawn, and the agent printed nothing.
Such a run is now planned again from the tree as it then is: a protected
path now absent is held with a placeholder, as any path missing at plan time;
one whose whole repo went is what a repo created during the run is
(docs/limits.md). The plans are 20, 100 and 400 ms apart, since what removes
it is usually a whole tree going; a path still vanishing after four fails the
run closed, named. - A vanished mount target is skipped only when a new plan would not protect
it better. 0.1.0-alpha.21 skipped any vanished hide or pin, and a read-only
bind if it looked like another run's placeholder. Now a bind is planned again
when its path, absent, would be held by a placeholder (an explicit write deny
in a root, a handoff path at a root's top level or in git internals): skipping
it left that path creatable by the agent. A read-denied directory replaced by
a unix socket fails its hide with ENOTDIR and is planned again, so the socket
is covered; skipping it let the agent connect. A read deny that is itself a
socket or fifo is now covered with /dev/null. Everything else that vanished
is skipped, as a new plan would leave it: parallel seisin runs hide each
other's temp directories, which come and go many times a second, and
planning again on each never converged. The child reports the race with a
value of its own, so a real spawn error (an ESTALE from NFS, say) keeps its
message.
tartaros 0.1.0-alpha.22
Fixed
- A read-denied directory inside a writable root could be listed on Linux.
Its files stayed unreadable, but once the read carve was on (a read-deny on an
absent path above the root: seisin's~/.awson a host without one, with the
repo under the home) the right to read a directory came down from the root, and
lsshowed the names in another role's private folder. With a mount namespace
it is now hidden like a read-denied directory outside the roots. Corpus:
read-deny-dir-listing-inside-root. Without one the limit stays
(docs/limits.md).
tartaros 0.1.0-alpha.21
Fixed
- Parallel runs stopped each other on Linux with namespaces. The mounts are
planned in the parent before the fork, and a target another run removed in
between (its run directory, a temporary, a placeholder it took back) failed the
whole spawn withspawn: No such file or directory. Measured: 4 runs out of
422 in a parallel seisin suite. A target that is gone is now skipped (nothing
to hide or pin, as if it had been absent from the start); any other mount
error still fails closed. - An explicit write deny on a path that does not exist yet no longer costs the
directory its new files (Linux with namespaces). It forced the carve, where a
directory holding a withheld entry takes no new ones:--write-deny app/.git/index.lock(seisin'snever_writes) left the role unable to create
app/ok.txtin its own territory. It is now held by a placeholder, as an
absent handoff path is, and removed after the run.
tartaros 0.1.0-alpha.20
Fixed
- An any-depth
denyWritein a settings file was accepted and not held. A
pattern was taken as covered by the trust-handoff set whenever it mentioned a
handoff name, so<root>/**/.git/worktrees/**/commondirand<root>/**/.claude/**
passedexplainand the run wrote.git/worktrees/w/commondirand
.claude/notes.mdanyway. macOS now compiles any**pattern to a Seatbelt
regex (read or write); Linux denies every existing match,.gitincluded, and
refuses a pattern it cannot hold (docs/limits.md). A relative base is taken from
the working directory and a symlinked prefix resolved (on macOS/tmp/x/**/y
matched nothing under/private/tmp); a class of several characters is refused.
tartaros whyanswers from the pattern. - Ctrl-C reaches the agent. The run has a process group of its own, so a
SIGINT to the launcher's group (a terminal's Ctrl-C) killed the launcher and
the run with it, even an agent that catches SIGINT to interrupt rather than quit.
SIGINT, SIGTERM, SIGHUP and SIGQUIT are now passed on to the run, once per
delivery, and the launcher exits with the run's status. The handlers are in
place before the agent starts (so--isolate-pids's init does not die of a
Ctrl-C meant for the agent) and gone when the run ends. - A relative write from the working directory failed on Linux with
namespaces. Under the bind-mount strategy (root, or a user namespace) the
inherited cwd pointed into the mount below the writable root's own, which the
read-only seal covers, soecho x > a/ffrom inside the root failed with
EROFS while the same write by absolute path went through. The run enters its
working directory again once the mounts are in place. --egress-logcould miss a refusal at the end of a run. The broker's
decisions are written by one thread, and the launcher exited as soon as the
agent did: a refusal in the run's last milliseconds (acurlthat gets its 403
and quits) was sometimes never written. The launcher now waits, at most two
seconds, for the broker's log before it exits.- A linked worktree's
config.worktreeandcommondir, and.git/config.worktree,
are trust-handoff. Either one points a trustedgitin that worktree at
planted hooks (core.hooksPath, or another gitdir). Corpus:
trust-handoff-worktree-commondir,trust-handoff-worktree-config,
trust-handoff-worktree-main-config.
tartaros 0.1.0-alpha.19
Added
- An SBOM with each release binary.
tartaros-<platform>.cdx.json
(CycloneDX 1.5) lists the crates compiled into that binary for its target,
with version, license and Cargo.lock checksum, and the binary's own SHA-256.
It is in SHA256SUMS and carries the same build-provenance attestation.
packaging/sbom.mjsbuilds it fromcargo tree -p tartaros-cli(features as
the release build resolves them) andcargo metadata; no new tool in CI. --mutatecovers trust-handoff.drop-trust-handoffremoves the whole
trust-handoff set and must flip everytrust-handoffscenario, not just one
(21 of 21 on macOS, 23 of 23 on Linux with and without namespaces). The
switch exists only in a build oftartaros-corewith themutation-testing
feature, which the security bench enables and the released binary
(-p tartaros-cli) never does.
Fixed
- Linux without namespaces: chmod, chown, utimes and xattrs outside the
roots. Landlock does not mediate them and there is no read-only remount
without a mount namespace, so an agent couldchmod 0777a file outside its
territory or forge an mtime. Those calls now go to the supervisor (seccomp user
notification), which makes the change on the object it opened only where the
run may write, with its capabilities dropped; EACCES elsewhere. Corpus:
filesystem-chmod-outside,filesystem-utime-outside. - A writable root nested inside a write deny (
--write proj/out --write-deny proj) was writable under the Linux carve; the deny wins, as on macOS
(write-deny-covers-nested-root). - Measured as a non-root user on a host that restricts user namespaces (the
Ubuntu 23.10+ default): corpus violations from 5 to 2, broken controls
unchanged at 8. The two left (an ancestor rename around a read deny inside a
root, an absent top-level.mcp.json) are in docs/limits.md with their
workarounds.
tartaros 0.1.0-alpha.18
Added
run --snapshotandtartaros rollback ID. The writable roots are
recorded before the run (a manifest, and each file's content stored once by
sha256 outside the roots, read-denied for the run);tartaros snapshot diff ID
lists what changed andtartaros rollback IDrestores it. Rollback snapshots
the current state first, so it can be undone, and never writes through a
symlink.--snapshot-exclude NAME,--snapshot-max-bytes(2 GiB; over it the
run is refused, 78). Read-denied paths are neither copied nor touched.- Signed releases and Homebrew. Each binary gets a GitHub build-provenance
attestation (Sigstore, keyless;gh attestation verify), and the release
carries a Homebrew formula generated from its SHA256SUMS. tartaros why read|write PATH [options]: the rule of the policy that allows
or denies one access (the writable root, the write-deny, the trust-handoff name
and the root it is protected under, the read-deny or the read-allow that wins).
Exit 0 allowed, 1 denied. It reads the policy, so it answers the same on every
backend, Linux included, where Landlock reports nothing about a denial. A test
checks each answer against a real write under the same policy.- Masked AWS keys are re-signed (SigV4).
credentials.awsPairs, or
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYboth masked: the agent's SDK
signs with a fake pair, and on the pair's hosts the broker re-signs with the
real one (same scope, signed headers and date; the body read whole, up to
32 MiB, its hash recomputed). Anaws-chunkedbody, a presigned URL and SigV4a
cannot be re-signed and are refused (403). The signer is ported from
sandbox-runtime (NOTICE) and checked against AWS's documented example; a
request to a host the pair does not inject into keeps its fake signature and
fails there.credentials.awsPairswas refused before. - Request rules:
--allow-request "METHODS host[/path]"([[request_rule]]in
a policy file,network.requestRulesin--settings): the methods and path
prefixes an allowed domain gets. The broker terminates that host's TLS with the
run's CA and refuses any other request with 403, logged with its method and
path. Paths fail closed (dot segments in any spelling, prefixes at a segment
boundary, spelled as written). A rule for a host that is not allowed, or that
excludeDomainsexcludes, refuses the run (78). Measured against httpbin.org on
macOS:GET /getallowed;POST /post,GET /anythingand/get/../anything
refused. docs/broker.md. --handoff-exempt DIR(andhandoff_exemptin a policy file,
filesystem.handoffExemptin--settings): the trust-handoff set is not
protected inside DIR, a dependency tree a setup phase fills (composer install,npm install) whose packages ship their own.git/,.github/or
.gitattributes. DIR must be strictly inside a writable root, not a writable
root, not inside a handoff surface and not named like one; otherwise the run
is refused (exit 78). Every other rule still holds inside, explicit write-denies
included, and the repo's own surfaces stay protected. Said at the start of every
run and inexplain. Measured on macOS: a package with its own.git/hooks
installs undervendor/, and the repo's.git/hooksstays denied.--egress-log FILE: the egress broker's decisions as JSON lines, without
the effect collectors--logstarts (about 0.07 s a run, against 1.6 s with
--log, measured on macOS). A launcher reads the refused hosts from it instead
of parsing stderr; seisin does.explain --json: whatexplainprints, as data: the roots, the denies, the
egress allow-list, the masked credentials, the trust-handoff names and
exemptions, the limits, what this host enforces and whether it would refuse. An
--env KEY=VALUEvalue is never in it, only the name.
Fixed
- Linux without namespaces: a repository's missing
.git/hookscould be
created. The carve protects only what exists, so in a.gitwith nohooks
directory (a go-git or libgit2 checkout,git init --template=)mkdir .git/hooks && echo x > .git/hooks/pre-commitsucceeded,--materialize
included; the corpus scenariotrust-handoff-hooks-dir-absentfailed as a
non-root user on Ubuntu. The run now puts an emptyhooksdirectory and an
empty read-onlyconfigthere before building the carve and removes them when
it ends, if still empty. With a mount namespace nothing changes (placeholders
bound read-only, as before).
Changed
- docs/limits.md, Windows: Schannel measured. Under the
sandbox-usertier
Windows refuses SSPI credentials to the restricted token, socurl.exe,
PowerShell and .NET fail HTTPS there. The alternatives were measured on 16,000
folders: a token with deny-only groups (Schannel works) leaves 23 folders
writable outside the roots, among them installer and updater folders a
privileged process later runs from; a low-integrity token keeps writes closed
but stops BusyBox and batch files from running. The restricted token stays.
tartaros 0.1.0-alpha.17
Added
- Masked credentials on Windows,
sandbox-usertier.--settings
credentials.envVarswithmode: "mask"now run there: the swap is the same
broker's, on the setup's broker port, and the agent is another user, who cannot
read tartaros's environment. Measured on Windows 10: the agent holds the
sentinel, no variable of its holds the value, httpbin.org receives the real
one. Theappcontainertier refuses them by name (exit 78): it has no broker
channel. - Windows in the security bench's certified set. 69 scenarios that passed on
both Windows tiers, their baselines included, now listwindowsinos: the
certified run is 87 scenarios on each tier, all passing (it was 18).
Changed
--settingsruns on Windows. A settings-driven run is strict, and the
Windows backend refusedstricttogether with--seccomp-strict. Its
validation already refuses every field it cannot enforce, none best-effort, so
strictis now accepted; the syscall hardening itself is still refused.- seccompiler 0.5.0 (was 0.4.0), Linux. Its syscall tables move to kernel 6.12
and it adds riscv64; tartaros builds its filters fromlibcsyscall numbers,
not those tables, so no filter changes. Measured on kernel 6.8: the workspace
tests and the security bench (137 scenarios, 0 violations, 0 broken controls). - windows-sys 0.61.2 (was 0.59.0), for the Windows backend, which is not
wired totartaros runyet.
Fixed
- macOS: a symlink can no longer take the name of a handoff directory. An
agent could create.claudeas a symlink to a decoy directory holding its own
settings.json: Seatbelt checks the resolved path, so the rules on
.claude/settings.jsonsaw the decoy, and a trusted agent would later load it
(trust-handoff-symlink-absent-parent, critical). Creating a symlink named
after an ancestor of a handoff surface (.claude,.git,.github) is now
denied at any depth, a rename onto that name included;mkdir .claudeand
git initstill work. - 15 scenarios move from the known gaps to the security bench's corpus: they
pass on every system they list, measured today (chmod, utime and ancestor
renames outside the territory, handoff hooks and.claudesymlinks, request
smuggling through the broker, loader-injection variables under
--inherit-env, among others). The corpus is 156 scenarios: Linux 151 of 152
(the PID-namespace one is vacuous in a container), macOS 121 of 121. The 14
left keep their per-system status as of today. - docs/limits.md, Windows: it said
tartaros rundid not dispatch to the
backend and that there was no egress broker; both have been there since the
backend landed. It now also says what thesandbox-usertier breaks: Windows
refuses SSPI credentials to the restricted token the agent runs under, so a
client on the system TLS stack (curl.exe, PowerShell, .NET) fails every HTTPS
connection there; clients with their own TLS stack (Node, Python, Git, rustls)
work. - A release run no longer fails on a version another run of the same tag just
published. The registry can hold it staged, not yet visible tonpm view,
and answer 409; that now counts as published. Seen on 0.1.0-alpha.16, whose
tag started two runs: the second failed although the first had published
everything.
tartaros 0.1.0-alpha.16
Fixed
tlsTerminateis read undernetwork, where sandbox-runtime's schema
puts it and seisin writes it. It was read from the top level of the settings
file, so a real seisinkey_mode = "inject"policy was rejected as an
unknown field (exit 2) instead of being masked.- The upstream leg of a terminated connection trusts the launcher's
NODE_EXTRA_CA_CERTS, besides the webpki roots, as sandbox-runtime's proxy
does. A host behind a site-local root, or a TLS-inspecting proxy, failed there
although the agent's own trust bundle already held that root. - A run whose secret another process exposes is refused (exit 78). Before
the agent starts, tartaros reads every exec record it can, as the agent could,
and refuses when one holds a masked credential's or the broker's secret (a
shell thatexported it hands it to every child), naming the variable and the
process, not the value.--allow-exposed-secretsruns anyway. On Linux, runs
with--isolate-pidsor--isolate-procare not checked. - macOS:
isolate_process_infodoes not hide other processes' environment,
and the docs no longer say it does.KERN_PROCARGS2returns another
process's arguments, and its environment unless the binary is Apple's own; no
Seatbelt rule we found denies it. The bench'sisolation-process-info-environ
passed on macOS becauseps -Efails inside Seatbelt for reasons of its own;
it is now Linux-only, andisolation-process-info-procargs-macoskeeps the gap,
measured with the call itself.
Changed
- A run's secrets are out of tartaros's exec environment. Masked
credentials and[broker]value_env/token_envsecrets used to stay in the
exec-time environment of the tartaros process, which the agent (same user)
could read. tartaros now hands them to a clean re-exec of
itself through a pipe before the broker or the agent start. A live test scans
every readable record from inside a run. The launcher's own environment is
outside its reach (docs/broker.md). - Masked credentials under
--settings(sandbox-runtime's
credentials.envVarswithmode: "mask"). The agent's variable holds a
sentinel, also with--inherit-env; the egress broker terminates TLS toward
the credential's own hosts with a CA made per run and swaps the sentinel for
the real value in the request's headers and streamed body, gated per
credential. Other hosts keep the blind tunnel. The CA's key stays in memory;
its certificate and a trust bundle go to a run directory the trust variables
point at, removed after the run. Files, AWS pairs, other modes and a caller's
own CA are refused by name (exit 78), as istlsTerminatealone. New
dependencies:rcgen0.14.10 and rustls as a server (the rustls/ring the
HTTPS client already used). docs/broker.md says what it costs (a MITM over
those hosts) and what it does not close (reflection). - The broker's HTTPS client is ureq 3 (was 2.12). ureq 2.12 could panic
inside its response reader when an upstream cut a body short (failed to read exact buffer length from stream, seen once in CI): the connection's thread
died and the client got an empty reply. ureq 3 reads the environment's
HTTP(S)_PROXYby default; the broker turns that off, so a request carrying
an injected credential still goes to the capability's upstream alone. A test
sets every proxy variable to a listener and checks nothing reaches it, and
that a 404 and a 302 are relayed (not a 502, not followed). Fewer crates in
the lockfile.