ADRs

Architecture Decision Records (ADRs) capture the reasoning behind significant technical choices in mcpose. Each ADR records the decision, the options that were considered, and the consequences that follow.

The records live in the mcpose repository at docs/adr/, numbered in sequence from 0001. They are the source of truth for why the code works the way it does. Read the ADRs that touch an area before changing that area.

ADR-0001: Integrity-chained log, not a managed audit store

@mcpose/audit emits tamper-evident events, HMAC chain plus Merkle root per session, to an operator-supplied sink. It does not manage its own write-once storage, query API, or report generator. Building a full audit substrate was considered and rejected: it turns mcpose from a library into a product, locks operators out of their existing Splunk, Elasticsearch, or S3 infrastructure, and puts mcpose in the critical path of a compliance failure if the store is compromised. The trust comes from producing cryptographically provable events, not from owning the store. A managed audit service remains an option once there are enough operator deployments to justify it.

ADR-0002: ProxyOptions arrays use response-processing order

The toolMiddleware, resourceMiddleware, and listToolsMiddleware arrays in ProxyOptions are in response-processing order. The first element processes the response first (innermost layer) and the last element processes it last (outermost layer). This is the opposite of compose(), which takes outermost-first. The dominant use case [piiMW, auditMW] reads "PII runs before audit", meaning PII redacts the response before audit ever sees it, which maps naturally to array position. The two conventions are explicitly not interchangeable: ProxyOptions arrays must not be passed directly to compose(). See the middleware model for how middleware layers compose.

ADR-0003: Subkeys derived from the signing secret via the oracle, not from keyId

@mcpose/audit derives its per-entry HMAC chain key and its high-tier AES encryption root by calling SigningKeyProvider.sign() with domain-separation labels, never from keyId. keyId is treated strictly as a public identifier, published in ReplayManifest.signedBy, and is never key material. An earlier implementation keyed both the HMAC chain and the AES encryption off Buffer.from(keyId, 'hex'), where keyId = SHA256(secret). Because keyId is published in every manifest, a manifest-holder could recompute any chainHash (forging the chain) and re-derive any per-event key (decrypting high-tier payloads). That collapsed tamper-evidence to the lone signed Merkle root and voided high-tier confidentiality entirely. See the @mcpose/audit package docs for the derivation details and the security page for the trust model this underpins.

ADR-0004: Audit format v2: canonical serialization, full-manifest signature, hardened derivations

The v1 audit format had five independent weaknesses, each individually format-breaking to fix, so they ship together as one mcpose/v1/* to mcpose/v2/* scheme rotation in @mcpose/audit 3.0.0.

  1. Canonical preimages. v1 hashed JSON.stringify output, making object-key insertion order load-bearing. v2 hashes canonicalJson with keys sorted lexicographically at every depth and domain-tagged framing, so only the field set is load-bearing.
  2. The signature covers the whole manifest. v1 signed only the Merkle root, leaving sessionId, identity, eventCount, and the proofs swappable around a validly signed root. v2 signs every field except the signature itself, and signing eventCount also neutralizes the duplicate-last-leaf padding ambiguity.
  3. Merkle domain separation. Leaves and internal nodes are domain-tagged, so an internal node can never be replayed as a leaf, and verifyMerkleProof rejects malformed proofs.
  4. keyId is no longer SHA256(secret). That construction let anyone holding a published manifest brute-force a low-entropy secret offline from signedBy. v2 uses an HMAC construction that cannot be checked against a candidate secret faster than attacking the signature itself.
  5. Ciphertext binding. Per-event AES keys are bound to the session, position, and event ID, and AES-GCM gets AAD, so keys stay unique under request ID reuse and input and output ciphertexts cannot be swapped within an event.

Chains, manifests, keyIds, and ciphertexts written under v1 do not verify under v2 and vice versa. The label strings are pinned by tests, and any future scheme change repeats this rotation ritual as v3. See the @mcpose/audit package docs for the v2 format details.

Adding a new ADR

New ADRs are added to docs/adr/ in the mcpose repository with sequential numbering: the next one is 0005-...md. Add an ADR when a decision is non-obvious or reverses a prior choice. Existing ADR numbers never change, so references stay stable.

Next steps

ADR-0005

The HTTP proxy binds loopback by default and derives enforcing Host and Origin allowlists.

ADR-0006

hiddenTools accepts a predicate so a dispatcher cannot reach a hidden tool by name.

ADR-0007

Local tools route to their handler from inside the innermost next, so the full pipeline still applies.

ADR-0008

Request _meta is stripped before it reaches the upstream.

ADR-0009

Result _meta is stripped before it reaches the client.

ADR-0010

The tool catalog is an egress channel, sanitized by an opt-in middleware.

ADR-0011

Proxy-originated outbound calls carry the inbound delegation chain, stamped by the host.

ADR-0012

Proxy identity is recorded as provenance, not as a principal.

ADR-0013

One governed endpoint over many upstreams, with double-underscore namespacing and no inferred routing.

ADR-0014

Prompt calls run a middleware pipeline and are audited as a new event kind.

ADR-0015

The sensitivity tier is covered by the chain, amending format v2 in place.

ADR-0016

The delegation chain crosses the wire in request _meta as unsigned attribution.

ADR-0017

The policy engine is deny-by-default middleware that stamps its decision on the context.

ADR-0018

Cryptographic erasure destroys stored subject keys and never touches the chain.

ADR-0019

The v3 release ships audit format v2, amended in place; the label rotation is reserved.

ADR-0020

There is no fintech identity package; identity mapping is host resolver code.

ADR-0021

A resumed session replays the client's initialize into a fresh transport, and carries its deadline with it.

ADR-0022

A mesh exposes upstream resources under mcpose://<backendKey>/<uri> and routes reads by that prefix alone.