@integraledger/agent-guard 0.10.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @integraledger/agent-guard
2
2
 
3
+ ## 0.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 5154cfb: Install one copy of the protocol packages, not two.
8
+
9
+ `lcp-mcp-server` pinned its `@integraledger/lcp-*` dependencies to an exact version while `agent-guard`
10
+ — which it depends on — declares the same packages as peers at a caret range. Those two declarations
11
+ cannot be satisfied by a single copy, so installing both packages resolved the protocol line **twice**:
12
+ once hoisted to satisfy the caret, and once nested under `lcp-mcp-server` to satisfy the exact pin. The
13
+ two halves of one install then read different protocol code, with `instanceof` failing across the seam
14
+ between them.
15
+
16
+ `lcp-mcp-server` now declares those dependencies as a caret at the minor line's zero patch, matching its
17
+ sibling. A tree holding both resolves a single copy, and `npx lcp-mcp` still installs standalone.
18
+
19
+ Nothing about either package's API, behaviour or guarantees changes. Both were correct in isolation; only
20
+ a tree holding both was affected.
21
+
22
+ - ccf4bb4: The runtime table is measured now, not argued.
23
+
24
+ It used to assert that this package works on Bun and Deno because of what the import graph implies — the
25
+ one Node built-in that arrives, `node:crypto`, is polyfilled on both. That was a sound argument and it was
26
+ only ever an argument; nothing ran.
27
+
28
+ Every release now drives the guard's whole decision on Node, Bun and Deno, against the packed tarball
29
+ installed the way you would install it, with the protocol line resolved from npmjs. Both halves are
30
+ asserted on each runtime, because a runtime where the guard refused everything would pass a check that only
31
+ looked for the halt: tampered terms must halt with the signer never reached, and matching terms must sign
32
+ with the signer reached exactly once.
33
+
34
+ Measured on this release: Node 24.19.0, Bun 1.4.0, Deno 2.9.5.
35
+
36
+ The table now separates what is measured from what is reasoned. Workers with `nodejs_compat` and the
37
+ bundler row remain arguments from the import graph and say so, rather than sitting in the same column as
38
+ the rows that ran.
39
+
3
40
  ## 0.10.0
4
41
 
5
42
  ### Minor Changes
package/README.md CHANGED
@@ -59,12 +59,20 @@ no DNS module at all. That much is enforced by the code and is the part worth re
59
59
  ours: `@integraledger/lcp-evidence` re-exports its CAR support from its index, which pulls `multiformats`'
60
60
  Node SHA-2 build, which statically imports `node:crypto`. So:
61
61
 
62
- | Target | Works |
63
- |---|---|
64
- | Node | yes |
65
- | Deno, Bun, Workers with `nodejs_compat` | yes`node:crypto` is polyfilled |
66
- | A bundler honouring `multiformats`' `browser` export condition | yesit maps to the browser SHA-2 build |
67
- | A plain unbundled ESM import in a browser or service worker | **no** |
62
+ | Target | Works | How we know |
63
+ |---|---|---|
64
+ | Node | yes | **measured** — every release runs the guard's whole decision here |
65
+ | Bun | yes | **measured**same run, same assertions |
66
+ | Deno | yes | **measured**same run, same assertions |
67
+ | Workers with `nodejs_compat` | yes `node:crypto` is polyfilled | reasoned from the import graph, not measured |
68
+ | A bundler honouring `multiformats`' `browser` export condition | yes — it maps to the browser SHA-2 build | reasoned from the export conditions, not measured |
69
+ | A plain unbundled ESM import in a browser or service worker | **no** | reasoned — `node:crypto` has nothing to resolve to |
70
+
71
+ The measured rows run against **the packed tarball**, installed the way you would install it, with the
72
+ protocol line resolved from npmjs — not against this repository's own tree. Both halves of the guarantee are
73
+ asserted on each runtime, because a runtime where the guard refused everything would pass a check that only
74
+ looked for the halt: tampered terms must halt with the signer never reached, and matching terms must sign
75
+ with the signer reached exactly once.
68
76
 
69
77
  The guard needs `fetch` and Web Crypto and nothing else of its own. Removing the last hop is an upstream
70
78
  change — a subpath export on `lcp-evidence` so importing one predicate does not drag CAR and `multiformats`
@@ -132,12 +140,20 @@ Four answers are worth stating plainly, because each is a place a friendlier lib
132
140
  ACP/UCP one is contingent on the document, while `ap2`'s rule is a strict subset of `a2a`'s, so **every**
133
141
  AP2 envelope matches both and none is reachable through `parseProposalUniversal`. AP2 is a detect-and-name
134
142
  protocol here; `parseProposalFromAp2Envelope` exists and is called by name.
135
- - **An unlocatable terms URL is reported, not answered.** `legalContextUrl` is a union — `read`,
136
- `no-field-declared`, `declared-field-empty`, or `undeclared-at-answering-carrier` because
137
- `PlacementManifest.termsUrlField` is singular and x402's names a path inside one of its two carriers. A
138
- §C.4-illustrated challenge advertising in `accepts[].extra` really does carry a terms URL, and a bare
139
- `undefined` there would contradict `parseProposalFromChallenge` reading the same bytes. The fix is a
140
- per-alias declaration on the manifest; until it lands the shortfall is named.
143
+ - **An absent terms URL says WHICH absence it is.** `legalContextUrl` is a union — `read`,
144
+ `no-field-declared`, or `declared-fields-empty` because a bare `undefined` conflates two different
145
+ facts. `no-field-declared` is a fact about the PROTOCOL: it has nowhere to put a locator, and no document
146
+ of that protocol can be faulted for lacking one. `declared-fields-empty` is a fact about THIS DOCUMENT:
147
+ the protocol has room and this seller left every declared slot empty. Reporting the second as the first
148
+ would blame a protocol for a seller's silence.
149
+
150
+ It used to carry a fourth state, `undeclared-at-answering-carrier`, and that state is gone because the
151
+ defect requiring it is fixed. The manifest's terms-URL member was singular, so x402 could declare only one
152
+ of its two slots: a §C.4-illustrated challenge advertising in `accepts[].extra` really does carry a terms
153
+ URL, while the single declared path sat empty inside `extensions` — and calling that "no terms advertised"
154
+ would have asserted a silence this reader could not see. The member is plural now, every declared slot is
155
+ read and reconciled, and a slot riding a container the placement owns is declared on that container. There
156
+ is no carrier a declaration fails to reach, so the state is unreachable rather than merely unused.
141
157
  - **Carrier disagreement refuses.** Where a protocol declares more than one carrier, all of them are read
142
158
  and compared. Two different hashes on one document would let a seller advertise different terms to
143
159
  different readers of it, so this is deliberately stricter than the placement adapter's own `extract`,
@@ -6,7 +6,7 @@ import type { GateProposal, ProposalContext } from "./proposal.js";
6
6
  * non-integer minor-unit amount.
7
7
  *
8
8
  * `legal_context_url` is required, and the ACP placement manifest DECLARES it as
9
- * `termsUrlField: "metadata.legal_context_url"` — the field this parser demands is a field the placement
9
+ * `termsUrlFields: ["metadata.legal_context_url"]` — the field this parser demands is a field the placement
10
10
  * names, which is what makes the round-trip compose. It is deliberately NOT read from ACP's native
11
11
  * `links[type=terms_of_use]`: that link is the merchant's standing policy page, while this names the ATR
12
12
  * terms document for this transaction, and falling back to it would substitute one for the other.
@@ -20,7 +20,7 @@ const AcpSessionSchema = z.object({
20
20
  * non-integer minor-unit amount.
21
21
  *
22
22
  * `legal_context_url` is required, and the ACP placement manifest DECLARES it as
23
- * `termsUrlField: "metadata.legal_context_url"` — the field this parser demands is a field the placement
23
+ * `termsUrlFields: ["metadata.legal_context_url"]` — the field this parser demands is a field the placement
24
24
  * names, which is what makes the round-trip compose. It is deliberately NOT read from ACP's native
25
25
  * `links[type=terms_of_use]`: that link is the merchant's standing policy page, while this names the ATR
26
26
  * terms document for this transaction, and falling back to it would substitute one for the other.
@@ -81,7 +81,7 @@ export interface Ap2ProposalContext extends ProposalContext {
81
81
  /**
82
82
  * The terms document's locator.
83
83
  *
84
- * AP2's placement is INTEGRITY-ONLY: `AP2_PLACEMENT` declares no `termsUrlField`, because the A2A
84
+ * AP2's placement is INTEGRITY-ONLY: `AP2_PLACEMENT` declares no `termsUrlFields`, because the A2A
85
85
  * `Message.metadata` map holds the reference and AP2 models no terms-URL field anywhere. So unlike ACP
86
86
  * (`metadata.legal_context_url`) and x402 (`extra.legalContextUrl`), the locator cannot come off the wire.
87
87
  * It comes from LCP §2 discovery — the seller's `/.well-known/legal-context.json` — resolved by the
@@ -24,7 +24,7 @@ import type { GateProposal, ProposalContext } from "./proposal.js";
24
24
  * restating its regex keeps one definition of what an ATR hash is.
25
25
  *
26
26
  * **`legalContextUrl` is the field the placement names.** `placement-mpp` declares
27
- * `termsUrlField: "methodDetails.legalContextUrl"`, so the field this parser demands is the field the
27
+ * `termsUrlFields: ["methodDetails.legalContextUrl"]`, so the field this parser demands is the field the
28
28
  * manifest names — which is what makes the seller's write and this buyer's read compose. There is no
29
29
  * fallback to any other member: MPP defines none that means "these terms", and inventing one would gate the
30
30
  * buyer against a document nobody pointed at.
@@ -53,7 +53,7 @@ const BASE_UNIT_INT = /^[0-9]+$/;
53
53
  * restating its regex keeps one definition of what an ATR hash is.
54
54
  *
55
55
  * **`legalContextUrl` is the field the placement names.** `placement-mpp` declares
56
- * `termsUrlField: "methodDetails.legalContextUrl"`, so the field this parser demands is the field the
56
+ * `termsUrlFields: ["methodDetails.legalContextUrl"]`, so the field this parser demands is the field the
57
57
  * manifest names — which is what makes the seller's write and this buyer's read compose. There is no
58
58
  * fallback to any other member: MPP defines none that means "these terms", and inventing one would gate the
59
59
  * buyer against a document nobody pointed at.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integraledger/agent-guard",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -39,17 +39,18 @@
39
39
  "@integraledger/lcp-verify": "^0.12.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@integraledger/lcp-authority": "0.12.0",
43
- "@integraledger/lcp-binding-core": "0.12.0",
44
- "@integraledger/lcp-discovery": "0.12.0",
45
- "@integraledger/lcp-evidence": "0.12.0",
46
- "@integraledger/lcp-kernel": "0.12.0",
47
- "@integraledger/lcp-placement-ack": "0.12.0",
48
- "@integraledger/lcp-placement-acp": "0.12.0",
49
- "@integraledger/lcp-placement-ap2": "0.12.0",
50
- "@integraledger/lcp-placements": "0.12.0",
51
- "@integraledger/lcp-verify": "0.12.0",
42
+ "@integraledger/lcp-authority": "0.12.2",
43
+ "@integraledger/lcp-binding-core": "0.12.2",
44
+ "@integraledger/lcp-discovery": "0.12.2",
45
+ "@integraledger/lcp-evidence": "0.12.2",
46
+ "@integraledger/lcp-kernel": "0.12.2",
47
+ "@integraledger/lcp-placement-ack": "0.12.2",
48
+ "@integraledger/lcp-placement-acp": "0.12.2",
49
+ "@integraledger/lcp-placement-ap2": "0.12.2",
50
+ "@integraledger/lcp-placements": "0.12.2",
51
+ "@integraledger/lcp-verify": "0.12.2",
52
52
  "@types/node": "24.13.3",
53
+ "fast-check": "4.9.0",
53
54
  "vitest": "4.1.10"
54
55
  },
55
56
  "license": "Apache-2.0",
@@ -23,7 +23,7 @@ const AcpSessionSchema = z.object({
23
23
  * non-integer minor-unit amount.
24
24
  *
25
25
  * `legal_context_url` is required, and the ACP placement manifest DECLARES it as
26
- * `termsUrlField: "metadata.legal_context_url"` — the field this parser demands is a field the placement
26
+ * `termsUrlFields: ["metadata.legal_context_url"]` — the field this parser demands is a field the placement
27
27
  * names, which is what makes the round-trip compose. It is deliberately NOT read from ACP's native
28
28
  * `links[type=terms_of_use]`: that link is the merchant's standing policy page, while this names the ATR
29
29
  * terms document for this transaction, and falling back to it would substitute one for the other.
@@ -114,7 +114,7 @@ export interface Ap2ProposalContext extends ProposalContext {
114
114
  /**
115
115
  * The terms document's locator.
116
116
  *
117
- * AP2's placement is INTEGRITY-ONLY: `AP2_PLACEMENT` declares no `termsUrlField`, because the A2A
117
+ * AP2's placement is INTEGRITY-ONLY: `AP2_PLACEMENT` declares no `termsUrlFields`, because the A2A
118
118
  * `Message.metadata` map holds the reference and AP2 models no terms-URL field anywhere. So unlike ACP
119
119
  * (`metadata.legal_context_url`) and x402 (`extra.legalContextUrl`), the locator cannot come off the wire.
120
120
  * It comes from LCP §2 discovery — the seller's `/.well-known/legal-context.json` — resolved by the
@@ -57,7 +57,7 @@ const BASE_UNIT_INT = /^[0-9]+$/;
57
57
  * restating its regex keeps one definition of what an ATR hash is.
58
58
  *
59
59
  * **`legalContextUrl` is the field the placement names.** `placement-mpp` declares
60
- * `termsUrlField: "methodDetails.legalContextUrl"`, so the field this parser demands is the field the
60
+ * `termsUrlFields: ["methodDetails.legalContextUrl"]`, so the field this parser demands is the field the
61
61
  * manifest names — which is what makes the seller's write and this buyer's read compose. There is no
62
62
  * fallback to any other member: MPP defines none that means "these terms", and inventing one would gate the
63
63
  * buyer against a document nobody pointed at.