agents-can-communicate 0.3.1 → 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 (161) hide show
  1. package/README.md +95 -52
  2. package/SECURITY.md +1 -1
  3. package/bin/acc-bootstrap.mjs +5 -54
  4. package/bin/acc-claude-channel.mjs +8 -175
  5. package/bin/acc-hook.mjs +4 -133
  6. package/bin/acc-mcp.mjs +4 -59
  7. package/bin/acc-update-worker.mjs +8 -0
  8. package/bin/acc.mjs +5 -39
  9. package/bin/entrypoints/acc-bootstrap.mjs +47 -0
  10. package/bin/entrypoints/acc-claude-channel.mjs +149 -0
  11. package/bin/entrypoints/acc-hook.mjs +58 -0
  12. package/bin/entrypoints/acc-mcp.mjs +70 -0
  13. package/bin/entrypoints/acc.mjs +51 -0
  14. package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
  15. package/bin/entrypoints/hook-output.mjs +77 -0
  16. package/docs/ADAPTER_AUTHORING.md +52 -8
  17. package/docs/ARCHITECTURE.md +64 -11
  18. package/docs/CAPABILITIES.md +41 -37
  19. package/docs/CLI.md +145 -25
  20. package/docs/CONCEPTS.md +13 -6
  21. package/docs/CONFIGURATION.md +37 -22
  22. package/docs/DESIGN_DECISIONS.md +16 -11
  23. package/docs/GETTING_STARTED.md +41 -7
  24. package/docs/GLOSSARY.md +9 -3
  25. package/docs/HOW_IT_WORKS.md +32 -11
  26. package/docs/MCP.md +91 -24
  27. package/docs/PROTOCOL.md +69 -12
  28. package/docs/RELEASING.md +28 -10
  29. package/docs/SECURITY_MODEL.md +47 -13
  30. package/docs/TROUBLESHOOTING.md +102 -20
  31. package/docs/UPGRADING.md +154 -0
  32. package/docs/WHY_ACC.md +5 -4
  33. package/docs/index.md +8 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +106 -5
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
  38. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
  41. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
  51. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
  52. package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
  53. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +106 -5
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
  57. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
  58. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
  59. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
  60. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
  61. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -2
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +106 -5
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
  66. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  67. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +122 -8
  68. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +5 -3
  69. package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +9 -0
  70. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
  71. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  72. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +106 -5
  73. package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
  74. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
  75. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
  80. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  81. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
  82. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
  83. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
  84. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
  85. package/node_modules/@agents-can-communicate/cli/package.json +4 -2
  86. package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
  87. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
  88. package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
  89. package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
  90. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +24 -5
  91. package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +64 -0
  94. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
  95. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
  96. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
  97. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +78 -0
  98. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
  99. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
  100. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
  101. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
  102. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
  103. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
  104. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
  105. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +123 -0
  106. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
  107. package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +1 -1
  108. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
  109. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
  110. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  111. package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
  112. package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
  113. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +9 -25
  114. package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
  115. package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
  116. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
  117. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
  118. package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
  119. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
  120. package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
  121. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
  122. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
  123. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
  124. package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
  125. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
  127. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
  128. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  129. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
  130. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
  131. package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
  132. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +30 -0
  133. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
  134. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +172 -67
  135. package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
  136. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  137. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
  138. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
  139. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
  140. package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
  141. package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
  142. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
  143. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
  144. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
  145. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  146. package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
  147. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
  148. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
  149. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
  150. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  151. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
  152. package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
  153. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
  154. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
  155. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  156. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
  157. package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
  158. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
  159. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
  160. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
  161. package/package.json +2 -1
@@ -13,7 +13,7 @@ import { parseArgs, positiveNumber } from "./args.mjs";
13
13
  // A usage error names what is missing rather than failing deeper in a service
14
14
  // with the argument already half-applied.
15
15
  const usage = message => new AccError(EXIT.USAGE, message);
16
- import { describeCommands, helpText } from "./help.mjs";
16
+ import { commandHelpText, describeCommand, describeCommands, helpText } from "./help.mjs";
17
17
  import { runUpdateCommand } from "./update-command.mjs";
18
18
  import { runConfigCommand } from "./config-command.mjs";
19
19
  import { runInstallCommand } from "./install-command.mjs";
@@ -73,23 +73,24 @@ async function locateContext(options, runtime) {
73
73
  gitProbe: runtime.gitProbe ?? createGitProbe(),
74
74
  explicitConfig: options.workspace,
75
75
  });
76
+ const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
77
+ env: runtime.env });
76
78
  const paths = runtimePaths({
77
- dataHome: runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
78
- env: runtime.env }),
79
+ dataHome,
79
80
  workspaceId: descriptor.id,
80
81
  workspaceRoots: descriptor.roots,
81
82
  });
82
- return { descriptor, paths };
83
+ return { descriptor, paths, dataHome };
83
84
  }
84
85
 
85
- async function withService({ descriptor, paths }, runtime) {
86
+ async function withService({ descriptor, paths, dataHome }, runtime) {
86
87
  const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
87
88
  ids: runtime.ids, workspaceId: descriptor.id });
88
89
  const service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
89
- return { descriptor, paths,
90
+ return { descriptor, paths, dataHome,
90
91
  service,
91
92
  deliveryRouter: typeof runtime.createDeliveryRouter === "function"
92
- ? runtime.createDeliveryRouter({ service, clock: runtime.clock }) : null };
93
+ ? runtime.createDeliveryRouter({ service, clock: runtime.clock, dataHome }) : null };
93
94
  }
94
95
 
95
96
  async function openContext(options, runtime) {
@@ -200,7 +201,9 @@ const HANDLERS = Object.freeze({
200
201
  sync: async ({ options, context }) => {
201
202
  const result = await context.service.sync({ sessionId: options.session,
202
203
  cursor: options.cursor ?? null, scope: options.scope,
204
+ messageId: options.message, kind: options.type, current: options.current,
203
205
  limit: options.limit ? positiveNumber(options.limit, "limit") : undefined });
206
+ if (result.scope === "history") return { data: result, text: JSON.stringify(result, null, 2) };
204
207
  // Solo zero-overhead: nothing to say means nothing printed, not a banner.
205
208
  const text = result.solo ? "" : `${result.attention.length} attention; `
206
209
  + `${result.roster.length} session(s); cursor ${result.cursor}`;
@@ -250,7 +253,9 @@ const HANDLERS = Object.freeze({
250
253
  context.service.sendMessage({ sessionId: options.session,
251
254
  generation: options.generation, clientMessageId: clientMessageId(options, context),
252
255
  toParticipantIds, kind,
253
- obligation: obligationFor(kind, options.obligation, toParticipantIds.length > 0),
256
+ supersedes: options.supersedes, withdraws: options.withdraws,
257
+ obligation: obligationFor(kind, options.obligation, toParticipantIds.length > 0
258
+ || options.supersedes !== undefined || options.withdraws !== undefined),
254
259
  subject: options.subject, body: options.body, descriptor: context.descriptor }) });
255
260
  const message = routed.recorded;
256
261
  return { data: { message, delivery: routed.delivery },
@@ -258,6 +263,15 @@ const HANDLERS = Object.freeze({
258
263
  },
259
264
 
260
265
  inbox: async ({ options, context }) => {
266
+ if (options.message === undefined) {
267
+ const page = await context.service.listInbox({ sessionId: options.session,
268
+ generation: options.generation, cursor: options.cursor,
269
+ limit: options.limit === undefined ? undefined : positiveNumber(options.limit, "limit") });
270
+ return { data: page, text: JSON.stringify(page, null, 2) };
271
+ }
272
+ if (options.cursor !== undefined || options.limit !== undefined) {
273
+ throw usage("an exact inbox read cannot use --cursor or --limit");
274
+ }
261
275
  const messages = await context.service.readInbox({ sessionId: options.session,
262
276
  generation: options.generation, messageId: options.message });
263
277
  return { data: messages, text: messages.length === 0
@@ -271,8 +285,8 @@ const HANDLERS = Object.freeze({
271
285
  generation: options.generation, messageId: options.message, body: options.body,
272
286
  subject: options.subject, clientMessageId: clientMessageId(options, context) }) });
273
287
  const result = routed.recorded;
274
- return { data: { message: result.reply, delivery: routed.delivery },
275
- text: recordedText(result.reply, routed.delivery) };
288
+ return { data: { message: result.reply, receipt: result.receipt, delivery: routed.delivery },
289
+ text: `${recordedText(result.reply, routed.delivery)}; acknowledged ${result.receipt.messageId}` };
276
290
  },
277
291
 
278
292
  request: async ({ options, context }) => {
@@ -346,7 +360,7 @@ const HANDLERS = Object.freeze({
346
360
  ids: runtime.ids });
347
361
  if (result.subcommand === "validate") {
348
362
  return { data: result, text: result.present
349
- ? `${result.file} is valid` : `no ${CONFIG_FILENAME}; defaults apply` };
363
+ ? `${result.file} is valid` : `no ${CONFIG_FILENAME} in selected directory; ancestors not checked` };
350
364
  }
351
365
  return { data: result,
352
366
  text: result.written ? `wrote ${result.file}` : `not written: ${result.file}` };
@@ -362,7 +376,13 @@ const HANDLERS = Object.freeze({
362
376
 
363
377
  update: async ({ options, runtime }) => runUpdateCommand({ options, runtime }),
364
378
 
365
- help: async () => ({ data: { commands: describeCommands() }, text: helpText() }),
379
+ help: async ({ options }) => {
380
+ if (options.helpCommand === undefined) {
381
+ return { data: { commands: describeCommands() }, text: helpText() };
382
+ }
383
+ const data = describeCommand(options.helpCommand, options.subcommand);
384
+ return { data, text: commandHelpText(data) };
385
+ },
366
386
 
367
387
  version: async ({ runtime }) => {
368
388
  // Read by the composition root from the package manifest: `bin/` sits at
@@ -390,6 +410,9 @@ export async function main(argv, runtime) {
390
410
  let parsed;
391
411
  try {
392
412
  parsed = parseArgs(argv);
413
+ if (runtime.managementOnly && !["help", "version", "update"].includes(parsed.command)) {
414
+ throw new AccError(EXIT.DATA, "runtime unavailable; retry after the update or run acc update to recover");
415
+ }
393
416
  // `config` is the one command that must work on a workspace ACC cannot
394
417
  // open. Discovery validates the config too, so a broken one would fail
395
418
  // there first and `acc config validate` - the command a user runs to find
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { assertPortableId } from "@agents-can-communicate/protocol";
6
+ import { readSessionRecord } from "@agents-can-communicate/storage-filesystem";
7
+ import { listRuntimeHolds } from "./leases.mjs";
8
+ import { confirmedDead, defaultPidIsAlive, withManagerLock } from "./mutex.mjs";
9
+ import { canonicalManagerRoot, managedDirectory, readControl, readManagedJson, writeControl } from "./state.mjs";
10
+
11
+ // Both 0.3.1 and current MCP write this exact continuity record, without
12
+ // native client facts. A foreign harness id may look like an MCP key: require
13
+ // its canonical file and a validated matching MCP owner before exempting it.
14
+ async function isMcpContinuity(record, name, workspaceRoot, workspaceId) {
15
+ if (record?.schemaVersion !== 1 || Object.keys(record).sort().join(",")
16
+ !== "accSessionId,generation,harnessSessionId,schemaVersion") return false;
17
+ try {
18
+ const parts = record.harnessSessionId.split(":");
19
+ if (parts.length !== 3 || parts[0] !== "mcp" || parts[2] !== workspaceId) return false;
20
+ for (const id of [parts[1], parts[2], record.accSessionId, record.generation]) assertPortableId(id, "MCP continuity id");
21
+ const expected = createHash("sha256").update(record.harnessSessionId).digest("hex").slice(0, 32) + ".json";
22
+ if (name !== expected) return false;
23
+ const owner = await readSessionRecord({ root: workspaceRoot, workspaceId, sessionId: record.accSessionId });
24
+ return owner?.harness === "mcp" && owner.participantId === parts[1]
25
+ && owner.workspaceId === workspaceId && owner.sessionId === record.accSessionId
26
+ && owner.generation === record.generation;
27
+ } catch { return false; } // Ambiguous or corrupt ownership remains a safety hold.
28
+ }
29
+
30
+ /** Native bindings outlive finish/TTL; MCP process lifetime is covered by its runtime lease. */
31
+ export async function listNativeHolds(root, { pidIsAlive = defaultPidIsAlive } = {}) {
32
+ const workspaces = path.join(path.dirname(await canonicalManagerRoot(root)), "workspaces");
33
+ if (!await managedDirectory(workspaces)) return [];
34
+ const holds = [];
35
+ for (const entry of await readdir(workspaces, { withFileTypes: true })) {
36
+ if (!entry.isDirectory()) {
37
+ if (entry.isSymbolicLink()) holds.push({ kind: "native", reason: "unknown_workspace" });
38
+ continue;
39
+ }
40
+ const bindings = path.join(workspaces, entry.name, "bindings");
41
+ if (!await managedDirectory(bindings)) continue;
42
+ for (const name of await readdir(bindings)) {
43
+ if (!name.endsWith(".json")) continue;
44
+ const record = await readManagedJson(path.join(bindings, name)).catch(() => null);
45
+ if (record === undefined) continue; // Concurrent lifecycle removal, before fencing.
46
+ if (await isMcpContinuity(record, name, path.dirname(bindings), entry.name)) continue;
47
+ const pid = record?.schemaVersion === 1 && Number.isSafeInteger(record.clientPid)
48
+ && record.clientPid > 0 ? record.clientPid : null;
49
+ if (pid !== null && await confirmedDead(pid, pidIsAlive)) continue;
50
+ holds.push({ kind: "native", pid, reason: pid === null ? "unknown_client_pid" : "client_running" });
51
+ }
52
+ }
53
+ return holds;
54
+ }
55
+
56
+ async function prepareCandidate(control, root, env) {
57
+ const file = path.join(control.pending.root, "node_modules", "@agents-can-communicate", "cli",
58
+ "src", "managed-runtime", "refresh.mjs");
59
+ const { prepareRefresh } = await import(pathToFileURL(file).href);
60
+ return prepareRefresh({ control, root, env });
61
+ }
62
+ // JSON parser diagnostics may quote private config bytes. Keep the cause and
63
+ // known artifact paths, never the offending input or terminal control bytes.
64
+ const safeText = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, " ").slice(0, 1000);
65
+ function safeFailure(error) {
66
+ const message = typeof error === "string" ? error : "integration refresh failed";
67
+ if (/(?:valid|parse|parsing) JSON|JSON (?:input|at position)|Unexpected token|Expected.*(?:property|comma|colon)/i.test(message)) return "configuration is not valid JSON";
68
+ return safeText(message);
69
+ }
70
+ const recipe = c => JSON.stringify([c.active, c.pending, c.home, c.targets, c.auto, c.pin]);
71
+
72
+ /** Detection is outside the fence; every integration write and commit stays inside it. */
73
+ export async function activatePending(root, { prepare = prepareCandidate, env = process.env,
74
+ pidIsAlive = defaultPidIsAlive, ignorePid = null } = {}) {
75
+ root = await canonicalManagerRoot(root);
76
+ const before = await readControl(root);
77
+ if (!before?.pending) return { activated: false, reason: "no_pending_update" };
78
+ const apply = await prepare(before, root, env);
79
+ return withManagerLock(root, async () => {
80
+ const current = await readControl(root);
81
+ if (!current || recipe(current) !== recipe(before)) return { activated: false, reason: "state_changed" };
82
+ const holds = (await listRuntimeHolds(root, { pidIsAlive })).filter(lease => lease.pid !== ignorePid);
83
+ holds.push(...await listNativeHolds(root, { pidIsAlive }));
84
+ if (holds.length) {
85
+ const notice = `ACC ${current.pending.version} is ready; waiting for ${holds.length} active or unidentified process(es).`;
86
+ await writeControl(root, { ...current, notice });
87
+ return { activated: false, reason: "processes_active", holds: holds.length, notice };
88
+ }
89
+ const activating = { ...current, phase: "activating", notice: "Finishing integration refresh." };
90
+ await writeControl(root, activating);
91
+ let result;
92
+ try {
93
+ result = await apply(); // Never release the fence with a mutating promise still running.
94
+ if (!result || !Array.isArray(result.failed) || result.failed.length) {
95
+ throw new Error("integration refresh is incomplete");
96
+ }
97
+ } catch (error) {
98
+ const notice = "Integration refresh is incomplete; run acc update to recover.";
99
+ await writeControl(root, { ...activating, notice });
100
+ const installation = result && Array.isArray(result.failed) ? { ...result,
101
+ failed: result.failed.map(failure => ({ ...failure, adapterId: safeText(failure.adapterId), error: safeFailure(failure.error),
102
+ paths: (failure.paths ?? []).map(file => safeText(file)) })) } : undefined;
103
+ return { activated: false, reason: "refresh_failed", notice, error: safeFailure(error.message),
104
+ ...(installation ? { installation } : {}) };
105
+ }
106
+ await writeControl(root, { ...activating, active: current.pending, pending: null,
107
+ phase: "ready", notice: `ACC ${current.pending.version} is active.` });
108
+ return { activated: true, version: current.pending.version, installation: result };
109
+ });
110
+ }
@@ -0,0 +1,64 @@
1
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
+ import { stableVersion, newerVersion } from "./download.mjs";
3
+ import { withManagerLock } from "./mutex.mjs";
4
+ import { scheduleWorker } from "./schedule.mjs";
5
+ import { readControl, writeControl } from "./state.mjs";
6
+ import { performUpdate, runWorker } from "./worker.mjs";
7
+
8
+ export function validateUpdateOptions(options) {
9
+ if (options.auto !== undefined && !["on", "off"].includes(options.auto)) {
10
+ throw new AccError(EXIT.USAGE, "--auto expects on or off");
11
+ }
12
+ if (options.pin !== undefined && options.pin !== "none" && !stableVersion(options.pin)) {
13
+ throw new AccError(EXIT.USAGE, "--pin expects an exact stable version or none");
14
+ }
15
+ if (options.check && (options.auto !== undefined || options.pin !== undefined)) {
16
+ throw new AccError(EXIT.USAGE, "--check cannot change update settings");
17
+ }
18
+ }
19
+
20
+ export async function runManagedUpdate({ options, runtime }) {
21
+ const root = runtime.managerRoot;
22
+ const env = runtime.env ?? {};
23
+ try {
24
+ if (options.auto !== undefined || options.pin !== undefined) {
25
+ const control = await withManagerLock(root, async () => {
26
+ const current = await readControl(root);
27
+ if (!current) throw new Error("run acc install before configuring automatic updates");
28
+ if (current.phase !== "ready" && options.pin !== undefined) throw new Error("finish the interrupted update before changing its version pin");
29
+ const pin = options.pin === undefined ? current.pin : options.pin === "none" ? null : options.pin;
30
+ if (pin !== null && newerVersion(current.active.version, pin)) {
31
+ throw new Error("pin cannot downgrade workspace data; choose the running version or a newer release");
32
+ }
33
+ return writeControl(root, { ...current,
34
+ auto: options.auto === undefined ? current.auto : options.auto === "on", pin,
35
+ autoPreference: options.auto === undefined ? current.autoPreference ?? current.auto : options.auto === "on",
36
+ pending: options.pin !== undefined && current.pending?.version !== pin ? null : current.pending,
37
+ notice: null });
38
+ });
39
+ await scheduleWorker(root, control, { env });
40
+ return { data: { auto: control.auto, pin: control.pin, running: control.active.version },
41
+ text: `Automatic updates ${control.auto ? "on" : "off"}. ${control.pin ? `Pinned to ${control.pin}.` : "Following stable releases."}` };
42
+ }
43
+ const result = options.check
44
+ ? await performUpdate(root, { check: true, env })
45
+ : await runWorker(root, { force: true, env, ignorePid: process.pid });
46
+ const text = result.activated ? `Updated ACC to ${result.version}; integrations refreshed.`
47
+ : result.notice && result.reason === "processes_active" ? result.notice
48
+ : result.reason === "refresh_failed" ? [result.notice,
49
+ ...(result.installation?.failed ?? []).map(failure =>
50
+ `${failure.adapterId}: ${failure.error}${failure.paths?.length ? ` (${failure.paths.join(", ")})` : ""}`),
51
+ ...(!result.installation?.failed?.length && result.error ? [result.error] : [])].join("\n")
52
+ : result.checked ? result.newer ? `ACC ${result.latest} is available; run acc update.`
53
+ : `ACC ${result.running} is current${result.pin ? ` for pin ${result.pin}` : ""}.`
54
+ : result.reason === "network_disabled" ? "Update networking is off (ACC_NO_UPDATE_CHECK)."
55
+ : result.notice ?? "Update is pending.";
56
+ return { data: result, text,
57
+ ...(result.reason === "refresh_failed" ? { error: new AccError(EXIT.DATA, text) } : {}) };
58
+ } catch (error) {
59
+ if (/manager lock held/.test(error.message)) {
60
+ return { data: { inProgress: true }, text: "An update is already in progress; ACC will activate it when running clients exit." };
61
+ }
62
+ throw error instanceof AccError ? error : new AccError(EXIT.DATA, error.message);
63
+ }
64
+ }
@@ -0,0 +1,71 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { stageOwnGeneration } from "./generation.mjs";
6
+ import { verifyGeneration } from "./install.mjs";
7
+ import { managedDirectory } from "./state.mjs";
8
+ import { ENTRY_KINDS } from "./entry.mjs";
9
+
10
+ const NAME = "agents-can-communicate";
11
+ const exec = promisify(execFile);
12
+ export const stableVersion = version => typeof version === "string"
13
+ && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
14
+ export function newerVersion(candidate, current) {
15
+ if (!stableVersion(candidate) || !stableVersion(current)) return false;
16
+ const left = candidate.split(".").map(BigInt), right = current.split(".").map(BigInt);
17
+ for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] > right[i];
18
+ return false;
19
+ }
20
+
21
+ /** Metadata only. Downloading and npm run exclusively in the update worker. */
22
+ export async function fetchRelease({ pin = null, env = process.env, get = fetch } = {}) {
23
+ if (pin !== null && !stableVersion(pin)) throw new Error("pin must be an exact stable version");
24
+ const registry = new URL(env.npm_config_registry || "https://registry.npmjs.org/");
25
+ if (!["https:", "http:"].includes(registry.protocol)) throw new Error("invalid npm registry");
26
+ registry.pathname = `${registry.pathname.replace(/\/$/, "")}/`;
27
+ const url = new URL(`${NAME}/${pin ?? "latest"}`, registry).href;
28
+ const response = await get(url, { signal: AbortSignal.timeout(5_000), headers: { accept: "application/json" } });
29
+ if (!response?.ok) throw new Error(`registry request failed (${response?.status ?? "no response"})`);
30
+ const body = await response.json();
31
+ if (body?.name !== NAME) throw new Error("release package identity mismatch");
32
+ if (!stableVersion(body.version) || pin !== null && body.version !== pin) throw new Error("release is not the requested stable version");
33
+ const integrity = body.dist?.integrity;
34
+ if (typeof integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity)
35
+ || Buffer.from(integrity.slice(7), "base64").length !== 64) throw new Error("release integrity is missing or invalid");
36
+ return { version: body.version, integrity, registry: registry.href };
37
+ }
38
+
39
+ async function npmCli(env) {
40
+ const directories = [path.dirname(process.execPath), ...(env.PATH ?? "").split(path.delimiter)];
41
+ for (const directory of directories.filter(Boolean)) {
42
+ try { return await realpath(path.join(directory, "npm")); } catch { /* Try the next installed npm. */ }
43
+ }
44
+ throw new Error("npm is unavailable; the working ACC runtime was kept");
45
+ }
46
+
47
+ export async function downloadRelease(root, release, { env = process.env } = {}) {
48
+ const downloads = path.join(root, "downloads");
49
+ await managedDirectory(downloads, { create: true });
50
+ const temporary = await mkdtemp(path.join(downloads, ".download-"));
51
+ try {
52
+ await writeFile(path.join(temporary, "package.json"), JSON.stringify({ private: true, name: "acc-runtime-download", version: "1.0.0" }));
53
+ await exec(process.execPath, [await npmCli(env), "install", "--ignore-scripts", "--omit=dev",
54
+ "--global=false", "--prefix", temporary, "--package-lock=true",
55
+ "--package-lock-only=false", "--dry-run=false", "--workspaces=false",
56
+ "--include-workspace-root=false", "--no-audit", "--no-fund", "--save=true", "--save-exact", "--registry", release.registry,
57
+ `${NAME}@${release.version}`], { cwd: temporary, env, timeout: 90_000, maxBuffer: 1024 * 1024 });
58
+ const lock = JSON.parse(await readFile(path.join(temporary, "package-lock.json"), "utf8"));
59
+ if (lock.packages?.[`node_modules/${NAME}`]?.integrity !== release.integrity) {
60
+ throw new Error("downloaded release integrity differs from discovery");
61
+ }
62
+ const packageRoot = path.join(temporary, "node_modules", NAME);
63
+ const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
64
+ if (manifest.name !== NAME || manifest.version !== release.version) throw new Error("downloaded package identity mismatch");
65
+ for (const name of ENTRY_KINDS) await access(path.join(packageRoot, "bin", "entrypoints", `${name}.mjs`));
66
+ await access(path.join(packageRoot, "bin", "acc-update-worker.mjs"));
67
+ const generation = await stageOwnGeneration({ packageRoot, managerRoot: root });
68
+ await verifyGeneration(generation, { env });
69
+ return { version: generation.version, root: generation.root };
70
+ } finally { await rm(temporary, { recursive: true, force: true }); }
71
+ }
@@ -0,0 +1,110 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { scheduleWorker } from "./schedule.mjs";
5
+ import { acquireRuntime } from "./leases.mjs";
6
+ import { canonicalManagerRoot, readControl } from "./state.mjs";
7
+
8
+ export const ENTRY_KINDS = Object.freeze([
9
+ "acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel",
10
+ ]);
11
+
12
+ const BOOTSTRAP_FLAGS = new Map([["--adapter", "adapter"], ["--real-executable", "realExecutable"],
13
+ ["--data-home", "dataHome"]]);
14
+
15
+ /** Shared with the runtime; parse before admission without importing installer code. */
16
+ export function parseBootstrapOptions(args) {
17
+ const options = {};
18
+ for (let index = 0; index < args.length; index += 2) {
19
+ const key = BOOTSTRAP_FLAGS.get(args[index]);
20
+ const value = args[index + 1];
21
+ if (key === undefined || typeof value !== "string" || value === "" || key in options) return null;
22
+ options[key] = value;
23
+ }
24
+ return BOOTSTRAP_FLAGS.values().every(key => key in options) && path.isAbsolute(options.dataHome)
25
+ ? options : null;
26
+ }
27
+
28
+ // Bootstrap resolution deliberately needs only Node built-ins: no selected
29
+ // generation is imported until admission and its actual PID lease are durable.
30
+ export function managerLocation({ env = process.env, platform = process.platform } = {}) {
31
+ const data = env.ACC_DATA_HOME || (platform === "win32" ? env.APPDATA
32
+ : platform === "darwin" ? env.HOME && path.join(env.HOME, "Library", "Application Support")
33
+ : env.XDG_DATA_HOME || env.HOME && path.join(env.HOME, ".local", "share"));
34
+ if (typeof data !== "string" || !path.isAbsolute(data)) throw new Error("cannot resolve ACC data home");
35
+ return path.join(data, "acc", "runtime");
36
+ }
37
+
38
+ export function invokedDirectly(url) {
39
+ try { return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(url)); }
40
+ catch { return false; }
41
+ }
42
+
43
+ function unavailable(kind) {
44
+ // Never reflect control file content or payloads into a client diagnostic.
45
+ if (kind === "acc-hook" || kind === "acc-claude-channel") {
46
+ process.stderr.write("acc: coordination unavailable during runtime update; session may continue\n");
47
+ process.exitCode = 0;
48
+ } else if (kind === "acc-bootstrap") {
49
+ process.exitCode = 1; // The vendor shell shim launches its original command.
50
+ } else {
51
+ process.stderr.write("acc: runtime unavailable; retry after the update or run acc update to recover\n");
52
+ process.exitCode = 4;
53
+ }
54
+ }
55
+
56
+ /** The lease lasts until OS process death, including callbacks after main returns. */
57
+ export async function runEntry({ kind, packageRoot, managerRoot, managedRequired = false }) {
58
+ if (!ENTRY_KINDS.includes(kind)) throw new Error("unknown ACC entry point");
59
+ const bootstrapOptions = kind === "acc-bootstrap" ? parseBootstrapOptions(process.argv.slice(2)) : null;
60
+ if (kind === "acc-bootstrap" && bootstrapOptions === null) {
61
+ if (process.env.ACC_BOOTSTRAP_DEBUG === "1") {
62
+ process.stderr.write("acc-bootstrap: usage: --adapter <id> --real-executable <path> --data-home <path>\n");
63
+ }
64
+ process.exitCode = 2;
65
+ return;
66
+ }
67
+ let selected = packageRoot;
68
+ let managed = null;
69
+ let root;
70
+ try {
71
+ const requestedDataHome = bootstrapOptions === null ? null : await canonicalManagerRoot(bootstrapOptions.dataHome);
72
+ const requestedRoot = requestedDataHome === null ? null
73
+ : await canonicalManagerRoot(path.join(requestedDataHome, "acc", "runtime"));
74
+ if (managerRoot && requestedDataHome !== null
75
+ && requestedDataHome !== await canonicalManagerRoot(path.dirname(path.dirname(managerRoot)))) {
76
+ throw new Error("bootstrap cache home differs from its launcher");
77
+ }
78
+ root = await canonicalManagerRoot(managerRoot ?? requestedRoot ?? managerLocation());
79
+ if (requestedRoot !== null && root !== requestedRoot) throw new Error("bootstrap data home differs from its launcher");
80
+ const control = await readControl(root);
81
+ if (control !== null) {
82
+ selected = control.active.root;
83
+ managed = root;
84
+ const lease = await acquireRuntime(root, { pid: process.pid, kind });
85
+ selected = lease.runtime.root;
86
+ managed = root;
87
+ const quiet = kind === "acc" && ["update", "install", "uninstall", "help", "version",
88
+ "--help", "-h", "--version", "-v", "-V"].includes(process.argv[2]);
89
+ if (!quiet) await scheduleWorker(root, control);
90
+ } else if (managedRequired) throw new Error("managed runtime is not initialized");
91
+ } catch {
92
+ // Help and update recovery must remain reachable when state cannot admit a
93
+ // workspace command. The CLI parser enforces this restricted path; merely
94
+ // putting --help in a message value cannot bypass runtime admission.
95
+ if (kind === "acc" && selected) {
96
+ const runtime = await import(pathToFileURL(path.join(selected, "bin", "entrypoints", "acc.mjs")).href);
97
+ await runtime.main({ managerRoot: root ?? null, packageRoot: selected, managementOnly: true });
98
+ return;
99
+ }
100
+ unavailable(kind);
101
+ if (kind === "acc-claude-channel" && selected) {
102
+ const { startInertChannel } = await import(pathToFileURL(path.join(selected,
103
+ "bin", "entrypoints", "claude-channel-stdio.mjs")).href);
104
+ startInertChannel();
105
+ }
106
+ return;
107
+ }
108
+ const runtime = await import(pathToFileURL(path.join(selected, "bin", "entrypoints", `${kind}.mjs`)).href);
109
+ await runtime.main({ managerRoot: managed, packageRoot: selected });
110
+ }
@@ -0,0 +1,167 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm }
3
+ from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ const NAME = "agents-can-communicate";
7
+ const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
8
+
9
+ async function canonicalFuturePath(file) {
10
+ let current = path.resolve(file);
11
+ const trailing = [];
12
+ for (;;) {
13
+ try { return path.join(await realpath(current), ...trailing); }
14
+ catch (error) {
15
+ if (error.code !== "ENOENT") throw error;
16
+ const parent = path.dirname(current);
17
+ if (parent === current) throw error;
18
+ trailing.unshift(path.basename(current));
19
+ current = parent;
20
+ }
21
+ }
22
+ }
23
+
24
+ async function directory(file) {
25
+ await mkdir(file, { recursive: true, mode: 0o700 });
26
+ const stat = await lstat(file);
27
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed runtime directory is not a regular directory");
28
+ }
29
+
30
+ function declaredPath(value) {
31
+ if (typeof value !== "string" || value === "" || /[\\*?\[\]{}\0]/.test(value)
32
+ || path.isAbsolute(value) || value.split("/").includes("..")) {
33
+ throw new Error("invalid declared runtime path");
34
+ }
35
+ return value.replace(/\/+$/, "");
36
+ }
37
+
38
+ async function collectFile(source, relative, collected) {
39
+ const stat = await lstat(source);
40
+ if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
41
+ if (stat.isDirectory()) {
42
+ for (const entry of (await readdir(source)).sort()) {
43
+ await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
44
+ }
45
+ } else if (stat.isFile()) {
46
+ collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
47
+ } else throw new Error(`unsupported runtime file: ${relative}`);
48
+ }
49
+
50
+ async function checkDeclaredComponents(source, relative) {
51
+ let current = source;
52
+ for (const component of relative.split("/")) {
53
+ current = path.join(current, component);
54
+ if ((await lstat(current)).isSymbolicLink()) {
55
+ throw new Error(`symbolic link inside declared runtime path: ${relative}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ function requireOutside(source, root) {
61
+ const relative = path.relative(source, root);
62
+ if (relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))) {
63
+ throw new Error("managed runtime must live outside its source package");
64
+ }
65
+ }
66
+
67
+ async function collectPackage(source, prefix, collected, expectedName, version) {
68
+ await checkDeclaredComponents(source, "package.json");
69
+ const manifestFile = path.join(source, "package.json");
70
+ const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
71
+ if (manifest.name !== expectedName || manifest.version !== version
72
+ || !Array.isArray(manifest.files)) throw new Error("runtime package identity or declared files are invalid");
73
+ const declared = new Set(["package.json", ...manifest.files.map(declaredPath)]);
74
+ // npm includes these public package files regardless of the files whitelist.
75
+ for (const file of await readdir(source)) {
76
+ if (/^(readme|licen[cs]e)(\..*)?$/i.test(file)) declared.add(file);
77
+ }
78
+ for (const relative of declared) {
79
+ await checkDeclaredComponents(source, relative);
80
+ await collectFile(path.join(source, relative), path.posix.join(prefix, relative), collected);
81
+ }
82
+ return manifest;
83
+ }
84
+
85
+ function fingerprint(files) {
86
+ const hash = createHash("sha256");
87
+ for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
88
+ hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
89
+ hash.update(file.bytes);
90
+ }
91
+ return hash.digest("hex");
92
+ }
93
+
94
+ async function existingMatches(target, files) {
95
+ try {
96
+ const actual = new Map();
97
+ await collectFile(target, "", actual);
98
+ if (fingerprint(actual) !== fingerprint(files)) throw new Error("managed runtime generation integrity changed");
99
+ return true;
100
+ } catch (error) {
101
+ if (error.code === "ENOENT") {
102
+ // Only an absent generation is new. A damaged existing tree is not a
103
+ // staging directory we are entitled to overwrite.
104
+ const exists = await lstat(target).then(() => true, e => {
105
+ if (e.code === "ENOENT") return false;
106
+ throw e;
107
+ });
108
+ if (!exists) return false;
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /** Materialize the package's shipped files; never copy a checkout or workspace symlink. */
115
+ export async function stageOwnGeneration({ packageRoot, managerRoot }) {
116
+ const source = await realpath(packageRoot);
117
+ const root = await canonicalFuturePath(managerRoot);
118
+ requireOutside(source, root);
119
+ await checkDeclaredComponents(source, "package.json");
120
+ const manifest = JSON.parse(await readFile(path.join(source, "package.json"), "utf8"));
121
+ if (manifest.name !== NAME || !STABLE.test(manifest.version)) throw new Error("invalid ACC runtime version");
122
+ const files = new Map();
123
+ await collectPackage(source, "", files, NAME, manifest.version);
124
+ if (!Array.isArray(manifest.bundleDependencies)) throw new Error("runtime must declare its bundled workspaces");
125
+ for (const name of manifest.bundleDependencies) {
126
+ if (!/^@agents-can-communicate\/[a-z0-9-]+$/.test(name)) throw new Error("invalid bundled runtime package");
127
+ const location = await realpath(path.join(source, "node_modules", name));
128
+ requireOutside(location, root);
129
+ await collectPackage(location, `node_modules/${name}`, files, name, manifest.version);
130
+ }
131
+ const digest = fingerprint(files);
132
+ await directory(root);
133
+ const generations = path.join(root, "generations");
134
+ await directory(generations);
135
+ const target = path.join(generations, `${manifest.version}-${digest.slice(0, 24)}`);
136
+ const result = { version: manifest.version, root: target, digest };
137
+ if (await existingMatches(target, files)) return result;
138
+ const staging = await mkdtemp(path.join(generations, ".staging-"));
139
+ try {
140
+ const directories = new Set([staging]);
141
+ for (const [name, file] of files) {
142
+ const destination = path.join(staging, name);
143
+ await mkdir(path.dirname(destination), { recursive: true });
144
+ for (let parent = path.dirname(destination); parent !== staging; parent = path.dirname(parent)) directories.add(parent);
145
+ const handle = await open(destination, "wx", file.mode);
146
+ try {
147
+ await handle.writeFile(file.bytes);
148
+ await handle.chmod(file.mode);
149
+ await handle.sync();
150
+ } finally { await handle.close(); }
151
+ }
152
+ for (const directory of [...directories].sort((a, b) => b.length - a.length)) {
153
+ const handle = await open(directory, "r");
154
+ try { await handle.sync(); } finally { await handle.close(); }
155
+ }
156
+ try {
157
+ await rename(staging, target);
158
+ } catch (error) {
159
+ if (!["EEXIST", "ENOTEMPTY"].includes(error.code) || !await existingMatches(target, files)) throw error;
160
+ }
161
+ const handle = await open(generations, "r");
162
+ try { await handle.sync(); } finally { await handle.close(); }
163
+ return result;
164
+ } finally {
165
+ await rm(staging, { recursive: true, force: true });
166
+ }
167
+ }