This page says what the reference implementation (SDK and CLI 0.1) actually enforces. Where something is declared but not enforced, it says so. "Tested" means an automated test exercises it, run locally with npm test.
Threats considered#
| Threat | Defence | Status |
|---|---|---|
| Tampered download or mirror | Archive SHA-256 and size pinned in a signed catalogue; per-file hashes and the manifest digest checked before extraction | Enforced, tested |
| Forged or substituted package | Ed25519 publisher signature over the manifest digest; publisher keys come from the signed catalogue | Enforced, tested |
| Compromised registry host | Catalogue signed by a root key pinned in the client; package documents pinned by hash | Enforced, tested |
| Stale or older catalogue replayed (rollback) | Monotonic sequence, remembered across runs | Enforced, tested |
| Frozen catalogue (freeze attack) | Catalogue expires; an online client refuses an expired one; offline use warns | Enforced, tested. Needs the maintainers to re-sign before expiry (registry build --refresh) |
| Compromised publisher key | Per-key retired / revoked status; revoked keys invalidate what they signed; whole-publisher suspended | Enforced, tested |
| Compromised registry root key | Rotation: a catalogue signed by a trusted root may add roots and revoke roots; clients remember it | Enforced, tested. A client that never saw the new root must update its pins |
Hostile archives: ../, absolute paths, symlinks, hard links, duplicates, decompression bombs | Strict tar reader, regular files only, entry and size limits | Enforced, tested |
| Code running at install or browse time | The format has no scripts or hooks | By construction, tested |
| A tool doing more than it declared | Per-tool permissions plus the OS sandbox below | Enforced on Linux with bubblewrap, tested; refused elsewhere |
| An update (or a dependency update) widening permissions | Grants stored per package; any new permission needs a new approval | Enforced, tested |
| A bad version already installed | revoked status refuses installs and refuses to run; doctor reports it | Enforced, tested |
| Local tampering after install | Every run re-hashes the installed files | Enforced, tested |
| Corrupt or poisoned large resources | SHA-256 of the whole file, shard-by-shard chain, resumed downloads re-verified | Enforced, tested |
| Entitled content fetched without entitlement | Host authoriser; reference gateway issues short-lived, content-bound, revocable grants | Client side and gateway logic tested; no production entitlement service exists |
The tool sandbox#
Tools run through runTool. Before starting a process it checks integrity, revocation and suspension, granted permissions and the input schema. There is no unrestricted fallback: where no enforcing sandbox is available the tool does not run (there are no allowUnsandboxed or partial-sandbox options). On Linux with bubblewrap:
- Namespaces: user, PID, IPC, UTS, network and cgroup unshared;
--die-with-parent --new-session. - Syscalls (seccomp-BPF): a filter assembled by Arcotheon denies
ptrace,mount,umount2,pivot_root,chroot,unshare,setns,bpf,perf_event_open,io_uring_*,userfaultfd,keyctl,process_vm_*, module loading,reboot, clock and swap calls and others;clonewith anyCLONE_NEW*flag; andclone3(libc falls back toclone). Sockets are limited to AF_UNIX, AF_INET and AF_INET6. Other ABIs are killed, and architectures without a table (anything but x86_64 and aarch64) refuse to start. It is a denylist: an allowlist would break arbitrary tools. - Filesystem: only system libraries (read-only), the capability's own directory and its dependencies (read-only), the verified-resource cache (read-only, if it declared resources) and the workspace read-only or read-write if it holds that permission. Other capabilities and the Arcotheon state do not exist for it.
- Executables:
/usr/bin,/usr/sbin,/usr/local/bin,/usr/libexecare replaced by empty directories; a tool can start only its interpreter and the programsprocess:spawnnames. Holds for Node, Python and native tools. - Network: none. A tool with
net:connecthas no direct route either: the only way out is a unix socket to a host-side filtering proxy, bridged to loopback inside. The proxy permits only declared hosts and ports (no port means 80 and 443), resolves names itself and dials the address it vetted, refuses names that resolve to private, loopback, link-local, unique-local or IPv4-mapped-private addresses unless the rule names that literal address (defeating DNS rebinding), and supports IPv6 literals. Ignoring the proxy settings gets nothing: tests show direct IPv4, IPv6, private ranges, the metadata address and DNS all fail, and the only interface is an empty loopback. - Limits: memory, swap and task ceilings through a transient cgroup-v2 scope (
systemd-run --user) when the user session supports it; otherwiseRLIMIT_DATA(with a 1 GiB floor, because Node aborts below about 768 MiB under it) and a process ceiling relative to the user's current thread count;ARCOTHEON_DISABLE_CGROUP=1forces the fallback. CPU time, file size and open files viaprlimit; wall-clock timeout; output cap. Tests: a tool allocating past its ceiling dies, a fork attempt stops at the ceiling. - Termination and cleanup: the whole process group is killed on timeout, output overflow, abort and crash; scratch directories and proxy sockets are removed (also on process exit and SIGINT/SIGTERM). Tests show no stray processes or directories after a timeout with background children.
- Environment: empty apart from
env:readnames. Secrets: only to tools holdingsecrets:<name>, if the host supplies them. - Every result reports what was enforced and by which mechanism.
What each operating system gives you#
| Linux (bwrap + userns) | macOS | Windows | |
|---|---|---|---|
| Install, verify, resolve, update | yes | yes | yes |
| Run tools | enforced (above) | refused unless ARCOTHEON_EXPERIMENTAL_SEATBELT=1 | refused |
| Native backend | bubblewrap | Seatbelt profile (sandbox-exec): written, not validated on a real Mac | none |
- macOS: the Seatbelt backend generates a deny-by-default profile (paths, executables, no network) and is implemented but has never run on a Mac; it is off by default. When on, it has no network allowlist (tools needing
net:connectare refused), and no memory or process limits (reported asnone). - Windows: there is no native backend. AppContainer plus a job object needs a native helper (for example a small Rust program calling
CreateAppContainerProfile,CreateProcesswith a restricted token and job limits) that was neither built nor tested. Run Arcotheon inside WSL2 with bubblewrap, where the Linux guarantees apply.explainNoSandbox()tells the user exactly this. - If host-level enforcement needs a kernel feature that is missing (user namespaces, cgroup v2 delegation, prlimit), the tool fails closed with the specific reason, or (for memory/process limits only) uses the weaker fallback and reports it.
Known limitations#
- A denylist seccomp filter and namespaces are strong but not perfect: a kernel or bubblewrap vulnerability could still allow escape. This is defence in depth against honest mistakes and ordinary malware, not a guarantee against a kernel exploit.
- A capability with
net:connectandfs:read workspacecan read the workspace and send it to its allowed hosts. That is what the permission means; the prompt says so. The proxy filters by name and port, not content (no TLS inspection). gpuis not provided to tools.- Whoever can write to
state.jsoncan change grants; protect the store directory.
Supply chain of the registry#
- Git is the source of truth.
CODEOWNERSroutesregistry/publishers,registry/statusand the pinned root keys to the maintainers; GitHub branch protection formainmust be turned on by an owner (it has not been configured here). - Entries are immutable: the build fails if a published version's content changes.
- Keys. Signing keys may live in a file (mode 0600), an environment variable, or outside this process: with
--signer-cmdand--key-idArcotheon hands the message to a command (an HSM or KMS wrapper) and never sees the private key. The keys used for the sample registry are development keys kept outside the repository. No production trust root has been created. The procedure is in production signing. - Publisher identity. A publisher can be marked
trustedonly with a recorded identity verification (who checked and when); the build rejects it otherwise. Sarja's record is "first-party", since Sarja operates the registry. - CI: the workflow is manual and read-only and has not been run. Nothing is published from CI.
- The directory at agdc.dev serves the same files and the CLI re-verifies signatures on the client, so the website is not a trust anchor.
Reporting#
Report vulnerabilities privately through GitHub security advisories on the repository.