agents-can-communicate 0.3.0 → 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 +112 -91
- package/SECURITY.md +8 -5
- 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 +62 -24
- package/docs/ARCHITECTURE.md +79 -20
- package/docs/CAPABILITIES.md +52 -30
- package/docs/CLI.md +164 -35
- package/docs/CONCEPTS.md +97 -93
- package/docs/CONFIGURATION.md +66 -45
- package/docs/DESIGN_DECISIONS.md +24 -15
- package/docs/GETTING_STARTED.md +124 -118
- package/docs/GLOSSARY.md +12 -6
- package/docs/HOW_IT_WORKS.md +136 -229
- package/docs/MCP.md +98 -31
- package/docs/PROTOCOL.md +76 -14
- package/docs/RELEASING.md +128 -86
- package/docs/SECURITY_MODEL.md +52 -15
- package/docs/TROUBLESHOOTING.md +88 -18
- package/docs/UPGRADING.md +119 -0
- package/docs/WHY_ACC.md +19 -16
- package/docs/index.md +40 -24
- 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 +112 -9
- 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 +111 -8
- 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 +111 -8
- 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 +111 -8
- 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 +111 -8
- 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 +66 -18
- 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 +48 -24
- 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 +4 -2
- 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
|
@@ -6,12 +6,17 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
|
6
6
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
7
7
|
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
8
8
|
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
9
|
-
import { LIVE_POLICIES, applyPlan,
|
|
9
|
+
import { LIVE_POLICIES, applyPlan, detectInstallation, livePolicyOf,
|
|
10
10
|
loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
|
|
11
11
|
from "@agents-can-communicate/installer";
|
|
12
12
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
13
13
|
|
|
14
14
|
import { platformPaths } from "./platform-paths.mjs";
|
|
15
|
+
import { installManaged, uninstallManaged } from "./managed-runtime/install.mjs";
|
|
16
|
+
|
|
17
|
+
// Kept cohesive above 300 lines because this is the install command boundary:
|
|
18
|
+
// detection, consent, planning, application, and reporting share one context
|
|
19
|
+
// and result contract that would otherwise be duplicated across modules.
|
|
15
20
|
|
|
16
21
|
// Where each client keeps its own configuration. All of it derives from one
|
|
17
22
|
// home, so a test - or an operator with a second account - can point the whole
|
|
@@ -19,11 +24,13 @@ import { platformPaths } from "./platform-paths.mjs";
|
|
|
19
24
|
// Each client keeps its own directory under the user's home, and an adapter
|
|
20
25
|
// pointed at the home itself writes beside them rather than inside them. That
|
|
21
26
|
// install reports success and the client never reads a byte of it.
|
|
22
|
-
export const clientContext = (home, stateRoot, { shell = null, env = {} } = {}) => ({
|
|
27
|
+
export const clientContext = (home, stateRoot, { shell = null, env = {}, dataHome } = {}) => ({
|
|
23
28
|
home,
|
|
29
|
+
...(dataHome === undefined ? {} : { dataHome }),
|
|
24
30
|
configDir: path.join(home, ".claude"),
|
|
25
31
|
agentsHome: home,
|
|
26
|
-
codexHome:
|
|
32
|
+
codexHome: typeof env.CODEX_HOME === "string" && env.CODEX_HOME !== ""
|
|
33
|
+
? env.CODEX_HOME : path.join(home, ".codex"),
|
|
27
34
|
kimiHome: path.join(home, ".kimi-code"),
|
|
28
35
|
grokHome: path.join(home, ".grok"),
|
|
29
36
|
// The user's login shell and PATH, for the optional native shell bootstrap:
|
|
@@ -142,6 +149,9 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
142
149
|
...operations.filter(operation => operation.applied
|
|
143
150
|
&& typeof operation.deliveryDiagnostic === "string")
|
|
144
151
|
.map(operation => ` ${operation.deliveryDiagnostic}`),
|
|
152
|
+
...operations.filter(operation => operation.applied)
|
|
153
|
+
.flatMap(operation => (operation.needsAction ?? [])
|
|
154
|
+
.map(step => ` ${operation.adapterId}: ${step}`)),
|
|
145
155
|
...skipped.map(entry => ` skip ${entry.adapterId}: ${entry.reason}`),
|
|
146
156
|
...failed.map(entry => ` ${entry.adapterId}: ${entry.error}`),
|
|
147
157
|
// Said once, where it is needed: the reader has just been shown a list of
|
|
@@ -192,18 +202,43 @@ export function actedOn(result) {
|
|
|
192
202
|
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
193
203
|
&& runtime.isInteractive() === true;
|
|
194
204
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
/**
|
|
206
|
+
* One default-No question per eligible client, asked only after detection has
|
|
207
|
+
* finished and only when a person is at both ends of the terminal.
|
|
208
|
+
*
|
|
209
|
+
* Written for someone who has never heard of this project. It used to open with
|
|
210
|
+
* "Enable native live delivery" over a list of internal artefact names, which
|
|
211
|
+
* told a first-time reader neither what they would gain nor what it would cost -
|
|
212
|
+
* and left out the part that matters most: saying yes makes their own `claude`
|
|
213
|
+
* start with the vendor's `--dangerously-load-development-channels` flag. A
|
|
214
|
+
* consent prompt that hides the loudest consequence is not consent.
|
|
215
|
+
*
|
|
216
|
+
* So the order is the reader's, not the installer's: what changes for them,
|
|
217
|
+
* what it costs, what it touches, and that "no" costs them nothing.
|
|
218
|
+
*/
|
|
219
|
+
function questionFor(entry, context, { onlyEligible = false } = {}) {
|
|
220
|
+
const bootstrap = entry.nativeDelivery.activationPlan.mechanisms
|
|
221
|
+
.find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
|
|
222
|
+
// The presence of a flag is what makes this worth asking about at all: it is
|
|
223
|
+
// why the client will warn at every start from now on. Which flag it is, and
|
|
224
|
+
// that a shim adds it, are answers to a question nobody is asking here - the
|
|
225
|
+
// shim `exec`s and is gone, so nothing runs "through" acc afterwards either.
|
|
226
|
+
const flags = (bootstrap?.prefixArgs ?? []).filter(argument => argument.startsWith("--"));
|
|
227
|
+
void context;
|
|
228
|
+
// Why the default is No, and why only one client is being asked about, are
|
|
229
|
+
// the two questions the bare prompt provokes and never answered: a `[y/N]`
|
|
230
|
+
// reads as "not recommended" on its own, and a single question among four
|
|
231
|
+
// installed clients reads as an omission. Both have the same short answer.
|
|
232
|
+
const aside = ["experimental", ...(onlyEligible ? [`${entry.displayName} only`] : [])]
|
|
233
|
+
.join("; ");
|
|
234
|
+
return [
|
|
235
|
+
`Let idle agents answer each other while you are away? (${aside})`,
|
|
236
|
+
...(flags.length === 0 || bootstrap === null
|
|
237
|
+
? [" Yes: they reply without waiting for you."]
|
|
238
|
+
: [` Yes: they reply without waiting for you, and ${entry.displayName} asks you`,
|
|
239
|
+
" to allow development channels every time it starts."]),
|
|
240
|
+
" No: messages still arrive, at the session's next turn.",
|
|
241
|
+
].join("\n");
|
|
207
242
|
}
|
|
208
243
|
|
|
209
244
|
/**
|
|
@@ -220,6 +255,11 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
|
|
|
220
255
|
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
221
256
|
}
|
|
222
257
|
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
258
|
+
// Whether this client is the only one that could be asked at all. When it is,
|
|
259
|
+
// the question says so, because otherwise being asked about one of four
|
|
260
|
+
// installed clients looks like the other three were forgotten.
|
|
261
|
+
const onlyEligible = detected
|
|
262
|
+
.filter(candidate => candidate.nativeDelivery?.state === "eligible").length === 1;
|
|
223
263
|
const deliveryByAdapter = {};
|
|
224
264
|
const asked = [];
|
|
225
265
|
let withheld = 0;
|
|
@@ -234,7 +274,7 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
|
|
|
234
274
|
deliveryByAdapter[entry.adapterId] = "off";
|
|
235
275
|
if (eligible) withheld += 1;
|
|
236
276
|
} else {
|
|
237
|
-
const yes = await runtime.confirm(questionFor(entry, context),
|
|
277
|
+
const yes = await runtime.confirm(questionFor(entry, context, { onlyEligible }),
|
|
238
278
|
{ input: runtime.input, output: runtime.output }) === true;
|
|
239
279
|
deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
|
|
240
280
|
asked.push(entry.adapterId);
|
|
@@ -263,7 +303,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
263
303
|
const { data: dataHome } = platformPaths({ platform: runtime.platform,
|
|
264
304
|
env: runtime.env ?? {} });
|
|
265
305
|
const context = clientContext(home, path.join(dataHome, "acc"),
|
|
266
|
-
{ shell: shellOf(runtime.env), env: runtime.env ?? {} });
|
|
306
|
+
{ shell: shellOf(runtime.env), env: runtime.env ?? {}, dataHome });
|
|
267
307
|
|
|
268
308
|
const detected = await detectInstallation({ adapters, context,
|
|
269
309
|
probeTimeoutMs: probeTimeout(runtime.env) });
|
|
@@ -297,7 +337,15 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
297
337
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
298
338
|
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
299
339
|
deliveryByAdapter: decided.deliveryByAdapter });
|
|
300
|
-
const
|
|
340
|
+
const apply = (paths = {}) => applyPlan({ plan, adapters, context: { ...context, ...paths },
|
|
341
|
+
dataHome, dryRun, accVersion, activation: { bootstrap: paths.bootstrap } });
|
|
342
|
+
const result = action === "install" && !dryRun && runtime.packageRoot
|
|
343
|
+
? await installManaged({ packageRoot: runtime.packageRoot,
|
|
344
|
+
managerRoot: path.join(dataHome, "acc", "runtime"), dataHome, home, cwd: options.cwd ?? runtime.cwd,
|
|
345
|
+
targets: plan.operations.map(operation => operation.adapterId), env: runtime.env, apply })
|
|
346
|
+
: action === "uninstall" && !dryRun && runtime.managerRoot
|
|
347
|
+
? await uninstallManaged({ managerRoot: runtime.managerRoot, apply })
|
|
348
|
+
: await apply();
|
|
301
349
|
|
|
302
350
|
const acted = actedOn(result);
|
|
303
351
|
if (dryRun) {
|
|
@@ -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,63 @@
|
|
|
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
|
+
pending: options.pin !== undefined && current.pending?.version !== pin ? null : current.pending,
|
|
36
|
+
notice: null });
|
|
37
|
+
});
|
|
38
|
+
await scheduleWorker(root, control, { env });
|
|
39
|
+
return { data: { auto: control.auto, pin: control.pin, running: control.active.version },
|
|
40
|
+
text: `Automatic updates ${control.auto ? "on" : "off"}. ${control.pin ? `Pinned to ${control.pin}.` : "Following stable releases."}` };
|
|
41
|
+
}
|
|
42
|
+
const result = options.check
|
|
43
|
+
? await performUpdate(root, { check: true, env })
|
|
44
|
+
: await runWorker(root, { force: true, env, ignorePid: process.pid });
|
|
45
|
+
const text = result.activated ? `Updated ACC to ${result.version}; integrations refreshed.`
|
|
46
|
+
: result.notice && result.reason === "processes_active" ? result.notice
|
|
47
|
+
: result.reason === "refresh_failed" ? [result.notice,
|
|
48
|
+
...(result.installation?.failed ?? []).map(failure =>
|
|
49
|
+
`${failure.adapterId}: ${failure.error}${failure.paths?.length ? ` (${failure.paths.join(", ")})` : ""}`),
|
|
50
|
+
...(!result.installation?.failed?.length && result.error ? [result.error] : [])].join("\n")
|
|
51
|
+
: result.checked ? result.newer ? `ACC ${result.latest} is available; run acc update.`
|
|
52
|
+
: `ACC ${result.running} is current${result.pin ? ` for pin ${result.pin}` : ""}.`
|
|
53
|
+
: result.reason === "network_disabled" ? "Update networking is off (ACC_NO_UPDATE_CHECK)."
|
|
54
|
+
: result.notice ?? "Update is pending.";
|
|
55
|
+
return { data: result, text,
|
|
56
|
+
...(result.reason === "refresh_failed" ? { error: new AccError(EXIT.DATA, text) } : {}) };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (/manager lock held/.test(error.message)) {
|
|
59
|
+
return { data: { inProgress: true }, text: "An update is already in progress; ACC will activate it when running clients exit." };
|
|
60
|
+
}
|
|
61
|
+
throw error instanceof AccError ? error : new AccError(EXIT.DATA, error.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -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
|
+
}
|