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.
- package/README.md +11 -7
- package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
- package/bin/entrypoints/acc-hook.mjs +33 -19
- package/bin/entrypoints/hook-output.mjs +14 -5
- package/docs/ADAPTER_AUTHORING.md +29 -0
- package/docs/CAPABILITIES.md +7 -6
- package/docs/CLI.md +5 -0
- package/docs/CONFIGURATION.md +15 -10
- package/docs/DESIGN_DECISIONS.md +0 -2
- package/docs/GETTING_STARTED.md +39 -6
- package/docs/HOW_IT_WORKS.md +13 -8
- package/docs/SECURITY_MODEL.md +26 -5
- package/docs/TROUBLESHOOTING.md +63 -8
- package/docs/UPGRADING.md +33 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +13 -2
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
- package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +18 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
- package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
- package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
- package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
- package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
- package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
- package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
- package/node_modules/@agents-can-communicate/installer/package.json +3 -2
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
- package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +6 -2
- package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
- package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
- package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
- package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
- package/package.json +1 -5
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
// A hook runs in front of someone's turn. Reading the table is worth a few
|
|
9
|
+
// hundred milliseconds once per session and nothing at all if it is slow.
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 1_000;
|
|
11
|
+
|
|
12
|
+
const LINE = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/;
|
|
13
|
+
// A pid, then the whole rest of the line: a command line has spaces anywhere.
|
|
14
|
+
const ARGS_LINE = /^\s*(\d+)\s+(.+?)\s*$/;
|
|
15
|
+
// Command lines are long; measured at 231 KB for 1,137 processes on a desktop
|
|
16
|
+
// Mac, a quarter of execFile's default buffer.
|
|
17
|
+
const ARGS_BUFFER = 16 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
// Deep enough for a client that wraps its hook in a shell and a launcher.
|
|
20
|
+
const WINDOWS_HOPS = 8;
|
|
21
|
+
|
|
22
|
+
async function posixTable({ run, timeoutMs }) {
|
|
23
|
+
const options = { timeout: timeoutMs, killSignal: "SIGKILL", windowsHide: true };
|
|
24
|
+
const [tree, commandLines] = await Promise.all([
|
|
25
|
+
run("ps", ["-o", "pid=,ppid=,comm=", "-A"], options).then(({ stdout }) => stdout, () => null),
|
|
26
|
+
run("ps", ["-A", "-o", "pid=,args="], { ...options, maxBuffer: ARGS_BUFFER })
|
|
27
|
+
.then(({ stdout }) => stdout, () => ""),
|
|
28
|
+
]);
|
|
29
|
+
if (typeof tree !== "string") return new Map();
|
|
30
|
+
const args = new Map();
|
|
31
|
+
for (const line of String(commandLines).split("\n")) {
|
|
32
|
+
const match = ARGS_LINE.exec(line);
|
|
33
|
+
if (match !== null) args.set(Number(match[1]), match[2]);
|
|
34
|
+
}
|
|
35
|
+
const table = new Map();
|
|
36
|
+
for (const line of tree.split("\n")) {
|
|
37
|
+
const match = LINE.exec(line);
|
|
38
|
+
if (match === null) continue;
|
|
39
|
+
const pid = Number(match[1]);
|
|
40
|
+
table.set(pid, { ppid: Number(match[2]), comm: match[3],
|
|
41
|
+
...(args.has(pid) ? { args: args.get(pid) } : {}) });
|
|
42
|
+
}
|
|
43
|
+
return table;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Windows has no `ps` and no longer ships `wmic`. Win32_Process through CIM
|
|
47
|
+
// answers pid, parent, creation time and command line. A full scan took 1.07 s
|
|
48
|
+
// in Windows PowerShell 5.1 on a windows-latest runner, so the script asks only
|
|
49
|
+
// for the chain it needs, one filtered query per hop. The creation time goes
|
|
50
|
+
// out as a string: a FILETIME is larger than a double holds exactly.
|
|
51
|
+
const chainScript = (from, hops) => `$ErrorActionPreference = 'Stop'
|
|
52
|
+
$next = ${Number(from)}
|
|
53
|
+
$chain = New-Object System.Collections.ArrayList
|
|
54
|
+
for ($hop = 0; $hop -lt ${Number(hops)}; $hop++) {
|
|
55
|
+
$p = Get-CimInstance Win32_Process -Filter "ProcessId=$next" -Property ProcessId,ParentProcessId,CreationDate,Name,CommandLine
|
|
56
|
+
if ($null -eq $p) { break }
|
|
57
|
+
[void]$chain.Add([pscustomobject]@{ pid = [int]$p.ProcessId; ppid = [int]$p.ParentProcessId; name = [string]$p.Name; start = $p.CreationDate.ToFileTimeUtc().ToString(); cmd = [string]$p.CommandLine })
|
|
58
|
+
if ($p.ParentProcessId -eq 0 -or $p.ParentProcessId -eq $next) { break }
|
|
59
|
+
$next = [int]$p.ParentProcessId
|
|
60
|
+
}
|
|
61
|
+
ConvertTo-Json -Compress -InputObject @($chain)`;
|
|
62
|
+
|
|
63
|
+
// PowerShell by absolute path only, so a PATH entry cannot stand in for it.
|
|
64
|
+
// PowerShell 7 is not part of Windows; where it is installed it answers faster
|
|
65
|
+
// (0.37 s against 1.07 s for a full scan, measured), and a missing one fails to
|
|
66
|
+
// spawn at once.
|
|
67
|
+
function powershellCandidates(env) {
|
|
68
|
+
const root = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
|
|
69
|
+
const programs = env.ProgramFiles ?? env.PROGRAMFILES ?? "C:\\Program Files";
|
|
70
|
+
return [path.win32.join(programs, "PowerShell", "7", "pwsh.exe"),
|
|
71
|
+
path.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Windows Script Host reads the same chain through WMI in a quarter of the
|
|
75
|
+
// time: 145 ms against PowerShell's 580, and 258 against 1,080 with four hooks
|
|
76
|
+
// at once (windows-latest, measured), because it starts no .NET runtime. A
|
|
77
|
+
// machine can switch it off by policy; PowerShell then reads the chain.
|
|
78
|
+
const WMI_CHAIN = fileURLToPath(new URL("./windows-process-chain.wsf", import.meta.url));
|
|
79
|
+
|
|
80
|
+
// WMI's `yyyymmddHHMMSS.ffffff+UUU` - offset in minutes - as a FILETIME string,
|
|
81
|
+
// the form the PowerShell script prints.
|
|
82
|
+
function wmiFileTime(value) {
|
|
83
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{6})([+-])(\d{3})$/.exec(value ?? "");
|
|
84
|
+
if (match === null) return undefined;
|
|
85
|
+
const [, year, month, day, hour, minute, second, micros, sign, offset] = match;
|
|
86
|
+
const local = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour),
|
|
87
|
+
Number(minute), Number(second));
|
|
88
|
+
const utc = local - (sign === "+" ? 1 : -1) * Number(offset) * 60_000;
|
|
89
|
+
return String((BigInt(utc) + 11_644_473_600_000n) * 10_000n + BigInt(micros) * 10n);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const chainOf = stdout => {
|
|
93
|
+
const parsed = JSON.parse(stdout);
|
|
94
|
+
return (Array.isArray(parsed) ? parsed : [parsed]).map(hop => (typeof hop?.created === "string"
|
|
95
|
+
? { ...hop, start: wmiFileTime(hop.created) } : hop));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
async function windowsChain({ from, hops, run, timeoutMs, env }) {
|
|
99
|
+
const deadline = Date.now() + timeoutMs;
|
|
100
|
+
const options = () => ({ timeout: Math.max(1, deadline - Date.now()), killSignal: "SIGKILL",
|
|
101
|
+
windowsHide: true, maxBuffer: ARGS_BUFFER });
|
|
102
|
+
const root = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
|
|
103
|
+
try {
|
|
104
|
+
const { stdout } = await run(path.win32.join(root, "System32", "cscript.exe"),
|
|
105
|
+
["//Nologo", WMI_CHAIN, String(Number(from)), String(Number(hops))], options());
|
|
106
|
+
return chainOf(stdout);
|
|
107
|
+
} catch {
|
|
108
|
+
// Script Host is off or failed; PowerShell below.
|
|
109
|
+
}
|
|
110
|
+
const encoded = Buffer.from(chainScript(from, hops), "utf16le").toString("base64");
|
|
111
|
+
for (const shell of powershellCandidates(env)) {
|
|
112
|
+
if (Date.now() >= deadline) break;
|
|
113
|
+
try {
|
|
114
|
+
const { stdout } = await run(shell, ["-NoLogo", "-NoProfile", "-NonInteractive",
|
|
115
|
+
"-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], options());
|
|
116
|
+
return chainOf(stdout);
|
|
117
|
+
} catch {
|
|
118
|
+
// Try the next shell; none left is the empty answer below.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Windows: one process - pid, parent, image name, creation time as a FILETIME
|
|
126
|
+
* string (to the microsecond, as WMI keeps it) and command line - or null when
|
|
127
|
+
* it cannot be read.
|
|
128
|
+
*/
|
|
129
|
+
export async function readWindowsProcess(pid, { run = execFileAsync, timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
130
|
+
env = process.env } = {}) {
|
|
131
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
132
|
+
const [found] = await windowsChain({ from: pid, hops: 1, run, timeoutMs, env });
|
|
133
|
+
return found?.pid === pid ? found : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function windowsTable({ from, run, timeoutMs, env }) {
|
|
137
|
+
const table = new Map();
|
|
138
|
+
let child = null;
|
|
139
|
+
for (const hop of await windowsChain({ from, hops: WINDOWS_HOPS, run, timeoutMs, env })) {
|
|
140
|
+
if (!Number.isSafeInteger(hop?.pid) || typeof hop.name !== "string") break;
|
|
141
|
+
// Windows never re-parents an orphan: its ppid can name a process that
|
|
142
|
+
// died and whose number was reused. A parent created after its child is
|
|
143
|
+
// exactly that, and nothing above it belongs to this chain.
|
|
144
|
+
if (child !== null && !(BigInt(hop.start ?? "0") <= BigInt(child.start ?? "0"))) break;
|
|
145
|
+
table.set(hop.pid, { ppid: hop.ppid, comm: hop.name,
|
|
146
|
+
...(typeof hop.cmd === "string" && hop.cmd !== "" ? { args: hop.cmd } : {}),
|
|
147
|
+
...(typeof hop.start === "string" ? { start: hop.start } : {}) });
|
|
148
|
+
child = hop;
|
|
149
|
+
}
|
|
150
|
+
return table;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Processes on this machine, as pid -> parent, executable and, when it could be
|
|
155
|
+
* read, command line.
|
|
156
|
+
*
|
|
157
|
+
* POSIX reads the whole table with `ps`. Windows reads the chain from `from`
|
|
158
|
+
* upward - the only part a hook asks about - with a creation time for each hop.
|
|
159
|
+
* A failed read is an empty map, never an error: an empty table resolves no
|
|
160
|
+
* client, which is the "nobody knows" answer every caller already handles.
|
|
161
|
+
*/
|
|
162
|
+
export async function readProcessTable({ platform = process.platform, from = process.pid,
|
|
163
|
+
run = execFileAsync, timeoutMs = DEFAULT_TIMEOUT_MS, env = process.env } = {}) {
|
|
164
|
+
if (platform === "win32") return windowsTable({ from, run, timeoutMs, env });
|
|
165
|
+
return posixTable({ run, timeoutMs });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The words one process was started with, or null when they cannot be read.
|
|
170
|
+
*
|
|
171
|
+
* POSIX splits `ps -o args=` on whitespace, as every caller did before. Windows
|
|
172
|
+
* keeps one command-line string per process, split here the way the C runtime
|
|
173
|
+
* splits it.
|
|
174
|
+
*/
|
|
175
|
+
export async function readProcessArgs(pid, { platform = process.platform, run = execFileAsync,
|
|
176
|
+
timeoutMs = DEFAULT_TIMEOUT_MS, env = process.env, ps = "ps" } = {}) {
|
|
177
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
178
|
+
if (platform === "win32") {
|
|
179
|
+
const [own] = await windowsChain({ from: pid, hops: 1, run, timeoutMs, env });
|
|
180
|
+
return own?.pid === pid && typeof own.cmd === "string" && own.cmd !== ""
|
|
181
|
+
? splitWindowsCommandLine(own.cmd) : null;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const { stdout } = await run(ps, ["-o", "args=", "-p", String(pid)],
|
|
185
|
+
{ timeout: timeoutMs, killSignal: "SIGKILL", windowsHide: true });
|
|
186
|
+
const words = stdout.trim().split(/\s+/).filter(Boolean);
|
|
187
|
+
return words.length > 0 ? words : null;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Split a Windows command line into argv the way CommandLineToArgvW does:
|
|
195
|
+
* whitespace separates words outside quotes; 2n backslashes before a quote are
|
|
196
|
+
* n backslashes and the quote toggles quoting; 2n+1 are n backslashes and a
|
|
197
|
+
* literal quote; backslashes elsewhere are literal.
|
|
198
|
+
*/
|
|
199
|
+
export function splitWindowsCommandLine(line) {
|
|
200
|
+
const words = [];
|
|
201
|
+
let word = "";
|
|
202
|
+
let started = false;
|
|
203
|
+
let quoted = false;
|
|
204
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
205
|
+
const char = line[index];
|
|
206
|
+
if (char === "\\") {
|
|
207
|
+
let count = 0;
|
|
208
|
+
while (line[index] === "\\") { count += 1; index += 1; }
|
|
209
|
+
if (line[index] === "\"") {
|
|
210
|
+
word += "\\".repeat(Math.floor(count / 2));
|
|
211
|
+
if (count % 2 === 1) word += "\"";
|
|
212
|
+
else quoted = !quoted;
|
|
213
|
+
} else {
|
|
214
|
+
word += "\\".repeat(count);
|
|
215
|
+
index -= 1;
|
|
216
|
+
}
|
|
217
|
+
started = true;
|
|
218
|
+
} else if (char === "\"") {
|
|
219
|
+
quoted = !quoted;
|
|
220
|
+
started = true;
|
|
221
|
+
} else if ((char === " " || char === "\t") && !quoted) {
|
|
222
|
+
if (started) words.push(word);
|
|
223
|
+
word = "";
|
|
224
|
+
started = false;
|
|
225
|
+
} else {
|
|
226
|
+
word += char;
|
|
227
|
+
started = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (started) words.push(word);
|
|
231
|
+
return words;
|
|
232
|
+
}
|
|
@@ -43,13 +43,17 @@ function assertBindingDeadline(deadlineAt) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function storeSessionBinding({ runtimeDir, harnessSessionId, accSessionId,
|
|
46
|
-
generation, clientVersion, platform, clientPid, deadlineAt, storeVersion, runtimeRoot }) {
|
|
46
|
+
generation, clientVersion, clientName, platform, clientPid, deadlineAt, storeVersion, runtimeRoot }) {
|
|
47
47
|
assertBindingDeadline(deadlineAt);
|
|
48
48
|
const file = fileFor(runtimeDir, harnessSessionId);
|
|
49
49
|
const record = { schemaVersion: SCHEMA_VERSION, harnessSessionId, accSessionId, generation };
|
|
50
50
|
assertIdentity(record);
|
|
51
51
|
await mkdir(path.dirname(file), { recursive: true });
|
|
52
52
|
if (typeof clientVersion === "string" && clientVersion !== "") record.clientVersion = clientVersion;
|
|
53
|
+
// The product the session runs in, when it is one of the adapter's variants
|
|
54
|
+
// rather than its own client: its evidence and its version line are its own.
|
|
55
|
+
// A reader that predates the field ignores it.
|
|
56
|
+
if (typeof clientName === "string" && clientName !== "") record.clientName = clientName;
|
|
53
57
|
if (typeof platform === "string" && platform !== "") record.platform = platform;
|
|
54
58
|
// The vendor process this session runs in, resolved once at SessionStart.
|
|
55
59
|
// A native endpoint is matched against it, so a binding without one can
|
|
@@ -106,6 +110,7 @@ export async function loadSessionBinding({ runtimeDir, harnessSessionId }) {
|
|
|
106
110
|
assertIdentity(record, harnessSessionId);
|
|
107
111
|
const binding = { accSessionId: record.accSessionId, generation: record.generation };
|
|
108
112
|
if (typeof record.clientVersion === "string") binding.clientVersion = record.clientVersion;
|
|
113
|
+
if (typeof record.clientName === "string" && record.clientName !== "") binding.clientName = record.clientName;
|
|
109
114
|
if (typeof record.platform === "string") binding.platform = record.platform;
|
|
110
115
|
if (isPid(record.clientPid)) binding.clientPid = record.clientPid;
|
|
111
116
|
return binding;
|
|
@@ -147,6 +152,8 @@ export async function listSessionBindings({ runtimeDir }) {
|
|
|
147
152
|
accSessionId: record.accSessionId, generation: record.generation,
|
|
148
153
|
...(typeof record.clientVersion === "string"
|
|
149
154
|
? { clientVersion: record.clientVersion } : {}),
|
|
155
|
+
...(typeof record.clientName === "string" && record.clientName !== ""
|
|
156
|
+
? { clientName: record.clientName } : {}),
|
|
150
157
|
...(typeof record.platform === "string" ? { platform: record.platform } : {}),
|
|
151
158
|
...(isPid(record.clientPid) ? { clientPid: record.clientPid } : {}) });
|
|
152
159
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// A hook command in the form each client's Windows hook runner reads. Measured
|
|
2
|
+
// from the clients' sources and binaries (docs/design/2026-09-30-native-windows-
|
|
3
|
+
// support.md): Codex and Gemini CLI run PowerShell, Grok detects PowerShell,
|
|
4
|
+
// Git Bash or cmd, Kimi Code runs cmd.exe, and Antigravity runs `cmd /c` through
|
|
5
|
+
// Go, which escapes inner quotes as `\"` - a form cmd does not read.
|
|
6
|
+
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
|
|
10
|
+
const defaultRun = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
// Characters one of those shells expands or ends a quoted word on. A path that
|
|
13
|
+
// carries one is refused: writing it would run something other than the shim.
|
|
14
|
+
const EXPANDED = /[$`"%]/;
|
|
15
|
+
// What cmd acts on in a word outside quotes, whitespace included.
|
|
16
|
+
const UNQUOTED_UNSAFE = /[\s&|<>^(),;=!]/;
|
|
17
|
+
|
|
18
|
+
const powershellLiteral = value => `'${String(value).replaceAll("'", "''")}'`;
|
|
19
|
+
const forward = value => String(value).replaceAll("\\", "/");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {"powershell"|"portable"|"cmd"|"unquoted"} shell
|
|
23
|
+
* @param {{ node: string, shim: string, args: string[] }} parts
|
|
24
|
+
*/
|
|
25
|
+
export function windowsHookCommand(shell, { node, shim, args }) {
|
|
26
|
+
for (const word of [node, shim]) {
|
|
27
|
+
if (EXPANDED.test(word)) {
|
|
28
|
+
throw new Error(`a hook command cannot name ${word}: a Windows shell would expand it`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const tail = args.join(" ");
|
|
32
|
+
switch (shell) {
|
|
33
|
+
// `&` runs a quoted program; single quotes take nothing literally but `''`.
|
|
34
|
+
// pwsh -Command reports exit 2 as 1 unless the command exits itself.
|
|
35
|
+
case "powershell":
|
|
36
|
+
return `& ${powershellLiteral(node)} ${powershellLiteral(shim)} ${tail}; exit $LASTEXITCODE`;
|
|
37
|
+
// Node from PATH, which every one of the three shells finds; the shim then
|
|
38
|
+
// prefers the pinned runner. Forward slashes survive bash unquoting.
|
|
39
|
+
case "portable":
|
|
40
|
+
return `node "${forward(shim)}" ${tail}`;
|
|
41
|
+
case "cmd":
|
|
42
|
+
return `"${node}" "${shim}" ${tail}`;
|
|
43
|
+
// The path reaches cmd bare: & | < > ^ ( ) run as cmd's own operators and
|
|
44
|
+
// , ; = split the word the way a space does, so none may be in it.
|
|
45
|
+
case "unquoted":
|
|
46
|
+
if (UNQUOTED_UNSAFE.test(shim)) {
|
|
47
|
+
throw new Error(`a hook command cannot name ${shim} unquoted: it has a space or a character cmd acts on`);
|
|
48
|
+
}
|
|
49
|
+
return `node ${forward(shim)} ${tail}`;
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`unknown Windows hook shell: ${shell}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The 8.3 form of a path with a space in it, for a hook runner that cannot pass
|
|
57
|
+
* a quoted path (Antigravity CLI). cmd prints it through `%~s`. A volume with 8.3
|
|
58
|
+
* names turned off answers with the long path, which the caller then refuses.
|
|
59
|
+
*/
|
|
60
|
+
export async function shortPath(file, { run = defaultRun, env = process.env } = {}) {
|
|
61
|
+
if (!/\s/.test(file)) return file;
|
|
62
|
+
if (/["%^&|<>!]/.test(file)) return file;
|
|
63
|
+
const comspec = env.ComSpec ?? env.COMSPEC ?? "cmd.exe";
|
|
64
|
+
const { stdout } = await run(comspec, ["/d", "/c", `for %I in ("${file}") do @echo %~sI`],
|
|
65
|
+
{ windowsHide: true, windowsVerbatimArguments: true, timeout: 5_000 });
|
|
66
|
+
const answer = String(stdout).trim().split(/\r?\n/).at(-1) ?? "";
|
|
67
|
+
return answer === "" ? file : answer;
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<?xml version="1.0" ?>
|
|
2
|
+
<!--
|
|
3
|
+
The chain of processes from one pid upward, read through WMI by Windows Script
|
|
4
|
+
Host: cscript //Nologo windows-process-chain.wsf <pid> <hops>. Prints a JSON
|
|
5
|
+
array of { pid, ppid, name, created, cmd }, created in WMI's own date form.
|
|
6
|
+
JScript here predates JSON, so strings are escaped by hand, and everything
|
|
7
|
+
outside printable ASCII as \uXXXX: the output never depends on a code page.
|
|
8
|
+
-->
|
|
9
|
+
<job>
|
|
10
|
+
<script language="JScript"><![CDATA[
|
|
11
|
+
var next = parseInt(WScript.Arguments(0), 10);
|
|
12
|
+
var hops = parseInt(WScript.Arguments(1), 10);
|
|
13
|
+
var wmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2");
|
|
14
|
+
|
|
15
|
+
function text(value) {
|
|
16
|
+
if (value === null || typeof value === "undefined") return "null";
|
|
17
|
+
var source = String(value);
|
|
18
|
+
var out = "\"";
|
|
19
|
+
for (var index = 0; index < source.length; index++) {
|
|
20
|
+
var code = source.charCodeAt(index);
|
|
21
|
+
if (code === 34) out += "\\\"";
|
|
22
|
+
else if (code === 92) out += "\\\\";
|
|
23
|
+
else if (code < 32 || code > 126) {
|
|
24
|
+
var hex = code.toString(16);
|
|
25
|
+
out += "\\u" + "0000".substr(hex.length) + hex;
|
|
26
|
+
} else out += source.charAt(index);
|
|
27
|
+
}
|
|
28
|
+
return out + "\"";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var chain = [];
|
|
32
|
+
for (var hop = 0; hop < hops && next > 0; hop++) {
|
|
33
|
+
var found = new Enumerator(wmi.ExecQuery("SELECT ProcessId,ParentProcessId,Name,CommandLine,"
|
|
34
|
+
+ "CreationDate FROM Win32_Process WHERE ProcessId=" + next));
|
|
35
|
+
if (found.atEnd()) break;
|
|
36
|
+
var process = found.item();
|
|
37
|
+
chain.push("{\"pid\":" + process.ProcessId + ",\"ppid\":" + process.ParentProcessId
|
|
38
|
+
+ ",\"name\":" + text(process.Name) + ",\"created\":" + text(process.CreationDate)
|
|
39
|
+
+ ",\"cmd\":" + text(process.CommandLine) + "}");
|
|
40
|
+
if (process.ParentProcessId === 0 || process.ParentProcessId === next) break;
|
|
41
|
+
next = process.ParentProcessId;
|
|
42
|
+
}
|
|
43
|
+
WScript.StdOut.Write("[" + chain.join(",") + "]");
|
|
44
|
+
]]></script>
|
|
45
|
+
</job>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents-can-communicate/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.mjs",
|
|
8
|
+
"./hook-support": "./src/hook-support.mjs",
|
|
8
9
|
"./managed-entry": "./src/managed-runtime/entry.mjs",
|
|
9
10
|
"./managed-worker": "./src/managed-runtime/worker.mjs"
|
|
10
11
|
},
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { AccError, CONFIG_FILENAME, EXIT, createId, defaultProjectConfig,
|
|
6
6
|
validateProjectConfig } from "@agents-can-communicate/protocol";
|
|
7
|
+
import { openNoFollow } from "@agents-can-communicate/storage-filesystem";
|
|
7
8
|
|
|
8
9
|
const configPathIn = cwd => path.join(cwd, CONFIG_FILENAME);
|
|
9
10
|
|
|
@@ -32,7 +33,7 @@ async function readConfig(cwd) {
|
|
|
32
33
|
try {
|
|
33
34
|
// Same rule as discovery: a config reached through a symlink may point
|
|
34
35
|
// anywhere, so it is refused rather than followed.
|
|
35
|
-
handle = await
|
|
36
|
+
handle = await openNoFollow(file, constants.O_RDONLY);
|
|
36
37
|
} catch (error) {
|
|
37
38
|
if (error.code === "ENOENT") return null;
|
|
38
39
|
throw new AccError(EXIT.DATA, "cannot safely read the workspace config",
|
|
@@ -50,13 +50,66 @@ import { diagnoseFilesystemStore, repairFilesystemStore }
|
|
|
50
50
|
*
|
|
51
51
|
* Null for anything unreadable. "Might be old" on every run is not a diagnosis.
|
|
52
52
|
*/
|
|
53
|
+
/**
|
|
54
|
+
* The package directory of the runner a shim or config names, or null.
|
|
55
|
+
*
|
|
56
|
+
* ACC writes the runner in double quotes in every form: a shell shim
|
|
57
|
+
* (`ACC_RUNNER="..."`), the Node shim Windows gets (a JSON string) and Kimi's
|
|
58
|
+
* config (a TOML string holding a command, whose own quotes are cmd's on
|
|
59
|
+
* Windows and TOML's elsewhere). Each quoted string is read the way a shell
|
|
60
|
+
* reads one: a backslash escapes only a quote, a backslash, $ or a backtick,
|
|
61
|
+
* which is all the shim escapes and all a path needs undone in JSON or TOML,
|
|
62
|
+
* so an unescaped C:\Users\... in a shell script stays that path. A string
|
|
63
|
+
* holding quotes is a command. A Windows path in it is cmd's, which has no
|
|
64
|
+
* escape character, so it is read as it stands and a UNC path keeps its leading
|
|
65
|
+
* \\; other words are read the shell's way, since Kimi's POSIX command escapes a
|
|
66
|
+
* quote in a path; then, when no word names the runner, the string is read
|
|
67
|
+
* whole, since a POSIX path may hold a quote.
|
|
68
|
+
* An apostrophe, as in C:\Users\O'Neil, is part of a path. An unquoted path is
|
|
69
|
+
* read to the whitespace before it.
|
|
70
|
+
*/
|
|
71
|
+
const RUNNER = /[\\/]agents-can-communicate(?=[\\/]bin[\\/]acc-hook\.mjs)/;
|
|
72
|
+
const absoluteRoot = value => {
|
|
73
|
+
const match = RUNNER.exec(value);
|
|
74
|
+
if (match === null) return null;
|
|
75
|
+
const root = value.slice(0, match.index + match[0].length);
|
|
76
|
+
return /^(?:[A-Za-z]:)?[\\/]/.test(root) ? root : null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const quotedStrings = text => [...text.matchAll(/"((?:[^"\\]|\\[\s\S])*)"/g)]
|
|
80
|
+
.map(([, quoted]) => quoted.replace(/\\(["\\$`])/g, "$1"));
|
|
81
|
+
|
|
82
|
+
const commandRoot = command => {
|
|
83
|
+
for (const [, word] of command.matchAll(/"([^"]*)"/g)) {
|
|
84
|
+
const root = /^(?:[A-Za-z]:\\|\\\\)/.test(word) ? absoluteRoot(word) : null;
|
|
85
|
+
if (root !== null) return root;
|
|
86
|
+
}
|
|
87
|
+
for (const word of quotedStrings(command)) {
|
|
88
|
+
const root = absoluteRoot(word);
|
|
89
|
+
if (root !== null) return root;
|
|
90
|
+
}
|
|
91
|
+
return absoluteRoot(command);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function runnerRoot(text) {
|
|
95
|
+
for (const value of quotedStrings(String(text))) {
|
|
96
|
+
const root = value.includes("\"") ? commandRoot(value) : absoluteRoot(value);
|
|
97
|
+
if (root !== null) return root;
|
|
98
|
+
}
|
|
99
|
+
const flat = String(text);
|
|
100
|
+
const match = RUNNER.exec(flat);
|
|
101
|
+
if (match === null) return null;
|
|
102
|
+
const start = flat.slice(0, match.index).search(/\S+$/);
|
|
103
|
+
return start < 0 ? null : absoluteRoot(flat.slice(start));
|
|
104
|
+
}
|
|
105
|
+
|
|
53
106
|
export async function wiredVersion(shimPath) {
|
|
54
107
|
if (typeof shimPath !== "string" || shimPath === "") return null;
|
|
55
108
|
const text = await readFile(shimPath, "utf8").catch(() => null);
|
|
56
109
|
if (text === null) return null;
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
const manifest = await readFile(path.join(
|
|
110
|
+
const root = runnerRoot(text);
|
|
111
|
+
if (root === null) return null;
|
|
112
|
+
const manifest = await readFile(path.join(root, "package.json"), "utf8")
|
|
60
113
|
.catch(() => null);
|
|
61
114
|
if (manifest === null) return null;
|
|
62
115
|
try {
|
|
@@ -103,7 +156,10 @@ async function findShims(root, depth) {
|
|
|
103
156
|
for (const entry of entries) {
|
|
104
157
|
const target = path.join(root, entry.name);
|
|
105
158
|
if (entry.isDirectory()) found.push(...await findShims(target, depth - 1));
|
|
106
|
-
|
|
159
|
+
// The shell shim on POSIX, the Node shim on Windows.
|
|
160
|
+
else if (/^acc-(?:hook|cli)\.(?:sh|mjs)$/.test(entry.name) || entry.name.endsWith(".sh")) {
|
|
161
|
+
found.push(target);
|
|
162
|
+
}
|
|
107
163
|
}
|
|
108
164
|
return found;
|
|
109
165
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// What a hook needs from this package, without the rest of it. A hook starts a
|
|
2
|
+
// process on every turn, and the package index loads the whole command line and
|
|
3
|
+
// the installer with it: modules a hook never runs, about a millisecond each on
|
|
4
|
+
// windows-latest.
|
|
5
|
+
export { resolveHookWorkspace } from "./hook-workspace.mjs";
|
|
6
|
+
export { createGitProbe } from "./git-probe.mjs";
|
|
7
|
+
export { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
8
|
+
export { clearPin, hookEntrypointFor, resolvePinnedGeneration, writePin }
|
|
9
|
+
from "./managed-runtime/pins.mjs";
|
|
@@ -12,7 +12,7 @@ import { runtimePaths } from "./runtime-paths.mjs";
|
|
|
12
12
|
// this survives client exit: resuming the same native conversation keeps its
|
|
13
13
|
// room. Adapter + native id distinguish independent conversations.
|
|
14
14
|
export async function resolveHookWorkspace({ adapterId, event, room, cwd, dataHome, env = {},
|
|
15
|
-
gitProbe, clock, deadlineAt }) {
|
|
15
|
+
gitProbe, clock, deadlineAt, platform = process.platform }) {
|
|
16
16
|
const identityKey = (adapter, native) => createHash("sha256")
|
|
17
17
|
.update(JSON.stringify([adapter, native])).digest("hex");
|
|
18
18
|
if (room !== undefined && !/^[a-f0-9]{64}$/.test(room)) {
|
|
@@ -72,7 +72,13 @@ export async function resolveHookWorkspace({ adapterId, event, room, cwd, dataHo
|
|
|
72
72
|
let current = descriptor;
|
|
73
73
|
if (descriptor.source === "git" && path.resolve(event.cwd) !== workspaceCwd) {
|
|
74
74
|
const git = await gitProbe({ cwd: event.cwd }).catch(() => null);
|
|
75
|
-
|
|
75
|
+
// Both sides canonical: Git spells the directory the way the cwd did
|
|
76
|
+
// (through a symlink, or `C:/…` on Windows), and the room kept that
|
|
77
|
+
// spelling.
|
|
78
|
+
const same = git !== null && await Promise.all([git.commonDir, descriptor.git.commonDir]
|
|
79
|
+
.map(directory => realpath(directory).catch(() => null)))
|
|
80
|
+
.then(([here, room]) => here !== null && here === room);
|
|
81
|
+
if (same) {
|
|
76
82
|
current = { ...descriptor, git };
|
|
77
83
|
}
|
|
78
84
|
}
|
|
@@ -97,13 +103,16 @@ export async function resolveHookWorkspace({ adapterId, event, room, cwd, dataHo
|
|
|
97
103
|
const raced = await read();
|
|
98
104
|
if (raced !== null) return discover(raced);
|
|
99
105
|
deadline();
|
|
106
|
+
// On Windows the session this hook opens next flushes, and that commits
|
|
107
|
+
// this name with NTFS's journal; a crash before it leaves the room to be
|
|
108
|
+
// chosen again by the session's next hook.
|
|
100
109
|
await writeManagedJson(file, { schemaVersion: 1, adapterId,
|
|
101
110
|
nativeSessionId: event.sessionId, cwd: context.workspaceCwd,
|
|
102
111
|
workspaceId: context.descriptor.id, source: context.descriptor.source,
|
|
103
112
|
...(context.descriptor.source === "git" ? { git: {
|
|
104
113
|
commonDir: await realpath(context.descriptor.git.commonDir),
|
|
105
114
|
worktreeRoot: await realpath(context.descriptor.git.worktreeRoot),
|
|
106
|
-
} } : {}) });
|
|
115
|
+
} } : {}) }, { flushName: platform !== "win32" });
|
|
107
116
|
return { ...context, workspaceRef: `acc://${key}` };
|
|
108
117
|
});
|
|
109
118
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readdir
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { assertPortableId } from "@agents-can-communicate/protocol";
|
|
@@ -8,6 +8,7 @@ import { listRuntimeHolds } from "./leases.mjs";
|
|
|
8
8
|
import { confirmedDead, defaultPidIsAlive, withManagerLock } from "./mutex.mjs";
|
|
9
9
|
import { reapPins } from "./pins.mjs";
|
|
10
10
|
import { reapStagingHolds } from "./staging.mjs";
|
|
11
|
+
import { removeTree } from "./portable-fs.mjs";
|
|
11
12
|
import { canonicalManagerRoot, managedDirectory, readControl, readManagedJson, syncDirectory, writeControl } from "./state.mjs";
|
|
12
13
|
|
|
13
14
|
/** The staged generation states its own contract in its manifest, so the
|
|
@@ -250,7 +251,7 @@ export async function reclaimGenerations({ root, active = null, pidIsAlive = def
|
|
|
250
251
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
251
252
|
const directory = path.join(generations, entry.name);
|
|
252
253
|
if (referenced.has(directory)) continue;
|
|
253
|
-
await
|
|
254
|
+
await removeTree(directory);
|
|
254
255
|
removed.push(entry.name);
|
|
255
256
|
}
|
|
256
257
|
if (removed.length) await syncDirectory(generations);
|
|
@@ -38,10 +38,19 @@ export async function fetchRelease({ pin = null, env = process.env, get = fetch
|
|
|
38
38
|
return { version: body.version, integrity, registry: registry.href };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/** npm's own script, beside this node first. POSIX's `npm` links to it; on
|
|
42
|
+
* Windows that name is a shell script, and node would run it as JavaScript. */
|
|
43
|
+
export async function npmCli(env, { platform = process.platform, execPath = process.execPath,
|
|
44
|
+
realpath: resolve = realpath } = {}) {
|
|
45
|
+
const flavour = platform === "win32" ? path.win32 : path.posix;
|
|
46
|
+
// A copy of Windows' environment keeps the variable's own spelling, `Path`.
|
|
47
|
+
const search = Object.entries(env).find(([name]) => name.toUpperCase() === "PATH")?.[1] ?? "";
|
|
48
|
+
const directories = [flavour.dirname(execPath), ...search.split(flavour.delimiter)];
|
|
43
49
|
for (const directory of directories.filter(Boolean)) {
|
|
44
|
-
|
|
50
|
+
const candidate = platform === "win32"
|
|
51
|
+
? flavour.join(directory, "node_modules", "npm", "bin", "npm-cli.js")
|
|
52
|
+
: flavour.join(directory, "npm");
|
|
53
|
+
try { return await resolve(candidate); } catch { /* Try the next installed npm. */ }
|
|
45
54
|
}
|
|
46
55
|
throw new Error("npm is unavailable; the working ACC runtime was kept");
|
|
47
56
|
}
|
|
@@ -17,12 +17,14 @@ export const RETIRED_ENTRY_KINDS = Object.freeze(["acc-bootstrap", "acc-claude-c
|
|
|
17
17
|
|
|
18
18
|
// Bootstrap resolution deliberately needs only Node built-ins: no selected
|
|
19
19
|
// generation is imported until admission and its actual PID lease are durable.
|
|
20
|
+
// The same rule as platformDataHome, which this module may not import.
|
|
20
21
|
export function managerLocation({ env = process.env, platform = process.platform } = {}) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const flavour = platform === "win32" ? path.win32 : path.posix;
|
|
23
|
+
const data = env.ACC_DATA_HOME || (platform === "win32" ? env.LOCALAPPDATA
|
|
24
|
+
: platform === "darwin" ? env.HOME && flavour.join(env.HOME, "Library", "Application Support")
|
|
25
|
+
: env.XDG_DATA_HOME || env.HOME && flavour.join(env.HOME, ".local", "share"));
|
|
26
|
+
if (typeof data !== "string" || !flavour.isAbsolute(data)) throw new Error("cannot resolve ACC data home");
|
|
27
|
+
return flavour.join(data, "acc", "runtime");
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function invokedDirectly(url) {
|