agent6 treats the LLM as untrusted. Each section states what is enforced and what is not.
Report privately through GitHub's Security tab.
Include: agent6 version (agent6 --version), kernel + distro (uname -a, /etc/os-release), agent6 check sandbox output, and a minimal repro (ideally a failing test under tests/security/).
Adversary: a fully malicious worker model, or an honest model prompt-injected by a file in the workspace.
The adversary controls the text of every LLM response, the choice of tool calls and their arguments within the published JSON schema, and the content of any file the agent reads. Outside its control: the kernel, the agent6 binary, the provider endpoints.
Holds
- No writes outside the workspace
sandbox.extra_write_pathsand the per-repo memory dir widen it, visibly inconfig show
- No reads outside the workspace and a read-only system set for installed toolchains
strict:/usr,/bin,/sbin,/lib,/lib64,/etc/alternatives, a minimal/etcthe launcher writes, a curated/devhardened: Landlock grants/usr,/bin,/sbin,/lib,/lib64,/etc,/devsandbox.extra_read_pathsadds more;agent6 check boundariesprints the resolved set
/tmpis writable at every levelstrict: a private tmpfs discarded with the runhardened: the host's/tmp;HOME(/tmp/agent6-home) is a host dir shared by every run on the machine
- agent6's own git never pushes, force-pushes, rewrites history, or
reset --hard(Git)- a
gitthe model runs throughrun_commandis bounded by the sandbox instead:protect_gitkeeps.gitunwritable understrict, and push needs egress
- a
- No persistence after the run: no daemon, cron,
.bashrcwrite, or setuid binary- chmod-family syscalls (
fchmodat2included) deny modes carryingS_ISUID/S_ISGID; ordinary chmod passes - every mount carries
nosuidandnodev, except the bound/devnodes (the builtin five:null,zero,urandom,random,full, plus anysandbox.extra_device_pathsgrant), whichnodevwould make unusable /tmpallows exec (toolchain helpers)- children write inside the jail's mount namespace (
strict) or the Landlock write grants (hardened) - nothing a command starts outlives it:
strict's PID namespace takes the tree down;hardenedholdsPR_SET_CHILD_SUBREAPERand kills every process that appeared during the command (asetsiddaemon included) - a survivor the sweep cannot kill fails the command
- chmod-family syscalls (
Does not hold
- The agent process's own egress is unbounded
- agent6 reaches the configured providers (each
[providers.*].base_urlhost, plus the fixed ChatGPT OAuth authority for token grants); nothing stops the process reaching elsewhere - a jailed command's egress is bounded (Network)
- agent6 reaches the configured providers (each
- On
hardened, a command can hand work to a user daemon already running (tmux,systemd --user): unix sockets have no Landlock hook and stay nameable without a mount namespacestrictdoes not expose them
agent6 check boundaries prints the resolved picture per actor (in-process tools, jailed commands, each MCP server): reachable paths, network, approval mode, secrets posture, and the cause when auto selected less than strict.
The agent's Python process is trusted and runs unconfined at every isolation level; the model is untrusted.
- the process holds the provider keys, writes the per-repo state dir, spawns the jail
- isolation levels differ only in which jail features the launcher enables
The model reaches the machine through two surfaces:
- The in-process tools, whose paths resolve against the file boundary (File access).
- Anything that executes, which runs in the jail (Sandbox).
The model sees the fixed tool set in src/agent6/tools/schema.py.
- structured edits, read-only navigation, fixed-argv verify and metric commands,
finish_session,ask_user, a curator task notepad, approval-gatedfetch(Network), capability-gatedrun_command - no
shell, nowrite_file, noeval - adding a tool needs a security review note (AGENTS.md)
Every tool call that allows the model to run arbitrary commands (run_command, run_verify_command, backgrounded commands, and MCP servers) runs in a jail at the effective isolation level.
Modes ([sandbox].isolation)
| Mode | Applies |
|---|---|
strict |
user + mount + PID + IPC + UTS + network namespaces, pivot_root rootfs, private /proc and /tmp, curated /dev, Landlock, seccomp, NO_NEW_PRIVS, capability drop, timeout |
hardened |
Landlock (ABI >= 3), seccomp, NO_NEW_PRIVS, capability drop, timeout. No namespaces, no rootfs |
none |
Nothing. Timeout only |
auto (default) |
The strongest of the three this host supports |
Landlock is best-effort under strict (a kernel without it skips that layer, warned once per run) and is hardened's only filesystem boundary.
Resolution
Probed per host: unshare for namespaces, the Landlock ABI syscall, the seccomp filter's architecture (x86_64 and aarch64).
| Setting | Host | Effective |
|---|---|---|
auto |
namespaces | strict |
auto |
no namespaces, Landlock ABI >= 3 | hardened |
auto |
no namespaces, Landlock ABI 1-2 | hardened, warned: truncation outside write grants is unconfined |
auto |
no namespaces, no Landlock | none, warned |
auto |
non-Linux, or no seccomp filter for the arch | none, warned |
strict |
no namespaces | refused |
hardened |
Landlock ABI < 3 | refused |
strict, hardened |
non-Linux, or no seccomp filter for the arch | refused |
none |
any | none, warned |
--dangerously-disable-sandbox and AGENT6_DANGEROUSLY_DISABLE_SANDBOX=1 force none for one invocation.
Config, flag, and env var are operator-only; the model reaches neither argv nor env.
Inside the jail
- Mounts (
strict): cwd and a private/tmpwritable, system paths read-only,extra_read_paths/extra_write_pathsand operator tool dirs at their real paths- every mount keeps the path it has outside
- Masked last, after every bind (
strict): the config dir, the state base,[sandbox].hide_paths- a grant at or inside a private dir is refused at config load
hardenedcannot mask: a grant containing a private dir warns, an unmaskablehide_pathsentry refuses
/dev(strict):null,zero,urandom,random,full, a privateshm; no/dev/ttysandbox.extra_device_pathsbinds named/devnodes read-write (GPU compute); each must be a char/block device on the host or the launch refuses, and onhardenedthe same grant is a Landlock read+write rule on the node
/proc(strict): fresh and private, empty if that fails- the launcher runs with an empty environment; it is PID 1 there, so the command can read
/proc/1/environ
- the launcher runs with an empty environment; it is PID 1 there, so the command can read
- seccomp: deny-list returning
EPERMforptrace,pidfd_getfd,process_vm_readv,process_vm_writev,kcmp,io_uring_setup,userfaultfd,mount,setns,unshare,kexec,bpf,perf,keyctl, module loading,reboot, clock-set; everything else allowed - Capabilities: cleared between fork and exec.
- Timeout:
timeout_s(verify and metric gates use[workflow].verify_timeout_s, default 600), then SIGKILL of the process group, rc=124- a model's
run_commandis not wall-clock killed: at[workflow].command_checkin_sit is handed back as a background job (Commands and environment)
- a model's
- One launcher per run at every isolation level; its commands share that netns, PID namespace, and
/tmp- closing the run's channel takes the PID namespace down
- a launcher that cannot start leaves each command its own
- The policy arrives as JSON on the launcher's stdin, validated against a strict schema; unknown fields are refused.
[sandbox].memory_limit_mb(default 0, off): per-processRLIMIT_DATA.
Verify
agent6 check sandbox prints the isolation this host resolves to, the reason when it is not strict, and every tool bin symlink resolving out of its bin dir (whose target directory is mounted read-only into the jail).
It then runs live probes at that isolation.
Exit 0 when all pass, 1 otherwise.
| Probe | Passes when |
|---|---|
landlock_abi |
the kernel reports Landlock ABI >= 1 |
jail_true |
/usr/bin/true runs in the jail, rc=0 |
jail_blocks_network |
a jailed getent hosts example.com fails. n/a under hardened, which has no per-command network namespace |
jail_blocks_etc_write |
a jailed write to /etc/agent6-escape leaves no file on the host |
read_file, list_dir, outline, find_definition, find_references, apply_edit, and apply_patch run in the agent process and ask no approval.
Every path they take resolves through Workspace:
workspace root
+ [sandbox].extra_read_paths (read) / extra_write_paths (write)
- [sandbox].hide_paths
- agent6's own config dir and state base
- The boundary comes from config values
- a degradation (
autofalling back tohardenedornone, a host with no jail) leaves it unchanged; undernoneisolation it is the only boundary
- a degradation (
- A denied path is refused with the reason
list_dirdrops the entry and reportshidden: N- the jail masks the path instead (empty dir, empty file; Sandbox)
- The symbol index skips a hidden file (an indexed one leaks symbol names and line numbers through
find_definition) - The edit tools refuse a write into the project's own
.git, raw or symlink-resolved, at every isolation level- the name matches case-folded on every platform (macOS and Windows open
.GIT/configas.git/config; macOS runs unsandboxed) - the same refusal covers a
pyvenv.cfgdir, asite-packagesancestor, and an operator protect path
- the name matches case-folded on every platform (macOS and Windows open
- Rewriting an editable-install
.pthcorrupts a venv invisibly (venvs are gitignored), so those writes refuse- reads stay allowed; an editable install still imports itself inside the jail (the repo is bound at its real path)
- Inside the state dir the edit tools may write the memory dir (
<state-dir>/<repo-id>/memory/) and nothing else (the exempt list intools/_path_safety.py). - Uncovered: with
isolation = "none"andrun_commands = "yes", a command reads a denied path directly (run_commandargv is not screened)
Repo memory is a prompt-injection persistence channel: MEMORY.md joins the system prompt of later runs on the same repo.
- Entries are inert data.
- The injected index is size-capped and framed as untrusted.
- The store is auditable (
agent6 memory list/show). - The jail never mounts it.
- Files are freely editable, so a hostile write can erase earlier memories.
- Sandbox, egress, and git policy come from config, so memory content cannot move them.
Every command tool (run_command, run_verify_command, stop_background) answers to [sandbox].run_commands and runs jailed.
run_commands = "no" withholds the verify gate too, and such a run starts gateless.
Under ask, a denied gate is withheld the same way for the rest of the run (no retry loop can discharge a refusal), and the run ends unverified.
The agent works within the environment it is given and cannot expand it:
sudocannot escalate, even passwordless:NO_NEW_PRIVSvoids setuid, so jailedsudofails under anyNOPASSWDrule.- Package installs fail:
apt/dnf/apkneed root (blocked), a route to a mirror (the defaultnetworkhas none off the box), and/usror/varwrites (denied) - Host-installed toolchains compile and run; a networked build step needs
networkloosened. - Toolchains, venvs, and deps are installed outside agent6. Access widens through config (
extra_read_paths,network,[providers.*].base_url), all visible inconfig show. - Running agent6 as root needs
--allow-root/AGENT6_ALLOW_ROOT=1(plus a banner) and weakens the boundarystrictmaps inside-root to real root: jailed children run as real root under Landlock, seccomp, andNO_NEW_PRIVSonly- writes outside the workspace and routes off the box stay closed
- readable files now include root-only ones (
/etc/shadowunderhardened;strict's rootfs hides it)
- Under
sudo, agent6 reads the real user's config and secrets (SUDO_UID/SUDO_USER) and chowns state-dir writes back- it does not drop privileges in-process; confinement comes from the jail
The agent process's own egress is unbounded. Everything below bounds a child of it.
fetch is the model's only direct egress.
- One https URL, GET, no redirects followed, no credential, text only, 1 MiB.
- Hosts on
sandbox.fetch_hostsare read without asking (empty by default); any other host prompts. - Nothing resolves before that gate: a DNS query delivers the hostname to whoever runs its authoritative server, so an unapproved URL never reaches a resolver.
- Hidden entirely when
network = "host", where a jailed command has its own route off the box.
Which network a jailed child joins. One of:
host: the machine's network.session: the run's own. Its members reach each other and nothing off the box.none: its own, alone.auto(default): the safest of these the host supports.
The run owns one session network.
A holder process creates it, the run keeps it alive with an open descriptor on /proc/<holder>/ns/{user,net}, and every child that asks joins those.
Entering a network namespace needs CAP_SYS_ADMIN in the user namespace that owns it, so a joiner enters that user namespace too; it still gets its own mount, PID, IPC, and UTS namespaces, so two members cannot see or signal each other.
Under strict, the only level with namespaces:
| jailed command | auto (def) |
session |
only_explicit_states |
host |
|---|---|---|---|---|
run_command |
the run's session network | same | same | host network |
tool, network auto(def)/none |
own, alone | own, alone | own, alone | own, alone |
tool, network = host |
⛔ refuse | ⛔ refuse | host network | host network |
On hardened there is no network namespace: auto degrades to the agent process's network with a once-per-run warning, while session and none refuse.
Under none isolation nothing is enforced or refused.
Refusals (fail-closed)
| Configuration | When |
|---|---|
a tool sets network = host under network auto/session |
machine start |
network = only_explicit_states, or explicit network = session |
run start, hardened |
a machine under network = session, or any tool with network = none |
machine start, hardened |
MCP servers take the same values per server, default auto:
auto: a network of its own where the host can give one, degrading to the host's with a warning.none: refuses instead of degrading.session: joins the run's network, so a browser server reaches the dev server a background command started.- A server is spawned on stdio or dialled at an operator-set
url, outbound either way.
Ingress. The loop opens no accept-side socket; the task graph is an in-process curator.
agent6 web opens one, and only when started.
- It binds loopback (
127.0.0.1) by default with no app auth (run it behindtailscale serve, where the tailnet identity is the access control; see the web UI). A non-loopback bind needs[web].allow_non_loopback = truefor[web].host, or--allow-non-loopbackfor--host. - The server renders folded state and drives typed contracts.
New-work spawns fixed argv with the task behind
--; machine-run is allow-listed to authored files; answers write only the addressed run's answer files (session id, answer id, and machine target state dir each validated to one path component); merge, prune, and config-set are fixed agent6 subcommands. - State-changing POSTs carry a CSRF guard: the body must be
Content-Type: application/json(a cross-sitefetchwith it triggers a preflight the server never answers) and anyOriginmust matchHost. It holds on loopback and behindtailscale serve, and does not cover DNS rebinding (that needs a Host allow-list incompatible with the tailnet name). - Request framing is bounded: 1 MiB body cap (413), chunked refused (411), and any unread-body refusal closes the connection.
- The machine write surface (
POST /api/machine/<name>/{poke,stop,steer,approve,answer}) uses the same guards.pokewrites only the instance signal file (inert JSON the nexttoolreads); the others write only the current agent state's per-state dir. - PWA assets are static and the service worker is a no-op passthrough (no Web Push, no VAPID). No telemetry, no auto-update, no remote control plane.
[sandbox].run_commands(askdefault,yes,no) gates every command tool; anaskprompt shows the argv- Each
mcp__<server>__<tool>call prompts with its arguments ([mcp.servers.<name>].approve, defaultask). - An "allow all" answer covers that server for the run, never the command tools or another server
- A tool name matching no configured server is refused, not prompted.
- The
fetchoff-list host prompt and the sandbox-off gate take no standing answer; both say so, and no front-end shows the button isolation = "none"with auto-approvedrun_commandadds a one-time gate:Continue? [y/N]interactively, a warning in CI andmachine run.- A prompt with no operator to answer it denies.
agent6's own git
- agent6's own git writes go through
git_ops.pyalone- it wraps the safe ops (status, add, commit, diff, branch, checkout)
- it refuses
push,reset --hard,commit --amend,rebase,filter-branch/filter-repo,branch -D/--force, and any--force/-fon a destructive verb - read-only exceptions live on the subprocess allowlist: the
review/sessions diff/askcollectors carry the same hardening flags;skills installclones with fixed argv
- One operator-only exception:
sessions prune --delete-squashedforce-deletes a run branch the manifest confirms was squash-merged (the commit survives in the reflog). git_ops.pyruns git with the configuredapi_key_envnames removed from its environment: a credential helper or content driver never inherits one- PATH, SSH, proxy, and credential-helper vars stay
- the read-only collectors inherit the environment untouched (no remote contact; the hardening flags leave no repo-controlled code to receive it)
A git the model runs through run_command is bounded by the sandbox, and its argv is not screened.
protect_git(default on) keeps.gitunwritable understrict: re-bound read-only, recursively (a mount nested under it stays visible and read-only)- a rewrite fails;
pushhas no egress
- a rewrite fails;
protect_gitis strict-only: onhardenedthe default degrades with a warning, an explicittruerefuses to run- a jailed command there can plant a
filter.<n>.cleanplus a.gitattributes, which agent6's own auto-commit (a temp-indexgit add -Aon the host) then runs, reaching$HOMEand the network - Landlock cannot express the exclusion: a directory grant is recursive and stacked rulesets intersect, so denying
.gitmeans not granting the workspace root either
- a jailed command there can plant a
- The protected scope is the project's own
.gitat every isolation level (the in-process edit tools refuse writes under it; File access)- a nested
.git(a vendored repo's, a submodule's) is workspace content, writable like any other file
- a nested
Repo-controlled host code in a poisoned .git/config
core.fsmonitoranddiff.externalare always off..git/hooks/*run only undergit.run_repo_hooks = true(default false);core.hooksPathpoints away, so a hook cannot fire on agent6's auto-commit.- Content drivers (
filter.<n>.clean/smudge/process,merge.<n>.driver) are off by default (git.run_repo_filters), neutralized per name- the clean filter runs on the auto-commit's
git add, the merge driver on the chain merge'smerge-tree: a cloned poisoned repo fires one without any model action truehonors them (the Git-LFS opt-in)
- the clean filter runs on the auto-commit's
- Provider keys live in
$XDG_CONFIG_HOME/agent6/secrets.toml,0600and owner-only (refused if group- or other-readable, or foreign-owned), or come from[providers.<name>].api_key_env(env wins). - They are absent from transcripts, redacted in
config show, and masked from the jail: the config dir stays masked even under an explicit grant, and a grant naming it directly is refused at config load. agent6 connectprompts locally (getpass) and writes config and secrets- one read-only
GETto the provider's key endpoint confirms auth (status only;--no-verifyskips it) - it executes nothing a remote returns
- one read-only
agent6 connect chatgptis a PKCE OAuth sign-in- a browser hits the authorize page of OpenAI's fixed OAuth authority (a constant, not config); the code returns on
localhost:1455(or is pasted), state-checked either way - token exchange and refreshes
POSTonly to the authority's/oauth/token - tokens live in
secrets.tomlunder the same0600and executes-nothing rules - agent6 never sends a rating or feedback on a response and has no rating surface: the backend may use rated turns for training, and that choice is never made on the operator's behalf
- a browser hits the authorize page of OpenAI's fixed OAuth authority (a constant, not config); the code returns on
- An in-process
GraphCuratorowns the task graph- every mutation validates against a pydantic schema before writing, under a per-mutation flock on the session dir
- a write-path fault after the in-memory update reloads from disk before surfacing: a later read never observes a node that was never persisted
- Per-repo state lives at
$XDG_STATE_HOME/agent6/<repo-id>/(override with[agent6].state_dir), outside the working directory jailed commands run in. - The config write lock serializes read-modify-write cycles and enforces nothing
- publishes are atomic: a torn config is impossible with or without it
- it fails open (a planted symlink refuses
O_NOFOLLOW; a stale root-owned lock is ignored); a write proceeding without it is kept, reported "kept as written" (docs/config.md)
agent6 run --parallel, agent6 sessions compare, and a live run's /parallel steer directive (see architecture.md, Parallel runs) each spawn subordinate work.
- Every lane is an ordinary run: a detached
agent6 runon its own clone, its own jail persandbox.isolation, its ownrun_commandspolicy- no sandbox socket is shared across lanes or with the parent
- Every spawned lane carries
AGENT6_SUBRUN=1; both the--parallelflag and the coordinator'slane_spawnerwiring refuse when it is set, so a lane cannot fan out or dispatch again. - A lane's config carries key references, never secret values
- the orchestrator writes each lane a
--configviamaterialize()(the resolvedConfig: providerbase_url,api_key_envnames), never a raw key - the lane reads the same
secrets.tomlor provider env var as any other run
- the orchestrator writes each lane a
- Lane git plumbing (clone, fetch, merge) goes through
git_ops.pyand lane spawning throughui/spawn.py, both already on the subprocess allowlist. - A lane starts from committed state only
- the fan-out clones HEAD; a coordinator dispatch cuts lanes at the run's chain tip after chain-committing changes
--parallelrefuses an origin with uncommitted tracked changes undergit.require_clean_worktree
machine run is a supervisor that makes no network calls.
Each tool state is jailed, so a per-tool network sets its netns independently (Network): a machine can keep agents on the provider API while one reviewed, fixed-argv tool reaches the network.
Operator-gated policy
networkis read only from the operator's config- a machine's
[config]overlay is rejected at load if it declares[providers.*],[sandbox.*],[presets.*],[mcp.*],machine.notify,notify.on_complete,git.run_repo_hooks, orgit.run_repo_filters
- a machine's
- A
toolonly declaresnetwork; honoringallowis the operator's call, and every conflict is refused at startup naming the state.
Bundle confinement
- Scripts live in a reviewed
scripts/beside the.asm.tomlmachine checkverifies every entry and static reference resolves inside the bundle (escaping symlinks rejected)
- Scripts are operator-authored and committed, never fetched or generated at run time
- the
.asm.tomlandscripts/are read-only in every jail during a run
- the
- Front-ends render
machine.notifyas an overlay, andattach/ TUI callnotify-sendwith fixed argv, so a model message is inert data. - The out-of-band hook
[machine.notify].on_eventruns an operator argv on the host with the minimalhook_envenv plusAGENT6_MACHINE_*.
Everything the model can influence runs through run_in_jail (Sandbox).
A fixed set of modules also shells out directly with subprocess.run / Popen, each with fixed argv depending only on operator input.
tests/security/test_subprocess_allowlist.py pins the file list; audit with rg 'subprocess\.|os\.(system|exec|posix_spawn)' src/agent6/.
git_ops.py: agent6's own git operations (Git).sandbox/detect.py: probes the host's sandboxing capabilities.sandbox/jail.py: the jail launcher.tools/mcp_client.py: operator-configured[mcp.servers.*]commands. A server with a sandbox policy spawns through the same launcher andJailPolicya jailed command gets (spawn_in_jail); a server the operator opted out is a plain subprocess.providers/token_command.py: the[providers.*].token_commandthat mints a provider bearer.sessions/ipc.py:ps -p <pid> -o lstart=on hosts without/proc(macOS), for theworker.pidstart-time identity, over a pid agent6 recorded.ui/btw.py: spawnsagent6 askdetached for/btw(every composer), so the side question keeps provider egress while the run is confined. Argv is the agent6 exe plus the question the operator typed, with--before it.ui/spawn.py: the shared front-end spawn helper. Spawns the agent6 CLI detached for run and machine launches, and capturessessions merge/prune/config set.ui/notify.py:notify-sendwith fixed argv (exe,--, two positional data args, no shell) for the device-present machine notification.ui/cli/helpers:$EDITORfor plan and steer editing;git diff/logfor the review subcommand and thesessions/askdiff views, with argv from the run manifest the CLI wrote outside the jail;rgfor history search; the fixed-argvpython -m agent6.ui.tuico-process behindrun --tui;cp/rm/apparmor_parservia sudo with fixed argv foragent6 system apparmor.app/finalize.py: the[notify].on_completehook at run end. Argv from config, env fromhook_env(a minimal base plusAGENT6_SESSION_*, never the provider keys in the operator environment).app/machine/_scriptcheck.py: ruff and ty with fixed argv, reading generated scripts statically. Those scripts execute only viarun_in_jail.app/machine_agent.py: spawns each agent state as a fixed-argvpython -m agent6.ui.cli.machine_agentsubprocess whose request travels in a temp file. Its[machine.notify].on_eventhook (fired fromapp/machine/_preflight.py) runs on the host with the same minimalhook_envbase plusAGENT6_MACHINE_*.ui/cli/skills_cmds.py:git clone --depth 1 -- <url>with fixed argv foragent6 skills install. The URL is operator-supplied and nothing fetched is executed.ui/tui/clipboard.py:tmux set-buffer -wwith the copied transcript text as one data argument.ui/tui/conversation.py: the operator's$PAGER, argv from the environment, transcript text on stdin.
- A skill is operator-installed config: install only from trusted sources
skills install <url>is an operator-initiated CLI fetch (theconnecttrust class); what it installs enters the system prompt and tool results verbatim
- Nothing in a skill runs at install or load
- its scripts run only through the jailed command path, subject to
run_commands
- its scripts run only through the jailed command path, subject to
use_skillis read-only and path-contained: the skill's own dir through a component-walked descriptor (any symlink hop or..refused)- skill dirs are not mounted into the jail; content reaches the model engine-side
- Repo-local
.claude/skills/are not discovered; only the installed dir and[skills].extra_dirsare scanned
tests/security/test_prompt_injection.py runs an adversarial corpus through the planner, worker, and reviewer prompts and asserts no exfiltration, no out-of-policy tool calls, and no following of embedded instructions to weaken constraints.
It catches prompt regressions; the structural defenses above confine a model that follows an injection.
- User namespaces must be enabled; agent6 refuses
stricton distros that disable them. - AppArmor userns (Ubuntu 24.04+) blocks unprivileged userns without a profile
- agent6 ships one scoped to the launcher (
agent6 system apparmor install): with itstrict, without ithardened
- agent6 ships one scoped to the launcher (
- seccomp is required; kernels that block it from unprivileged callers make the jail fail closed.
- Devcontainers get
hardened: the container bounds filesystem damage; jailed commands share the container's network (Network)- the XDG state base is ephemeral (lost on rebuild): mount a volume at the state dir or set
[agent6].state_dir
- the XDG state base is ephemeral (lost on rebuild): mount a volume at the state dir or set
- agent6 installed inside the project it works on (pip into the project's own venv) puts the running agent's code in the jail's writable workspace
- a jailed command can rewrite it; the next tool call runs the rewrite as you, outside the jail
- install agent6 outside the tree (pipx,
uv tool); agent6 warns at run entry on this shape
- Side channels: no claim about timing, cache, or speculative side channels.
- Supply chain: pin your install
- runtime deps
pydantic,httpx2,argcomplete, thetree-sitterpair,textual,ruff,ty; build dephatchling; jail cratesnix,libc,landlock,seccompiler,serde,serde_json
- runtime deps