@agen-ai/acp-registry 0.0.0-stage → 0.4.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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -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 +254 -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 +187 -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,375 @@
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 source SDK version is `0.4.1` and remains unpublished; the published
5
+ release is `0.4.0`. The package is ESM, MIT licensed and side-effect-free on import.
6
+ The metadata graph depends only on `@agen-ai/validation` and exact `zod@4.4.3`. Explicit Node
7
+ installation uses pinned tar, ZIP and bzip2 readers. Public contracts are plain TypeScript values
8
+ and AgenAI validation issues, with no exposed Zod schemas or Agent Runtime dependency.
9
+
10
+ ## Boundaries
11
+
12
+ The root parses inert metadata and creates plans. It never fetches, writes files, installs packages,
13
+ spawns processes, chooses credentials or activates an agent. `/node` provides explicit bounded
14
+ fetching and installation into an explicitly opened owned store. Provider composition, credentials
15
+ and process lifetime remain caller-owned.
16
+
17
+ Catalog labels, licenses, launch metadata and `bundled` flags grant no authority. Digests identify
18
+ inputs; they do not authenticate their publisher. Use an independently trusted source and expected
19
+ artifact evidence appropriate to your host. A plan's `verified: false` never changes. A separate
20
+ installation receipt records verification of its declared obligations against actual artifacts and
21
+ toolchains. This source preparation does not publish the package or authorize a host deployment.
22
+
23
+ ## Parse, search and plan
24
+
25
+ ```ts
26
+ import {
27
+ createAcpInstallPlan,
28
+ parseAcpRegistrySnapshot,
29
+ searchAcpRegistry,
30
+ } from '@agen-ai/acp-registry';
31
+
32
+ const snapshot = parseAcpRegistrySnapshot({
33
+ source: {
34
+ namespace: 'my-company',
35
+ kind: 'local',
36
+ channel: 'stable',
37
+ location: 'approved-catalog-2026-10-09',
38
+ profile: 'published-v1',
39
+ },
40
+ content: catalogJson, // string or Uint8Array supplied by the caller
41
+ });
42
+ const [entry] = searchAcpRegistry(snapshot, {
43
+ query: 'coding',
44
+ target: 'linux-x86_64',
45
+ distributionKinds: ['binary'],
46
+ });
47
+ if (!entry) throw new Error('No approved binary distribution matches.');
48
+
49
+ const plan = createAcpInstallPlan(snapshot, {
50
+ entryIdentity: entry.identity,
51
+ expectedSnapshotDigest: snapshot.digest,
52
+ expectedEntryDigest: entry.digest,
53
+ target: 'linux-x86_64',
54
+ distributionPreference: ['binary'],
55
+ policy: {
56
+ network: 'offline',
57
+ allowedOrigins: [],
58
+ allowInsecureHttp: false,
59
+ packageScripts: 'deny',
60
+ integrity: 'require-expected-sha256',
61
+ reproducibility: 'require-complete',
62
+ allowedEnvironmentKeys: [],
63
+ },
64
+ });
65
+ ```
66
+
67
+ This example requires a matching binary with an expected SHA-256 and no catalog environment keys.
68
+ Missing evidence or a conflicting caller pin throws `AcpRegistryValidationError`; it never silently
69
+ weakens policy. For a binary without catalog integrity, supply `binaryIntegrity` with an independently
70
+ expected digest and the digest of its resolution evidence. Alternatively, explicitly select weaker
71
+ integrity/reproducibility policy and inspect the plan's recorded gaps.
72
+
73
+ Search matches all whitespace-separated query tokens case-insensitively against ID, name, description
74
+ and authors, then sorts by source-qualified identity. The explicit distribution preference is ranked;
75
+ selection never substitutes another architecture or a distribution outside that list. The six target
76
+ identifiers are `darwin`, `linux` or `windows` followed by `-aarch64` or `-x86_64`. Unsupported Windows
77
+ `.cmd`/`.bat` launches fail explicitly; no implicit shell interpreter is introduced.
78
+
79
+ Snapshots, entries, search results, distributions and plans are deeply frozen. Serializable values
80
+ are structurally revalidated and their digests recomputed before lookup or planning, so JSON round
81
+ trips remain usable without a hidden object-identity registry. The host must retain its expected
82
+ snapshot and entry pins; recomputing a digest does not establish trust in a modified object.
83
+
84
+ ## Identity and the accepted profile
85
+
86
+ Source identity includes the caller-selected namespace, local/remote kind, stable/preview channel,
87
+ canonical location and profile. Agent IDs are unique within that source. Changing the source location
88
+ or channel produces a different identity. Redirect destinations never replace the selected source.
89
+ `rawDigest` is SHA-256 over exact accepted UTF-8 bytes, including whitespace and any UTF-8 BOM.
90
+ `snapshot.digest` binds the normalized source, entries and raw digest; `entry.digest` binds the
91
+ normalized entry. Public digests use `sha256:` followed by lowercase hexadecimal. Upstream binary
92
+ `sha256` fields remain normalized bare hexadecimal in distribution metadata.
93
+
94
+ The `published-v1` profile is grounded in upstream registry commit
95
+ [`7f9be29696108572a2bca70e0563af528073a0c3`](https://github.com/agentclientprotocol/registry/commit/7f9be29696108572a2bca70e0563af528073a0c3),
96
+ its [format](https://github.com/agentclientprotocol/registry/blob/7f9be29696108572a2bca70e0563af528073a0c3/FORMAT.md),
97
+ [agent schema](https://github.com/agentclientprotocol/registry/blob/7f9be29696108572a2bca70e0563af528073a0c3/agent.schema.json),
98
+ and [builder](https://github.com/agentclientprotocol/registry/blob/7f9be29696108572a2bca70e0563af528073a0c3/.github/workflows/build_registry.py).
99
+ It accepts the complete published `{ version: '1.0.0', agents, extensions?: [] }` envelope. The builder
100
+ emits an empty `extensions` array even though that commit's registry JSON Schema omits the field.
101
+ Only the empty form is accepted; extension installation and source-manifest preview overlays are
102
+ unsupported. Stable and preview are separate complete catalogs, selected by the caller.
103
+
104
+ The committed catalog fixtures are derived from pinned source entries and the builder's output
105
+ shape. They are not downloaded CDN publication bytes and do not establish the freshness or
106
+ authenticity of a live registry response. Retain and approve each actual source snapshot separately.
107
+
108
+ Agent release versions are opaque dotted numeric identities, including leading-zero date releases
109
+ such as `2026.10.01` and preview suffixes such as `1.2.3-preview.1`. They never resolve a package version.
110
+ Unknown fields, unknown distributions, duplicate agent identities or JSON keys, unsafe paths, NUL,
111
+ invalid UTF-8, unpaired surrogates and malformed URLs are rejected. URLs and argument strings remain
112
+ inert metadata; no shell expansion occurs. Binary paths become contained POSIX-relative paths,
113
+ including normalization of Windows separators. The parser accepts clean HTTP metadata, while all
114
+ network operations require their own explicit transport policy.
115
+
116
+ Default limits are 8 MiB of input, 1,024 entries, depth 16, 200,000 JSON values, 16 KiB per string,
117
+ 128 launch arguments and 128 environment entries. `DEFAULT_ACP_REGISTRY_LIMITS` exposes these values;
118
+ `limits` can adjust them only within finite package ceilings. Parsing fails as a whole, without
119
+ returning a partially accepted catalog. Validation failures carry immutable `issues` with code,
120
+ path and message.
121
+
122
+ ## Package resolution and reproducibility
123
+
124
+ `npx` and `uvx` describe catalog distribution kinds, not instructions to run download-on-demand
125
+ launchers. Planning requires an explicit `packageResolution` with the normalized package name,
126
+ exact version and package-owned bin or console-script command. An unversioned name or named tag
127
+ also requires caller-supplied resolution evidence. Exact catalog pins must match the supplied version.
128
+ Supported range syntax remains searchable metadata, but planning rejects it explicitly until range
129
+ resolution semantics are qualified; a digest alone does not prove that a version satisfies a range.
130
+ The command may differ from the package name. Aliases, local paths and VCS specifiers are unsupported.
131
+ No package registry is contacted by planning.
132
+
133
+ Package plans require exact Node/npm or Python/uv toolchain versions and measured executable digests,
134
+ without embedding machine-specific toolchain paths. Strict package reproducibility requires expected
135
+ artifact integrity, disabled package scripts and a supplied dependency lock. Supported lock input
136
+ profiles are a bounded npm package-lock v3 subset and hashed Python requirements. Both require a
137
+ single declared dependency closure containing the selected package, exact versions, target artifact
138
+ SHA-256 values and a digest of the complete lock document. These are locked inputs with pending
139
+ installer verification, not proof that a resolver or publisher is trustworthy. A top-level version
140
+ alone records a missing dependency closure; enabling scripts records another gap.
141
+
142
+ The npm profile requires one exact root dependency, contained `node_modules` paths, canonical
143
+ HTTP(S) artifact URLs and strong SHA-256/384/512 SRI. Links, bundled, dev, optional, platform-conditional
144
+ and peer dependency metadata require additional qualification and are rejected. Declared dependency
145
+ names must agree with the lock. The Python profile accepts canonical `name==version` lines with one
146
+ target SHA-256 each, comments and line continuations; direct URLs, options, extras and markers are
147
+ unsupported. Python requirements do not encode dependency edges, so the supplied graph remains
148
+ caller evidence until an installer compares actual package metadata and verifies closure. The same
149
+ installed-artifact verification remains mandatory for npm.
150
+
151
+ Plans bind the source, accepted snapshot and entry, selected distribution, target and preference,
152
+ literal launch arguments/environment, exact resolution and lock, toolchain identities, expected
153
+ integrity, mapping profile and explicit policy. Repeated equal normalized inputs yield the same plan
154
+ digest. `pinned-binary` describes the payload input only; it does not pin an operating system, shared
155
+ library set or all runtime behavior. Catalog environment variables are launch-only and require an
156
+ exact allowlist; process, shell, installer, proxy and runtime plumbing variables remain reserved.
157
+
158
+ ## Explicit fetching
159
+
160
+ ```ts
161
+ import { fetchAcpRegistrySnapshot } from '@agen-ai/acp-registry/node';
162
+
163
+ const fetched = await fetchAcpRegistrySnapshot({
164
+ source: {
165
+ namespace: 'approved-registry',
166
+ kind: 'remote',
167
+ channel: 'stable',
168
+ location: 'https://registry.example.test/catalog.json',
169
+ profile: 'published-v1',
170
+ },
171
+ trustedOrigins: ['https://registry.example.test'],
172
+ timeoutMs: 15_000,
173
+ maxRedirects: 3,
174
+ signal: abortController.signal,
175
+ });
176
+ ```
177
+
178
+ Fetching requires an exact canonical origin allowlist and validates the initial URL and every
179
+ redirect before contacting it. HTTP requires `allowInsecureHttp: true` in addition to the origin
180
+ allowlist; this also explicitly permits a trusted HTTPS-to-HTTP redirect. Credentials and cookies
181
+ are not forwarded. Origin trust is the caller's network policy, including any private or loopback
182
+ address it chooses; the package does not claim DNS or network sandbox enforcement.
183
+
184
+ The total deadline defaults to 15 seconds (maximum five minutes), redirects to three (maximum eight),
185
+ and the decoded response byte ceiling follows the catalog limits. Content-Length and streamed bytes
186
+ are checked, including decompressed responses. Only HTTP 200 is accepted. Cancellation, deadline,
187
+ origin, redirect, status and size failures produce `AcpRegistryFetchError` with a stable `code`;
188
+ invalid catalogs retain `AcpRegistryValidationError`. No response diagnostic body is included in
189
+ HTTP-status errors. `fetchedAt` is an observation time, not a freshness or authenticity guarantee.
190
+
191
+ The result includes the validated snapshot, exact accepted UTF-8 `content`, final URL and observation
192
+ time. Persist content and its source descriptor under caller policy if needed. Fetch failure never
193
+ chooses cached data, changes an installation or activates a provider. No cache directory is created.
194
+
195
+
196
+ ## Install, retain and launch
197
+
198
+ The following lifecycle sketch assumes an approved binary plan and independently retained input
199
+ pins. A production host must persist the installation identity/reference and own error recovery.
200
+
201
+ ```ts
202
+ import { parseAgentInstanceId, parseAgentProviderKey } from '@agen-ai/agent-protocol';
203
+ import { createAgentProviderRegistry } from '@agen-ai/agent-runtime';
204
+ import { openAcpInstallationStore } from '@agen-ai/acp-registry/node';
205
+ import { createNodeAcpProviderDriver } from '@agen-ai/provider-acp/node';
206
+
207
+ const store = await openAcpInstallationStore({ root: '/var/lib/my-host/acp-installations' });
208
+ const receipt = await store.install({
209
+ plan,
210
+ expectedPlanDigest: approvedPlanDigest,
211
+ localArtifacts: [{ path: '/approved-inputs/agent.tar.gz', sha256: approvedArtifactSha256 }],
212
+ signal: abortController.signal,
213
+ });
214
+ const identity = { planDigest: receipt.plan.digest, installationId: receipt.installationId };
215
+ const reference = await store.acquire({ ...identity, ownerId: 'session-123' });
216
+ // Durably persist the exact reference under the host's ownership before process launch.
217
+ const launch = await store.resolveLaunch(reference);
218
+ const providerKey = parseAgentProviderKey('installed-acp-agent');
219
+ const instanceId = parseAgentInstanceId('installed-agent:primary');
220
+ const driver = createNodeAcpProviderDriver({
221
+ providerKey,
222
+ launch: {
223
+ executable: launch.executable,
224
+ args: launch.args,
225
+ env: { HOME: '/var/lib/my-host/agent-home', PATH: '/usr/bin:/bin', ...launch.env },
226
+ cwd: '/work/project',
227
+ },
228
+ });
229
+ const registry = await createAgentProviderRegistry({
230
+ drivers: [driver],
231
+ definitions: [{ providerKey, instanceId, driverConfiguration: {} }],
232
+ });
233
+ try {
234
+ const instance = registry.requireInstance(instanceId);
235
+ // Open a session and consume startTurn().outputs, answering through the execution handle.
236
+ } finally {
237
+ await registry.dispose();
238
+ // A failed native cleanup throws above and deliberately leaves the reference retained.
239
+ await store.release(reference);
240
+ }
241
+ await store.remove(identity);
242
+ ```
243
+
244
+ Persist the exact reference before launching. Keep it until all processes using the installation
245
+ have stopped, including startup that fails before a registry is returned. Failed startup or cleanup
246
+ does not prove quiescence and must not trigger an unconditional reference release or store deletion.
247
+ Retry owned cleanup or establish quiescence through the host before releasing the exact reference.
248
+ Keep that installation reference throughout an execution's pending human requests and later output.
249
+ An independent `resolveRequest` delivery receipt does not complete the turn or release its native
250
+ process. Retain one `startTurn` execution/output reader until actual terminality and cleanup; route
251
+ human decisions through a separately owned handler. The public
252
+ [registry example](../../examples/registry-install-run.ts) composes the shared session helper, and
253
+ the [live-output example](../../examples/live-request-output.ts) isolates that runtime lifecycle.
254
+ The provider launcher supplies no sandbox, account credentials or filesystem/network authority.
255
+ The registry is also usable without the provider package when another explicit process owner
256
+ consumes the verified launch recipe.
257
+
258
+ The `0.4.1` session-interaction API extends the same dependency rule to decisions
259
+ outside a turn. Register the runtime handler before opening and retain the exact
260
+ installation reference while a decision is pending, after a local response receipt,
261
+ and through session close and callback/native cleanup. An `answered`, `expired`,
262
+ `session_closed` or uncertain terminal does not release the reference. An uncertain
263
+ choice must never be replayed as part of installation or process recovery.
264
+
265
+ The optional Node `grok-folder-trust-v1` mapping requires an explicitly supplied
266
+ `executionBoundary` before materialization. The registry verifies installation
267
+ identity and returns a launch recipe; it supplies no process containment or
268
+ session-interaction policy. Use the provider's
269
+ [execution ownership guidance](../provider-acp/README.md#reserved-execution-ownership)
270
+ and the maintained [Linux reference](../../examples/execution/linux/README.md)
271
+ when composing that selected profile. Its exact local native qualification does
272
+ not qualify all registry-installed agents or other platforms.
273
+
274
+ If registry construction fails before returning a registry, preserve the original
275
+ `AgentProviderRegistryError.cause`. A retained `NodeAcpExecutionCleanupError` there
276
+ keeps the startup cleanup callable; invoke its `retryCleanup()` and retain the
277
+ external host and installation reference until native shutdown and entered
278
+ callbacks have actually joined. For a failed session opening, retain the exact
279
+ `AgentProviderSessionCleanupError` associated with that input object; for a returned
280
+ session, retain its close owner. Releasing a reference or deleting the store cannot
281
+ repair incomplete provider cleanup, and serializing an error does not preserve
282
+ the callable owner.
283
+
284
+ Installation and launch require the current OS/architecture to match the plan. The caller supplies
285
+ an absent store directory under an existing owned parent, or reopens a valid owned store. Foreign
286
+ and substituted roots fail closed. The library creates a private stage, verifies every retained file,
287
+ directory and contained symlink, writes a receipt, synchronizes supported filesystems and publishes
288
+ with one same-filesystem rename. POSIX receipts record `synced-rename`; platforms without directory
289
+ synchronization record only `atomic-rename`. Native Windows/macOS qualification is separate from
290
+ Linux source fixtures. Archive paths, links, special files, case collisions, payload size, entry count,
291
+ path depth and decoder output are bounded. Supported binary formats are tar, tar.gz/tgz,
292
+ tar.bz2/tbz2, ZIP and raw executables; ambiguous URLs require `binaryFormat` in the plan. System
293
+ installer formats and implicit shell command expansion are unsupported.
294
+
295
+ Binary input can be an explicitly mapped local file or a bounded download under the plan's origin,
296
+ HTTP and redirect policy. Expected SHA-256 and measured SHA-256 remain separate receipt facts.
297
+ Permissive missing-integrity plans retain their gap after successful measurement. The receipt binds
298
+ the complete payload inventory, plan, launch recipe and verification evidence. Its content identity
299
+ excludes observation time and machine-specific toolchain paths; its receipt digest binds the complete
300
+ record. Hashes establish byte identity, not publisher authenticity or immunity to same-user tampering.
301
+ Use a host-owned store and trusted toolchain roots; the library does not create an OS sandbox.
302
+
303
+ `parseAcpInstallPlan(serializedValue, independentlyRetainedDigest)` validates retained plans without
304
+ I/O. Installation reuses this check, derives launch/policy requirements again and rejects a matching
305
+ self-computed digest whose fields contradict those requirements. `read(identity)` verifies the exact
306
+ receipt and complete current payload. For `synced-rename` receipts, reads also synchronize the
307
+ installation and its parent before returning, reestablishing persistence after an earlier rename
308
+ whose final synchronization may have failed. Reads never download repairs. `resolveLaunch(reference)` also
309
+ checks retained toolchain entry hashes and needs a live exact reference. No registry access, latest
310
+ resolution, `npx` or `uvx` invocation occurs during launch. Removal and publication serialize against
311
+ references; changing a plan creates a distinct installation and leaves older referenced payloads usable.
312
+
313
+ ## Explicit package toolchains and artifacts
314
+
315
+ Call `inspectAcpToolchains({ toolchains, workRoot })` with absolute locations for Node plus the npm CLI
316
+ JavaScript file, or Python plus uv. It runs bounded version probes with an explicit temporary home
317
+ and environment and measures the canonical executable entries. Use the resulting kind/version/SHA
318
+ identities in `createAcpInstallPlan`; provide the locations separately to `store.install`. An entry hash
319
+ does not attest an entire external Node/Python installation, standard library, npm dependency tree or
320
+ OS. Those roots remain caller-managed dependencies. Installation and retained launch reject changed
321
+ entry bytes instead of finding another executable on PATH.
322
+
323
+ npm receives verified local tarballs through a transformed private lock and runs real `npm ci` in
324
+ offline mode with lifecycle scripts, bin links, audit and funding disabled. The original logical lock
325
+ is retained. Actual manifests, dependency closure, version constraints and the root-owned JavaScript
326
+ bin are checked. Python receives verified wheels and runs real offline `uv pip install` into a retained
327
+ site-packages directory with hash requirements and no index, configuration, cache or source builds.
328
+ Actual metadata and the root-owned console script are checked without importing installed package
329
+ code. The launch bootstrap uses the exact Python executable and its retained payload path.
330
+
331
+ `localArtifacts` maps approved bytes by URL and/or SHA-256. `packageArtifacts` supplies explicit
332
+ name/version/URL/filename/SHA mappings where required, particularly Python wheel filenames and a
333
+ complete closure for plans without an original lock. Package managers never fetch missing artifacts.
334
+ A plan may explicitly permit the library to download approved artifacts before invoking the offline
335
+ manager. Newly observed closure does not remove a plan's missing-original-lock gap. Enabled scripts,
336
+ Python source distributions, peer/optional/platform-conditioned npm dependencies, and Python
337
+ markers/extras require additional profiles and fail explicitly here. Offline manager flags are not
338
+ network confinement; execution of arbitrary install scripts is deliberately unsupported.
339
+
340
+ ## Limits, failures and recovery
341
+
342
+ `DEFAULT_ACP_INSTALLATION_LIMITS` exposes finite configurable resource limits: 256 MiB per artifact,
343
+ 1 GiB total artifact and payload bytes, 2 GiB decoded archive bytes, 25,000 entries, depth 32,
344
+ 4,096-byte paths, three download redirects, two-minute artifact/tool deadlines, 1 MiB combined tool
345
+ output, 16 MiB receipts and 1,024 active references. Larger values remain bounded by package ceilings.
346
+ Errors use `AcpInstallationError` with stable `code`; malformed plans retain registry validation issues.
347
+ Registry and installation failures remain distinct from Agent Protocol V11 operational failures and
348
+ provider retry observations. This package has no runtime dependency or policy for retrying an agent
349
+ turn; retained installation integrity and process quiescence do not settle uncertain native execution.
350
+
351
+ Identical-plan operations use an exclusive cross-process lock and return `store_busy` when occupied.
352
+ 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
353
+ and waits for the direct child to exit before cleanup. Processes that deliberately escape that group
354
+ require caller-provided OS containment; Windows does not gain a process-tree sandbox from this API.
355
+ Failed publication never exposes a partial installation. Crash residue remains
356
+ inspectable through `inspectAttempt(planDigest)`. `recoverAttempt({ ...attempt, ownerQuiesced: true })`
357
+ requires the exact retained attempt and caller-established quiescence before cleaning owned residue.
358
+ Unknown or conflicting residue is retained. An active reference blocks removal until its exact owner
359
+ releases it; the library does not infer process lifetime or automatically expire references.
360
+
361
+ ## Real installation test prerequisites
362
+
363
+ The package tests execute real npm and uv against committed synthetic local tarballs and wheels.
364
+ They require Node/npm, Python and uv on the test host; missing tools fail with an explicit
365
+ prerequisite error. No registry credentials, real agent accounts or inference are involved. Linux
366
+ CI installs Python `3.14.4` and uv `0.11.28` through immutable action revisions before running these
367
+ tests. The standalone SDK CI runs them on Node 22 and 24; its release workflow uses the same
368
+ Python/uv prerequisites. These fixtures qualify the Linux test lane; they do not establish native
369
+ Windows or macOS installation qualification.
370
+
371
+ Run `pnpm --filter @agen-ai/acp-registry test` after installing those prerequisites. The fixture
372
+ discovery helper measures the actual tool entries and versions before constructing each plan;
373
+ production requires explicit caller-selected paths. `tests/fixtures/install-packages/generate.py`
374
+ recreates the small deterministic fixtures using only the Python standard library. Fixture
375
+ 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