agents-can-communicate 0.8.5 → 0.9.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 (142) hide show
  1. package/README.md +11 -7
  2. package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
  3. package/bin/entrypoints/acc-hook.mjs +33 -19
  4. package/bin/entrypoints/hook-output.mjs +14 -5
  5. package/docs/ADAPTER_AUTHORING.md +29 -0
  6. package/docs/CAPABILITIES.md +7 -6
  7. package/docs/CLI.md +5 -0
  8. package/docs/CONFIGURATION.md +41 -10
  9. package/docs/DESIGN_DECISIONS.md +0 -2
  10. package/docs/GETTING_STARTED.md +39 -6
  11. package/docs/HOW_IT_WORKS.md +13 -8
  12. package/docs/SECURITY_MODEL.md +32 -5
  13. package/docs/TROUBLESHOOTING.md +76 -8
  14. package/docs/UPGRADING.md +47 -5
  15. package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -1
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +3 -0
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +13 -2
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
  35. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +3 -0
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
  42. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
  43. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
  44. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
  45. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  46. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +3 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/allow-rule.mjs +129 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
  49. package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
  51. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +36 -13
  52. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
  53. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
  56. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  57. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +3 -0
  58. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
  60. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
  61. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  62. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +3 -0
  63. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
  64. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  65. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +3 -0
  66. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
  67. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
  68. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  69. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
  70. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
  73. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/own-version.mjs +27 -1
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
  80. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
  81. package/node_modules/@agents-can-communicate/cli/package.json +2 -1
  82. package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
  83. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
  84. package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
  85. package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
  86. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
  87. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
  88. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
  89. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
  90. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
  91. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
  94. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
  95. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
  96. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
  97. package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
  98. package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
  99. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
  100. package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
  101. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  102. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
  103. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
  104. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
  105. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
  106. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  107. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
  109. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
  110. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
  111. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
  112. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
  113. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
  114. package/node_modules/@agents-can-communicate/installer/package.json +3 -2
  115. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
  116. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
  117. package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
  118. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
  119. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -2
  120. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
  121. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  122. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  123. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +6 -2
  124. package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
  125. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
  129. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
  130. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
  131. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
  132. package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
  133. package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
  134. package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
  135. package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
  136. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
  137. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
  138. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
  139. package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
  140. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
  141. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
  142. package/package.json +1 -5
@@ -8,20 +8,22 @@ import { capabilityEvidence, clearNativeAttempt, clearSessionBinding, effectiveC
8
8
  loadSessionBinding, normalizedEvent, storeSessionBinding }
9
9
  from "@agents-can-communicate/adapter-sdk";
10
10
  import { createCoordinationService } from "@agents-can-communicate/core";
11
- import { loadOwnership } from "@agents-can-communicate/installer";
11
+ import { loadOwnership } from "@agents-can-communicate/installer/hook-support";
12
12
  import { AccError, createId } from "@agents-can-communicate/protocol";
13
13
  import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
14
14
  import { clearPin, createGitProbe, resolveHookWorkspace, platformDataHome, runtimePaths,
15
15
  writePin as writeRuntimePin }
16
- from "@agents-can-communicate/cli";
16
+ from "@agents-can-communicate/cli/hook-support";
17
17
 
18
- import { resolveClientPid } from "./client-pid.mjs";
19
- import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
18
+ import { resolveClient } from "./client-pid.mjs";
19
+ import { ownExecutable, probeClientVersion as defaultProbeClientVersion, probeExecutableVersion }
20
+ from "./client-version.mjs";
20
21
  import { bindNative, callRelease, nativeActivationHintFor, nativeDiagnosticDeadline }
21
22
  from "./native-attempt.mjs";
22
23
  import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
23
24
  import { withSessionLifecycle } from "./session-lifecycle.mjs";
24
- import { appendStartOwner, appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
25
+ import { appendStartOwner, appendToolOwner, assertOwnerQuotable, ownerHeader, ownerOnlyOutcome }
26
+ from "./owner-context.mjs";
25
27
 
26
28
  // Kept cohesive above 300 lines because every handler shares one fail-open
27
29
  // hook boundary, binding lifecycle, and client-specific outcome contract.
@@ -31,6 +33,16 @@ import { appendStartOwner, appendToolOwner, ownerHeader, ownerOnlyOutcome } from
31
33
  // A hook runs in front of the user's turn, so it gets a hard ceiling. Better to
32
34
  // let a call through than to make someone's session sit waiting on us.
33
35
  const DEFAULT_BUDGET_MS = 5_000;
36
+ // Failures the hook output explains with advice of its own; any other error
37
+ // text stays out of what the client shows.
38
+ const KNOWN_FAILURES = new Set(["workspace_contains_runtime", "workspace_path_unquotable"]);
39
+ // Windows reads the table through WMI in Script Host, 145 ms on a
40
+ // windows-latest runner, and through PowerShell where Script Host is off, whose
41
+ // first start on a machine took 3.8 s. It gets more of the budget there, and
42
+ // leaves the rest for the write that follows.
43
+ const WINDOWS = process.platform === "win32";
44
+ const PROCESS_TABLE_MS = WINDOWS ? 3_000 : 1_000;
45
+ const TABLE_RESERVE_MS = WINDOWS ? 500 : 0;
34
46
 
35
47
  function assertHookBudget(deadline, message = "hook deadline expired") {
36
48
  if (Date.now() >= deadline) throw new Error(message);
@@ -396,13 +408,71 @@ async function projectActivation(input, offered) {
396
408
  }
397
409
  }
398
410
 
411
+ const STABLE_OR_PRERELEASE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
412
+ const isVersion = value => typeof value === "string" && STABLE_OR_PRERELEASE.test(value);
413
+
414
+ /**
415
+ * Which of its products the adapter recognised the client as. A variant name
416
+ * the adapter does not declare is ignored together with the version it came
417
+ * with: that version counts on a line ACC has no evidence for.
418
+ */
419
+ function declaredProduct(adapter, client) {
420
+ const name = client?.certificationName;
421
+ if (typeof name !== "string") return { clientName: undefined, version: client?.version };
422
+ const primary = adapter.client?.certificationName ?? adapter.client?.command;
423
+ if (name === primary) return { clientName: undefined, version: client.version };
424
+ const declared = (adapter.client?.variants ?? []).some(variant => variant?.certificationName === name);
425
+ return declared ? { clientName: name, version: client.version }
426
+ : { clientName: undefined, version: undefined };
427
+ }
428
+
429
+ /**
430
+ * The version the session's own client runs, or null to fall back to the PATH
431
+ * probe: what its command line named, then the adapter's reading of the
432
+ * running client (Claude Code's session registry), then the client's own
433
+ * executable when `ps` names it by full path. Each step is bounded by the
434
+ * hook's budget and fails open.
435
+ */
436
+ async function runningClientVersion({ adapter, client, product, env, deadline }) {
437
+ if (client === null || client === undefined) return null;
438
+ if (isVersion(product.version)) return product.version;
439
+ const remaining = cap => Math.max(1, Math.min(cap, deadline - Date.now() - TABLE_RESERVE_MS));
440
+ if (typeof adapter.clientVersionOf === "function" && client.entry !== undefined) {
441
+ let timer;
442
+ const read = await Promise.race([
443
+ Promise.resolve().then(() => adapter.clientVersionOf({ pid: client.pid, entry: client.entry, env }))
444
+ .catch(() => null),
445
+ new Promise(resolve => { timer = setTimeout(() => resolve(null), remaining(500)); }),
446
+ ]).finally(() => clearTimeout(timer));
447
+ if (isVersion(read)) return read;
448
+ }
449
+ const file = ownExecutable(client.entry, adapter.client?.command);
450
+ if (file === null) return null;
451
+ return probeExecutableVersion(file, adapter.client?.versionArgs ?? ["--version"],
452
+ { timeoutMs: remaining(1_000) });
453
+ }
454
+
455
+ // Whether the client process a binding names has exited. Signal 0 delivers
456
+ // nothing; EPERM means the process exists under another user.
457
+ function clientGone(binding) {
458
+ const pid = binding?.clientPid;
459
+ if (!Number.isInteger(pid) || pid <= 0) return false;
460
+ try {
461
+ process.kill(pid, 0);
462
+ return false;
463
+ } catch (error) {
464
+ return error?.code === "ESRCH";
465
+ }
466
+ }
467
+
399
468
  const HANDLERS = {
400
469
  // `knownClientPid` and `pins: false` are registration's (#167): it runs in
401
470
  // a peer's process, so the client's pid is handed over rather than found in
402
471
  // this process's ancestry, and it pins nothing because no hook code of any
403
472
  // generation runs for the session until its first real hook pins its own.
404
473
  async sessionStart({ event, context, adapter, adapterId, binding, paths,
405
- readProcessTable, probeClientVersion, platform, deadline, knownClientPid, pins = true }) {
474
+ readProcessTable, probeClientVersion, platform, deadline, knownClientPid, pins = true,
475
+ tableRead: started = null, mark = () => {}, clientRestarted = false }) {
406
476
  // Where this session's pin lives, resolved once and reused at every write
407
477
  // below. Beside `paths.root`, never inside it: see `managerRootFor`.
408
478
  const pinRoot = managerRootFor(context.dataHome);
@@ -419,23 +489,42 @@ const HANDLERS = {
419
489
  await writePin({ root: pinRoot, harnessSessionId: event.sessionId, runtimeRoot: facts.runtimeRoot,
420
490
  version: facts.version, storeVersion: facts.storeVersion, clientPid: binding.clientPid });
421
491
  }
422
- const clientVersion = await probeClientVersion(adapter,
423
- { timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
424
- assertHookBudget(deadline);
425
- const clientFacts = { clientVersion, platform };
426
- const capabilities = effectiveCapabilities(adapter, clientFacts);
427
492
  // Once per session, never per turn. A client that cannot be found yields
428
493
  // null, and the session is then judged by age alone - which is exactly the
429
494
  // behaviour every session had before this existed.
430
495
  const command = adapter.client?.command ?? null;
431
- const pid = Number.isInteger(knownClientPid) && knownClientPid > 0 ? knownClientPid
432
- : command === null ? null
433
- : resolveClientPid({ table: await readProcessTable({
434
- timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) }), from: process.pid, command });
496
+ const known = Number.isInteger(knownClientPid) && knownClientPid > 0;
497
+ // Read while the client reports its version: neither needs the other, and
498
+ // each can start a program. A session start began the read already, when
499
+ // its event was known. Handled here so a probe that fails first leaves no
500
+ // rejection unobserved.
501
+ const tableRead = known || command === null ? null : started ?? Promise.resolve(readProcessTable({
502
+ timeoutMs: Math.max(1, Math.min(PROCESS_TABLE_MS, deadline - Date.now() - TABLE_RESERVE_MS)) }));
503
+ tableRead?.catch(() => {});
504
+ // The binary on PATH answers in parallel, as the fallback: the version the
505
+ // running client names for itself, when it names one, wins below.
506
+ const pathVersion = probeClientVersion(adapter,
507
+ { timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
508
+ mark("reading the process table");
509
+ const client = known ? { pid: knownClientPid } : command === null ? null
510
+ : resolveClient({ table: await tableRead, from: process.pid, command,
511
+ clientPackage: adapter.client?.package, identify: adapter.identifyClientProcess });
435
512
  assertHookBudget(deadline);
513
+ mark("probing the client version");
514
+ const product = declaredProduct(adapter, client);
515
+ const clientVersion = await runningClientVersion({ adapter, client, product, env: context.env,
516
+ deadline }) ?? await pathVersion;
517
+ assertHookBudget(deadline);
518
+ const clientFacts = { clientVersion, platform,
519
+ ...(product.clientName === undefined ? {} : { clientName: product.clientName }) };
520
+ const capabilities = effectiveCapabilities(adapter, clientFacts);
521
+ const pid = client?.pid ?? null;
522
+ mark("opening the session");
436
523
  const clientPid = Number.isInteger(pid) && pid > 0 ? pid : undefined;
437
- const native = hookBinding => bindNative({ adapter, event, hookBinding, ...clientFacts,
438
- context, paths, deadline });
524
+ const native = hookBinding => {
525
+ mark("binding live delivery");
526
+ return bindNative({ adapter, event, hookBinding, ...clientFacts, context, paths, deadline });
527
+ };
439
528
  const metadata = {
440
529
  pid,
441
530
  enforcement: capabilities.guards?.beforeWrite === true ? "guarded" : "advisory",
@@ -446,13 +535,18 @@ const HANDLERS = {
446
535
  };
447
536
  if (event.kind === "beforeTurn" && binding !== null) {
448
537
  // A genuine prompt can resume after an ephemeral removal or a durable
449
- // close. Recheck after probes: a CLI replacement can run outside the
450
- // native lifecycle lock. Never adopt that replacement's identity.
538
+ // close, or carry on a session whose client restarted. Recheck after
539
+ // probes: a CLI replacement can run outside the native lifecycle lock.
540
+ // Never adopt that replacement's identity.
541
+ // A session can also close between the turn's check and this one while
542
+ // its client was down; that is the closed-session resumption below,
543
+ // not a replacement.
451
544
  const previous = await context.service.locateSession(binding.accSessionId);
452
- if (previous !== null && (previous.record.state !== "closed"
453
- || previous.record.generation !== binding.generation)) {
454
- throw new Error("the completed hook owner changed during turn registration");
455
- }
545
+ const sameGeneration = previous?.record.generation === binding.generation;
546
+ const expected = previous === null
547
+ || sameGeneration && (previous.record.state === "closed"
548
+ || clientRestarted && previous.record.state === "open");
549
+ if (!expected) throw new Error("the completed hook owner changed during turn registration");
456
550
  }
457
551
  if (binding !== null) {
458
552
  const resumed = await context.service.resumeSession({
@@ -571,6 +665,21 @@ const HANDLERS = {
571
665
  const turn = await projectActivation({ ...input, binding: fresh }, offered);
572
666
  return { ...turn, nativeBinding: started.nativeBinding };
573
667
  }
668
+ if (clientGone(binding)) {
669
+ // The client this session was bound to has exited, and a turn arrived
670
+ // anyway: the client restarted under the same conversation. Antigravity
671
+ // 2.0 reopens a conversation without a second SessionStart, so without
672
+ // this the session stayed bound to a dead process and lost live delivery
673
+ // for good. The start path finds the client running now, keeps this ACC
674
+ // session and generation, and binds again.
675
+ const started = await HANDLERS.sessionStart({ ...input, clientRestarted: true });
676
+ const fresh = await loadSessionBinding({ runtimeDir: paths.root,
677
+ harnessSessionId: event.sessionId });
678
+ const offered = await nativeActivationHintFor({ adapter, event,
679
+ nativeBinding: started.nativeBinding, binding: fresh, context, paths, deadline });
680
+ const turn = await projectActivation({ ...input, binding: fresh }, offered);
681
+ return { ...turn, nativeBinding: started.nativeBinding };
682
+ }
574
683
  // A turn is the clearest sign a session is alive. Never a reason to fail:
575
684
  // this runs in front of somebody's prompt.
576
685
  await context.service.heartbeatSession({ sessionId: binding.accSessionId,
@@ -704,35 +813,51 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
704
813
  const deadline = Date.now() + budgetMs;
705
814
  const fallback = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
706
815
  commitOffers: async () => {} };
816
+ // Where the hook is, so a budget that runs out can say which step held it.
817
+ const trail = { phase: "reading the hook event" };
818
+ const mark = phase => { trail.phase = phase; };
707
819
  const execute = async () => {
708
820
  assertHookBudget(deadline);
709
821
  const adapter = adapters?.[adapterId];
710
822
  if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
711
823
 
712
824
  const event = await adapter.normalizeHook(payload, { args });
825
+ // A session start reads the process table once, and it depends on nothing
826
+ // below: it starts now, while the store opens. On Windows it is the part
827
+ // of a start that runs another program.
828
+ const tableRead = event.kind !== "sessionStart" || (adapter.client?.command ?? null) === null
829
+ ? null : Promise.resolve(readProcessTable({ timeoutMs: Math.max(1,
830
+ Math.min(PROCESS_TABLE_MS, deadline - Date.now() - TABLE_RESERVE_MS)) }));
831
+ tableRead?.catch(() => {});
832
+ mark("finding the workspace");
713
833
  const context = await openContext({ event, adapterId, dataHome, runtime, env, deadline });
834
+ assertOwnerQuotable(context.workspaceCwd, context.workspaceRef, String(platform).split("-")[0]);
714
835
  const handler = HANDLERS[event.kind];
715
836
  const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
716
837
  const invoke = async () => {
717
838
  assertHookBudget(deadline);
718
839
  if (lifecycle) {
840
+ mark("opening the store");
719
841
  // A previous holder may have died after journalling its session while
720
842
  // this process waited. Recover that write before interpreting its binding.
721
843
  const store = await openFilesystemStore({ root: context.paths.root, clock: runtime.clock,
722
844
  ids: runtime.ids, workspaceId: context.descriptor.id, deadlineAt: deadline });
723
845
  context.service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
724
846
  }
847
+ mark("reading the session binding");
725
848
  const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
726
849
  harnessSessionId: event.sessionId });
727
850
  assertHookBudget(deadline);
851
+ mark("handling the event");
728
852
  // The raw payload goes to a handler only to be handed back to the adapter
729
853
  // that produced it; nothing in core reads it.
730
854
  const result = handler === undefined ? {} : await handler({ event, context, adapter, adapterId,
731
855
  binding, paths: context.paths, payload,
732
- readProcessTable, probeClientVersion, platform, deadline });
856
+ readProcessTable, probeClientVersion, platform, deadline, tableRead, mark });
733
857
  return appendToolOwner(appendStartOwner(result, { event, context, adapter }),
734
858
  { event, binding, context, adapter });
735
859
  };
860
+ if (lifecycle) mark("waiting for the session lock");
736
861
  const work = lifecycle
737
862
  ? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
738
863
  clock: runtime.clock, deadlineAt: deadline }, invoke)
@@ -755,6 +880,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
755
880
  };
756
881
  delete result.offerInputs;
757
882
 
883
+ mark("reading the status");
758
884
  const status = await context.service.collectStatus({
759
885
  workspaceId: context.descriptor.id });
760
886
  assertHookBudget(deadline);
@@ -769,15 +895,15 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
769
895
  // final status read. Store/binding deadlines fence undecided writes; an
770
896
  // activated journal still completes under its writer lock after expiry.
771
897
  const budget = new Promise(resolve => {
772
- timer = setTimeout(() => resolve({ ...fallback, timedOut: true }),
898
+ timer = setTimeout(() => resolve({ ...fallback, timedOut: true, phase: trail.phase }),
773
899
  Math.max(0, deadline - Date.now()));
774
900
  });
775
901
  return await Promise.race([execute(), budget]);
776
902
  } catch (error) {
777
903
  return { ...fallback, failed: true, reason: error.message,
778
- ...(error instanceof AccError && error.details?.reasonCode === "workspace_contains_runtime"
779
- ? { failureCode: "workspace_contains_runtime" } : {}),
780
- ...(Date.now() >= deadline ? { timedOut: true } : {}) };
904
+ ...(error instanceof AccError && KNOWN_FAILURES.has(error.details?.reasonCode)
905
+ ? { failureCode: error.details.reasonCode } : {}),
906
+ ...(Date.now() >= deadline ? { timedOut: true, phase: trail.phase } : {}) };
781
907
  } finally {
782
908
  clearTimeout(timer);
783
909
  }
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/index.mjs"
7
+ ".": "./src/index.mjs",
8
+ "./hook-support": "./src/hook-support.mjs"
8
9
  },
9
10
  "files": [
10
11
  "src/"
@@ -3,7 +3,8 @@ import { isLaunchOption } from "@agents-can-communicate/adapter-sdk";
3
3
  // Shared presentation of observed delivery facts. Vendor probes provide closed
4
4
  // reasons; installation and doctor must not turn a version floor into readiness.
5
5
  const REASONS = Object.freeze({
6
- native_delivery_unsupported: "this adapter has no native delivery channel",
6
+ // Said of a client that has no live channel ACC supports.
7
+ native_delivery_unsupported: "this client has no live delivery channel ACC supports here",
7
8
  version_unavailable: "the client version could not be verified",
8
9
  below_minimum_version: "the client is below the native delivery minimum version",
9
10
  known_bad_version: "this client version has a known native delivery failure",
@@ -1,8 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
 
4
- import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility,
5
- validateNativeActivationPlan }
4
+ import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility, mergeEnv, pathOf,
5
+ resolveExecutable as resolveOnPath, runExecutable, validateNativeActivationPlan }
6
6
  from "@agents-can-communicate/adapter-sdk";
7
7
 
8
8
  import { resolveExecutable, shimDirFor } from "./native-activation.mjs";
@@ -34,6 +34,14 @@ const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b
34
34
  // `env` decides where the command is looked up, so detection given an
35
35
  // environment finds the client on that environment's PATH.
36
36
  export const spawnProbe = async (command, args, { env } = {}) => {
37
+ // Windows finds `codex.cmd` or `claude.exe`, never the bare name, and starts a
38
+ // .cmd only through cmd.exe.
39
+ if (process.platform === "win32") {
40
+ const file = await resolveOnPath(command, { pathEnv: pathOf(env ?? process.env) });
41
+ if (file === null) throw Object.assign(new Error(`${command} is not on PATH`), { code: "ENOENT" });
42
+ const { stdout, stderr } = await runExecutable(file, args, env === undefined ? {} : { env });
43
+ return `${stdout}${stderr}`.trim();
44
+ }
37
45
  const { stdout, stderr } = await run(command, args, env === undefined ? {} : { env });
38
46
  return `${stdout}${stderr}`.trim();
39
47
  };
@@ -128,7 +136,7 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
128
136
  // own environment must not find a different client, or none.
129
137
  Promise.resolve(probe(adapter.client.command,
130
138
  adapter.client.versionArgs ?? ["--version"],
131
- { env: context?.env === undefined ? undefined : { ...process.env, ...context.env } })),
139
+ { env: context?.env === undefined ? undefined : mergeEnv(process.env, context.env) })),
132
140
  probeTimeoutMs, `${adapter.id} version probe`);
133
141
  if (typeof output === "string" && output !== "") {
134
142
  entry.present = true;
@@ -167,6 +175,8 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
167
175
  if (detected.inboundDelivery) entry.inboundDelivery = detected.inboundDelivery;
168
176
  // Whether the client asks before each ACC command, and what would stop it.
169
177
  if (detected.commandApproval) entry.commandApproval = detected.commandApproval;
178
+ // Whether the command that starts live delivery asks first.
179
+ if (detected.relayApproval) entry.relayApproval = detected.relayApproval;
170
180
  if (detected.nativeSetup) entry.nativeSetup = detected.nativeSetup;
171
181
  entry.diagnostics = [...(detected.diagnostics ?? [])];
172
182
  // What a person has to do, as opposed to what is true. Adapters that
@@ -0,0 +1,5 @@
1
+ // What a hook reads from the installation record, without the installer: the
2
+ // package index loads detection, planning and application, which a hook never
3
+ // runs, on every turn.
4
+ export { loadOwnership } from "./ownership.mjs";
5
+ export { readInstalledLivePolicyState } from "./live-policy.mjs";
@@ -1,7 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
- import { access, constants } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
 
4
+ import { resolveExecutable as resolveOnPath } from "@agents-can-communicate/adapter-sdk";
5
+
5
6
  import { uninstallShellBootstrap } from "./shell-bootstrap.mjs";
6
7
  import { LIVE_POLICIES, livePolicyOf } from "./live-policy.mjs";
7
8
 
@@ -22,19 +23,10 @@ export const shimDirFor = stateRoot => path.join(stateRoot, "bin");
22
23
 
23
24
  // The vendor executable to probe: the first executable on PATH outside the
24
25
  // excluded directories, so a kept 0.7.x shim is never probed as the client.
25
- export async function resolveExecutable(command, { pathEnv = "", exclude = [] } = {}) {
26
- const excluded = exclude.filter(Boolean).map(directory => path.resolve(directory));
27
- for (const directory of String(pathEnv).split(path.delimiter).filter(Boolean)) {
28
- if (excluded.includes(path.resolve(directory))) continue;
29
- const candidate = path.join(directory, command);
30
- try {
31
- await access(candidate, constants.X_OK);
32
- return candidate;
33
- } catch {
34
- // not here; keep walking PATH
35
- }
36
- }
37
- return null;
26
+ // Windows adds an extension from PATHEXT; see the SDK's resolveExecutable.
27
+ export async function resolveExecutable(command, { pathEnv = "", exclude = [], pathExt } = {}) {
28
+ return resolveOnPath(command, { pathEnv, exclude,
29
+ ...(pathExt === undefined ? {} : { pathExt }) });
38
30
  }
39
31
 
40
32
  const defaultExec = (executable, args) => new Promise((resolve, reject) => {
@@ -29,6 +29,7 @@ export async function fingerprint(file) {
29
29
  }
30
30
 
31
31
  const CLIENT_ORPHAN_MARKER = ".orphaned_at";
32
+ const CLIENT_IN_USE_DIR = ".in_use";
32
33
 
33
34
  /**
34
35
  * Fingerprint of a whole directory ACC created.
@@ -50,8 +51,11 @@ export async function treeFingerprint(root) {
50
51
  // Claude Code marks a plugin cache version its registry no longer names with
51
52
  // `.orphaned_at`, and removes the marker again if the version comes back. That
52
53
  // is the client's own bookkeeping inside a copy ACC keeps on purpose, never an
53
- // edit, and ACC never writes a file by that name.
54
- const files = entries.filter(entry => entry.isFile() && entry.name !== CLIENT_ORPHAN_MARKER)
54
+ // edit, and ACC never writes a file by that name. A running session's
55
+ // `.in_use/<pid>` is the same kind of bookkeeping (2.1.286): after an update it
56
+ // stood in the previous version ACC keeps on purpose.
57
+ const files = entries.filter(entry => entry.isFile() && entry.name !== CLIENT_ORPHAN_MARKER
58
+ && path.basename(entry.parentPath ?? entry.path) !== CLIENT_IN_USE_DIR)
55
59
  .map(entry => path.relative(root, path.join(entry.parentPath ?? entry.path, entry.name)))
56
60
  .sort();
57
61
  const hash = createHash("sha256");
@@ -78,6 +78,20 @@ export function planInstallation({ adapters, detected, context, action = "instal
78
78
  + `if it is, wire it with --adapter ${entry.adapterId}` });
79
79
  continue;
80
80
  }
81
+ // An uninstall takes back what this data home installed. A client that is
82
+ // here but that this home has no record of was wired by another ACC data
83
+ // home - the operator's own, beside an isolated test home - or before this
84
+ // home kept records, and removing it unasked took the operator's wiring
85
+ // with it (#253). Naming the client still removes it.
86
+ if (action === "uninstall" && entry.present && recordedById.get(entry.adapterId) === undefined
87
+ && !askedFor.has(entry.adapterId)) {
88
+ // Also what a second uninstall says: by then this home has no record left.
89
+ skipped.push({ adapterId: entry.adapterId,
90
+ reason: `${entry.displayName ?? entry.adapterId} has no install record in this ACC data home, `
91
+ + "so ACC leaves it as it is; to remove ACC wiring another data home put there, run "
92
+ + `acc uninstall --adapter ${entry.adapterId}` });
93
+ continue;
94
+ }
81
95
  // An adapter can refuse itself for a reason that is nobody's fault and that
82
96
  // no file on disk shows: Antigravity CLI cannot be wired until somebody
83
97
  // picks between a machine-wide and a per-workspace hook registration. It is
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -53,8 +53,12 @@ function assertRoot(root, source) {
53
53
  if (typeof root !== "string" || root === "") {
54
54
  data("each workspace root must be a non-empty string", { source, root });
55
55
  }
56
- // Absolute is one machine's layout committed to a shared repository.
57
- if (root.startsWith("/") || /^[A-Za-z]:[\\/]/.test(root)) {
56
+ // Absolute is one machine's layout committed to a shared repository. On
57
+ // Windows a leading separator is rooted on the current drive, `\\host\share`
58
+ // reaches another machine and `D:x` is relative to another drive's current
59
+ // directory, so a drive letter or a leading separator is refused on every
60
+ // platform: a config that is harmless on POSIX travels to Windows unchanged.
61
+ if (/^[\\/]/.test(root) || /^[A-Za-z]:/.test(root)) {
58
62
  data("a workspace root must be relative to the config", { source, root });
59
63
  }
60
64
  // Checked on the segments rather than the string: `packages/../../escape`
@@ -14,6 +14,10 @@
14
14
  */
15
15
  const GLOB = "/**";
16
16
 
17
+ // A Windows agent separates segments with a backslash. Only the path part of a
18
+ // `file:` resource is converted; see normaliseResource.
19
+ const slashes = value => value.replaceAll("\\", "/");
20
+
17
21
  function normalisePath(value) {
18
22
  const segments = [];
19
23
  for (const segment of value.split("/")) {
@@ -44,7 +48,7 @@ function normalisePath(value) {
44
48
  */
45
49
  export function assertMatchableResource(resource, fail) {
46
50
  if (typeof resource !== "string" || !resource.startsWith("file:")) return resource;
47
- const rest = resource.slice("file:".length);
51
+ const rest = slashes(resource.slice("file:".length));
48
52
  if (rest.endsWith("/") && rest !== "/") {
49
53
  fail(`${resource} names a directory; claim ${resource}** to cover what is in it`);
50
54
  }
@@ -61,7 +65,7 @@ export function normaliseResource(resource) {
61
65
  const colon = resource.indexOf(":");
62
66
  if (colon === -1 || resource.slice(0, colon) !== "file") return resource;
63
67
 
64
- const rest = resource.slice(colon + 1);
68
+ const rest = slashes(resource.slice(colon + 1));
65
69
  const glob = rest.endsWith(GLOB);
66
70
  const body = glob ? rest.slice(0, -GLOB.length) : rest;
67
71
  // An absolute path keeps its leading slash: it names a different thing from
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
 
5
5
  import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protocol";
6
6
 
7
- import { encode, publishAtomic } from "./atomic-json.mjs";
7
+ import { encode, nameCommittedLater, publishAtomic } from "./atomic-json.mjs";
8
8
  import { withRegularNoFollow } from "./safe-file.mjs";
9
9
 
10
10
  const ACTIVE_JOURNAL_VERSION = 2;
@@ -181,7 +181,10 @@ export async function initialiseActiveJournal(paths, options) {
181
181
  const current = await readCurrent(paths, options.root);
182
182
  if (current !== null) return current.record;
183
183
  try {
184
- await publishAtomic(activeJournalPath(paths, 0), encodeAuthority(genesis()), options);
184
+ // The first record any write publishes commits this name on Windows; a
185
+ // crash before it leaves no pointer, which the next open initialises.
186
+ await publishAtomic(activeJournalPath(paths, 0), encodeAuthority(genesis()),
187
+ { ...options, durability: nameCommittedLater(options.platform) });
185
188
  } catch (error) {
186
189
  if (error.code !== EXIT.CONFLICT) throw error;
187
190
  }