agents-can-communicate 0.5.2 → 0.5.4
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/CLI.md +19 -5
- package/docs/CONCEPTS.md +7 -0
- package/docs/CONFIGURATION.md +7 -3
- package/docs/GETTING_STARTED.md +19 -3
- package/docs/MCP.md +5 -1
- package/docs/PROTOCOL.md +17 -3
- package/docs/TROUBLESHOOTING.md +7 -7
- package/docs/UPGRADING.md +7 -4
- 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 +57 -10
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +57 -10
- 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/extension/skills/acc/SKILL.md +57 -10
- 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-grok/plugin/skills/acc/SKILL.md +57 -10
- 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 +57 -10
- 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 +10 -3
- 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/refresh.mjs +6 -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/core/src/inbox.mjs +8 -8
- 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/mcp-server/src/tools.mjs +8 -5
- 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}`] : [])]),
|
|
@@ -34,7 +34,7 @@ const SUMMARY = Object.freeze({
|
|
|
34
34
|
work: "publish what this session is doing, or --clear when it has stopped",
|
|
35
35
|
claim: "reserve a resource; exit 5 when someone else already holds it",
|
|
36
36
|
release: "give a claim back",
|
|
37
|
-
ack: "
|
|
37
|
+
ack: "acknowledge receipt without accepting work or writing an answer",
|
|
38
38
|
message: "send a typed message to named participants",
|
|
39
39
|
inbox: "list pending message headers; --message reads one complete message",
|
|
40
40
|
reply: "reply to one message and acknowledge it in the same operation",
|
|
@@ -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."],
|
|
@@ -98,8 +101,12 @@ const NOTES = Object.freeze({
|
|
|
98
101
|
"With history, --type filters before paging; --cursor takes the complete message ID in nextCursor.",
|
|
99
102
|
"history --type decision --current lists terminal decisions and withdrawals, including conflicts. Current means no recorded successor, not consensus.",
|
|
100
103
|
"history --message reads one complete historical message without changing receipts; do not combine with cursor, limit, or type."],
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
ack: ["An unanswered question/request requires reply; ack refuses without changing its receipt.",
|
|
105
|
+
"Acknowledgement does not prevent a later reply."],
|
|
106
|
+
reply: ["Returns the outgoing message/delivery plus the original acknowledged receipt.",
|
|
107
|
+
"May follow ack or an earlier answer. Reuse a client-message-id only for the same send retry."],
|
|
108
|
+
finish: ["Default status: partial. Records a handoff, releases claims, and closes this ACC session.",
|
|
109
|
+
"Status describes the original goal; neither partial nor an acknowledged receipt means the peer accepted work."],
|
|
103
110
|
config: ["init writes optional workspace configuration; validate only checks it.",
|
|
104
111
|
"Use acc config <subcommand> --help for its options."],
|
|
105
112
|
update: ["Run acc update to install the latest release and refresh client integrations.",
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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,
|
|
@@ -110,6 +110,12 @@ export function createInboxService(ports, sessions) {
|
|
|
110
110
|
const now = clock.now();
|
|
111
111
|
return store.transaction(async tx => {
|
|
112
112
|
await requireOpen(input, "acknowledge a message", tx);
|
|
113
|
+
const { message, receipt } = requireOwnedReceipt(tx, session, input.messageId);
|
|
114
|
+
if (message.obligation === "reply" && receipt.state !== "acknowledged") {
|
|
115
|
+
throw new AccError(EXIT.CONFLICT,
|
|
116
|
+
"this message requires a reply; use reply to answer, clarify, or decline",
|
|
117
|
+
{ messageId: input.messageId, obligation: message.obligation });
|
|
118
|
+
}
|
|
113
119
|
return advanceOwned(tx, session, input.messageId, "acknowledged", now).receipt;
|
|
114
120
|
}, { kinds: ["session", "message", "receipt"] });
|
|
115
121
|
}
|
|
@@ -120,14 +126,8 @@ export function createInboxService(ports, sessions) {
|
|
|
120
126
|
const replyId = ids.next("message");
|
|
121
127
|
return store.transaction(tx => {
|
|
122
128
|
const original = requireOwnedReceipt(tx, session, input.messageId);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
&& message.fromParticipantId === session.participantId
|
|
126
|
-
&& message.clientMessageId === input.clientMessageId).at(0);
|
|
127
|
-
if (original.receipt.state === "acknowledged" && matchingReply === undefined) {
|
|
128
|
-
throw new AccError(EXIT.CONFLICT, "that message is already resolved",
|
|
129
|
-
{ messageId: input.messageId });
|
|
130
|
-
}
|
|
129
|
+
// A receipt settles delivery, not the conversation. Distinct replies may
|
|
130
|
+
// follow an ack or an earlier answer; recording still enforces retry keys.
|
|
131
131
|
const recorded = recordMessageInTransaction({ tx, session, now, messageId: replyId, ids,
|
|
132
132
|
action: "reply to a message", input: {
|
|
133
133
|
clientMessageId: input.clientMessageId,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyServiceSetup } from "./service-setup.mjs";
|
|
1
2
|
import { applyNativeActivation, deactivateNative } from "./native-activation.mjs";
|
|
2
3
|
import { finalizeRemoval, missingArtifactParents, recordInstall,
|
|
3
4
|
removeEmptyOwnedDirectories, removeOwnedArtifacts } from "./ownership.mjs";
|
|
@@ -68,7 +69,8 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
68
69
|
await recordInstall({ dataHome, adapterId: adapter.id,
|
|
69
70
|
version: operation.clientVersion ?? null, accVersion,
|
|
70
71
|
artifacts: operation.artifacts, createdDirectories,
|
|
71
|
-
deliveryPolicy: operation.livePolicy,
|
|
72
|
+
deliveryPolicy: operation.livePolicy, deliveryDecision: operation.deliveryDecision,
|
|
73
|
+
nativeActivation: native });
|
|
72
74
|
results.operations.push({ ...operation, applied: true, appendedRcBlock,
|
|
73
75
|
needsAction: outcome.needsAction ?? [],
|
|
74
76
|
changes: outcome.changes ?? [], diagnostics: [
|
|
@@ -77,6 +79,8 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
77
79
|
...(outcome.diagnostics ?? []),
|
|
78
80
|
...notes,
|
|
79
81
|
] });
|
|
82
|
+
await applyServiceSetup({ adapter, context: installContext,
|
|
83
|
+
operation: results.operations.at(-1), results });
|
|
80
84
|
} else {
|
|
81
85
|
// Config ownership can veto removal. Ask before teardown or recorded
|
|
82
86
|
// artifact deletion; an adapter-local check would run after those writes.
|
|
@@ -131,7 +135,7 @@ function describeTeardown(report) {
|
|
|
131
135
|
for (const service of report.services) {
|
|
132
136
|
lines.push(service.outcome === "stopped" ? `stopped the ${service.serviceId} service`
|
|
133
137
|
: `retained the ${service.serviceId} service (${service.outcome === "retained_pre_existing"
|
|
134
|
-
? "it
|
|
138
|
+
? "it was reused at activation" : "no vendor teardown exists"})`);
|
|
135
139
|
}
|
|
136
140
|
return lines;
|
|
137
141
|
}
|
|
@@ -145,6 +145,17 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
145
145
|
: unsupported(adapter.nativeDelivery === undefined
|
|
146
146
|
? "native_delivery_unsupported" : "version_unavailable");
|
|
147
147
|
|
|
148
|
+
if (entry.present && typeof adapter.inspectNativeServiceSetup === "function") {
|
|
149
|
+
try {
|
|
150
|
+
entry.nativeServiceSetup = await withTimeout(Promise.resolve().then(() =>
|
|
151
|
+
adapter.inspectNativeServiceSetup({ ...context, platform, clientVersion: entry.version })),
|
|
152
|
+
probeTimeoutMs, `${adapter.id} service setup probe`);
|
|
153
|
+
} catch {
|
|
154
|
+
entry.nativeServiceSetup = { state: "blocked", reasonCode: "service_inspection_failed",
|
|
155
|
+
diagnostic: `${adapter.displayName} service preparation could not be inspected; check the vendor service and retry install` };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
try {
|
|
149
160
|
const detected = await adapter.detect({ ...context, clientVersion: entry.version,
|
|
150
161
|
platform, nativeDelivery: entry.nativeDelivery });
|
|
@@ -110,7 +110,8 @@ async function saveOwnership({ dataHome, record }) {
|
|
|
110
110
|
* runtime, and leaves the bundle inside the client exactly where it was.
|
|
111
111
|
*/
|
|
112
112
|
export async function recordInstall({ dataHome, adapterId, version, accVersion = null,
|
|
113
|
-
artifacts, createdDirectories = [], deliveryPolicy,
|
|
113
|
+
artifacts, createdDirectories = [], deliveryPolicy, deliveryDecision,
|
|
114
|
+
nativeActivation = null }) {
|
|
114
115
|
const stamped = await Promise.all(artifacts.map(async artifact => ({
|
|
115
116
|
path: artifact.path,
|
|
116
117
|
kind: artifact.kind ?? "file",
|
|
@@ -120,6 +121,7 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
|
|
|
120
121
|
})));
|
|
121
122
|
const record = await loadOwnership({ dataHome });
|
|
122
123
|
const previous = record.installs.find(install => install.adapterId === adapterId);
|
|
124
|
+
const retainedDeliveryDecision = deliveryDecision ?? previous?.deliveryDecision;
|
|
123
125
|
const directories = [...new Set([
|
|
124
126
|
...(previous?.createdDirectories ?? []), ...createdDirectories,
|
|
125
127
|
])].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length
|
|
@@ -129,6 +131,9 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
|
|
|
129
131
|
{ adapterId, version, accVersion, artifacts: stamped,
|
|
130
132
|
...(directories.length === 0 ? {} : { createdDirectories: directories }),
|
|
131
133
|
...(deliveryPolicy === undefined ? {} : { deliveryPolicy }),
|
|
134
|
+
...(retainedDeliveryDecision === undefined ? {} : {
|
|
135
|
+
deliveryDecision: retainedDeliveryDecision,
|
|
136
|
+
}),
|
|
132
137
|
// Present only for a consented native activation. A record without it
|
|
133
138
|
// - every 0.2 install - reads as live policy off and is never rewritten
|
|
134
139
|
// merely to add the field.
|
|
@@ -14,7 +14,7 @@ import { LIVE_POLICIES, describeActivation, describeDeactivation, planActivation
|
|
|
14
14
|
*/
|
|
15
15
|
export function planInstallation({ adapters, detected, context, action = "install",
|
|
16
16
|
recorded = [], accVersion = null, allowDowngrade = false, requested = [],
|
|
17
|
-
deliveryByAdapter = {} }) {
|
|
17
|
+
deliveryByAdapter = {}, deliveryDecisionByAdapter = {}, allowServiceSetup = false }) {
|
|
18
18
|
if (!["install", "uninstall"].includes(action)) {
|
|
19
19
|
throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
|
|
20
20
|
}
|
|
@@ -104,6 +104,9 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
104
104
|
// and so what will be removed. Asking the adapter instead would describe an
|
|
105
105
|
// install for a machine this one no longer is.
|
|
106
106
|
const delivery = deliveryByAdapter[entry.adapterId] ?? "off";
|
|
107
|
+
const deliveryDecision = deliveryDecisionByAdapter[entry.adapterId]
|
|
108
|
+
?? recordedById.get(entry.adapterId)?.deliveryDecision
|
|
109
|
+
?? { source: "legacy-unknown", completeSetup: false };
|
|
107
110
|
const native = entry.nativeDelivery ?? null;
|
|
108
111
|
const liveDeliverySupported = native?.state === "eligible"
|
|
109
112
|
&& native.activationPlan?.eligible === true;
|
|
@@ -122,8 +125,10 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
122
125
|
? describeInstallDelivery(entry, delivery, effectiveLivePolicy) : null;
|
|
123
126
|
const installContext = { ...context, requestedLivePolicy: delivery,
|
|
124
127
|
livePolicy: configuredLivePolicy, clientVersion: entry.version, platform: entry.platform };
|
|
128
|
+
const nativeServiceSetup = action === "install" && allowServiceSetup && delivery !== "off"
|
|
129
|
+
&& deliveryDecision.completeSetup === true ? entry.nativeServiceSetup : undefined;
|
|
125
130
|
const setupNotes = action === "install" && delivery !== "off"
|
|
126
|
-
? [entry.outgoingDelivery?.setup, entry.nativeSetup].filter(note => typeof note === "string") : [];
|
|
131
|
+
? [entry.outgoingDelivery?.setup, nativeServiceSetup ? null : entry.nativeSetup].filter(note => typeof note === "string") : [];
|
|
127
132
|
// A consented activation that this run keeps, activates, or takes back.
|
|
128
133
|
// Only an explicit off or an uninstall removes one; an absent record never
|
|
129
134
|
// creates one.
|
|
@@ -154,11 +159,13 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
154
159
|
alreadyInstalled: entry.installed === true,
|
|
155
160
|
livePolicy: delivery,
|
|
156
161
|
effectiveLivePolicy,
|
|
162
|
+
...(action === "install" ? { deliveryDecision } : {}),
|
|
157
163
|
...(retainedActivation ? { configuredLivePolicy, retainedNativeActivation: retainedActivation } : {}),
|
|
158
164
|
...(deliveryDiagnostic === null ? {} : { deliveryDiagnostic }),
|
|
159
165
|
...(deliverySummary === null ? {} : { deliverySummary }),
|
|
160
166
|
...(setupNotes.length ? { setupNotes } : {}),
|
|
161
167
|
...(nativeActivation === null ? {} : { nativeActivation }),
|
|
168
|
+
...(nativeServiceSetup ? { nativeServiceSetup } : {}),
|
|
162
169
|
...(deactivation === null ? {} : { deactivation }),
|
|
163
170
|
artifacts,
|
|
164
171
|
// Said in the operator's terms, not in paths: which files ACC creates
|
|
@@ -169,6 +176,7 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
169
176
|
...(deliverySummary === null ? [] : [deliverySummary]),
|
|
170
177
|
...(retainedActivation ? ["keep existing native delivery setup; current readiness is unverified"] : []),
|
|
171
178
|
...setupNotes,
|
|
179
|
+
...(nativeServiceSetup ? [nativeServiceSetup.diagnostic] : []),
|
|
172
180
|
...artifacts.filter(a => a.kind === "tree")
|
|
173
181
|
.map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
|
|
174
182
|
...artifacts.filter(a => a.kind === "merge")
|