agents-can-communicate 0.8.4 → 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 +32 -0
- package/docs/ARCHITECTURE.md +7 -4
- 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 +74 -12
- package/docs/UPGRADING.md +50 -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 +87 -13
- package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +16 -6
- 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 +66 -26
- 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 +11 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +6 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +82 -17
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +9 -2
- 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/main.mjs +2 -1
- 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/managed-runtime/worker.mjs +18 -3
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +15 -4
- 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/delivery-router/src/native-reason.mjs +6 -2
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +9 -5
- 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 +7 -2
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -3
- 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 +167 -34
- package/node_modules/@agents-can-communicate/installer/package.json +3 -2
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +11 -2
- 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);
|
|
@@ -238,8 +250,8 @@ async function openContext({ event, adapterId, dataHome, runtime, env, deadline
|
|
|
238
250
|
// offered. Runs after the heartbeat and the bounded native retry.
|
|
239
251
|
// `render` is how the projected text reaches this client: before a turn it is
|
|
240
252
|
// the adapter's context envelope, at the end of one it is a continuation.
|
|
241
|
-
async function projectTurn({ binding, context, adapter, adapterId,
|
|
242
|
-
|
|
253
|
+
async function projectTurn({ binding, context, adapter, adapterId, render = null,
|
|
254
|
+
activationHint = null, activationNotice = null, repeats = true }) {
|
|
243
255
|
const sync = await context.service.sync({ sessionId: binding.accSessionId,
|
|
244
256
|
cursor: null, scope: "delta" });
|
|
245
257
|
|
|
@@ -283,11 +295,17 @@ async function projectTurn({ binding, context, adapter, adapterId,
|
|
|
283
295
|
const owner = hintFits ? `${header}\n${activationHint}` : header;
|
|
284
296
|
const withHint = (outcome, attached) => ({ ...outcome,
|
|
285
297
|
activationHintIncluded: attached === true && outcome.stdout !== "" });
|
|
298
|
+
// The ask's line for the user travels with its line for the model, or not
|
|
299
|
+
// at all: the adapter shows it where the client shows a hook's message.
|
|
300
|
+
const notice = hintFits && activationNotice !== null ? { userMessage: activationNotice } : null;
|
|
301
|
+
const inject = text => notice === null ? adapter.injectOutcome?.(text)
|
|
302
|
+
: adapter.injectOutcome?.(text, notice);
|
|
303
|
+
const renderText = render ?? inject;
|
|
286
304
|
// A peer can join after this prompt has begun. The current turn must already
|
|
287
305
|
// have its own arguments when it needs inbox/reply, without reattaching or
|
|
288
306
|
// waiting for another user prompt. Solo emits identity, not a peer notice.
|
|
289
307
|
if (sync.solo && messages.length === 0) {
|
|
290
|
-
return withHint(ownerOnlyOutcome(
|
|
308
|
+
return withHint(ownerOnlyOutcome(inject, owner, totalBudget),
|
|
291
309
|
hintFits);
|
|
292
310
|
}
|
|
293
311
|
|
|
@@ -336,7 +354,7 @@ async function projectTurn({ binding, context, adapter, adapterId,
|
|
|
336
354
|
// Own claims make sync non-solo but produce no peer context. They cannot
|
|
337
355
|
// remove this turn's identity or consume a nonexistent body separator.
|
|
338
356
|
if (body === "" && messages.length === 0) {
|
|
339
|
-
return withHint(ownerOnlyOutcome(
|
|
357
|
+
return withHint(ownerOnlyOutcome(inject, owner, totalBudget),
|
|
340
358
|
hintFits);
|
|
341
359
|
}
|
|
342
360
|
const projected = body === "" ? "" : ownerFits ? `${owner}\n${body}` : body;
|
|
@@ -361,7 +379,7 @@ async function projectTurn({ binding, context, adapter, adapterId,
|
|
|
361
379
|
// The entry point owns the transport boundary. This handler only prepares
|
|
362
380
|
// offer inputs; recording them here would claim delivery before stdout's
|
|
363
381
|
// callback proves that the bytes crossed.
|
|
364
|
-
const outcome = { stdout: "", ...
|
|
382
|
+
const outcome = { stdout: "", ...renderText(projected) };
|
|
365
383
|
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
366
384
|
return withHint({ ...outcome,
|
|
367
385
|
stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
|
|
@@ -373,7 +391,8 @@ async function projectTurn({ binding, context, adapter, adapterId,
|
|
|
373
391
|
// so the reservation goes with it.
|
|
374
392
|
async function projectActivation(input, offered) {
|
|
375
393
|
try {
|
|
376
|
-
const turn = await projectTurn({ ...input, activationHint: offered?.line ?? null
|
|
394
|
+
const turn = await projectTurn({ ...input, activationHint: offered?.line ?? null,
|
|
395
|
+
activationNotice: offered?.userMessage ?? null });
|
|
377
396
|
const included = turn.activationHintIncluded === true;
|
|
378
397
|
delete turn.activationHintIncluded;
|
|
379
398
|
if (!offered) return turn;
|
|
@@ -389,13 +408,71 @@ async function projectActivation(input, offered) {
|
|
|
389
408
|
}
|
|
390
409
|
}
|
|
391
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
|
+
|
|
392
468
|
const HANDLERS = {
|
|
393
469
|
// `knownClientPid` and `pins: false` are registration's (#167): it runs in
|
|
394
470
|
// a peer's process, so the client's pid is handed over rather than found in
|
|
395
471
|
// this process's ancestry, and it pins nothing because no hook code of any
|
|
396
472
|
// generation runs for the session until its first real hook pins its own.
|
|
397
473
|
async sessionStart({ event, context, adapter, adapterId, binding, paths,
|
|
398
|
-
readProcessTable, probeClientVersion, platform, deadline, knownClientPid, pins = true
|
|
474
|
+
readProcessTable, probeClientVersion, platform, deadline, knownClientPid, pins = true,
|
|
475
|
+
tableRead: started = null, mark = () => {}, clientRestarted = false }) {
|
|
399
476
|
// Where this session's pin lives, resolved once and reused at every write
|
|
400
477
|
// below. Beside `paths.root`, never inside it: see `managerRootFor`.
|
|
401
478
|
const pinRoot = managerRootFor(context.dataHome);
|
|
@@ -412,23 +489,42 @@ const HANDLERS = {
|
|
|
412
489
|
await writePin({ root: pinRoot, harnessSessionId: event.sessionId, runtimeRoot: facts.runtimeRoot,
|
|
413
490
|
version: facts.version, storeVersion: facts.storeVersion, clientPid: binding.clientPid });
|
|
414
491
|
}
|
|
415
|
-
const clientVersion = await probeClientVersion(adapter,
|
|
416
|
-
{ timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
|
|
417
|
-
assertHookBudget(deadline);
|
|
418
|
-
const clientFacts = { clientVersion, platform };
|
|
419
|
-
const capabilities = effectiveCapabilities(adapter, clientFacts);
|
|
420
492
|
// Once per session, never per turn. A client that cannot be found yields
|
|
421
493
|
// null, and the session is then judged by age alone - which is exactly the
|
|
422
494
|
// behaviour every session had before this existed.
|
|
423
495
|
const command = adapter.client?.command ?? null;
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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 });
|
|
428
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");
|
|
429
523
|
const clientPid = Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
430
|
-
const native = hookBinding =>
|
|
431
|
-
|
|
524
|
+
const native = hookBinding => {
|
|
525
|
+
mark("binding live delivery");
|
|
526
|
+
return bindNative({ adapter, event, hookBinding, ...clientFacts, context, paths, deadline });
|
|
527
|
+
};
|
|
432
528
|
const metadata = {
|
|
433
529
|
pid,
|
|
434
530
|
enforcement: capabilities.guards?.beforeWrite === true ? "guarded" : "advisory",
|
|
@@ -439,13 +535,18 @@ const HANDLERS = {
|
|
|
439
535
|
};
|
|
440
536
|
if (event.kind === "beforeTurn" && binding !== null) {
|
|
441
537
|
// A genuine prompt can resume after an ephemeral removal or a durable
|
|
442
|
-
// close
|
|
443
|
-
//
|
|
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.
|
|
444
544
|
const previous = await context.service.locateSession(binding.accSessionId);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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");
|
|
449
550
|
}
|
|
450
551
|
if (binding !== null) {
|
|
451
552
|
const resumed = await context.service.resumeSession({
|
|
@@ -564,6 +665,21 @@ const HANDLERS = {
|
|
|
564
665
|
const turn = await projectActivation({ ...input, binding: fresh }, offered);
|
|
565
666
|
return { ...turn, nativeBinding: started.nativeBinding };
|
|
566
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
|
+
}
|
|
567
683
|
// A turn is the clearest sign a session is alive. Never a reason to fail:
|
|
568
684
|
// this runs in front of somebody's prompt.
|
|
569
685
|
await context.service.heartbeatSession({ sessionId: binding.accSessionId,
|
|
@@ -697,35 +813,51 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
697
813
|
const deadline = Date.now() + budgetMs;
|
|
698
814
|
const fallback = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
|
|
699
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; };
|
|
700
819
|
const execute = async () => {
|
|
701
820
|
assertHookBudget(deadline);
|
|
702
821
|
const adapter = adapters?.[adapterId];
|
|
703
822
|
if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
|
|
704
823
|
|
|
705
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");
|
|
706
833
|
const context = await openContext({ event, adapterId, dataHome, runtime, env, deadline });
|
|
834
|
+
assertOwnerQuotable(context.workspaceCwd, context.workspaceRef, String(platform).split("-")[0]);
|
|
707
835
|
const handler = HANDLERS[event.kind];
|
|
708
836
|
const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
|
|
709
837
|
const invoke = async () => {
|
|
710
838
|
assertHookBudget(deadline);
|
|
711
839
|
if (lifecycle) {
|
|
840
|
+
mark("opening the store");
|
|
712
841
|
// A previous holder may have died after journalling its session while
|
|
713
842
|
// this process waited. Recover that write before interpreting its binding.
|
|
714
843
|
const store = await openFilesystemStore({ root: context.paths.root, clock: runtime.clock,
|
|
715
844
|
ids: runtime.ids, workspaceId: context.descriptor.id, deadlineAt: deadline });
|
|
716
845
|
context.service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
|
|
717
846
|
}
|
|
847
|
+
mark("reading the session binding");
|
|
718
848
|
const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
|
|
719
849
|
harnessSessionId: event.sessionId });
|
|
720
850
|
assertHookBudget(deadline);
|
|
851
|
+
mark("handling the event");
|
|
721
852
|
// The raw payload goes to a handler only to be handed back to the adapter
|
|
722
853
|
// that produced it; nothing in core reads it.
|
|
723
854
|
const result = handler === undefined ? {} : await handler({ event, context, adapter, adapterId,
|
|
724
855
|
binding, paths: context.paths, payload,
|
|
725
|
-
readProcessTable, probeClientVersion, platform, deadline });
|
|
856
|
+
readProcessTable, probeClientVersion, platform, deadline, tableRead, mark });
|
|
726
857
|
return appendToolOwner(appendStartOwner(result, { event, context, adapter }),
|
|
727
858
|
{ event, binding, context, adapter });
|
|
728
859
|
};
|
|
860
|
+
if (lifecycle) mark("waiting for the session lock");
|
|
729
861
|
const work = lifecycle
|
|
730
862
|
? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
|
|
731
863
|
clock: runtime.clock, deadlineAt: deadline }, invoke)
|
|
@@ -748,6 +880,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
748
880
|
};
|
|
749
881
|
delete result.offerInputs;
|
|
750
882
|
|
|
883
|
+
mark("reading the status");
|
|
751
884
|
const status = await context.service.collectStatus({
|
|
752
885
|
workspaceId: context.descriptor.id });
|
|
753
886
|
assertHookBudget(deadline);
|
|
@@ -762,15 +895,15 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
762
895
|
// final status read. Store/binding deadlines fence undecided writes; an
|
|
763
896
|
// activated journal still completes under its writer lock after expiry.
|
|
764
897
|
const budget = new Promise(resolve => {
|
|
765
|
-
timer = setTimeout(() => resolve({ ...fallback, timedOut: true }),
|
|
898
|
+
timer = setTimeout(() => resolve({ ...fallback, timedOut: true, phase: trail.phase }),
|
|
766
899
|
Math.max(0, deadline - Date.now()));
|
|
767
900
|
});
|
|
768
901
|
return await Promise.race([execute(), budget]);
|
|
769
902
|
} catch (error) {
|
|
770
903
|
return { ...fallback, failed: true, reason: error.message,
|
|
771
|
-
...(error instanceof AccError && error.details?.reasonCode
|
|
772
|
-
? { failureCode:
|
|
773
|
-
...(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 } : {}) };
|
|
774
907
|
} finally {
|
|
775
908
|
clearTimeout(timer);
|
|
776
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/"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { isLaunchOption } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
|
|
1
3
|
// Shared presentation of observed delivery facts. Vendor probes provide closed
|
|
2
4
|
// reasons; installation and doctor must not turn a version floor into readiness.
|
|
3
5
|
const REASONS = Object.freeze({
|
|
4
|
-
|
|
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",
|
|
5
8
|
version_unavailable: "the client version could not be verified",
|
|
6
9
|
below_minimum_version: "the client is below the native delivery minimum version",
|
|
7
10
|
known_bad_version: "this client version has a known native delivery failure",
|
|
@@ -16,10 +19,16 @@ const REASONS = Object.freeze({
|
|
|
16
19
|
+ "cannot reach",
|
|
17
20
|
});
|
|
18
21
|
|
|
19
|
-
export function describeNativeReason(reason, { clientVersion, minimumVersion } = {}) {
|
|
22
|
+
export function describeNativeReason(reason, { clientVersion, minimumVersion, launchOption } = {}) {
|
|
20
23
|
if (reason === "below_minimum_version" && clientVersion && minimumVersion) {
|
|
21
24
|
return `client ${clientVersion} needs version ${minimumVersion} or newer for native delivery`;
|
|
22
25
|
}
|
|
26
|
+
// A client can run embedded because of how it was started - Codex does for
|
|
27
|
+
// `--search`, `-c`, `--profile` and others while its daemon runs.
|
|
28
|
+
if (reason === "client_session_embedded" && isLaunchOption(launchOption)) {
|
|
29
|
+
return `the client was started with ${launchOption}, which runs this session on its own `
|
|
30
|
+
+ "embedded service that peers cannot reach";
|
|
31
|
+
}
|
|
23
32
|
return REASONS[reason] ?? reason ?? "readiness is unverified";
|
|
24
33
|
}
|
|
25
34
|
export const describeDeliveryFallback = entry => entry.capabilities?.delivery?.nextTurn === true
|
|
@@ -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
|
}
|