Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The thin-client reference implementation

examples/aa-devint-reference-client is a minimal TypeScript client for the Developer Integration API. It exists so that the first VS Code, JetBrains, Windsurf, Copilot, Claude Code or Codex package built on the DI-API starts from a correct skeleton instead of inventing its own status, authentication and error-handling behaviour — and so the DI-API server has a second, independent consumer in another language proving its boundaries.

It is a reference, not a marketplace listing. It has no UI framework, no retries, no caching and no background reconnect. That is the point: everything left in it is something a real plugin genuinely needs.

MCP is optional and independent of this protocol

This is the misconception the whole architecture exists to prevent, so it is stated first.

The client-to-core protocol is the DI-API, not MCP. Nothing in the reference client speaks MCP; it never loads an MCP server, never registers an MCP tool and never requires one to exist. MCP accounts for two of the twelve IntegrationCapability values the runtime may govern — McpDiscovery and McpGovernance (aa-core/src/integration/capability.rs; the mechanism-level view is product brief §2) — and only the loading-control half is non-cooperative, deciding which MCP servers a tool may load. Exposing AASM capabilities as MCP tools is agent-cooperative and is therefore defence-in-depth, never a substrate.

A plugin built on MCP instead of the DI-API would make protection depend on the agent choosing to call a tool. An integration that uses no MCP at all is fully governed; an integration that uses MCP is governed by exactly the same mechanisms.

What the client is responsible for

ResponsibilityWhere
Local runtime discovery ($AA_DEVINT_SOCKET, else ~/.aa/run/devint.sock)src/discovery.ts
Capability-token handling, mode-600 token filessrc/credential.ts
Version negotiation before any verb, degraded surfaced not swallowedsrc/client.ts
Tool list and integration statusDevIntClient.listTools / .status
Plan / apply / verify / repair / removeDevIntClient.plan.remove
Protection-level display, evidence split, Host Enforced unavailabilitysrc/render.ts
Privacy-preserving recent eventsDevIntClient.scopedEvents / renderEvents
Approval-prompt relayDevIntClient.relayApproval
Actionable degraded / incompatible errorssrc/errors.ts

What the client must never be responsible for

These are not conventions — each is a property of the code, checked by test/guards.test.ts, which reads the shipped source and fails the build if the capability appears.

ExcludedHow it is prevented
Evaluating policyThere is no policy verb in the closed verb space, and the package’s generated bindings come from proto/devint.proto alone — a policy frame is undecodable here, not merely unrequested.
Scanning or redacting sensitive contentNo scanner is imported and no response type can carry content to scan.
Modifying Claude/Codex/IDE configuration directlyNo source file performs a filesystem write. Mutation happens by asking the runtime to apply a plan the runtime authored.
Holding unrestricted core or organisation credentialsCapabilityToken is the only credential type in the package; there is exactly one call site that exposes its secret, and it is the one that writes a Request.
Deciding the achieved protection levelThe renderer is a lookup table with no ordering, comparison or ranking of levels. Every level string it emits came off the wire. This is ADR 0030 forbidden design 10.
Starting arbitrary binariesNo process API is imported anywhere in src/.

Bindings come from the proto, never from a transcription

src/generated/devint_pb.ts is generated by buf generate from proto/devint.proto — the same file the Rust server and Rust reference client are generated from. pnpm generate:check regenerates into a scratch directory and byte-compares, so a proto change without a regeneration fails CI rather than surfacing as a mis-decoded frame.

A hand-written mirror of a wire schema is the failure this rule exists to prevent, and a generated file nobody diffs becomes a hand-written mirror the moment the proto moves.

The UX vocabulary is fixed

Use these words, verbatim, in any client. A user comparing the CLI, the dashboard and an editor extension must see one word for one thing.

  • Profiles: Recommended, Strict, Observe (§6).
  • Levels (the full ladder, low to high): Not Installed, Detected — Not Integrated, Partially Integrated, Integrated, Gateway Protected, Host Enforced (§7; aa-core/src/integration/state.rs). The lower three are what a client displays most often.
  • Overriding states: Drifted, Degraded, Incompatible.

Two display rules are load-bearing:

  1. Host Enforced is named on every status, not omitted. Silence reads as “there is nothing above what I have”, which is the over-claim the level model exists to prevent. What is said about it is the adapter’s answer, read from its declared capability support — a client may not assert that a platform cannot do something it has not examined (AAASM-5454). The reference client still emits one fixed sentence (HOST_ENFORCED_UNAVAILABLE in src/render.ts); aa-cli is the surface that distinguishes available, unsupported and unmeasured, and is the one to follow for new clients.
  2. Exercised evidence is shown separately from read-back evidence. A configuration that exists is not protection. splitEvidence() partitions by EvidenceView.kind, and the status renderer prints the two on their own lines.

A status is also always rendered with observed_at_unix_secs: the claim is “verified at T”, not “true now”.

Security properties, and how they are tested

The contract suite runs against the real aa_runtime::devint::DevIntServer over a real Unix socket — examples/aa-devint-reference-client/harness stands it up behind a stand-in lifecycle service. Most negative tests drive raw frames rather than the client, because a compromised extension would not politely use the reference client either.

PropertyResult
A token scoped to tool A cannot act on tool BEvery tool-scoped verb on another tool is DENY_CODE_OUT_OF_SCOPE.
A read-only token cannot mutateapply, repair, remove are all refused; reads still work.
No response carries a secretThe lifecycle fixture poisons a plan step’s environment value with a sentinel; it appears in no message and no rendered line.
Unrelated core operations are unreachableAn out-of-set verb discriminant is DENY_CODE_UNKNOWN_VERB; Request has no method, path, filter or payload field.
No anonymous tierAn unenrolled client negotiates and is then denied every verb.
No silent downgradeA v1-only offer is DEGRADED with the missing verbs named; a second Hello is DENY_CODE_PROTOCOL_VIOLATION; no shared version is Incompatible plus remediation.

Porting this to a marketplace package

For a VS Code, JetBrains, Claude Code or Codex extension:

  1. Copy the shape, generate your own bindings. Run buf generate against proto/devint.proto for your language. Do not transcribe the schema.
  2. Keep enrolment out of the plugin. A client that can mint its own credential has made enrolment a formality. The operator CLI enrols; the plugin reads the token it was given, and refuses a token file other users can read.
  3. Ask for the narrowest scope you need. A status panel wants list_tools, status, scoped_events, verify — not the lifecycle. A per-tool client is scoped to that tool, so a stolen token cannot reach another integration.
  4. Negotiate first and show the outcome. Offer your whole version window. If the connection is DEGRADED, hide the affected UI and show the remediation — do not let a user press a button for a verb the runtime does not have.
  5. Treat a missing socket as “the runtime is not running”. It is a bootstrap prompt, not a retry loop. The plugin is the only layer that exists when the runtime does not.
  6. Render, never derive. Show the level, the state, the evidence split, the observation timestamp, the next level and why it is blocked. Do not compute, rank or upgrade any of them.
  7. Relay approvals; do not decide them. ApprovalRelayAck says the input was accepted for adjudication. Rendering it as a verdict would make the plugin an authority it is not.
  8. Keep the dependency surface auditable. The reference client has exactly one runtime dependency — the protobuf runtime its own bindings need. A thin client’s blast radius is its dependency tree.

Running it

cd examples/aa-devint-reference-client
pnpm install
pnpm generate:check     # bindings still match proto/devint.proto
pnpm typecheck && pnpm lint
cargo build -p aa-devint-harness   # the contract suite needs the real server
pnpm test

pnpm build
AA_DEVINT_TOKEN=<token> node dist/cli.js status claude-code

Last updated: 2026-08-03 by Chisanan232