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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, readdir, realpath, rename, rm } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
5
|
+
import { closedTo, isWindowsPlatform, openRegularNoFollow } from "@agents-can-communicate/adapter-sdk";
|
|
6
|
+
|
|
6
7
|
import { PROTOCOL_CONTRACT, versionOrder } from "./app-server-client.mjs";
|
|
7
8
|
|
|
8
9
|
const KEYS = ["schemaVersion", "endpointId", "socketPath", "threadId", "cwd",
|
|
@@ -28,7 +29,7 @@ function valid(record) {
|
|
|
28
29
|
&& Number.isFinite(Date.parse(record.leaseUntil));
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
async function directory(runtimeDir, create = false) {
|
|
32
|
+
async function directory(runtimeDir, create = false, platform = process.platform) {
|
|
32
33
|
if (!absolute(runtimeDir)) throw invalid();
|
|
33
34
|
if (create) await mkdir(runtimeDir, { recursive: true, mode: 0o700 });
|
|
34
35
|
const root = await realpath(runtimeDir);
|
|
@@ -37,11 +38,25 @@ async function directory(runtimeDir, create = false) {
|
|
|
37
38
|
if (error.code !== "EEXIST") throw error;
|
|
38
39
|
});
|
|
39
40
|
const info = await lstat(dir);
|
|
40
|
-
if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || (info
|
|
41
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || !closedTo(info, 0o022, { platform })) {
|
|
42
|
+
throw invalid();
|
|
43
|
+
}
|
|
41
44
|
return dir;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
/**
|
|
48
|
+
* An owned socket, never itself a link. On Windows Node's lstat refuses the
|
|
49
|
+
* daemon's AF_UNIX socket file with EACCES (measured on windows-latest, Codex
|
|
50
|
+
* 0.159.3), so there the socket counts while its directory lists it; Codex keeps
|
|
51
|
+
* that directory to its user.
|
|
52
|
+
*/
|
|
53
|
+
export async function socketIsReady(socketPath, { platform = process.platform } = {}) {
|
|
54
|
+
if (isWindowsPlatform(platform)) {
|
|
55
|
+
if (!absolute(socketPath)) return false;
|
|
56
|
+
const name = path.basename(socketPath).toLowerCase();
|
|
57
|
+
const names = await readdir(path.dirname(socketPath)).catch(() => []);
|
|
58
|
+
return names.some(entry => entry.toLowerCase() === name);
|
|
59
|
+
}
|
|
45
60
|
if (!absolute(socketPath)) return false;
|
|
46
61
|
try {
|
|
47
62
|
const info = await lstat(socketPath);
|
|
@@ -53,7 +68,14 @@ export async function socketIsReady(socketPath) {
|
|
|
53
68
|
* symlink at the path it reports and the socket itself under /private/tmp
|
|
54
69
|
* (measured 2026-09-26), so the path is followed once and the socket at its
|
|
55
70
|
* end is held to socketIsReady: an owned socket, never itself a link. */
|
|
56
|
-
export async function readySocketPath(socketPath) {
|
|
71
|
+
export async function readySocketPath(socketPath, { platform = process.platform } = {}) {
|
|
72
|
+
if (isWindowsPlatform(platform)) {
|
|
73
|
+
// The socket file itself refuses to be opened; its directory resolves.
|
|
74
|
+
if (!absolute(socketPath)) return null;
|
|
75
|
+
const directory = await realpath(path.dirname(socketPath)).catch(() => null);
|
|
76
|
+
const resolved = directory === null ? null : path.join(directory, path.basename(socketPath));
|
|
77
|
+
return resolved !== null && await socketIsReady(resolved, { platform }) ? resolved : null;
|
|
78
|
+
}
|
|
57
79
|
if (!absolute(socketPath)) return null;
|
|
58
80
|
const resolved = await realpath(socketPath).catch(() => null);
|
|
59
81
|
return resolved !== null && await socketIsReady(resolved) ? resolved : null;
|
|
@@ -61,11 +83,11 @@ export async function readySocketPath(socketPath) {
|
|
|
61
83
|
|
|
62
84
|
// A fresh, immutable registration for one hook binding. Refresh observes the
|
|
63
85
|
// same registration again; it cannot overwrite a successor's random endpoint.
|
|
64
|
-
export async function writeNativeEndpoint({ runtimeDir, record }) {
|
|
65
|
-
if (!valid(record) || !await socketIsReady(record.socketPath)) throw invalid();
|
|
86
|
+
export async function writeNativeEndpoint({ runtimeDir, record, platform = process.platform }) {
|
|
87
|
+
if (!valid(record) || !await socketIsReady(record.socketPath, { platform })) throw invalid();
|
|
66
88
|
const text = `${JSON.stringify(record)}\n`;
|
|
67
89
|
if (Buffer.byteLength(text) > MAX_BYTES) throw invalid();
|
|
68
|
-
const dir = await directory(runtimeDir, true);
|
|
90
|
+
const dir = await directory(runtimeDir, true, platform);
|
|
69
91
|
const file = path.join(dir, `${record.endpointId}.json`);
|
|
70
92
|
const temporary = path.join(dir, `.${record.endpointId}.${randomBytes(8).toString("hex")}.tmp`);
|
|
71
93
|
let handle;
|
|
@@ -82,25 +104,26 @@ export async function writeNativeEndpoint({ runtimeDir, record }) {
|
|
|
82
104
|
|
|
83
105
|
// Expired records are metadata for revalidation, never proof of reachability.
|
|
84
106
|
// Refuse links and oversized files before reading; no foreign error text escapes.
|
|
85
|
-
export async function readNativeEndpoint({ runtimeDir, endpointId }) {
|
|
107
|
+
export async function readNativeEndpoint({ runtimeDir, endpointId, platform = process.platform }) {
|
|
86
108
|
if (typeof endpointId !== "string" || !ENDPOINT.test(endpointId)) return null;
|
|
87
109
|
let handle;
|
|
88
110
|
try {
|
|
89
|
-
const dir = await directory(runtimeDir);
|
|
90
|
-
handle = await
|
|
91
|
-
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
111
|
+
const dir = await directory(runtimeDir, false, platform);
|
|
112
|
+
handle = await openRegularNoFollow(path.join(dir, `${endpointId}.json`), undefined, { platform });
|
|
92
113
|
const info = await handle.stat();
|
|
93
|
-
if (!info.isFile() || !own(info) || info.size > MAX_BYTES || (info
|
|
114
|
+
if (!info.isFile() || !own(info) || info.size > MAX_BYTES || !closedTo(info, 0o077, { platform })) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
94
117
|
const record = JSON.parse(await handle.readFile("utf8"));
|
|
95
118
|
return valid(record) && record.endpointId === endpointId ? record : null;
|
|
96
119
|
} catch { return null; }
|
|
97
120
|
finally { await handle?.close().catch(() => null); }
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
export async function removeNativeEndpoint({ runtimeDir, endpointId }) {
|
|
123
|
+
export async function removeNativeEndpoint({ runtimeDir, endpointId, platform = process.platform }) {
|
|
101
124
|
if (typeof endpointId !== "string" || !ENDPOINT.test(endpointId)) return;
|
|
102
125
|
try {
|
|
103
|
-
const dir = await directory(runtimeDir);
|
|
126
|
+
const dir = await directory(runtimeDir, false, platform);
|
|
104
127
|
await rm(path.join(dir, `${endpointId}.json`), { force: true });
|
|
105
128
|
} catch { /* retirement is already recorded; cleanup is best effort */ }
|
|
106
129
|
}
|
|
@@ -75,8 +75,10 @@ export function refusesConnections(socketPath, { timeoutMs = 1_000 } = {}) {
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// `connect` returns the byte stream to speak over: a socket by default, or on
|
|
79
|
+
// Windows the stdio of `codex app-server proxy`, which relays to the same server.
|
|
78
80
|
export function openWebSocketPeer({ socketPath, timeoutMs, path = "/", host = "localhost",
|
|
79
|
-
retainNotifications = true }) {
|
|
81
|
+
retainNotifications = true, connect = () => net.createConnection(socketPath) }) {
|
|
80
82
|
const notifications = [];
|
|
81
83
|
const pending = new Map();
|
|
82
84
|
const key = randomBytes(16).toString("base64");
|
|
@@ -90,7 +92,7 @@ export function openWebSocketPeer({ socketPath, timeoutMs, path = "/", host = "l
|
|
|
90
92
|
const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
91
93
|
ready.catch(() => {});
|
|
92
94
|
|
|
93
|
-
const socket =
|
|
95
|
+
const socket = connect();
|
|
94
96
|
socket.on("connect", () => socket.write(`GET ${path} HTTP/1.1\r\nHost: ${host}\r\n`
|
|
95
97
|
+ `Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\n`
|
|
96
98
|
+ "Sec-WebSocket-Version: 13\r\n\r\n"));
|
|
@@ -43,7 +43,8 @@ export function createGeminiCliAdapter() {
|
|
|
43
43
|
// The binary this client actually installs. Probed for a version to
|
|
44
44
|
// decide whether the client is on this machine, so it has to be the
|
|
45
45
|
// real command rather than the adapter id: `0.57.0`.
|
|
46
|
-
client: { command: "gemini",
|
|
46
|
+
client: { command: "gemini", package: "@google/gemini-cli", certificationName: "gemini-cli",
|
|
47
|
+
versionArgs: ["--version"] },
|
|
47
48
|
certification,
|
|
48
49
|
capabilities: {
|
|
49
50
|
lifecycle: { sessionStart: true, sessionEnd: true },
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
|
|
7
7
|
import { acccreatedFile, bakeSkillCommand, blankJson, ownVersion, removeIfEmpty,
|
|
8
8
|
removeInstalledTree,
|
|
9
|
-
stampPluginVersion, writeCliShim, writeForeignJson, writeHookShim }
|
|
9
|
+
shellQuote, stampPluginVersion, windowsHookCommand, writeCliShim, writeForeignJson, writeHookShim }
|
|
10
10
|
from "@agents-can-communicate/adapter-sdk";
|
|
11
11
|
|
|
12
12
|
const bundle = fileURLToPath(new URL("../extension", import.meta.url));
|
|
@@ -46,11 +46,16 @@ const writeJson = (file, value) => writeForeignJson(file, value,
|
|
|
46
46
|
const isOurs = hook => typeof hook?.name === "string" && hook.name.startsWith(OWNER_PREFIX);
|
|
47
47
|
|
|
48
48
|
// Replace the bundle's placeholder command with the shim that was just written.
|
|
49
|
-
|
|
49
|
+
// On Windows this client runs hooks through PowerShell, which has no `sh`: the
|
|
50
|
+
// pinned node runs the Node shim there.
|
|
51
|
+
const withShim = (wiring, shim, windowsNode = null) => ({ hooks: Object.fromEntries(
|
|
50
52
|
Object.entries(wiring.hooks).map(([event, entries]) => [event, entries.map(entry => ({
|
|
51
53
|
...entry,
|
|
52
|
-
hooks: entry.hooks.map(hook =>
|
|
53
|
-
|
|
54
|
+
hooks: entry.hooks.map(hook => {
|
|
55
|
+
const kind = hook.command.split(" ").pop();
|
|
56
|
+
return { ...hook, command: windowsNode === null ? `sh ${shellQuote(shim)} ${kind}`
|
|
57
|
+
: windowsHookCommand("powershell", { node: windowsNode, shim, args: [kind] }) };
|
|
58
|
+
}),
|
|
54
59
|
}))])) });
|
|
55
60
|
|
|
56
61
|
/**
|
|
@@ -62,7 +67,8 @@ const withShim = (wiring, shim) => ({ hooks: Object.fromEntries(
|
|
|
62
67
|
* No environment variable is copied or persisted. The extension declares what
|
|
63
68
|
* it needs; secrets stay where the user put them.
|
|
64
69
|
*/
|
|
65
|
-
export async function installGeminiExtension({ home, runner, cli, node
|
|
70
|
+
export async function installGeminiExtension({ home, runner, cli, node,
|
|
71
|
+
hostPlatform = process.platform }) {
|
|
66
72
|
// Read before writing: a settings file that will not parse must not be found
|
|
67
73
|
// out after the extension tree is already on disk.
|
|
68
74
|
const found = await readJson(settingsPath(home), null);
|
|
@@ -82,15 +88,15 @@ export async function installGeminiExtension({ home, runner, cli, node }) {
|
|
|
82
88
|
await rm(path.join(target, "hooks", "hooks.json"), { force: true });
|
|
83
89
|
// The skill ships with a placeholder where the command belongs: `acc` is
|
|
84
90
|
// not on PATH everywhere, and an agent that cannot run it improvises.
|
|
85
|
-
const cliShim = await writeCliShim({ dir: target, cli, node });
|
|
86
|
-
await bakeSkillCommand({ root: target, cliShim });
|
|
91
|
+
const cliShim = await writeCliShim({ dir: target, cli, node, platform: hostPlatform });
|
|
92
|
+
await bakeSkillCommand({ root: target, cliShim, platform: hostPlatform });
|
|
87
93
|
// This client offers no plugin-root variable in a hook command, so the shim's
|
|
88
94
|
// absolute path is written in at install time.
|
|
89
95
|
const shim = await writeHookShim({ dir: path.join(target, "hooks"),
|
|
90
|
-
adapterId: "gemini_cli", runner, node });
|
|
96
|
+
adapterId: "gemini_cli", runner, node, platform: hostPlatform });
|
|
91
97
|
|
|
92
98
|
const ours = withShim(await readJson(path.join(bundle, "hooks", "hooks.json"),
|
|
93
|
-
{ hooks: {} }), shim);
|
|
99
|
+
{ hooks: {} }), shim, hostPlatform === "win32" ? node ?? process.execPath : null);
|
|
94
100
|
const file = settingsPath(home);
|
|
95
101
|
const merged = { ...existing, hooks: { ...(existing.hooks ?? {}) } };
|
|
96
102
|
// Recorded now: afterwards a settings file holding `{}` looks the same
|
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
6
|
-
import { bakeSkillCommand, removeInstalledTree,
|
|
6
|
+
import { bakeSkillCommand, removeInstalledTree, shellQuote, windowsHookCommand, writeCliShim,
|
|
7
|
+
writeHookShim }
|
|
7
8
|
from "@agents-can-communicate/adapter-sdk";
|
|
8
9
|
|
|
9
10
|
const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
|
|
@@ -15,7 +16,9 @@ export const grokHomeOf = context =>
|
|
|
15
16
|
context.grokHome ?? path.join(context.home, ".grok");
|
|
16
17
|
|
|
17
18
|
export const hooksFile = home => path.join(home, "hooks", HOOKS_NAME);
|
|
18
|
-
|
|
19
|
+
// Windows gets the Node shim: no Windows client can be relied on to have `sh`.
|
|
20
|
+
export const shimPath = (home, platform = process.platform) => path.join(home, "hooks",
|
|
21
|
+
platform === "win32" ? SHIM_NAME.replace(/\.sh$/, ".mjs") : SHIM_NAME);
|
|
19
22
|
export const skillPath = home => path.join(home, "skills", "acc");
|
|
20
23
|
|
|
21
24
|
async function readJson(file, fallback) {
|
|
@@ -46,37 +49,44 @@ const writeJson = async (file, value) => {
|
|
|
46
49
|
// for plugins; we install into ~/.grok/hooks, which is always trusted and does
|
|
47
50
|
// not need [plugins].enabled. The shim's absolute path is written in at install
|
|
48
51
|
// time, the same lesson Codex taught: a relative hook command fails silently.
|
|
49
|
-
|
|
52
|
+
// On Windows this client runs hooks through the shell it detects - pwsh, Git
|
|
53
|
+
// Bash or cmd - so the command is written in the one form all three read.
|
|
54
|
+
const withShim = (wiring, shim, windowsNode = null) => ({
|
|
50
55
|
description: wiring.description,
|
|
51
56
|
hooks: Object.fromEntries(Object.entries(wiring.hooks).map(([event, entries]) =>
|
|
52
57
|
[event, entries.map(entry => ({
|
|
53
58
|
...entry,
|
|
54
|
-
hooks: entry.hooks.map(hook =>
|
|
55
|
-
|
|
59
|
+
hooks: entry.hooks.map(hook => {
|
|
60
|
+
const kind = hook.command.split(" ").pop();
|
|
61
|
+
return { ...hook, command: windowsNode === null ? `sh ${shellQuote(shim)} ${kind}`
|
|
62
|
+
: windowsHookCommand("portable", { node: windowsNode, shim, args: [kind] }) };
|
|
63
|
+
}),
|
|
56
64
|
}))])),
|
|
57
65
|
});
|
|
58
66
|
|
|
59
|
-
export async function installGrokHooks({ grokHome, home, runner, cli, node
|
|
67
|
+
export async function installGrokHooks({ grokHome, home, runner, cli, node,
|
|
68
|
+
hostPlatform = process.platform }) {
|
|
60
69
|
const root = grokHome ?? grokHomeOf({ home, grokHome });
|
|
61
70
|
const template = await readJson(path.join(bundle, "hooks", "hooks.json"), { hooks: {} });
|
|
62
71
|
const shim = await writeHookShim({ dir: path.join(root, "hooks"), adapterId: "grok",
|
|
63
|
-
runner, node, name: SHIM_NAME });
|
|
72
|
+
runner, node, name: SHIM_NAME, platform: hostPlatform });
|
|
64
73
|
|
|
65
74
|
const skills = skillPath(root);
|
|
66
75
|
await rm(skills, { recursive: true, force: true });
|
|
67
76
|
await mkdir(path.dirname(skills), { recursive: true });
|
|
68
77
|
await cp(path.join(bundle, "skills", "acc"), skills, { recursive: true });
|
|
69
|
-
const cliShim = await writeCliShim({ dir: skills, cli, node });
|
|
70
|
-
await bakeSkillCommand({ root: skills, cliShim });
|
|
78
|
+
const cliShim = await writeCliShim({ dir: skills, cli, node, platform: hostPlatform });
|
|
79
|
+
await bakeSkillCommand({ root: skills, cliShim, platform: hostPlatform });
|
|
71
80
|
|
|
72
|
-
await writeJson(hooksFile(root), withShim(template, shim
|
|
81
|
+
await writeJson(hooksFile(root), withShim(template, shim,
|
|
82
|
+
hostPlatform === "win32" ? node ?? process.execPath : null));
|
|
73
83
|
return { ok: true, changes: [hooksFile(root), shim, skills], diagnostics: [] };
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
export async function uninstallGrokHooks({ grokHome, home, keep = [] }) {
|
|
86
|
+
export async function uninstallGrokHooks({ grokHome, home, keep = [], hostPlatform }) {
|
|
77
87
|
const root = grokHome ?? grokHomeOf({ home, grokHome });
|
|
78
88
|
const changes = [];
|
|
79
|
-
for (const target of [hooksFile(root), shimPath(root), skillPath(root)]) {
|
|
89
|
+
for (const target of [hooksFile(root), shimPath(root, hostPlatform), skillPath(root)]) {
|
|
80
90
|
if (await removeInstalledTree(target, keep)) changes.push(target);
|
|
81
91
|
}
|
|
82
92
|
return { ok: true, changes, diagnostics: [] };
|
|
@@ -87,7 +97,8 @@ export async function detectGrok({ grokHome, home }) {
|
|
|
87
97
|
const wired = await readJson(hooksFile(root), null);
|
|
88
98
|
const installed = typeof wired?.hooks?.SessionStart?.[0]?.hooks?.[0]?.command
|
|
89
99
|
=== "string"
|
|
90
|
-
|
|
100
|
+
// The shell shim on POSIX, the Node shim on Windows.
|
|
101
|
+
&& /acc-hook\.(?:sh|mjs)\b/.test(wired.hooks.SessionStart[0].hooks[0].command);
|
|
91
102
|
return { ok: true, changes: [],
|
|
92
103
|
diagnostics: [installed ? "acc hooks registered" : "acc hooks not registered"] };
|
|
93
104
|
}
|
|
@@ -96,7 +107,7 @@ export function planGrokInstall(context) {
|
|
|
96
107
|
const root = grokHomeOf(context);
|
|
97
108
|
return [
|
|
98
109
|
{ path: hooksFile(root), kind: "tree" },
|
|
99
|
-
{ path: shimPath(root), kind: "tree" },
|
|
110
|
+
{ path: shimPath(root, context.hostPlatform), kind: "tree" },
|
|
100
111
|
{ path: skillPath(root), kind: "tree" },
|
|
101
112
|
];
|
|
102
113
|
}
|
|
@@ -32,7 +32,8 @@ export function createKimiAdapter() {
|
|
|
32
32
|
// The binary this client actually installs. Probed for a version to
|
|
33
33
|
// decide whether the client is on this machine, so it has to be the
|
|
34
34
|
// real command rather than the adapter id: `0.36.1`.
|
|
35
|
-
client: { command: "kimi",
|
|
35
|
+
client: { command: "kimi", package: "@moonshot-ai/kimi-code", certificationName: "kimi",
|
|
36
|
+
versionArgs: ["--version"] },
|
|
36
37
|
certification,
|
|
37
38
|
capabilities: {
|
|
38
39
|
lifecycle: { sessionStart: true, heartbeat: true },
|
|
@@ -4,9 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
import { assertRunner, bakeSkillCommand, blankText, defaultRunner, removeIfEmpty,
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { assertRunner, bakeSkillCommand, blankText, defaultRunner, ownVersion, removeIfEmpty,
|
|
8
|
+
removeInstalledTree, runnerExists, shellQuote, stampPluginVersion, windowsHookCommand,
|
|
9
|
+
writeCliShim, writeForeignJson }
|
|
10
10
|
from "@agents-can-communicate/adapter-sdk";
|
|
11
11
|
|
|
12
12
|
const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
|
|
@@ -99,13 +99,17 @@ export function stripBlock(source) {
|
|
|
99
99
|
return kept.join("\n");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export function renderBlock(runner, node = process.execPath) {
|
|
102
|
+
export function renderBlock(runner, node = process.execPath, platform = process.platform) {
|
|
103
103
|
const entries = WIRING.map(({ event, kind, matcher }) => {
|
|
104
104
|
// Both paths are shell-quoted inside the TOML string: a hook runs through a
|
|
105
105
|
// shell, and a space in either path would otherwise split the command.
|
|
106
106
|
// The interpreter is named outright rather than left to PATH, which a hook's
|
|
107
|
-
// environment does not reliably carry.
|
|
108
|
-
|
|
107
|
+
// environment does not reliably carry. On Windows that shell is cmd.exe, and
|
|
108
|
+
// a TOML-escaped path inside the command would reach it with every
|
|
109
|
+
// backslash doubled.
|
|
110
|
+
const command = platform === "win32"
|
|
111
|
+
? windowsHookCommand("cmd", { node, shim: runner, args: ["kimi", kind] })
|
|
112
|
+
: `${shellQuote(node)} ${shellQuote(runner)} kimi ${kind}`;
|
|
109
113
|
const lines = ["[[hooks]]", `event = ${tomlString(event)}`,
|
|
110
114
|
`command = ${tomlString(command)}`];
|
|
111
115
|
if (matcher !== undefined) lines.push(`matcher = ${tomlString(matcher)}`);
|
|
@@ -121,7 +125,8 @@ const registerPlugin = (registry, root) => {
|
|
|
121
125
|
plugins: [...plugins, { id: PLUGIN_NAME, root, source: "local", enabled: true }] };
|
|
122
126
|
};
|
|
123
127
|
|
|
124
|
-
export async function installKimiPlugin({ home, runner = defaultRunner(), node, cli
|
|
128
|
+
export async function installKimiPlugin({ home, runner = defaultRunner(), node, cli,
|
|
129
|
+
hostPlatform = process.platform }) {
|
|
125
130
|
// A hook whose command does not exist fails silently, on every event, for as
|
|
126
131
|
// long as it stays installed: the client reports nothing and ACC simply never
|
|
127
132
|
// sees a session. Writing that entry and hoping is worse than refusing.
|
|
@@ -139,8 +144,8 @@ export async function installKimiPlugin({ home, runner = defaultRunner(), node,
|
|
|
139
144
|
version: await ownVersion(import.meta.url), io: { readFile, writeFile } });
|
|
140
145
|
// The skill ships with a placeholder where the command belongs: `acc` is
|
|
141
146
|
// not on PATH everywhere, and an agent that cannot run it improvises.
|
|
142
|
-
const cliShim = await writeCliShim({ dir: target, cli, node });
|
|
143
|
-
await bakeSkillCommand({ root: target, cliShim });
|
|
147
|
+
const cliShim = await writeCliShim({ dir: target, cli, node, platform: hostPlatform });
|
|
148
|
+
await bakeSkillCommand({ root: target, cliShim, platform: hostPlatform });
|
|
144
149
|
|
|
145
150
|
const file = configPath(home);
|
|
146
151
|
const existing = await readText(file, "");
|
|
@@ -149,7 +154,7 @@ export async function installKimiPlugin({ home, runner = defaultRunner(), node,
|
|
|
149
154
|
// user's last section cannot swallow our entries.
|
|
150
155
|
const separator = withoutOurs === "" || withoutOurs.endsWith("\n") ? "" : "\n";
|
|
151
156
|
await mkdir(path.dirname(file), { recursive: true });
|
|
152
|
-
await writeFile(file, `${withoutOurs}${separator}${renderBlock(runner, node)}\n`);
|
|
157
|
+
await writeFile(file, `${withoutOurs}${separator}${renderBlock(runner, node, hostPlatform)}\n`);
|
|
153
158
|
|
|
154
159
|
const registry = registryPath(home);
|
|
155
160
|
await mkdir(path.dirname(registry), { recursive: true });
|
|
@@ -81,6 +81,41 @@ export function assertCapabilities(declared = {}, implementation = {}, certifica
|
|
|
81
81
|
return freezeCapabilities(resolved);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The other products whose hooks one adapter serves. Antigravity 2.0 runs the
|
|
86
|
+
* same `~/.gemini/config/hooks.json` as Antigravity CLI, on its own version
|
|
87
|
+
* line, so its evidence is recorded under its own certification name. A
|
|
88
|
+
* variant names a product, not a launch mode: one adapter's hooks, one more
|
|
89
|
+
* version line.
|
|
90
|
+
*/
|
|
91
|
+
function validateClientVariants(variants, primary) {
|
|
92
|
+
if (variants === undefined) return [];
|
|
93
|
+
if (!Array.isArray(variants)) usage("client.variants must be an array", { variants });
|
|
94
|
+
const seen = new Set([primary]);
|
|
95
|
+
return variants.map((variant, index) => {
|
|
96
|
+
if (variant === null || typeof variant !== "object" || Array.isArray(variant)) {
|
|
97
|
+
usage(`client.variants entry ${index} must be an object`);
|
|
98
|
+
}
|
|
99
|
+
for (const key of Object.keys(variant)) {
|
|
100
|
+
if (!["certificationName", "displayName"].includes(key)) {
|
|
101
|
+
usage(`client.variants entry ${index} has unknown field ${key}`, { key });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const { certificationName, displayName } = variant;
|
|
105
|
+
if (typeof certificationName !== "string" || certificationName.trim() === "") {
|
|
106
|
+
usage(`client.variants entry ${index} certificationName must be a non-empty string`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof displayName !== "string" || displayName.trim() === "") {
|
|
109
|
+
usage(`client.variants entry ${index} displayName must be a non-empty string`);
|
|
110
|
+
}
|
|
111
|
+
if (seen.has(certificationName)) {
|
|
112
|
+
usage(`client.variants entry ${index} repeats ${certificationName}`, { certificationName });
|
|
113
|
+
}
|
|
114
|
+
seen.add(certificationName);
|
|
115
|
+
return { certificationName, displayName };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
export function defineAdapter(manifest) {
|
|
85
120
|
if (typeof manifest?.id !== "string") usage("an adapter must declare an id");
|
|
86
121
|
assertPortableId(manifest.id, "adapter id");
|
|
@@ -104,15 +139,25 @@ export function defineAdapter(manifest) {
|
|
|
104
139
|
const certification = validateCertification(manifest.certification);
|
|
105
140
|
const known = new Set(Object.entries(CAPABILITY_SHAPE)
|
|
106
141
|
.flatMap(([group, names]) => names.map(name => `${group}.${name}`)));
|
|
142
|
+
const primary = manifest.client.certificationName ?? manifest.client.command;
|
|
143
|
+
const clients = new Set([primary, ...validateClientVariants(manifest.client.variants, primary)
|
|
144
|
+
.map(variant => variant.certificationName)]);
|
|
107
145
|
for (const item of certification.evidence) {
|
|
108
146
|
if (!known.has(item.capability)) {
|
|
109
147
|
usage(`unknown certified capability: ${item.capability}`,
|
|
110
148
|
{ capability: item.capability });
|
|
111
149
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
if (!clients.has(item.client)) {
|
|
151
|
+
usage(`certification evidence client ${item.client} does not match ${primary}`,
|
|
152
|
+
{ evidenceClient: item.client, client: primary });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// How the hook runner learns which product and which version a session runs
|
|
156
|
+
// in, from the process that runs it rather than the binary on PATH. Both are
|
|
157
|
+
// optional; an adapter without them is matched by client.command.
|
|
158
|
+
for (const method of ["identifyClientProcess", "clientVersionOf"]) {
|
|
159
|
+
if (manifest[method] !== undefined && typeof manifest[method] !== "function") {
|
|
160
|
+
usage(`optional client method ${method} must be a function`, { id: manifest.id, method });
|
|
116
161
|
}
|
|
117
162
|
}
|
|
118
163
|
// A floor said "judge later versions by this capture". Evidence now does that
|
|
@@ -139,7 +184,7 @@ export function defineAdapter(manifest) {
|
|
|
139
184
|
if (manifest.nativeDelivery !== undefined) {
|
|
140
185
|
const client = manifest.client.certificationName ?? manifest.client.command;
|
|
141
186
|
native.nativeDelivery = validateNativeDeliveryContract(manifest.nativeDelivery,
|
|
142
|
-
{ certification, client });
|
|
187
|
+
{ certification, client, clients: [...clients] });
|
|
143
188
|
for (const method of NATIVE_METHODS) {
|
|
144
189
|
if (typeof manifest[method] !== "function") {
|
|
145
190
|
usage(`a native delivery contract requires ${method}()`, { id: manifest.id, method });
|
|
@@ -178,12 +178,21 @@ function falseCapabilities() {
|
|
|
178
178
|
* first capture, whose version is returned) or `recorded-failure` (a capture at
|
|
179
179
|
* the returned version recorded the loss).
|
|
180
180
|
*/
|
|
181
|
-
export function capabilityEvidence(adapter, { clientVersion, platform } = {},
|
|
181
|
+
export function capabilityEvidence(adapter, { clientVersion, platform, clientName } = {},
|
|
182
|
+
capability) {
|
|
182
183
|
const [group, name] = capability.split(".");
|
|
183
184
|
if (adapter.capabilities?.[group]?.[name] !== true) {
|
|
184
185
|
return { granted: false, reason: "undeclared", version: null };
|
|
185
186
|
}
|
|
186
|
-
|
|
187
|
+
// `clientName` names the product the session runs in when it is one of the
|
|
188
|
+
// adapter's variants. Another product's evidence never stands in for it: its
|
|
189
|
+
// versions count on a different line. A name the adapter does not declare
|
|
190
|
+
// has no evidence at all.
|
|
191
|
+
const primary = adapter.client?.certificationName ?? adapter.client?.command;
|
|
192
|
+
const declared = [primary, ...(adapter.client?.variants ?? [])
|
|
193
|
+
.map(variant => variant?.certificationName)];
|
|
194
|
+
const client = typeof clientName === "string" ? clientName : primary;
|
|
195
|
+
if (!declared.includes(client)) return { granted: false, reason: "unobserved", version: null };
|
|
187
196
|
const named = (adapter.certification?.evidence ?? [])
|
|
188
197
|
.filter(item => item.client === client && item.capability === capability);
|
|
189
198
|
if (named.length === 0) return { granted: false, reason: "unobserved", version: null };
|