Skip to content

Releases

tartaros 0.1.0-alpha.25

Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Nov 03:43
28a153f

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 and git add
    and git commit failed 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 for lxc-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; --print shows the profile. With
    it, on the same host and as the same user: namespaces on, files at the top of
    the repository and git commit work, 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's git status, and git add . && git commit committed 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 scenario trust-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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Nov 18:43
060c3fc

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, so CFPreferencesAppSynchronize failed 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's apple.cfprefs.<uid>v1 and apple.shm.notification_center. Writing,
    creating and unlinking segments stay denied. It also lets cfprefsd answer the
    program: defaults read now 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Nov 07:22
31666fb

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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Nov 04:38
4fd40a5

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 ~/.aws on a host without one, with the
    repo under the home) the right to read a directory came down from the root, and
    ls showed 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Nov 04:01
1c23307

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 with spawn: 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's never_writes) left the role unable to create
    app/ok.txt in 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Nov 02:23
493607d

Fixed

  • An any-depth denyWrite in 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/**/commondir and <root>/**/.claude/**
    passed explain and the run wrote .git/worktrees/w/commondir and
    .claude/notes.md anyway. macOS now compiles any ** pattern to a Seatbelt
    regex (read or write); Linux denies every existing match, .git included, 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 why answers 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, so echo x > a/f from 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-log could 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 (a curl that 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.worktree and commondir, and .git/config.worktree,
    are trust-handoff.
    Either one points a trusted git in 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 03 Nov 14:12
f2d5a76

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.mjs builds it from cargo tree -p tartaros-cli (features as
    the release build resolves them) and cargo metadata; no new tool in CI.
  • --mutate covers trust-handoff. drop-trust-handoff removes the whole
    trust-handoff set and must flip every trust-handoff scenario, not just one
    (21 of 21 on macOS, 23 of 23 on Linux with and without namespaces). The
    switch exists only in a build of tartaros-core with the mutation-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 could chmod 0777 a 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Oct 03:14

Added

  • run --snapshot and tartaros 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 and tartaros rollback ID restores 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_ID and AWS_SECRET_ACCESS_KEY both 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). An aws-chunked body, 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.awsPairs was refused before.
  • Request rules: --allow-request "METHODS host[/path]" ([[request_rule]] in
    a policy file, network.requestRules in --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
    excludeDomains excludes, refuses the run (78). Measured against httpbin.org on
    macOS: GET /get allowed; POST /post, GET /anything and /get/../anything
    refused. docs/broker.md.
  • --handoff-exempt DIR (and handoff_exempt in a policy file,
    filesystem.handoffExempt in --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 in explain. Measured on macOS: a package with its own .git/hooks
    installs under vendor/, and the repo's .git/hooks stays denied.
  • --egress-log FILE: the egress broker's decisions as JSON lines, without
    the effect collectors --log starts (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: what explain prints, 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=VALUE value is never in it, only the name.

Fixed

  • Linux without namespaces: a repository's missing .git/hooks could be
    created.
    The carve protects only what exists, so in a .git with no hooks
    directory (a go-git or libgit2 checkout, git init --template=) mkdir .git/hooks && echo x > .git/hooks/pre-commit succeeded, --materialize
    included; the corpus scenario trust-handoff-hooks-dir-absent failed as a
    non-root user on Ubuntu. The run now puts an empty hooks directory and an
    empty read-only config there 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-user tier
    Windows refuses SSPI credentials to the restricted token, so curl.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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Oct 02:31

Added

  • Masked credentials on Windows, sandbox-user tier. --settings
    credentials.envVars with mode: "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. The appcontainer tier 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 list windows in os: the
    certified run is 87 scenarios on each tier, all passing (it was 18).

Changed

  • --settings runs on Windows. A settings-driven run is strict, and the
    Windows backend refused strict together with --seccomp-strict. Its
    validation already refuses every field it cannot enforce, none best-effort, so
    strict is 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 from libc syscall 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 to tartaros run yet.

Fixed

  • macOS: a symlink can no longer take the name of a handoff directory. An
    agent could create .claude as a symlink to a decoy directory holding its own
    settings.json: Seatbelt checks the resolved path, so the rules on
    .claude/settings.json saw 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 .claude and
    git init still 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 .claude symlinks, 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 run did 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 the sandbox-user tier 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 to npm 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 26 Oct 19:16

Fixed

  • tlsTerminate is read under network, 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 seisin key_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 that exported it hands it to every child), naming the variable and the
    process, not the value. --allow-exposed-secrets runs anyway. On Linux, runs
    with --isolate-pids or --isolate-proc are not checked.
  • macOS: isolate_process_info does not hide other processes' environment,
    and the docs no longer say it does.
    KERN_PROCARGS2 returns another
    process's arguments, and its environment unless the binary is Apple's own; no
    Seatbelt rule we found denies it. The bench's isolation-process-info-environ
    passed on macOS because ps -E fails inside Seatbelt for reasons of its own;
    it is now Linux-only, and isolation-process-info-procargs-macos keeps 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_env secrets 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.envVars with mode: "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 is tlsTerminate alone. New
    dependencies: rcgen 0.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)_PROXY by 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.