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
|
@@ -13,7 +13,7 @@ import { parseArgs, positiveNumber } from "./args.mjs";
|
|
|
13
13
|
// A usage error names what is missing rather than failing deeper in a service
|
|
14
14
|
// with the argument already half-applied.
|
|
15
15
|
const usage = message => new AccError(EXIT.USAGE, message);
|
|
16
|
-
import { describeCommands, helpText } from "./help.mjs";
|
|
16
|
+
import { commandHelpText, describeCommand, describeCommands, helpText } from "./help.mjs";
|
|
17
17
|
import { runUpdateCommand } from "./update-command.mjs";
|
|
18
18
|
import { runConfigCommand } from "./config-command.mjs";
|
|
19
19
|
import { runInstallCommand } from "./install-command.mjs";
|
|
@@ -73,23 +73,24 @@ async function locateContext(options, runtime) {
|
|
|
73
73
|
gitProbe: runtime.gitProbe ?? createGitProbe(),
|
|
74
74
|
explicitConfig: options.workspace,
|
|
75
75
|
});
|
|
76
|
+
const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
|
|
77
|
+
env: runtime.env });
|
|
76
78
|
const paths = runtimePaths({
|
|
77
|
-
dataHome
|
|
78
|
-
env: runtime.env }),
|
|
79
|
+
dataHome,
|
|
79
80
|
workspaceId: descriptor.id,
|
|
80
81
|
workspaceRoots: descriptor.roots,
|
|
81
82
|
});
|
|
82
|
-
return { descriptor, paths };
|
|
83
|
+
return { descriptor, paths, dataHome };
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
async function withService({ descriptor, paths }, runtime) {
|
|
86
|
+
async function withService({ descriptor, paths, dataHome }, runtime) {
|
|
86
87
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
87
88
|
ids: runtime.ids, workspaceId: descriptor.id });
|
|
88
89
|
const service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
|
|
89
|
-
return { descriptor, paths,
|
|
90
|
+
return { descriptor, paths, dataHome,
|
|
90
91
|
service,
|
|
91
92
|
deliveryRouter: typeof runtime.createDeliveryRouter === "function"
|
|
92
|
-
? runtime.createDeliveryRouter({ service, clock: runtime.clock }) : null };
|
|
93
|
+
? runtime.createDeliveryRouter({ service, clock: runtime.clock, dataHome }) : null };
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
async function openContext(options, runtime) {
|
|
@@ -200,7 +201,9 @@ const HANDLERS = Object.freeze({
|
|
|
200
201
|
sync: async ({ options, context }) => {
|
|
201
202
|
const result = await context.service.sync({ sessionId: options.session,
|
|
202
203
|
cursor: options.cursor ?? null, scope: options.scope,
|
|
204
|
+
messageId: options.message, kind: options.type, current: options.current,
|
|
203
205
|
limit: options.limit ? positiveNumber(options.limit, "limit") : undefined });
|
|
206
|
+
if (result.scope === "history") return { data: result, text: JSON.stringify(result, null, 2) };
|
|
204
207
|
// Solo zero-overhead: nothing to say means nothing printed, not a banner.
|
|
205
208
|
const text = result.solo ? "" : `${result.attention.length} attention; `
|
|
206
209
|
+ `${result.roster.length} session(s); cursor ${result.cursor}`;
|
|
@@ -250,7 +253,9 @@ const HANDLERS = Object.freeze({
|
|
|
250
253
|
context.service.sendMessage({ sessionId: options.session,
|
|
251
254
|
generation: options.generation, clientMessageId: clientMessageId(options, context),
|
|
252
255
|
toParticipantIds, kind,
|
|
253
|
-
|
|
256
|
+
supersedes: options.supersedes, withdraws: options.withdraws,
|
|
257
|
+
obligation: obligationFor(kind, options.obligation, toParticipantIds.length > 0
|
|
258
|
+
|| options.supersedes !== undefined || options.withdraws !== undefined),
|
|
254
259
|
subject: options.subject, body: options.body, descriptor: context.descriptor }) });
|
|
255
260
|
const message = routed.recorded;
|
|
256
261
|
return { data: { message, delivery: routed.delivery },
|
|
@@ -258,6 +263,15 @@ const HANDLERS = Object.freeze({
|
|
|
258
263
|
},
|
|
259
264
|
|
|
260
265
|
inbox: async ({ options, context }) => {
|
|
266
|
+
if (options.message === undefined) {
|
|
267
|
+
const page = await context.service.listInbox({ sessionId: options.session,
|
|
268
|
+
generation: options.generation, cursor: options.cursor,
|
|
269
|
+
limit: options.limit === undefined ? undefined : positiveNumber(options.limit, "limit") });
|
|
270
|
+
return { data: page, text: JSON.stringify(page, null, 2) };
|
|
271
|
+
}
|
|
272
|
+
if (options.cursor !== undefined || options.limit !== undefined) {
|
|
273
|
+
throw usage("an exact inbox read cannot use --cursor or --limit");
|
|
274
|
+
}
|
|
261
275
|
const messages = await context.service.readInbox({ sessionId: options.session,
|
|
262
276
|
generation: options.generation, messageId: options.message });
|
|
263
277
|
return { data: messages, text: messages.length === 0
|
|
@@ -271,8 +285,8 @@ const HANDLERS = Object.freeze({
|
|
|
271
285
|
generation: options.generation, messageId: options.message, body: options.body,
|
|
272
286
|
subject: options.subject, clientMessageId: clientMessageId(options, context) }) });
|
|
273
287
|
const result = routed.recorded;
|
|
274
|
-
return { data: { message: result.reply, delivery: routed.delivery },
|
|
275
|
-
text: recordedText(result.reply, routed.delivery) };
|
|
288
|
+
return { data: { message: result.reply, receipt: result.receipt, delivery: routed.delivery },
|
|
289
|
+
text: `${recordedText(result.reply, routed.delivery)}; acknowledged ${result.receipt.messageId}` };
|
|
276
290
|
},
|
|
277
291
|
|
|
278
292
|
request: async ({ options, context }) => {
|
|
@@ -346,7 +360,7 @@ const HANDLERS = Object.freeze({
|
|
|
346
360
|
ids: runtime.ids });
|
|
347
361
|
if (result.subcommand === "validate") {
|
|
348
362
|
return { data: result, text: result.present
|
|
349
|
-
? `${result.file} is valid` : `no ${CONFIG_FILENAME};
|
|
363
|
+
? `${result.file} is valid` : `no ${CONFIG_FILENAME} in selected directory; ancestors not checked` };
|
|
350
364
|
}
|
|
351
365
|
return { data: result,
|
|
352
366
|
text: result.written ? `wrote ${result.file}` : `not written: ${result.file}` };
|
|
@@ -362,7 +376,13 @@ const HANDLERS = Object.freeze({
|
|
|
362
376
|
|
|
363
377
|
update: async ({ options, runtime }) => runUpdateCommand({ options, runtime }),
|
|
364
378
|
|
|
365
|
-
help: async (
|
|
379
|
+
help: async ({ options }) => {
|
|
380
|
+
if (options.helpCommand === undefined) {
|
|
381
|
+
return { data: { commands: describeCommands() }, text: helpText() };
|
|
382
|
+
}
|
|
383
|
+
const data = describeCommand(options.helpCommand, options.subcommand);
|
|
384
|
+
return { data, text: commandHelpText(data) };
|
|
385
|
+
},
|
|
366
386
|
|
|
367
387
|
version: async ({ runtime }) => {
|
|
368
388
|
// Read by the composition root from the package manifest: `bin/` sits at
|
|
@@ -390,6 +410,9 @@ export async function main(argv, runtime) {
|
|
|
390
410
|
let parsed;
|
|
391
411
|
try {
|
|
392
412
|
parsed = parseArgs(argv);
|
|
413
|
+
if (runtime.managementOnly && !["help", "version", "update"].includes(parsed.command)) {
|
|
414
|
+
throw new AccError(EXIT.DATA, "runtime unavailable; retry after the update or run acc update to recover");
|
|
415
|
+
}
|
|
393
416
|
// `config` is the one command that must work on a workspace ACC cannot
|
|
394
417
|
// open. Discovery validates the config too, so a broken one would fail
|
|
395
418
|
// there first and `acc config validate` - the command a user runs to find
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { assertPortableId } from "@agents-can-communicate/protocol";
|
|
6
|
+
import { readSessionRecord } from "@agents-can-communicate/storage-filesystem";
|
|
7
|
+
import { listRuntimeHolds } from "./leases.mjs";
|
|
8
|
+
import { confirmedDead, defaultPidIsAlive, withManagerLock } from "./mutex.mjs";
|
|
9
|
+
import { canonicalManagerRoot, managedDirectory, readControl, readManagedJson, writeControl } from "./state.mjs";
|
|
10
|
+
|
|
11
|
+
// Both 0.3.1 and current MCP write this exact continuity record, without
|
|
12
|
+
// native client facts. A foreign harness id may look like an MCP key: require
|
|
13
|
+
// its canonical file and a validated matching MCP owner before exempting it.
|
|
14
|
+
async function isMcpContinuity(record, name, workspaceRoot, workspaceId) {
|
|
15
|
+
if (record?.schemaVersion !== 1 || Object.keys(record).sort().join(",")
|
|
16
|
+
!== "accSessionId,generation,harnessSessionId,schemaVersion") return false;
|
|
17
|
+
try {
|
|
18
|
+
const parts = record.harnessSessionId.split(":");
|
|
19
|
+
if (parts.length !== 3 || parts[0] !== "mcp" || parts[2] !== workspaceId) return false;
|
|
20
|
+
for (const id of [parts[1], parts[2], record.accSessionId, record.generation]) assertPortableId(id, "MCP continuity id");
|
|
21
|
+
const expected = createHash("sha256").update(record.harnessSessionId).digest("hex").slice(0, 32) + ".json";
|
|
22
|
+
if (name !== expected) return false;
|
|
23
|
+
const owner = await readSessionRecord({ root: workspaceRoot, workspaceId, sessionId: record.accSessionId });
|
|
24
|
+
return owner?.harness === "mcp" && owner.participantId === parts[1]
|
|
25
|
+
&& owner.workspaceId === workspaceId && owner.sessionId === record.accSessionId
|
|
26
|
+
&& owner.generation === record.generation;
|
|
27
|
+
} catch { return false; } // Ambiguous or corrupt ownership remains a safety hold.
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Native bindings outlive finish/TTL; MCP process lifetime is covered by its runtime lease. */
|
|
31
|
+
export async function listNativeHolds(root, { pidIsAlive = defaultPidIsAlive } = {}) {
|
|
32
|
+
const workspaces = path.join(path.dirname(await canonicalManagerRoot(root)), "workspaces");
|
|
33
|
+
if (!await managedDirectory(workspaces)) return [];
|
|
34
|
+
const holds = [];
|
|
35
|
+
for (const entry of await readdir(workspaces, { withFileTypes: true })) {
|
|
36
|
+
if (!entry.isDirectory()) {
|
|
37
|
+
if (entry.isSymbolicLink()) holds.push({ kind: "native", reason: "unknown_workspace" });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const bindings = path.join(workspaces, entry.name, "bindings");
|
|
41
|
+
if (!await managedDirectory(bindings)) continue;
|
|
42
|
+
for (const name of await readdir(bindings)) {
|
|
43
|
+
if (!name.endsWith(".json")) continue;
|
|
44
|
+
const record = await readManagedJson(path.join(bindings, name)).catch(() => null);
|
|
45
|
+
if (record === undefined) continue; // Concurrent lifecycle removal, before fencing.
|
|
46
|
+
if (await isMcpContinuity(record, name, path.dirname(bindings), entry.name)) continue;
|
|
47
|
+
const pid = record?.schemaVersion === 1 && Number.isSafeInteger(record.clientPid)
|
|
48
|
+
&& record.clientPid > 0 ? record.clientPid : null;
|
|
49
|
+
if (pid !== null && await confirmedDead(pid, pidIsAlive)) continue;
|
|
50
|
+
holds.push({ kind: "native", pid, reason: pid === null ? "unknown_client_pid" : "client_running" });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return holds;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function prepareCandidate(control, root, env) {
|
|
57
|
+
const file = path.join(control.pending.root, "node_modules", "@agents-can-communicate", "cli",
|
|
58
|
+
"src", "managed-runtime", "refresh.mjs");
|
|
59
|
+
const { prepareRefresh } = await import(pathToFileURL(file).href);
|
|
60
|
+
return prepareRefresh({ control, root, env });
|
|
61
|
+
}
|
|
62
|
+
// JSON parser diagnostics may quote private config bytes. Keep the cause and
|
|
63
|
+
// known artifact paths, never the offending input or terminal control bytes.
|
|
64
|
+
const safeText = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, " ").slice(0, 1000);
|
|
65
|
+
function safeFailure(error) {
|
|
66
|
+
const message = typeof error === "string" ? error : "integration refresh failed";
|
|
67
|
+
if (/(?:valid|parse|parsing) JSON|JSON (?:input|at position)|Unexpected token|Expected.*(?:property|comma|colon)/i.test(message)) return "configuration is not valid JSON";
|
|
68
|
+
return safeText(message);
|
|
69
|
+
}
|
|
70
|
+
const recipe = c => JSON.stringify([c.active, c.pending, c.home, c.targets, c.auto, c.pin]);
|
|
71
|
+
|
|
72
|
+
/** Detection is outside the fence; every integration write and commit stays inside it. */
|
|
73
|
+
export async function activatePending(root, { prepare = prepareCandidate, env = process.env,
|
|
74
|
+
pidIsAlive = defaultPidIsAlive, ignorePid = null } = {}) {
|
|
75
|
+
root = await canonicalManagerRoot(root);
|
|
76
|
+
const before = await readControl(root);
|
|
77
|
+
if (!before?.pending) return { activated: false, reason: "no_pending_update" };
|
|
78
|
+
const apply = await prepare(before, root, env);
|
|
79
|
+
return withManagerLock(root, async () => {
|
|
80
|
+
const current = await readControl(root);
|
|
81
|
+
if (!current || recipe(current) !== recipe(before)) return { activated: false, reason: "state_changed" };
|
|
82
|
+
const holds = (await listRuntimeHolds(root, { pidIsAlive })).filter(lease => lease.pid !== ignorePid);
|
|
83
|
+
holds.push(...await listNativeHolds(root, { pidIsAlive }));
|
|
84
|
+
if (holds.length) {
|
|
85
|
+
const notice = `ACC ${current.pending.version} is ready; waiting for ${holds.length} active or unidentified process(es).`;
|
|
86
|
+
await writeControl(root, { ...current, notice });
|
|
87
|
+
return { activated: false, reason: "processes_active", holds: holds.length, notice };
|
|
88
|
+
}
|
|
89
|
+
const activating = { ...current, phase: "activating", notice: "Finishing integration refresh." };
|
|
90
|
+
await writeControl(root, activating);
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await apply(); // Never release the fence with a mutating promise still running.
|
|
94
|
+
if (!result || !Array.isArray(result.failed) || result.failed.length) {
|
|
95
|
+
throw new Error("integration refresh is incomplete");
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const notice = "Integration refresh is incomplete; run acc update to recover.";
|
|
99
|
+
await writeControl(root, { ...activating, notice });
|
|
100
|
+
const installation = result && Array.isArray(result.failed) ? { ...result,
|
|
101
|
+
failed: result.failed.map(failure => ({ ...failure, adapterId: safeText(failure.adapterId), error: safeFailure(failure.error),
|
|
102
|
+
paths: (failure.paths ?? []).map(file => safeText(file)) })) } : undefined;
|
|
103
|
+
return { activated: false, reason: "refresh_failed", notice, error: safeFailure(error.message),
|
|
104
|
+
...(installation ? { installation } : {}) };
|
|
105
|
+
}
|
|
106
|
+
await writeControl(root, { ...activating, active: current.pending, pending: null,
|
|
107
|
+
phase: "ready", notice: `ACC ${current.pending.version} is active.` });
|
|
108
|
+
return { activated: true, version: current.pending.version, installation: result };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
|
+
import { stableVersion, newerVersion } from "./download.mjs";
|
|
3
|
+
import { withManagerLock } from "./mutex.mjs";
|
|
4
|
+
import { scheduleWorker } from "./schedule.mjs";
|
|
5
|
+
import { readControl, writeControl } from "./state.mjs";
|
|
6
|
+
import { performUpdate, runWorker } from "./worker.mjs";
|
|
7
|
+
|
|
8
|
+
export function validateUpdateOptions(options) {
|
|
9
|
+
if (options.auto !== undefined && !["on", "off"].includes(options.auto)) {
|
|
10
|
+
throw new AccError(EXIT.USAGE, "--auto expects on or off");
|
|
11
|
+
}
|
|
12
|
+
if (options.pin !== undefined && options.pin !== "none" && !stableVersion(options.pin)) {
|
|
13
|
+
throw new AccError(EXIT.USAGE, "--pin expects an exact stable version or none");
|
|
14
|
+
}
|
|
15
|
+
if (options.check && (options.auto !== undefined || options.pin !== undefined)) {
|
|
16
|
+
throw new AccError(EXIT.USAGE, "--check cannot change update settings");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runManagedUpdate({ options, runtime }) {
|
|
21
|
+
const root = runtime.managerRoot;
|
|
22
|
+
const env = runtime.env ?? {};
|
|
23
|
+
try {
|
|
24
|
+
if (options.auto !== undefined || options.pin !== undefined) {
|
|
25
|
+
const control = await withManagerLock(root, async () => {
|
|
26
|
+
const current = await readControl(root);
|
|
27
|
+
if (!current) throw new Error("run acc install before configuring automatic updates");
|
|
28
|
+
if (current.phase !== "ready" && options.pin !== undefined) throw new Error("finish the interrupted update before changing its version pin");
|
|
29
|
+
const pin = options.pin === undefined ? current.pin : options.pin === "none" ? null : options.pin;
|
|
30
|
+
if (pin !== null && newerVersion(current.active.version, pin)) {
|
|
31
|
+
throw new Error("pin cannot downgrade workspace data; choose the running version or a newer release");
|
|
32
|
+
}
|
|
33
|
+
return writeControl(root, { ...current,
|
|
34
|
+
auto: options.auto === undefined ? current.auto : options.auto === "on", pin,
|
|
35
|
+
autoPreference: options.auto === undefined ? current.autoPreference ?? current.auto : options.auto === "on",
|
|
36
|
+
pending: options.pin !== undefined && current.pending?.version !== pin ? null : current.pending,
|
|
37
|
+
notice: null });
|
|
38
|
+
});
|
|
39
|
+
await scheduleWorker(root, control, { env });
|
|
40
|
+
return { data: { auto: control.auto, pin: control.pin, running: control.active.version },
|
|
41
|
+
text: `Automatic updates ${control.auto ? "on" : "off"}. ${control.pin ? `Pinned to ${control.pin}.` : "Following stable releases."}` };
|
|
42
|
+
}
|
|
43
|
+
const result = options.check
|
|
44
|
+
? await performUpdate(root, { check: true, env })
|
|
45
|
+
: await runWorker(root, { force: true, env, ignorePid: process.pid });
|
|
46
|
+
const text = result.activated ? `Updated ACC to ${result.version}; integrations refreshed.`
|
|
47
|
+
: result.notice && result.reason === "processes_active" ? result.notice
|
|
48
|
+
: result.reason === "refresh_failed" ? [result.notice,
|
|
49
|
+
...(result.installation?.failed ?? []).map(failure =>
|
|
50
|
+
`${failure.adapterId}: ${failure.error}${failure.paths?.length ? ` (${failure.paths.join(", ")})` : ""}`),
|
|
51
|
+
...(!result.installation?.failed?.length && result.error ? [result.error] : [])].join("\n")
|
|
52
|
+
: result.checked ? result.newer ? `ACC ${result.latest} is available; run acc update.`
|
|
53
|
+
: `ACC ${result.running} is current${result.pin ? ` for pin ${result.pin}` : ""}.`
|
|
54
|
+
: result.reason === "network_disabled" ? "Update networking is off (ACC_NO_UPDATE_CHECK)."
|
|
55
|
+
: result.notice ?? "Update is pending.";
|
|
56
|
+
return { data: result, text,
|
|
57
|
+
...(result.reason === "refresh_failed" ? { error: new AccError(EXIT.DATA, text) } : {}) };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (/manager lock held/.test(error.message)) {
|
|
60
|
+
return { data: { inProgress: true }, text: "An update is already in progress; ACC will activate it when running clients exit." };
|
|
61
|
+
}
|
|
62
|
+
throw error instanceof AccError ? error : new AccError(EXIT.DATA, error.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { stageOwnGeneration } from "./generation.mjs";
|
|
6
|
+
import { verifyGeneration } from "./install.mjs";
|
|
7
|
+
import { managedDirectory } from "./state.mjs";
|
|
8
|
+
import { ENTRY_KINDS } from "./entry.mjs";
|
|
9
|
+
|
|
10
|
+
const NAME = "agents-can-communicate";
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
export const stableVersion = version => typeof version === "string"
|
|
13
|
+
&& /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
|
|
14
|
+
export function newerVersion(candidate, current) {
|
|
15
|
+
if (!stableVersion(candidate) || !stableVersion(current)) return false;
|
|
16
|
+
const left = candidate.split(".").map(BigInt), right = current.split(".").map(BigInt);
|
|
17
|
+
for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] > right[i];
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Metadata only. Downloading and npm run exclusively in the update worker. */
|
|
22
|
+
export async function fetchRelease({ pin = null, env = process.env, get = fetch } = {}) {
|
|
23
|
+
if (pin !== null && !stableVersion(pin)) throw new Error("pin must be an exact stable version");
|
|
24
|
+
const registry = new URL(env.npm_config_registry || "https://registry.npmjs.org/");
|
|
25
|
+
if (!["https:", "http:"].includes(registry.protocol)) throw new Error("invalid npm registry");
|
|
26
|
+
registry.pathname = `${registry.pathname.replace(/\/$/, "")}/`;
|
|
27
|
+
const url = new URL(`${NAME}/${pin ?? "latest"}`, registry).href;
|
|
28
|
+
const response = await get(url, { signal: AbortSignal.timeout(5_000), headers: { accept: "application/json" } });
|
|
29
|
+
if (!response?.ok) throw new Error(`registry request failed (${response?.status ?? "no response"})`);
|
|
30
|
+
const body = await response.json();
|
|
31
|
+
if (body?.name !== NAME) throw new Error("release package identity mismatch");
|
|
32
|
+
if (!stableVersion(body.version) || pin !== null && body.version !== pin) throw new Error("release is not the requested stable version");
|
|
33
|
+
const integrity = body.dist?.integrity;
|
|
34
|
+
if (typeof integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity)
|
|
35
|
+
|| Buffer.from(integrity.slice(7), "base64").length !== 64) throw new Error("release integrity is missing or invalid");
|
|
36
|
+
return { version: body.version, integrity, registry: registry.href };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function npmCli(env) {
|
|
40
|
+
const directories = [path.dirname(process.execPath), ...(env.PATH ?? "").split(path.delimiter)];
|
|
41
|
+
for (const directory of directories.filter(Boolean)) {
|
|
42
|
+
try { return await realpath(path.join(directory, "npm")); } catch { /* Try the next installed npm. */ }
|
|
43
|
+
}
|
|
44
|
+
throw new Error("npm is unavailable; the working ACC runtime was kept");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function downloadRelease(root, release, { env = process.env } = {}) {
|
|
48
|
+
const downloads = path.join(root, "downloads");
|
|
49
|
+
await managedDirectory(downloads, { create: true });
|
|
50
|
+
const temporary = await mkdtemp(path.join(downloads, ".download-"));
|
|
51
|
+
try {
|
|
52
|
+
await writeFile(path.join(temporary, "package.json"), JSON.stringify({ private: true, name: "acc-runtime-download", version: "1.0.0" }));
|
|
53
|
+
await exec(process.execPath, [await npmCli(env), "install", "--ignore-scripts", "--omit=dev",
|
|
54
|
+
"--global=false", "--prefix", temporary, "--package-lock=true",
|
|
55
|
+
"--package-lock-only=false", "--dry-run=false", "--workspaces=false",
|
|
56
|
+
"--include-workspace-root=false", "--no-audit", "--no-fund", "--save=true", "--save-exact", "--registry", release.registry,
|
|
57
|
+
`${NAME}@${release.version}`], { cwd: temporary, env, timeout: 90_000, maxBuffer: 1024 * 1024 });
|
|
58
|
+
const lock = JSON.parse(await readFile(path.join(temporary, "package-lock.json"), "utf8"));
|
|
59
|
+
if (lock.packages?.[`node_modules/${NAME}`]?.integrity !== release.integrity) {
|
|
60
|
+
throw new Error("downloaded release integrity differs from discovery");
|
|
61
|
+
}
|
|
62
|
+
const packageRoot = path.join(temporary, "node_modules", NAME);
|
|
63
|
+
const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
64
|
+
if (manifest.name !== NAME || manifest.version !== release.version) throw new Error("downloaded package identity mismatch");
|
|
65
|
+
for (const name of ENTRY_KINDS) await access(path.join(packageRoot, "bin", "entrypoints", `${name}.mjs`));
|
|
66
|
+
await access(path.join(packageRoot, "bin", "acc-update-worker.mjs"));
|
|
67
|
+
const generation = await stageOwnGeneration({ packageRoot, managerRoot: root });
|
|
68
|
+
await verifyGeneration(generation, { env });
|
|
69
|
+
return { version: generation.version, root: generation.root };
|
|
70
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
71
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { scheduleWorker } from "./schedule.mjs";
|
|
5
|
+
import { acquireRuntime } from "./leases.mjs";
|
|
6
|
+
import { canonicalManagerRoot, readControl } from "./state.mjs";
|
|
7
|
+
|
|
8
|
+
export const ENTRY_KINDS = Object.freeze([
|
|
9
|
+
"acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const BOOTSTRAP_FLAGS = new Map([["--adapter", "adapter"], ["--real-executable", "realExecutable"],
|
|
13
|
+
["--data-home", "dataHome"]]);
|
|
14
|
+
|
|
15
|
+
/** Shared with the runtime; parse before admission without importing installer code. */
|
|
16
|
+
export function parseBootstrapOptions(args) {
|
|
17
|
+
const options = {};
|
|
18
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
19
|
+
const key = BOOTSTRAP_FLAGS.get(args[index]);
|
|
20
|
+
const value = args[index + 1];
|
|
21
|
+
if (key === undefined || typeof value !== "string" || value === "" || key in options) return null;
|
|
22
|
+
options[key] = value;
|
|
23
|
+
}
|
|
24
|
+
return BOOTSTRAP_FLAGS.values().every(key => key in options) && path.isAbsolute(options.dataHome)
|
|
25
|
+
? options : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Bootstrap resolution deliberately needs only Node built-ins: no selected
|
|
29
|
+
// generation is imported until admission and its actual PID lease are durable.
|
|
30
|
+
export function managerLocation({ env = process.env, platform = process.platform } = {}) {
|
|
31
|
+
const data = env.ACC_DATA_HOME || (platform === "win32" ? env.APPDATA
|
|
32
|
+
: platform === "darwin" ? env.HOME && path.join(env.HOME, "Library", "Application Support")
|
|
33
|
+
: env.XDG_DATA_HOME || env.HOME && path.join(env.HOME, ".local", "share"));
|
|
34
|
+
if (typeof data !== "string" || !path.isAbsolute(data)) throw new Error("cannot resolve ACC data home");
|
|
35
|
+
return path.join(data, "acc", "runtime");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function invokedDirectly(url) {
|
|
39
|
+
try { return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(url)); }
|
|
40
|
+
catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unavailable(kind) {
|
|
44
|
+
// Never reflect control file content or payloads into a client diagnostic.
|
|
45
|
+
if (kind === "acc-hook" || kind === "acc-claude-channel") {
|
|
46
|
+
process.stderr.write("acc: coordination unavailable during runtime update; session may continue\n");
|
|
47
|
+
process.exitCode = 0;
|
|
48
|
+
} else if (kind === "acc-bootstrap") {
|
|
49
|
+
process.exitCode = 1; // The vendor shell shim launches its original command.
|
|
50
|
+
} else {
|
|
51
|
+
process.stderr.write("acc: runtime unavailable; retry after the update or run acc update to recover\n");
|
|
52
|
+
process.exitCode = 4;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The lease lasts until OS process death, including callbacks after main returns. */
|
|
57
|
+
export async function runEntry({ kind, packageRoot, managerRoot, managedRequired = false }) {
|
|
58
|
+
if (!ENTRY_KINDS.includes(kind)) throw new Error("unknown ACC entry point");
|
|
59
|
+
const bootstrapOptions = kind === "acc-bootstrap" ? parseBootstrapOptions(process.argv.slice(2)) : null;
|
|
60
|
+
if (kind === "acc-bootstrap" && bootstrapOptions === null) {
|
|
61
|
+
if (process.env.ACC_BOOTSTRAP_DEBUG === "1") {
|
|
62
|
+
process.stderr.write("acc-bootstrap: usage: --adapter <id> --real-executable <path> --data-home <path>\n");
|
|
63
|
+
}
|
|
64
|
+
process.exitCode = 2;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let selected = packageRoot;
|
|
68
|
+
let managed = null;
|
|
69
|
+
let root;
|
|
70
|
+
try {
|
|
71
|
+
const requestedDataHome = bootstrapOptions === null ? null : await canonicalManagerRoot(bootstrapOptions.dataHome);
|
|
72
|
+
const requestedRoot = requestedDataHome === null ? null
|
|
73
|
+
: await canonicalManagerRoot(path.join(requestedDataHome, "acc", "runtime"));
|
|
74
|
+
if (managerRoot && requestedDataHome !== null
|
|
75
|
+
&& requestedDataHome !== await canonicalManagerRoot(path.dirname(path.dirname(managerRoot)))) {
|
|
76
|
+
throw new Error("bootstrap cache home differs from its launcher");
|
|
77
|
+
}
|
|
78
|
+
root = await canonicalManagerRoot(managerRoot ?? requestedRoot ?? managerLocation());
|
|
79
|
+
if (requestedRoot !== null && root !== requestedRoot) throw new Error("bootstrap data home differs from its launcher");
|
|
80
|
+
const control = await readControl(root);
|
|
81
|
+
if (control !== null) {
|
|
82
|
+
selected = control.active.root;
|
|
83
|
+
managed = root;
|
|
84
|
+
const lease = await acquireRuntime(root, { pid: process.pid, kind });
|
|
85
|
+
selected = lease.runtime.root;
|
|
86
|
+
managed = root;
|
|
87
|
+
const quiet = kind === "acc" && ["update", "install", "uninstall", "help", "version",
|
|
88
|
+
"--help", "-h", "--version", "-v", "-V"].includes(process.argv[2]);
|
|
89
|
+
if (!quiet) await scheduleWorker(root, control);
|
|
90
|
+
} else if (managedRequired) throw new Error("managed runtime is not initialized");
|
|
91
|
+
} catch {
|
|
92
|
+
// Help and update recovery must remain reachable when state cannot admit a
|
|
93
|
+
// workspace command. The CLI parser enforces this restricted path; merely
|
|
94
|
+
// putting --help in a message value cannot bypass runtime admission.
|
|
95
|
+
if (kind === "acc" && selected) {
|
|
96
|
+
const runtime = await import(pathToFileURL(path.join(selected, "bin", "entrypoints", "acc.mjs")).href);
|
|
97
|
+
await runtime.main({ managerRoot: root ?? null, packageRoot: selected, managementOnly: true });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
unavailable(kind);
|
|
101
|
+
if (kind === "acc-claude-channel" && selected) {
|
|
102
|
+
const { startInertChannel } = await import(pathToFileURL(path.join(selected,
|
|
103
|
+
"bin", "entrypoints", "claude-channel-stdio.mjs")).href);
|
|
104
|
+
startInertChannel();
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const runtime = await import(pathToFileURL(path.join(selected, "bin", "entrypoints", `${kind}.mjs`)).href);
|
|
109
|
+
await runtime.main({ managerRoot: managed, packageRoot: selected });
|
|
110
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm }
|
|
3
|
+
from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const NAME = "agents-can-communicate";
|
|
7
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
8
|
+
|
|
9
|
+
async function canonicalFuturePath(file) {
|
|
10
|
+
let current = path.resolve(file);
|
|
11
|
+
const trailing = [];
|
|
12
|
+
for (;;) {
|
|
13
|
+
try { return path.join(await realpath(current), ...trailing); }
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code !== "ENOENT") throw error;
|
|
16
|
+
const parent = path.dirname(current);
|
|
17
|
+
if (parent === current) throw error;
|
|
18
|
+
trailing.unshift(path.basename(current));
|
|
19
|
+
current = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function directory(file) {
|
|
25
|
+
await mkdir(file, { recursive: true, mode: 0o700 });
|
|
26
|
+
const stat = await lstat(file);
|
|
27
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed runtime directory is not a regular directory");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function declaredPath(value) {
|
|
31
|
+
if (typeof value !== "string" || value === "" || /[\\*?\[\]{}\0]/.test(value)
|
|
32
|
+
|| path.isAbsolute(value) || value.split("/").includes("..")) {
|
|
33
|
+
throw new Error("invalid declared runtime path");
|
|
34
|
+
}
|
|
35
|
+
return value.replace(/\/+$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function collectFile(source, relative, collected) {
|
|
39
|
+
const stat = await lstat(source);
|
|
40
|
+
if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
|
|
41
|
+
if (stat.isDirectory()) {
|
|
42
|
+
for (const entry of (await readdir(source)).sort()) {
|
|
43
|
+
await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
|
|
44
|
+
}
|
|
45
|
+
} else if (stat.isFile()) {
|
|
46
|
+
collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
|
|
47
|
+
} else throw new Error(`unsupported runtime file: ${relative}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function checkDeclaredComponents(source, relative) {
|
|
51
|
+
let current = source;
|
|
52
|
+
for (const component of relative.split("/")) {
|
|
53
|
+
current = path.join(current, component);
|
|
54
|
+
if ((await lstat(current)).isSymbolicLink()) {
|
|
55
|
+
throw new Error(`symbolic link inside declared runtime path: ${relative}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requireOutside(source, root) {
|
|
61
|
+
const relative = path.relative(source, root);
|
|
62
|
+
if (relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))) {
|
|
63
|
+
throw new Error("managed runtime must live outside its source package");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function collectPackage(source, prefix, collected, expectedName, version) {
|
|
68
|
+
await checkDeclaredComponents(source, "package.json");
|
|
69
|
+
const manifestFile = path.join(source, "package.json");
|
|
70
|
+
const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
|
|
71
|
+
if (manifest.name !== expectedName || manifest.version !== version
|
|
72
|
+
|| !Array.isArray(manifest.files)) throw new Error("runtime package identity or declared files are invalid");
|
|
73
|
+
const declared = new Set(["package.json", ...manifest.files.map(declaredPath)]);
|
|
74
|
+
// npm includes these public package files regardless of the files whitelist.
|
|
75
|
+
for (const file of await readdir(source)) {
|
|
76
|
+
if (/^(readme|licen[cs]e)(\..*)?$/i.test(file)) declared.add(file);
|
|
77
|
+
}
|
|
78
|
+
for (const relative of declared) {
|
|
79
|
+
await checkDeclaredComponents(source, relative);
|
|
80
|
+
await collectFile(path.join(source, relative), path.posix.join(prefix, relative), collected);
|
|
81
|
+
}
|
|
82
|
+
return manifest;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fingerprint(files) {
|
|
86
|
+
const hash = createHash("sha256");
|
|
87
|
+
for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
|
|
88
|
+
hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
|
|
89
|
+
hash.update(file.bytes);
|
|
90
|
+
}
|
|
91
|
+
return hash.digest("hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function existingMatches(target, files) {
|
|
95
|
+
try {
|
|
96
|
+
const actual = new Map();
|
|
97
|
+
await collectFile(target, "", actual);
|
|
98
|
+
if (fingerprint(actual) !== fingerprint(files)) throw new Error("managed runtime generation integrity changed");
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.code === "ENOENT") {
|
|
102
|
+
// Only an absent generation is new. A damaged existing tree is not a
|
|
103
|
+
// staging directory we are entitled to overwrite.
|
|
104
|
+
const exists = await lstat(target).then(() => true, e => {
|
|
105
|
+
if (e.code === "ENOENT") return false;
|
|
106
|
+
throw e;
|
|
107
|
+
});
|
|
108
|
+
if (!exists) return false;
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Materialize the package's shipped files; never copy a checkout or workspace symlink. */
|
|
115
|
+
export async function stageOwnGeneration({ packageRoot, managerRoot }) {
|
|
116
|
+
const source = await realpath(packageRoot);
|
|
117
|
+
const root = await canonicalFuturePath(managerRoot);
|
|
118
|
+
requireOutside(source, root);
|
|
119
|
+
await checkDeclaredComponents(source, "package.json");
|
|
120
|
+
const manifest = JSON.parse(await readFile(path.join(source, "package.json"), "utf8"));
|
|
121
|
+
if (manifest.name !== NAME || !STABLE.test(manifest.version)) throw new Error("invalid ACC runtime version");
|
|
122
|
+
const files = new Map();
|
|
123
|
+
await collectPackage(source, "", files, NAME, manifest.version);
|
|
124
|
+
if (!Array.isArray(manifest.bundleDependencies)) throw new Error("runtime must declare its bundled workspaces");
|
|
125
|
+
for (const name of manifest.bundleDependencies) {
|
|
126
|
+
if (!/^@agents-can-communicate\/[a-z0-9-]+$/.test(name)) throw new Error("invalid bundled runtime package");
|
|
127
|
+
const location = await realpath(path.join(source, "node_modules", name));
|
|
128
|
+
requireOutside(location, root);
|
|
129
|
+
await collectPackage(location, `node_modules/${name}`, files, name, manifest.version);
|
|
130
|
+
}
|
|
131
|
+
const digest = fingerprint(files);
|
|
132
|
+
await directory(root);
|
|
133
|
+
const generations = path.join(root, "generations");
|
|
134
|
+
await directory(generations);
|
|
135
|
+
const target = path.join(generations, `${manifest.version}-${digest.slice(0, 24)}`);
|
|
136
|
+
const result = { version: manifest.version, root: target, digest };
|
|
137
|
+
if (await existingMatches(target, files)) return result;
|
|
138
|
+
const staging = await mkdtemp(path.join(generations, ".staging-"));
|
|
139
|
+
try {
|
|
140
|
+
const directories = new Set([staging]);
|
|
141
|
+
for (const [name, file] of files) {
|
|
142
|
+
const destination = path.join(staging, name);
|
|
143
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
144
|
+
for (let parent = path.dirname(destination); parent !== staging; parent = path.dirname(parent)) directories.add(parent);
|
|
145
|
+
const handle = await open(destination, "wx", file.mode);
|
|
146
|
+
try {
|
|
147
|
+
await handle.writeFile(file.bytes);
|
|
148
|
+
await handle.chmod(file.mode);
|
|
149
|
+
await handle.sync();
|
|
150
|
+
} finally { await handle.close(); }
|
|
151
|
+
}
|
|
152
|
+
for (const directory of [...directories].sort((a, b) => b.length - a.length)) {
|
|
153
|
+
const handle = await open(directory, "r");
|
|
154
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await rename(staging, target);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (!["EEXIST", "ENOTEMPTY"].includes(error.code) || !await existingMatches(target, files)) throw error;
|
|
160
|
+
}
|
|
161
|
+
const handle = await open(generations, "r");
|
|
162
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
163
|
+
return result;
|
|
164
|
+
} finally {
|
|
165
|
+
await rm(staging, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|