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
| Responsibility | Where |
|---|---|
Local runtime discovery ($AA_DEVINT_SOCKET, else ~/.aa/run/devint.sock) | src/discovery.ts |
Capability-token handling, mode-600 token files | src/credential.ts |
| Version negotiation before any verb, degraded surfaced not swallowed | src/client.ts |
| Tool list and integration status | DevIntClient.listTools / .status |
| Plan / apply / verify / repair / remove | DevIntClient.plan … .remove |
Protection-level display, evidence split, Host Enforced unavailability | src/render.ts |
| Privacy-preserving recent events | DevIntClient.scopedEvents / renderEvents |
| Approval-prompt relay | DevIntClient.relayApproval |
| Actionable degraded / incompatible errors | src/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.
| Excluded | How it is prevented |
|---|---|
| Evaluating policy | There 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 content | No scanner is imported and no response type can carry content to scan. |
| Modifying Claude/Codex/IDE configuration directly | No source file performs a filesystem write. Mutation happens by asking the runtime to apply a plan the runtime authored. |
| Holding unrestricted core or organisation credentials | CapabilityToken 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 level | The 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 binaries | No 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:
Host Enforcedis 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_UNAVAILABLEinsrc/render.ts);aa-cliis the surface that distinguishes available, unsupported and unmeasured, and is the one to follow for new clients.- Exercised evidence is shown separately from read-back evidence. A
configuration that exists is not protection.
splitEvidence()partitions byEvidenceView.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.
| Property | Result |
|---|---|
| A token scoped to tool A cannot act on tool B | Every tool-scoped verb on another tool is DENY_CODE_OUT_OF_SCOPE. |
| A read-only token cannot mutate | apply, repair, remove are all refused; reads still work. |
| No response carries a secret | The 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 unreachable | An out-of-set verb discriminant is DENY_CODE_UNKNOWN_VERB; Request has no method, path, filter or payload field. |
| No anonymous tier | An unenrolled client negotiates and is then denied every verb. |
| No silent downgrade | A 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:
- Copy the shape, generate your own bindings. Run
buf generateagainstproto/devint.protofor your language. Do not transcribe the schema. - 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.
- 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. - 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. - 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.
- 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.
- Relay approvals; do not decide them.
ApprovalRelayAcksays the input was accepted for adjudication. Rendering it as a verdict would make the plugin an authority it is not. - 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