@agen-ai/acp-registry 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +347 -2
  3. package/dist/catalog/assert.d.ts +6 -0
  4. package/dist/catalog/assert.js +46 -0
  5. package/dist/catalog/contracts.d.ts +70 -0
  6. package/dist/catalog/contracts.js +0 -0
  7. package/dist/catalog/launch.d.ts +4 -0
  8. package/dist/catalog/launch.js +19 -0
  9. package/dist/catalog/limits.d.ts +5 -0
  10. package/dist/catalog/limits.js +40 -0
  11. package/dist/catalog/normalize.d.ts +9 -0
  12. package/dist/catalog/normalize.js +82 -0
  13. package/dist/catalog/parse.d.ts +3 -0
  14. package/dist/catalog/parse.js +46 -0
  15. package/dist/catalog/schemas.d.ts +215 -0
  16. package/dist/catalog/schemas.js +62 -0
  17. package/dist/catalog/source.d.ts +19 -0
  18. package/dist/catalog/source.js +46 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +16 -0
  21. package/dist/node/contracts.d.ts +30 -0
  22. package/dist/node/contracts.js +13 -0
  23. package/dist/node/fetch-policy.d.ts +11 -0
  24. package/dist/node/fetch-policy.js +64 -0
  25. package/dist/node/fetch.d.ts +4 -0
  26. package/dist/node/fetch.js +74 -0
  27. package/dist/node/index.d.ts +11 -0
  28. package/dist/node/index.js +14 -0
  29. package/dist/node/install/artifacts/acquire.d.ts +14 -0
  30. package/dist/node/install/artifacts/acquire.js +191 -0
  31. package/dist/node/install/binary/archive.d.ts +18 -0
  32. package/dist/node/install/binary/archive.js +256 -0
  33. package/dist/node/install/binary/paths.d.ts +6 -0
  34. package/dist/node/install/binary/paths.js +37 -0
  35. package/dist/node/install/binary/prepare.d.ts +4 -0
  36. package/dist/node/install/binary/prepare.js +66 -0
  37. package/dist/node/install/binary/sink.d.ts +37 -0
  38. package/dist/node/install/binary/sink.js +188 -0
  39. package/dist/node/install/contracts.d.ts +163 -0
  40. package/dist/node/install/contracts.js +0 -0
  41. package/dist/node/install/errors.d.ts +6 -0
  42. package/dist/node/install/errors.js +11 -0
  43. package/dist/node/install/index.d.ts +4 -0
  44. package/dist/node/install/index.js +86 -0
  45. package/dist/node/install/input.d.ts +23 -0
  46. package/dist/node/install/input.js +52 -0
  47. package/dist/node/install/internal.d.ts +33 -0
  48. package/dist/node/install/internal.js +0 -0
  49. package/dist/node/install/limits.d.ts +4 -0
  50. package/dist/node/install/limits.js +50 -0
  51. package/dist/node/install/packages/context.d.ts +14 -0
  52. package/dist/node/install/packages/context.js +111 -0
  53. package/dist/node/install/packages/metadata.d.ts +14 -0
  54. package/dist/node/install/packages/metadata.js +149 -0
  55. package/dist/node/install/packages/npm.d.ts +4 -0
  56. package/dist/node/install/packages/npm.js +109 -0
  57. package/dist/node/install/packages/python-scripts.d.ts +4 -0
  58. package/dist/node/install/packages/python-scripts.js +24 -0
  59. package/dist/node/install/packages/sources.d.ts +8 -0
  60. package/dist/node/install/packages/sources.js +56 -0
  61. package/dist/node/install/packages/uv.d.ts +4 -0
  62. package/dist/node/install/packages/uv.js +127 -0
  63. package/dist/node/install/store/errors.d.ts +6 -0
  64. package/dist/node/install/store/errors.js +25 -0
  65. package/dist/node/install/store/filesystem.d.ts +22 -0
  66. package/dist/node/install/store/filesystem.js +187 -0
  67. package/dist/node/install/store/index.d.ts +35 -0
  68. package/dist/node/install/store/index.js +179 -0
  69. package/dist/node/install/store/lock.d.ts +14 -0
  70. package/dist/node/install/store/lock.js +100 -0
  71. package/dist/node/install/store/payload.d.ts +14 -0
  72. package/dist/node/install/store/payload.js +167 -0
  73. package/dist/node/install/store/receipt.d.ts +7 -0
  74. package/dist/node/install/store/receipt.js +175 -0
  75. package/dist/node/install/store/references.d.ts +9 -0
  76. package/dist/node/install/store/references.js +105 -0
  77. package/dist/node/install/store/stage.d.ts +13 -0
  78. package/dist/node/install/store/stage.js +66 -0
  79. package/dist/node/install/tools/inspect.d.ts +12 -0
  80. package/dist/node/install/tools/inspect.js +105 -0
  81. package/dist/node/install/tools/run.d.ts +4 -0
  82. package/dist/node/install/tools/run.js +66 -0
  83. package/dist/node/response.d.ts +3 -0
  84. package/dist/node/response.js +41 -0
  85. package/dist/planning/binary.d.ts +3 -0
  86. package/dist/planning/binary.js +20 -0
  87. package/dist/planning/contracts.d.ts +107 -0
  88. package/dist/planning/contracts.js +0 -0
  89. package/dist/planning/create.d.ts +4 -0
  90. package/dist/planning/create.js +33 -0
  91. package/dist/planning/derive.d.ts +6 -0
  92. package/dist/planning/derive.js +68 -0
  93. package/dist/planning/lock.d.ts +10 -0
  94. package/dist/planning/lock.js +120 -0
  95. package/dist/planning/package.d.ts +6 -0
  96. package/dist/planning/package.js +81 -0
  97. package/dist/planning/parse.d.ts +4 -0
  98. package/dist/planning/parse.js +91 -0
  99. package/dist/planning/policy.d.ts +6 -0
  100. package/dist/planning/policy.js +72 -0
  101. package/dist/planning/specifier.d.ts +4 -0
  102. package/dist/planning/specifier.js +24 -0
  103. package/dist/selection/distribution.d.ts +11 -0
  104. package/dist/selection/distribution.js +43 -0
  105. package/dist/selection/input.d.ts +21 -0
  106. package/dist/selection/input.js +54 -0
  107. package/dist/selection/search.d.ts +9 -0
  108. package/dist/selection/search.js +29 -0
  109. package/dist/shared/bounds.d.ts +7 -0
  110. package/dist/shared/bounds.js +149 -0
  111. package/dist/shared/digest.d.ts +3 -0
  112. package/dist/shared/digest.js +20 -0
  113. package/dist/shared/errors.d.ts +9 -0
  114. package/dist/shared/errors.js +21 -0
  115. package/dist/shared/immutable.d.ts +2 -0
  116. package/dist/shared/immutable.js +14 -0
  117. package/package.json +62 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Trevor Nichols
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,348 @@
1
- # Temporary Holding Version
1
+ # @agen-ai/acp-registry
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Optional ACP catalog validation, search, immutable planning and verified installation for Node.js 22 or newer.
4
+ The coordinated SDK version is `0.4.0`. The package is ESM, MIT licensed and side-effect-free on import.
5
+ The metadata graph depends only on `@agen-ai/validation` and exact `zod@4.4.3`. Explicit Node
6
+ installation uses pinned tar, ZIP and bzip2 readers. Public contracts are plain TypeScript values
7
+ and AgenAI validation issues, with no exposed Zod schemas or Agent Runtime dependency.
8
+
9
+ ## Boundaries
10
+
11
+ The root parses inert metadata and creates plans. It never fetches, writes files, installs packages,
12
+ spawns processes, chooses credentials or activates an agent. `/node` provides explicit bounded
13
+ fetching and installation into an explicitly opened owned store. Provider composition, credentials
14
+ and process lifetime remain caller-owned.
15
+
16
+ Catalog labels, licenses, launch metadata and `bundled` flags grant no authority. Digests identify
17
+ inputs; they do not authenticate their publisher. Use an independently trusted source and expected
18
+ artifact evidence appropriate to your host. A plan's `verified: false` never changes. A separate
19
+ installation receipt records verification of its declared obligations against actual artifacts and
20
+ toolchains. This source preparation does not publish the package or authorize a host deployment.
21
+
22
+ ## Parse, search and plan
23
+
24
+ ```ts
25
+ import {
26
+ createAcpInstallPlan,
27
+ parseAcpRegistrySnapshot,
28
+ searchAcpRegistry,
29
+ } from '@agen-ai/acp-registry';
30
+
31
+ const snapshot = parseAcpRegistrySnapshot({
32
+ source: {
33
+ namespace: 'my-company',
34
+ kind: 'local',
35
+ channel: 'stable',
36
+ location: 'approved-catalog-2026-10-09',
37
+ profile: 'published-v1',
38
+ },
39
+ content: catalogJson, // string or Uint8Array supplied by the caller
40
+ });
41
+ const [entry] = searchAcpRegistry(snapshot, {
42
+ query: 'coding',
43
+ target: 'linux-x86_64',
44
+ distributionKinds: ['binary'],
45
+ });
46
+ if (!entry) throw new Error('No approved binary distribution matches.');
47
+
48
+ const plan = createAcpInstallPlan(snapshot, {
49
+ entryIdentity: entry.identity,
50
+ expectedSnapshotDigest: snapshot.digest,
51
+ expectedEntryDigest: entry.digest,
52
+ target: 'linux-x86_64',
53
+ distributionPreference: ['binary'],
54
+ policy: {
55
+ network: 'offline',
56
+ allowedOrigins: [],
57
+ allowInsecureHttp: false,
58
+ packageScripts: 'deny',
59
+ integrity: 'require-expected-sha256',
60
+ reproducibility: 'require-complete',
61
+ allowedEnvironmentKeys: [],
62
+ },
63
+ });
64
+ ```
65
+
66
+ This example requires a matching binary with an expected SHA-256 and no catalog environment keys.
67
+ Missing evidence or a conflicting caller pin throws `AcpRegistryValidationError`; it never silently
68
+ weakens policy. For a binary without catalog integrity, supply `binaryIntegrity` with an independently
69
+ expected digest and the digest of its resolution evidence. Alternatively, explicitly select weaker
70
+ integrity/reproducibility policy and inspect the plan's recorded gaps.
71
+
72
+ Search matches all whitespace-separated query tokens case-insensitively against ID, name, description
73
+ and authors, then sorts by source-qualified identity. The explicit distribution preference is ranked;
74
+ selection never substitutes another architecture or a distribution outside that list. The six target
75
+ identifiers are `darwin`, `linux` or `windows` followed by `-aarch64` or `-x86_64`. Unsupported Windows
76
+ `.cmd`/`.bat` launches fail explicitly; no implicit shell interpreter is introduced.
77
+
78
+ Snapshots, entries, search results, distributions and plans are deeply frozen. Serializable values
79
+ are structurally revalidated and their digests recomputed before lookup or planning, so JSON round
80
+ trips remain usable without a hidden object-identity registry. The host must retain its expected
81
+ snapshot and entry pins; recomputing a digest does not establish trust in a modified object.
82
+
83
+ ## Identity and the accepted profile
84
+
85
+ Source identity includes the caller-selected namespace, local/remote kind, stable/preview channel,
86
+ canonical location and profile. Agent IDs are unique within that source. Changing the source location
87
+ or channel produces a different identity. Redirect destinations never replace the selected source.
88
+ `rawDigest` is SHA-256 over exact accepted UTF-8 bytes, including whitespace and any UTF-8 BOM.
89
+ `snapshot.digest` binds the normalized source, entries and raw digest; `entry.digest` binds the
90
+ normalized entry. Public digests use `sha256:` followed by lowercase hexadecimal. Upstream binary
91
+ `sha256` fields remain normalized bare hexadecimal in distribution metadata.
92
+
93
+ The `published-v1` profile is grounded in upstream registry commit
94
+ [`7f9be29696108572a2bca70e0563af528073a0c3`](https://github.com/agentclientprotocol/registry/commit/7f9be29696108572a2bca70e0563af528073a0c3),
95
+ its [format](https://github.com/agentclientprotocol/registry/blob/7f9be29696108572a2bca70e0563af528073a0c3/FORMAT.md),
96
+ [agent schema](https://github.com/agentclientprotocol/registry/blob/7f9be29696108572a2bca70e0563af528073a0c3/agent.schema.json),
97
+ and [builder](https://github.com/agentclientprotocol/registry/blob/7f9be29696108572a2bca70e0563af528073a0c3/.github/workflows/build_registry.py).
98
+ It accepts the complete published `{ version: '1.0.0', agents, extensions?: [] }` envelope. The builder
99
+ emits an empty `extensions` array even though that commit's registry JSON Schema omits the field.
100
+ Only the empty form is accepted; extension installation and source-manifest preview overlays are
101
+ unsupported. Stable and preview are separate complete catalogs, selected by the caller.
102
+
103
+ The committed catalog fixtures are derived from pinned source entries and the builder's output
104
+ shape. They are not downloaded CDN publication bytes and do not establish the freshness or
105
+ authenticity of a live registry response. Retain and approve each actual source snapshot separately.
106
+
107
+ Agent release versions are opaque dotted numeric identities, including leading-zero date releases
108
+ such as `2026.10.01` and preview suffixes such as `1.2.3-preview.1`. They never resolve a package version.
109
+ Unknown fields, unknown distributions, duplicate agent identities or JSON keys, unsafe paths, NUL,
110
+ invalid UTF-8, unpaired surrogates and malformed URLs are rejected. URLs and argument strings remain
111
+ inert metadata; no shell expansion occurs. Binary paths become contained POSIX-relative paths,
112
+ including normalization of Windows separators. The parser accepts clean HTTP metadata, while all
113
+ network operations require their own explicit transport policy.
114
+
115
+ Default limits are 8 MiB of input, 1,024 entries, depth 16, 200,000 JSON values, 16 KiB per string,
116
+ 128 launch arguments and 128 environment entries. `DEFAULT_ACP_REGISTRY_LIMITS` exposes these values;
117
+ `limits` can adjust them only within finite package ceilings. Parsing fails as a whole, without
118
+ returning a partially accepted catalog. Validation failures carry immutable `issues` with code,
119
+ path and message.
120
+
121
+ ## Package resolution and reproducibility
122
+
123
+ `npx` and `uvx` describe catalog distribution kinds, not instructions to run download-on-demand
124
+ launchers. Planning requires an explicit `packageResolution` with the normalized package name,
125
+ exact version and package-owned bin or console-script command. An unversioned name or named tag
126
+ also requires caller-supplied resolution evidence. Exact catalog pins must match the supplied version.
127
+ Supported range syntax remains searchable metadata, but planning rejects it explicitly until range
128
+ resolution semantics are qualified; a digest alone does not prove that a version satisfies a range.
129
+ The command may differ from the package name. Aliases, local paths and VCS specifiers are unsupported.
130
+ No package registry is contacted by planning.
131
+
132
+ Package plans require exact Node/npm or Python/uv toolchain versions and measured executable digests,
133
+ without embedding machine-specific toolchain paths. Strict package reproducibility requires expected
134
+ artifact integrity, disabled package scripts and a supplied dependency lock. Supported lock input
135
+ profiles are a bounded npm package-lock v3 subset and hashed Python requirements. Both require a
136
+ single declared dependency closure containing the selected package, exact versions, target artifact
137
+ SHA-256 values and a digest of the complete lock document. These are locked inputs with pending
138
+ installer verification, not proof that a resolver or publisher is trustworthy. A top-level version
139
+ alone records a missing dependency closure; enabling scripts records another gap.
140
+
141
+ The npm profile requires one exact root dependency, contained `node_modules` paths, canonical
142
+ HTTP(S) artifact URLs and strong SHA-256/384/512 SRI. Links, bundled, dev, optional, platform-conditional
143
+ and peer dependency metadata require additional qualification and are rejected. Declared dependency
144
+ names must agree with the lock. The Python profile accepts canonical `name==version` lines with one
145
+ target SHA-256 each, comments and line continuations; direct URLs, options, extras and markers are
146
+ unsupported. Python requirements do not encode dependency edges, so the supplied graph remains
147
+ caller evidence until an installer compares actual package metadata and verifies closure. The same
148
+ installed-artifact verification remains mandatory for npm.
149
+
150
+ Plans bind the source, accepted snapshot and entry, selected distribution, target and preference,
151
+ literal launch arguments/environment, exact resolution and lock, toolchain identities, expected
152
+ integrity, mapping profile and explicit policy. Repeated equal normalized inputs yield the same plan
153
+ digest. `pinned-binary` describes the payload input only; it does not pin an operating system, shared
154
+ library set or all runtime behavior. Catalog environment variables are launch-only and require an
155
+ exact allowlist; process, shell, installer, proxy and runtime plumbing variables remain reserved.
156
+
157
+ ## Explicit fetching
158
+
159
+ ```ts
160
+ import { fetchAcpRegistrySnapshot } from '@agen-ai/acp-registry/node';
161
+
162
+ const fetched = await fetchAcpRegistrySnapshot({
163
+ source: {
164
+ namespace: 'approved-registry',
165
+ kind: 'remote',
166
+ channel: 'stable',
167
+ location: 'https://registry.example.test/catalog.json',
168
+ profile: 'published-v1',
169
+ },
170
+ trustedOrigins: ['https://registry.example.test'],
171
+ timeoutMs: 15_000,
172
+ maxRedirects: 3,
173
+ signal: abortController.signal,
174
+ });
175
+ ```
176
+
177
+ Fetching requires an exact canonical origin allowlist and validates the initial URL and every
178
+ redirect before contacting it. HTTP requires `allowInsecureHttp: true` in addition to the origin
179
+ allowlist; this also explicitly permits a trusted HTTPS-to-HTTP redirect. Credentials and cookies
180
+ are not forwarded. Origin trust is the caller's network policy, including any private or loopback
181
+ address it chooses; the package does not claim DNS or network sandbox enforcement.
182
+
183
+ The total deadline defaults to 15 seconds (maximum five minutes), redirects to three (maximum eight),
184
+ and the decoded response byte ceiling follows the catalog limits. Content-Length and streamed bytes
185
+ are checked, including decompressed responses. Only HTTP 200 is accepted. Cancellation, deadline,
186
+ origin, redirect, status and size failures produce `AcpRegistryFetchError` with a stable `code`;
187
+ invalid catalogs retain `AcpRegistryValidationError`. No response diagnostic body is included in
188
+ HTTP-status errors. `fetchedAt` is an observation time, not a freshness or authenticity guarantee.
189
+
190
+ The result includes the validated snapshot, exact accepted UTF-8 `content`, final URL and observation
191
+ time. Persist content and its source descriptor under caller policy if needed. Fetch failure never
192
+ chooses cached data, changes an installation or activates a provider. No cache directory is created.
193
+
194
+
195
+ ## Install, retain and launch
196
+
197
+ The following lifecycle sketch assumes an approved binary plan and independently retained input
198
+ pins. A production host must persist the installation identity/reference and own error recovery.
199
+
200
+ ```ts
201
+ import { parseAgentInstanceId, parseAgentProviderKey } from '@agen-ai/agent-protocol';
202
+ import { createAgentProviderRegistry } from '@agen-ai/agent-runtime';
203
+ import { openAcpInstallationStore } from '@agen-ai/acp-registry/node';
204
+ import { createNodeAcpProviderDriver } from '@agen-ai/provider-acp/node';
205
+
206
+ const store = await openAcpInstallationStore({ root: '/var/lib/my-host/acp-installations' });
207
+ const receipt = await store.install({
208
+ plan,
209
+ expectedPlanDigest: approvedPlanDigest,
210
+ localArtifacts: [{ path: '/approved-inputs/agent.tar.gz', sha256: approvedArtifactSha256 }],
211
+ signal: abortController.signal,
212
+ });
213
+ const identity = { planDigest: receipt.plan.digest, installationId: receipt.installationId };
214
+ const reference = await store.acquire({ ...identity, ownerId: 'session-123' });
215
+ // Durably persist the exact reference under the host's ownership before process launch.
216
+ const launch = await store.resolveLaunch(reference);
217
+ const providerKey = parseAgentProviderKey('installed-acp-agent');
218
+ const instanceId = parseAgentInstanceId('installed-agent:primary');
219
+ const driver = createNodeAcpProviderDriver({
220
+ providerKey,
221
+ launch: {
222
+ executable: launch.executable,
223
+ args: launch.args,
224
+ env: { HOME: '/var/lib/my-host/agent-home', PATH: '/usr/bin:/bin', ...launch.env },
225
+ cwd: '/work/project',
226
+ },
227
+ });
228
+ const registry = await createAgentProviderRegistry({
229
+ drivers: [driver],
230
+ definitions: [{ providerKey, instanceId, driverConfiguration: {} }],
231
+ });
232
+ try {
233
+ const instance = registry.requireInstance(instanceId);
234
+ // Open a session and consume startTurn().outputs, answering through the execution handle.
235
+ } finally {
236
+ await registry.dispose();
237
+ // A failed native cleanup throws above and deliberately leaves the reference retained.
238
+ await store.release(reference);
239
+ }
240
+ await store.remove(identity);
241
+ ```
242
+
243
+ Persist the exact reference before launching. Keep it until all processes using the installation
244
+ have stopped, including startup that fails before a registry is returned. Failed startup or cleanup
245
+ does not prove quiescence and must not trigger an unconditional reference release or store deletion.
246
+ Retry owned cleanup or establish quiescence through the host before releasing the exact reference.
247
+ Keep that installation reference throughout an execution's pending human requests and later output.
248
+ An independent `resolveRequest` delivery receipt does not complete the turn or release its native
249
+ process. Retain one `startTurn` execution/output reader until actual terminality and cleanup; route
250
+ human decisions through a separately owned handler. The public
251
+ [registry example](../../examples/registry-install-run.ts) composes the shared session helper, and
252
+ the [live-output example](../../examples/live-request-output.ts) isolates that runtime lifecycle.
253
+ The provider launcher supplies no sandbox, account credentials or filesystem/network authority.
254
+ The registry is also usable without the provider package when another explicit process owner
255
+ consumes the verified launch recipe.
256
+
257
+ Installation and launch require the current OS/architecture to match the plan. The caller supplies
258
+ an absent store directory under an existing owned parent, or reopens a valid owned store. Foreign
259
+ and substituted roots fail closed. The library creates a private stage, verifies every retained file,
260
+ directory and contained symlink, writes a receipt, synchronizes supported filesystems and publishes
261
+ with one same-filesystem rename. POSIX receipts record `synced-rename`; platforms without directory
262
+ synchronization record only `atomic-rename`. Native Windows/macOS qualification is separate from
263
+ Linux source fixtures. Archive paths, links, special files, case collisions, payload size, entry count,
264
+ path depth and decoder output are bounded. Supported binary formats are tar, tar.gz/tgz,
265
+ tar.bz2/tbz2, ZIP and raw executables; ambiguous URLs require `binaryFormat` in the plan. System
266
+ installer formats and implicit shell command expansion are unsupported.
267
+
268
+ Binary input can be an explicitly mapped local file or a bounded download under the plan's origin,
269
+ HTTP and redirect policy. Expected SHA-256 and measured SHA-256 remain separate receipt facts.
270
+ Permissive missing-integrity plans retain their gap after successful measurement. The receipt binds
271
+ the complete payload inventory, plan, launch recipe and verification evidence. Its content identity
272
+ excludes observation time and machine-specific toolchain paths; its receipt digest binds the complete
273
+ record. Hashes establish byte identity, not publisher authenticity or immunity to same-user tampering.
274
+ Use a host-owned store and trusted toolchain roots; the library does not create an OS sandbox.
275
+
276
+ `parseAcpInstallPlan(serializedValue, independentlyRetainedDigest)` validates retained plans without
277
+ I/O. Installation reuses this check, derives launch/policy requirements again and rejects a matching
278
+ self-computed digest whose fields contradict those requirements. `read(identity)` verifies the exact
279
+ receipt and complete current payload. For `synced-rename` receipts, reads also synchronize the
280
+ installation and its parent before returning, reestablishing persistence after an earlier rename
281
+ whose final synchronization may have failed. Reads never download repairs. `resolveLaunch(reference)` also
282
+ checks retained toolchain entry hashes and needs a live exact reference. No registry access, latest
283
+ resolution, `npx` or `uvx` invocation occurs during launch. Removal and publication serialize against
284
+ references; changing a plan creates a distinct installation and leaves older referenced payloads usable.
285
+
286
+ ## Explicit package toolchains and artifacts
287
+
288
+ Call `inspectAcpToolchains({ toolchains, workRoot })` with absolute locations for Node plus the npm CLI
289
+ JavaScript file, or Python plus uv. It runs bounded version probes with an explicit temporary home
290
+ and environment and measures the canonical executable entries. Use the resulting kind/version/SHA
291
+ identities in `createAcpInstallPlan`; provide the locations separately to `store.install`. An entry hash
292
+ does not attest an entire external Node/Python installation, standard library, npm dependency tree or
293
+ OS. Those roots remain caller-managed dependencies. Installation and retained launch reject changed
294
+ entry bytes instead of finding another executable on PATH.
295
+
296
+ npm receives verified local tarballs through a transformed private lock and runs real `npm ci` in
297
+ offline mode with lifecycle scripts, bin links, audit and funding disabled. The original logical lock
298
+ is retained. Actual manifests, dependency closure, version constraints and the root-owned JavaScript
299
+ bin are checked. Python receives verified wheels and runs real offline `uv pip install` into a retained
300
+ site-packages directory with hash requirements and no index, configuration, cache or source builds.
301
+ Actual metadata and the root-owned console script are checked without importing installed package
302
+ code. The launch bootstrap uses the exact Python executable and its retained payload path.
303
+
304
+ `localArtifacts` maps approved bytes by URL and/or SHA-256. `packageArtifacts` supplies explicit
305
+ name/version/URL/filename/SHA mappings where required, particularly Python wheel filenames and a
306
+ complete closure for plans without an original lock. Package managers never fetch missing artifacts.
307
+ A plan may explicitly permit the library to download approved artifacts before invoking the offline
308
+ manager. Newly observed closure does not remove a plan's missing-original-lock gap. Enabled scripts,
309
+ Python source distributions, peer/optional/platform-conditioned npm dependencies, and Python
310
+ markers/extras require additional profiles and fail explicitly here. Offline manager flags are not
311
+ network confinement; execution of arbitrary install scripts is deliberately unsupported.
312
+
313
+ ## Limits, failures and recovery
314
+
315
+ `DEFAULT_ACP_INSTALLATION_LIMITS` exposes finite configurable resource limits: 256 MiB per artifact,
316
+ 1 GiB total artifact and payload bytes, 2 GiB decoded archive bytes, 25,000 entries, depth 32,
317
+ 4,096-byte paths, three download redirects, two-minute artifact/tool deadlines, 1 MiB combined tool
318
+ output, 16 MiB receipts and 1,024 active references. Larger values remain bounded by package ceilings.
319
+ Errors use `AcpInstallationError` with stable `code`; malformed plans retain registry validation issues.
320
+ Registry and installation failures remain distinct from Agent Protocol V10 operational failures and
321
+ provider retry observations. This package has no runtime dependency or policy for retrying an agent
322
+ turn; retained installation integrity and process quiescence do not settle uncertain native execution.
323
+
324
+ Identical-plan operations use an exclusive cross-process lock and return `store_busy` when occupied.
325
+ They do not wait indefinitely or replace a lock based on age or PID. Cancellation terminates the directly owned tool and its POSIX process group, closes retained pipes
326
+ and waits for the direct child to exit before cleanup. Processes that deliberately escape that group
327
+ require caller-provided OS containment; Windows does not gain a process-tree sandbox from this API.
328
+ Failed publication never exposes a partial installation. Crash residue remains
329
+ inspectable through `inspectAttempt(planDigest)`. `recoverAttempt({ ...attempt, ownerQuiesced: true })`
330
+ requires the exact retained attempt and caller-established quiescence before cleaning owned residue.
331
+ Unknown or conflicting residue is retained. An active reference blocks removal until its exact owner
332
+ releases it; the library does not infer process lifetime or automatically expire references.
333
+
334
+ ## Real installation test prerequisites
335
+
336
+ The package tests execute real npm and uv against committed synthetic local tarballs and wheels.
337
+ They require Node/npm, Python and uv on the test host; missing tools fail with an explicit
338
+ prerequisite error. No registry credentials, real agent accounts or inference are involved. Linux
339
+ CI installs Python `3.14.4` and uv `0.11.28` through immutable action revisions before running these
340
+ tests. The standalone SDK CI runs them on Node 22 and 24; its release workflow uses the same
341
+ Python/uv prerequisites. These fixtures qualify the Linux test lane; they do not establish native
342
+ Windows or macOS installation qualification.
343
+
344
+ Run `pnpm --filter @agen-ai/acp-registry test` after installing those prerequisites. The fixture
345
+ discovery helper measures the actual tool entries and versions before constructing each plan;
346
+ production requires explicit caller-selected paths. `tests/fixtures/install-packages/generate.py`
347
+ recreates the small deterministic fixtures using only the Python standard library. Fixture
348
+ generation never imports package code or invokes package lifecycle scripts.
@@ -0,0 +1,6 @@
1
+ import type { AcpRegistryEntry, AcpRegistrySnapshot } from './contracts.js';
2
+ /** Verifies serializable normalized metadata and self-consistency; it does not authenticate its publisher. */
3
+ export declare function validateAcpRegistryEntry(input: unknown): AcpRegistryEntry;
4
+ /** A retained rawDigest is provenance evidence, never proof that these bytes came from a trusted publisher. */
5
+ export declare function validateAcpRegistrySnapshot(input: unknown): AcpRegistrySnapshot;
6
+ //# sourceMappingURL=assert.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { MAX_ACP_REGISTRY_LIMITS } from "./limits.js";
2
+ import { acpRegistryEntryToPublished, normalizeAcpRegistryEntry } from "./normalize.js";
3
+ import { acpRegistrySchemas } from "./schemas.js";
4
+ import { acpRegistrySourceIdentity, parseAcpRegistrySource } from "./source.js";
5
+ import { copyBoundedJson } from "../shared/bounds.js";
6
+ import { canonicalDigest } from "../shared/digest.js";
7
+ import { invalidRegistry, registrySchemaError } from "../shared/errors.js";
8
+ import { deepFreeze } from "../shared/immutable.js";
9
+ function validateAcpRegistryEntry(input) {
10
+ const captured = copyBoundedJson(input, MAX_ACP_REGISTRY_LIMITS);
11
+ const schemas = acpRegistrySchemas(MAX_ACP_REGISTRY_LIMITS);
12
+ const result = schemas.normalizedEntry.safeParse(captured);
13
+ if (!result.success) registrySchemaError(result.error, captured);
14
+ assertCanonicalEntry(result.data);
15
+ return deepFreeze(result.data);
16
+ }
17
+ function assertCanonicalEntry(entry) {
18
+ const sourceIdentity = entry.identity.slice(0, entry.identity.lastIndexOf("/"));
19
+ const normalized = normalizeAcpRegistryEntry(acpRegistryEntryToPublished(entry), sourceIdentity, "preview", 0);
20
+ if (canonicalDigest(normalized) !== canonicalDigest(entry)) invalidRegistry("entry_digest_mismatch", ["digest"], "Registry entry metadata, identity or digest is not canonical.");
21
+ }
22
+ function validateAcpRegistrySnapshot(input) {
23
+ const captured = copyBoundedJson(input, MAX_ACP_REGISTRY_LIMITS);
24
+ const result = acpRegistrySchemas(MAX_ACP_REGISTRY_LIMITS).normalizedSnapshot.safeParse(captured);
25
+ if (!result.success) registrySchemaError(result.error, captured);
26
+ const snapshot = result.data;
27
+ const source = parseAcpRegistrySource(snapshot.source);
28
+ if (snapshot.sourceIdentity !== acpRegistrySourceIdentity(source) || canonicalDigest(source) !== canonicalDigest(snapshot.source)) {
29
+ invalidRegistry("source_identity_mismatch", ["sourceIdentity"], "Registry source identity does not match its canonical descriptor.");
30
+ }
31
+ let previous = "";
32
+ for (const [index, entry] of snapshot.entries.entries()) {
33
+ assertCanonicalEntry(entry);
34
+ if (entry.id <= previous) invalidRegistry("duplicate_identity", ["entries", index, "id"], "Registry entries must have unique identities in canonical order.");
35
+ previous = entry.id;
36
+ if (entry.identity !== `${snapshot.sourceIdentity}/${entry.id}`) invalidRegistry("source_identity_mismatch", ["entries", index, "identity"], "Registry entry belongs to a different source.");
37
+ if (source.channel === "stable" && entry.version.includes("-")) invalidRegistry("channel_mismatch", ["entries", index, "version"], "Stable catalogs cannot contain preview release identities.");
38
+ }
39
+ const { digest, ...body } = snapshot;
40
+ if (canonicalDigest(body) !== digest) invalidRegistry("snapshot_digest_mismatch", ["digest"], "Registry normalized snapshot digest does not match its metadata.");
41
+ return deepFreeze(snapshot);
42
+ }
43
+ export {
44
+ validateAcpRegistryEntry,
45
+ validateAcpRegistrySnapshot
46
+ };
@@ -0,0 +1,70 @@
1
+ export type AcpRegistryTarget = 'darwin-aarch64' | 'darwin-x86_64' | 'linux-aarch64' | 'linux-x86_64' | 'windows-aarch64' | 'windows-x86_64';
2
+ export type AcpRegistryDistributionKind = 'binary' | 'npx' | 'uvx';
3
+ export interface AcpRegistrySource {
4
+ readonly namespace: string;
5
+ readonly kind: 'remote' | 'local';
6
+ readonly channel: 'stable' | 'preview';
7
+ readonly location: string;
8
+ readonly profile: 'published-v1';
9
+ }
10
+ interface AcpRegistryLaunchArguments {
11
+ readonly args: readonly string[];
12
+ readonly env: Readonly<Record<string, string>>;
13
+ }
14
+ export interface AcpRegistryBinaryDistribution extends AcpRegistryLaunchArguments {
15
+ readonly kind: 'binary';
16
+ readonly target: AcpRegistryTarget;
17
+ readonly archive: string;
18
+ readonly sha256?: string;
19
+ /** A contained POSIX-relative payload path, independent of the host platform. */
20
+ readonly executable: string;
21
+ }
22
+ export interface AcpRegistryPackageDistribution extends AcpRegistryLaunchArguments {
23
+ readonly kind: 'npx' | 'uvx';
24
+ /** Literal catalog metadata; an unversioned specifier has no immutable resolution. */
25
+ readonly package: string;
26
+ }
27
+ export type AcpRegistryDistribution = AcpRegistryBinaryDistribution | AcpRegistryPackageDistribution;
28
+ export interface AcpRegistryEntry {
29
+ readonly identity: string;
30
+ readonly id: string;
31
+ readonly name: string;
32
+ readonly version: string;
33
+ readonly description: string;
34
+ readonly repository?: string;
35
+ readonly website?: string;
36
+ readonly authors: readonly string[];
37
+ readonly license?: string;
38
+ readonly licenseUrl?: string;
39
+ readonly icon?: string;
40
+ /** Presentation metadata only; it grants no installation or execution authority. */
41
+ readonly bundled?: boolean;
42
+ readonly distributions: readonly AcpRegistryDistribution[];
43
+ readonly digest: string;
44
+ }
45
+ export interface AcpRegistrySnapshot {
46
+ readonly format: 'acp-registry-published-v1';
47
+ readonly source: AcpRegistrySource;
48
+ readonly sourceIdentity: string;
49
+ /** SHA-256 of the exact accepted UTF-8 input bytes. */
50
+ readonly rawDigest: string;
51
+ /** Canonical digest binding the normalized snapshot and its raw-byte provenance. */
52
+ readonly digest: string;
53
+ readonly entries: readonly AcpRegistryEntry[];
54
+ }
55
+ export interface AcpRegistryLimits {
56
+ readonly maxSnapshotBytes: number;
57
+ readonly maxEntries: number;
58
+ readonly maxDepth: number;
59
+ readonly maxValues: number;
60
+ readonly maxStringBytes: number;
61
+ readonly maxArgs: number;
62
+ readonly maxEnvEntries: number;
63
+ }
64
+ export interface ParseAcpRegistrySnapshotInput {
65
+ readonly source: AcpRegistrySource;
66
+ readonly content: string | Uint8Array;
67
+ readonly limits?: Partial<AcpRegistryLimits>;
68
+ }
69
+ export {};
70
+ //# sourceMappingURL=contracts.d.ts.map
File without changes
@@ -0,0 +1,4 @@
1
+ /** Both separators describe contained archive paths; they never choose a host shell or working directory. */
2
+ export declare function normalizeRegistryExecutable(input: string): string;
3
+ export declare function validateRegistryPackage(input: string, kind: 'npx' | 'uvx'): void;
4
+ //# sourceMappingURL=launch.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { invalidRegistry } from "../shared/errors.js";
2
+ function normalizeRegistryExecutable(input) {
3
+ const path = input.replaceAll("\\", "/");
4
+ if (!path || path.startsWith("/") || /[:\0\p{Cc}\p{Cf}]/u.test(path)) invalidRegistry("unsafe_executable", ["cmd"], "Registry commands must identify contained payload files.");
5
+ const segments = path.split("/");
6
+ if (segments.some((segment) => segment === ".." || segment === "" || /[. ]$/u.test(segment) && segment !== ".")) invalidRegistry("unsafe_executable", ["cmd"], "Registry payload paths cannot traverse, contain empty segments or ambiguous trailing dots/spaces.");
7
+ const normalized = segments.filter((segment) => segment !== ".").join("/");
8
+ if (!normalized) invalidRegistry("unsafe_executable", ["cmd"], "Registry commands must identify a payload file.");
9
+ return normalized;
10
+ }
11
+ function validateRegistryPackage(input, kind) {
12
+ const npm = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@(?:\*|[a-zA-Z0-9][a-zA-Z0-9.+_-]*|[\^~][0-9][a-zA-Z0-9.+_*-]*))?$/u;
13
+ const python = /^[a-zA-Z0-9][a-zA-Z0-9._-]*(?:@[a-zA-Z0-9][a-zA-Z0-9.+!_-]*|(?:(?:==|!=|>=|<=|~=|>|<)[a-zA-Z0-9][a-zA-Z0-9.+!*_-]*)(?:,(?:==|!=|>=|<=|~=|>|<)[a-zA-Z0-9][a-zA-Z0-9.+!*_-]*)*)?$/u;
14
+ if (!(kind === "npx" ? npm : python).test(input)) invalidRegistry("unsafe_package", ["package"], "Registry packages require literal registry names and supported version, tag or range selectors.");
15
+ }
16
+ export {
17
+ normalizeRegistryExecutable,
18
+ validateRegistryPackage
19
+ };
@@ -0,0 +1,5 @@
1
+ import type { AcpRegistryLimits } from './contracts.js';
2
+ export declare const DEFAULT_ACP_REGISTRY_LIMITS: AcpRegistryLimits;
3
+ export declare const MAX_ACP_REGISTRY_LIMITS: AcpRegistryLimits;
4
+ export declare function resolveAcpRegistryLimits(input?: Partial<AcpRegistryLimits>): AcpRegistryLimits;
5
+ //# sourceMappingURL=limits.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { invalidRegistry } from "../shared/errors.js";
2
+ const DEFAULT_ACP_REGISTRY_LIMITS = Object.freeze({
3
+ maxSnapshotBytes: 8 * 1024 * 1024,
4
+ maxEntries: 1024,
5
+ maxDepth: 16,
6
+ maxValues: 2e5,
7
+ maxStringBytes: 16 * 1024,
8
+ maxArgs: 128,
9
+ maxEnvEntries: 128
10
+ });
11
+ const MAX_ACP_REGISTRY_LIMITS = Object.freeze({
12
+ maxSnapshotBytes: 64 * 1024 * 1024,
13
+ maxEntries: 1e4,
14
+ maxDepth: 64,
15
+ maxValues: 1e6,
16
+ maxStringBytes: 1024 * 1024,
17
+ maxArgs: 1024,
18
+ maxEnvEntries: 1024
19
+ });
20
+ function resolveAcpRegistryLimits(input = {}) {
21
+ if (input === null || typeof input !== "object" || Object.getPrototypeOf(input) !== Object.prototype) invalidRegistry("invalid_limits", ["limits"], "Registry limits must be a plain object.");
22
+ const resolved = { ...DEFAULT_ACP_REGISTRY_LIMITS };
23
+ for (const key of Reflect.ownKeys(input)) {
24
+ if (typeof key !== "string" || !Object.hasOwn(MAX_ACP_REGISTRY_LIMITS, key)) invalidRegistry("invalid_limits", ["limits"], "Unknown registry limit.");
25
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
26
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) invalidRegistry("invalid_limits", ["limits", key], "Registry limits require enumerable data properties.");
27
+ const value = descriptor.value;
28
+ const minimum = key === "maxArgs" || key === "maxEnvEntries" ? 0 : 1;
29
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > MAX_ACP_REGISTRY_LIMITS[key]) {
30
+ invalidRegistry("invalid_limits", ["limits", key], "Registry limit is outside its supported finite range.");
31
+ }
32
+ resolved[key] = value;
33
+ }
34
+ return Object.freeze(resolved);
35
+ }
36
+ export {
37
+ DEFAULT_ACP_REGISTRY_LIMITS,
38
+ MAX_ACP_REGISTRY_LIMITS,
39
+ resolveAcpRegistryLimits
40
+ };
@@ -0,0 +1,9 @@
1
+ import type { z } from 'zod/v4';
2
+ import type { AcpRegistryEntry } from './contracts.js';
3
+ import { type acpRegistrySchemas } from './schemas.js';
4
+ type RawEntry = z.infer<ReturnType<typeof acpRegistrySchemas>['rawEntry']>;
5
+ export declare function normalizeAcpRegistryEntry(input: RawEntry, sourceIdentity: string, channel: 'stable' | 'preview', index: number): AcpRegistryEntry;
6
+ /** Reuses the published shape's closure for normalized snapshot admission without trusting retained digests. */
7
+ export declare function acpRegistryEntryToPublished(entry: AcpRegistryEntry): RawEntry;
8
+ export {};
9
+ //# sourceMappingURL=normalize.d.ts.map