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.
- 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 +41 -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 +32 -5
- package/docs/TROUBLESHOOTING.md +76 -8
- package/docs/UPGRADING.md +47 -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/plugin/skills/acc/SKILL.md +3 -0
- 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/plugin/skills/acc/SKILL.md +3 -0
- 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/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/allow-rule.mjs +129 -0
- 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 +36 -13
- 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/extension/skills/acc/SKILL.md +3 -0
- 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/plugin/skills/acc/SKILL.md +3 -0
- 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/plugin/skills/acc/SKILL.md +3 -0
- 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/own-version.mjs +27 -1
- 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/ownership.mjs +6 -2
- 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
|
@@ -28,15 +28,17 @@ const usage = (message, details) => {
|
|
|
28
28
|
throw new AccError(EXIT.USAGE, message, details);
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// Pids, locks and pipe names belong to one machine, so state and cache live in
|
|
32
|
+
// local app data. A roaming profile would carry them to another machine, where
|
|
33
|
+
// every recorded pid names something else; there is no roaming fallback.
|
|
31
34
|
function windowsPaths(env) {
|
|
32
|
-
if (!set(env.
|
|
33
|
-
usage("cannot resolve the Windows application data directory");
|
|
35
|
+
if (!set(env.LOCALAPPDATA)) {
|
|
36
|
+
usage("cannot resolve the Windows local application data directory");
|
|
34
37
|
}
|
|
35
38
|
return {
|
|
36
|
-
data: env.
|
|
37
|
-
config: env.APPDATA,
|
|
38
|
-
|
|
39
|
-
cache: set(env.LOCALAPPDATA) ? env.LOCALAPPDATA : env.APPDATA,
|
|
39
|
+
data: env.LOCALAPPDATA,
|
|
40
|
+
config: set(env.APPDATA) ? env.APPDATA : env.LOCALAPPDATA,
|
|
41
|
+
cache: env.LOCALAPPDATA,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -45,18 +47,21 @@ function requireHome(env) {
|
|
|
45
47
|
return env;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
// POSIX paths are built with path.posix whatever the host: the resolution is a
|
|
51
|
+
// pure function of platform and environment, and a Windows host would turn
|
|
52
|
+
// every forward slash of a POSIX home into a backslash.
|
|
48
53
|
function macosPaths(env) {
|
|
49
|
-
const support = path.join(env.HOME, "Library", "Application Support");
|
|
54
|
+
const support = path.posix.join(env.HOME, "Library", "Application Support");
|
|
50
55
|
// XDG variables are deliberately ignored here. They are common on a machine
|
|
51
56
|
// that also runs Linux tooling, and letting one relocate macOS state would
|
|
52
57
|
// move a user's sessions the day they install something unrelated.
|
|
53
58
|
return { data: support, config: support,
|
|
54
|
-
cache: path.join(env.HOME, "Library", "Caches") };
|
|
59
|
+
cache: path.posix.join(env.HOME, "Library", "Caches") };
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
function xdgPaths(env) {
|
|
58
|
-
const fallback = { data: path.join(env.HOME, ".local", "share"),
|
|
59
|
-
config: path.join(env.HOME, ".config"), cache: path.join(env.HOME, ".cache") };
|
|
63
|
+
const fallback = { data: path.posix.join(env.HOME, ".local", "share"),
|
|
64
|
+
config: path.posix.join(env.HOME, ".config"), cache: path.posix.join(env.HOME, ".cache") };
|
|
60
65
|
return Object.fromEntries(AREAS.map(area =>
|
|
61
66
|
[area, set(env[XDG[area]]) ? env[XDG[area]] : fallback[area]]));
|
|
62
67
|
}
|
|
@@ -54,9 +54,10 @@ export function platformDataHome({ platform = process.platform, env = process.en
|
|
|
54
54
|
if (typeof env.ACC_DATA_HOME === "string" && env.ACC_DATA_HOME.length > 0) {
|
|
55
55
|
return env.ACC_DATA_HOME;
|
|
56
56
|
}
|
|
57
|
+
// Machine-local: see windowsPaths in platform-paths.mjs.
|
|
57
58
|
if (platform === "win32") {
|
|
58
|
-
if (typeof env.
|
|
59
|
-
throw new AccError(EXIT.USAGE, "cannot resolve the Windows application data directory");
|
|
59
|
+
if (typeof env.LOCALAPPDATA === "string" && env.LOCALAPPDATA.length > 0) return env.LOCALAPPDATA;
|
|
60
|
+
throw new AccError(EXIT.USAGE, "cannot resolve the Windows local application data directory");
|
|
60
61
|
}
|
|
61
62
|
if (typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.length > 0
|
|
62
63
|
&& platform !== "darwin") {
|
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
|
|
5
|
+
import { pathOf, resolveExecutable, runExecutable } from "@agents-can-communicate/adapter-sdk";
|
|
5
6
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
6
7
|
|
|
7
8
|
import { platformPaths } from "./platform-paths.mjs";
|
|
@@ -11,6 +12,15 @@ import { runManagedUpdate, validateUpdateOptions } from "./managed-runtime/comma
|
|
|
11
12
|
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
|
|
15
|
+
// npm and acc are .cmd shims on Windows: found through PATHEXT, and started
|
|
16
|
+
// through cmd.exe. POSIX runs them by name as before.
|
|
17
|
+
async function runStep(command, argv, env) {
|
|
18
|
+
if (process.platform !== "win32") return execFileAsync(command, argv, { env });
|
|
19
|
+
const file = await resolveExecutable(command, { pathEnv: pathOf(env) });
|
|
20
|
+
if (file === null) throw new Error(`${command} is not on PATH`);
|
|
21
|
+
return runExecutable(file, argv, { env });
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* The two commands an upgrade takes, and why it is two.
|
|
16
26
|
*
|
|
@@ -76,7 +86,7 @@ export async function runUpdateCommand({ options, runtime }) {
|
|
|
76
86
|
...steps.map(spell), "", "or run: acc update"].join("\n") };
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
const spawn = runtime.spawn ?? ((command, argv) =>
|
|
89
|
+
const spawn = runtime.spawn ?? ((command, argv) => runStep(command, argv, env));
|
|
80
90
|
const done = [];
|
|
81
91
|
let installation = { stdout: "", stderr: "" };
|
|
82
92
|
for (const [command, argv] of steps) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { AccError, CONFIG_FILENAME, EXIT, validateProjectConfig }
|
|
7
7
|
from "@agents-can-communicate/protocol";
|
|
8
|
+
import { openNoFollow } from "@agents-can-communicate/storage-filesystem";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {{ id: string, roots: string[], source: "config" | "git" | "directory",
|
|
@@ -17,7 +18,8 @@ const stableId = value =>
|
|
|
17
18
|
async function readConfigNoFollow(configPath) {
|
|
18
19
|
let handle;
|
|
19
20
|
try {
|
|
20
|
-
|
|
21
|
+
// Windows has no O_NOFOLLOW; openNoFollow keeps the refusal there too.
|
|
22
|
+
handle = await openNoFollow(configPath, constants.O_RDONLY);
|
|
21
23
|
} catch (error) {
|
|
22
24
|
if (error.code === "ENOENT") return null;
|
|
23
25
|
// ELOOP is what O_NOFOLLOW reports for a symlink. A config reached through
|
|
@@ -91,6 +91,13 @@ export function createDeliveryBindingService(ports, sessions) {
|
|
|
91
91
|
async function clearDeliveryBinding(input) {
|
|
92
92
|
const { sessionId, generation } = input;
|
|
93
93
|
const endpointScoped = Object.hasOwn(input, "opaqueEndpointRef");
|
|
94
|
+
// Every hook clears when live delivery is not running, and on Windows that
|
|
95
|
+
// is every hook. Absent, or retired for this generation, is a state the
|
|
96
|
+
// update below leaves exactly as it is, so it is read first without the
|
|
97
|
+
// writer lock; a record read this way is as current as one read under it.
|
|
98
|
+
const seen = await store.ephemeral.get("deliveryBinding", sessionId);
|
|
99
|
+
if (seen === null || seen === undefined) return;
|
|
100
|
+
if (seen.generation === generation && seen.retiredAt !== null && seen.retiredAt !== undefined) return;
|
|
94
101
|
await store.ephemeral.update("deliveryBinding", sessionId, async current => {
|
|
95
102
|
if (current === null || current === undefined) return null;
|
|
96
103
|
if (current.generation !== generation) throw conflict(sessionId);
|
|
@@ -21,9 +21,19 @@ export async function isMaterialised(store, workspaceId) {
|
|
|
21
21
|
return (await store.snapshot(workspaceId, { kinds: ["workspace"] })).workspace !== null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* `opening` is a second live session that opens here, in the transaction that
|
|
26
|
+
* materialises, instead of first writing an ephemeral copy of itself to be
|
|
27
|
+
* copied and retired a moment later. It opens only while the session it joins
|
|
28
|
+
* is still live under the lock; otherwise it would be a lone session, which
|
|
29
|
+
* leaves no durable trace, and nothing is written: the result is false and the
|
|
30
|
+
* caller opens it ephemerally.
|
|
31
|
+
*/
|
|
32
|
+
export async function materialise({ store, clock, ids },
|
|
33
|
+
{ workspaceId, descriptor, reason, opening = null }) {
|
|
25
34
|
const now = clock.now();
|
|
26
35
|
const staged = [];
|
|
36
|
+
let joined = true;
|
|
27
37
|
|
|
28
38
|
await store.transaction(async tx => {
|
|
29
39
|
// Ephemeral writers use this same mutex. Reading before taking it could
|
|
@@ -31,6 +41,13 @@ export async function materialise({ store, clock, ids }, { workspaceId, descript
|
|
|
31
41
|
for (const entry of EPHEMERAL_KINDS) {
|
|
32
42
|
staged.push({ ...entry, records: await store.ephemeral.list(entry.kind) });
|
|
33
43
|
}
|
|
44
|
+
if (opening !== null && tx.get("workspace", workspaceId) === null
|
|
45
|
+
&& !staged.find(entry => entry.kind === "session").records.some(record =>
|
|
46
|
+
record.state === "open" && record.sessionId !== opening.session.sessionId)) {
|
|
47
|
+
joined = false;
|
|
48
|
+
staged.length = 0;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
34
51
|
const actorSessionId = staged.find(entry => entry.kind === "session")?.records.at(-1)?.sessionId
|
|
35
52
|
?? "session_bootstrap";
|
|
36
53
|
// Asked again inside the lock. `ensureMaterialised` asks before taking it,
|
|
@@ -53,8 +70,14 @@ export async function materialise({ store, clock, ids }, { workspaceId, descript
|
|
|
53
70
|
payload: { reason } });
|
|
54
71
|
}
|
|
55
72
|
|
|
73
|
+
// The opening session's own ephemeral copy - left by a process that died
|
|
74
|
+
// mid-open - is what the opening replaces, not a session to promote first:
|
|
75
|
+
// promoting it would record the session opening twice.
|
|
76
|
+
const own = opening === null ? null : staged.find(entry => entry.kind === "session")
|
|
77
|
+
.records.find(record => record.sessionId === opening.session.sessionId) ?? null;
|
|
56
78
|
for (const entry of staged) {
|
|
57
79
|
for (const record of entry.records) {
|
|
80
|
+
if (record === own) continue;
|
|
58
81
|
// Whoever got here first may have promoted this record already, and
|
|
59
82
|
// promoting is a copy: the ephemeral and durable shapes are the same,
|
|
60
83
|
// so the one already there is the one this would write.
|
|
@@ -66,11 +89,25 @@ export async function materialise({ store, clock, ids }, { workspaceId, descript
|
|
|
66
89
|
payload: { sessionId: record.sessionId } });
|
|
67
90
|
}
|
|
68
91
|
}
|
|
92
|
+
|
|
93
|
+
if (opening !== null) {
|
|
94
|
+
const { participant, session } = opening;
|
|
95
|
+
const current = tx.get("session", session.sessionId) ?? own;
|
|
96
|
+
opening.assertAvailable(current);
|
|
97
|
+
if (tx.get("participant", participant.participantId) === null) {
|
|
98
|
+
tx.put("participant", participant.participantId, participant);
|
|
99
|
+
}
|
|
100
|
+
tx.put("session", session.sessionId, session, tx.generationOf("session", session.sessionId));
|
|
101
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"), workspaceId,
|
|
102
|
+
actorSessionId: session.sessionId, type: "session.opened", occurredAt: now,
|
|
103
|
+
payload: { replaced: current?.generation ?? null } });
|
|
104
|
+
}
|
|
69
105
|
// The promoted kinds are named by the loop above, not written literally in
|
|
70
106
|
// the body, so they are derived rather than repeated: a new ephemeral kind
|
|
71
107
|
// added to that list is read here without anyone remembering to say so.
|
|
72
108
|
}, { kinds: ["workspace", ...EPHEMERAL_KINDS.map(entry => entry.kind)] });
|
|
73
109
|
|
|
110
|
+
if (!joined) return false;
|
|
74
111
|
// The ephemeral copies are retired only after the durable transaction
|
|
75
112
|
// committed, so a crash in between leaves a recoverable duplicate rather than
|
|
76
113
|
// a hole.
|
|
@@ -79,6 +116,7 @@ export async function materialise({ store, clock, ids }, { workspaceId, descript
|
|
|
79
116
|
await store.ephemeral.delete(entry.kind, record[entry.key]);
|
|
80
117
|
}
|
|
81
118
|
}
|
|
119
|
+
return true;
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
export async function ensureMaterialised(ports, { workspaceId, descriptor, reason }) {
|
|
@@ -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
|
-
|
|
96
|
-
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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";
|