agent-passport-system-mcp 3.0.0 → 3.1.1
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.
- package/README.md +11 -6
- package/build/__tests__/capability-token-e2e.test.d.ts +1 -0
- package/build/__tests__/capability-token-e2e.test.js +381 -0
- package/build/capabilityToken/authorityEvaluation.d.ts +11 -0
- package/build/capabilityToken/authorityEvaluation.js +24 -0
- package/build/capabilityToken/canonical.d.ts +6 -0
- package/build/capabilityToken/canonical.js +19 -0
- package/build/capabilityToken/challengeReceipt.d.ts +15 -0
- package/build/capabilityToken/challengeReceipt.js +48 -0
- package/build/capabilityToken/effectReceipt.d.ts +8 -0
- package/build/capabilityToken/effectReceipt.js +29 -0
- package/build/capabilityToken/index.d.ts +10 -0
- package/build/capabilityToken/index.js +10 -0
- package/build/capabilityToken/nullifierSet.d.ts +13 -0
- package/build/capabilityToken/nullifierSet.js +20 -0
- package/build/capabilityToken/sinkChallenge.d.ts +13 -0
- package/build/capabilityToken/sinkChallenge.js +30 -0
- package/build/capabilityToken/types.d.ts +89 -0
- package/build/capabilityToken/types.js +3 -0
- package/build/capabilityToken/verify.d.ts +30 -0
- package/build/capabilityToken/verify.js +85 -0
- package/build/index.js +295 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -12,13 +12,13 @@ Enforcement and accountability layer for AI agents. Bring your own identity. 20
|
|
|
12
12
|
APS_PROFILE=essential npx agent-passport-system-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`essential` is the default profile — the 20 tools 90% of integrations need. Set `APS_PROFILE=full` for all
|
|
15
|
+
`essential` is the default profile — the 20 tools 90% of integrations need. Set `APS_PROFILE=full` for all 150 tools.
|
|
16
16
|
|
|
17
17
|
Available profiles: essential (default), identity, governance, coordination, commerce, data, gateway, comms, minimal, full.
|
|
18
18
|
|
|
19
19
|
> **For AI agents:** visit [aeoess.com/llms.txt](https://aeoess.com/llms.txt) for machine-readable documentation or [llms-full.txt](https://aeoess.com/llms-full.txt) for the complete technical reference. MCP discovery: [.well-known/mcp.json](https://aeoess.com/.well-known/mcp.json).
|
|
20
20
|
|
|
21
|
-
Works with any MCP client: Claude Desktop, Claude Code, Cursor, Windsurf, and more. Full surface area under `APS_PROFILE=full`:
|
|
21
|
+
Works with any MCP client: Claude Desktop, Claude Code, Cursor, Windsurf, and more. Full surface area under `APS_PROFILE=full`: 150 tools across 127 modules (84 core + 41 v2 constitutional governance). Independently cited by [PDR in Production (Nanook & Gerundium, UBC)](https://doi.org/10.5281/zenodo.19323172).
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
@@ -216,11 +216,16 @@ Layer 1 — Agent Passport Protocol (Ed25519 identity)
|
|
|
216
216
|
|
|
217
217
|
## Links
|
|
218
218
|
|
|
219
|
-
- npm SDK: [agent-passport-system](https://www.npmjs.com/package/agent-passport-system) (v2.
|
|
220
|
-
- Python SDK: [agent-passport-system](https://pypi.org/project/agent-passport-system/) (
|
|
221
|
-
- Paper (
|
|
222
|
-
- Paper (
|
|
219
|
+
- npm SDK: [agent-passport-system](https://www.npmjs.com/package/agent-passport-system) (v2.5.0-alpha, 2,479 tests)
|
|
220
|
+
- Python SDK: [agent-passport-system](https://pypi.org/project/agent-passport-system/) (v2.5.0-alpha)
|
|
221
|
+
- Paper (Social Contract): [doi.org/10.5281/zenodo.18749779](https://doi.org/10.5281/zenodo.18749779)
|
|
222
|
+
- Paper (Monotonic Narrowing): [doi.org/10.5281/zenodo.18932404](https://doi.org/10.5281/zenodo.18932404)
|
|
223
|
+
- Paper (Faceted Authority Attenuation): [doi.org/10.5281/zenodo.19260073](https://doi.org/10.5281/zenodo.19260073)
|
|
223
224
|
- Paper (Behavioral Derivation Rights): [doi.org/10.5281/zenodo.19476002](https://doi.org/10.5281/zenodo.19476002)
|
|
225
|
+
- Paper (Physics-Enforced Delegation): [doi.org/10.5281/zenodo.19478584](https://doi.org/10.5281/zenodo.19478584)
|
|
226
|
+
- Paper (Governance in the Medium): [doi.org/10.5281/zenodo.19582550](https://doi.org/10.5281/zenodo.19582550)
|
|
227
|
+
- Paper (Cognitive Attestation): [doi.org/10.5281/zenodo.19646276](https://doi.org/10.5281/zenodo.19646276)
|
|
228
|
+
- IETF Internet-Draft: `draft-pidlisnyi-aps-00`
|
|
224
229
|
- Docs: [aeoess.com/llms-full.txt](https://aeoess.com/llms-full.txt)
|
|
225
230
|
- Agora: [aeoess.com/agora.html](https://aeoess.com/agora.html)
|
|
226
231
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// End-to-end test for the v0.1 capability-token reference implementation.
|
|
2
|
+
// Spec: agent-passport-system/docs/CAPABILITY-TOKEN-SPEC-DRAFT.md
|
|
3
|
+
//
|
|
4
|
+
// Demonstrates the closure property: any party holding (M1, M3, M4) can
|
|
5
|
+
// reconstruct the attestation chain with all four signatures verifying.
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { generateKeyPair, sign } from "agent-passport-system";
|
|
9
|
+
import { issueSinkChallenge, buildEvaluationRequest, mintChallengeReceipt, signEffectReceipt, challengeHash, challengeReceiptHash, deriveDelegationChainRoot, InMemoryNullifierSet, verifySinkChallenge, verifyAuthorityEvaluationRequest, verifyChallengeReceipt, verifyEffectReceipt, reconstructAttestationChain, canonicalWithoutSignature, } from "../capabilityToken/index.js";
|
|
10
|
+
// Local helper: every actor needs an Ed25519 key pair.
|
|
11
|
+
function newActorKey() {
|
|
12
|
+
const k = generateKeyPair();
|
|
13
|
+
return { publicKey: k.publicKey, privateKey: k.privateKey };
|
|
14
|
+
}
|
|
15
|
+
function setupActors() {
|
|
16
|
+
return {
|
|
17
|
+
delegator: newActorKey(),
|
|
18
|
+
subject: newActorKey(),
|
|
19
|
+
sink: newActorKey(),
|
|
20
|
+
gateway: newActorKey(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Minimal valid action shape for fixture flows.
|
|
24
|
+
function sampleAction() {
|
|
25
|
+
return {
|
|
26
|
+
kind: "fs.write",
|
|
27
|
+
target: "file:///tmp/aps-capability-test.txt",
|
|
28
|
+
parameters: { content_length: 42 },
|
|
29
|
+
resource_version: "v3-2026-04-22",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// In v0.1 the delegation envelope is opaque to this protocol — the SDK's v2.x
|
|
33
|
+
// chain composes by reference. The reference impl commits to a JCS hash of
|
|
34
|
+
// the chain. For test fixtures a stub envelope is sufficient.
|
|
35
|
+
function sampleDelegationChain(actors) {
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
issuer: actors.delegator.publicKey,
|
|
39
|
+
subject: actors.subject.publicKey,
|
|
40
|
+
scope: ["fs.write"],
|
|
41
|
+
issued_at: "2026-04-22T00:00:00Z",
|
|
42
|
+
not_after: "2026-04-29T00:00:00Z",
|
|
43
|
+
authority_token_merkle_root: "stub-root-for-v0.1",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function sampleAuthorityToken() {
|
|
48
|
+
return {
|
|
49
|
+
token_preimage: Buffer.from("preimage-001-".padEnd(32, "x")).toString("base64url"),
|
|
50
|
+
merkle_proof: ["sibling-1", "sibling-2"],
|
|
51
|
+
scope_class: "fs.write",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function sampleFreshnessBeacon(actors) {
|
|
55
|
+
const ts = "2026-04-22T00:00:01Z";
|
|
56
|
+
// Minimal beacon: signed timestamp from the delegator. Full spec requires
|
|
57
|
+
// a richer payload; this is sufficient to exercise the flow.
|
|
58
|
+
const beaconSig = sign(`beacon:${ts}`, actors.delegator.privateKey);
|
|
59
|
+
return {
|
|
60
|
+
delegator_id: actors.delegator.publicKey,
|
|
61
|
+
beacon_timestamp: ts,
|
|
62
|
+
beacon_signature: beaconSig,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Drives the full M1→M2→M3→M4 cycle. Returns every artifact so individual
|
|
66
|
+
// tests can assert on whichever step they care about.
|
|
67
|
+
function runFullCycle(actors, nullifiers) {
|
|
68
|
+
// M1: sink issues a challenge.
|
|
69
|
+
const challenge = issueSinkChallenge({
|
|
70
|
+
sink_id: actors.sink.publicKey,
|
|
71
|
+
subject_id: actors.subject.publicKey,
|
|
72
|
+
action: sampleAction(),
|
|
73
|
+
sink_key: actors.sink,
|
|
74
|
+
});
|
|
75
|
+
// M2: subject builds an authority-evaluation request.
|
|
76
|
+
const chain = sampleDelegationChain(actors);
|
|
77
|
+
const request = buildEvaluationRequest({
|
|
78
|
+
challenge,
|
|
79
|
+
delegation_chain: chain,
|
|
80
|
+
authority_token: sampleAuthorityToken(),
|
|
81
|
+
freshness_beacon: sampleFreshnessBeacon(actors),
|
|
82
|
+
subject_key: actors.subject,
|
|
83
|
+
});
|
|
84
|
+
// M3: gateway mints a permit ChallengeReceipt over the sink's exact
|
|
85
|
+
// challenge_hash and the subject's exact delegation_chain_root.
|
|
86
|
+
const receipt = mintChallengeReceipt({
|
|
87
|
+
request,
|
|
88
|
+
decision: "permit",
|
|
89
|
+
policy_digest: "policy-bundle-sha256-v0.1-fixture",
|
|
90
|
+
gateway_key: actors.gateway,
|
|
91
|
+
});
|
|
92
|
+
// Sink-side: verify M3 binds to the M1 the sink issued, then check the
|
|
93
|
+
// nullifier set, then consume the token.
|
|
94
|
+
const m3Verify = verifyChallengeReceipt({
|
|
95
|
+
receipt,
|
|
96
|
+
expected_challenge: challenge,
|
|
97
|
+
expected_delegation_chain_root: request.delegation_chain_root,
|
|
98
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
99
|
+
});
|
|
100
|
+
assert.equal(m3Verify.ok, true, "sink must accept M3");
|
|
101
|
+
assert.equal(nullifiers.isConsumed(receipt.authority_token_preimage), false);
|
|
102
|
+
nullifiers.consume(receipt.authority_token_preimage);
|
|
103
|
+
// M4: sink signs an effect receipt.
|
|
104
|
+
const effectReceipt = signEffectReceipt({
|
|
105
|
+
challenge_receipt: receipt,
|
|
106
|
+
effect: {
|
|
107
|
+
executed_at: "2026-04-22T00:00:02Z",
|
|
108
|
+
outcome: "success",
|
|
109
|
+
result_digest: "sha256-of-effect-result",
|
|
110
|
+
},
|
|
111
|
+
sink_key: actors.sink,
|
|
112
|
+
});
|
|
113
|
+
return { challenge, request, receipt, effectReceipt, chain };
|
|
114
|
+
}
|
|
115
|
+
test("M1: sink-issued challenge has a valid sink signature", () => {
|
|
116
|
+
const actors = setupActors();
|
|
117
|
+
const challenge = issueSinkChallenge({
|
|
118
|
+
sink_id: actors.sink.publicKey,
|
|
119
|
+
subject_id: actors.subject.publicKey,
|
|
120
|
+
action: sampleAction(),
|
|
121
|
+
sink_key: actors.sink,
|
|
122
|
+
});
|
|
123
|
+
assert.equal(challenge.type, "aps.capability.v1.SinkChallenge");
|
|
124
|
+
assert.equal(challenge.sink_id, actors.sink.publicKey);
|
|
125
|
+
assert.ok(challenge.sink_signature.length > 0, "signature populated");
|
|
126
|
+
const result = verifySinkChallenge(challenge, actors.sink.publicKey);
|
|
127
|
+
assert.equal(result.ok, true);
|
|
128
|
+
assert.equal(challengeHash(challenge).length, 64, "SHA-256 hex");
|
|
129
|
+
});
|
|
130
|
+
test("M2: subject-signed evaluation request verifies and carries M1 verbatim", () => {
|
|
131
|
+
const actors = setupActors();
|
|
132
|
+
const challenge = issueSinkChallenge({
|
|
133
|
+
sink_id: actors.sink.publicKey,
|
|
134
|
+
subject_id: actors.subject.publicKey,
|
|
135
|
+
action: sampleAction(),
|
|
136
|
+
sink_key: actors.sink,
|
|
137
|
+
});
|
|
138
|
+
const chain = sampleDelegationChain(actors);
|
|
139
|
+
const request = buildEvaluationRequest({
|
|
140
|
+
challenge,
|
|
141
|
+
delegation_chain: chain,
|
|
142
|
+
authority_token: sampleAuthorityToken(),
|
|
143
|
+
freshness_beacon: sampleFreshnessBeacon(actors),
|
|
144
|
+
subject_key: actors.subject,
|
|
145
|
+
});
|
|
146
|
+
assert.equal(request.delegation_depth, 1);
|
|
147
|
+
assert.equal(request.delegation_chain_root, deriveDelegationChainRoot(chain));
|
|
148
|
+
const result = verifyAuthorityEvaluationRequest(request, actors.subject.publicKey, actors.sink.publicKey);
|
|
149
|
+
assert.equal(result.ok, true);
|
|
150
|
+
});
|
|
151
|
+
test("M3: gateway permit binds challenge_hash + delegation_chain_root", () => {
|
|
152
|
+
const actors = setupActors();
|
|
153
|
+
const challenge = issueSinkChallenge({
|
|
154
|
+
sink_id: actors.sink.publicKey,
|
|
155
|
+
subject_id: actors.subject.publicKey,
|
|
156
|
+
action: sampleAction(),
|
|
157
|
+
sink_key: actors.sink,
|
|
158
|
+
});
|
|
159
|
+
const request = buildEvaluationRequest({
|
|
160
|
+
challenge,
|
|
161
|
+
delegation_chain: sampleDelegationChain(actors),
|
|
162
|
+
authority_token: sampleAuthorityToken(),
|
|
163
|
+
freshness_beacon: sampleFreshnessBeacon(actors),
|
|
164
|
+
subject_key: actors.subject,
|
|
165
|
+
});
|
|
166
|
+
const receipt = mintChallengeReceipt({
|
|
167
|
+
request,
|
|
168
|
+
decision: "permit",
|
|
169
|
+
policy_digest: "p-digest",
|
|
170
|
+
gateway_key: actors.gateway,
|
|
171
|
+
});
|
|
172
|
+
assert.equal(receipt.challenge_hash, challengeHash(challenge));
|
|
173
|
+
assert.equal(receipt.delegation_chain_root, request.delegation_chain_root);
|
|
174
|
+
assert.equal(receipt.epistemic_claims.policy_evaluated, "closed");
|
|
175
|
+
assert.equal(receipt.epistemic_claims.effect_occurred, "unresolved");
|
|
176
|
+
const result = verifyChallengeReceipt({
|
|
177
|
+
receipt,
|
|
178
|
+
expected_challenge: challenge,
|
|
179
|
+
expected_delegation_chain_root: request.delegation_chain_root,
|
|
180
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
181
|
+
});
|
|
182
|
+
assert.equal(result.ok, true);
|
|
183
|
+
});
|
|
184
|
+
test("E2E: full four-message cycle — every signature verifies, attestation chain reconstructs", () => {
|
|
185
|
+
const actors = setupActors();
|
|
186
|
+
const nullifiers = new InMemoryNullifierSet();
|
|
187
|
+
const { challenge, request, receipt, effectReceipt } = runFullCycle(actors, nullifiers);
|
|
188
|
+
// Each individual signature verifies against the right key.
|
|
189
|
+
assert.equal(verifySinkChallenge(challenge, actors.sink.publicKey).ok, true);
|
|
190
|
+
assert.equal(verifyAuthorityEvaluationRequest(request, actors.subject.publicKey, actors.sink.publicKey).ok, true);
|
|
191
|
+
assert.equal(verifyChallengeReceipt({
|
|
192
|
+
receipt,
|
|
193
|
+
expected_challenge: challenge,
|
|
194
|
+
expected_delegation_chain_root: request.delegation_chain_root,
|
|
195
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
196
|
+
}).ok, true);
|
|
197
|
+
assert.equal(verifyEffectReceipt({
|
|
198
|
+
receipt: effectReceipt,
|
|
199
|
+
challenge_receipt: receipt,
|
|
200
|
+
sink_public_key: actors.sink.publicKey,
|
|
201
|
+
}).ok, true);
|
|
202
|
+
// M4 binds back to M3 by gateway_receipt_hash.
|
|
203
|
+
assert.equal(effectReceipt.gateway_receipt_hash, challengeReceiptHash(receipt));
|
|
204
|
+
assert.equal(effectReceipt.epistemic_claims.effect_occurred, "closed");
|
|
205
|
+
assert.equal(effectReceipt.epistemic_claims.policy_evaluation_correct, "witnessed");
|
|
206
|
+
// Closure: a verifier holding M1, M3, M4 (no M2 needed) can reconstruct the
|
|
207
|
+
// chain. This is the spec's "(SinkChallenge, ChallengeReceipt, EffectReceipt)
|
|
208
|
+
// is the full attestation record" claim made executable.
|
|
209
|
+
const reconstructed = reconstructAttestationChain({
|
|
210
|
+
challenge,
|
|
211
|
+
receipt,
|
|
212
|
+
effect: effectReceipt,
|
|
213
|
+
sink_public_key: actors.sink.publicKey,
|
|
214
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
215
|
+
});
|
|
216
|
+
assert.equal(reconstructed.ok, true, "attestation chain must reconstruct");
|
|
217
|
+
// Nullifier set consumed exactly one preimage.
|
|
218
|
+
assert.equal(nullifiers.size(), 1);
|
|
219
|
+
});
|
|
220
|
+
// ─── Negative tests ───────────────────────────────────────────────────────
|
|
221
|
+
// Each models an adversarial gateway, a replay attempt, or an attempt to
|
|
222
|
+
// bypass delegation-chain validation. The protocol's closure property means
|
|
223
|
+
// each MUST be detected by the sink-side or any independent verifier.
|
|
224
|
+
test("NEGATIVE: gateway substitutes a different challenge_hash — sink rejects M3", () => {
|
|
225
|
+
const actors = setupActors();
|
|
226
|
+
const challenge = issueSinkChallenge({
|
|
227
|
+
sink_id: actors.sink.publicKey,
|
|
228
|
+
subject_id: actors.subject.publicKey,
|
|
229
|
+
action: sampleAction(),
|
|
230
|
+
sink_key: actors.sink,
|
|
231
|
+
});
|
|
232
|
+
const request = buildEvaluationRequest({
|
|
233
|
+
challenge,
|
|
234
|
+
delegation_chain: sampleDelegationChain(actors),
|
|
235
|
+
authority_token: sampleAuthorityToken(),
|
|
236
|
+
freshness_beacon: sampleFreshnessBeacon(actors),
|
|
237
|
+
subject_key: actors.subject,
|
|
238
|
+
});
|
|
239
|
+
// Adversarial gateway swaps the challenge_hash to one of its own choosing.
|
|
240
|
+
const forgedReceipt = mintChallengeReceipt({
|
|
241
|
+
request,
|
|
242
|
+
decision: "permit",
|
|
243
|
+
policy_digest: "p-digest",
|
|
244
|
+
gateway_key: actors.gateway,
|
|
245
|
+
override_challenge_hash: "0".repeat(64),
|
|
246
|
+
});
|
|
247
|
+
// Gateway signature itself still verifies (gateway holds its own key).
|
|
248
|
+
// What MUST fail: the binding check between challenge_hash and the M1 the
|
|
249
|
+
// sink actually issued.
|
|
250
|
+
const result = verifyChallengeReceipt({
|
|
251
|
+
receipt: forgedReceipt,
|
|
252
|
+
expected_challenge: challenge,
|
|
253
|
+
expected_delegation_chain_root: request.delegation_chain_root,
|
|
254
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
255
|
+
});
|
|
256
|
+
assert.equal(result.ok, false);
|
|
257
|
+
if (!result.ok) {
|
|
258
|
+
assert.match(result.reason, /challenge_hash does not match/);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
test("NEGATIVE: subject reuses authority_token_preimage — nullifier set rejects replay", () => {
|
|
262
|
+
const actors = setupActors();
|
|
263
|
+
const nullifiers = new InMemoryNullifierSet();
|
|
264
|
+
// First cycle succeeds.
|
|
265
|
+
const first = runFullCycle(actors, nullifiers);
|
|
266
|
+
assert.ok(first.effectReceipt.sink_signature.length > 0);
|
|
267
|
+
// Second cycle: same actors, same authority_token_preimage. Even if the
|
|
268
|
+
// gateway re-mints a fresh M3 (different challenge, different evaluated_at),
|
|
269
|
+
// the sink's nullifier set MUST refuse the second consumption attempt.
|
|
270
|
+
const challenge2 = issueSinkChallenge({
|
|
271
|
+
sink_id: actors.sink.publicKey,
|
|
272
|
+
subject_id: actors.subject.publicKey,
|
|
273
|
+
action: { ...sampleAction(), parameters: { content_length: 100 } },
|
|
274
|
+
sink_key: actors.sink,
|
|
275
|
+
});
|
|
276
|
+
const request2 = buildEvaluationRequest({
|
|
277
|
+
challenge: challenge2,
|
|
278
|
+
delegation_chain: sampleDelegationChain(actors),
|
|
279
|
+
authority_token: sampleAuthorityToken(), // same preimage as cycle #1
|
|
280
|
+
freshness_beacon: sampleFreshnessBeacon(actors),
|
|
281
|
+
subject_key: actors.subject,
|
|
282
|
+
});
|
|
283
|
+
const receipt2 = mintChallengeReceipt({
|
|
284
|
+
request: request2,
|
|
285
|
+
decision: "permit",
|
|
286
|
+
policy_digest: "p-digest",
|
|
287
|
+
gateway_key: actors.gateway,
|
|
288
|
+
});
|
|
289
|
+
// Sink-side verification of M3 still passes (it's a fresh sink-authored
|
|
290
|
+
// challenge with a valid gateway signature). The replay defense is
|
|
291
|
+
// structurally separate: it lives in the nullifier set.
|
|
292
|
+
const m3v = verifyChallengeReceipt({
|
|
293
|
+
receipt: receipt2,
|
|
294
|
+
expected_challenge: challenge2,
|
|
295
|
+
expected_delegation_chain_root: request2.delegation_chain_root,
|
|
296
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
297
|
+
});
|
|
298
|
+
assert.equal(m3v.ok, true);
|
|
299
|
+
// Replay attempt MUST throw at consume(). This is what stops the gateway
|
|
300
|
+
// from batch-consuming a leaked token.
|
|
301
|
+
assert.throws(() => nullifiers.consume(receipt2.authority_token_preimage), /nullifier replay/);
|
|
302
|
+
});
|
|
303
|
+
test("NEGATIVE: gateway omits or forges delegation_chain_root — verifier detects missing/invalid", () => {
|
|
304
|
+
const actors = setupActors();
|
|
305
|
+
const challenge = issueSinkChallenge({
|
|
306
|
+
sink_id: actors.sink.publicKey,
|
|
307
|
+
subject_id: actors.subject.publicKey,
|
|
308
|
+
action: sampleAction(),
|
|
309
|
+
sink_key: actors.sink,
|
|
310
|
+
});
|
|
311
|
+
const request = buildEvaluationRequest({
|
|
312
|
+
challenge,
|
|
313
|
+
delegation_chain: sampleDelegationChain(actors),
|
|
314
|
+
authority_token: sampleAuthorityToken(),
|
|
315
|
+
freshness_beacon: sampleFreshnessBeacon(actors),
|
|
316
|
+
subject_key: actors.subject,
|
|
317
|
+
});
|
|
318
|
+
// Variant A: gateway omits the delegation_chain_root entirely (skipped
|
|
319
|
+
// chain validation). Verifier MUST detect.
|
|
320
|
+
const omittedRoot = mintChallengeReceipt({
|
|
321
|
+
request,
|
|
322
|
+
decision: "permit",
|
|
323
|
+
policy_digest: "p-digest",
|
|
324
|
+
gateway_key: actors.gateway,
|
|
325
|
+
omit_delegation_chain_root: true,
|
|
326
|
+
});
|
|
327
|
+
const omittedResult = verifyChallengeReceipt({
|
|
328
|
+
receipt: omittedRoot,
|
|
329
|
+
expected_challenge: challenge,
|
|
330
|
+
expected_delegation_chain_root: request.delegation_chain_root,
|
|
331
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
332
|
+
});
|
|
333
|
+
assert.equal(omittedResult.ok, false);
|
|
334
|
+
if (!omittedResult.ok) {
|
|
335
|
+
assert.match(omittedResult.reason, /delegation_chain_root/);
|
|
336
|
+
}
|
|
337
|
+
// Variant B: gateway forges a different chain root than the subject
|
|
338
|
+
// committed to (e.g. trying to attest to a broader scope class).
|
|
339
|
+
const forgedRoot = mintChallengeReceipt({
|
|
340
|
+
request,
|
|
341
|
+
decision: "permit",
|
|
342
|
+
policy_digest: "p-digest",
|
|
343
|
+
gateway_key: actors.gateway,
|
|
344
|
+
override_delegation_chain_root: "deadbeef".repeat(8),
|
|
345
|
+
});
|
|
346
|
+
const forgedResult = verifyChallengeReceipt({
|
|
347
|
+
receipt: forgedRoot,
|
|
348
|
+
expected_challenge: challenge,
|
|
349
|
+
expected_delegation_chain_root: request.delegation_chain_root,
|
|
350
|
+
gateway_public_key: actors.gateway.publicKey,
|
|
351
|
+
});
|
|
352
|
+
assert.equal(forgedResult.ok, false);
|
|
353
|
+
if (!forgedResult.ok) {
|
|
354
|
+
assert.match(forgedResult.reason, /delegation_chain_root/);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
test("Hash stability: re-issuing M1 with the same nonce + clock yields the same challenge_hash", () => {
|
|
358
|
+
const actors = setupActors();
|
|
359
|
+
const fixedNow = new Date("2026-04-22T12:00:00Z");
|
|
360
|
+
const fixedNonce = "deterministic-nonce-for-fixture-vector".padEnd(43, "x").slice(0, 43);
|
|
361
|
+
const a = issueSinkChallenge({
|
|
362
|
+
sink_id: actors.sink.publicKey,
|
|
363
|
+
subject_id: actors.subject.publicKey,
|
|
364
|
+
action: sampleAction(),
|
|
365
|
+
sink_key: actors.sink,
|
|
366
|
+
now: fixedNow,
|
|
367
|
+
nonce: fixedNonce,
|
|
368
|
+
});
|
|
369
|
+
const b = issueSinkChallenge({
|
|
370
|
+
sink_id: actors.sink.publicKey,
|
|
371
|
+
subject_id: actors.subject.publicKey,
|
|
372
|
+
action: sampleAction(),
|
|
373
|
+
sink_key: actors.sink,
|
|
374
|
+
now: fixedNow,
|
|
375
|
+
nonce: fixedNonce,
|
|
376
|
+
});
|
|
377
|
+
// Canonical JCS payload is identical → challenge_hash matches. Signatures
|
|
378
|
+
// also match because Ed25519 over identical input is deterministic.
|
|
379
|
+
assert.equal(challengeHash(a), challengeHash(b));
|
|
380
|
+
assert.equal(canonicalWithoutSignature(a, "sink_signature"), canonicalWithoutSignature(b, "sink_signature"));
|
|
381
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuthorityEvaluationRequest, AuthorityTokenReveal, DelegationEnvelope, FreshnessBeacon, KeyPair, SinkChallenge } from "./types.js";
|
|
2
|
+
export interface BuildEvaluationRequestOptions {
|
|
3
|
+
challenge: SinkChallenge;
|
|
4
|
+
delegation_chain: DelegationEnvelope[];
|
|
5
|
+
authority_token: AuthorityTokenReveal;
|
|
6
|
+
freshness_beacon: FreshnessBeacon;
|
|
7
|
+
subject_key: KeyPair;
|
|
8
|
+
delegation_chain_root?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildEvaluationRequest(opts: BuildEvaluationRequestOptions): AuthorityEvaluationRequest;
|
|
11
|
+
export declare function deriveDelegationChainRoot(chain: DelegationEnvelope[]): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// M2: Subject builds an AuthorityEvaluationRequest carrying the sink challenge,
|
|
2
|
+
// the delegation chain, and one revealed authority-token preimage.
|
|
3
|
+
import { canonicalizeJCS, sign } from "agent-passport-system";
|
|
4
|
+
import { canonicalWithoutSignature, sha256Hex } from "./canonical.js";
|
|
5
|
+
export function buildEvaluationRequest(opts) {
|
|
6
|
+
const root = opts.delegation_chain_root ?? deriveDelegationChainRoot(opts.delegation_chain);
|
|
7
|
+
const unsigned = {
|
|
8
|
+
type: "aps.capability.v1.AuthorityEvaluationRequest",
|
|
9
|
+
challenge: opts.challenge,
|
|
10
|
+
delegation_chain: opts.delegation_chain,
|
|
11
|
+
delegation_chain_root: root,
|
|
12
|
+
delegation_depth: opts.delegation_chain.length,
|
|
13
|
+
authority_token: opts.authority_token,
|
|
14
|
+
freshness_beacon: opts.freshness_beacon,
|
|
15
|
+
};
|
|
16
|
+
const signature = sign(canonicalWithoutSignature({ ...unsigned, subject_signature: "" }, "subject_signature"), opts.subject_key.privateKey);
|
|
17
|
+
return { ...unsigned, subject_signature: signature };
|
|
18
|
+
}
|
|
19
|
+
// SHA-256 over the canonical serialization of the chain. v0.1: simple flat
|
|
20
|
+
// commitment, sufficient to demonstrate the closure property. The wire
|
|
21
|
+
// format reserves room for a richer Merkle tree in v0.2.
|
|
22
|
+
export function deriveDelegationChainRoot(chain) {
|
|
23
|
+
return sha256Hex(canonicalizeJCS(chain));
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const SIGNATURE_FIELDS: readonly ["sink_signature", "subject_signature", "gateway_signature"];
|
|
2
|
+
type SignatureField = (typeof SIGNATURE_FIELDS)[number];
|
|
3
|
+
export declare function canonicalWithoutSignature(obj: object, signatureField: SignatureField): string;
|
|
4
|
+
export declare function sha256Hex(input: string): string;
|
|
5
|
+
export declare function sha256Base64Url(input: string): string;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Helpers for canonical serialization + hashing of v0.1 capability-token
|
|
2
|
+
// messages. JCS (RFC 8785) canonicalization is delegated to the SDK.
|
|
3
|
+
import { canonicalizeJCS } from "agent-passport-system";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
const SIGNATURE_FIELDS = [
|
|
6
|
+
"sink_signature",
|
|
7
|
+
"subject_signature",
|
|
8
|
+
"gateway_signature",
|
|
9
|
+
];
|
|
10
|
+
export function canonicalWithoutSignature(obj, signatureField) {
|
|
11
|
+
const { [signatureField]: _omit, ...rest } = obj;
|
|
12
|
+
return canonicalizeJCS(rest);
|
|
13
|
+
}
|
|
14
|
+
export function sha256Hex(input) {
|
|
15
|
+
return createHash("sha256").update(input).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
export function sha256Base64Url(input) {
|
|
18
|
+
return createHash("sha256").update(input).digest("base64url");
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuthorityEvaluationRequest, ChallengeReceipt, ChallengeReceiptEpistemicClaims, Decision, KeyPair } from "./types.js";
|
|
2
|
+
export interface MintChallengeReceiptOptions {
|
|
3
|
+
request: AuthorityEvaluationRequest;
|
|
4
|
+
decision: Decision;
|
|
5
|
+
deny_reason?: string;
|
|
6
|
+
policy_digest: string;
|
|
7
|
+
gateway_key: KeyPair;
|
|
8
|
+
epistemic_claims?: ChallengeReceiptEpistemicClaims;
|
|
9
|
+
now?: Date;
|
|
10
|
+
override_challenge_hash?: string;
|
|
11
|
+
override_delegation_chain_root?: string;
|
|
12
|
+
omit_delegation_chain_root?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function mintChallengeReceipt(opts: MintChallengeReceiptOptions): ChallengeReceipt;
|
|
15
|
+
export declare function challengeReceiptHash(receipt: ChallengeReceipt): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// M3: Gateway mints a ChallengeReceipt over the sink-authored challenge.
|
|
2
|
+
// The gateway's signature binds to the canonical challenge_hash; it cannot
|
|
3
|
+
// describe a different action without producing a hash the sink will refuse.
|
|
4
|
+
import { sign } from "agent-passport-system";
|
|
5
|
+
import { canonicalWithoutSignature, sha256Hex } from "./canonical.js";
|
|
6
|
+
import { challengeHash } from "./sinkChallenge.js";
|
|
7
|
+
const CLOSED_PERMIT_CLAIMS = {
|
|
8
|
+
policy_evaluated: "closed",
|
|
9
|
+
authority_consumed: "closed",
|
|
10
|
+
scope_within_bounds: "closed",
|
|
11
|
+
effect_occurred: "unresolved",
|
|
12
|
+
};
|
|
13
|
+
const CLOSED_DENY_CLAIMS = {
|
|
14
|
+
policy_evaluated: "closed",
|
|
15
|
+
authority_consumed: "closed",
|
|
16
|
+
scope_within_bounds: "closed",
|
|
17
|
+
effect_occurred: "unresolved",
|
|
18
|
+
};
|
|
19
|
+
export function mintChallengeReceipt(opts) {
|
|
20
|
+
const cHash = opts.override_challenge_hash ?? challengeHash(opts.request.challenge);
|
|
21
|
+
const claims = opts.epistemic_claims ??
|
|
22
|
+
(opts.decision === "permit" ? CLOSED_PERMIT_CLAIMS : CLOSED_DENY_CLAIMS);
|
|
23
|
+
const evaluatedAt = (opts.now ?? new Date()).toISOString();
|
|
24
|
+
const chainRoot = opts.omit_delegation_chain_root
|
|
25
|
+
? ""
|
|
26
|
+
: opts.override_delegation_chain_root ?? opts.request.delegation_chain_root;
|
|
27
|
+
const unsigned = {
|
|
28
|
+
type: "aps.capability.v1.ChallengeReceipt",
|
|
29
|
+
challenge_hash: cHash,
|
|
30
|
+
decision: opts.decision,
|
|
31
|
+
...(opts.decision === "deny" && opts.deny_reason
|
|
32
|
+
? { deny_reason: opts.deny_reason }
|
|
33
|
+
: {}),
|
|
34
|
+
delegation_chain_root: chainRoot,
|
|
35
|
+
delegation_depth: opts.request.delegation_depth,
|
|
36
|
+
...(opts.decision === "permit"
|
|
37
|
+
? { authority_token_preimage: opts.request.authority_token.token_preimage }
|
|
38
|
+
: {}),
|
|
39
|
+
evaluated_at: evaluatedAt,
|
|
40
|
+
policy_digest: opts.policy_digest,
|
|
41
|
+
epistemic_claims: claims,
|
|
42
|
+
};
|
|
43
|
+
const signature = sign(canonicalWithoutSignature({ ...unsigned, gateway_signature: "" }, "gateway_signature"), opts.gateway_key.privateKey);
|
|
44
|
+
return { ...unsigned, gateway_signature: signature };
|
|
45
|
+
}
|
|
46
|
+
export function challengeReceiptHash(receipt) {
|
|
47
|
+
return sha256Hex(canonicalWithoutSignature(receipt, "gateway_signature"));
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChallengeReceipt, EffectReceipt, EffectReceiptEpistemicClaims, KeyPair, SinkEffect } from "./types.js";
|
|
2
|
+
export interface SignEffectReceiptOptions {
|
|
3
|
+
challenge_receipt: ChallengeReceipt;
|
|
4
|
+
effect: SinkEffect;
|
|
5
|
+
sink_key: KeyPair;
|
|
6
|
+
epistemic_claims?: EffectReceiptEpistemicClaims;
|
|
7
|
+
}
|
|
8
|
+
export declare function signEffectReceipt(opts: SignEffectReceiptOptions): EffectReceipt;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// M4: After execution, the sink signs an EffectReceipt that binds the
|
|
2
|
+
// consumed authority token to the actual effect.
|
|
3
|
+
import { sign } from "agent-passport-system";
|
|
4
|
+
import { canonicalWithoutSignature } from "./canonical.js";
|
|
5
|
+
import { challengeReceiptHash } from "./challengeReceipt.js";
|
|
6
|
+
const DEFAULT_CLAIMS = {
|
|
7
|
+
effect_occurred: "closed",
|
|
8
|
+
effect_result_bound: "closed",
|
|
9
|
+
policy_evaluation_correct: "witnessed",
|
|
10
|
+
};
|
|
11
|
+
export function signEffectReceipt(opts) {
|
|
12
|
+
if (opts.challenge_receipt.decision !== "permit") {
|
|
13
|
+
throw new Error("EffectReceipt requires a permit ChallengeReceipt");
|
|
14
|
+
}
|
|
15
|
+
const preimage = opts.challenge_receipt.authority_token_preimage;
|
|
16
|
+
if (!preimage) {
|
|
17
|
+
throw new Error("ChallengeReceipt is missing authority_token_preimage");
|
|
18
|
+
}
|
|
19
|
+
const unsigned = {
|
|
20
|
+
type: "aps.capability.v1.EffectReceipt",
|
|
21
|
+
challenge_hash: opts.challenge_receipt.challenge_hash,
|
|
22
|
+
authority_token_preimage: preimage,
|
|
23
|
+
gateway_receipt_hash: challengeReceiptHash(opts.challenge_receipt),
|
|
24
|
+
effect: opts.effect,
|
|
25
|
+
epistemic_claims: opts.epistemic_claims ?? DEFAULT_CLAIMS,
|
|
26
|
+
};
|
|
27
|
+
const signature = sign(canonicalWithoutSignature({ ...unsigned, sink_signature: "" }, "sink_signature"), opts.sink_key.privateKey);
|
|
28
|
+
return { ...unsigned, sink_signature: signature };
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { canonicalWithoutSignature, sha256Hex, sha256Base64Url } from "./canonical.js";
|
|
3
|
+
export { issueSinkChallenge, challengeHash } from "./sinkChallenge.js";
|
|
4
|
+
export { buildEvaluationRequest, deriveDelegationChainRoot, } from "./authorityEvaluation.js";
|
|
5
|
+
export { mintChallengeReceipt, challengeReceiptHash } from "./challengeReceipt.js";
|
|
6
|
+
export { signEffectReceipt } from "./effectReceipt.js";
|
|
7
|
+
export { InMemoryNullifierSet } from "./nullifierSet.js";
|
|
8
|
+
export type { NullifierStore } from "./nullifierSet.js";
|
|
9
|
+
export { verifySinkChallenge, verifyAuthorityEvaluationRequest, verifyChallengeReceipt, verifyEffectReceipt, reconstructAttestationChain, } from "./verify.js";
|
|
10
|
+
export type { VerifyResult } from "./verify.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Public surface for the v0.1 capability-token reference implementation.
|
|
2
|
+
// Spec: agent-passport-system/docs/CAPABILITY-TOKEN-SPEC-DRAFT.md
|
|
3
|
+
export * from "./types.js";
|
|
4
|
+
export { canonicalWithoutSignature, sha256Hex, sha256Base64Url } from "./canonical.js";
|
|
5
|
+
export { issueSinkChallenge, challengeHash } from "./sinkChallenge.js";
|
|
6
|
+
export { buildEvaluationRequest, deriveDelegationChainRoot, } from "./authorityEvaluation.js";
|
|
7
|
+
export { mintChallengeReceipt, challengeReceiptHash } from "./challengeReceipt.js";
|
|
8
|
+
export { signEffectReceipt } from "./effectReceipt.js";
|
|
9
|
+
export { InMemoryNullifierSet } from "./nullifierSet.js";
|
|
10
|
+
export { verifySinkChallenge, verifyAuthorityEvaluationRequest, verifyChallengeReceipt, verifyEffectReceipt, reconstructAttestationChain, } from "./verify.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface NullifierStore {
|
|
2
|
+
isConsumed(preimage: string): boolean;
|
|
3
|
+
consume(preimage: string): void;
|
|
4
|
+
size(): number;
|
|
5
|
+
clear(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare class InMemoryNullifierSet implements NullifierStore {
|
|
8
|
+
private readonly seen;
|
|
9
|
+
isConsumed(preimage: string): boolean;
|
|
10
|
+
consume(preimage: string): void;
|
|
11
|
+
size(): number;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// In-memory nullifier set. v0.1: process-local Set, no persistence. The
|
|
2
|
+
// sink consults this on every M3 redemption to prevent token replay.
|
|
3
|
+
export class InMemoryNullifierSet {
|
|
4
|
+
seen = new Set();
|
|
5
|
+
isConsumed(preimage) {
|
|
6
|
+
return this.seen.has(preimage);
|
|
7
|
+
}
|
|
8
|
+
consume(preimage) {
|
|
9
|
+
if (this.seen.has(preimage)) {
|
|
10
|
+
throw new Error(`nullifier replay: token preimage ${preimage.slice(0, 12)}... already consumed`);
|
|
11
|
+
}
|
|
12
|
+
this.seen.add(preimage);
|
|
13
|
+
}
|
|
14
|
+
size() {
|
|
15
|
+
return this.seen.size;
|
|
16
|
+
}
|
|
17
|
+
clear() {
|
|
18
|
+
this.seen.clear();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { KeyPair, PolicyFreshnessRequirement, SinkAction, SinkChallenge } from "./types.js";
|
|
2
|
+
export interface IssueSinkChallengeOptions {
|
|
3
|
+
sink_id: string;
|
|
4
|
+
subject_id: string;
|
|
5
|
+
action: SinkAction;
|
|
6
|
+
validity_seconds?: number;
|
|
7
|
+
required_policy_freshness?: PolicyFreshnessRequirement;
|
|
8
|
+
sink_key: KeyPair;
|
|
9
|
+
now?: Date;
|
|
10
|
+
nonce?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function issueSinkChallenge(opts: IssueSinkChallengeOptions): SinkChallenge;
|
|
13
|
+
export declare function challengeHash(challenge: SinkChallenge): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// M1: SinkChallenge issuance and challenge-hash computation.
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { sign } from "agent-passport-system";
|
|
4
|
+
import { canonicalWithoutSignature, sha256Hex } from "./canonical.js";
|
|
5
|
+
const DEFAULT_VALIDITY_SECONDS = 60;
|
|
6
|
+
const DEFAULT_FRESHNESS = {
|
|
7
|
+
max_age_seconds: 30,
|
|
8
|
+
beacon_hash_required: true,
|
|
9
|
+
};
|
|
10
|
+
export function issueSinkChallenge(opts) {
|
|
11
|
+
const issued = opts.now ?? new Date();
|
|
12
|
+
const validity = opts.validity_seconds ?? DEFAULT_VALIDITY_SECONDS;
|
|
13
|
+
const expires = new Date(issued.getTime() + validity * 1000);
|
|
14
|
+
const nonce = opts.nonce ?? randomBytes(32).toString("base64url");
|
|
15
|
+
const unsigned = {
|
|
16
|
+
type: "aps.capability.v1.SinkChallenge",
|
|
17
|
+
sink_id: opts.sink_id,
|
|
18
|
+
subject_id: opts.subject_id,
|
|
19
|
+
action: opts.action,
|
|
20
|
+
nonce,
|
|
21
|
+
issued_at: issued.toISOString(),
|
|
22
|
+
expires_at: expires.toISOString(),
|
|
23
|
+
required_policy_freshness: opts.required_policy_freshness ?? DEFAULT_FRESHNESS,
|
|
24
|
+
};
|
|
25
|
+
const signature = sign(canonicalWithoutSignature({ ...unsigned, sink_signature: "" }, "sink_signature"), opts.sink_key.privateKey);
|
|
26
|
+
return { ...unsigned, sink_signature: signature };
|
|
27
|
+
}
|
|
28
|
+
export function challengeHash(challenge) {
|
|
29
|
+
return sha256Hex(canonicalWithoutSignature(challenge, "sink_signature"));
|
|
30
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type EpistemicType = "closed" | "witnessed" | "unresolved" | "self-asserted" | "witnessed-by-subject" | "corroborated";
|
|
2
|
+
export interface SinkAction {
|
|
3
|
+
kind: string;
|
|
4
|
+
target: string;
|
|
5
|
+
parameters: Record<string, unknown>;
|
|
6
|
+
resource_version: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PolicyFreshnessRequirement {
|
|
9
|
+
max_age_seconds: number;
|
|
10
|
+
beacon_hash_required: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface SinkChallenge {
|
|
13
|
+
type: "aps.capability.v1.SinkChallenge";
|
|
14
|
+
sink_id: string;
|
|
15
|
+
subject_id: string;
|
|
16
|
+
action: SinkAction;
|
|
17
|
+
nonce: string;
|
|
18
|
+
issued_at: string;
|
|
19
|
+
expires_at: string;
|
|
20
|
+
required_policy_freshness: PolicyFreshnessRequirement;
|
|
21
|
+
sink_signature: string;
|
|
22
|
+
}
|
|
23
|
+
export interface DelegationEnvelope {
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface AuthorityTokenReveal {
|
|
27
|
+
token_preimage: string;
|
|
28
|
+
merkle_proof: string[];
|
|
29
|
+
scope_class: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FreshnessBeacon {
|
|
32
|
+
delegator_id: string;
|
|
33
|
+
beacon_timestamp: string;
|
|
34
|
+
beacon_signature: string;
|
|
35
|
+
}
|
|
36
|
+
export interface AuthorityEvaluationRequest {
|
|
37
|
+
type: "aps.capability.v1.AuthorityEvaluationRequest";
|
|
38
|
+
challenge: SinkChallenge;
|
|
39
|
+
delegation_chain: DelegationEnvelope[];
|
|
40
|
+
delegation_chain_root: string;
|
|
41
|
+
delegation_depth: number;
|
|
42
|
+
authority_token: AuthorityTokenReveal;
|
|
43
|
+
freshness_beacon: FreshnessBeacon;
|
|
44
|
+
subject_signature: string;
|
|
45
|
+
}
|
|
46
|
+
export type Decision = "permit" | "deny";
|
|
47
|
+
export interface ChallengeReceiptEpistemicClaims {
|
|
48
|
+
policy_evaluated: EpistemicType;
|
|
49
|
+
authority_consumed: EpistemicType;
|
|
50
|
+
scope_within_bounds: EpistemicType;
|
|
51
|
+
effect_occurred: EpistemicType;
|
|
52
|
+
}
|
|
53
|
+
export interface ChallengeReceipt {
|
|
54
|
+
type: "aps.capability.v1.ChallengeReceipt";
|
|
55
|
+
challenge_hash: string;
|
|
56
|
+
decision: Decision;
|
|
57
|
+
deny_reason?: string;
|
|
58
|
+
delegation_chain_root: string;
|
|
59
|
+
delegation_depth: number;
|
|
60
|
+
authority_token_preimage?: string;
|
|
61
|
+
evaluated_at: string;
|
|
62
|
+
policy_digest: string;
|
|
63
|
+
epistemic_claims: ChallengeReceiptEpistemicClaims;
|
|
64
|
+
gateway_signature: string;
|
|
65
|
+
}
|
|
66
|
+
export type EffectOutcome = "success" | "failure" | "partial";
|
|
67
|
+
export interface SinkEffect {
|
|
68
|
+
executed_at: string;
|
|
69
|
+
outcome: EffectOutcome;
|
|
70
|
+
result_digest: string;
|
|
71
|
+
}
|
|
72
|
+
export interface EffectReceiptEpistemicClaims {
|
|
73
|
+
effect_occurred: EpistemicType;
|
|
74
|
+
effect_result_bound: EpistemicType;
|
|
75
|
+
policy_evaluation_correct: EpistemicType;
|
|
76
|
+
}
|
|
77
|
+
export interface EffectReceipt {
|
|
78
|
+
type: "aps.capability.v1.EffectReceipt";
|
|
79
|
+
challenge_hash: string;
|
|
80
|
+
authority_token_preimage: string;
|
|
81
|
+
gateway_receipt_hash: string;
|
|
82
|
+
effect: SinkEffect;
|
|
83
|
+
epistemic_claims: EffectReceiptEpistemicClaims;
|
|
84
|
+
sink_signature: string;
|
|
85
|
+
}
|
|
86
|
+
export interface KeyPair {
|
|
87
|
+
publicKey: string;
|
|
88
|
+
privateKey: string;
|
|
89
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AuthorityEvaluationRequest, ChallengeReceipt, EffectReceipt, SinkChallenge } from "./types.js";
|
|
2
|
+
export type VerifyResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function verifySinkChallenge(challenge: SinkChallenge, sinkPublicKey: string): VerifyResult;
|
|
9
|
+
export declare function verifyAuthorityEvaluationRequest(request: AuthorityEvaluationRequest, subjectPublicKey: string, sinkPublicKey: string): VerifyResult;
|
|
10
|
+
export interface VerifyChallengeReceiptOptions {
|
|
11
|
+
receipt: ChallengeReceipt;
|
|
12
|
+
expected_challenge: SinkChallenge;
|
|
13
|
+
expected_delegation_chain_root: string;
|
|
14
|
+
gateway_public_key: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function verifyChallengeReceipt(opts: VerifyChallengeReceiptOptions): VerifyResult;
|
|
17
|
+
export interface VerifyEffectReceiptOptions {
|
|
18
|
+
receipt: EffectReceipt;
|
|
19
|
+
challenge_receipt: ChallengeReceipt;
|
|
20
|
+
sink_public_key: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function verifyEffectReceipt(opts: VerifyEffectReceiptOptions): VerifyResult;
|
|
23
|
+
export interface ReconstructAttestationOptions {
|
|
24
|
+
challenge: SinkChallenge;
|
|
25
|
+
receipt: ChallengeReceipt;
|
|
26
|
+
effect: EffectReceipt;
|
|
27
|
+
sink_public_key: string;
|
|
28
|
+
gateway_public_key: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function reconstructAttestationChain(opts: ReconstructAttestationOptions): VerifyResult;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Signature + binding verification for the four message types.
|
|
2
|
+
// Verifiers are pure: they take messages and public keys, return either
|
|
3
|
+
// {ok: true} or {ok: false, reason}.
|
|
4
|
+
import { verify } from "agent-passport-system";
|
|
5
|
+
import { canonicalWithoutSignature } from "./canonical.js";
|
|
6
|
+
import { challengeHash } from "./sinkChallenge.js";
|
|
7
|
+
import { challengeReceiptHash } from "./challengeReceipt.js";
|
|
8
|
+
export function verifySinkChallenge(challenge, sinkPublicKey) {
|
|
9
|
+
const payload = canonicalWithoutSignature(challenge, "sink_signature");
|
|
10
|
+
return verify(payload, challenge.sink_signature, sinkPublicKey)
|
|
11
|
+
? { ok: true }
|
|
12
|
+
: { ok: false, reason: "sink_signature invalid" };
|
|
13
|
+
}
|
|
14
|
+
export function verifyAuthorityEvaluationRequest(request, subjectPublicKey, sinkPublicKey) {
|
|
15
|
+
const payload = canonicalWithoutSignature(request, "subject_signature");
|
|
16
|
+
if (!verify(payload, request.subject_signature, subjectPublicKey)) {
|
|
17
|
+
return { ok: false, reason: "subject_signature invalid" };
|
|
18
|
+
}
|
|
19
|
+
return verifySinkChallenge(request.challenge, sinkPublicKey);
|
|
20
|
+
}
|
|
21
|
+
export function verifyChallengeReceipt(opts) {
|
|
22
|
+
const payload = canonicalWithoutSignature(opts.receipt, "gateway_signature");
|
|
23
|
+
if (!verify(payload, opts.receipt.gateway_signature, opts.gateway_public_key)) {
|
|
24
|
+
return { ok: false, reason: "gateway_signature invalid" };
|
|
25
|
+
}
|
|
26
|
+
const expectedHash = challengeHash(opts.expected_challenge);
|
|
27
|
+
if (opts.receipt.challenge_hash !== expectedHash) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
reason: "challenge_hash does not match the sink-issued challenge",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (!opts.receipt.delegation_chain_root ||
|
|
34
|
+
opts.receipt.delegation_chain_root !== opts.expected_delegation_chain_root) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: "delegation_chain_root missing or does not match the M2 commitment",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (opts.receipt.decision === "permit" && !opts.receipt.authority_token_preimage) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: "permit ChallengeReceipt missing authority_token_preimage",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { ok: true };
|
|
47
|
+
}
|
|
48
|
+
export function verifyEffectReceipt(opts) {
|
|
49
|
+
const payload = canonicalWithoutSignature(opts.receipt, "sink_signature");
|
|
50
|
+
if (!verify(payload, opts.receipt.sink_signature, opts.sink_public_key)) {
|
|
51
|
+
return { ok: false, reason: "sink_signature invalid" };
|
|
52
|
+
}
|
|
53
|
+
if (opts.receipt.challenge_hash !== opts.challenge_receipt.challenge_hash) {
|
|
54
|
+
return { ok: false, reason: "challenge_hash mismatch between M3 and M4" };
|
|
55
|
+
}
|
|
56
|
+
if (opts.receipt.authority_token_preimage !==
|
|
57
|
+
opts.challenge_receipt.authority_token_preimage) {
|
|
58
|
+
return { ok: false, reason: "authority_token_preimage mismatch between M3 and M4" };
|
|
59
|
+
}
|
|
60
|
+
if (opts.receipt.gateway_receipt_hash !== challengeReceiptHash(opts.challenge_receipt)) {
|
|
61
|
+
return { ok: false, reason: "gateway_receipt_hash does not match M3 canonical hash" };
|
|
62
|
+
}
|
|
63
|
+
return { ok: true };
|
|
64
|
+
}
|
|
65
|
+
export function reconstructAttestationChain(opts) {
|
|
66
|
+
const m1 = verifySinkChallenge(opts.challenge, opts.sink_public_key);
|
|
67
|
+
if (!m1.ok)
|
|
68
|
+
return m1;
|
|
69
|
+
const m3 = verifyChallengeReceipt({
|
|
70
|
+
receipt: opts.receipt,
|
|
71
|
+
expected_challenge: opts.challenge,
|
|
72
|
+
expected_delegation_chain_root: opts.receipt.delegation_chain_root,
|
|
73
|
+
gateway_public_key: opts.gateway_public_key,
|
|
74
|
+
});
|
|
75
|
+
if (!m3.ok)
|
|
76
|
+
return m3;
|
|
77
|
+
const m4 = verifyEffectReceipt({
|
|
78
|
+
receipt: opts.effect,
|
|
79
|
+
challenge_receipt: opts.receipt,
|
|
80
|
+
sink_public_key: opts.sink_public_key,
|
|
81
|
+
});
|
|
82
|
+
if (!m4.ok)
|
|
83
|
+
return m4;
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
package/build/index.js
CHANGED
|
@@ -63,6 +63,17 @@ computeDataAxisWeights, computeComputeAxisWeights, DEFAULT_WEIGHT_PROFILE,
|
|
|
63
63
|
// Attribution Settlement (Build C — per-period signed settlement record)
|
|
64
64
|
aggregateAttributionPrimitives, buildContributorQueryResponse, signSettlementRecord, verifySettlementRecord, } from "agent-passport-system";
|
|
65
65
|
// ═══════════════════════════════════════
|
|
66
|
+
// Mutual Authentication v1 (SDK v2.2.0)
|
|
67
|
+
// ═══════════════════════════════════════
|
|
68
|
+
import { buildCertificate, signCertificate, certificateId, verifyBundle, verifyAttest, deriveSession, } from "agent-passport-system";
|
|
69
|
+
// ═══════════════════════════════════════
|
|
70
|
+
// Capability Token v0.1 (reference implementation)
|
|
71
|
+
// Spec: agent-passport-system/docs/CAPABILITY-TOKEN-SPEC-DRAFT.md
|
|
72
|
+
// ═══════════════════════════════════════
|
|
73
|
+
import { issueSinkChallenge as issueCapabilitySinkChallenge, buildEvaluationRequest as buildCapabilityEvaluationRequest, mintChallengeReceipt as mintCapabilityChallengeReceipt, signEffectReceipt as signCapabilityEffectReceipt, InMemoryNullifierSet, verifyChallengeReceipt as verifyCapabilityChallengeReceipt, challengeHash as capabilityChallengeHash, } from "./capabilityToken/index.js";
|
|
74
|
+
// One nullifier set per MCP process. v0.1: in-memory, no persistence.
|
|
75
|
+
const capabilityNullifierSet = new InMemoryNullifierSet();
|
|
76
|
+
// ═══════════════════════════════════════
|
|
66
77
|
// State Management
|
|
67
78
|
// ═══════════════════════════════════════
|
|
68
79
|
const STORE_PATH = join(process.env.HOME || '.', '.agent-passport-tasks.json');
|
|
@@ -649,6 +660,11 @@ const TOOL_SCOPE_MAP = {
|
|
|
649
660
|
'aps_aggregate_settlement': 'settlement',
|
|
650
661
|
'aps_verify_settlement': 'settlement',
|
|
651
662
|
'aps_build_contributor_query': 'settlement',
|
|
663
|
+
// Capability Token v0.1 (reference impl) — new 'capability' scope
|
|
664
|
+
'aps_capability_issue_challenge': 'capability',
|
|
665
|
+
'aps_capability_evaluate_authority': 'capability',
|
|
666
|
+
'aps_capability_mint_receipt': 'capability',
|
|
667
|
+
'aps_capability_sign_effect': 'capability',
|
|
652
668
|
};
|
|
653
669
|
// ═══════════════════════════════════════
|
|
654
670
|
// TOOL: list_profiles
|
|
@@ -4353,6 +4369,285 @@ server.tool("aps_build_contributor_query", "Build a contributor-query response:
|
|
|
4353
4369
|
return { content: [{ type: "text", text: safeError("buildContributorQuery failed", e) }], isError: true };
|
|
4354
4370
|
}
|
|
4355
4371
|
});
|
|
4372
|
+
// ═══════════════════════════════════════
|
|
4373
|
+
// Mutual Authentication v1 tools (SDK v2.2.0)
|
|
4374
|
+
// ═══════════════════════════════════════
|
|
4375
|
+
server.tool("mutualAuthBuildCertificate", "Build and sign a mutual-auth certificate identifying an agent or information system. Returns the signed MutualAuthCertificate object ready to carry into a handshake. The issuer's Ed25519 private key (hex) signs over the canonical (JCS) form.", {
|
|
4376
|
+
role: z.enum(["agent", "information_system"]).describe("Role of the subject this cert identifies"),
|
|
4377
|
+
subject_id: z.string().describe("Stable subject identifier (e.g., agent DID, IS endpoint URL)"),
|
|
4378
|
+
subject_pubkey_hex: z.string().describe("Ed25519 public key (hex) of the subject"),
|
|
4379
|
+
issuer_id: z.string().describe("Issuer identifier"),
|
|
4380
|
+
issuer_role: z.enum(["agent", "information_system", "trust_anchor"]).describe("Role of the issuer"),
|
|
4381
|
+
issuer_pubkey_hex: z.string().describe("Ed25519 public key (hex) of the issuer"),
|
|
4382
|
+
issuer_privkey_hex: z.string().describe("Ed25519 private key (hex) of the issuer — used to sign"),
|
|
4383
|
+
binding: z.string().describe("For an agent: the APS agent_id. For an IS: the resource domain (e.g., mcp://api.bank.com)"),
|
|
4384
|
+
not_before: z.number().describe("Earliest valid time (unix ms)"),
|
|
4385
|
+
not_after: z.number().describe("Latest valid time (unix ms)"),
|
|
4386
|
+
supported_versions: z.array(z.string()).describe("Protocol versions supported, highest first (e.g., ['1.0'])"),
|
|
4387
|
+
attestation_grade: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]).optional().describe("For agents: APS attestation grade 0-3"),
|
|
4388
|
+
capabilities: z.array(z.string()).optional().describe("Optional capability tags"),
|
|
4389
|
+
}, async (args) => {
|
|
4390
|
+
try {
|
|
4391
|
+
const unsigned = buildCertificate({
|
|
4392
|
+
role: args.role,
|
|
4393
|
+
subject_id: args.subject_id,
|
|
4394
|
+
subject_pubkey_hex: args.subject_pubkey_hex,
|
|
4395
|
+
issuer_id: args.issuer_id,
|
|
4396
|
+
issuer_role: args.issuer_role,
|
|
4397
|
+
binding: args.binding,
|
|
4398
|
+
not_before: args.not_before,
|
|
4399
|
+
not_after: args.not_after,
|
|
4400
|
+
supported_versions: args.supported_versions,
|
|
4401
|
+
attestation_grade: args.attestation_grade,
|
|
4402
|
+
capabilities: args.capabilities,
|
|
4403
|
+
}, args.issuer_pubkey_hex);
|
|
4404
|
+
const cert = signCertificate(unsigned, args.issuer_privkey_hex);
|
|
4405
|
+
return {
|
|
4406
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
4407
|
+
certificate: cert,
|
|
4408
|
+
certificate_id: certificateId(cert),
|
|
4409
|
+
}, null, 2) }],
|
|
4410
|
+
};
|
|
4411
|
+
}
|
|
4412
|
+
catch (e) {
|
|
4413
|
+
return { content: [{ type: "text", text: safeError("mutualAuthBuildCertificate failed", e) }], isError: true };
|
|
4414
|
+
}
|
|
4415
|
+
});
|
|
4416
|
+
server.tool("mutualAuthVerifyAttest", "Verify a MutualAuthAttest against policy and trust anchors. Runs all 10 verification checks: signature, version negotiation, nonce match, timestamp freshness, certificate validity, issuer anchor check, binding constraints, downgrade detection, attestation grade policy, capability policy. Returns ok:true on success or a failure reason on rejection.", {
|
|
4417
|
+
attest: z.any().describe("MutualAuthAttest to verify"),
|
|
4418
|
+
expected_peer_nonce_b64: z.string().describe("The nonce the peer sent in their prior hello or attest"),
|
|
4419
|
+
expected_own_nonce_b64: z.string().describe("The nonce we sent in our own prior hello or attest"),
|
|
4420
|
+
policy: z.any().describe("MutualAuthPolicy (accepted_versions, min_agent_grade, required_capabilities, max_clock_skew_ms, max_session_ms)"),
|
|
4421
|
+
trust_anchors: z.array(z.any()).describe("TrustAnchor[] — local trusted roots"),
|
|
4422
|
+
revoked_anchor_ids: z.array(z.string()).optional().describe("IDs of anchors revoked since the bundle was issued"),
|
|
4423
|
+
now_ms: z.number().optional().describe("Current unix ms — defaults to Date.now()"),
|
|
4424
|
+
}, async (args) => {
|
|
4425
|
+
try {
|
|
4426
|
+
const res = verifyAttest({
|
|
4427
|
+
attest: args.attest,
|
|
4428
|
+
expected_peer_nonce_b64: args.expected_peer_nonce_b64,
|
|
4429
|
+
expected_own_nonce_b64: args.expected_own_nonce_b64,
|
|
4430
|
+
policy: args.policy,
|
|
4431
|
+
trust_anchors: args.trust_anchors,
|
|
4432
|
+
revoked_anchor_ids: args.revoked_anchor_ids,
|
|
4433
|
+
now_ms: args.now_ms ?? Date.now(),
|
|
4434
|
+
});
|
|
4435
|
+
return {
|
|
4436
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
4437
|
+
};
|
|
4438
|
+
}
|
|
4439
|
+
catch (e) {
|
|
4440
|
+
return { content: [{ type: "text", text: safeError("mutualAuthVerifyAttest failed", e) }], isError: true };
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4443
|
+
server.tool("mutualAuthDeriveSession", "Derive the shared mutual-auth session record from both sides' Attests. Both parties MUST compute identical session_id given identical inputs (canonical JCS + sha256 of chosen_version, both cert ids, both nonces). Returns a MutualAuthSession with session_id + both certificates + expiry bounds, or failure reason.", {
|
|
4444
|
+
agent_attest: z.any().describe("The agent's MutualAuthAttest"),
|
|
4445
|
+
is_attest: z.any().describe("The information system's MutualAuthAttest"),
|
|
4446
|
+
policy: z.any().describe("MutualAuthPolicy"),
|
|
4447
|
+
now_ms: z.number().optional().describe("Current unix ms — defaults to Date.now()"),
|
|
4448
|
+
}, async (args) => {
|
|
4449
|
+
try {
|
|
4450
|
+
const res = deriveSession(args.agent_attest, args.is_attest, args.policy, args.now_ms ?? Date.now());
|
|
4451
|
+
return {
|
|
4452
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
4453
|
+
};
|
|
4454
|
+
}
|
|
4455
|
+
catch (e) {
|
|
4456
|
+
return { content: [{ type: "text", text: safeError("mutualAuthDeriveSession failed", e) }], isError: true };
|
|
4457
|
+
}
|
|
4458
|
+
});
|
|
4459
|
+
server.tool("mutualAuthVerifyTrustBundle", "Verify a TrustAnchorBundle signature and freshness. Caller supplies the list of trusted publisher public keys (root configuration). Returns ok:true on success or failure reason (untrusted_publisher, signature_invalid, bundle_expired, not_yet_valid).", {
|
|
4460
|
+
bundle: z.any().describe("TrustAnchorBundle to verify"),
|
|
4461
|
+
trusted_publisher_pubkeys_hex: z.array(z.string()).describe("List of Ed25519 pubkeys (hex) authorized to publish bundles"),
|
|
4462
|
+
now_ms: z.number().optional().describe("Current unix ms — defaults to Date.now()"),
|
|
4463
|
+
}, async (args) => {
|
|
4464
|
+
try {
|
|
4465
|
+
const res = verifyBundle(args.bundle, args.trusted_publisher_pubkeys_hex, args.now_ms ?? Date.now());
|
|
4466
|
+
return {
|
|
4467
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
4470
|
+
catch (e) {
|
|
4471
|
+
return { content: [{ type: "text", text: safeError("mutualAuthVerifyTrustBundle failed", e) }], isError: true };
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
4474
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4475
|
+
// Capability Token v0.1 — reference implementation tools
|
|
4476
|
+
// Spec: agent-passport-system/docs/CAPABILITY-TOKEN-SPEC-DRAFT.md
|
|
4477
|
+
// Namespace: aps.capability.v1.* (the protocol message type strings).
|
|
4478
|
+
// Tool names use snake_case per MCP convention.
|
|
4479
|
+
//
|
|
4480
|
+
// MCP is APS-aware by construction — tools are sinks. These four tools
|
|
4481
|
+
// expose the four protocol roles (sink, subject, gateway) so any caller
|
|
4482
|
+
// can drive the cycle end-to-end against this single process.
|
|
4483
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4484
|
+
server.tool("aps_capability_issue_challenge", "v0.1 capability-token sink challenge (M1). Sink issues a signed canonical action statement. Returns the SinkChallenge and its challenge_hash. Used to bind the gateway's later policy evaluation to a specific action the sink authored. Search keywords: capability token, sink challenge, M1.", {
|
|
4485
|
+
sink_id: z.string().describe("DID of the sink issuing the challenge"),
|
|
4486
|
+
subject_id: z.string().describe("DID of the subject the challenge is addressed to"),
|
|
4487
|
+
action: z.object({
|
|
4488
|
+
kind: z.string(),
|
|
4489
|
+
target: z.string(),
|
|
4490
|
+
parameters: z.record(z.any()),
|
|
4491
|
+
resource_version: z.string(),
|
|
4492
|
+
}).describe("Canonical action statement"),
|
|
4493
|
+
sink_private_key: z.string().describe("Sink Ed25519 private key (hex)"),
|
|
4494
|
+
sink_public_key: z.string().describe("Sink Ed25519 public key (hex)"),
|
|
4495
|
+
validity_seconds: z.number().int().positive().optional(),
|
|
4496
|
+
required_policy_freshness: z.object({
|
|
4497
|
+
max_age_seconds: z.number().int().nonnegative(),
|
|
4498
|
+
beacon_hash_required: z.boolean(),
|
|
4499
|
+
}).optional(),
|
|
4500
|
+
}, async (args) => {
|
|
4501
|
+
try {
|
|
4502
|
+
const sinkKey = {
|
|
4503
|
+
publicKey: args.sink_public_key,
|
|
4504
|
+
privateKey: args.sink_private_key,
|
|
4505
|
+
};
|
|
4506
|
+
const challenge = issueCapabilitySinkChallenge({
|
|
4507
|
+
sink_id: args.sink_id,
|
|
4508
|
+
subject_id: args.subject_id,
|
|
4509
|
+
action: args.action,
|
|
4510
|
+
validity_seconds: args.validity_seconds,
|
|
4511
|
+
required_policy_freshness: args.required_policy_freshness,
|
|
4512
|
+
sink_key: sinkKey,
|
|
4513
|
+
});
|
|
4514
|
+
const hash = capabilityChallengeHash(challenge);
|
|
4515
|
+
return {
|
|
4516
|
+
content: [{
|
|
4517
|
+
type: "text",
|
|
4518
|
+
text: JSON.stringify({ challenge, challenge_hash: hash }, null, 2),
|
|
4519
|
+
}],
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
catch (e) {
|
|
4523
|
+
return { content: [{ type: "text", text: safeError("aps_capability_issue_challenge failed", e) }], isError: true };
|
|
4524
|
+
}
|
|
4525
|
+
});
|
|
4526
|
+
server.tool("aps_capability_evaluate_authority", "v0.1 capability-token authority evaluation request (M2). Subject signs a request carrying the sink's M1, the delegation chain, and a revealed authority-token preimage. The gateway consumes this to decide permit/deny. Search keywords: capability token, authority evaluation, M2.", {
|
|
4527
|
+
challenge: z.any().describe("SinkChallenge object from M1"),
|
|
4528
|
+
delegation_chain: z.array(z.any()).describe("v2.x delegation envelopes"),
|
|
4529
|
+
authority_token: z.object({
|
|
4530
|
+
token_preimage: z.string(),
|
|
4531
|
+
merkle_proof: z.array(z.string()),
|
|
4532
|
+
scope_class: z.string(),
|
|
4533
|
+
}),
|
|
4534
|
+
freshness_beacon: z.object({
|
|
4535
|
+
delegator_id: z.string(),
|
|
4536
|
+
beacon_timestamp: z.string(),
|
|
4537
|
+
beacon_signature: z.string(),
|
|
4538
|
+
}),
|
|
4539
|
+
subject_private_key: z.string().describe("Subject Ed25519 private key (hex)"),
|
|
4540
|
+
subject_public_key: z.string().describe("Subject Ed25519 public key (hex)"),
|
|
4541
|
+
delegation_chain_root: z.string().optional().describe("Override; otherwise computed from chain"),
|
|
4542
|
+
}, async (args) => {
|
|
4543
|
+
try {
|
|
4544
|
+
const subjectKey = {
|
|
4545
|
+
publicKey: args.subject_public_key,
|
|
4546
|
+
privateKey: args.subject_private_key,
|
|
4547
|
+
};
|
|
4548
|
+
const request = buildCapabilityEvaluationRequest({
|
|
4549
|
+
challenge: args.challenge,
|
|
4550
|
+
delegation_chain: args.delegation_chain,
|
|
4551
|
+
authority_token: args.authority_token,
|
|
4552
|
+
freshness_beacon: args.freshness_beacon,
|
|
4553
|
+
subject_key: subjectKey,
|
|
4554
|
+
delegation_chain_root: args.delegation_chain_root,
|
|
4555
|
+
});
|
|
4556
|
+
return {
|
|
4557
|
+
content: [{ type: "text", text: JSON.stringify(request, null, 2) }],
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
4560
|
+
catch (e) {
|
|
4561
|
+
return { content: [{ type: "text", text: safeError("aps_capability_evaluate_authority failed", e) }], isError: true };
|
|
4562
|
+
}
|
|
4563
|
+
});
|
|
4564
|
+
server.tool("aps_capability_mint_receipt", "v0.1 capability-token gateway receipt (M3). Gateway signs a permit or deny over the sink's exact challenge_hash. Echoes the M2 delegation_chain_root so the sink can verify the gateway saw the same chain the subject committed to. Search keywords: capability token, challenge receipt, gateway receipt, M3.", {
|
|
4565
|
+
request: z.any().describe("AuthorityEvaluationRequest from M2"),
|
|
4566
|
+
decision: z.enum(["permit", "deny"]),
|
|
4567
|
+
deny_reason: z.string().optional(),
|
|
4568
|
+
policy_digest: z.string().describe("SHA-256 of the policy bundle used in evaluation"),
|
|
4569
|
+
gateway_private_key: z.string(),
|
|
4570
|
+
gateway_public_key: z.string(),
|
|
4571
|
+
}, async (args) => {
|
|
4572
|
+
try {
|
|
4573
|
+
const gatewayKey = {
|
|
4574
|
+
publicKey: args.gateway_public_key,
|
|
4575
|
+
privateKey: args.gateway_private_key,
|
|
4576
|
+
};
|
|
4577
|
+
const receipt = mintCapabilityChallengeReceipt({
|
|
4578
|
+
request: args.request,
|
|
4579
|
+
decision: args.decision,
|
|
4580
|
+
deny_reason: args.deny_reason,
|
|
4581
|
+
policy_digest: args.policy_digest,
|
|
4582
|
+
gateway_key: gatewayKey,
|
|
4583
|
+
});
|
|
4584
|
+
return {
|
|
4585
|
+
content: [{ type: "text", text: JSON.stringify(receipt, null, 2) }],
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
catch (e) {
|
|
4589
|
+
return { content: [{ type: "text", text: safeError("aps_capability_mint_receipt failed", e) }], isError: true };
|
|
4590
|
+
}
|
|
4591
|
+
});
|
|
4592
|
+
server.tool("aps_capability_sign_effect", "v0.1 capability-token sink effect receipt (M4). Sink consumes the token preimage from the gateway's M3 (rejecting on nullifier replay), executes the action, and signs an EffectReceipt binding the consumed token to the result. The (M1, M3, M4) tuple is the full attestation record. Search keywords: capability token, effect receipt, M4, sink attestation.", {
|
|
4593
|
+
challenge: z.any().describe("Original SinkChallenge from M1 (for binding verification)"),
|
|
4594
|
+
challenge_receipt: z.any().describe("ChallengeReceipt from M3"),
|
|
4595
|
+
effect: z.object({
|
|
4596
|
+
executed_at: z.string(),
|
|
4597
|
+
outcome: z.enum(["success", "failure", "partial"]),
|
|
4598
|
+
result_digest: z.string(),
|
|
4599
|
+
}),
|
|
4600
|
+
sink_private_key: z.string(),
|
|
4601
|
+
sink_public_key: z.string(),
|
|
4602
|
+
gateway_public_key: z.string().describe("Used to verify M3 before consuming the token"),
|
|
4603
|
+
expected_delegation_chain_root: z.string().optional().describe("If omitted, falls back to the receipt's own root"),
|
|
4604
|
+
}, async (args) => {
|
|
4605
|
+
try {
|
|
4606
|
+
const sinkKey = {
|
|
4607
|
+
publicKey: args.sink_public_key,
|
|
4608
|
+
privateKey: args.sink_private_key,
|
|
4609
|
+
};
|
|
4610
|
+
const challenge = args.challenge;
|
|
4611
|
+
const receipt = args.challenge_receipt;
|
|
4612
|
+
const verification = verifyCapabilityChallengeReceipt({
|
|
4613
|
+
receipt,
|
|
4614
|
+
expected_challenge: challenge,
|
|
4615
|
+
expected_delegation_chain_root: args.expected_delegation_chain_root ?? receipt.delegation_chain_root,
|
|
4616
|
+
gateway_public_key: args.gateway_public_key,
|
|
4617
|
+
});
|
|
4618
|
+
if (!verification.ok) {
|
|
4619
|
+
return {
|
|
4620
|
+
content: [{ type: "text", text: JSON.stringify({ error: "M3 verification failed", reason: verification.reason }) }],
|
|
4621
|
+
isError: true,
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
if (receipt.decision !== "permit") {
|
|
4625
|
+
return {
|
|
4626
|
+
content: [{ type: "text", text: JSON.stringify({ error: "M3 is a deny — refusing to emit M4", deny_reason: receipt.deny_reason }) }],
|
|
4627
|
+
isError: true,
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
const preimage = receipt.authority_token_preimage;
|
|
4631
|
+
capabilityNullifierSet.consume(preimage);
|
|
4632
|
+
const effectReceipt = signCapabilityEffectReceipt({
|
|
4633
|
+
challenge_receipt: receipt,
|
|
4634
|
+
effect: args.effect,
|
|
4635
|
+
sink_key: sinkKey,
|
|
4636
|
+
});
|
|
4637
|
+
return {
|
|
4638
|
+
content: [{
|
|
4639
|
+
type: "text",
|
|
4640
|
+
text: JSON.stringify({
|
|
4641
|
+
effect_receipt: effectReceipt,
|
|
4642
|
+
nullifier_set_size: capabilityNullifierSet.size(),
|
|
4643
|
+
}, null, 2),
|
|
4644
|
+
}],
|
|
4645
|
+
};
|
|
4646
|
+
}
|
|
4647
|
+
catch (e) {
|
|
4648
|
+
return { content: [{ type: "text", text: safeError("aps_capability_sign_effect failed", e) }], isError: true };
|
|
4649
|
+
}
|
|
4650
|
+
});
|
|
4356
4651
|
server.prompt("coordination_role", "Get instructions for your assigned coordination role", {}, async () => {
|
|
4357
4652
|
const role = state.agentRole || 'default';
|
|
4358
4653
|
const instructions = ROLE_PROMPTS[role] || ROLE_PROMPTS['default'];
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-passport-system-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"mcpName": "io.github.aeoess/agent-passport-mcp",
|
|
5
|
-
"description": "MCP server for the Agent Passport System — protocol-layer tools only.
|
|
5
|
+
"description": "MCP server for the Agent Passport System — protocol-layer tools only. 150 tools (132 protocol + 10 gateway deprecation stubs). Identity, delegation, reputation, attestation, coordination, commerce, attribution primitive, attribution settlement. Tracks SDK v2.5.0-alpha (Wave 1 accountability primitives shipped in SDK; MCP exposure deferred). For gateway-runtime tools (ProxyGateway, AgentContext, DataEnforcementGate), use gateway.aeoess.com REST API or pin to v3.1.1.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"agent-passport-system-mcp": "./build/bin.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "npx tsc && chmod 755 build/bin.js build/index.js build/setup.js",
|
|
18
|
-
"test": "node --test tests/*.test.mjs",
|
|
18
|
+
"test": "npm run build && node --test tests/*.test.mjs build/__tests__/*.test.js",
|
|
19
19
|
"watch": "tsc --watch",
|
|
20
20
|
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
21
21
|
"prepublishOnly": "npm run build && npm test",
|
|
@@ -50,11 +50,14 @@
|
|
|
50
50
|
"homepage": "https://github.com/aeoess/agent-passport-mcp",
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
53
|
-
"agent-passport-system": "^2.
|
|
53
|
+
"agent-passport-system": "^2.5.0-alpha",
|
|
54
54
|
"zod": "^3.25.76"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^22.19.17",
|
|
58
58
|
"typescript": "^5.9.3"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18.0.0"
|
|
59
62
|
}
|
|
60
63
|
}
|