Skip to content

Security: 4shil/aac

Security

SECURITY.md

Security Policy

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.


Summary of guarantees

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

Threat model

What AAC is

A local process that:

  • reads configuration files belonging to other tools (OpenCode's opencode.json, Claude Code's settings.json, Codex's config.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.

What AAC is not

  • Not a network service. It binds 127.0.0.1 for 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_PROXY tunnels. 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.

Assets

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

Trust boundaries

  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.


Explicit statements

AAC never requests or stores credentials

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 of mcp servers. It does not read credentials, and install() never opens opencode.json for 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 — including model_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.

AAC never bypasses harness permissions

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".

AAC never becomes a single point of failure — the fail-open contract as a security property

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.

AAC never executes a tool or calls a model itself

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.


Data-privacy defaults

Local-only, and why

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.

Redaction on egress

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.

What is stored locally

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 trust boundary at the adapter

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:

  1. <AAC data dir>/adapters/*.py — every invocation, files whose names start with _ skipped.
  2. aac adapter install <path> — a one-off load.
  3. The aac.adapters entry 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.

Auditing a third-party adapter

Before installing one:

  1. Read detect(). Does it spawn anything? Write anything? Touch the network?

  2. Read get_capabilities(). Is every supports_* flag backed by evidence, with a citation in a comment?

  3. Read install() and uninstall(). Is the removal gated on a marker the adapter wrote? Could it delete a user's file?

  4. Read execute_action(). Could it report ok=True for something that did not happen?

  5. Grep for os.environ, open(, requests, urllib, subprocess, socket, shutil, Path.unlink, os.remove.

  6. Run it against a throwaway AAC_HOME and 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-run writes nothing and prints the exact plan.


Reporting a vulnerability

Use GitHub private vulnerability reporting. Do not open a public issue for a security bug.

  1. Go to the repository's Security tab.
  2. Choose Report a vulnerability.
  3. 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/README to match. Until then, use GitHub's private reporting, which is already available and requires no additional setup.

What to expect

  • Acknowledgement within 48 hours.
  • A status update within 7 days.
  • We will credit reporters in the changelog unless you ask us not to.

Scope

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.

Hard rules for security fixes

A security fix that breaks either hard rule will be rejected:

  1. Fail open. A fix must not introduce a new way for AAC to block, hang, or crash the host agent.
  2. 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.


Supported versions

Version Supported
0.2.x Yes — security fixes
0.1.x and earlier No — the research prototype; not a supported control-plane release

Acknowledgements

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.

There aren't any published security advisories