agents-can-communicate 0.3.1 → 0.4.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 +28 -7
- 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 +73 -0
- package/docs/ADAPTER_AUTHORING.md +41 -7
- package/docs/ARCHITECTURE.md +62 -11
- package/docs/CAPABILITIES.md +40 -36
- package/docs/CLI.md +143 -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 +37 -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 +45 -13
- package/docs/TROUBLESHOOTING.md +76 -18
- package/docs/UPGRADING.md +119 -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 +106 -5
- 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 +22 -4
- 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 +63 -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 +73 -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 +122 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
- 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/process-table.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -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
|
@@ -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 { assertPortableId, 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,7 @@ 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";
|
|
17
20
|
|
|
18
21
|
// Kept cohesive above 300 lines because every handler shares one fail-open
|
|
19
22
|
// hook boundary, binding lifecycle, and client-specific outcome contract.
|
|
@@ -24,6 +27,10 @@ import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs
|
|
|
24
27
|
// let a call through than to make someone's session sit waiting on us.
|
|
25
28
|
const DEFAULT_BUDGET_MS = 5_000;
|
|
26
29
|
|
|
30
|
+
function assertHookBudget(deadline, message = "hook deadline expired") {
|
|
31
|
+
if (Date.now() >= deadline) throw new Error(message);
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
const byteLength = value => Buffer.byteLength(value, "utf8");
|
|
28
35
|
|
|
29
36
|
function compactInboxRecovery(messages) {
|
|
@@ -46,6 +53,13 @@ function fitDegradation(projection, visibleDegradation, messages, budgetBytes) {
|
|
|
46
53
|
return projection;
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
function ownerOnlyOutcome(adapter, owner, budgetBytes) {
|
|
57
|
+
if (byteLength(owner) > budgetBytes) {
|
|
58
|
+
return { stdout: "", stderr: "acc: context budget cannot fit owner arguments; increase contextBudgetBytes" };
|
|
59
|
+
}
|
|
60
|
+
return { stdout: "", ...adapter.injectOutcome?.(owner) };
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
// Declared by this process on the session it opens, so peers can tell an idle
|
|
50
64
|
// session from a dead one. Only one of the four clients fires a heartbeat event,
|
|
51
65
|
// so the rest refresh here: on every turn, and during a long one whenever the
|
|
@@ -171,17 +185,20 @@ export function participantFor(adapterId, harnessSessionId, env = {}) {
|
|
|
171
185
|
return `${adapterId}-${suffix}`;
|
|
172
186
|
}
|
|
173
187
|
|
|
174
|
-
async function openContext({ cwd, dataHome, runtime, env }) {
|
|
188
|
+
async function openContext({ cwd, dataHome, runtime, env, deadline }) {
|
|
189
|
+
assertHookBudget(deadline);
|
|
175
190
|
const descriptor = await discoverWorkspace({ cwd, env: env ?? {},
|
|
176
|
-
gitProbe: createGitProbe() });
|
|
191
|
+
gitProbe: createGitProbe({ deadlineAt: deadline }) });
|
|
192
|
+
assertHookBudget(deadline);
|
|
193
|
+
const resolvedDataHome = dataHome ?? platformDataHome({ env: env ?? {} });
|
|
177
194
|
const paths = runtimePaths({
|
|
178
|
-
dataHome:
|
|
195
|
+
dataHome: resolvedDataHome,
|
|
179
196
|
workspaceId: descriptor.id,
|
|
180
197
|
workspaceRoots: descriptor.roots,
|
|
181
198
|
});
|
|
182
199
|
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,
|
|
200
|
+
ids: runtime.ids, workspaceId: descriptor.id, deadlineAt: deadline });
|
|
201
|
+
return { descriptor, paths, dataHome: resolvedDataHome, env: env ?? {}, realpath: runtime.realpath ?? realpath,
|
|
185
202
|
service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
|
|
186
203
|
}
|
|
187
204
|
|
|
@@ -207,12 +224,19 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
207
224
|
exceptSessionId: binding.accSessionId });
|
|
208
225
|
const messages = delivery.queuedMessages;
|
|
209
226
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
227
|
+
// Only this hook's payload selected the binding. Supply its own pair as
|
|
228
|
+
// trusted context, outside peer bodies, rather than exporting inheritable
|
|
229
|
+
// credentials or teaching the CLI to guess from a public roster.
|
|
230
|
+
const owner = "ACC CLI (append): --session "
|
|
231
|
+
+ assertPortableId(binding.accSessionId, "sessionId") + " --generation "
|
|
232
|
+
+ assertPortableId(binding.generation, "generation");
|
|
233
|
+
const totalBudget = context.descriptor.policy?.contextBudgetBytes ?? 6_000;
|
|
234
|
+
// A peer can join after this prompt has begun. The current turn must already
|
|
235
|
+
// have its own arguments when it needs inbox/reply, without reattaching or
|
|
236
|
+
// waiting for another user prompt. Solo emits identity, not a peer notice.
|
|
237
|
+
if (sync.solo && messages.length === 0) {
|
|
238
|
+
return ownerOnlyOutcome(adapter, owner, totalBudget);
|
|
239
|
+
}
|
|
216
240
|
|
|
217
241
|
// The ceiling a team agreed on in `acc.workspace.json`, or the default when
|
|
218
242
|
// there is no config. Validated by the protocol and, until now, never read:
|
|
@@ -222,10 +246,18 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
222
246
|
const canOfferNextTurn = effective.delivery.nextTurn === true && hasStructuredRenderer;
|
|
223
247
|
const projectionInput = { ...sync, messages: canOfferNextTurn ? messages : [],
|
|
224
248
|
liveOfferedMessageIds: delivery.liveOfferedMessageIds,
|
|
249
|
+
reminderMessageIds: delivery.reminderMessageIds,
|
|
225
250
|
roomMessageIds: delivery.roomMessageIds,
|
|
226
251
|
currentParticipantId: mine?.participantId };
|
|
252
|
+
// Credentials alone cannot replace the command that reaches a queued body.
|
|
253
|
+
// If both cannot fit, preserve recovery and explain the missing owner pair.
|
|
254
|
+
const minimumBodyBytes = messages.length === 0 ? 1
|
|
255
|
+
: byteLength(compactInboxRecovery(messages.slice(0, 1)));
|
|
256
|
+
const ownerFits = byteLength(owner) + 1 + minimumBodyBytes <= totalBudget;
|
|
257
|
+
const ownerWarning = ownerFits ? null
|
|
258
|
+
: "acc: context budget cannot fit owner arguments with coordination context; increase contextBudgetBytes";
|
|
227
259
|
const projectionOptions = {
|
|
228
|
-
budgetBytes:
|
|
260
|
+
budgetBytes: ownerFits ? totalBudget - byteLength(owner) - 1 : totalBudget };
|
|
229
261
|
// Delivery is state, not text parsing. Peer-controlled bodies can imitate
|
|
230
262
|
// another message's visible header, so only projector metadata proves
|
|
231
263
|
// which complete groups survived the byte budget. A custom adapter without
|
|
@@ -246,10 +278,15 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
246
278
|
+ `${messages[0].messageId} with acc inbox --message ${messages[0].messageId}` : null;
|
|
247
279
|
const visibleDegradation = degradation === null ? "" : `ACC: ${degradation.slice(5)}`;
|
|
248
280
|
const budgetBytes = projectionOptions.budgetBytes ?? 6_000;
|
|
249
|
-
const
|
|
281
|
+
const body = degradation === null ? projection.text
|
|
250
282
|
: fitDegradation(projection.text, visibleDegradation, messages, budgetBytes);
|
|
283
|
+
// Own claims make sync non-solo but produce no peer context. They cannot
|
|
284
|
+
// remove this turn's identity or consume a nonexistent body separator.
|
|
285
|
+
if (body === "" && messages.length === 0) return ownerOnlyOutcome(adapter, owner, totalBudget);
|
|
286
|
+
const projected = body === "" ? "" : ownerFits ? `${owner}\n${body}` : body;
|
|
251
287
|
if (projected === "") {
|
|
252
|
-
return
|
|
288
|
+
return { stdout: "", stderr: messages.length === 0 ? ""
|
|
289
|
+
: [ownerWarning, degradation].filter(Boolean).join("\n") };
|
|
253
290
|
}
|
|
254
291
|
|
|
255
292
|
// The renderer returns ids as metadata, never as text to parse. A peer body
|
|
@@ -269,20 +306,24 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
269
306
|
// callback proves that the bytes crossed.
|
|
270
307
|
const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
|
|
271
308
|
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
272
|
-
if (degradation === null) return { ...outcome, offerInputs: writableOffers };
|
|
273
309
|
return { ...outcome,
|
|
274
|
-
stderr: [outcome.stderr, degradation].filter(Boolean).join("\n"),
|
|
310
|
+
stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
|
|
275
311
|
offerInputs: writableOffers };
|
|
276
312
|
}
|
|
277
313
|
|
|
278
314
|
// One bounded, fail-open native handshake for this exact session generation.
|
|
279
|
-
//
|
|
280
|
-
//
|
|
315
|
+
// New adapters read durable consent from their installation record; legacy
|
|
316
|
+
// adapters keep the environment exported by an owned shell bootstrap.
|
|
281
317
|
async function bindNative({ adapter, event, hookBinding, clientVersion, platform, context, paths,
|
|
282
318
|
deadline }) {
|
|
319
|
+
assertHookBudget(deadline);
|
|
320
|
+
const livePolicy = adapter?.nativeDelivery?.policySource === "installation-record"
|
|
321
|
+
? await readInstalledLivePolicy({ dataHome: context.dataHome, adapterId: adapter.id })
|
|
322
|
+
: livePolicyFrom(context.env);
|
|
323
|
+
assertHookBudget(deadline);
|
|
283
324
|
return establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
|
|
284
|
-
livePolicy
|
|
285
|
-
clock: context.service.clock,
|
|
325
|
+
livePolicy, service: context.service, runtimeDir: paths.root,
|
|
326
|
+
clock: context.service.clock, env: context.env,
|
|
286
327
|
timeoutMs: Math.max(1, Math.min(750, deadline - Date.now())) });
|
|
287
328
|
}
|
|
288
329
|
|
|
@@ -293,11 +334,12 @@ const HANDLERS = {
|
|
|
293
334
|
// certified facts before any probe, PID lookup, resume, or open can fail;
|
|
294
335
|
// keep only the generation identity needed for a successful resume.
|
|
295
336
|
if (binding !== null) {
|
|
296
|
-
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
337
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
297
338
|
accSessionId: binding.accSessionId, generation: binding.generation });
|
|
298
339
|
}
|
|
299
340
|
const clientVersion = await probeClientVersion(adapter,
|
|
300
341
|
{ timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
|
|
342
|
+
assertHookBudget(deadline);
|
|
301
343
|
const clientFacts = { clientVersion, platform };
|
|
302
344
|
const capabilities = effectiveCapabilities(adapter, clientFacts);
|
|
303
345
|
// Once per session, never per turn. A client that cannot be found yields
|
|
@@ -305,7 +347,9 @@ const HANDLERS = {
|
|
|
305
347
|
// behaviour every session had before this existed.
|
|
306
348
|
const command = adapter.client?.command ?? null;
|
|
307
349
|
const pid = command === null ? null
|
|
308
|
-
: resolveClientPid({ table: await readProcessTable(
|
|
350
|
+
: resolveClientPid({ table: await readProcessTable({
|
|
351
|
+
timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) }), from: process.pid, command });
|
|
352
|
+
assertHookBudget(deadline);
|
|
309
353
|
const clientPid = Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
310
354
|
const native = hookBinding => bindNative({ adapter, event, hookBinding, ...clientFacts,
|
|
311
355
|
context, paths, deadline });
|
|
@@ -317,6 +361,16 @@ const HANDLERS = {
|
|
|
317
361
|
checkoutRoot: context.descriptor.git?.worktreeRoot ?? context.descriptor.roots[0],
|
|
318
362
|
branch: context.descriptor.git?.branch ?? null,
|
|
319
363
|
};
|
|
364
|
+
if (event.kind === "beforeTurn" && binding !== null) {
|
|
365
|
+
// A real prompt may continue after finish closed this native owner's
|
|
366
|
+
// record. Recheck after the probes: a CLI replacement can run outside
|
|
367
|
+
// the native lifecycle lock. Never adopt that replacement's identity.
|
|
368
|
+
const previous = await context.service.locateSession(binding.accSessionId);
|
|
369
|
+
if (previous?.record.state !== "closed"
|
|
370
|
+
|| previous.record.generation !== binding.generation) {
|
|
371
|
+
throw new Error("the completed hook owner changed during turn registration");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
320
374
|
if (binding !== null) {
|
|
321
375
|
const resumed = await context.service.resumeSession({
|
|
322
376
|
sessionId: binding.accSessionId,
|
|
@@ -326,13 +380,21 @@ const HANDLERS = {
|
|
|
326
380
|
if (resumed !== null) {
|
|
327
381
|
const hookBinding = { accSessionId: resumed.sessionId, generation: resumed.generation,
|
|
328
382
|
...clientFacts, clientPid };
|
|
329
|
-
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
383
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
330
384
|
...hookBinding });
|
|
331
385
|
return { accSessionId: resumed.sessionId, generation: resumed.generation,
|
|
332
386
|
...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
|
|
333
387
|
}
|
|
334
388
|
}
|
|
389
|
+
// Persist ownership before creating its session. If the hook dies after
|
|
390
|
+
// creation, the next start resumes this exact pair. If creation never
|
|
391
|
+
// happened (or it was closed), a retry allocates a NEW pair, never revives it.
|
|
392
|
+
const opening = { sessionId: context.service.ids.next("session"),
|
|
393
|
+
generation: context.service.ids.next("generation") };
|
|
394
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
395
|
+
accSessionId: opening.sessionId, generation: opening.generation });
|
|
335
396
|
const session = await context.service.openSession({
|
|
397
|
+
...opening,
|
|
336
398
|
workspaceId: context.descriptor.id,
|
|
337
399
|
participantId: participantFor(adapterId, event.sessionId, context.env),
|
|
338
400
|
displayName: participantFor(adapterId, event.sessionId, context.env),
|
|
@@ -348,7 +410,7 @@ const HANDLERS = {
|
|
|
348
410
|
});
|
|
349
411
|
const hookBinding = { accSessionId: session.sessionId, generation: session.generation,
|
|
350
412
|
...clientFacts, clientPid };
|
|
351
|
-
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
413
|
+
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
352
414
|
...hookBinding });
|
|
353
415
|
return { accSessionId: session.sessionId, generation: session.generation,
|
|
354
416
|
...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
|
|
@@ -361,27 +423,59 @@ const HANDLERS = {
|
|
|
361
423
|
return {};
|
|
362
424
|
},
|
|
363
425
|
|
|
364
|
-
async sessionEnd({ binding, context, event, paths }) {
|
|
426
|
+
async sessionEnd({ binding, context, event, paths, adapter, deadline }) {
|
|
365
427
|
if (binding === null) return {};
|
|
366
|
-
await context.service
|
|
367
|
-
generation: binding.generation
|
|
368
|
-
|
|
428
|
+
await retireNativeBinding({ adapter, service: context.service, sessionId: binding.accSessionId,
|
|
429
|
+
generation: binding.generation, runtimeDir: paths.root,
|
|
430
|
+
timeoutMs: Math.max(1, Math.min(200, deadline - Date.now())) });
|
|
431
|
+
const owner = { sessionId: binding.accSessionId, generation: binding.generation };
|
|
432
|
+
const current = await context.service.locateSession(binding.accSessionId);
|
|
433
|
+
if (current?.record.generation === binding.generation && current.record.state === "open") {
|
|
434
|
+
await context.service.closeSession(owner);
|
|
435
|
+
} else {
|
|
436
|
+
// Close may have committed before this hook died. Retire only the old
|
|
437
|
+
// endpoint and binding; do not close a replacement or repeat a close event.
|
|
438
|
+
await context.service.clearDeliveryBinding(owner);
|
|
439
|
+
}
|
|
440
|
+
await clearSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId });
|
|
369
441
|
return {};
|
|
370
442
|
},
|
|
371
443
|
|
|
372
444
|
async beforeTurn(input) {
|
|
373
445
|
const { binding, context, adapter, event, paths, deadline } = input;
|
|
374
|
-
if (binding === null)
|
|
446
|
+
if (binding === null) {
|
|
447
|
+
// SessionStart can be skipped when an automatic runtime refresh races
|
|
448
|
+
// process admission. A genuine prompt is the next safe proof that this
|
|
449
|
+
// native conversation exists, and it is already inside the same
|
|
450
|
+
// lifecycle mutex as startup. Reuse that opening path so publication,
|
|
451
|
+
// crash recovery, client facts, and the native handshake stay atomic.
|
|
452
|
+
const started = await HANDLERS.sessionStart(input);
|
|
453
|
+
const fresh = await loadSessionBinding({ runtimeDir: paths.root,
|
|
454
|
+
harnessSessionId: event.sessionId });
|
|
455
|
+
const turn = await projectTurn({ ...input, binding: fresh });
|
|
456
|
+
return { ...turn, nativeBinding: started.nativeBinding };
|
|
457
|
+
}
|
|
458
|
+
const current = await context.service.locateSession(binding.accSessionId);
|
|
459
|
+
if (current?.record.state === "closed" && current.record.generation === binding.generation) {
|
|
460
|
+
// finish ends an ACC incarnation, not the native conversation. Only a
|
|
461
|
+
// genuine new user turn can start another one; tool hooks cannot. Reuse
|
|
462
|
+
// the crash-safe opening path, retaining its full published client facts
|
|
463
|
+
// and its single native handshake rather than binding a second time.
|
|
464
|
+
const started = await HANDLERS.sessionStart(input);
|
|
465
|
+
const fresh = await loadSessionBinding({ runtimeDir: paths.root,
|
|
466
|
+
harnessSessionId: event.sessionId });
|
|
467
|
+
const turn = await projectTurn({ ...input, binding: fresh });
|
|
468
|
+
return { ...turn, nativeBinding: started.nativeBinding };
|
|
469
|
+
}
|
|
375
470
|
// A turn is the clearest sign a session is alive. Never a reason to fail:
|
|
376
471
|
// this runs in front of somebody's prompt.
|
|
377
472
|
await context.service.heartbeatSession({ sessionId: binding.accSessionId,
|
|
378
|
-
generation: binding.generation })
|
|
473
|
+
generation: binding.generation });
|
|
379
474
|
// A native transport that became ready only after SessionStart is picked
|
|
380
475
|
// up here and a live lease is renewed: bounded, fail-open, never on a guard.
|
|
381
|
-
const nativeBinding =
|
|
382
|
-
:
|
|
383
|
-
|
|
384
|
-
deadline });
|
|
476
|
+
const nativeBinding = await bindNative({ adapter, event, hookBinding: binding,
|
|
477
|
+
clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
|
|
478
|
+
deadline });
|
|
385
479
|
const turn = await projectTurn(input);
|
|
386
480
|
return nativeBinding === undefined ? turn : { ...turn, nativeBinding };
|
|
387
481
|
},
|
|
@@ -458,42 +552,48 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
458
552
|
probeClientVersion = defaultProbeClientVersion,
|
|
459
553
|
platform = `${process.platform}-${process.arch}` }) {
|
|
460
554
|
const deadline = Date.now() + budgetMs;
|
|
461
|
-
const
|
|
555
|
+
const fallback = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
|
|
462
556
|
commitOffers: async () => {} };
|
|
463
|
-
|
|
464
|
-
|
|
557
|
+
const execute = async () => {
|
|
558
|
+
assertHookBudget(deadline);
|
|
465
559
|
const adapter = adapters?.[adapterId];
|
|
466
560
|
if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
|
|
467
561
|
|
|
468
562
|
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
|
-
|
|
563
|
+
const context = await openContext({ cwd: event.cwd, dataHome, runtime, env, deadline });
|
|
473
564
|
const handler = HANDLERS[event.kind];
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
565
|
+
const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
|
|
566
|
+
const invoke = async () => {
|
|
567
|
+
assertHookBudget(deadline);
|
|
568
|
+
if (lifecycle) {
|
|
569
|
+
// A previous holder may have died after journalling its session while
|
|
570
|
+
// this process waited. Recover that write before interpreting its binding.
|
|
571
|
+
const store = await openFilesystemStore({ root: context.paths.root, clock: runtime.clock,
|
|
572
|
+
ids: runtime.ids, workspaceId: context.descriptor.id, deadlineAt: deadline });
|
|
573
|
+
context.service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
|
|
574
|
+
}
|
|
575
|
+
const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
|
|
576
|
+
harnessSessionId: event.sessionId });
|
|
577
|
+
assertHookBudget(deadline);
|
|
578
|
+
return handler === undefined ? {} : handler({ event, context, adapter, adapterId,
|
|
579
|
+
binding, paths: context.paths,
|
|
477
580
|
readProcessTable, probeClientVersion, platform, deadline });
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
581
|
+
};
|
|
582
|
+
const work = lifecycle
|
|
583
|
+
? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
|
|
584
|
+
clock: runtime.clock, deadlineAt: deadline }, invoke)
|
|
585
|
+
: invoke();
|
|
586
|
+
// Keep the late continuation's result private. Once the budget wins, it
|
|
587
|
+
// cannot change the fail-open answer or restore withdrawn output/offers.
|
|
588
|
+
const result = { ...fallback, ...await work };
|
|
589
|
+
assertHookBudget(deadline);
|
|
486
590
|
const offerInputs = result.offerInputs ?? [];
|
|
487
591
|
let commitPromise = null;
|
|
488
592
|
result.commitOffers = () => {
|
|
489
593
|
if (commitPromise !== null) return commitPromise;
|
|
490
594
|
commitPromise = (async () => {
|
|
491
595
|
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.
|
|
596
|
+
assertHookBudget(deadline, "hook budget exhausted before offer commit");
|
|
497
597
|
await context.service.recordOfferSucceeded({ ...input, deadlineAt: deadline });
|
|
498
598
|
}
|
|
499
599
|
})();
|
|
@@ -503,18 +603,26 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
503
603
|
|
|
504
604
|
const status = await context.service.collectStatus({
|
|
505
605
|
workspaceId: context.descriptor.id });
|
|
606
|
+
assertHookBudget(deadline);
|
|
506
607
|
result.sessions = status.participants.filter(p => p.presence !== "offline");
|
|
507
608
|
result.service = context.service;
|
|
609
|
+
return result;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
let timer;
|
|
613
|
+
try {
|
|
614
|
+
// One budget covers normalization, discovery, recovery, handling and the
|
|
615
|
+
// final status read. Store/binding deadlines fence undecided writes; an
|
|
616
|
+
// activated journal still completes under its writer lock after expiry.
|
|
617
|
+
const budget = new Promise(resolve => {
|
|
618
|
+
timer = setTimeout(() => resolve({ ...fallback, timedOut: true }),
|
|
619
|
+
Math.max(0, deadline - Date.now()));
|
|
620
|
+
});
|
|
621
|
+
return await Promise.race([execute(), budget]);
|
|
508
622
|
} catch (error) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
result.decision = "allow";
|
|
512
|
-
result.stdout = "";
|
|
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 () => {};
|
|
623
|
+
return { ...fallback, failed: true, reason: error.message,
|
|
624
|
+
...(Date.now() >= deadline ? { timedOut: true } : {}) };
|
|
516
625
|
} finally {
|
|
517
|
-
|
|
626
|
+
clearTimeout(timer);
|
|
518
627
|
}
|
|
519
|
-
return result;
|
|
520
628
|
}
|
|
@@ -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
|
+
}
|