@adastracomputing/ink 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/CODE_OF_CONDUCT.md +42 -0
  3. package/LICENSE-APACHE +201 -0
  4. package/LICENSE-MIT +21 -0
  5. package/README.md +133 -0
  6. package/SECURITY.md +57 -0
  7. package/docs/key-rotation-rule.md +108 -0
  8. package/docs/logo.svg +8 -0
  9. package/docs/maturity.md +81 -0
  10. package/docs/threat-model.md +150 -0
  11. package/package.json +72 -0
  12. package/specs/ink-agent-containment-and-governance-extension-spec.md +508 -0
  13. package/specs/ink-auditability.md +652 -0
  14. package/specs/ink-authorization-chain.md +242 -0
  15. package/specs/ink-compatibility-policy.md +263 -0
  16. package/specs/ink-compliance-checklist.md +309 -0
  17. package/specs/ink-containment-phase1-implementation-spec.md +593 -0
  18. package/specs/ink-introduction-receipts-extension.md +501 -0
  19. package/specs/ink-key-rotation-spec.md +535 -0
  20. package/src/crypto/ink.ts +902 -0
  21. package/src/crypto/keys.ts +211 -0
  22. package/src/crypto/multi-key-verify.ts +170 -0
  23. package/src/crypto/sign.ts +155 -0
  24. package/src/crypto/verify.ts +1 -0
  25. package/src/discovery/agent-card.ts +508 -0
  26. package/src/index.ts +59 -0
  27. package/src/ink/checkpoint.ts +75 -0
  28. package/src/ink/discovery-gating.ts +147 -0
  29. package/src/ink/handshake-budget.ts +413 -0
  30. package/src/ink/receipts.ts +114 -0
  31. package/src/ink/transport-auth.ts +96 -0
  32. package/src/middleware/ink-auth.ts +263 -0
  33. package/src/models/agent-card.ts +63 -0
  34. package/src/models/ink-audit.ts +205 -0
  35. package/src/models/ink-handshake.ts +123 -0
  36. package/src/models/intent.ts +201 -0
  37. package/src/models/key-entry.ts +52 -0
  38. package/src/models/profile.ts +31 -0
  39. package/test-vectors/README.md +129 -0
  40. package/test-vectors/encryption.json +90 -0
  41. package/test-vectors/handshake.json +482 -0
  42. package/test-vectors/jcs.json +30 -0
  43. package/test-vectors/key-rotation.json +101 -0
  44. package/test-vectors/keys.json +32 -0
  45. package/test-vectors/receipts-and-audit.json +142 -0
  46. package/test-vectors/replay.json +88 -0
  47. package/test-vectors/signing.json +61 -0
  48. package/test-vectors/witness.json +394 -0
@@ -0,0 +1,652 @@
1
+ # INK Protocol-Level Auditability (draft extension)
2
+
3
+ **Status:** Draft
4
+ **Authors:** Ad Astra Computing
5
+ **Date:** 2026-03-19
6
+
7
+ ## Problem
8
+
9
+ The current INK v0.1 model has strong internal audit capabilities (`AgentAuditStore` with 130+ event types, extension `AuditEntry` schema) but these are **application-layer**, they live inside each agent's host process and are not part of the wire protocol. This creates gaps:
10
+
11
+ 1. **No delivery receipts**, the sender doesn't know if the recipient's agent received, queued, rejected or acted on the message. The HTTP 200 from the inbox endpoint only means "I got it."
12
+ 2. **No cryptographic audit trail**, audit events are stored internally and could be modified. There's no tamper-evident log that both parties can reference.
13
+ 3. **No cross-agent audit reconciliation**, if Alice says she sent a message and Bob says he never got it, there's no shared record to resolve the dispute.
14
+ 4. **No standardized audit format**, each INK implementation would have to reverse-engineer tulpa's `AgentAuditStore` schema. The audit data isn't portable.
15
+
16
+ ## Design
17
+
18
+ ### 1. Message Receipts (Wire Protocol)
19
+
20
+ Receipts are a **new INK message type** (`network.tulpa.receipt`), not an intent type. INK v0.1 distinguishes message types (`network.tulpa.intent`, `network.tulpa.challenge`, `network.tulpa.resolution`, `network.tulpa.rejection`, `network.tulpa.encrypted`) from intent types (`scheduling`, `intro_request`, etc.) within `network.tulpa.intent` messages. Receipts are a distinct protocol-level concern and MUST NOT be shoehorned into the intent envelope.
21
+
22
+ ```json
23
+ {
24
+ "protocol": "ink/0.1",
25
+ "type": "network.tulpa.receipt",
26
+ "from": "did:plc:recipient",
27
+ "to": "did:plc:sender",
28
+
29
+ "messageId": "original-message-id",
30
+
31
+ "disposition": "received | delivered | acted | rejected | expired",
32
+
33
+ "dispositionAt": "2026-03-19T12:00:00Z",
34
+
35
+ "note": "optional detail (rejection reason, action taken)",
36
+
37
+ "messageHash": "<SHA-256 hash, see §1.1 for hash scope>",
38
+
39
+ "nonce": "<base64url-encoded 128-bit nonce>",
40
+ "timestamp": "2026-03-19T12:00:01Z"
41
+ }
42
+ ```
43
+
44
+ **Disposition types:**
45
+
46
+ | Disposition | Meaning |
47
+ |-------------|---------|
48
+ | `received` | Envelope accepted, queued for processing |
49
+ | `delivered` | Message shown to owner or processed by rule |
50
+ | `acted` | Owner/agent took action (accepted, declined, etc.) |
51
+ | `rejected` | Message rejected by pipeline |
52
+ | `expired` | Message expired before processing |
53
+
54
+ #### 1.1 `messageHash` Scope
55
+
56
+ `messageHash` is always the SHA-256 of the **JCS-canonicalized plaintext message body**, regardless of transport encryption.
57
+
58
+ For **plaintext messages** (type `network.tulpa.intent`, `network.tulpa.challenge`, etc.):
59
+ - `messageHash` = SHA-256 of the JCS-canonicalized INK message body, excluding transport headers (the `Authorization` header carries the signature, it is not part of the JSON body).
60
+
61
+ For **encrypted messages** (type `network.tulpa.encrypted`):
62
+ - `messageHash` = SHA-256 of the **decrypted plaintext intent body**, not the outer `InkEncryptedPayload` envelope.
63
+ - Rationale: both sender and recipient possess the plaintext (sender before encryption, recipient after decryption). Hashing the plaintext binds the receipt to semantic content rather than transport encoding and avoids sensitivity to ciphertext non-determinism.
64
+
65
+ Implementations MUST use JCS canonicalization before hashing to ensure byte-level determinism.
66
+
67
+ **Receipt flow:**
68
+
69
+ ```
70
+ Sender Recipient
71
+ |-- POST /ink/v1/intent -------->|
72
+ |<-- HTTP 200 { accepted } ------|
73
+ | | (processes message)
74
+ |<-- POST /ink/v1/receipt -------| (type: network.tulpa.receipt)
75
+ |-- HTTP 200 ------------------->|
76
+ ```
77
+
78
+ **Properties:**
79
+ - Receipts are full INK messages: signed per §3.3 (METHOD + PATH + recipientDid + JCS(body) + timestamp), with nonce and timestamp for replay protection per §3.5
80
+ - Receipts are delivered via `POST /ink/v1/receipt` (a new endpoint, separate from `/ink/v1/intent`)
81
+ - Receipts are **opt-in** per agent, advertised in the Agent Card capabilities
82
+ - Receipts for receipts are NOT sent (receiving a `network.tulpa.receipt` MUST NOT trigger a receipt response, loop prevention)
83
+ - The `from`/`to` fields are reversed relative to the original message (the recipient becomes the sender of the receipt)
84
+
85
+ **Agent Card capability:**
86
+
87
+ ```typescript
88
+ capabilities: {
89
+ receipts: {
90
+ send: true, // "I send receipts for messages I receive"
91
+ dispositions: [ // which disposition types I report
92
+ "received",
93
+ "delivered",
94
+ "acted",
95
+ "rejected",
96
+ ],
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### 2. Audit Event Envelope (Portable Format)
102
+
103
+ Define a standardized audit event format that any INK implementation can produce and consume.
104
+
105
+ ```typescript
106
+ InkAuditEventSchema = z.object({
107
+ // Event identity
108
+ id: z.string(), // ULID
109
+ version: z.literal("ink-audit/1"),
110
+
111
+ // Who logged this event
112
+ agentId: z.string(),
113
+ agentSignature: z.string(), // Ed25519 signature over the event (minus this field)
114
+
115
+ // Chain position (inspired by SSB feed structure)
116
+ sequence: z.number().int().positive(), // monotonically increasing, starting at 1
117
+ previousEventHash: z.string().nullable(), // SHA-256 of the prior event; null for sequence=1
118
+
119
+ // What happened
120
+ eventType: z.enum([
121
+ // Message lifecycle
122
+ "message.sent",
123
+ "message.received",
124
+ "message.queued",
125
+ "message.delivered",
126
+ "message.acted",
127
+ "message.rejected",
128
+ "message.expired",
129
+ "message.retracted",
130
+ // Receipt lifecycle
131
+ "receipt.sent",
132
+ "receipt.received",
133
+ // Delegation
134
+ "delegation.granted",
135
+ "delegation.used",
136
+ "delegation.revoked",
137
+ "delegation.expired",
138
+ // Connection
139
+ "connection.requested",
140
+ "connection.accepted",
141
+ "connection.declined",
142
+ // Verification
143
+ "signature.verified",
144
+ "signature.verified_retired",
145
+ "signature.failed",
146
+ "signature.revoked_rejected",
147
+ "replay.detected",
148
+ // Key lifecycle
149
+ "key.rotated",
150
+ "key.revoked",
151
+ // Introduction lifecycle
152
+ "introduction.requested",
153
+ "introduction.approved",
154
+ "introduction.declined",
155
+ "introduction.forwarded",
156
+ "introduction.completed",
157
+ "introduction.expired",
158
+ "introduction.receipt_sent",
159
+ "introduction.receipt_received",
160
+ // Enclave lifecycle
161
+ "enclave.requested",
162
+ "enclave.authorized",
163
+ "enclave.opened",
164
+ "enclave.operation_submitted",
165
+ "enclave.resolved",
166
+ "enclave.expired",
167
+ "enclave.aborted",
168
+ "enclave.receipt_sent",
169
+ "enclave.receipt_received",
170
+ // Containment
171
+ "transport_scope_violation",
172
+ "handshake_rate_limited",
173
+ "handshake_budget_exhausted",
174
+ "discovery_query_received",
175
+ "discovery_query_granted",
176
+ "discovery_query_denied",
177
+ ]),
178
+
179
+ // Event timestamp
180
+ timestamp: z.string().datetime(),
181
+
182
+ // References
183
+ messageId: z.string().optional(),
184
+ correlationId: z.string().optional(),
185
+ counterpartyId: z.string().optional(), // the other agent involved
186
+ signingKeyId: z.string().optional(), // key used to sign this event, for historical verification
187
+
188
+ // Event-specific data (schema varies by eventType)
189
+ data: z.record(z.unknown()).optional(),
190
+ });
191
+ ```
192
+
193
+ **Tamper evidence (hash chain + sequence numbers):**
194
+
195
+ The audit chain uses **both** a hash chain and a monotonic sequence number, following SSB's feed model:
196
+
197
+ - **`sequence`:** monotonically increasing integer starting at 1. Provides a human-readable position and makes gaps immediately detectable (if you see sequence 5 followed by 7, sequence 6 was deleted or suppressed).
198
+ - **`previousEventHash`:** SHA-256 of the JCS-canonicalized prior event (excluding `agentSignature`). Null for the first event (sequence=1). Provides cryptographic chain linkage, if any event is modified, all subsequent hashes break.
199
+ - **`agentSignature`:** Ed25519 signature over the JCS-canonicalized event (excluding the `agentSignature` field itself). Proves the agent attested to this event at this chain position.
200
+
201
+ **Fork detection (per SSB):**
202
+ - If an agent presents two different events with the same `sequence` number, the chain is forked. A forked chain SHOULD be treated as untrusted during reconciliation.
203
+ - During audit exchange (§3), both parties can compare sequence numbers and hashes. If Alice has sequence 1-10 for a message and Bob has sequence 1-8, the gap is immediately visible. If their hashes diverge at sequence 5, that's the point of tampering.
204
+
205
+ The chain is per-agent (not global), each agent maintains its own append-only log.
206
+
207
+ ### 3. Audit Exchange Protocol
208
+
209
+ Agents can request audit records from each other for reconciliation.
210
+
211
+ **New endpoint:** `POST /ink/v1/audit`
212
+
213
+ Audit queries use **POST** (not GET) to fit INK's existing authentication model. INK v0.1 auth (§3.3) signs `METHOD + PATH + recipientDid + JCS(body) + timestamp`, which requires a request body for canonicalization. GET requests have no body, so they cannot be authenticated or replay-protected under the current INK auth scheme.
214
+
215
+ **Agent Card advertisement:**
216
+
217
+ ```typescript
218
+ {
219
+ endpoint: "https://agent.example.com/ink/v1",
220
+ capabilities: {
221
+ auditExchange: true, // "I support the audit exchange protocol"
222
+ }
223
+ }
224
+ ```
225
+
226
+ **Request (signed per §3.3, replay-protected per §3.5):**
227
+
228
+ ```json
229
+ POST /ink/v1/audit
230
+ Authorization: INK-Ed25519 <signature>
231
+
232
+ {
233
+ "protocol": "ink/0.1",
234
+ "type": "network.tulpa.audit_query",
235
+ "from": "did:plc:alice",
236
+ "to": "did:plc:bob",
237
+ "messageId": "msg-123",
238
+ "nonce": "<base64url-encoded 128-bit nonce>",
239
+ "timestamp": "2026-03-19T12:00:00Z"
240
+ }
241
+ ```
242
+
243
+ The signature base follows §3.3 exactly:
244
+ ```
245
+ signatureBase = "ink/0.1\nPOST\n/ink/v1/audit\ndid:plc:bob\n{...JCS(body)...}\n2026-03-19T12:00:00Z"
246
+ ```
247
+
248
+ **Response:**
249
+
250
+ ```json
251
+ {
252
+ "protocol": "ink/0.1",
253
+ "type": "network.tulpa.audit_response",
254
+ "messageId": "msg-123",
255
+ "events": [ /* InkAuditEvent[] */ ],
256
+ "responseSignature": "<Ed25519 signature over JCS(events array)>"
257
+ }
258
+ ```
259
+
260
+ The `responseSignature` is the responder's Ed25519 signature over the JCS-canonicalized `events` array, allowing the requester to prove the responder attested to this specific audit slice. The signature alone does NOT prove the slice is internally consistent. Consumers MUST run two checks before treating the events as authoritative: `verifyAuditResponseSignature(events, signature, key)` (proves the responder produced this exact slice) AND `verifyAuditEventChain(events)` (proves the slice has no internal `sequence_gap`, `sequence_fork`, or `previous_hash_mismatch`). A slice that passes one and fails the other MUST be rejected.
261
+
262
+ **Access control:**
263
+ - The responder MUST verify (via its message store) that the requester's DID (`from`) is either the sender or recipient of the referenced `messageId`. If not, return error code `access_denied`.
264
+ - The request is authenticated and replay-protected using the standard INK auth flow, no special-case logic needed.
265
+ - Events are filtered to only include the specific message's lifecycle.
266
+
267
+ ### 4. Dispute Resolution
268
+
269
+ When two agents disagree about a message's status, they perform mutual audit exchange via `POST /ink/v1/audit` (§3):
270
+
271
+ ```
272
+ Alice Bob
273
+ |-- POST /ink/v1/audit (messageId=123) ----->|
274
+ |<-- { events: [...], responseSignature } ---|
275
+ | |
276
+ | Alice has her own events for msg-123 |
277
+ | and Bob's signed events for msg-123 |
278
+ | |
279
+ | Compare sequence numbers and hashes: |
280
+ | - Matching hashes: agreement |
281
+ | - Divergent hashes: flag for human review |
282
+ | - Sequence gaps: events were suppressed |
283
+ | - Fork (same sequence, different hash): |
284
+ | chain is untrusted |
285
+ ```
286
+
287
+ **Reconciliation algorithm:**
288
+ 1. Both agents exchange audit events for the disputed message via `POST /ink/v1/audit`
289
+ 2. Each response is signed by the responder (`responseSignature`), creating non-repudiable evidence of what each party claims happened
290
+ 3. Verify each response with `verifyAuditResponseSignature`. Reject slices that fail.
291
+ 4. Run `verifyAuditEventChain(events)` on each verified slice. Reject slices that return `sequence_gap`, `sequence_fork`, or `previous_hash_mismatch`, a slice that fails this gate is internally inconsistent regardless of who signed it.
292
+ 5. Compare `sequence` numbers and `previousEventHash` chains across the two slices to find the earliest point of divergence
293
+ 6. If the recipient has `message.received` but not `message.delivered`, the message was lost internally
294
+ 7. If the sender has `message.sent` but the recipient has no events, the message was lost in transit
295
+ 8. Both parties' signed responses can be presented to a human mediator if automated reconciliation fails
296
+
297
+ ### 5. Audit Retention and Export
298
+
299
+ **Retention policy (protocol-level recommendation):**
300
+ - Message lifecycle events: 12 months minimum
301
+ - Delegation events: lifetime of the delegation + 12 months
302
+ - Connection events: lifetime of the connection + 6 months
303
+
304
+ **Export format:**
305
+ - JSON Lines (one `InkAuditEvent` per line, newline-delimited)
306
+ - File naming: `ink-audit-{agentId}-{startDate}-{endDate}.jsonl`
307
+ - Includes a trailing line with the final hash chain value for integrity verification
308
+
309
+ ### 6. Integration with Existing AgentAuditStore
310
+
311
+ The internal `AgentAuditStore` (130+ event types, rich metadata) remains the primary audit system for the tulpa application layer. The INK audit protocol is a **subset** designed for interoperability:
312
+
313
+ | Concern | AgentAuditStore | INK Audit Protocol |
314
+ |---------|----------------|--------------------|
315
+ | Scope | All agent activity | Cross-agent message lifecycle only |
316
+ | Event types | 130+ application-specific | ~20 protocol-standard |
317
+ | Audience | Owner dashboard | Other INK agents and dispute resolution |
318
+ | Tamper evidence | No (mutable SQL) | Yes (hash chain) |
319
+ | Signature | No | Yes (Ed25519 per event) |
320
+ | Portability | Tulpa-specific schema | Standard INK format |
321
+
322
+ **Bridge:** when the internal audit store logs a message event (e.g. `intent_received`, `intent_sent`), a corresponding INK audit event is generated and appended to the protocol-level hash chain.
323
+
324
+ ## Migration Path
325
+
326
+ 1. Add `network.tulpa.receipt` as a new INK message type with `POST /ink/v1/receipt` endpoint
327
+ 2. Add `network.tulpa.audit_query` / `network.tulpa.audit_response` as new INK message types with `POST /ink/v1/audit` endpoint
328
+ 3. Add receipt generation to `receiveMessage` pipeline (opt-in via agent config)
329
+ 4. Add `InkAuditEvent` generation alongside existing `AgentAuditStore` logging, with sequence numbers and hash chain
330
+ 5. Advertise receipt and audit capabilities in Agent Card
331
+ 6. All new endpoints use the existing INK auth model (§3.3) and replay protection (§3.5), no special-case authentication
332
+ 7. _(Future)_ Deploy a INK-native audit service (§7) for third-party witnessing of high-value interactions
333
+
334
+ ## Prior Art and Research
335
+
336
+ This design was validated against established receipt and audit trail protocols.
337
+
338
+ ### Message Disposition Notification (MDN, RFC 8098)
339
+ Email's receipt protocol. INK's disposition types are directly influenced by MDN's action/disposition model:
340
+ - MDN separates **action mode** (`manual-action` vs `automatic-action`) from **disposition type** (`displayed`, `deleted`, `processed`, `denied`, `failed`). INK simplifies this into a single `disposition` enum since INK agents always process programmatically.
341
+ - **Critical lesson: MDN is advisory and unreliable.** Receipts can be silently suppressed by intermediaries, spam filters or the recipient's MUA. You cannot distinguish "not read" from "MDN suppressed." INK addresses this by making receipts protocol-level intent messages (signed and delivered via the same inbox mechanism), not a separate transport.
342
+ - MDN's `denied` disposition lets a recipient refuse to send a receipt without revealing read status. INK adopts this via selective disposition reporting in the Agent Card.
343
+ - **MDN has no delivery receipt**, that's DSN (RFC 3461). INK's `received` disposition covers delivery; `delivered`/`acted` cover read/action. This is a deliberate consolidation.
344
+
345
+ ### XMPP Receipts (XEP-0184) and Chat Markers (XEP-0333)
346
+ XMPP provides two relevant patterns:
347
+ - **Cumulative acknowledgment:** marking message N as `displayed` implicitly marks messages 1..N. This reduces bandwidth in catch-up scenarios. INK does NOT adopt this, INK messages are not linearly ordered (they span multiple conversations/correlationIds), so cumulative receipts would be ambiguous.
348
+ - **Disposition escalation:** XEP-0333 defines `received` → `displayed` → `acknowledged` as increasing levels of confirmation. INK's `received` → `delivered` → `acted` follows the same escalation pattern.
349
+ - **No tamper evidence.** XMPP receipts are plaintext XML within TLS. A malicious server can forge or suppress them. INK's receipts are Ed25519-signed by the recipient's key.
350
+
351
+ ### Matrix Protocol Receipts
352
+ Matrix puts receipts outside the persistent DAG (as ephemeral EDUs). Key lessons:
353
+ - **Receipts as ephemeral vs. persistent is a core design choice.** Matrix chose ephemeral to avoid bloating the room DAG. INK puts receipts IN the audit hash chain (persistent) because they serve as evidence, not just UX signals. This is a deliberate tradeoff, more storage for stronger guarantees.
354
+ - **Federation receipt loss.** Matrix receipts can be lost during federation disruptions with no replay mechanism. INK mitigates this by treating receipts as regular intent messages with the same delivery guarantees.
355
+ - **Private read receipts.** Matrix added `m.read.private` because public receipts leaked too much. INK's selective disposition reporting (Agent Card `capabilities.receipts.dispositions`) achieves the same, an agent can report only `received` and `rejected` but not `delivered` or `acted`.
356
+
357
+ ### Certificate Transparency (RFC 6962)
358
+ CT's Merkle tree approach is the gold standard for tamper-evident append-only logs:
359
+ - **Signed Certificate Timestamp (SCT) as receipt.** CT's SCT is a signed promise: "I will include this in my log within the Maximum Merge Delay." This is stronger than INK's receipts, CT receipts come from an independent third party (the log), not the recipient. INK's receipts are bilateral (between sender and recipient), which is simpler but has weaker trust properties.
360
+ - **Inclusion proofs and consistency proofs.** CT can prove a specific entry exists in the log (inclusion) and that the log is append-only (consistency). INK's hash chain provides append-only evidence but not efficient inclusion proofs. For INK's scale (50 messages/day per agent), the hash chain is sufficient, Merkle trees add complexity without proportional benefit.
361
+ - **The split-view attack.** A malicious CT log can show different views to different clients. INK has the same risk, a malicious agent can maintain two different hash chains. Mutual audit exchange (Section 4) is INK's mitigation, analogous to CT's gossip protocol.
362
+
363
+ ### Secure Scuttlebutt (SSB)
364
+ SSB's single-writer append-only feed is the closest analog to INK's per-agent hash chain:
365
+ - **Hash chain structure.** Each SSB message contains `previous` (hash of prior message), `sequence` (monotonic counter), `author` (public key) and `signature`. INK's `InkAuditEvent` adopts the same pattern with `previousEventHash`.
366
+ - **Fork detection.** SSB detects when a feed owner publishes two messages with the same sequence number. The feed is permanently "poisoned." INK should adopt fork detection: if an agent presents two different events with the same sequence position, the chain is untrusted.
367
+ - **No editing or deletion.** SSB's immutability is a feature for audit but a problem for GDPR. INK addresses this with the `redact` capability in `AgentAuditStore`, the event remains in the chain but its content is replaced with `[redacted]`.
368
+ - **JSON canonicalization.** SSB's signature is over `JSON.stringify` with specific key ordering, which has caused interop bugs. INK uses JCS (RFC 8785) canonicalization, which is a proper standard.
369
+
370
+ ### DIDComm Messaging v2
371
+ DIDComm's problem reports and trust ping provide patterns for cross-agent status signaling:
372
+ - **Problem reports** use structured error codes (`e.p.msg.not-understood`, `e.m.req.not-accepted`). INK's receipt `note` field is simpler, a free-text rejection reason. Consider adopting structured codes in a future version.
373
+ - **Deniability vs. non-repudiation.** DIDComm explicitly offers both modes (signed JWS for non-repudiation, authcrypt for deniability). INK receipts are always signed (non-repudiation). This is the right choice for audit trails, deniability and auditability are fundamentally at odds.
374
+ - **No built-in receipt protocol.** DIDComm v2 deliberately omits a core receipt protocol because its multi-transport model (HTTP, WebSocket, Bluetooth, QR) makes reliable delivery confirmation impossible. INK avoids this by standardizing on HTTP.
375
+
376
+ ### COSE Receipts / SCITT (Supply Chain Integrity, Transparency and Trust)
377
+ SCITT applies CT concepts to arbitrary claims, not just certificates:
378
+ - **Access-controlled transparency.** Unlike CT (fully public), SCITT supports Merkle proofs over access-controlled logs. This is critical for agent protocols where message contents should not be publicly visible. INK's `/audit` endpoint with sender/recipient access control follows this model.
379
+ - **Multiple transparency services.** SCITT allows a single statement to accumulate receipts from multiple independent services. INK could adopt this for high-stakes messages, both agents publish audit events to an independent transparency service for stronger guarantees.
380
+ - **Separation of issuer and transparency service.** In SCITT, the entity making claims is separate from the entity recording them. INK currently has agents self-recording their own audit events. For stronger guarantees, INK could support optional third-party audit services.
381
+
382
+ ### C2SP Witness Cosigning Protocol
383
+ The Community Cryptography Specification Project defines a witness protocol for transparency logs. Witnesses verify consistency proofs between checkpoints (signed tree heads) and return cosignatures, they never see log contents, only tree sizes and root hashes. Key properties:
384
+ - **Privacy by design.** A witness attests "this log is append-only" without knowing what's in it. Perfect for INK where message content is private.
385
+ - **Single honest witness sufficiency.** One non-colluding witness is enough to detect a split-view attack (a log presenting different histories to different clients).
386
+ - **Ed25519 native.** Checkpoints and cosignatures use Ed25519 note signatures, matching INK's key model exactly.
387
+ - **Lightweight.** One HTTP request per checkpoint, not per event. At INK's scale (~50 events/day), an agent might publish one checkpoint per hour, 24 witness requests/day.
388
+
389
+ The checkpoint format (`tlog-checkpoint`) is a simple text format: origin line, tree size, base64 root hash, followed by note signatures. INK's per-agent hash chain maps naturally to this, the agent is the "log" and publishes periodic checkpoints.
390
+
391
+ ### Sigstore (Rekor, Fulcio, Cosign)
392
+ Sigstore provides a transparency log (Rekor) for software supply chain signatures. Rekor v2 (GA 2025) uses Trillian Tessera with integrated witness cosigning. Relevant patterns:
393
+ - **Hash notarization.** Rekor accepts `(artifact_hash, signature, public_key)` tuples. INK could submit `(audit_chain_hash, agent_signature, agent_public_key)` to get a free, independent timestamp proof. Content stays private, only the hash is public.
394
+ - **Rekor v2 tile-based architecture.** Static file serving for Merkle tree tiles, reducing infrastructure cost dramatically. If INK builds its own transparency service, this architecture is the model.
395
+ - **Public key visibility tradeoff.** Rekor entries expose the signer's public key. For INK, this means agent DIDs would be visible even though message content is not. Acceptable for agents that want public transparency; use witnesses instead for full privacy.
396
+
397
+ ### OpenTimestamps
398
+ A Bitcoin-anchored timestamping protocol. Aggregates hashes into a Merkle tree and anchors the root in a Bitcoin transaction. Provides the strongest possible "this data existed at time T" proof (Bitcoin's security model) at the cost of 1–2 hour confirmation latency. Free calendar servers, no registration. Best suited as a periodic anchor (once/day) for the audit chain's latest hash, providing legally defensible timestamps for compliance scenarios.
399
+
400
+ ### Design Decisions Informed by Research
401
+
402
+ | Decision | Rationale | Prior Art |
403
+ |----------|-----------|-----------|
404
+ | Receipts as signed intent messages | Stronger than advisory MDN/XMPP; same delivery path as regular messages | MDN lesson: advisory receipts are unreliable |
405
+ | Per-message receipts (not cumulative) | INK messages aren't linearly ordered across conversations | XMPP cumulative receipts assume linear order |
406
+ | Receipts in hash chain (persistent) | Audit evidence, not just UX | Matrix chose ephemeral; INK needs evidence |
407
+ | Per-agent hash chain (not Merkle tree) | Sufficient for INK's scale; simpler | SSB feeds vs. CT Merkle trees |
408
+ | Selective disposition reporting | Privacy control for recipients | Matrix `m.read.private`, MDN `denied` |
409
+ | JCS canonicalization for signing | Proper standard, avoids SSB's JSON.stringify bugs | SSB interop issues |
410
+ | Access-controlled audit endpoint | Messages are private; audit should be too | SCITT access-controlled transparency |
411
+ | Fork detection on hash chain | Detect malicious chain rebuilds | SSB fork poisoning |
412
+ | Witness cosigning over full transparency service | Privacy-preserving, minimal infrastructure | C2SP `tlog-witness`, CT gossip |
413
+ | Tiered approach (witnesses → SCITT → Tessera) | Match effort to threat model | Sigstore ecosystem layering |
414
+
415
+ ### 7. Third-Party Audit Services (Optional)
416
+
417
+ Bilateral audit exchange (§3–§4) has a fundamental limitation: a malicious agent can maintain two different hash chains and show each counterparty a different history. This is the **split-view attack**, well-known from Certificate Transparency research. Mutual exchange detects inconsistencies only when both parties compare, it can't prevent a malicious agent from presenting a consistent-looking but fabricated chain to each party independently.
418
+
419
+ Third-party audit services solve this by introducing an independent witness that neither party controls.
420
+
421
+ #### 7.0 Service Identity and Auth Model
422
+
423
+ A third-party audit service is a **INK service role**, not a standard INK agent. It differs from the human-delegate model (§2) in several ways:
424
+
425
+ | Concern | INK Agent | Audit Service |
426
+ |---------|-----------|--------------|
427
+ | Identity | DID bound to a human via `agentLink` | `did:web` or `did:key`, self-sovereign, no human owner |
428
+ | Discovery | `TulpaAgentEndpoint` in DID document | Advertised in subscribing agents' Agent Card `capabilities.thirdPartyAudit.services` |
429
+ | Auth (inbound) | INK auth §3.3, verifies sender's `agentLink` delegation | INK auth §3.3, verifies sender's `agentLink` delegation (same as any INK endpoint) |
430
+ | Auth (outbound) | Signs with `agentLink.signingKeyMultibase` | Signs with its own Ed25519 key (published in subscribing agents' Agent Card) |
431
+ | Delegation proof | Required, must trace authority back to a human DID | Not applicable, the service is independently trusted by each subscribing agent |
432
+
433
+ **Key distinctions:**
434
+
435
+ 1. **No `agentLink` verification.** When an agent receives a response from an audit service, it does NOT verify an `agentLink` delegation chain. Instead, it verifies the response signature against the service's public key as configured in the agent's own `capabilities.thirdPartyAudit.services[].publicKey`. Trust in the service is **configured, not discovered**, the agent operator chose to use this service.
436
+
437
+ 2. **Inbound auth is standard INK.** When agents submit events TO the service, the service verifies the submitter's identity via standard INK auth (§3.3), resolve the sender's DID, find their `agentLink`, verify the signature. The service is a normal INK recipient in this direction.
438
+
439
+ 3. **Service DID resolution.** The service's `did:web` (or `did:key`) is resolved normally for TLS binding and key discovery, but the service does NOT need a `TulpaAgentEndpoint` service entry in its DID document. Its endpoint is provided directly in the subscribing agent's Agent Card configuration.
440
+
441
+ 4. **No inbox, no intents.** The audit service does not accept INK intents, challenges or resolutions. It exposes only the audit-specific endpoints (`/ink/v1/audit/submit`, `/ink/v1/audit/query`).
442
+
443
+ Implementations MUST treat the audit service as an external dependency, not a INK peer. If the service is unavailable, agents fall back to bilateral audit exchange (§3–§4).
444
+
445
+ #### 7.1 Architecture
446
+
447
+ ```
448
+ Agent A Audit Service Agent B
449
+ | | |
450
+ |-- submit(event) --------→ | |
451
+ |←- receipt(inclusion) ----- | |
452
+ | | |
453
+ | | ←------- submit(event) -------|
454
+ | | -------- receipt(inclusion) -→ |
455
+ | | |
456
+ |-- query(messageId) -----→ | |
457
+ |←- proof(events, merkle) -- | |
458
+ ```
459
+
460
+ The audit service:
461
+ 1. Accepts signed `InkAuditEvent` submissions from agents
462
+ 2. Appends them to a **Merkle tree** (not just a hash chain, enables efficient inclusion proofs)
463
+ 3. Returns a **signed inclusion receipt** proving the event was recorded at a specific tree position and timestamp
464
+ 4. Serves **inclusion proofs** and **consistency proofs** on demand
465
+
466
+ The service CANNOT forge events (they carry the submitting agent's Ed25519 signature). It CAN prove:
467
+ - That a specific event was submitted at a specific time (inclusion)
468
+ - That the log is append-only and no events have been removed (consistency)
469
+ - That two parties submitted conflicting events for the same message (conflict detection)
470
+
471
+ #### 7.2 Submission Protocol
472
+
473
+ Agents submit audit events to the service alongside their normal hash chain maintenance. Submission is **asynchronous** and **non-blocking**, the agent does not wait for the service receipt before proceeding.
474
+
475
+ ```json
476
+ POST /ink/v1/audit/submit
477
+ Authorization: INK-Ed25519 <signature>
478
+
479
+ {
480
+ "protocol": "ink/0.1",
481
+ "type": "network.tulpa.audit_submit",
482
+ "from": "did:plc:agent",
483
+ "to": "did:web:audit.example.com",
484
+ "event": { /* InkAuditEvent */ },
485
+ "nonce": "<base64url>",
486
+ "timestamp": "2026-03-19T12:00:00Z"
487
+ }
488
+ ```
489
+
490
+ **Response (Signed Inclusion Receipt):**
491
+
492
+ ```json
493
+ {
494
+ "protocol": "ink/0.1",
495
+ "type": "network.tulpa.audit_inclusion",
496
+ "eventId": "01JBTEST0001",
497
+ "treeSize": 48291,
498
+ "leafIndex": 48290,
499
+ "rootHash": "<SHA-256 hex of Merkle tree root>",
500
+ "timestamp": "2026-03-19T12:00:01Z",
501
+ "serviceSignature": "<Ed25519 signature over (eventId + treeSize + rootHash + timestamp)>"
502
+ }
503
+ ```
504
+
505
+ The inclusion receipt is analogous to CT's Signed Certificate Timestamp (SCT). The agent stores it alongside the audit event and can present it as proof of timely submission.
506
+
507
+ #### 7.3 Verification Protocol
508
+
509
+ Any party to a message can request the service's view of the audit trail:
510
+
511
+ ```json
512
+ POST /ink/v1/audit/query
513
+ Authorization: INK-Ed25519 <signature>
514
+
515
+ {
516
+ "protocol": "ink/0.1",
517
+ "type": "network.tulpa.audit_query",
518
+ "from": "did:plc:requester",
519
+ "to": "did:web:audit.example.com",
520
+ "messageId": "msg-123",
521
+ "nonce": "<base64url>",
522
+ "timestamp": "2026-03-19T13:00:00Z"
523
+ }
524
+ ```
525
+
526
+ **Response includes Merkle inclusion proofs:**
527
+
528
+ ```json
529
+ {
530
+ "protocol": "ink/0.1",
531
+ "type": "network.tulpa.audit_response",
532
+ "messageId": "msg-123",
533
+ "events": [ /* InkAuditEvent[] from all agents */ ],
534
+ "proofs": [
535
+ {
536
+ "eventId": "01JBTEST0001",
537
+ "leafIndex": 48290,
538
+ "inclusionPath": ["<hash>", "<hash>", "..."],
539
+ "treeSize": 48291,
540
+ "rootHash": "<SHA-256 hex>"
541
+ }
542
+ ],
543
+ "serviceSignature": "<Ed25519 over JCS(response)>"
544
+ }
545
+ ```
546
+
547
+ #### 7.4 Access Control
548
+
549
+ The audit service operates under **access-controlled transparency** (per SCITT):
550
+ - Events are tagged with the `messageId` and the DIDs of sender/recipient
551
+ - Only the sender, recipient or a party with a valid delegation chain (§ INK Authorization Chain) can query events for a given `messageId`
552
+ - The service verifies the requester's identity via INK auth (§3.3) before serving events
553
+ - The Merkle tree structure is public (anyone can verify consistency proofs) but event contents are access-controlled
554
+
555
+ This follows SCITT's model: the transparency guarantee (append-only, no suppression) is public, but the data itself is private.
556
+
557
+ #### 7.5 Trust Model
558
+
559
+ The audit service is a **semi-trusted witness**, not an arbiter:
560
+ - It CANNOT forge events (Ed25519 signatures from agents)
561
+ - It CANNOT modify events without breaking Merkle proofs
562
+ - It CAN suppress events by refusing to include them (detectable via consistency proofs between submissions)
563
+ - It CAN be unavailable (agents fall back to bilateral exchange)
564
+ - It CAN collude with one party to suppress the other's events (mitigated by using multiple services)
565
+
566
+ **Multiple services:** For high-stakes interactions, agents MAY submit to multiple independent audit services. If any one service is compromised, the others still have the complete record. This mirrors CT's approach of requiring certificates to appear in multiple independent logs.
567
+
568
+ #### 7.6 Agent Card Advertisement
569
+
570
+ ```typescript
571
+ capabilities: {
572
+ auditExchange: true,
573
+ thirdPartyAudit: {
574
+ services: [
575
+ {
576
+ endpoint: "https://audit.example.com/ink/v1",
577
+ did: "did:web:audit.example.com",
578
+ publicKey: "<Ed25519 public key hex>"
579
+ }
580
+ ],
581
+ submitPolicy: "all" // "all" | "high_value" | "none"
582
+ }
583
+ }
584
+ ```
585
+
586
+ `submitPolicy` controls which events are submitted:
587
+ - `all`: every audit event is submitted to the service
588
+ - `high_value`: only events for messages with encryption or delegation chains
589
+ - `none`: advertised but not actively submitting (can still query)
590
+
591
+ #### 7.7 Implementation Options
592
+
593
+ INK does not mandate a specific transparency log implementation. The options fall into three tiers based on effort and guarantees:
594
+
595
+ **Tier 1, Lowest effort, highest immediate value:**
596
+
597
+ | Approach | How it works | Privacy | Cost |
598
+ |----------|-------------|---------|------|
599
+ | **Witness cosigning** (C2SP `tlog-witness`) | Agents publish periodic checkpoints of their hash chain. Independent witnesses verify consistency proofs and return cosignatures. Split-view attacks become detectable when clients compare cosigned roots or use multiple witnesses. | Inherent, witnesses see only tree size + root hash, never event content | Free (public witnesses exist) |
600
+ | **Rekor hash notary** (Sigstore) | Submit SHA-256 hashes of audit chain checkpoints to Rekor's public Merkle log. Provides an independent timestamp proof that a chain state existed at time T. | Hash-only, public key and timing are visible, content is not | Free (rekor.sigstore.dev) |
601
+
602
+ Witness cosigning is the **recommended starting point**. The C2SP witness protocol (`tlog-witness`, `tlog-cosignature`, `tlog-checkpoint`) uses Ed25519 natively and maps directly to INK's existing hash chain. Agents already compute sequential hashes, publishing a checkpoint is just exposing the latest `(sequence, rootHash)` pair. The checkpoint format:
603
+
604
+ ```
605
+ ink-audit/<agentDid>
606
+ <sequence number>
607
+ <base64 root hash>
608
+
609
+ <Ed25519 note signature>
610
+ ```
611
+
612
+ **Tier 2, Medium effort, stronger guarantees:**
613
+
614
+ | Approach | How it works | Privacy | Cost |
615
+ |----------|-------------|---------|------|
616
+ | **SCITT transparency service** | A hosted service accepts COSE_Sign1-wrapped audit events, applies registration policy (INK auth for access control) and returns Merkle inclusion receipts. Follows IETF draft-ietf-scitt-architecture. | Access-controlled, events stored with sender/recipient ACL | DataTrails (commercial) or self-hosted |
617
+ | **INK-native Merkle service** | A Tulpa-operated transparency service using the submission protocol from §7.2. Reuses INK auth (§3.3) and message format. | Full INK access control | Self-hosted |
618
+
619
+ SCITT is the best architectural fit for a full third-party audit service. The VeritasChain Protocol (draft-kamimura-scitt-vcp) demonstrates SCITT profiles for financial audit trails, a INK profile would follow the same pattern. However, SCITT is still a draft standard and the ecosystem is immature.
620
+
621
+ **Tier 3, Infrastructure investment:**
622
+
623
+ | Approach | How it works | Privacy | Cost |
624
+ |----------|-------------|---------|------|
625
+ | **Tessera-based log** (Google/transparency-dev) | Build a INK "personality" on top of Trillian Tessera. The Merkle tree is served as static tiles (any S3-compatible object store). Combined with external witnesses for independent attestation. | Full control, you build the personality | Self-hosted, significant engineering |
626
+ | **OpenTimestamps Bitcoin anchor** | Once per day, anchor the latest audit chain hash to Bitcoin. Provides the strongest "this data existed at time T" proof. 1–2 hour confirmation latency. | Hash-only, Bitcoin sees only the Merkle root | Free |
627
+
628
+ **Recommended deployment path:**
629
+
630
+ 1. **Now:** Implement witness cosigning for per-agent hash chains (Tier 1). Minimal code, agents expose a checkpoint endpoint, collect cosignatures from public witnesses.
631
+ 2. **When needed:** Add Rekor hash notarization for agents that want a public timestamp record (Tier 1).
632
+ 3. **For high-value interactions:** Deploy a INK-native Merkle service or adopt a SCITT transparency service (Tier 2). Agents advertise this in their Agent Card `capabilities.thirdPartyAudit`.
633
+ 4. **For compliance:** Add periodic OpenTimestamps Bitcoin anchoring (Tier 3) for legally defensible timestamps.
634
+
635
+ ### Open Questions (Raised by Research)
636
+
637
+ 1. **Should receipts be deniable?** DIDComm supports deniable messages. INK's current design makes all receipts non-repudiable (signed). For casual interactions, this may be overly formal. Consider an optional unsigned receipt mode for low-stakes messages.
638
+
639
+ 2. **Structured error codes for rejections?** DIDComm's structured problem codes (`e.p.msg.*`) are more machine-parseable than INK's free-text `note`. Consider adopting a structured code scheme in a future version.
640
+
641
+ 3. **Audit service federation:** should audit services be able to cross-replicate, similar to CT log mirroring? This would increase resilience but adds complexity.
642
+
643
+ 4. **Audit service discovery:** should there be a well-known audit service registry, or is Agent Card advertisement sufficient?
644
+
645
+ ## Security Considerations
646
+
647
+ - **Receipt spam:** receipts are rate-limited like regular messages. An agent can disable receipt sending. Receipts for receipts are never sent (loop prevention).
648
+ - **Audit data privacy:** audit events for a message are only available to the sender and recipient. Events are filtered before serving via the `/audit` endpoint access control.
649
+ - **Hash chain integrity:** the chain is only as trustworthy as the agent maintaining it. A malicious agent can rebuild the chain. The value is in **mutual** verification, both parties' chains must agree. Fork detection (per SSB) flags agents that present inconsistent chains.
650
+ - **Storage cost:** audit events are compact (~200 bytes each). At 50 messages/day, 12 months of audit = ~3.5 MB per agent. This is well within typical per-agent storage budgets.
651
+ - **Selective disclosure:** agents can choose which disposition types to report. A privacy-conscious agent might only send `received` and `rejected` but not `delivered` or `acted`. This follows Matrix's precedent with private read receipts.
652
+ - **Split-view attacks:** a malicious agent could show different audit histories to different parties. Mutual audit exchange (§4) detects inconsistencies after the fact. Third-party audit services (§7) make split-view attacks detectable under consistency checking, a single honest witness that compares roots will catch divergence, but a single access-controlled witness can still equivocate unless clients independently verify consistency proofs or multiple witnesses are used. For high-stakes interactions, agents SHOULD submit to at least two independent audit services.