AAC is a local control plane for agent computation. It attaches to AI agent harnesses, observes their steps, and returns one decision per step: which model, tool, or cached trajectory that step should use.
This document states AAC's threat model, the properties it guarantees, the properties it explicitly does not, and how to report a vulnerability.
| Guarantee | Status |
|---|---|
| AAC never requests, reads, stores, or forwards a credential | Guaranteed by design — no code path exists |
| AAC never bypasses harness permissions | Guaranteed by design — adapters report resources, they do not acquire them |
| AAC never becomes a single point of failure | Guaranteed — the fail-open contract |
| AAC never executes a tool or calls a model itself | Guaranteed — the controller emits an ID, the harness resolves it |
| No telemetry leaves the machine by default | Guaranteed — telemetry.mode: local-only |
| AAC deletes only files it can prove it wrote | Guaranteed — marker-gated uninstall |
| AAC stops short of modifying your agent configuration without consent | Guaranteed — aac install shows a plan and asks |
A local process that:
- reads configuration files belonging to other tools (OpenCode's
opencode.json, Claude Code'ssettings.json, Codex'sconfig.toml, MCP declarations), - optionally writes one file of its own into each of those tools' config directories,
- spawns a child process when acting as a transparent proxy (
aac connect,aac proxy), - serves a loopback HTTP dashboard,
- speaks MCP over stdio,
- writes a local SQLite database and a local NDJSON log.
- Not a network service. It binds
127.0.0.1for the dashboard and stdio for MCP; nothing listens on a public interface. - Not a privilege boundary. AAC has exactly the privileges of the user who ran it, and it never asks for more.
- Not an interceptor of provider traffic. AAC does not terminate TLS, does not install a root CA, and does not read
HTTPS_PROXYtunnels. That is deliberate: TLS interception requires writing to the system trust store, which is admin/root-equivalent and out of scope for a local control plane. - Not a sandbox. AAC does not attempt to contain a hostile harness; it assumes the harness is trusted and the control plane is what must stay safe.
| Asset | Where it lives | Exposure |
|---|---|---|
| Harness configuration files | ~/.config/opencode/, ~/.claude/, ~/.codex/, <workspace>/.mcp.json |
Read always; written only by aac install, with consent and a backup |
| Provider credentials | The harness's own files — auth.json, ~/.claude.json, ~/.codex/auth.json |
Never read, never written |
| Task content (goals, tool names, outputs) | The experience DB and the event log, both local | Never leaves the machine unless telemetry is explicitly non-local |
| The agent's execution path | The harness's own loop | AAC can advise; it cannot inject a tool call |
TRUSTED UNTRUSTED
─────── ─────────
The user A malformed config file
The host harness A malformed protocol line
The Python interpreter A hostile PATH
The local filesystem A broken or read-only disk
AAC's own code A subprocess that dies mid-line
A third-party adapter (unless you audited it)
A dashboard HTTP request body
Every crossing of that boundary is guarded. The pattern is uniform: degrade to a smaller capability set, or to "no opinion" — never raise, never guess.
There is no code path in AAC that reads, requests, writes, or forwards an API key, token, password, or OAuth secret.
What adapters read instead:
- OpenCode reads
provider.<name>.models,model,small_model,tools,agent/agents, and the names and transports ofmcpservers. It does not read credentials, andinstall()never opensopencode.jsonfor writing. - MCP enumerates server names and transports only. The source comment is explicit: "A server entry routinely holds secrets, and AAC has no reason to surface them into a snapshot."
- Codex reads
config.toml— includingmodel_providers.<name>.env_key, which names an environment variable rather than its value.
Generated commands use sys.executable, quoted, not a bare python — so a hook cannot be hijacked by a different interpreter earlier on PATH.
Redaction (aac.protocol.serialization.redact) is a second line of defence on the egress path, not the first: secret-shaped keys (api_key, apikey, authorization, token, secret, password, credential, bearer, cookie) become <redacted>, strings over 4096 characters are truncated, recursion is capped at depth 12.
An adapter reports resources; it does not acquire permissions. The capability object carries a permissions list describing what the harness has granted, and ActionSpace.from_capabilities() only offers actions whose required capability key is set.
DecisionValidator is constructed with allow_privilege_escalation = False, and the comment on it is load-bearing: "Deliberately no path sets this to True automatically. AAC must never escalate privileges on its own."
A permission denial is terminal. RuleBasedPolicy._handle_failure() returns DONE with permission_denied_harness_is_authoritative — never a retry, never an escalation. And execute_action() returning allowed=False means "the harness said no", not "try harder".
Failing open is usually framed as an availability choice. Here it is also a safety one, and it is worth being precise about why.
A control plane that cannot fail open has exactly one failure mode available to it: block. If AAC's hook exits non-zero, the agent stops. If AAC's HTTP endpoint hangs, the agent waits. If AAC's decision is wrong and the harness obeys it, the agent does the wrong thing. Every one of those turns a bug in a measurement tool into an outage in the user's work.
So the contract is:
| Layer | Guarantee |
|---|---|
AACRuntime.decide() |
Never raises. Any internal exception becomes FALLBACK_TO_HARNESS. |
AACAdapter |
Every method returns normally even when the harness is uncooperative. |
AACRuntime.discover() |
An adapter that raises during detection or initialization is disabled, not fatal. |
aac.hook.main() |
Always exits 0, even on total internal failure. stdout is a JSON decision or empty. A slow hook past AAC_HOOK_BUDGET_MS (default 40 ms) says nothing at all. |
apply_plan() |
Each adapter runs in its own try; one failure does not stop the others. |
LineFramer / StdioServer |
A malformed line is one dropped event, not a dead stream. Oversized lines are dropped, not buffered forever. |
Metrics / EventBus |
A subscriber that raises is counted as dropped and swallowed. |
ExperienceStore |
A corrupt or read-only database degrades to :memory:. |
| Dashboard | No runtime means zeros and 200, never 500. |
| Lifecycle | Every transition is declared; FAILED exists so a broken AAC can withdraw and report why. |
And the direction of every failure is the same: toward less AAC. A rejected decision is repaired to FALLBACK_TO_HARNESS, never escalated. A policy that raises yields FALLBACK_TO_HARNESS. An adapter that cannot observe yields an empty snapshot, which yields FALLBACK_TO_HARNESS.
A harness that implements one rule — treat fallback_to_harness: true as "proceed normally, ignoring AAC" — is fully protected from every AAC bug.
The controller emits an opaque resource ID. DecisionPayload.resource is a ResourceRef with exactly one of tool_id / model_id / subagent_id / cache_entry_id set. There is no arguments, no input, no call.
Every adapter's execute_action() hands over or refuses:
- OpenCode returns
delegated-to-harness— "OpenCode issues the request". - Claude Code returns
advisory-hook— "hooks observe and may block; they do not inject tool calls". - Codex returns
advisory. - Any adapter, for a resource the harness never advertised, returns
RESOURCE_UNAVAILABLE.
Reporting ok=True with a real-looking output would let the controller believe a computation happened that did not. That is why a refused resource is a failure, not a success.
telemetry.mode defaults to local-only. This is not a setting we expect anyone to change; it is the product decision.
A local control plane that phones home is a different product, and nobody consented to that one. Concretely:
| Component | Network behaviour |
|---|---|
aac.experience.store |
Pure SQLite. "The store never calls the network. Nothing here can phone home." |
aac.dashboard.server |
Binds 127.0.0.1. No CDN, no external script, no external font, no build step. |
aac.adapters.mcp.MCPServer |
Serves stdio only. --transport http prints a message and falls back: a listening socket would widen the attack surface for no integration gain. |
aac.runtime.ipc.connect_http |
A client: POSTs to a URL you configured, returns None on any failure. AAC initiates nothing. |
aac.cli.universal |
The single outbound call is the release check (GET api.github.com/repos/4shil/aac/releases/latest, guarded, 3 s timeout, silent on failure). It never installs anything — it prints the upgrade command and leaves. |
telemetry.redact defaults to true. Applied on the egress path only, so local storage keeps full fidelity:
- secret-shaped dict keys →
<redacted>, recursively, case-insensitive, substring-matched, - strings longer than 4096 characters → truncated to 512 plus
...<truncated N chars>, - recursion capped at depth 12, beyond which the value is
<max-depth>.
If you set telemetry.mode: team-server or remote and leave endpoint: null, nothing is sent. That is a safe misconfiguration, not a silent leak.
| Path | Contents |
|---|---|
<data dir>/experience.db |
Validated trajectories: task pattern, action sequence, tool and model IDs, result, cost, latency, failures, timestamp, environment, harness. |
<data dir>/logs/aac.jsonl |
The event log: runtime events and decisions, with action, resource, reason, confidence, latency. |
<data dir>/backups/ |
AAC's own pre-change copies of config files it modified. |
<data dir>/aac.yaml |
Your configuration, if you wrote one. |
<data dir>/adapters/ |
Drop-in adapter modules you installed. |
<data dir> is %LOCALAPPDATA%\aac on Windows and ~/.local/share/aac elsewhere, overridable with AAC_HOME. Delete it to erase all of it.
The adapter is the trust boundary, and third-party adapters are the largest attack surface in this design. AAC loads and executes arbitrary Python from three places:
<AAC data dir>/adapters/*.py— every invocation, files whose names start with_skipped.aac adapter install <path>— a one-off load.- The
aac.adaptersentry point of any installed package.
An adapter is code you have chosen to trust. It runs with your full user privileges, in-process with the runtime, on every aac command.
What AAC does to bound that:
| Control | Effect |
|---|---|
aac_api version gate |
An adapter outside the running AAC API range is registered incompatible, warned about, and never loaded. |
| Per-adapter exception isolation | An adapter that raises during detection, initialization, install, or execution is disabled. It cannot take the runtime or the host agent down. |
already_integrated() gate |
aac uninstall skips any adapter without the method — an adapter that cannot prove it wrote something is left alone. |
| Marker-gated uninstall | Built-in adapters only delete files carrying their own _aac_managed marker (OpenCode), their own hook command (Claude Code), or their own [mcp_servers.aac] table span (Codex). |
| Backup-before-write | A backup that fails means the write does not happen. |
| Plan-before-apply | apply_plan() refuses to act at all without confirmed=True. |
What AAC does not do:
- It does not sandbox adapters.
- It does not verify their provenance or signatures.
- It does not audit their capability claims.
If you install a third-party adapter, you are granting it your privileges. Read it first. The compatibility checklist in ADAPTERS.md exists so you know what to look for — and the single most dangerous thing an adapter can do is claim a capability the harness does not have, because that converts a correct "no opinion" into a loop, and can make the controller record savings that never happened.
Before installing one:
-
Read
detect(). Does it spawn anything? Write anything? Touch the network? -
Read
get_capabilities(). Is everysupports_*flag backed by evidence, with a citation in a comment? -
Read
install()anduninstall(). Is the removal gated on a marker the adapter wrote? Could it delete a user's file? -
Read
execute_action(). Could it reportok=Truefor something that did not happen? -
Grep for
os.environ,open(,requests,urllib,subprocess,socket,shutil,Path.unlink,os.remove. -
Run it against a throwaway
AAC_HOMEand a throwaway home directory first:AAC_HOME=/tmp/aac-sandbox HOME=/tmp/aac-sandbox-home aac adapter install ./their_adapter.py AAC_HOME=/tmp/aac-sandbox HOME=/tmp/aac-sandbox-home aac install --dry-run
--dry-runwrites nothing and prints the exact plan.
Use GitHub private vulnerability reporting. Do not open a public issue for a security bug.
- Go to the repository's Security tab.
- Choose Report a vulnerability.
- Include: affected version, the component (
aac/runtime,aac/adapters/...,aac/cli, the installers), a description, reproduction steps, and impact.
This placeholder must be replaced before public release.
Field Value Security contact <security contact>PGP key <PGP fingerprint>Expected acknowledgement 48 hours Expected status update 7 days The address, key, and timelines above are placeholders. Set them before the repository is made public, and update this section and
SUPPORT/READMEto match. Until then, use GitHub's private reporting, which is already available and requires no additional setup.
- Acknowledgement within 48 hours.
- A status update within 7 days.
- We will credit reporters in the changelog unless you ask us not to.
In scope: anything in src/aac/; the installers in installer/; the Makefile and make.ps1; and the shipped adapters' handling of user configuration files.
Out of scope: the historical research prototype under src/aac/v1–v4, src/aac/routers, src/aac/environments, src/aac/cache, src/aac/api, src/aac/harness, and the scripts/ directory. Those are not part of the control plane, are not installed by pip install . in the sense of being imported by aac, and are not exercised by the universal runtime. Reports against them will be triaged but are lower priority.
Also out of scope: a harness misbehaving because AAC advised it badly. That is a correctness bug, not a security bug — and AAC does not execute the advice.
A security fix that breaks either hard rule will be rejected:
- Fail open. A fix must not introduce a new way for AAC to block, hang, or crash the host agent.
- No credential handling. A fix must not introduce a code path that reads, stores, or forwards a credential.
If a vulnerability seems to require violating one of those, the vulnerability is in the design and the conversation is larger than a patch. Say so in the report.
| Version | Supported |
|---|---|
0.2.x |
Yes — security fixes |
0.1.x and earlier |
No — the research prototype; not a supported control-plane release |
This policy is shaped by verified research into what AI agent harnesses actually expose, including which of them have no hook system at all, which have no documented per-step model control, and which require admin-equivalent trust-store modification to observe at all. Those findings are why AAC's default posture is observe, advise, and stay quiet rather than intercept and control.