agents-can-communicate 0.8.5 → 0.9.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 (133) 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 +15 -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 +26 -5
  13. package/docs/TROUBLESHOOTING.md +63 -8
  14. package/docs/UPGRADING.md +33 -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/src/adapter.mjs +13 -2
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
  42. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
  43. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  44. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
  45. package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +18 -9
  48. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
  49. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
  50. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
  51. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
  56. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  57. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
  58. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
  60. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
  61. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  62. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
  63. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
  64. package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
  65. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
  66. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
  67. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
  68. package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
  69. package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
  70. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
  73. package/node_modules/@agents-can-communicate/cli/package.json +2 -1
  74. package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
  75. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
  76. package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
  77. package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
  78. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
  79. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
  80. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
  81. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
  82. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
  83. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
  84. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
  85. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
  86. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
  87. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
  88. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
  89. package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
  90. package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
  91. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
  92. package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
  93. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
  95. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
  96. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
  97. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
  98. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  99. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  100. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
  101. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
  102. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
  103. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
  104. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
  105. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
  106. package/node_modules/@agents-can-communicate/installer/package.json +3 -2
  107. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
  108. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
  109. package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
  110. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
  111. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
  112. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  113. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  114. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +6 -2
  115. package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
  116. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  117. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
  118. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
  119. package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
  120. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
  121. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
  122. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
  123. package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
  124. package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
  129. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
  130. package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
  131. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
  132. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
  133. package/package.json +1 -5
@@ -100,6 +100,17 @@ export function createSessionOpener(ports, locate) {
100
100
  };
101
101
  if (await isMaterialised(store, workspaceId)) return openDurable();
102
102
 
103
+ // Another live session already here makes this the second, which
104
+ // materialises the workspace: this one opens in that same transaction
105
+ // rather than as an ephemeral copy to be copied and retired at once. The
106
+ // look is taken without the lock; materialise asks again under it.
107
+ const joining = (await store.ephemeral.list("session")).some(item => item.state === "open"
108
+ && item.sessionId !== sessionId);
109
+ if (joining && await materialise(ports, { workspaceId, descriptor: input.descriptor,
110
+ reason: "second_live_session", opening: { participant, session, assertAvailable } })) {
111
+ return session;
112
+ }
113
+
103
114
  // Preparing a participant must not rename one that already exists, or
104
115
  // recreate an ephemeral copy after promotion has taken ownership.
105
116
  await store.ephemeral.update("participant", participant.participantId, async current =>
@@ -92,8 +92,14 @@ export function createSessionService(ports) {
92
92
  // presence transitions surface through cursor sync. Read and validate under
93
93
  // the writer lock, so a pending heartbeat cannot restore an old generation.
94
94
  // Promoted copies may await cleanup; their durable record already owns writes.
95
- const ephemeral = await store.ephemeral.update("session", sessionId, async current =>
96
- current !== null && !await isMaterialised(store, current.workspaceId) ? beat(current) : null);
95
+ // The update below writes only a lone session's ephemeral record. A session
96
+ // with none, or in a materialised workspace, is durable - and stays so: a
97
+ // workspace never returns to ephemeral - so it goes straight to the durable
98
+ // record rather than taking the writer lock to find nothing.
99
+ const seen = await store.ephemeral.get("session", sessionId);
100
+ const ephemeral = seen === null || await isMaterialised(store, seen.workspaceId) ? null
101
+ : await store.ephemeral.update("session", sessionId, async current =>
102
+ current !== null && !await isMaterialised(store, current.workspaceId) ? beat(current) : null);
97
103
  if (ephemeral !== null) return ephemeral;
98
104
  return store.transaction(tx => {
99
105
  const beaten = beat(tx.get("session", sessionId));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/hook-runner",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import { splitWindowsCommandLine } from "@agents-can-communicate/adapter-sdk";
2
2
 
3
3
  // Deep enough for a client that wraps its hook in a shell and a launcher, short
4
4
  // enough that a table which disagrees with itself cannot spin.
@@ -8,11 +8,35 @@ const MAX_HOPS = 16;
8
8
  // interpreter. Gemini CLI is one, measured on 0.60.0. For these, the script the
9
9
  // interpreter was given names the client.
10
10
  const SCRIPT_HOSTS = new Set(["node"]);
11
+ // What scriptsOf reports for a script inside the client's own package.
12
+ const CLIENT_PACKAGE = Symbol("client package");
11
13
  const SCRIPT_EXTENSION = /\.[mc]?js$/;
12
14
 
15
+ // Either separator: `ps` names POSIX paths, Win32_Process names Windows ones,
16
+ // and the host running the walk may be either.
17
+ const basename = value => value.split(/[\\/]/).at(-1);
18
+
19
+ /** An image name as the client's command: Windows adds `.exe` in any case. */
20
+ function imageName(comm) {
21
+ const name = basename(comm);
22
+ return /\.exe$/i.test(name) ? name.slice(0, -4).toLowerCase() : name;
23
+ }
24
+
25
+ /** The words of a command line: Windows quotes paths with spaces. */
26
+ const wordsOf = entry => (/\.exe$/i.test(entry.comm) ? splitWindowsCommandLine(entry.args)
27
+ : entry.args.trim().split(/\s+/));
28
+
13
29
  /** What the script a script host runs could be called, without extensions. */
14
- function scriptsOf(entry) {
15
- if (!SCRIPT_HOSTS.has(path.basename(entry.comm)) || typeof entry.args !== "string") return [];
30
+ function scriptsOf(entry, clientPackage) {
31
+ if (!SCRIPT_HOSTS.has(imageName(entry.comm)) || typeof entry.args !== "string") return [];
32
+ if (/\.exe$/i.test(entry.comm)) {
33
+ // Windows: the client is node.exe running its package's entry file, whose
34
+ // own name (`index.js`) says nothing; the package directory does.
35
+ const script = wordsOf(entry).slice(1).find(word => !word.startsWith("-"));
36
+ const inPackage = typeof clientPackage === "string" && typeof script === "string"
37
+ && script.replaceAll("\\", "/").includes(`/node_modules/${clientPackage}/`);
38
+ return inPackage ? [CLIENT_PACKAGE] : [];
39
+ }
16
40
  // `args` starts with the interpreter as it was invoked, which is the comm
17
41
  // itself on macOS; past it, the first word that is no option is the script -
18
42
  // unless an option written without `=` came right before it, since that word
@@ -26,7 +50,7 @@ function scriptsOf(entry) {
26
50
  for (const word of rest.trim().split(/\s+/)) {
27
51
  if (word === "") continue;
28
52
  if (word.startsWith("-")) { afterBareOption = !word.includes("="); continue; }
29
- candidates.push(path.basename(word).replace(SCRIPT_EXTENSION, ""));
53
+ candidates.push(basename(word).replace(SCRIPT_EXTENSION, ""));
30
54
  if (!afterBareOption) break;
31
55
  afterBareOption = false;
32
56
  }
@@ -44,16 +68,51 @@ function scriptsOf(entry) {
44
68
  *
45
69
  * Null is a first-class answer: it means judge this session by age alone.
46
70
  */
47
- export function resolveClientPid({ table, from, command, maxHops = MAX_HOPS }) {
71
+ export function resolveClientPid({ table, from, command, clientPackage, maxHops = MAX_HOPS }) {
72
+ return resolveClient({ table, from, command, clientPackage, maxHops })?.pid ?? null;
73
+ }
74
+
75
+ // What an adapter's identifyClientProcess said about one ancestor, or null.
76
+ // It reads a vendor's command line, so a throw is a "no", never a failed hook.
77
+ function recognised(identify, entry) {
78
+ if (typeof identify !== "function") return null;
79
+ try {
80
+ const answer = identify({ comm: entry.comm, args: typeof entry.args === "string" ? entry.args : "" });
81
+ return answer !== null && typeof answer === "object" ? answer : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * The client this hook runs under: its pid, its process-table entry, and what
89
+ * the adapter recognised it as - another product it serves, and that
90
+ * product's version when the process names it. Null when nobody knows.
91
+ *
92
+ * At each ancestor the adapter's own identifier is asked first; the declared
93
+ * command still matches whatever it does not recognise. Antigravity 2.0 runs
94
+ * hooks under a language server that names its version on its command line,
95
+ * where Antigravity CLI runs them under `agy`.
96
+ */
97
+ export function resolveClient({ table, from, command, clientPackage, identify,
98
+ maxHops = MAX_HOPS }) {
48
99
  const seen = new Set();
49
100
  let current = from;
50
101
  for (let hop = 0; hop < maxHops; hop += 1) {
51
102
  const entry = table.get(current);
52
103
  if (entry === undefined || seen.has(current)) return null;
53
104
  seen.add(current);
105
+ const product = recognised(identify, entry);
106
+ if (product !== null) {
107
+ return { pid: current, entry, certificationName: product.certificationName,
108
+ version: product.version };
109
+ }
54
110
  // `ps` reports some entries bare (`claude`) and some with a path
55
111
  // (`/bin/zsh`), so the comparison has to be on the basename.
56
- if (path.basename(entry.comm) === command || scriptsOf(entry).includes(command)) return current;
112
+ if (imageName(entry.comm) === command
113
+ || scriptsOf(entry, clientPackage).some(name => name === command || name === CLIENT_PACKAGE)) {
114
+ return { pid: current, entry, certificationName: undefined, version: undefined };
115
+ }
57
116
  if (entry.ppid === current || entry.ppid <= 1) return null;
58
117
  current = entry.ppid;
59
118
  }
@@ -1,4 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { readFile as readText } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { resolveExecutable, runExecutable } from "@agents-can-communicate/adapter-sdk";
2
6
 
3
7
  const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b|$)/;
4
8
 
@@ -8,6 +12,7 @@ export function parseClientVersion(output) {
8
12
 
9
13
  /** Probe the client binary once when its real session starts. */
10
14
  export function probeClientVersion(adapter, { timeoutMs = 1_000 } = {}) {
15
+ if (process.platform === "win32") return probeOnWindows(adapter, timeoutMs);
11
16
  return new Promise(resolve => {
12
17
  execFile(adapter.client.command, adapter.client.versionArgs ?? ["--version"], {
13
18
  timeout: timeoutMs,
@@ -19,3 +24,78 @@ export function probeClientVersion(adapter, { timeoutMs = 1_000 } = {}) {
19
24
  });
20
25
  });
21
26
  }
27
+
28
+ /**
29
+ * The executable a client process runs, when `ps` names it by absolute path and
30
+ * that path is the client's command: a desktop app starts its own build by full
31
+ * path (Claude.app's claude, ChatGPT.app's codex), and asking that file is
32
+ * asking the session's own build. Null otherwise, and always on Windows, whose
33
+ * process names carry no path.
34
+ */
35
+ export function ownExecutable(entry, command) {
36
+ if (process.platform === "win32" || typeof entry?.comm !== "string") return null;
37
+ return path.isAbsolute(entry.comm) && path.basename(entry.comm) === command ? entry.comm : null;
38
+ }
39
+
40
+ /** That executable's `--version`, parsed like the PATH probe's; null on any failure. */
41
+ export function probeExecutableVersion(file, versionArgs = ["--version"], { timeoutMs = 1_000 } = {}) {
42
+ return new Promise(resolve => {
43
+ execFile(file, versionArgs, { timeout: timeoutMs, killSignal: "SIGKILL", windowsHide: true },
44
+ (error, stdout, stderr) => resolve(error === null ? parseClientVersion(`${stdout}${stderr}`) : null));
45
+ });
46
+ }
47
+
48
+ // A package script the npm .cmd shim runs: `"%dp0%\<path inside the prefix>"`.
49
+ const SHIM_TARGET = /"%dp0%\\([^"%]+\.(?:c|m)?js)"/gi;
50
+ const DEPTH = 6;
51
+
52
+ /**
53
+ * The version of the package an npm .cmd shim runs, or null when the file is no
54
+ * such shim. npm writes a .cmd per bin that runs node on the package's script;
55
+ * the package whose `bin` names that script is the client, and its version is
56
+ * what the client's --version prints - without starting cmd.exe, node and, for
57
+ * Codex, the native binary behind it.
58
+ */
59
+ export async function npmShimVersion(file, { readFile = readText } = {}) {
60
+ try {
61
+ const targets = [...String(await readFile(file, "utf8")).matchAll(SHIM_TARGET)];
62
+ if (targets.length !== 1) return null;
63
+ const script = path.win32.join(path.win32.dirname(file), targets[0][1]);
64
+ let directory = path.win32.dirname(script);
65
+ for (let depth = 0; depth < DEPTH; depth += 1) {
66
+ const manifest = await readFile(path.win32.join(directory, "package.json"), "utf8")
67
+ .then(JSON.parse, error => (error.code === "ENOENT" ? null : Promise.reject(error)));
68
+ if (manifest !== null) {
69
+ const bins = typeof manifest.bin === "string" ? [manifest.bin] : Object.values(manifest.bin ?? {});
70
+ const runs = bins.some(bin => typeof bin === "string"
71
+ && path.win32.resolve(directory, bin).toLowerCase() === path.win32.resolve(script).toLowerCase());
72
+ return runs ? parseClientVersion(String(manifest.version)) : null;
73
+ }
74
+ const parent = path.win32.dirname(directory);
75
+ if (parent === directory) return null;
76
+ directory = parent;
77
+ }
78
+ return null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ // Windows finds `codex.cmd` or `claude.exe`, never the bare name, and starts a
85
+ // .cmd only through cmd.exe.
86
+ async function probeOnWindows(adapter, timeoutMs) {
87
+ const started = Date.now();
88
+ try {
89
+ const file = await resolveExecutable(adapter.client.command);
90
+ if (file === null) return null;
91
+ if (/\.cmd$/i.test(file)) {
92
+ const packaged = await npmShimVersion(file);
93
+ if (packaged !== null) return packaged;
94
+ }
95
+ const { stdout, stderr } = await runExecutable(file, adapter.client.versionArgs ?? ["--version"],
96
+ { timeout: Math.max(1, timeoutMs - (Date.now() - started)), killSignal: "SIGKILL" });
97
+ return parseClientVersion(`${stdout}${stderr}`);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
@@ -1,5 +1,5 @@
1
1
  import { storeNativeAttempt } from "@agents-can-communicate/adapter-sdk";
2
- import { readInstalledLivePolicyState } from "@agents-can-communicate/installer";
2
+ import { readInstalledLivePolicyState } from "@agents-can-communicate/installer/hook-support";
3
3
 
4
4
  import { establishNativeBinding } from "./native-binding.mjs";
5
5
 
@@ -1,11 +1,39 @@
1
- import { assertPortableId } from "@agents-can-communicate/protocol";
1
+ import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protocol";
2
2
 
3
- const shellQuote = value => `'${value.replaceAll("'", "'\\''")}'`;
3
+ const posixQuote = value => `'${value.replaceAll("'", "'\\''")}'`;
4
+ // The model appends the header in whatever shell its client gives it, and on
5
+ // Windows that can be cmd.exe, PowerShell or Git Bash. Double quotes are the one
6
+ // quoting all three read alike, for a value with none of what one of them acts
7
+ // on inside double quotes: a double quote - PowerShell also ends a string at
8
+ // U+201C to U+201E, which NTFS allows in names - $ and a backtick, which
9
+ // PowerShell and bash expand, and %, which cmd expands. Single quotes are no
10
+ // answer: cmd takes them as part of the path. Such a value is refused rather
11
+ // than handed to a model that would read it as something else.
12
+ const DOUBLE_QUOTE_BREAKERS = /["\u201C-\u201E$`%]/;
13
+ const windowsQuote = value => {
14
+ if (!DOUBLE_QUOTE_BREAKERS.test(value)) return `"${value}"`;
15
+ throw new AccError(EXIT.USAGE, `${value} has a double quote, $, \` or % in it, which no quoting `
16
+ + "reads alike in cmd, PowerShell and Git Bash, and ACC's hooks will not hand the model a "
17
+ + "command line it would read differently; rename the directory",
18
+ { value, reasonCode: "workspace_path_unquotable" });
19
+ };
4
20
 
5
- export const ownerHeader = (binding, cwd, workspaceRef) => "ACC CLI (append): --session "
6
- + assertPortableId(binding.accSessionId, "sessionId") + " --generation "
7
- + assertPortableId(binding.generation, "generation") + " --cwd " + shellQuote(cwd)
8
- + (workspaceRef === undefined ? "" : " --workspace " + shellQuote(workspaceRef));
21
+ // Before anything opens: a session that attached and could then never be told
22
+ // its CLI arguments would look live to its peers, and every one of its turns
23
+ // would fail before their messages reached it.
24
+ export function assertOwnerQuotable(cwd, workspaceRef, platform = process.platform) {
25
+ if (platform !== "win32") return;
26
+ windowsQuote(cwd);
27
+ if (workspaceRef !== undefined) windowsQuote(workspaceRef);
28
+ }
29
+
30
+ export const ownerHeader = (binding, cwd, workspaceRef, platform = process.platform) => {
31
+ const quote = platform === "win32" ? windowsQuote : posixQuote;
32
+ return "ACC CLI (append): --session "
33
+ + assertPortableId(binding.accSessionId, "sessionId") + " --generation "
34
+ + assertPortableId(binding.generation, "generation") + " --cwd " + quote(cwd)
35
+ + (workspaceRef === undefined ? "" : " --workspace " + quote(workspaceRef));
36
+ };
9
37
 
10
38
  export function ownerOnlyOutcome(inject, owner, budgetBytes) {
11
39
  if (Buffer.byteLength(owner, "utf8") > budgetBytes) {
@@ -1,50 +1,3 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
-
4
- const run = promisify(execFile);
5
-
6
- // A hook runs in front of someone's turn. Reading the table is worth a few
7
- // hundred milliseconds once per session and nothing at all if it is slow.
8
- const DEFAULT_TIMEOUT_MS = 1_000;
9
-
10
- const LINE = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/;
11
- // A pid, then the whole rest of the line: a command line has spaces anywhere.
12
- const ARGS_LINE = /^\s*(\d+)\s+(.+?)\s*$/;
13
- // Command lines are long; measured at 231 KB for 1,137 processes on a desktop
14
- // Mac, a quarter of execFile's default buffer.
15
- const ARGS_BUFFER = 16 * 1024 * 1024;
16
-
17
- /**
18
- * Every process on this machine, as pid -> parent, executable and, when it
19
- * could be read, command line.
20
- *
21
- * Returns an empty map rather than throwing when the platform has no `ps`
22
- * (Windows) or the call fails. An empty table resolves no client, which is the
23
- * "nobody knows" answer the caller already handles. The command lines are read
24
- * at the same time and within the same timeout; they only name a client that
25
- * is a script, so when that read fails every entry keeps what it had before.
26
- */
27
- export async function readProcessTable({ run: exec = run,
28
- timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
29
- const options = { timeout: timeoutMs, killSignal: "SIGKILL" };
30
- const [tree, commandLines] = await Promise.all([
31
- exec("ps", ["-o", "pid=,ppid=,comm=", "-A"], options).then(({ stdout }) => stdout, () => null),
32
- exec("ps", ["-A", "-o", "pid=,args="], { ...options, maxBuffer: ARGS_BUFFER })
33
- .then(({ stdout }) => stdout, () => ""),
34
- ]);
35
- if (typeof tree !== "string") return new Map();
36
- const args = new Map();
37
- for (const line of String(commandLines).split("\n")) {
38
- const match = ARGS_LINE.exec(line);
39
- if (match !== null) args.set(Number(match[1]), match[2]);
40
- }
41
- const table = new Map();
42
- for (const line of tree.split("\n")) {
43
- const match = LINE.exec(line);
44
- if (match === null) continue;
45
- const pid = Number(match[1]);
46
- table.set(pid, { ppid: Number(match[2]), comm: match[3],
47
- ...(args.has(pid) ? { args: args.get(pid) } : {}) });
48
- }
49
- return table;
50
- }
1
+ // The table lives in the adapter SDK, which adapters that read a process's
2
+ // arguments share with the hook runner; this keeps the runner's own name.
3
+ export { readProcessTable } from "@agents-can-communicate/adapter-sdk";