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