agents-can-communicate 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -52
- package/SECURITY.md +1 -1
- package/bin/acc-bootstrap.mjs +5 -54
- package/bin/acc-claude-channel.mjs +8 -175
- package/bin/acc-hook.mjs +4 -133
- package/bin/acc-mcp.mjs +4 -59
- package/bin/acc-update-worker.mjs +8 -0
- package/bin/acc.mjs +5 -39
- package/bin/entrypoints/acc-bootstrap.mjs +47 -0
- package/bin/entrypoints/acc-claude-channel.mjs +149 -0
- package/bin/entrypoints/acc-hook.mjs +58 -0
- package/bin/entrypoints/acc-mcp.mjs +70 -0
- package/bin/entrypoints/acc.mjs +51 -0
- package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
- package/bin/entrypoints/hook-output.mjs +77 -0
- package/docs/ADAPTER_AUTHORING.md +52 -8
- package/docs/ARCHITECTURE.md +64 -11
- package/docs/CAPABILITIES.md +41 -37
- package/docs/CLI.md +145 -25
- package/docs/CONCEPTS.md +13 -6
- package/docs/CONFIGURATION.md +37 -22
- package/docs/DESIGN_DECISIONS.md +16 -11
- package/docs/GETTING_STARTED.md +41 -7
- package/docs/GLOSSARY.md +9 -3
- package/docs/HOW_IT_WORKS.md +32 -11
- package/docs/MCP.md +91 -24
- package/docs/PROTOCOL.md +69 -12
- package/docs/RELEASING.md +28 -10
- package/docs/SECURITY_MODEL.md +47 -13
- package/docs/TROUBLESHOOTING.md +102 -20
- package/docs/UPGRADING.md +154 -0
- package/docs/WHY_ACC.md +5 -4
- package/docs/index.md +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +106 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +106 -5
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
- package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +106 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +122 -8
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +5 -3
- package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +9 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +106 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
- package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/package.json +4 -2
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
- package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +24 -5
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +64 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +78 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +123 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
- package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +9 -25
- package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
- package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
- package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
- package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
- package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
- package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +30 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +172 -67
- package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
- package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
- package/package.json +2 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { assertPortableId } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
export const ownerHeader = binding => "ACC CLI (append): --session "
|
|
4
|
+
+ assertPortableId(binding.accSessionId, "sessionId") + " --generation "
|
|
5
|
+
+ assertPortableId(binding.generation, "generation");
|
|
6
|
+
|
|
7
|
+
export function ownerOnlyOutcome(inject, owner, budgetBytes) {
|
|
8
|
+
if (Buffer.byteLength(owner, "utf8") > budgetBytes) {
|
|
9
|
+
return { stdout: "", stderr: "acc: context budget cannot fit owner arguments; increase contextBudgetBytes" };
|
|
10
|
+
}
|
|
11
|
+
return { stdout: "", ...inject(owner) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// This is ownership context only. No peer projection, receipt, new session or
|
|
15
|
+
// inherited environment belongs here. A client may deliver the line after the
|
|
16
|
+
// tool has closed its owner; owned CLI operations must still validate the pair.
|
|
17
|
+
export async function appendToolOwner(result, { event, binding, context, adapter }) {
|
|
18
|
+
if (event.kind !== "beforeTool" || result.decision !== "allow" || binding === null
|
|
19
|
+
|| typeof adapter.injectToolOwnerOutcome !== "function") return result;
|
|
20
|
+
const current = await context.service.locateSession(binding.accSessionId, context.descriptor.id);
|
|
21
|
+
if (current?.record.state !== "open" || current.record.generation !== binding.generation) return result;
|
|
22
|
+
|
|
23
|
+
const owner = ownerHeader(binding);
|
|
24
|
+
const injected = adapter.injectToolOwnerOutcome({ owner, tool: event.tool });
|
|
25
|
+
if (injected === null) return result;
|
|
26
|
+
const fitted = ownerOnlyOutcome(() => injected, owner,
|
|
27
|
+
context.descriptor.policy?.contextBudgetBytes ?? 6_000);
|
|
28
|
+
return { ...result, stdout: fitted.stdout,
|
|
29
|
+
stderr: [result.stderr, fitted.stderr].filter(Boolean).join("\n") };
|
|
30
|
+
}
|
|
@@ -20,7 +20,7 @@ export async function readProcessTable({ run: exec = run,
|
|
|
20
20
|
timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
21
21
|
try {
|
|
22
22
|
const { stdout } = await exec("ps", ["-o", "pid=,ppid=,comm=", "-A"],
|
|
23
|
-
{ timeout: timeoutMs });
|
|
23
|
+
{ timeout: timeoutMs, killSignal: "SIGKILL" });
|
|
24
24
|
const table = new Map();
|
|
25
25
|
for (const line of stdout.split("\n")) {
|
|
26
26
|
const match = LINE.exec(line);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retireNativeBinding } from "./native-retirement.mjs";
|
|
1
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
3
|
import { realpath } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -5,7 +6,8 @@ import path from "node:path";
|
|
|
5
6
|
import { clearSessionBinding, effectiveCapabilities, loadSessionBinding, storeSessionBinding }
|
|
6
7
|
from "@agents-can-communicate/adapter-sdk";
|
|
7
8
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
8
|
-
import {
|
|
9
|
+
import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
|
|
10
|
+
import { AccError, createId } from "@agents-can-communicate/protocol";
|
|
9
11
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
10
12
|
import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
11
13
|
from "@agents-can-communicate/cli";
|
|
@@ -14,6 +16,8 @@ import { resolveClientPid } from "./client-pid.mjs";
|
|
|
14
16
|
import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
|
|
15
17
|
import { establishNativeBinding, livePolicyFrom } from "./native-binding.mjs";
|
|
16
18
|
import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
|
|
19
|
+
import { withSessionLifecycle } from "./session-lifecycle.mjs";
|
|
20
|
+
import { appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
|
|
17
21
|
|
|
18
22
|
// Kept cohesive above 300 lines because every handler shares one fail-open
|
|
19
23
|
// hook boundary, binding lifecycle, and client-specific outcome contract.
|
|
@@ -24,6 +28,10 @@ import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs
|
|
|
24
28
|
// let a call through than to make someone's session sit waiting on us.
|
|
25
29
|
const DEFAULT_BUDGET_MS = 5_000;
|
|
26
30
|
|
|
31
|
+
function assertHookBudget(deadline, message = "hook deadline expired") {
|
|
32
|
+
if (Date.now() >= deadline) throw new Error(message);
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
const byteLength = value => Buffer.byteLength(value, "utf8");
|
|
28
36
|
|
|
29
37
|
function compactInboxRecovery(messages) {
|
|
@@ -171,17 +179,20 @@ export function participantFor(adapterId, harnessSessionId, env = {}) {
|
|
|
171
179
|
return `${adapterId}-${suffix}`;
|
|
172
180
|
}
|
|
173
181
|
|
|
174
|
-
async function openContext({ cwd, dataHome, runtime, env }) {
|
|
182
|
+
async function openContext({ cwd, dataHome, runtime, env, deadline }) {
|
|
183
|
+
assertHookBudget(deadline);
|
|
175
184
|
const descriptor = await discoverWorkspace({ cwd, env: env ?? {},
|
|
176
|
-
gitProbe: createGitProbe() });
|
|
185
|
+
gitProbe: createGitProbe({ deadlineAt: deadline }) });
|
|
186
|
+
assertHookBudget(deadline);
|
|
187
|
+
const resolvedDataHome = dataHome ?? platformDataHome({ env: env ?? {} });
|
|
177
188
|
const paths = runtimePaths({
|
|
178
|
-
dataHome:
|
|
189
|
+
dataHome: resolvedDataHome,
|
|
179
190
|
workspaceId: descriptor.id,
|
|
180
191
|
workspaceRoots: descriptor.roots,
|
|
181
192
|
});
|
|
182
193
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
183
|
-
ids: runtime.ids, workspaceId: descriptor.id });
|
|
184
|
-
return { descriptor, paths, env: env ?? {}, realpath: runtime.realpath ?? realpath,
|
|
194
|
+
ids: runtime.ids, workspaceId: descriptor.id, deadlineAt: deadline });
|
|
195
|
+
return { descriptor, paths, dataHome: resolvedDataHome, env: env ?? {}, realpath: runtime.realpath ?? realpath,
|
|
185
196
|
service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
|
|
186
197
|
}
|
|
187
198
|
|
|
@@ -207,12 +218,17 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
207
218
|
exceptSessionId: binding.accSessionId });
|
|
208
219
|
const messages = delivery.queuedMessages;
|
|
209
220
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
// Only this hook's payload selected the binding. Supply its own pair as
|
|
222
|
+
// trusted context, outside peer bodies, rather than exporting inheritable
|
|
223
|
+
// credentials or teaching the CLI to guess from a public roster.
|
|
224
|
+
const owner = ownerHeader(binding);
|
|
225
|
+
const totalBudget = context.descriptor.policy?.contextBudgetBytes ?? 6_000;
|
|
226
|
+
// A peer can join after this prompt has begun. The current turn must already
|
|
227
|
+
// have its own arguments when it needs inbox/reply, without reattaching or
|
|
228
|
+
// waiting for another user prompt. Solo emits identity, not a peer notice.
|
|
229
|
+
if (sync.solo && messages.length === 0) {
|
|
230
|
+
return ownerOnlyOutcome(text => adapter.injectOutcome?.(text), owner, totalBudget);
|
|
231
|
+
}
|
|
216
232
|
|
|
217
233
|
// The ceiling a team agreed on in `acc.workspace.json`, or the default when
|
|
218
234
|
// there is no config. Validated by the protocol and, until now, never read:
|
|
@@ -222,10 +238,18 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
222
238
|
const canOfferNextTurn = effective.delivery.nextTurn === true && hasStructuredRenderer;
|
|
223
239
|
const projectionInput = { ...sync, messages: canOfferNextTurn ? messages : [],
|
|
224
240
|
liveOfferedMessageIds: delivery.liveOfferedMessageIds,
|
|
241
|
+
reminderMessageIds: delivery.reminderMessageIds,
|
|
225
242
|
roomMessageIds: delivery.roomMessageIds,
|
|
226
243
|
currentParticipantId: mine?.participantId };
|
|
244
|
+
// Credentials alone cannot replace the command that reaches a queued body.
|
|
245
|
+
// If both cannot fit, preserve recovery and explain the missing owner pair.
|
|
246
|
+
const minimumBodyBytes = messages.length === 0 ? 1
|
|
247
|
+
: byteLength(compactInboxRecovery(messages.slice(0, 1)));
|
|
248
|
+
const ownerFits = byteLength(owner) + 1 + minimumBodyBytes <= totalBudget;
|
|
249
|
+
const ownerWarning = ownerFits ? null
|
|
250
|
+
: "acc: context budget cannot fit owner arguments with coordination context; increase contextBudgetBytes";
|
|
227
251
|
const projectionOptions = {
|
|
228
|
-
budgetBytes:
|
|
252
|
+
budgetBytes: ownerFits ? totalBudget - byteLength(owner) - 1 : totalBudget };
|
|
229
253
|
// Delivery is state, not text parsing. Peer-controlled bodies can imitate
|
|
230
254
|
// another message's visible header, so only projector metadata proves
|
|
231
255
|
// which complete groups survived the byte budget. A custom adapter without
|
|
@@ -246,10 +270,17 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
246
270
|
+ `${messages[0].messageId} with acc inbox --message ${messages[0].messageId}` : null;
|
|
247
271
|
const visibleDegradation = degradation === null ? "" : `ACC: ${degradation.slice(5)}`;
|
|
248
272
|
const budgetBytes = projectionOptions.budgetBytes ?? 6_000;
|
|
249
|
-
const
|
|
273
|
+
const body = degradation === null ? projection.text
|
|
250
274
|
: fitDegradation(projection.text, visibleDegradation, messages, budgetBytes);
|
|
275
|
+
// Own claims make sync non-solo but produce no peer context. They cannot
|
|
276
|
+
// remove this turn's identity or consume a nonexistent body separator.
|
|
277
|
+
if (body === "" && messages.length === 0) {
|
|
278
|
+
return ownerOnlyOutcome(text => adapter.injectOutcome?.(text), owner, totalBudget);
|
|
279
|
+
}
|
|
280
|
+
const projected = body === "" ? "" : ownerFits ? `${owner}\n${body}` : body;
|
|
251
281
|
if (projected === "") {
|
|
252
|
-
return
|
|
282
|
+
return { stdout: "", stderr: messages.length === 0 ? ""
|
|
283
|
+
: [ownerWarning, degradation].filter(Boolean).join("\n") };
|
|
253
284
|
}
|
|
254
285
|
|
|
255
286
|
// The renderer returns ids as metadata, never as text to parse. A peer body
|
|
@@ -269,20 +300,24 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
269
300
|
// callback proves that the bytes crossed.
|
|
270
301
|
const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
|
|
271
302
|
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
272
|
-
if (degradation === null) return { ...outcome, offerInputs: writableOffers };
|
|
273
303
|
return { ...outcome,
|
|
274
|
-
stderr: [outcome.stderr, degradation].filter(Boolean).join("\n"),
|
|
304
|
+
stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
|
|
275
305
|
offerInputs: writableOffers };
|
|
276
306
|
}
|
|
277
307
|
|
|
278
308
|
// One bounded, fail-open native handshake for this exact session generation.
|
|
279
|
-
//
|
|
280
|
-
//
|
|
309
|
+
// New adapters read durable consent from their installation record; legacy
|
|
310
|
+
// adapters keep the environment exported by an owned shell bootstrap.
|
|
281
311
|
async function bindNative({ adapter, event, hookBinding, clientVersion, platform, context, paths,
|
|
282
312
|
deadline }) {
|
|
313
|
+
assertHookBudget(deadline);
|
|
314
|
+
const livePolicy = adapter?.nativeDelivery?.policySource === "installation-record"
|
|
315
|
+
? await readInstalledLivePolicy({ dataHome: context.dataHome, adapterId: adapter.id })
|
|
316
|
+
: livePolicyFrom(context.env);
|
|
317
|
+
assertHookBudget(deadline);
|
|
283
318
|
return establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
|
|
284
|
-
livePolicy
|
|
285
|
-
clock: context.service.clock,
|
|
319
|
+
livePolicy, service: context.service, runtimeDir: paths.root,
|
|
320
|
+
clock: context.service.clock, env: context.env,
|
|
286
321
|
timeoutMs: Math.max(1, Math.min(750, deadline - Date.now())) });
|
|
287
322
|
}
|
|
288
323
|
|
|
@@ -293,11 +328,12 @@ const HANDLERS = {
|
|
|
293
328
|
// certified facts before any probe, PID lookup, resume, or open can fail;
|
|
294
329
|
// keep only the generation identity needed for a successful resume.
|
|
295
330
|
if (binding !== null) {
|
|
296
|
-
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
331
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
297
332
|
accSessionId: binding.accSessionId, generation: binding.generation });
|
|
298
333
|
}
|
|
299
334
|
const clientVersion = await probeClientVersion(adapter,
|
|
300
335
|
{ timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
|
|
336
|
+
assertHookBudget(deadline);
|
|
301
337
|
const clientFacts = { clientVersion, platform };
|
|
302
338
|
const capabilities = effectiveCapabilities(adapter, clientFacts);
|
|
303
339
|
// Once per session, never per turn. A client that cannot be found yields
|
|
@@ -305,7 +341,9 @@ const HANDLERS = {
|
|
|
305
341
|
// behaviour every session had before this existed.
|
|
306
342
|
const command = adapter.client?.command ?? null;
|
|
307
343
|
const pid = command === null ? null
|
|
308
|
-
: resolveClientPid({ table: await readProcessTable(
|
|
344
|
+
: resolveClientPid({ table: await readProcessTable({
|
|
345
|
+
timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) }), from: process.pid, command });
|
|
346
|
+
assertHookBudget(deadline);
|
|
309
347
|
const clientPid = Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
310
348
|
const native = hookBinding => bindNative({ adapter, event, hookBinding, ...clientFacts,
|
|
311
349
|
context, paths, deadline });
|
|
@@ -317,6 +355,16 @@ const HANDLERS = {
|
|
|
317
355
|
checkoutRoot: context.descriptor.git?.worktreeRoot ?? context.descriptor.roots[0],
|
|
318
356
|
branch: context.descriptor.git?.branch ?? null,
|
|
319
357
|
};
|
|
358
|
+
if (event.kind === "beforeTurn" && binding !== null) {
|
|
359
|
+
// A real prompt may continue after finish closed this native owner's
|
|
360
|
+
// record. Recheck after the probes: a CLI replacement can run outside
|
|
361
|
+
// the native lifecycle lock. Never adopt that replacement's identity.
|
|
362
|
+
const previous = await context.service.locateSession(binding.accSessionId);
|
|
363
|
+
if (previous?.record.state !== "closed"
|
|
364
|
+
|| previous.record.generation !== binding.generation) {
|
|
365
|
+
throw new Error("the completed hook owner changed during turn registration");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
320
368
|
if (binding !== null) {
|
|
321
369
|
const resumed = await context.service.resumeSession({
|
|
322
370
|
sessionId: binding.accSessionId,
|
|
@@ -326,13 +374,21 @@ const HANDLERS = {
|
|
|
326
374
|
if (resumed !== null) {
|
|
327
375
|
const hookBinding = { accSessionId: resumed.sessionId, generation: resumed.generation,
|
|
328
376
|
...clientFacts, clientPid };
|
|
329
|
-
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
377
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
330
378
|
...hookBinding });
|
|
331
379
|
return { accSessionId: resumed.sessionId, generation: resumed.generation,
|
|
332
380
|
...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
|
|
333
381
|
}
|
|
334
382
|
}
|
|
383
|
+
// Persist ownership before creating its session. If the hook dies after
|
|
384
|
+
// creation, the next start resumes this exact pair. If creation never
|
|
385
|
+
// happened (or it was closed), a retry allocates a NEW pair, never revives it.
|
|
386
|
+
const opening = { sessionId: context.service.ids.next("session"),
|
|
387
|
+
generation: context.service.ids.next("generation") };
|
|
388
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
389
|
+
accSessionId: opening.sessionId, generation: opening.generation });
|
|
335
390
|
const session = await context.service.openSession({
|
|
391
|
+
...opening,
|
|
336
392
|
workspaceId: context.descriptor.id,
|
|
337
393
|
participantId: participantFor(adapterId, event.sessionId, context.env),
|
|
338
394
|
displayName: participantFor(adapterId, event.sessionId, context.env),
|
|
@@ -348,7 +404,7 @@ const HANDLERS = {
|
|
|
348
404
|
});
|
|
349
405
|
const hookBinding = { accSessionId: session.sessionId, generation: session.generation,
|
|
350
406
|
...clientFacts, clientPid };
|
|
351
|
-
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
407
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
352
408
|
...hookBinding });
|
|
353
409
|
return { accSessionId: session.sessionId, generation: session.generation,
|
|
354
410
|
...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
|
|
@@ -361,27 +417,59 @@ const HANDLERS = {
|
|
|
361
417
|
return {};
|
|
362
418
|
},
|
|
363
419
|
|
|
364
|
-
async sessionEnd({ binding, context, event, paths }) {
|
|
420
|
+
async sessionEnd({ binding, context, event, paths, adapter, deadline }) {
|
|
365
421
|
if (binding === null) return {};
|
|
366
|
-
await context.service
|
|
367
|
-
generation: binding.generation
|
|
368
|
-
|
|
422
|
+
await retireNativeBinding({ adapter, service: context.service, sessionId: binding.accSessionId,
|
|
423
|
+
generation: binding.generation, runtimeDir: paths.root,
|
|
424
|
+
timeoutMs: Math.max(1, Math.min(200, deadline - Date.now())) });
|
|
425
|
+
const owner = { sessionId: binding.accSessionId, generation: binding.generation };
|
|
426
|
+
const current = await context.service.locateSession(binding.accSessionId);
|
|
427
|
+
if (current?.record.generation === binding.generation && current.record.state === "open") {
|
|
428
|
+
await context.service.closeSession(owner);
|
|
429
|
+
} else {
|
|
430
|
+
// Close may have committed before this hook died. Retire only the old
|
|
431
|
+
// endpoint and binding; do not close a replacement or repeat a close event.
|
|
432
|
+
await context.service.clearDeliveryBinding(owner);
|
|
433
|
+
}
|
|
434
|
+
await clearSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId });
|
|
369
435
|
return {};
|
|
370
436
|
},
|
|
371
437
|
|
|
372
438
|
async beforeTurn(input) {
|
|
373
439
|
const { binding, context, adapter, event, paths, deadline } = input;
|
|
374
|
-
if (binding === null)
|
|
440
|
+
if (binding === null) {
|
|
441
|
+
// SessionStart can be skipped when an automatic runtime refresh races
|
|
442
|
+
// process admission. A genuine prompt is the next safe proof that this
|
|
443
|
+
// native conversation exists, and it is already inside the same
|
|
444
|
+
// lifecycle mutex as startup. Reuse that opening path so publication,
|
|
445
|
+
// crash recovery, client facts, and the native handshake stay atomic.
|
|
446
|
+
const started = await HANDLERS.sessionStart(input);
|
|
447
|
+
const fresh = await loadSessionBinding({ runtimeDir: paths.root,
|
|
448
|
+
harnessSessionId: event.sessionId });
|
|
449
|
+
const turn = await projectTurn({ ...input, binding: fresh });
|
|
450
|
+
return { ...turn, nativeBinding: started.nativeBinding };
|
|
451
|
+
}
|
|
452
|
+
const current = await context.service.locateSession(binding.accSessionId);
|
|
453
|
+
if (current?.record.state === "closed" && current.record.generation === binding.generation) {
|
|
454
|
+
// finish ends an ACC incarnation, not the native conversation. Only a
|
|
455
|
+
// genuine new user turn can start another one; tool hooks cannot. Reuse
|
|
456
|
+
// the crash-safe opening path, retaining its full published client facts
|
|
457
|
+
// and its single native handshake rather than binding a second time.
|
|
458
|
+
const started = await HANDLERS.sessionStart(input);
|
|
459
|
+
const fresh = await loadSessionBinding({ runtimeDir: paths.root,
|
|
460
|
+
harnessSessionId: event.sessionId });
|
|
461
|
+
const turn = await projectTurn({ ...input, binding: fresh });
|
|
462
|
+
return { ...turn, nativeBinding: started.nativeBinding };
|
|
463
|
+
}
|
|
375
464
|
// A turn is the clearest sign a session is alive. Never a reason to fail:
|
|
376
465
|
// this runs in front of somebody's prompt.
|
|
377
466
|
await context.service.heartbeatSession({ sessionId: binding.accSessionId,
|
|
378
|
-
generation: binding.generation })
|
|
467
|
+
generation: binding.generation });
|
|
379
468
|
// A native transport that became ready only after SessionStart is picked
|
|
380
469
|
// up here and a live lease is renewed: bounded, fail-open, never on a guard.
|
|
381
|
-
const nativeBinding =
|
|
382
|
-
:
|
|
383
|
-
|
|
384
|
-
deadline });
|
|
470
|
+
const nativeBinding = await bindNative({ adapter, event, hookBinding: binding,
|
|
471
|
+
clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
|
|
472
|
+
deadline });
|
|
385
473
|
const turn = await projectTurn(input);
|
|
386
474
|
return nativeBinding === undefined ? turn : { ...turn, nativeBinding };
|
|
387
475
|
},
|
|
@@ -458,42 +546,49 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
458
546
|
probeClientVersion = defaultProbeClientVersion,
|
|
459
547
|
platform = `${process.platform}-${process.arch}` }) {
|
|
460
548
|
const deadline = Date.now() + budgetMs;
|
|
461
|
-
const
|
|
549
|
+
const fallback = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
|
|
462
550
|
commitOffers: async () => {} };
|
|
463
|
-
|
|
464
|
-
|
|
551
|
+
const execute = async () => {
|
|
552
|
+
assertHookBudget(deadline);
|
|
465
553
|
const adapter = adapters?.[adapterId];
|
|
466
554
|
if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
|
|
467
555
|
|
|
468
556
|
const event = await adapter.normalizeHook(payload);
|
|
469
|
-
const context = await openContext({ cwd: event.cwd, dataHome, runtime, env });
|
|
470
|
-
const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
|
|
471
|
-
harnessSessionId: event.sessionId }).catch(() => null);
|
|
472
|
-
|
|
557
|
+
const context = await openContext({ cwd: event.cwd, dataHome, runtime, env, deadline });
|
|
473
558
|
const handler = HANDLERS[event.kind];
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
559
|
+
const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
|
|
560
|
+
const invoke = async () => {
|
|
561
|
+
assertHookBudget(deadline);
|
|
562
|
+
if (lifecycle) {
|
|
563
|
+
// A previous holder may have died after journalling its session while
|
|
564
|
+
// this process waited. Recover that write before interpreting its binding.
|
|
565
|
+
const store = await openFilesystemStore({ root: context.paths.root, clock: runtime.clock,
|
|
566
|
+
ids: runtime.ids, workspaceId: context.descriptor.id, deadlineAt: deadline });
|
|
567
|
+
context.service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
|
|
568
|
+
}
|
|
569
|
+
const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
|
|
570
|
+
harnessSessionId: event.sessionId });
|
|
571
|
+
assertHookBudget(deadline);
|
|
572
|
+
const result = handler === undefined ? {} : await handler({ event, context, adapter, adapterId,
|
|
573
|
+
binding, paths: context.paths,
|
|
477
574
|
readProcessTable, probeClientVersion, platform, deadline });
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
575
|
+
return appendToolOwner(result, { event, binding, context, adapter });
|
|
576
|
+
};
|
|
577
|
+
const work = lifecycle
|
|
578
|
+
? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
|
|
579
|
+
clock: runtime.clock, deadlineAt: deadline }, invoke)
|
|
580
|
+
: invoke();
|
|
581
|
+
// Keep the late continuation's result private. Once the budget wins, it
|
|
582
|
+
// cannot change the fail-open answer or restore withdrawn output/offers.
|
|
583
|
+
const result = { ...fallback, ...await work };
|
|
584
|
+
assertHookBudget(deadline);
|
|
486
585
|
const offerInputs = result.offerInputs ?? [];
|
|
487
586
|
let commitPromise = null;
|
|
488
587
|
result.commitOffers = () => {
|
|
489
588
|
if (commitPromise !== null) return commitPromise;
|
|
490
589
|
commitPromise = (async () => {
|
|
491
590
|
for (const input of offerInputs) {
|
|
492
|
-
|
|
493
|
-
if (remaining <= 0) throw new Error("hook budget exhausted before offer commit");
|
|
494
|
-
// The durable transaction owns deadline cancellation. Racing it here
|
|
495
|
-
// would only reject the public promise while the losing writer kept
|
|
496
|
-
// waiting and could publish later.
|
|
591
|
+
assertHookBudget(deadline, "hook budget exhausted before offer commit");
|
|
497
592
|
await context.service.recordOfferSucceeded({ ...input, deadlineAt: deadline });
|
|
498
593
|
}
|
|
499
594
|
})();
|
|
@@ -503,18 +598,28 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
503
598
|
|
|
504
599
|
const status = await context.service.collectStatus({
|
|
505
600
|
workspaceId: context.descriptor.id });
|
|
601
|
+
assertHookBudget(deadline);
|
|
506
602
|
result.sessions = status.participants.filter(p => p.presence !== "offline");
|
|
507
603
|
result.service = context.service;
|
|
604
|
+
return result;
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
let timer;
|
|
608
|
+
try {
|
|
609
|
+
// One budget covers normalization, discovery, recovery, handling and the
|
|
610
|
+
// final status read. Store/binding deadlines fence undecided writes; an
|
|
611
|
+
// activated journal still completes under its writer lock after expiry.
|
|
612
|
+
const budget = new Promise(resolve => {
|
|
613
|
+
timer = setTimeout(() => resolve({ ...fallback, timedOut: true }),
|
|
614
|
+
Math.max(0, deadline - Date.now()));
|
|
615
|
+
});
|
|
616
|
+
return await Promise.race([execute(), budget]);
|
|
508
617
|
} catch (error) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// A later failure may happen after a turn prepared offer inputs. Once the
|
|
514
|
-
// fail-open path withdraws stdout, no transport boundary remains to commit.
|
|
515
|
-
result.commitOffers = async () => {};
|
|
618
|
+
return { ...fallback, failed: true, reason: error.message,
|
|
619
|
+
...(error instanceof AccError && error.details?.reasonCode === "workspace_contains_runtime"
|
|
620
|
+
? { failureCode: "workspace_contains_runtime" } : {}),
|
|
621
|
+
...(Date.now() >= deadline ? { timedOut: true } : {}) };
|
|
516
622
|
} finally {
|
|
517
|
-
|
|
623
|
+
clearTimeout(timer);
|
|
518
624
|
}
|
|
519
|
-
return result;
|
|
520
625
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { withWriterMutex } from "@agents-can-communicate/storage-filesystem";
|
|
5
|
+
|
|
6
|
+
// Binding ownership spans load, resume/open/close, and publication. The store's
|
|
7
|
+
// writer lock protects each record, but cannot protect this whole hook sequence.
|
|
8
|
+
// Use the same crash-safe mutex in a separate directory to avoid nested locks.
|
|
9
|
+
export function withSessionLifecycle({ root, sessionId, clock, deadlineAt }, operation) {
|
|
10
|
+
const key = createHash("sha256").update(String(sessionId)).digest("hex");
|
|
11
|
+
return withWriterMutex({ locks: path.join(root, "lifecycle-locks", key) },
|
|
12
|
+
{ root, clock, deadlineAt }, operation);
|
|
13
|
+
}
|
|
@@ -42,10 +42,12 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
42
42
|
// a fixed order; an explicit off takes a recorded one back first so the
|
|
43
43
|
// record written below describes the machine as it now is.
|
|
44
44
|
const notes = [];
|
|
45
|
+
let retainedMechanisms = [];
|
|
45
46
|
if (operation.deactivation !== undefined) {
|
|
46
47
|
const report = await deactivateNative({ nativeActivation: operation.deactivation,
|
|
47
48
|
...activation });
|
|
48
49
|
notes.push(...describeTeardown(report));
|
|
50
|
+
retainedMechanisms = report.retainedMechanisms;
|
|
49
51
|
}
|
|
50
52
|
let native = null;
|
|
51
53
|
let appendedRcBlock = false;
|
|
@@ -55,13 +57,19 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
55
57
|
native = applied.nativeActivation;
|
|
56
58
|
appendedRcBlock = applied.appendedRcBlock;
|
|
57
59
|
}
|
|
60
|
+
if (retainedMechanisms.length > 0) {
|
|
61
|
+
native = { ...(native ?? operation.deactivation),
|
|
62
|
+
mechanisms: [...(native?.mechanisms ?? []), ...retainedMechanisms] };
|
|
63
|
+
}
|
|
58
64
|
// Recorded after the write, so a record never claims an install that
|
|
59
65
|
// did not happen. The reverse order would leave uninstall trying to
|
|
60
66
|
// remove files nothing created.
|
|
61
67
|
await recordInstall({ dataHome, adapterId: adapter.id,
|
|
62
68
|
version: operation.clientVersion ?? null, accVersion,
|
|
63
|
-
artifacts: operation.artifacts, createdDirectories,
|
|
69
|
+
artifacts: operation.artifacts, createdDirectories,
|
|
70
|
+
deliveryPolicy: operation.livePolicy, nativeActivation: native });
|
|
64
71
|
results.operations.push({ ...operation, applied: true, appendedRcBlock,
|
|
72
|
+
needsAction: outcome.needsAction ?? [],
|
|
65
73
|
changes: outcome.changes ?? [], diagnostics: [
|
|
66
74
|
...(operation.deliveryDiagnostic === undefined
|
|
67
75
|
? [] : [operation.deliveryDiagnostic]),
|
|
@@ -69,14 +77,19 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
69
77
|
...notes,
|
|
70
78
|
] });
|
|
71
79
|
} else {
|
|
80
|
+
// Config ownership can veto removal. Ask before teardown or recorded
|
|
81
|
+
// artifact deletion; an adapter-local check would run after those writes.
|
|
82
|
+
await adapter.preflightUninstall?.(context);
|
|
72
83
|
// Keep the record until every cleanup step succeeds. It is both the
|
|
73
84
|
// authority for deletion and the only durable recipe a retry has when
|
|
74
85
|
// the client or one of ACC's own artifacts is already gone.
|
|
75
86
|
const notes = [];
|
|
87
|
+
let retainedMechanisms = [];
|
|
76
88
|
if (operation.deactivation !== undefined) {
|
|
77
89
|
const report = await deactivateNative({ nativeActivation: operation.deactivation,
|
|
78
90
|
...activation });
|
|
79
91
|
notes.push(...describeTeardown(report));
|
|
92
|
+
retainedMechanisms = report.retainedMechanisms;
|
|
80
93
|
}
|
|
81
94
|
const owned = await removeOwnedArtifacts({ dataHome, adapterId: adapter.id });
|
|
82
95
|
// What ownership held back is passed on, because the adapter would
|
|
@@ -86,7 +99,9 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
86
99
|
const outcome = await adapter.uninstall({ ...context, keep: owned.kept });
|
|
87
100
|
const directories = await removeEmptyOwnedDirectories({ home: context.home,
|
|
88
101
|
directories: owned.createdDirectories });
|
|
89
|
-
await finalizeRemoval({ dataHome, adapterId: adapter.id
|
|
102
|
+
await finalizeRemoval({ dataHome, adapterId: adapter.id,
|
|
103
|
+
retainedNativeActivation: retainedMechanisms.length === 0 ? null
|
|
104
|
+
: { ...operation.deactivation, mechanisms: retainedMechanisms } });
|
|
90
105
|
results.operations.push({ ...operation, applied: true,
|
|
91
106
|
changes: outcome.changes ?? [], removed: owned.removed, kept: owned.kept,
|
|
92
107
|
removedDirectories: directories.removed, keptDirectories: directories.kept,
|
|
@@ -4,7 +4,7 @@ import { createReadStream } from "node:fs";
|
|
|
4
4
|
import { mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
|
-
import { evaluateNativeEligibility } from "@agents-can-communicate/adapter-sdk";
|
|
7
|
+
import { evaluateNativeEligibility, ownVersion } from "@agents-can-communicate/adapter-sdk";
|
|
8
8
|
|
|
9
9
|
// The launch-time check behind an owned shell shim. It answers one closed
|
|
10
10
|
// question - may this exact executable receive native delivery? - from the
|
|
@@ -13,9 +13,10 @@ import { evaluateNativeEligibility } from "@agents-can-communicate/adapter-sdk";
|
|
|
13
13
|
// command untouched, and this module is what makes "cannot decide" cheap.
|
|
14
14
|
//
|
|
15
15
|
// The cache key is the executable's identity - resolved path, symlink target,
|
|
16
|
-
// inode, size, mtime, and full sha256 -
|
|
17
|
-
//
|
|
18
|
-
// failure is short-lived; a repaired client is
|
|
16
|
+
// inode, size, mtime, and full sha256 - plus platform and this ACC runtime's
|
|
17
|
+
// version. A vendor upgrade, replacement, or changed certification contract is
|
|
18
|
+
// therefore a miss. A cached failure is short-lived; a repaired client is
|
|
19
|
+
// never disabled for long.
|
|
19
20
|
|
|
20
21
|
export const BOOTSTRAP_CACHE_SCHEMA = 1;
|
|
21
22
|
export const SUPPORTED_TTL_MS = 6 * 60 * 60 * 1_000;
|
|
@@ -86,7 +87,7 @@ const closedOutcome = (supported, reasonCode) => Object.freeze({ supported, reas
|
|
|
86
87
|
|
|
87
88
|
export async function checkNativeBootstrap({ adapter, realExecutable, platform, dataHome,
|
|
88
89
|
timeoutMs = DEFAULT_TIMEOUT_MS, clock = { now: () => new Date().toISOString() },
|
|
89
|
-
readVersion = defaultReadVersion }) {
|
|
90
|
+
readVersion = defaultReadVersion, accVersion }) {
|
|
90
91
|
try {
|
|
91
92
|
if (adapter?.nativeDelivery === undefined || typeof adapter.probeNativeDelivery !== "function") {
|
|
92
93
|
return closedOutcome(false, "native_delivery_unsupported");
|
|
@@ -94,8 +95,12 @@ export async function checkNativeBootstrap({ adapter, realExecutable, platform,
|
|
|
94
95
|
const identity = await executableIdentity(realExecutable);
|
|
95
96
|
const file = cachePathFor(dataHome, adapter.id);
|
|
96
97
|
const now = Date.parse(clock.now());
|
|
98
|
+
const runtimeVersion = accVersion === undefined
|
|
99
|
+
? await ownVersion(import.meta.url) : accVersion;
|
|
100
|
+
const runtimeVersionKnown = typeof runtimeVersion === "string" && runtimeVersion !== "";
|
|
97
101
|
const cached = await loadCache(file);
|
|
98
102
|
if (cached !== null && cached.adapterId === adapter.id && cached.platform === platform
|
|
103
|
+
&& runtimeVersionKnown && cached.accVersion === runtimeVersion
|
|
99
104
|
&& sameIdentity(cached.identity, identity) && Date.parse(cached.expiresAt) > now) {
|
|
100
105
|
return closedOutcome(cached.supported, cached.reasonCode);
|
|
101
106
|
}
|
|
@@ -131,6 +136,7 @@ export async function checkNativeBootstrap({ adapter, realExecutable, platform,
|
|
|
131
136
|
// Never command output, never a message body.
|
|
132
137
|
await saveCache(file, {
|
|
133
138
|
schemaVersion: BOOTSTRAP_CACHE_SCHEMA, adapterId: adapter.id, platform, identity,
|
|
139
|
+
accVersion: runtimeVersionKnown ? runtimeVersion : null,
|
|
134
140
|
clientVersion, supported, reasonCode: outcomeReason,
|
|
135
141
|
protocolContract: supported ? eligibility.protocolContract : null,
|
|
136
142
|
modes: supported ? [...eligibility.modes] : [],
|
|
@@ -66,7 +66,7 @@ async function detectNative(adapter, entry, { context, platform, probeTimeoutMs,
|
|
|
66
66
|
const facts = { realExecutable, probe: null, eligibility: null };
|
|
67
67
|
try {
|
|
68
68
|
facts.probe = await withTimeout(Promise.resolve(adapter.probeNativeDelivery({
|
|
69
|
-
realExecutable, timeoutMs: probeTimeoutMs })), probeTimeoutMs,
|
|
69
|
+
realExecutable, timeoutMs: probeTimeoutMs, env: context?.env })), probeTimeoutMs,
|
|
70
70
|
`${adapter.id} native probe`);
|
|
71
71
|
} catch {
|
|
72
72
|
facts.probe = null;
|
|
@@ -12,3 +12,4 @@ export { BLOCK_BEGIN, BLOCK_END, SHIM_MARKER, SHIM_POLICIES, SUPPORTED_SHELLS,
|
|
|
12
12
|
shellLiteral, uninstallShellBootstrap, validateShimEntry } from "./shell-bootstrap.mjs";
|
|
13
13
|
export { LIVE_POLICIES, describeActivation, describeDeactivation, livePolicyOf, rcFileFor,
|
|
14
14
|
resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
|
|
15
|
+
export { readInstalledLivePolicy } from "./live-policy.mjs";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadOwnership } from "./ownership.mjs";
|
|
2
|
+
|
|
3
|
+
export const LIVE_POLICIES = Object.freeze(["off", "actionable", "all"]);
|
|
4
|
+
|
|
5
|
+
export const livePolicyOf = install => {
|
|
6
|
+
if (Object.hasOwn(install ?? {}, "deliveryPolicy")) {
|
|
7
|
+
return LIVE_POLICIES.includes(install.deliveryPolicy) ? install.deliveryPolicy : "off";
|
|
8
|
+
}
|
|
9
|
+
return LIVE_POLICIES.includes(install?.nativeActivation?.livePolicy)
|
|
10
|
+
? install.nativeActivation.livePolicy : "off";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function readInstalledLivePolicy({ dataHome, adapterId }) {
|
|
14
|
+
try {
|
|
15
|
+
const record = await loadOwnership({ dataHome });
|
|
16
|
+
const install = record.installs.find(entry => entry.adapterId === adapterId);
|
|
17
|
+
return livePolicyOf(install);
|
|
18
|
+
} catch {
|
|
19
|
+
return "off";
|
|
20
|
+
}
|
|
21
|
+
}
|