agent-passport-system-mcp 3.2.2 → 3.2.3

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 CHANGED
@@ -69,7 +69,7 @@ Or for remote SSE:
69
69
  ```
70
70
  </details>
71
71
 
72
- ## Tools (154)
72
+ ## Tools (150)
73
73
 
74
74
  ### Identity (Layer 1) — 5 tools
75
75
 
@@ -209,14 +209,13 @@ Layer 1 — Agent Passport Protocol (Ed25519 identity)
209
209
 
210
210
  ## Recognition
211
211
 
212
- - Integrated into [Microsoft agent-governance-toolkit](https://github.com/microsoft/agent-governance-toolkit) (PR #274)
212
+ - Three contribution PRs merged into the Microsoft Agent Governance Toolkit by a Microsoft maintainer (#274, #598, #1328)
213
213
  - Public comment submitted to NIST NCCoE on AI Agent Identity and Authorization standards
214
214
  - Collaboration with IETF DAAP draft author on delegation spec
215
- - Endorsed by Garry Tan (CEO, Y Combinator)
216
215
 
217
216
  ## Links
218
217
 
219
- - npm SDK: [agent-passport-system](https://www.npmjs.com/package/agent-passport-system) (v2.6.0, 3,791 tests)
218
+ - npm SDK: [agent-passport-system](https://www.npmjs.com/package/agent-passport-system) (v2.9.0, 3,881 tests)
220
219
  - Python SDK: [agent-passport-system](https://pypi.org/project/agent-passport-system/) (v2.4.0a3 pre-release; v2.3.0 stable)
221
220
  - Go SDK: [agent-passport-go](https://pkg.go.dev/github.com/aeoess/agent-passport-go) (v0.2.0-alpha.1; `go get github.com/aeoess/agent-passport-go@v0.2.0-alpha.1`)
222
221
  - Paper (Social Contract): [doi.org/10.5281/zenodo.18749779](https://doi.org/10.5281/zenodo.18749779)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ // Copyright 2024-2026 Tymofii Pidlisnyi. Apache-2.0 license. See LICENSE.
2
+ // Regression: a capability SinkChallenge carries a signed expires_at that no
3
+ // verifier enforced, so an expired permit stayed redeemable forever.
4
+ // verifySinkChallenge / verifyChallengeReceipt now reject an expired challenge.
5
+ import { test } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { generateKeyPair } from "agent-passport-system";
8
+ import { issueSinkChallenge } from "../capabilityToken/sinkChallenge.js";
9
+ import { verifySinkChallenge } from "../capabilityToken/verify.js";
10
+ const action = {
11
+ kind: "fs.write",
12
+ target: "file:///tmp/aps-capability-expiry-test.txt",
13
+ parameters: { content_length: 1 },
14
+ resource_version: "v1",
15
+ };
16
+ test("verifySinkChallenge accepts a live challenge and rejects an expired one", () => {
17
+ const sink = generateKeyPair();
18
+ const issued = new Date("2026-01-01T00:00:00.000Z");
19
+ const challenge = issueSinkChallenge({
20
+ sink_id: sink.publicKey,
21
+ subject_id: "subject-1",
22
+ action,
23
+ sink_key: sink,
24
+ validity_seconds: 60,
25
+ now: issued,
26
+ });
27
+ // 30s after issue: within the 60s validity.
28
+ assert.equal(verifySinkChallenge(challenge, sink.publicKey, issued.getTime() + 30_000).ok, true);
29
+ // 120s after issue: past expiry.
30
+ const expired = verifySinkChallenge(challenge, sink.publicKey, issued.getTime() + 120_000);
31
+ assert.equal(expired.ok, false);
32
+ assert.match(expired.reason, /expired/);
33
+ // A valid signature with a wrong key still fails for the signature reason, not expiry.
34
+ const other = generateKeyPair();
35
+ assert.equal(verifySinkChallenge(challenge, other.publicKey, issued.getTime() + 30_000).ok, false);
36
+ });
@@ -379,3 +379,41 @@ test("Hash stability: re-issuing M1 with the same nonce + clock yields the same
379
379
  assert.equal(challengeHash(a), challengeHash(b));
380
380
  assert.equal(canonicalWithoutSignature(a, "sink_signature"), canonicalWithoutSignature(b, "sink_signature"));
381
381
  });
382
+ // Regression (round-3, item 4): the gateway M3 mint must fail closed on an expired challenge.
383
+ // expires_at is signed into the M1 challenge, so minting a PERMIT after it would let a stale
384
+ // challenge be redeemed. mintChallengeReceipt now downgrades an expired permit to a deny.
385
+ test("M3 mint downgrades a permit over an expired challenge to a deny", () => {
386
+ const actors = setupActors();
387
+ const challenge = issueSinkChallenge({
388
+ sink_id: actors.sink.publicKey,
389
+ subject_id: actors.subject.publicKey,
390
+ action: sampleAction(),
391
+ sink_key: actors.sink,
392
+ });
393
+ const request = buildEvaluationRequest({
394
+ challenge,
395
+ delegation_chain: sampleDelegationChain(actors),
396
+ authority_token: sampleAuthorityToken(),
397
+ freshness_beacon: sampleFreshnessBeacon(actors),
398
+ subject_key: actors.subject,
399
+ });
400
+ // Mint with a clock far past the challenge's expires_at: permit must become deny.
401
+ const expired = mintChallengeReceipt({
402
+ request,
403
+ decision: "permit",
404
+ policy_digest: "policy-bundle-sha256-v0.1-fixture",
405
+ gateway_key: actors.gateway,
406
+ now: new Date("3000-01-01T00:00:00Z"),
407
+ });
408
+ assert.equal(expired.decision, "deny");
409
+ assert.equal(expired.deny_reason, "challenge_expired");
410
+ assert.equal(expired.authority_token_preimage, undefined);
411
+ // Control: minting within the validity window still permits.
412
+ const fresh = mintChallengeReceipt({
413
+ request,
414
+ decision: "permit",
415
+ policy_digest: "policy-bundle-sha256-v0.1-fixture",
416
+ gateway_key: actors.gateway,
417
+ });
418
+ assert.equal(fresh.decision, "permit");
419
+ });
@@ -17,23 +17,34 @@ const CLOSED_DENY_CLAIMS = {
17
17
  effect_occurred: "unresolved",
18
18
  };
19
19
  export function mintChallengeReceipt(opts) {
20
+ const evaluatedAt = (opts.now ?? new Date()).toISOString();
21
+ // Fail closed on an expired challenge: expires_at is signed into the challenge, so the gateway
22
+ // must not mint a PERMIT after it. Downgrade an expired permit to a deny. nowMs defaults to the
23
+ // wall clock; an unparseable expires_at is treated as expired.
24
+ const nowMs = (opts.now ?? new Date()).getTime();
25
+ const expMs = Date.parse(opts.request.challenge.expires_at);
26
+ let decision = opts.decision;
27
+ let denyReason = opts.deny_reason;
28
+ if (decision === "permit" && (!Number.isFinite(expMs) || nowMs > expMs)) {
29
+ decision = "deny";
30
+ denyReason = "challenge_expired";
31
+ }
20
32
  const cHash = opts.override_challenge_hash ?? challengeHash(opts.request.challenge);
21
33
  const claims = opts.epistemic_claims ??
22
- (opts.decision === "permit" ? CLOSED_PERMIT_CLAIMS : CLOSED_DENY_CLAIMS);
23
- const evaluatedAt = (opts.now ?? new Date()).toISOString();
34
+ (decision === "permit" ? CLOSED_PERMIT_CLAIMS : CLOSED_DENY_CLAIMS);
24
35
  const chainRoot = opts.omit_delegation_chain_root
25
36
  ? ""
26
37
  : opts.override_delegation_chain_root ?? opts.request.delegation_chain_root;
27
38
  const unsigned = {
28
39
  type: "aps.capability.v1.ChallengeReceipt",
29
40
  challenge_hash: cHash,
30
- decision: opts.decision,
31
- ...(opts.decision === "deny" && opts.deny_reason
32
- ? { deny_reason: opts.deny_reason }
41
+ decision: decision,
42
+ ...(decision === "deny" && denyReason
43
+ ? { deny_reason: denyReason }
33
44
  : {}),
34
45
  delegation_chain_root: chainRoot,
35
46
  delegation_depth: opts.request.delegation_depth,
36
- ...(opts.decision === "permit"
47
+ ...(decision === "permit"
37
48
  ? { authority_token_preimage: opts.request.authority_token.token_preimage }
38
49
  : {}),
39
50
  evaluated_at: evaluatedAt,
@@ -5,15 +5,15 @@ export type VerifyResult = {
5
5
  ok: false;
6
6
  reason: string;
7
7
  };
8
- export declare function verifySinkChallenge(challenge: SinkChallenge, sinkPublicKey: string): VerifyResult;
9
- export declare function verifyAuthorityEvaluationRequest(request: AuthorityEvaluationRequest, subjectPublicKey: string, sinkPublicKey: string): VerifyResult;
8
+ export declare function verifySinkChallenge(challenge: SinkChallenge, sinkPublicKey: string, nowMs?: number): VerifyResult;
9
+ export declare function verifyAuthorityEvaluationRequest(request: AuthorityEvaluationRequest, subjectPublicKey: string, sinkPublicKey: string, nowMs?: number): VerifyResult;
10
10
  export interface VerifyChallengeReceiptOptions {
11
11
  receipt: ChallengeReceipt;
12
12
  expected_challenge: SinkChallenge;
13
13
  expected_delegation_chain_root: string;
14
14
  gateway_public_key: string;
15
15
  }
16
- export declare function verifyChallengeReceipt(opts: VerifyChallengeReceiptOptions): VerifyResult;
16
+ export declare function verifyChallengeReceipt(opts: VerifyChallengeReceiptOptions, nowMs?: number): VerifyResult;
17
17
  export interface VerifyEffectReceiptOptions {
18
18
  receipt: EffectReceipt;
19
19
  challenge_receipt: ChallengeReceipt;
@@ -5,24 +5,38 @@ import { verify } from "agent-passport-system";
5
5
  import { canonicalWithoutSignature } from "./canonical.js";
6
6
  import { challengeHash } from "./sinkChallenge.js";
7
7
  import { challengeReceiptHash } from "./challengeReceipt.js";
8
- export function verifySinkChallenge(challenge, sinkPublicKey) {
8
+ export function verifySinkChallenge(challenge, sinkPublicKey, nowMs = Date.now()) {
9
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" };
10
+ if (!verify(payload, challenge.sink_signature, sinkPublicKey)) {
11
+ return { ok: false, reason: "sink_signature invalid" };
12
+ }
13
+ // Enforce expiry. expires_at is signed and propagated but was never checked, so an expired
14
+ // capability challenge stayed redeemable forever. nowMs defaults to the wall clock so the check
15
+ // is enforced by default and cannot fail open; tests inject a fixed clock.
16
+ const expMs = Date.parse(challenge.expires_at);
17
+ if (!Number.isFinite(expMs) || nowMs > expMs) {
18
+ return { ok: false, reason: "challenge expired" };
19
+ }
20
+ return { ok: true };
13
21
  }
14
- export function verifyAuthorityEvaluationRequest(request, subjectPublicKey, sinkPublicKey) {
22
+ export function verifyAuthorityEvaluationRequest(request, subjectPublicKey, sinkPublicKey, nowMs = Date.now()) {
15
23
  const payload = canonicalWithoutSignature(request, "subject_signature");
16
24
  if (!verify(payload, request.subject_signature, subjectPublicKey)) {
17
25
  return { ok: false, reason: "subject_signature invalid" };
18
26
  }
19
- return verifySinkChallenge(request.challenge, sinkPublicKey);
27
+ return verifySinkChallenge(request.challenge, sinkPublicKey, nowMs);
20
28
  }
21
- export function verifyChallengeReceipt(opts) {
29
+ export function verifyChallengeReceipt(opts, nowMs = Date.now()) {
22
30
  const payload = canonicalWithoutSignature(opts.receipt, "gateway_signature");
23
31
  if (!verify(payload, opts.receipt.gateway_signature, opts.gateway_public_key)) {
24
32
  return { ok: false, reason: "gateway_signature invalid" };
25
33
  }
34
+ // Reject a receipt whose underlying challenge has expired. expires_at was never enforced, so an
35
+ // expired capability stayed redeemable. nowMs defaults to the wall clock (enforced by default).
36
+ const expMs = Date.parse(opts.expected_challenge.expires_at);
37
+ if (!Number.isFinite(expMs) || nowMs > expMs) {
38
+ return { ok: false, reason: "challenge expired" };
39
+ }
26
40
  const expectedHash = challengeHash(opts.expected_challenge);
27
41
  if (opts.receipt.challenge_hash !== expectedHash) {
28
42
  return {
package/build/index.js CHANGED
@@ -2309,13 +2309,21 @@ server.tool("commerce_preflight", "Run preflight checks before a purchase. Valid
2309
2309
  platform: 'node',
2310
2310
  models: ['mcp'],
2311
2311
  });
2312
- // Look up or create commerce delegation using actual scope/spend
2313
- const commerceDel = createCommerceDelegation({
2314
- agentId: args.agent_id,
2315
- delegationId: args.delegation_id,
2316
- spendLimit: actualSpendLimit,
2317
- approvedMerchants: [], // Empty = all merchants allowed
2318
- });
2312
+ // Look up or create commerce delegation using actual scope/spend.
2313
+ // Reflect spend already recorded on the session delegation instead of the hardcoded 0 that
2314
+ // createCommerceDelegation returns, so the spend gate is not a structural no-op that always
2315
+ // sees the full limit remaining. NOTE: nothing in the MCP yet INCREMENTS sessionDel.spentAmount
2316
+ // (a spend-record path is a metering decision flagged for Tima), so cumulative enforcement here
2317
+ // is only as complete as the session state that feeds it.
2318
+ const commerceDel = {
2319
+ ...createCommerceDelegation({
2320
+ agentId: args.agent_id,
2321
+ delegationId: args.delegation_id,
2322
+ spendLimit: actualSpendLimit,
2323
+ approvedMerchants: [], // Empty = all merchants allowed
2324
+ }),
2325
+ spentAmount: sessionDel?.spentAmount ?? 0,
2326
+ };
2319
2327
  const result = commercePreflight({
2320
2328
  signedPassport: agent.passport,
2321
2329
  delegation: commerceDel,
@@ -2339,11 +2347,18 @@ server.tool("get_commerce_spend", "Get spend analytics for a commerce delegation
2339
2347
  delegation_id: z.string().describe("Commerce delegation ID"),
2340
2348
  spend_limit: z.number().describe("Total allowed spend"),
2341
2349
  }, async (args) => {
2342
- const commerceDel = createCommerceDelegation({
2343
- agentId: args.agent_id,
2344
- delegationId: args.delegation_id,
2345
- spendLimit: args.spend_limit,
2346
- });
2350
+ // Report spend recorded on the session delegation instead of always reporting 0. Same caveat
2351
+ // as commerce_preflight: nothing in the MCP yet increments sessionDel.spentAmount (metering
2352
+ // record path flagged for Tima), so this reflects whatever the session state holds.
2353
+ const sessionDel = state.delegations.get(args.delegation_id);
2354
+ const commerceDel = {
2355
+ ...createCommerceDelegation({
2356
+ agentId: args.agent_id,
2357
+ delegationId: args.delegation_id,
2358
+ spendLimit: args.spend_limit,
2359
+ }),
2360
+ spentAmount: sessionDel?.spentAmount ?? 0,
2361
+ };
2347
2362
  const summary = getSpendSummary(commerceDel);
2348
2363
  return {
2349
2364
  content: [{
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "agent-passport-system-mcp",
3
- "version": "3.2.2",
3
+ "version": "3.2.3",
4
4
  "mcpName": "io.github.aeoess/agent-passport-mcp",
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.6.0 (Wave 1 accountability primitives + Phase 4.1 payment rails shipped in SDK; MCP rail-tool exposure deferred). For gateway-runtime tools (ProxyGateway, AgentContext, DataEnforcementGate), use gateway.aeoess.com REST API or pin to v3.1.x.",
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.9.0 (Wave 1 accountability primitives + Phase 4.1 payment rails shipped in SDK; MCP rail-tool exposure deferred). For gateway-runtime tools (ProxyGateway, AgentContext, DataEnforcementGate), use gateway.aeoess.com REST API or pin to v3.1.x.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "agent-passport-system-mcp": "./build/bin.js",