agents-can-communicate 0.5.3 → 0.5.5
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 +8 -5
- package/docs/ARCHITECTURE.md +6 -1
- package/docs/CLI.md +7 -4
- package/docs/CONFIGURATION.md +7 -3
- package/docs/GETTING_STARTED.md +12 -3
- package/docs/TROUBLESHOOTING.md +25 -9
- package/docs/UPGRADING.md +39 -10
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +5 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +37 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +135 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +10 -5
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +15 -71
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +152 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation-files.mjs +25 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +1 -22
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +2 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +6 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +23 -1
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +22 -5
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +6 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +11 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -2
- package/node_modules/@agents-can-communicate/installer/src/service-setup.mjs +30 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +1 -1
|
@@ -6,12 +6,12 @@ import { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnersh
|
|
|
6
6
|
from "@agents-can-communicate/installer";
|
|
7
7
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
8
8
|
|
|
9
|
-
import { describeNative, nativeRemediation, nativeState,
|
|
9
|
+
import { describeDeliveryDecision, describeNative, nativeRemediation, nativeState,
|
|
10
|
+
updateNativeRuntime } from "./native-delivery-status.mjs";
|
|
10
11
|
export { describeNative } from "./native-delivery-status.mjs";
|
|
11
|
-
|
|
12
12
|
import { nativeSessionLines, updateNativeSessions } from "./native-session-diagnostics.mjs";
|
|
13
|
-
|
|
14
13
|
import { ALL_ADAPTERS, clientContext, probeTimeout } from "./install-command.mjs";
|
|
14
|
+
import { decisionOf } from "./install-delivery-consent.mjs";
|
|
15
15
|
import { describePresence } from "./main.mjs";
|
|
16
16
|
import { platformPaths } from "./platform-paths.mjs";
|
|
17
17
|
import { noticeUpdate } from "./update-check.mjs";
|
|
@@ -173,7 +173,9 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
|
|
|
173
173
|
// is the acc that copied the skills and manifests into it. They diverge after
|
|
174
174
|
// an npm upgrade with no `acc install`, and reporting both is what makes the
|
|
175
175
|
// divergence legible rather than hidden behind a single reassuring number.
|
|
176
|
-
return { ...entry, stale, wired, bundleVersion,
|
|
176
|
+
return { ...entry, stale, wired, bundleVersion,
|
|
177
|
+
deliveryDecision: installed === undefined ? null : decisionOf(installed),
|
|
178
|
+
owned: { modified: owned.modified,
|
|
177
179
|
missing: owned.missing, intact: owned.intact.length },
|
|
178
180
|
nativeDelivery: nativeState(entry.nativeDelivery, livePolicyOf(installed), {
|
|
179
181
|
contract: adapters.find(adapter => adapter.id === entry.adapterId)?.nativeDelivery,
|
|
@@ -241,6 +243,7 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
241
243
|
registry: ALL_ADAPTERS() });
|
|
242
244
|
for (const adapter of adapters) {
|
|
243
245
|
adapter.remediation.push(...nativeRemediation(adapter));
|
|
246
|
+
adapter.remediation = [...new Set(adapter.remediation)];
|
|
244
247
|
}
|
|
245
248
|
|
|
246
249
|
const data = {
|
|
@@ -276,9 +279,11 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
276
279
|
...adapters.filter(adapter => adapter.present)
|
|
277
280
|
.map(adapter => ` ${adapter.displayName} live delivery: `
|
|
278
281
|
+ `${describeNative(adapter.nativeDelivery, { clientVersion: adapter.version })}; `
|
|
282
|
+
+ `${adapter.nativeDelivery.policy === "off" && adapter.deliveryDecision !== null
|
|
283
|
+
? `decision: ${describeDeliveryDecision(adapter.deliveryDecision)}; ` : ""}`
|
|
279
284
|
+ `fallback: ${describeDeliveryFallback(adapter)}`),
|
|
280
285
|
...nativeSessionLines(adapters),
|
|
281
|
-
...adapters.filter(adapter => adapter.present && adapter.outgoingDelivery)
|
|
286
|
+
...adapters.filter(adapter => adapter.present && adapter.outgoingDelivery?.state === "configured")
|
|
282
287
|
.map(adapter => ` ${adapter.displayName} ${adapter.outgoingDelivery.diagnostic}`),
|
|
283
288
|
...(manager === null ? [] : [` automatic updates ${manager.auto ? "on" : "off"}; ACC ${manager.active.version}`
|
|
284
289
|
+ (manager.pin ? `; pinned to ${manager.pin}` : ""), ...(update.notice ? [` ${update.notice}`] : [])]),
|
|
@@ -76,6 +76,9 @@ const CHOICES = Object.freeze({
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
const NOTES = Object.freeze({
|
|
79
|
+
install: ["Without --delivery, one default-No question covers all selected clients that need a decision.",
|
|
80
|
+
"--delivery actionable|all gives explicit complete-setup consent; --delivery off disables incoming automatic requests.",
|
|
81
|
+
"A dry run does not prompt, write configuration, or start a service. Service readiness does not establish a session binding."],
|
|
79
82
|
attach: ["Native hooks already manage their own session. Attach only for a manual CLI session.",
|
|
80
83
|
"Keep the returned session and generation together for subsequent calls.",
|
|
81
84
|
"--cadence is a positive interval in milliseconds."],
|
|
@@ -6,13 +6,16 @@ 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 {
|
|
9
|
+
import { applyPlan, detectInstallation,
|
|
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
15
|
import { installManaged, uninstallManaged } from "./managed-runtime/install.mjs";
|
|
16
|
+
import { decideDelivery } from "./install-delivery-consent.mjs";
|
|
17
|
+
|
|
18
|
+
export { decideDelivery } from "./install-delivery-consent.mjs";
|
|
16
19
|
|
|
17
20
|
// Kept cohesive above 300 lines because this is the install command boundary:
|
|
18
21
|
// detection, consent, planning, application, and reporting share one context
|
|
@@ -180,10 +183,11 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
180
183
|
* what a malformed `~/.claude/settings.json` produced: the adapter refused,
|
|
181
184
|
* correctly, and the script that ran the installer was told it had worked.
|
|
182
185
|
*/
|
|
183
|
-
export function failureOf({ action, acted, failed = [] }) {
|
|
186
|
+
export function failureOf({ action, acted, failed = [], operations = [], skipped = [], home }) {
|
|
184
187
|
if (failed.length === 0) return null;
|
|
185
188
|
return new AccError(EXIT.DATA,
|
|
186
|
-
describeOutcome({ action, acted, failed
|
|
189
|
+
[describeOutcome({ action, acted, failed, operations, skipped, home }),
|
|
190
|
+
...reloadAdvice(operations)].join("\n"), { failed });
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
/**
|
|
@@ -202,70 +206,6 @@ export function actedOn(result) {
|
|
|
202
206
|
+ (operation.changes?.length ?? 0) > 0)).length;
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
206
|
-
&& runtime.isInteractive() === true;
|
|
207
|
-
|
|
208
|
-
// One default-No question per supported client. Disclose token use and any
|
|
209
|
-
// repeated channel prompt; declining only promises a verified fallback.
|
|
210
|
-
function questionFor(entry) {
|
|
211
|
-
const bootstrap = entry.nativeDelivery.activationPlan?.mechanisms
|
|
212
|
-
.find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
|
|
213
|
-
const warns = (bootstrap?.prefixArgs ?? []).some(argument => argument.startsWith("--"));
|
|
214
|
-
return [
|
|
215
|
-
`Let ${entry.displayName} answer peer requests while idle? (experimental)`,
|
|
216
|
-
" Yes: automatic turns can spend tokens without waiting for you.",
|
|
217
|
-
...(entry.outgoingDelivery?.setup ? [` ${entry.outgoingDelivery.setup}`] : []),
|
|
218
|
-
...(warns ? [" Allow development channels when prompted; check the client's startup notice."] : []),
|
|
219
|
-
...(entry.nativeDelivery.state !== "eligible"
|
|
220
|
-
? [" Save consent now; delivery waits for an available client service."] : []),
|
|
221
|
-
entry.capabilities?.delivery?.nextTurn === true
|
|
222
|
-
? " No: use next-turn hooks (when enabled) or acc inbox."
|
|
223
|
-
: " No: read messages with acc inbox; automatic next-turn delivery is unavailable.",
|
|
224
|
-
].join("\n");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Which live policy each selected client gets.
|
|
229
|
-
*
|
|
230
|
-
* An explicit --delivery applies uniformly and never prompts. Otherwise a
|
|
231
|
-
* recorded opt-in is kept as it was consented to, a fresh or never-activated
|
|
232
|
-
* client is off unless a person answers yes here, and a dry run or a
|
|
233
|
-
* non-interactive stdin/stdout makes no interactive choice at all.
|
|
234
|
-
*/
|
|
235
|
-
export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
|
|
236
|
-
const explicit = options.delivery;
|
|
237
|
-
if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
|
|
238
|
-
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
239
|
-
}
|
|
240
|
-
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
241
|
-
const deliveryByAdapter = {};
|
|
242
|
-
const asked = [];
|
|
243
|
-
let withheld = 0;
|
|
244
|
-
for (const entry of detected) {
|
|
245
|
-
const eligible = entry.nativeDelivery?.state === "eligible"
|
|
246
|
-
|| entry.nativeDelivery?.consentAvailable === true;
|
|
247
|
-
const previous = livePolicyOf(recordedById.get(entry.adapterId));
|
|
248
|
-
if (explicit !== undefined) {
|
|
249
|
-
deliveryByAdapter[entry.adapterId] = explicit;
|
|
250
|
-
} else if (previous !== "off") {
|
|
251
|
-
deliveryByAdapter[entry.adapterId] = previous;
|
|
252
|
-
} else if (!eligible || dryRun || !isInteractive(runtime)) {
|
|
253
|
-
deliveryByAdapter[entry.adapterId] = "off";
|
|
254
|
-
if (eligible) withheld += 1;
|
|
255
|
-
} else {
|
|
256
|
-
const yes = await runtime.confirm(questionFor(entry),
|
|
257
|
-
{ input: runtime.input, output: runtime.output }) === true;
|
|
258
|
-
deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
|
|
259
|
-
asked.push(entry.adapterId);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const notes = explicit === undefined && dryRun && withheld > 0
|
|
263
|
-
? ["interactive choices were not made: this preview keeps native delivery off for "
|
|
264
|
-
+ `${withheld} eligible client(s) without a recorded opt-in`]
|
|
265
|
-
: [];
|
|
266
|
-
return { deliveryByAdapter, asked, notes };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
209
|
// Said once, after the first PATH block is written: a running shell and a
|
|
270
210
|
// running client know nothing about it.
|
|
271
211
|
function reloadAdvice(operations) {
|
|
@@ -315,7 +255,9 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
315
255
|
: { deliveryByAdapter: {}, asked: [], notes: [] };
|
|
316
256
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
317
257
|
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
318
|
-
deliveryByAdapter: decided.deliveryByAdapter
|
|
258
|
+
deliveryByAdapter: decided.deliveryByAdapter,
|
|
259
|
+
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
|
|
260
|
+
allowServiceSetup: action === "install" });
|
|
319
261
|
const apply = (paths = {}) => applyPlan({ plan, adapters, context: { ...context, ...paths },
|
|
320
262
|
dataHome, dryRun, accVersion, activation: { bootstrap: paths.bootstrap } });
|
|
321
263
|
const result = action === "install" && !dryRun && runtime.packageRoot
|
|
@@ -328,16 +270,18 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
328
270
|
|
|
329
271
|
const acted = actedOn(result);
|
|
330
272
|
if (dryRun) {
|
|
331
|
-
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter
|
|
273
|
+
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
|
|
274
|
+
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter },
|
|
332
275
|
text: [`would ${action}:`, ...plan.operations.flatMap(operation => operation.summary),
|
|
333
276
|
...plan.skipped.map(entry => `skip ${entry.adapterId}: ${entry.reason}`),
|
|
334
277
|
...decided.notes].join("\n") };
|
|
335
278
|
}
|
|
336
279
|
|
|
337
280
|
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
|
|
338
|
-
asked: decided.asked },
|
|
281
|
+
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter, asked: decided.asked },
|
|
339
282
|
text: [describeOutcome({ action, acted, failed: result.failed,
|
|
340
283
|
skipped: plan.skipped, operations: result.operations, home }),
|
|
341
284
|
...reloadAdvice(result.operations)].join("\n"),
|
|
342
|
-
error: failureOf({ action, acted, failed: result.failed
|
|
285
|
+
error: failureOf({ action, acted, failed: result.failed, operations: result.operations,
|
|
286
|
+
skipped: plan.skipped, home }) };
|
|
343
287
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { LIVE_POLICIES, livePolicyOf } from "@agents-can-communicate/installer";
|
|
2
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
3
|
+
|
|
4
|
+
const DECISION_SOURCES = new Set([
|
|
5
|
+
"interactive-accepted",
|
|
6
|
+
"interactive-declined",
|
|
7
|
+
"explicit-option",
|
|
8
|
+
"noninteractive-default",
|
|
9
|
+
"unsupported-default",
|
|
10
|
+
"legacy-unknown",
|
|
11
|
+
]);
|
|
12
|
+
const DELIBERATE_SOURCES = new Set([
|
|
13
|
+
"interactive-accepted", "interactive-declined", "explicit-option",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const legacyDecision = () => ({ source: "legacy-unknown", completeSetup: false });
|
|
17
|
+
|
|
18
|
+
function validDecision(record) {
|
|
19
|
+
const decision = record?.deliveryDecision;
|
|
20
|
+
if (!(decision !== null && typeof decision === "object"
|
|
21
|
+
&& DECISION_SOURCES.has(decision.source)
|
|
22
|
+
&& typeof decision.completeSetup === "boolean")) return false;
|
|
23
|
+
|
|
24
|
+
const enabled = livePolicyOf(record) !== "off";
|
|
25
|
+
if (decision.source === "interactive-accepted") return decision.completeSetup && enabled;
|
|
26
|
+
if (decision.source === "interactive-declined") return !decision.completeSetup && !enabled;
|
|
27
|
+
if (decision.source === "explicit-option") return decision.completeSetup === enabled;
|
|
28
|
+
if (["noninteractive-default", "unsupported-default"].includes(decision.source)) {
|
|
29
|
+
return !decision.completeSetup && !enabled;
|
|
30
|
+
}
|
|
31
|
+
return decision.completeSetup === false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function decisionOf(record) {
|
|
35
|
+
return validDecision(record)
|
|
36
|
+
? { source: record.deliveryDecision.source,
|
|
37
|
+
completeSetup: record.deliveryDecision.completeSetup }
|
|
38
|
+
: legacyDecision();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isEligible = entry => entry.nativeDelivery?.state === "eligible"
|
|
42
|
+
|| entry.nativeDelivery?.consentAvailable === true;
|
|
43
|
+
|
|
44
|
+
const needsExpandedSetup = entry => ["needed", "blocked"]
|
|
45
|
+
.includes(entry.nativeServiceSetup?.state);
|
|
46
|
+
|
|
47
|
+
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
48
|
+
&& runtime.isInteractive() === true;
|
|
49
|
+
|
|
50
|
+
function questionFor(entries) {
|
|
51
|
+
const names = entries.map(entry => entry.displayName ?? entry.adapterId).join(", ");
|
|
52
|
+
const setup = [...new Set(entries.map(entry => entry.outgoingDelivery?.setup)
|
|
53
|
+
.filter(value => typeof value === "string"))];
|
|
54
|
+
const channels = entries.some(entry => (entry.nativeDelivery?.activationPlan?.mechanisms ?? [])
|
|
55
|
+
.some(mechanism => mechanism.kind === "shell-bootstrap"
|
|
56
|
+
&& (mechanism.prefixArgs ?? []).some(argument => argument.startsWith("--"))));
|
|
57
|
+
return [
|
|
58
|
+
`Complete automatic peer-request setup for ${names}? (experimental)`,
|
|
59
|
+
" Yes: automatic turns can spend tokens without waiting for you.",
|
|
60
|
+
" Configure the required local permission grants.",
|
|
61
|
+
...(entries.some(entry => entry.adapterId === "codex")
|
|
62
|
+
? [" Start a missing supported Codex service."] : []),
|
|
63
|
+
...setup.map(note => ` ${note}`),
|
|
64
|
+
...(channels ? [" Allow Claude Code development Channels when prompted at startup."] : []),
|
|
65
|
+
" No: keep current delivery policies and use available next-turn hooks or acc inbox.",
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Decide delivery once for all clients whose complete setup has no recorded answer. */
|
|
70
|
+
export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
|
|
71
|
+
const explicit = options.delivery;
|
|
72
|
+
if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
|
|
73
|
+
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
77
|
+
const deliveryByAdapter = {};
|
|
78
|
+
const deliveryDecisionByAdapter = {};
|
|
79
|
+
const candidates = [];
|
|
80
|
+
let withheld = 0;
|
|
81
|
+
|
|
82
|
+
for (const entry of detected) {
|
|
83
|
+
const record = recordedById.get(entry.adapterId);
|
|
84
|
+
const previous = livePolicyOf(record);
|
|
85
|
+
const eligible = isEligible(entry);
|
|
86
|
+
|
|
87
|
+
if (explicit !== undefined) {
|
|
88
|
+
deliveryByAdapter[entry.adapterId] = explicit;
|
|
89
|
+
deliveryDecisionByAdapter[entry.adapterId] = {
|
|
90
|
+
source: "explicit-option", completeSetup: explicit !== "off",
|
|
91
|
+
};
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
deliveryByAdapter[entry.adapterId] = previous;
|
|
96
|
+
if (record !== undefined) {
|
|
97
|
+
deliveryDecisionByAdapter[entry.adapterId] = decisionOf(record);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!eligible) {
|
|
101
|
+
deliveryDecisionByAdapter[entry.adapterId] ??= {
|
|
102
|
+
source: "unsupported-default", completeSetup: false,
|
|
103
|
+
};
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const alreadyDecided = validDecision(record)
|
|
108
|
+
&& DELIBERATE_SOURCES.has(record.deliveryDecision.source);
|
|
109
|
+
const expandsLegacyOptIn = previous !== "off" && needsExpandedSetup(entry)
|
|
110
|
+
&& !decisionOf(record).completeSetup;
|
|
111
|
+
const freshDecision = previous === "off" && !alreadyDecided;
|
|
112
|
+
if (!freshDecision && !expandsLegacyOptIn) {
|
|
113
|
+
deliveryDecisionByAdapter[entry.adapterId] ??= legacyDecision();
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (dryRun || !isInteractive(runtime)) {
|
|
118
|
+
if (record === undefined) {
|
|
119
|
+
deliveryDecisionByAdapter[entry.adapterId] = {
|
|
120
|
+
source: "noninteractive-default", completeSetup: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (dryRun && freshDecision) withheld += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
candidates.push(entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const asked = candidates.map(entry => entry.adapterId);
|
|
130
|
+
if (candidates.length > 0) {
|
|
131
|
+
const yes = await runtime.confirm(questionFor(candidates),
|
|
132
|
+
{ input: runtime.input, output: runtime.output }) === true;
|
|
133
|
+
for (const entry of candidates) {
|
|
134
|
+
const record = recordedById.get(entry.adapterId);
|
|
135
|
+
const previous = livePolicyOf(record);
|
|
136
|
+
deliveryByAdapter[entry.adapterId] = yes
|
|
137
|
+
? (previous === "off" ? "actionable" : previous)
|
|
138
|
+
: previous;
|
|
139
|
+
deliveryDecisionByAdapter[entry.adapterId] = yes
|
|
140
|
+
? { source: "interactive-accepted", completeSetup: true }
|
|
141
|
+
: previous === "off"
|
|
142
|
+
? { source: "interactive-declined", completeSetup: false }
|
|
143
|
+
: decisionOf(record);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const notes = explicit === undefined && dryRun && withheld > 0
|
|
148
|
+
? ["interactive choices were not made: this preview keeps native delivery off for "
|
|
149
|
+
+ `${withheld} eligible client(s) without a recorded opt-in`]
|
|
150
|
+
: [];
|
|
151
|
+
return { deliveryByAdapter, deliveryDecisionByAdapter, asked, notes };
|
|
152
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export async function collectFile(source, relative, collected) {
|
|
6
|
+
const stat = await lstat(source);
|
|
7
|
+
if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
|
|
8
|
+
if (stat.isDirectory()) {
|
|
9
|
+
for (const entry of (await readdir(source)).sort()) {
|
|
10
|
+
await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
|
|
11
|
+
}
|
|
12
|
+
} else if (stat.isFile()) {
|
|
13
|
+
collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
|
|
14
|
+
} else throw new Error(`unsupported runtime file: ${relative}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** This format also names existing generations; keep it stable across upgrades. */
|
|
18
|
+
export function fingerprint(files) {
|
|
19
|
+
const hash = createHash("sha256");
|
|
20
|
+
for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
|
|
21
|
+
hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
|
|
22
|
+
hash.update(file.bytes);
|
|
23
|
+
}
|
|
24
|
+
return hash.digest("hex");
|
|
25
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm }
|
|
3
2
|
from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
5
|
+
import { collectFile, fingerprint } from "./generation-files.mjs";
|
|
6
6
|
import { attachStagingTemp, holdStagedGeneration, releaseStagingHold } from "./staging.mjs";
|
|
7
7
|
|
|
8
8
|
const NAME = "agents-can-communicate";
|
|
@@ -37,18 +37,6 @@ function declaredPath(value) {
|
|
|
37
37
|
return value.replace(/\/+$/, "");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async function collectFile(source, relative, collected) {
|
|
41
|
-
const stat = await lstat(source);
|
|
42
|
-
if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
|
|
43
|
-
if (stat.isDirectory()) {
|
|
44
|
-
for (const entry of (await readdir(source)).sort()) {
|
|
45
|
-
await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
|
|
46
|
-
}
|
|
47
|
-
} else if (stat.isFile()) {
|
|
48
|
-
collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
|
|
49
|
-
} else throw new Error(`unsupported runtime file: ${relative}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
40
|
async function checkDeclaredComponents(source, relative) {
|
|
53
41
|
let current = source;
|
|
54
42
|
for (const component of relative.split("/")) {
|
|
@@ -84,15 +72,6 @@ async function collectPackage(source, prefix, collected, expectedName, version)
|
|
|
84
72
|
return manifest;
|
|
85
73
|
}
|
|
86
74
|
|
|
87
|
-
function fingerprint(files) {
|
|
88
|
-
const hash = createHash("sha256");
|
|
89
|
-
for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
|
|
90
|
-
hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
|
|
91
|
-
hash.update(file.bytes);
|
|
92
|
-
}
|
|
93
|
-
return hash.digest("hex");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
75
|
async function existingMatches(target, files) {
|
|
97
76
|
try {
|
|
98
77
|
const actual = new Map();
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { ENTRY_KINDS } from "./entry.mjs";
|
|
5
5
|
import { managedDirectory, syncDirectory } from "./state.mjs";
|
|
6
6
|
|
|
7
|
-
const MODULES = ["entry.mjs", "state.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
|
|
7
|
+
const MODULES = ["entry.mjs", "state.mjs", "generation-files.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
|
|
8
8
|
async function durableFile(file, bytes, mode = 0o600) {
|
|
9
9
|
const handle = await open(file, "wx", mode);
|
|
10
10
|
try { await handle.writeFile(bytes); await handle.sync(); }
|
|
@@ -49,11 +49,11 @@ export async function listRuntimeHolds(root, { pidIsAlive = defaultPidIsAlive }
|
|
|
49
49
|
|| typeof lease.token !== "string" || name !== `${lease.token}.json`
|
|
50
50
|
|| typeof lease.kind !== "string" || !lease.kind
|
|
51
51
|
|| !Number.isFinite(Date.parse(lease.createdAt))) throw new Error("invalid runtime lease");
|
|
52
|
-
await validateRuntime(root, lease.runtime);
|
|
52
|
+
const runtime = await validateRuntime(root, lease.runtime);
|
|
53
53
|
if (await confirmedDead(lease.pid, pidIsAlive)) {
|
|
54
54
|
await rm(file, { force: true });
|
|
55
55
|
removed = true;
|
|
56
|
-
} else holds.push(lease);
|
|
56
|
+
} else holds.push({ ...lease, runtime });
|
|
57
57
|
}
|
|
58
58
|
if (removed) await syncDirectory(directory);
|
|
59
59
|
return holds;
|
|
@@ -18,11 +18,16 @@ export async function prepareRefresh({ control, root, env = process.env, callerP
|
|
|
18
18
|
const detected = await detectInstallation({ adapters, context, probeTimeoutMs: probeTimeout(env) });
|
|
19
19
|
const deliveryByAdapter = Object.fromEntries(adapters.map(adapter => [adapter.id,
|
|
20
20
|
livePolicyOf(recorded.find(record => record.adapterId === adapter.id))]));
|
|
21
|
+
const deliveryDecisionByAdapter = Object.fromEntries(adapters.flatMap(adapter => {
|
|
22
|
+
const decision = recorded.find(record => record.adapterId === adapter.id)?.deliveryDecision;
|
|
23
|
+
return decision === undefined ? [] : [[adapter.id, decision]];
|
|
24
|
+
}));
|
|
21
25
|
// Refresh configuration even when the client's service is temporarily absent.
|
|
22
26
|
// The installer preserves consent and existing activation; readiness stays a
|
|
23
27
|
// separate, current probe result rather than an update prerequisite.
|
|
24
28
|
const plan = planInstallation({ adapters, detected, context, recorded,
|
|
25
|
-
accVersion: control.pending.version, requested: control.targets, deliveryByAdapter
|
|
29
|
+
accVersion: control.pending.version, requested: control.targets, deliveryByAdapter,
|
|
30
|
+
deliveryDecisionByAdapter });
|
|
26
31
|
if (plan.skipped.length || plan.operations.length !== wanted.size) {
|
|
27
32
|
throw new Error("candidate did not plan every installed integration");
|
|
28
33
|
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { collectFile, fingerprint } from "./generation-files.mjs";
|
|
5
6
|
|
|
6
7
|
/** Resolve existing ancestors without creating an uninitialized manager. */
|
|
7
8
|
export async function canonicalManagerRoot(root) {
|
|
@@ -74,6 +75,25 @@ export async function writeManagedJson(file, value) {
|
|
|
74
75
|
const nonempty = value => typeof value === "string" && value.length > 0;
|
|
75
76
|
const nullableString = value => value === null || nonempty(value);
|
|
76
77
|
|
|
78
|
+
/** Older updaters dropped the contract while publishing a newer generation.
|
|
79
|
+
* Recover only from that exact, unchanged generation, never from the active
|
|
80
|
+
* version or a loose manifest. Explicit lease declarations remain historical
|
|
81
|
+
* facts. Missing or damaged files leave an unknown hold without failing hooks.
|
|
82
|
+
*/
|
|
83
|
+
async function recoverStoreVersion(generations, runtime) {
|
|
84
|
+
if (path.dirname(runtime.root) !== generations
|
|
85
|
+
|| !/^\d+\.\d+\.\d+-[a-f0-9]{24}$/.test(path.basename(runtime.root))) return null;
|
|
86
|
+
try {
|
|
87
|
+
const files = new Map();
|
|
88
|
+
await collectFile(runtime.root, "", files);
|
|
89
|
+
if (path.basename(runtime.root) !== `${runtime.version}-${fingerprint(files).slice(0, 24)}`) return null;
|
|
90
|
+
const manifest = JSON.parse(files.get("package.json")?.bytes.toString("utf8"));
|
|
91
|
+
return manifest.name === "agents-can-communicate" && manifest.version === runtime.version
|
|
92
|
+
&& Number.isSafeInteger(manifest.accStoreVersion) && manifest.accStoreVersion > 0
|
|
93
|
+
? manifest.accStoreVersion : null;
|
|
94
|
+
} catch { return null; }
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
export async function validateRuntime(root, runtime) {
|
|
78
98
|
if (!runtime || !nonempty(runtime.version) || typeof runtime.root !== "string"
|
|
79
99
|
|| !path.isAbsolute(runtime.root)) throw new Error("invalid control generation");
|
|
@@ -95,7 +115,9 @@ export async function validateRuntime(root, runtime) {
|
|
|
95
115
|
if (await managedDirectory(generations)) {
|
|
96
116
|
if (await realpath(generations) !== generations) throw new Error("invalid control generations directory");
|
|
97
117
|
}
|
|
98
|
-
|
|
118
|
+
const normalized = { version: runtime.version, root: resolved, storeVersion: runtime.storeVersion ?? null };
|
|
119
|
+
normalized.storeVersion ??= await recoverStoreVersion(generations, normalized);
|
|
120
|
+
return normalized;
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
async function validateControl(root, value) {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
2
2
|
|
|
3
|
+
export function describeDeliveryDecision(decision) {
|
|
4
|
+
if (decision?.source === "interactive-declined") return "declined during interactive setup";
|
|
5
|
+
if (decision?.source === "explicit-option") return "disabled by explicit option";
|
|
6
|
+
if (decision?.source === "noninteractive-default") return "defaulted off in a noninteractive run";
|
|
7
|
+
if (decision?.source === "unsupported-default") return "defaulted off because delivery was unsupported";
|
|
8
|
+
return "origin unknown (legacy install)";
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export function describeNative(native, { clientVersion } = {}) {
|
|
4
12
|
const differs = native.runtime === "active" && native.sessionPolicy
|
|
5
13
|
&& native.sessionPolicy !== native.policy;
|
|
@@ -20,24 +28,33 @@ export function nativeRemediation(entry) {
|
|
|
20
28
|
const native = entry.nativeDelivery;
|
|
21
29
|
if (!(entry.present || entry.installed) || native.eligibility === "unsupported") return [];
|
|
22
30
|
const steps = [];
|
|
31
|
+
const service = entry.nativeServiceSetup;
|
|
23
32
|
if (!native.configured && native.runtime !== "active") {
|
|
24
33
|
steps.push(`acc install --adapter ${entry.adapterId} --delivery actionable`
|
|
25
34
|
+ " # opt in to automatic peer requests; may spend tokens");
|
|
26
35
|
} else if (native.configured && native.activation === "missing") {
|
|
27
36
|
steps.push(`acc install --adapter ${entry.adapterId}`
|
|
28
37
|
+ " # complete the missing native launch setup from a supported shell");
|
|
29
|
-
} else if (native.configured && native.runtime !== "active"
|
|
38
|
+
} else if (native.configured && native.runtime !== "active"
|
|
39
|
+
&& !["needed", "blocked"].includes(service?.state)) {
|
|
30
40
|
steps.push(`${entry.displayName}: no verified live channel in this workspace; `
|
|
31
41
|
+ "open a new terminal, start a new client session and check its integration/channel prompts; "
|
|
32
42
|
+ "then run acc doctor here");
|
|
33
43
|
}
|
|
34
44
|
if (native.reasonCode === "native_endpoint_unavailable") {
|
|
35
|
-
steps.push(
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
if (service?.state === "blocked") steps.push(service.diagnostic);
|
|
46
|
+
else if (service?.state === "needed" && native.configured) {
|
|
47
|
+
steps.push(entry.deliveryDecision?.completeSetup === true
|
|
48
|
+
? `acc install --adapter ${entry.adapterId} # prepare the missing supported local service`
|
|
49
|
+
: `acc install --adapter ${entry.adapterId} --delivery actionable`
|
|
50
|
+
+ " # approve complete automatic peer-request setup");
|
|
51
|
+
} else if (service?.state !== "needed" && service?.state !== "ready") {
|
|
52
|
+
steps.push(entry.nativeSetup ?? `${entry.displayName}: check the client's local delivery service setup`);
|
|
53
|
+
}
|
|
54
|
+
} else if (native.reasonCode === "native_session_unavailable" && native.runtime !== "active") {
|
|
38
55
|
steps.push(`${entry.displayName}: open a session connected to the client's local delivery service`);
|
|
39
56
|
}
|
|
40
|
-
return steps;
|
|
57
|
+
return [...new Set(steps)];
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
// One closed native-delivery report per adapter, built only from detection,
|