agents-can-communicate 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +28 -7
  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 +73 -0
  16. package/docs/ADAPTER_AUTHORING.md +41 -7
  17. package/docs/ARCHITECTURE.md +62 -11
  18. package/docs/CAPABILITIES.md +40 -36
  19. package/docs/CLI.md +143 -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 +37 -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 +45 -13
  30. package/docs/TROUBLESHOOTING.md +76 -18
  31. package/docs/UPGRADING.md +119 -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 +106 -5
  68. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
  69. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +106 -5
  71. package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
  72. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
  73. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
  80. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
  81. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
  82. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
  83. package/node_modules/@agents-can-communicate/cli/package.json +4 -2
  84. package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
  85. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
  86. package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
  87. package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
  88. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +22 -4
  89. package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
  90. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
  91. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +63 -0
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
  94. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
  95. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +73 -0
  96. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
  97. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
  98. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
  99. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
  100. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
  101. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
  102. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
  103. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +122 -0
  104. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
  105. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
  106. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
  107. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
  109. package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
  110. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +9 -25
  111. package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
  112. package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
  113. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
  114. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
  115. package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
  116. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
  117. package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
  118. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
  119. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
  120. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
  121. package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
  122. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  123. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
  124. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
  125. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
  127. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
  128. package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
  129. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
  130. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -67
  131. package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
  132. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  133. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
  134. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
  135. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
  137. package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
  138. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
  139. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
  140. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
  141. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
  143. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
  144. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
  145. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
  146. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  147. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
  148. package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
  149. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
  150. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
  151. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  152. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
  153. package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
  154. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
  155. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
  156. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
  157. package/package.json +2 -1
@@ -1,3 +1,4 @@
1
+ import { retireNativeBinding } from "./native-retirement.mjs";
1
2
  import { createHash, randomBytes } from "node:crypto";
2
3
  import { realpath } from "node:fs/promises";
3
4
  import path from "node:path";
@@ -5,7 +6,8 @@ import path from "node:path";
5
6
  import { clearSessionBinding, effectiveCapabilities, loadSessionBinding, storeSessionBinding }
6
7
  from "@agents-can-communicate/adapter-sdk";
7
8
  import { createCoordinationService } from "@agents-can-communicate/core";
8
- import { createId } from "@agents-can-communicate/protocol";
9
+ import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
10
+ import { assertPortableId, createId } from "@agents-can-communicate/protocol";
9
11
  import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
10
12
  import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
11
13
  from "@agents-can-communicate/cli";
@@ -14,6 +16,7 @@ import { resolveClientPid } from "./client-pid.mjs";
14
16
  import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
15
17
  import { establishNativeBinding, livePolicyFrom } from "./native-binding.mjs";
16
18
  import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
19
+ import { withSessionLifecycle } from "./session-lifecycle.mjs";
17
20
 
18
21
  // Kept cohesive above 300 lines because every handler shares one fail-open
19
22
  // hook boundary, binding lifecycle, and client-specific outcome contract.
@@ -24,6 +27,10 @@ import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs
24
27
  // let a call through than to make someone's session sit waiting on us.
25
28
  const DEFAULT_BUDGET_MS = 5_000;
26
29
 
30
+ function assertHookBudget(deadline, message = "hook deadline expired") {
31
+ if (Date.now() >= deadline) throw new Error(message);
32
+ }
33
+
27
34
  const byteLength = value => Buffer.byteLength(value, "utf8");
28
35
 
29
36
  function compactInboxRecovery(messages) {
@@ -46,6 +53,13 @@ function fitDegradation(projection, visibleDegradation, messages, budgetBytes) {
46
53
  return projection;
47
54
  }
48
55
 
56
+ function ownerOnlyOutcome(adapter, owner, budgetBytes) {
57
+ if (byteLength(owner) > budgetBytes) {
58
+ return { stdout: "", stderr: "acc: context budget cannot fit owner arguments; increase contextBudgetBytes" };
59
+ }
60
+ return { stdout: "", ...adapter.injectOutcome?.(owner) };
61
+ }
62
+
49
63
  // Declared by this process on the session it opens, so peers can tell an idle
50
64
  // session from a dead one. Only one of the four clients fires a heartbeat event,
51
65
  // so the rest refresh here: on every turn, and during a long one whenever the
@@ -171,17 +185,20 @@ export function participantFor(adapterId, harnessSessionId, env = {}) {
171
185
  return `${adapterId}-${suffix}`;
172
186
  }
173
187
 
174
- async function openContext({ cwd, dataHome, runtime, env }) {
188
+ async function openContext({ cwd, dataHome, runtime, env, deadline }) {
189
+ assertHookBudget(deadline);
175
190
  const descriptor = await discoverWorkspace({ cwd, env: env ?? {},
176
- gitProbe: createGitProbe() });
191
+ gitProbe: createGitProbe({ deadlineAt: deadline }) });
192
+ assertHookBudget(deadline);
193
+ const resolvedDataHome = dataHome ?? platformDataHome({ env: env ?? {} });
177
194
  const paths = runtimePaths({
178
- dataHome: dataHome ?? platformDataHome({ env: env ?? {} }),
195
+ dataHome: resolvedDataHome,
179
196
  workspaceId: descriptor.id,
180
197
  workspaceRoots: descriptor.roots,
181
198
  });
182
199
  const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
183
- ids: runtime.ids, workspaceId: descriptor.id });
184
- return { descriptor, paths, env: env ?? {}, realpath: runtime.realpath ?? realpath,
200
+ ids: runtime.ids, workspaceId: descriptor.id, deadlineAt: deadline });
201
+ return { descriptor, paths, dataHome: resolvedDataHome, env: env ?? {}, realpath: runtime.realpath ?? realpath,
185
202
  service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
186
203
  }
187
204
 
@@ -207,12 +224,19 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
207
224
  exceptSessionId: binding.accSessionId });
208
225
  const messages = delivery.queuedMessages;
209
226
 
210
- // Solo costs nothing: nothing to say means nothing printed, not a banner
211
- // announcing that nobody else is here. But something already said to you is
212
- // not nothing - the check used to run before the inbox was read, so the
213
- // answer to your own request vanished the moment the agent working on it
214
- // closed and left you as the only session.
215
- if (sync.solo && messages.length === 0) return { stdout: "" };
227
+ // Only this hook's payload selected the binding. Supply its own pair as
228
+ // trusted context, outside peer bodies, rather than exporting inheritable
229
+ // credentials or teaching the CLI to guess from a public roster.
230
+ const owner = "ACC CLI (append): --session "
231
+ + assertPortableId(binding.accSessionId, "sessionId") + " --generation "
232
+ + assertPortableId(binding.generation, "generation");
233
+ const totalBudget = context.descriptor.policy?.contextBudgetBytes ?? 6_000;
234
+ // A peer can join after this prompt has begun. The current turn must already
235
+ // have its own arguments when it needs inbox/reply, without reattaching or
236
+ // waiting for another user prompt. Solo emits identity, not a peer notice.
237
+ if (sync.solo && messages.length === 0) {
238
+ return ownerOnlyOutcome(adapter, owner, totalBudget);
239
+ }
216
240
 
217
241
  // The ceiling a team agreed on in `acc.workspace.json`, or the default when
218
242
  // there is no config. Validated by the protocol and, until now, never read:
@@ -222,10 +246,18 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
222
246
  const canOfferNextTurn = effective.delivery.nextTurn === true && hasStructuredRenderer;
223
247
  const projectionInput = { ...sync, messages: canOfferNextTurn ? messages : [],
224
248
  liveOfferedMessageIds: delivery.liveOfferedMessageIds,
249
+ reminderMessageIds: delivery.reminderMessageIds,
225
250
  roomMessageIds: delivery.roomMessageIds,
226
251
  currentParticipantId: mine?.participantId };
252
+ // Credentials alone cannot replace the command that reaches a queued body.
253
+ // If both cannot fit, preserve recovery and explain the missing owner pair.
254
+ const minimumBodyBytes = messages.length === 0 ? 1
255
+ : byteLength(compactInboxRecovery(messages.slice(0, 1)));
256
+ const ownerFits = byteLength(owner) + 1 + minimumBodyBytes <= totalBudget;
257
+ const ownerWarning = ownerFits ? null
258
+ : "acc: context budget cannot fit owner arguments with coordination context; increase contextBudgetBytes";
227
259
  const projectionOptions = {
228
- budgetBytes: context.descriptor.policy?.contextBudgetBytes };
260
+ budgetBytes: ownerFits ? totalBudget - byteLength(owner) - 1 : totalBudget };
229
261
  // Delivery is state, not text parsing. Peer-controlled bodies can imitate
230
262
  // another message's visible header, so only projector metadata proves
231
263
  // which complete groups survived the byte budget. A custom adapter without
@@ -246,10 +278,15 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
246
278
  + `${messages[0].messageId} with acc inbox --message ${messages[0].messageId}` : null;
247
279
  const visibleDegradation = degradation === null ? "" : `ACC: ${degradation.slice(5)}`;
248
280
  const budgetBytes = projectionOptions.budgetBytes ?? 6_000;
249
- const projected = degradation === null ? projection.text
281
+ const body = degradation === null ? projection.text
250
282
  : fitDegradation(projection.text, visibleDegradation, messages, budgetBytes);
283
+ // Own claims make sync non-solo but produce no peer context. They cannot
284
+ // remove this turn's identity or consume a nonexistent body separator.
285
+ if (body === "" && messages.length === 0) return ownerOnlyOutcome(adapter, owner, totalBudget);
286
+ const projected = body === "" ? "" : ownerFits ? `${owner}\n${body}` : body;
251
287
  if (projected === "") {
252
- return degradation === null ? { stdout: "" } : { stdout: "", stderr: degradation };
288
+ return { stdout: "", stderr: messages.length === 0 ? ""
289
+ : [ownerWarning, degradation].filter(Boolean).join("\n") };
253
290
  }
254
291
 
255
292
  // The renderer returns ids as metadata, never as text to parse. A peer body
@@ -269,20 +306,24 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
269
306
  // callback proves that the bytes crossed.
270
307
  const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
271
308
  const writableOffers = outcome.stdout === "" ? [] : offerInputs;
272
- if (degradation === null) return { ...outcome, offerInputs: writableOffers };
273
309
  return { ...outcome,
274
- stderr: [outcome.stderr, degradation].filter(Boolean).join("\n"),
310
+ stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
275
311
  offerInputs: writableOffers };
276
312
  }
277
313
 
278
314
  // One bounded, fail-open native handshake for this exact session generation.
279
- // The policy comes only from the environment an owned shell bootstrap
280
- // exported; an ordinary launch has none and stays durable.
315
+ // New adapters read durable consent from their installation record; legacy
316
+ // adapters keep the environment exported by an owned shell bootstrap.
281
317
  async function bindNative({ adapter, event, hookBinding, clientVersion, platform, context, paths,
282
318
  deadline }) {
319
+ assertHookBudget(deadline);
320
+ const livePolicy = adapter?.nativeDelivery?.policySource === "installation-record"
321
+ ? await readInstalledLivePolicy({ dataHome: context.dataHome, adapterId: adapter.id })
322
+ : livePolicyFrom(context.env);
323
+ assertHookBudget(deadline);
283
324
  return establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
284
- livePolicy: livePolicyFrom(context.env), service: context.service, runtimeDir: paths.root,
285
- clock: context.service.clock,
325
+ livePolicy, service: context.service, runtimeDir: paths.root,
326
+ clock: context.service.clock, env: context.env,
286
327
  timeoutMs: Math.max(1, Math.min(750, deadline - Date.now())) });
287
328
  }
288
329
 
@@ -293,11 +334,12 @@ const HANDLERS = {
293
334
  // certified facts before any probe, PID lookup, resume, or open can fail;
294
335
  // keep only the generation identity needed for a successful resume.
295
336
  if (binding !== null) {
296
- await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
337
+ await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
297
338
  accSessionId: binding.accSessionId, generation: binding.generation });
298
339
  }
299
340
  const clientVersion = await probeClientVersion(adapter,
300
341
  { timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
342
+ assertHookBudget(deadline);
301
343
  const clientFacts = { clientVersion, platform };
302
344
  const capabilities = effectiveCapabilities(adapter, clientFacts);
303
345
  // Once per session, never per turn. A client that cannot be found yields
@@ -305,7 +347,9 @@ const HANDLERS = {
305
347
  // behaviour every session had before this existed.
306
348
  const command = adapter.client?.command ?? null;
307
349
  const pid = command === null ? null
308
- : resolveClientPid({ table: await readProcessTable(), from: process.pid, command });
350
+ : resolveClientPid({ table: await readProcessTable({
351
+ timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) }), from: process.pid, command });
352
+ assertHookBudget(deadline);
309
353
  const clientPid = Number.isInteger(pid) && pid > 0 ? pid : undefined;
310
354
  const native = hookBinding => bindNative({ adapter, event, hookBinding, ...clientFacts,
311
355
  context, paths, deadline });
@@ -317,6 +361,16 @@ const HANDLERS = {
317
361
  checkoutRoot: context.descriptor.git?.worktreeRoot ?? context.descriptor.roots[0],
318
362
  branch: context.descriptor.git?.branch ?? null,
319
363
  };
364
+ if (event.kind === "beforeTurn" && binding !== null) {
365
+ // A real prompt may continue after finish closed this native owner's
366
+ // record. Recheck after the probes: a CLI replacement can run outside
367
+ // the native lifecycle lock. Never adopt that replacement's identity.
368
+ const previous = await context.service.locateSession(binding.accSessionId);
369
+ if (previous?.record.state !== "closed"
370
+ || previous.record.generation !== binding.generation) {
371
+ throw new Error("the completed hook owner changed during turn registration");
372
+ }
373
+ }
320
374
  if (binding !== null) {
321
375
  const resumed = await context.service.resumeSession({
322
376
  sessionId: binding.accSessionId,
@@ -326,13 +380,21 @@ const HANDLERS = {
326
380
  if (resumed !== null) {
327
381
  const hookBinding = { accSessionId: resumed.sessionId, generation: resumed.generation,
328
382
  ...clientFacts, clientPid };
329
- await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
383
+ await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
330
384
  ...hookBinding });
331
385
  return { accSessionId: resumed.sessionId, generation: resumed.generation,
332
386
  ...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
333
387
  }
334
388
  }
389
+ // Persist ownership before creating its session. If the hook dies after
390
+ // creation, the next start resumes this exact pair. If creation never
391
+ // happened (or it was closed), a retry allocates a NEW pair, never revives it.
392
+ const opening = { sessionId: context.service.ids.next("session"),
393
+ generation: context.service.ids.next("generation") };
394
+ await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
395
+ accSessionId: opening.sessionId, generation: opening.generation });
335
396
  const session = await context.service.openSession({
397
+ ...opening,
336
398
  workspaceId: context.descriptor.id,
337
399
  participantId: participantFor(adapterId, event.sessionId, context.env),
338
400
  displayName: participantFor(adapterId, event.sessionId, context.env),
@@ -348,7 +410,7 @@ const HANDLERS = {
348
410
  });
349
411
  const hookBinding = { accSessionId: session.sessionId, generation: session.generation,
350
412
  ...clientFacts, clientPid };
351
- await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
413
+ await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
352
414
  ...hookBinding });
353
415
  return { accSessionId: session.sessionId, generation: session.generation,
354
416
  ...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
@@ -361,27 +423,59 @@ const HANDLERS = {
361
423
  return {};
362
424
  },
363
425
 
364
- async sessionEnd({ binding, context, event, paths }) {
426
+ async sessionEnd({ binding, context, event, paths, adapter, deadline }) {
365
427
  if (binding === null) return {};
366
- await context.service.closeSession({ sessionId: binding.accSessionId,
367
- generation: binding.generation });
368
- await clearSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId });
428
+ await retireNativeBinding({ adapter, service: context.service, sessionId: binding.accSessionId,
429
+ generation: binding.generation, runtimeDir: paths.root,
430
+ timeoutMs: Math.max(1, Math.min(200, deadline - Date.now())) });
431
+ const owner = { sessionId: binding.accSessionId, generation: binding.generation };
432
+ const current = await context.service.locateSession(binding.accSessionId);
433
+ if (current?.record.generation === binding.generation && current.record.state === "open") {
434
+ await context.service.closeSession(owner);
435
+ } else {
436
+ // Close may have committed before this hook died. Retire only the old
437
+ // endpoint and binding; do not close a replacement or repeat a close event.
438
+ await context.service.clearDeliveryBinding(owner);
439
+ }
440
+ await clearSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId });
369
441
  return {};
370
442
  },
371
443
 
372
444
  async beforeTurn(input) {
373
445
  const { binding, context, adapter, event, paths, deadline } = input;
374
- if (binding === null) return {};
446
+ if (binding === null) {
447
+ // SessionStart can be skipped when an automatic runtime refresh races
448
+ // process admission. A genuine prompt is the next safe proof that this
449
+ // native conversation exists, and it is already inside the same
450
+ // lifecycle mutex as startup. Reuse that opening path so publication,
451
+ // crash recovery, client facts, and the native handshake stay atomic.
452
+ const started = await HANDLERS.sessionStart(input);
453
+ const fresh = await loadSessionBinding({ runtimeDir: paths.root,
454
+ harnessSessionId: event.sessionId });
455
+ const turn = await projectTurn({ ...input, binding: fresh });
456
+ return { ...turn, nativeBinding: started.nativeBinding };
457
+ }
458
+ const current = await context.service.locateSession(binding.accSessionId);
459
+ if (current?.record.state === "closed" && current.record.generation === binding.generation) {
460
+ // finish ends an ACC incarnation, not the native conversation. Only a
461
+ // genuine new user turn can start another one; tool hooks cannot. Reuse
462
+ // the crash-safe opening path, retaining its full published client facts
463
+ // and its single native handshake rather than binding a second time.
464
+ const started = await HANDLERS.sessionStart(input);
465
+ const fresh = await loadSessionBinding({ runtimeDir: paths.root,
466
+ harnessSessionId: event.sessionId });
467
+ const turn = await projectTurn({ ...input, binding: fresh });
468
+ return { ...turn, nativeBinding: started.nativeBinding };
469
+ }
375
470
  // A turn is the clearest sign a session is alive. Never a reason to fail:
376
471
  // this runs in front of somebody's prompt.
377
472
  await context.service.heartbeatSession({ sessionId: binding.accSessionId,
378
- generation: binding.generation }).catch(() => null);
473
+ generation: binding.generation });
379
474
  // A native transport that became ready only after SessionStart is picked
380
475
  // up here and a live lease is renewed: bounded, fail-open, never on a guard.
381
- const nativeBinding = livePolicyFrom(context.env) === "off" ? undefined
382
- : await bindNative({ adapter, event, hookBinding: binding,
383
- clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
384
- deadline });
476
+ const nativeBinding = await bindNative({ adapter, event, hookBinding: binding,
477
+ clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
478
+ deadline });
385
479
  const turn = await projectTurn(input);
386
480
  return nativeBinding === undefined ? turn : { ...turn, nativeBinding };
387
481
  },
@@ -458,42 +552,48 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
458
552
  probeClientVersion = defaultProbeClientVersion,
459
553
  platform = `${process.platform}-${process.arch}` }) {
460
554
  const deadline = Date.now() + budgetMs;
461
- const result = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
555
+ const fallback = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
462
556
  commitOffers: async () => {} };
463
- let timer = null;
464
- try {
557
+ const execute = async () => {
558
+ assertHookBudget(deadline);
465
559
  const adapter = adapters?.[adapterId];
466
560
  if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
467
561
 
468
562
  const event = await adapter.normalizeHook(payload);
469
- const context = await openContext({ cwd: event.cwd, dataHome, runtime, env });
470
- const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
471
- harnessSessionId: event.sessionId }).catch(() => null);
472
-
563
+ const context = await openContext({ cwd: event.cwd, dataHome, runtime, env, deadline });
473
564
  const handler = HANDLERS[event.kind];
474
- const work = handler === undefined
475
- ? Promise.resolve({})
476
- : handler({ event, context, adapter, adapterId, binding, paths: context.paths,
565
+ const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
566
+ const invoke = async () => {
567
+ assertHookBudget(deadline);
568
+ if (lifecycle) {
569
+ // A previous holder may have died after journalling its session while
570
+ // this process waited. Recover that write before interpreting its binding.
571
+ const store = await openFilesystemStore({ root: context.paths.root, clock: runtime.clock,
572
+ ids: runtime.ids, workspaceId: context.descriptor.id, deadlineAt: deadline });
573
+ context.service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
574
+ }
575
+ const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
576
+ harnessSessionId: event.sessionId });
577
+ assertHookBudget(deadline);
578
+ return handler === undefined ? {} : handler({ event, context, adapter, adapterId,
579
+ binding, paths: context.paths,
477
580
  readProcessTable, probeClientVersion, platform, deadline });
478
-
479
- // The loser of a race is not cancelled, so the timer is cleared explicitly:
480
- // an outstanding one keeps the process alive long past its answer.
481
- const budget = new Promise(resolve => {
482
- timer = setTimeout(() => resolve({ timedOut: true }), budgetMs);
483
- });
484
- Object.assign(result, await Promise.race([work, budget]));
485
-
581
+ };
582
+ const work = lifecycle
583
+ ? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
584
+ clock: runtime.clock, deadlineAt: deadline }, invoke)
585
+ : invoke();
586
+ // Keep the late continuation's result private. Once the budget wins, it
587
+ // cannot change the fail-open answer or restore withdrawn output/offers.
588
+ const result = { ...fallback, ...await work };
589
+ assertHookBudget(deadline);
486
590
  const offerInputs = result.offerInputs ?? [];
487
591
  let commitPromise = null;
488
592
  result.commitOffers = () => {
489
593
  if (commitPromise !== null) return commitPromise;
490
594
  commitPromise = (async () => {
491
595
  for (const input of offerInputs) {
492
- const remaining = deadline - Date.now();
493
- if (remaining <= 0) throw new Error("hook budget exhausted before offer commit");
494
- // The durable transaction owns deadline cancellation. Racing it here
495
- // would only reject the public promise while the losing writer kept
496
- // waiting and could publish later.
596
+ assertHookBudget(deadline, "hook budget exhausted before offer commit");
497
597
  await context.service.recordOfferSucceeded({ ...input, deadlineAt: deadline });
498
598
  }
499
599
  })();
@@ -503,18 +603,26 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
503
603
 
504
604
  const status = await context.service.collectStatus({
505
605
  workspaceId: context.descriptor.id });
606
+ assertHookBudget(deadline);
506
607
  result.sessions = status.participants.filter(p => p.presence !== "offline");
507
608
  result.service = context.service;
609
+ return result;
610
+ };
611
+
612
+ let timer;
613
+ try {
614
+ // One budget covers normalization, discovery, recovery, handling and the
615
+ // final status read. Store/binding deadlines fence undecided writes; an
616
+ // activated journal still completes under its writer lock after expiry.
617
+ const budget = new Promise(resolve => {
618
+ timer = setTimeout(() => resolve({ ...fallback, timedOut: true }),
619
+ Math.max(0, deadline - Date.now()));
620
+ });
621
+ return await Promise.race([execute(), budget]);
508
622
  } catch (error) {
509
- result.failed = true;
510
- result.reason = error.message;
511
- result.decision = "allow";
512
- result.stdout = "";
513
- // A later failure may happen after a turn prepared offer inputs. Once the
514
- // fail-open path withdraws stdout, no transport boundary remains to commit.
515
- result.commitOffers = async () => {};
623
+ return { ...fallback, failed: true, reason: error.message,
624
+ ...(Date.now() >= deadline ? { timedOut: true } : {}) };
516
625
  } finally {
517
- if (timer !== null) clearTimeout(timer);
626
+ clearTimeout(timer);
518
627
  }
519
- return result;
520
628
  }
@@ -0,0 +1,13 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ import { withWriterMutex } from "@agents-can-communicate/storage-filesystem";
5
+
6
+ // Binding ownership spans load, resume/open/close, and publication. The store's
7
+ // writer lock protects each record, but cannot protect this whole hook sequence.
8
+ // Use the same crash-safe mutex in a separate directory to avoid nested locks.
9
+ export function withSessionLifecycle({ root, sessionId, clock, deadlineAt }, operation) {
10
+ const key = createHash("sha256").update(String(sessionId)).digest("hex");
11
+ return withWriterMutex({ locks: path.join(root, "lifecycle-locks", key) },
12
+ { root, clock, deadlineAt }, operation);
13
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -42,10 +42,12 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
42
42
  // a fixed order; an explicit off takes a recorded one back first so the
43
43
  // record written below describes the machine as it now is.
44
44
  const notes = [];
45
+ let retainedMechanisms = [];
45
46
  if (operation.deactivation !== undefined) {
46
47
  const report = await deactivateNative({ nativeActivation: operation.deactivation,
47
48
  ...activation });
48
49
  notes.push(...describeTeardown(report));
50
+ retainedMechanisms = report.retainedMechanisms;
49
51
  }
50
52
  let native = null;
51
53
  let appendedRcBlock = false;
@@ -55,13 +57,19 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
55
57
  native = applied.nativeActivation;
56
58
  appendedRcBlock = applied.appendedRcBlock;
57
59
  }
60
+ if (retainedMechanisms.length > 0) {
61
+ native = { ...(native ?? operation.deactivation),
62
+ mechanisms: [...(native?.mechanisms ?? []), ...retainedMechanisms] };
63
+ }
58
64
  // Recorded after the write, so a record never claims an install that
59
65
  // did not happen. The reverse order would leave uninstall trying to
60
66
  // remove files nothing created.
61
67
  await recordInstall({ dataHome, adapterId: adapter.id,
62
68
  version: operation.clientVersion ?? null, accVersion,
63
- artifacts: operation.artifacts, createdDirectories, nativeActivation: native });
69
+ artifacts: operation.artifacts, createdDirectories,
70
+ deliveryPolicy: operation.livePolicy, nativeActivation: native });
64
71
  results.operations.push({ ...operation, applied: true, appendedRcBlock,
72
+ needsAction: outcome.needsAction ?? [],
65
73
  changes: outcome.changes ?? [], diagnostics: [
66
74
  ...(operation.deliveryDiagnostic === undefined
67
75
  ? [] : [operation.deliveryDiagnostic]),
@@ -69,14 +77,19 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
69
77
  ...notes,
70
78
  ] });
71
79
  } else {
80
+ // Config ownership can veto removal. Ask before teardown or recorded
81
+ // artifact deletion; an adapter-local check would run after those writes.
82
+ await adapter.preflightUninstall?.(context);
72
83
  // Keep the record until every cleanup step succeeds. It is both the
73
84
  // authority for deletion and the only durable recipe a retry has when
74
85
  // the client or one of ACC's own artifacts is already gone.
75
86
  const notes = [];
87
+ let retainedMechanisms = [];
76
88
  if (operation.deactivation !== undefined) {
77
89
  const report = await deactivateNative({ nativeActivation: operation.deactivation,
78
90
  ...activation });
79
91
  notes.push(...describeTeardown(report));
92
+ retainedMechanisms = report.retainedMechanisms;
80
93
  }
81
94
  const owned = await removeOwnedArtifacts({ dataHome, adapterId: adapter.id });
82
95
  // What ownership held back is passed on, because the adapter would
@@ -86,7 +99,9 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
86
99
  const outcome = await adapter.uninstall({ ...context, keep: owned.kept });
87
100
  const directories = await removeEmptyOwnedDirectories({ home: context.home,
88
101
  directories: owned.createdDirectories });
89
- await finalizeRemoval({ dataHome, adapterId: adapter.id });
102
+ await finalizeRemoval({ dataHome, adapterId: adapter.id,
103
+ retainedNativeActivation: retainedMechanisms.length === 0 ? null
104
+ : { ...operation.deactivation, mechanisms: retainedMechanisms } });
90
105
  results.operations.push({ ...operation, applied: true,
91
106
  changes: outcome.changes ?? [], removed: owned.removed, kept: owned.kept,
92
107
  removedDirectories: directories.removed, keptDirectories: directories.kept,
@@ -4,7 +4,7 @@ import { createReadStream } from "node:fs";
4
4
  import { mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
 
7
- import { evaluateNativeEligibility } from "@agents-can-communicate/adapter-sdk";
7
+ import { evaluateNativeEligibility, ownVersion } from "@agents-can-communicate/adapter-sdk";
8
8
 
9
9
  // The launch-time check behind an owned shell shim. It answers one closed
10
10
  // question - may this exact executable receive native delivery? - from the
@@ -13,9 +13,10 @@ import { evaluateNativeEligibility } from "@agents-can-communicate/adapter-sdk";
13
13
  // command untouched, and this module is what makes "cannot decide" cheap.
14
14
  //
15
15
  // The cache key is the executable's identity - resolved path, symlink target,
16
- // inode, size, mtime, and full sha256 - so an upgrade or replacement is a miss
17
- // and an unchanged binary skips the version spawn and the probe. A cached
18
- // failure is short-lived; a repaired client is never disabled for long.
16
+ // inode, size, mtime, and full sha256 - plus platform and this ACC runtime's
17
+ // version. A vendor upgrade, replacement, or changed certification contract is
18
+ // therefore a miss. A cached failure is short-lived; a repaired client is
19
+ // never disabled for long.
19
20
 
20
21
  export const BOOTSTRAP_CACHE_SCHEMA = 1;
21
22
  export const SUPPORTED_TTL_MS = 6 * 60 * 60 * 1_000;
@@ -86,7 +87,7 @@ const closedOutcome = (supported, reasonCode) => Object.freeze({ supported, reas
86
87
 
87
88
  export async function checkNativeBootstrap({ adapter, realExecutable, platform, dataHome,
88
89
  timeoutMs = DEFAULT_TIMEOUT_MS, clock = { now: () => new Date().toISOString() },
89
- readVersion = defaultReadVersion }) {
90
+ readVersion = defaultReadVersion, accVersion }) {
90
91
  try {
91
92
  if (adapter?.nativeDelivery === undefined || typeof adapter.probeNativeDelivery !== "function") {
92
93
  return closedOutcome(false, "native_delivery_unsupported");
@@ -94,8 +95,12 @@ export async function checkNativeBootstrap({ adapter, realExecutable, platform,
94
95
  const identity = await executableIdentity(realExecutable);
95
96
  const file = cachePathFor(dataHome, adapter.id);
96
97
  const now = Date.parse(clock.now());
98
+ const runtimeVersion = accVersion === undefined
99
+ ? await ownVersion(import.meta.url) : accVersion;
100
+ const runtimeVersionKnown = typeof runtimeVersion === "string" && runtimeVersion !== "";
97
101
  const cached = await loadCache(file);
98
102
  if (cached !== null && cached.adapterId === adapter.id && cached.platform === platform
103
+ && runtimeVersionKnown && cached.accVersion === runtimeVersion
99
104
  && sameIdentity(cached.identity, identity) && Date.parse(cached.expiresAt) > now) {
100
105
  return closedOutcome(cached.supported, cached.reasonCode);
101
106
  }
@@ -131,6 +136,7 @@ export async function checkNativeBootstrap({ adapter, realExecutable, platform,
131
136
  // Never command output, never a message body.
132
137
  await saveCache(file, {
133
138
  schemaVersion: BOOTSTRAP_CACHE_SCHEMA, adapterId: adapter.id, platform, identity,
139
+ accVersion: runtimeVersionKnown ? runtimeVersion : null,
134
140
  clientVersion, supported, reasonCode: outcomeReason,
135
141
  protocolContract: supported ? eligibility.protocolContract : null,
136
142
  modes: supported ? [...eligibility.modes] : [],
@@ -66,7 +66,7 @@ async function detectNative(adapter, entry, { context, platform, probeTimeoutMs,
66
66
  const facts = { realExecutable, probe: null, eligibility: null };
67
67
  try {
68
68
  facts.probe = await withTimeout(Promise.resolve(adapter.probeNativeDelivery({
69
- realExecutable, timeoutMs: probeTimeoutMs })), probeTimeoutMs,
69
+ realExecutable, timeoutMs: probeTimeoutMs, env: context?.env })), probeTimeoutMs,
70
70
  `${adapter.id} native probe`);
71
71
  } catch {
72
72
  facts.probe = null;
@@ -12,3 +12,4 @@ export { BLOCK_BEGIN, BLOCK_END, SHIM_MARKER, SHIM_POLICIES, SUPPORTED_SHELLS,
12
12
  shellLiteral, uninstallShellBootstrap, validateShimEntry } from "./shell-bootstrap.mjs";
13
13
  export { LIVE_POLICIES, describeActivation, describeDeactivation, livePolicyOf, rcFileFor,
14
14
  resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
15
+ export { readInstalledLivePolicy } from "./live-policy.mjs";
@@ -0,0 +1,21 @@
1
+ import { loadOwnership } from "./ownership.mjs";
2
+
3
+ export const LIVE_POLICIES = Object.freeze(["off", "actionable", "all"]);
4
+
5
+ export const livePolicyOf = install => {
6
+ if (Object.hasOwn(install ?? {}, "deliveryPolicy")) {
7
+ return LIVE_POLICIES.includes(install.deliveryPolicy) ? install.deliveryPolicy : "off";
8
+ }
9
+ return LIVE_POLICIES.includes(install?.nativeActivation?.livePolicy)
10
+ ? install.nativeActivation.livePolicy : "off";
11
+ };
12
+
13
+ export async function readInstalledLivePolicy({ dataHome, adapterId }) {
14
+ try {
15
+ const record = await loadOwnership({ dataHome });
16
+ const install = record.installs.find(entry => entry.adapterId === adapterId);
17
+ return livePolicyOf(install);
18
+ } catch {
19
+ return "off";
20
+ }
21
+ }