Skip to content

Commit 1bedc24

Browse files
hyperpolymathclaude
andcommitted
Add K9-SVC and a2ml deployment narrative docs
Story-driven documentation (7 chapters each) walking through real deployment experiences: where contracts/attestations help, where they are overkill, and common failure modes. Covers gateway and automation router use cases with terminology glossaries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b8d92c0 commit 1bedc24

2 files changed

Lines changed: 692 additions & 0 deletions

File tree

docs/A2ML-EXPLAINED.adoc

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)
3+
= A2ML Attestations: What They Are and Where They Help
4+
:toc: left
5+
:toclevels: 3
6+
:icons: font
7+
:sectnums:
8+
9+
== Preface
10+
11+
A2ML (Attested Markup Language) has two faces. One is a document format — a
12+
Djot-like markup with typed guarantees. The other, and the one that matters
13+
here, is the _attestation envelope_: a content-addressable audit record that
14+
proves a decision was made, what the inputs were, and that nobody tampered
15+
with the record afterwards.
16+
17+
This document tells the story of actually using a2ml attestations in a
18+
production gateway and automation router. Where they help, where they are
19+
overkill, and what the terminology means.
20+
21+
For the a2ml format spec: `standards/a2ml/SPEC.adoc` +
22+
For K9-SVC contracts (which pair with a2ml): `docs/K9-SVC-EXPLAINED.adoc`
23+
24+
== The Problem: "Who Decided That?"
25+
26+
You are debugging a production incident. A request to `/api/v1/users/42` was
27+
denied with 403. The caller says they should have been allowed. You check the
28+
logs:
29+
30+
----
31+
[2026-02-28 14:23:07] DENY trust=untrusted exposure=authenticated path=/api/v1/users/42
32+
----
33+
34+
Okay, so the caller was untrusted and the endpoint requires authentication.
35+
But the caller says they sent a valid API key. You check the headers... and
36+
discover the log does not record headers. You have the _verdict_ but not the
37+
_evidence_.
38+
39+
Now multiply this by a hundred decisions per second across three services. When
40+
something goes wrong, you need to answer:
41+
42+
1. What was the exact input?
43+
2. What policy was applied?
44+
3. What was the decision?
45+
4. Can I prove the log entry has not been modified?
46+
47+
Without attestations, you are reconstructing history from incomplete, unsigned
48+
log lines.
49+
50+
== Chapter 1: "I'll Just Log More"
51+
52+
Your first attempt is to add more fields to the log:
53+
54+
[source,elixir]
55+
----
56+
Logger.info("access_decision",
57+
trust: trust_level,
58+
exposure: exposure_level,
59+
path: conn.request_path,
60+
method: conn.method,
61+
client_ip: client_ip,
62+
headers: sanitised_headers,
63+
decision: "deny",
64+
policy_version: current_policy_hash
65+
)
66+
----
67+
68+
This is better. You can now see the full context. But:
69+
70+
* **Logs are mutable.** Someone with access to the log aggregator can edit or
71+
delete entries. You have no proof that the log at 14:23:07 is the same log
72+
that was written at 14:23:07.
73+
* **Logs are not content-addressable.** If you have two log lines with the same
74+
timestamp, you cannot determine which is authentic.
75+
* **Log fields drift.** When you change the log format, historical entries
76+
become inconsistent with current entries. There is no schema enforcement
77+
across log lines.
78+
79+
=== What a2ml Attestations Add
80+
81+
An a2ml attestation wraps the decision in a content-addressable envelope:
82+
83+
[source,elixir]
84+
----
85+
%{
86+
version: "1.0",
87+
type: :access,
88+
issuer: "http-capability-gateway",
89+
issued_at: "2026-02-28T14:23:07.123Z",
90+
decision_hash: "a3f8b2c4d5e6...", # SHA-256 of the decision payload
91+
decision: %{
92+
trust: :untrusted,
93+
exposure: :authenticated,
94+
path: "/api/v1/users/42",
95+
method: "GET",
96+
verdict: :deny,
97+
policy_hash: "7b9e1f2a3c..."
98+
}
99+
}
100+
----
101+
102+
The `decision_hash` is the SHA-256 of the `decision` map, canonically serialised.
103+
If anyone modifies any field of the decision, the hash no longer matches. You can
104+
verify an attestation at any time by recomputing the hash and comparing.
105+
106+
This is not blockchain. There are no blocks, no consensus, no distributed
107+
ledger. It is a hash. Simple, fast, tamper-evident.
108+
109+
== Chapter 2: "Do I Need This for Every Request?"
110+
111+
You instrument the gateway to produce an attestation for every access decision.
112+
At 1000 requests per second, you are now producing 1000 attestation records per
113+
second. Each one has a SHA-256 hash computation, a JSON serialisation, and
114+
a write to the audit store (Redis, in this case).
115+
116+
After a day you check Redis: 86.4 million attestation records. Your Redis
117+
instance is using 12GB of memory for audit data.
118+
119+
**This is where a2ml attestations become expensive.** Every-request attestation
120+
is appropriate for high-security systems (financial transactions, healthcare
121+
data access, regulatory compliance). For a general-purpose API gateway, it is
122+
overkill.
123+
124+
=== What You Learn
125+
126+
Attest _decisions that matter_, not every request:
127+
128+
[horizontal]
129+
Always attest:: Access denials (every 403 needs a provable reason), trust level
130+
changes (escalation or de-escalation), policy reloads (the old and new policy
131+
hashes), circuit breaker state transitions.
132+
133+
Optionally attest:: Access grants to sensitive endpoints (configurable per
134+
route), routing decisions in the automation router (content-addressable plan).
135+
136+
Skip:: Health checks, metrics scrapes, static asset serving, internal
137+
keep-alive pings.
138+
139+
The automation router is different — there, every routing decision IS the
140+
product. You want an attestation for every plan because the plan determines
141+
which backend runs which infrastructure operation. A wrong routing decision
142+
can destroy a production environment. The cost of one SHA-256 per routing
143+
decision is negligible compared to the cost of an unauditable deployment
144+
mistake.
145+
146+
== Chapter 3: "The Sensitive Data Problem"
147+
148+
Your attestation for a routing decision includes the full decision payload.
149+
That payload includes the parameters the user sent. Those parameters include:
150+
151+
[source,json]
152+
----
153+
{
154+
"backend": "terraform-apply",
155+
"target": "production",
156+
"params": {
157+
"aws_access_key_id": "AKIA...",
158+
"aws_secret_access_key": "wJalrXUtnFEMI..."
159+
}
160+
}
161+
----
162+
163+
You just wrote AWS credentials to your audit log. In a content-addressable
164+
record. With a hash that proves it is authentic.
165+
166+
**This is a real security failure.** The attestation system, designed to improve
167+
security, has created a credential leak.
168+
169+
=== What You Learn
170+
171+
The attestation layer MUST redact sensitive parameters before hashing. The
172+
implementation maintains a list of sensitive key patterns:
173+
174+
[source,elixir]
175+
----
176+
@sensitive_keys ~w(password token secret key credential api_key
177+
access_key private_key)
178+
----
179+
180+
When building the attestation payload, any key matching these patterns gets its
181+
value replaced with `"[REDACTED]"`. The hash is computed over the _redacted_
182+
payload, so:
183+
184+
1. The hash still proves the _structure_ of the decision (which backend, which
185+
target, how many parameters).
186+
2. The hash does NOT prove the _values_ of sensitive parameters (because they
187+
are redacted before hashing).
188+
3. No credentials appear in the audit log.
189+
190+
This is a deliberate trade-off: you lose the ability to prove "the exact
191+
credential that was used" in exchange for not storing credentials in the audit
192+
system. For almost every use case, the structure proof is sufficient.
193+
194+
== Chapter 4: "Can I Use This in Git Forges?"
195+
196+
You want to attest commits. Every time someone pushes to your git forge, you
197+
generate an a2ml attestation recording the commit hash, the author, and the
198+
branch.
199+
200+
This works — but it duplicates what git already provides. A git commit IS a
201+
content-addressable record. The commit hash IS a SHA (SHA-1 or SHA-256 with
202+
newer git). The commit metadata already contains the author, timestamp, and
203+
parent references.
204+
205+
**A2ML attestations add nothing over git's built-in commit hashing for recording
206+
"who committed what."** Git is already a content-addressable audit trail for
207+
source code changes.
208+
209+
=== Where a2ml DOES Help in Git Forges
210+
211+
What git does NOT record:
212+
213+
* **Policy decisions.** When a CI pipeline decides to auto-merge a Dependabot PR,
214+
what policy was applied? Was the merge based on passing tests, review
215+
approval, or a timeout? Git records that the merge happened but not _why_.
216+
* **Access control decisions.** When a push is rejected by branch protection, what
217+
rule rejected it? Git returns "rejected" but the decision reasoning is
218+
ephemeral.
219+
* **Cross-repo propagation.** When you mirror a commit from GitHub to GitLab to
220+
Codeberg, how do you prove the mirrors are faithful? Git hashes prove content
221+
integrity per-commit, but a2ml can attest the _propagation decision_ ("mirror
222+
triggered at T, source hash X, destination hash Y, match: true").
223+
224+
The forge integration point is the _automation around git_, not git itself.
225+
Attesting CI/CD decisions, merge policies, and cross-forge propagation gives
226+
you an audit trail for the stuff that happens _between_ commits.
227+
228+
== Chapter 5: The Terminology
229+
230+
[cols="1,3"]
231+
|===
232+
| Term | Plain English
233+
234+
| **attestation**
235+
| A record that says "at time T, system S made decision D based on inputs I."
236+
The record carries a hash that proves it has not been modified.
237+
238+
| **content-addressable**
239+
| The attestation's identity IS its content. The ID is the SHA-256 hash of the
240+
decision payload. Same inputs → same hash. Different inputs → different hash.
241+
No separate ID generator needed.
242+
243+
| **decision_hash**
244+
| SHA-256 of the canonically serialised decision payload. This is the
245+
tamper-evidence mechanism. Recompute the hash from the stored decision; if it
246+
matches decision_hash, the record is authentic.
247+
248+
| **policy_hash**
249+
| SHA-256 of the policy rules that were in effect when the decision was made.
250+
If you later discover the policy was wrong, you can find all decisions made
251+
under that policy by searching for its hash.
252+
253+
| **envelope**
254+
| The wrapper around the decision: version, type, issuer, issued_at,
255+
decision_hash. The envelope metadata is NOT included in the decision_hash
256+
(the hash covers only the decision payload). This means you can add envelope
257+
metadata (like a verification timestamp) without invalidating the hash.
258+
259+
| **attestation type**
260+
| What kind of decision was attested. Types: `:routing` (which backend handles
261+
this?), `:access` (allow or deny?), `:policy` (policy reload), `:trust`
262+
(trust level change). The type is in the envelope, not the hash.
263+
264+
| **issuer**
265+
| Which system produced this attestation. In the gateway it is
266+
`"http-capability-gateway"`. In the router it is `"hybrid-automation-router"`.
267+
Used for provenance — you can trace an attestation back to its source.
268+
269+
| **verify**
270+
| Recompute the hash from the stored decision payload and compare it to the
271+
stored decision_hash. If they match, the record has not been tampered with.
272+
If they do not match, either the decision was modified or the hash was
273+
corrupted.
274+
275+
| **redaction**
276+
| Replacing sensitive values with `"[REDACTED]"` before computing the hash.
277+
The hash covers the redacted payload, so it proves structure but not sensitive
278+
values. This is a deliberate security trade-off.
279+
|===
280+
281+
== Chapter 6: How a2ml and K9-SVC Work Together
282+
283+
A2ML attestations and K9-SVC contracts are complementary layers:
284+
285+
[cols="1,2,2"]
286+
|===
287+
| | a2ml Attestation | K9-SVC Contract
288+
289+
| **Question answered**
290+
| "What happened and can I prove it?"
291+
| "Was it within spec and what do we do if not?"
292+
293+
| **When it acts**
294+
| After a decision is made (records the outcome)
295+
| Before and after execution (enforces the SLA)
296+
297+
| **What it produces**
298+
| A content-addressable audit record
299+
| A breach/success signal with enforcement action
300+
301+
| **Failure mode**
302+
| If attestation fails, the request still succeeds (audit gap)
303+
| If contract breaches, the breach policy fires (degrade, alert, circuit-break)
304+
305+
| **Storage**
306+
| Audit log (Redis, database, IPFS)
307+
| ETS table (in-memory, per-node)
308+
|===
309+
310+
In practice, the flow is:
311+
312+
----
313+
Request arrives
314+
315+
├─ SafeTrust evaluates access → a2ml attests the decision
316+
317+
├─ K9 contract pre-check (trust threshold, rate limit)
318+
319+
├─ Proxy to backend (timed)
320+
321+
├─ K9 contract post-check (latency SLA) → breach policy if exceeded
322+
323+
└─ a2ml attests the full outcome (including K9 breach if any)
324+
----
325+
326+
The attestation records that K9 fired a breach policy. K9 does not know or
327+
care about attestations. They are independent systems that compose naturally.
328+
329+
== Chapter 7: What a2ml Attestations Are Not
330+
331+
* **Not a replacement for logs.** Logs are for humans debugging at 3am.
332+
Attestations are for proving "the system made this decision" to an auditor,
333+
regulator, or post-incident review. You need both.
334+
* **Not a blockchain.** There are no blocks, no proof-of-work, no consensus
335+
protocol. It is a hash in a JSON envelope. Fast, simple, verifiable.
336+
* **Not encryption.** The attestation is plaintext. The hash proves integrity
337+
(not modified), not confidentiality (not readable). If you need secrecy,
338+
encrypt the audit store separately.
339+
* **Not useful for everything.** Health checks, metrics, and internal pings
340+
do not need attestation. The cost of hashing is low but the storage and
341+
query cost at high volume is real.
342+
343+
== Summary
344+
345+
A2ML attestations turn ephemeral log lines into content-addressable, tamper-evident
346+
audit records. They are most valuable for:
347+
348+
* Access denials (provable "why was I blocked?")
349+
* Routing decisions in infrastructure automation (provable "why did this backend
350+
get this operation?")
351+
* Policy changes (provable "what rules were in effect?")
352+
* Cross-service propagation decisions (provable "was the mirror faithful?")
353+
354+
They are least valuable for:
355+
356+
* Anything git already hashes (commits, trees, blobs)
357+
* High-volume, low-stakes requests (health checks, static assets)
358+
* Cases where the log already contains everything you need and tamper-evidence
359+
is not required
360+
361+
For the a2ml format spec: `standards/a2ml/SPEC.adoc` +
362+
For K9-SVC contracts: `docs/K9-SVC-EXPLAINED.adoc` +
363+
For anti-patterns and comparison tables: `standards/k9-svc/examples/NOT-a-good-fit.adoc`

0 commit comments

Comments
 (0)