agents-can-communicate 0.4.1 → 0.4.3
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 +17 -4
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/CLI.md +23 -3
- package/docs/CONFIGURATION.md +25 -0
- package/docs/GETTING_STARTED.md +17 -5
- package/docs/PROTOCOL.md +4 -0
- package/docs/RELEASING.md +1 -1
- package/docs/SECURITY_MODEL.md +7 -0
- package/docs/TROUBLESHOOTING.md +60 -7
- package/docs/UPGRADING.md +36 -19
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +3 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +2 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +10 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +4 -88
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +36 -15
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +110 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/permission-ownership.mjs +109 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/toml-scan.mjs +99 -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/adapter-sdk/src/channel-directory.mjs +8 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +3 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt-writer.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +65 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +19 -52
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +23 -45
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +3 -1
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +88 -0
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +64 -0
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +43 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +17 -27
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +2 -0
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +36 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +28 -12
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -1
- package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +10 -3
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +12 -2
- 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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
5
|
+
|
|
6
|
+
import { NATIVE_REASON_CODES, TIMESTAMP } from "./native-vocabulary.mjs";
|
|
7
|
+
|
|
8
|
+
// Optional diagnostic metadata beside the hook-owner file, never an event
|
|
9
|
+
// history or injected context. Copy only closed values: vendor errors, prompts,
|
|
10
|
+
// endpoints and extra properties must not survive persistence or doctor reads.
|
|
11
|
+
export function nativeAttemptFrom(value) {
|
|
12
|
+
if (!value || typeof value.at !== "string" || !TIMESTAMP.test(value.at)
|
|
13
|
+
|| !Number.isFinite(Date.parse(value.at))
|
|
14
|
+
|| !["sessionStart", "beforeTurn"].includes(value.event)
|
|
15
|
+
|| !["active", "off", "degraded", "unsupported"].includes(value.state)
|
|
16
|
+
|| !(value.reasonCode === null || NATIVE_REASON_CODES.includes(value.reasonCode))
|
|
17
|
+
|| !["off", "actionable", "all"].includes(value.policy)
|
|
18
|
+
|| !["bootstrap-environment", "installation-record"].includes(value.policySource)
|
|
19
|
+
|| !["enabled", "off", "missing", "invalid", "unavailable"].includes(value.policyStatus)
|
|
20
|
+
|| !["identified", "unknown"].includes(value.clientProcess)) return null;
|
|
21
|
+
return { at: value.at, event: value.event, state: value.state, reasonCode: value.reasonCode,
|
|
22
|
+
policy: value.policy, policySource: value.policySource, policyStatus: value.policyStatus,
|
|
23
|
+
clientProcess: value.clientProcess };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const keyFor = harnessSessionId => createHash("sha256").update(String(harnessSessionId))
|
|
27
|
+
.digest("hex").slice(0, 32);
|
|
28
|
+
const fileFor = (runtimeDir, harnessSessionId) =>
|
|
29
|
+
path.join(runtimeDir, "native-attempts", `${keyFor(harnessSessionId)}.json`);
|
|
30
|
+
export async function loadNativeAttempt({ runtimeDir, harnessSessionId, accSessionId, generation }) {
|
|
31
|
+
try {
|
|
32
|
+
const record = JSON.parse(await readFile(fileFor(runtimeDir, harnessSessionId), "utf8"));
|
|
33
|
+
return record.schemaVersion === 1 && record.accSessionId === accSessionId
|
|
34
|
+
&& record.generation === generation ? nativeAttemptFrom(record.attempt) : null;
|
|
35
|
+
} catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Isolate filesystem requests as well as JS waiting: even a stuck fs request
|
|
39
|
+
// must not keep the vendor's short-lived hook process alive after its output.
|
|
40
|
+
// The unreferenced worker owns its mutex through any late I/O continuation.
|
|
41
|
+
async function writeDiagnostic(operation, input) {
|
|
42
|
+
const deadlineAt = Math.min(input.deadlineAt ?? Infinity, Date.now() + 250);
|
|
43
|
+
if (Date.now() >= deadlineAt) return false;
|
|
44
|
+
let worker;
|
|
45
|
+
let timer;
|
|
46
|
+
try {
|
|
47
|
+
worker = new Worker(new URL("./native-attempt-writer.mjs", import.meta.url), {
|
|
48
|
+
workerData: { operation, input: { runtimeDir: input.runtimeDir,
|
|
49
|
+
harnessSessionId: input.harnessSessionId, accSessionId: input.accSessionId,
|
|
50
|
+
generation: input.generation, nativeAttempt: nativeAttemptFrom(input.nativeAttempt), deadlineAt } },
|
|
51
|
+
});
|
|
52
|
+
const result = new Promise(resolve => {
|
|
53
|
+
worker.once("message", value => resolve(value === true));
|
|
54
|
+
worker.once("error", () => resolve(false));
|
|
55
|
+
worker.once("exit", () => resolve(false));
|
|
56
|
+
timer = setTimeout(() => resolve(false), Math.max(0, deadlineAt - Date.now()));
|
|
57
|
+
});
|
|
58
|
+
worker.unref();
|
|
59
|
+
return await result;
|
|
60
|
+
} catch { return false; }
|
|
61
|
+
finally { clearTimeout(timer); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const storeNativeAttempt = input => writeDiagnostic("store", input);
|
|
65
|
+
export const clearNativeAttempt = input => writeDiagnostic("clear", input);
|
|
@@ -23,6 +23,7 @@ export const NATIVE_REASON_CODES = Object.freeze([
|
|
|
23
23
|
"native_delivery_unsupported", "platform_not_captured", "version_unavailable",
|
|
24
24
|
"prerelease_not_captured", "below_minimum_version", "known_bad_version",
|
|
25
25
|
"feature_probe_failed", "probe_timeout", "probe_version_mismatch", "protocol_mismatch",
|
|
26
|
+
"native_endpoint_unavailable", "native_session_unavailable",
|
|
26
27
|
"handshake_failed", "handshake_timeout", "handshake_version_mismatch",
|
|
27
28
|
"session_generation_stale", "client_process_unknown", "unsupported_shell",
|
|
28
29
|
// The current session's canonical workspace identity could not be verified.
|
|
@@ -2,10 +2,15 @@ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { detectInstallation, livePolicyOf, loadOwnership, shellOf, verifyOwned }
|
|
5
|
+
import { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnership, shellOf, verifyOwned }
|
|
6
6
|
from "@agents-can-communicate/installer";
|
|
7
7
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
8
8
|
|
|
9
|
+
import { describeNative, nativeRemediation, nativeState, updateNativeRuntime } from "./native-delivery-status.mjs";
|
|
10
|
+
export { describeNative } from "./native-delivery-status.mjs";
|
|
11
|
+
|
|
12
|
+
import { nativeSessionLines, updateNativeSessions } from "./native-session-diagnostics.mjs";
|
|
13
|
+
|
|
9
14
|
import { ALL_ADAPTERS, clientContext, probeTimeout } from "./install-command.mjs";
|
|
10
15
|
import { describePresence } from "./main.mjs";
|
|
11
16
|
import { platformPaths } from "./platform-paths.mjs";
|
|
@@ -66,21 +71,6 @@ export function staleInstall({ recorded, running }) {
|
|
|
66
71
|
return recorded === running ? null : { recorded, running };
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
const RUNTIME_LABEL = Object.freeze({ active: "active", waiting: "waiting for a live session",
|
|
70
|
-
inactive: "not enabled", degraded: "degraded", unsupported: "unsupported" });
|
|
71
|
-
|
|
72
|
-
/** One human clause for a native-delivery state, next-action implied, never overclaiming. */
|
|
73
|
-
export function describeNative(native) {
|
|
74
|
-
if (native.eligibility === "unsupported") {
|
|
75
|
-
return `unsupported${native.reasonCode ? ` (${native.reasonCode})` : ""}`;
|
|
76
|
-
}
|
|
77
|
-
const enabled = native.configured ? `enabled (${native.policy})` : "eligible, not enabled";
|
|
78
|
-
const runtime = RUNTIME_LABEL[native.runtime] ?? native.runtime;
|
|
79
|
-
const degraded = native.eligibility === "degraded" && native.reasonCode
|
|
80
|
-
? ` - ${native.reasonCode}` : "";
|
|
81
|
-
return `${native.eligibility} - ${enabled} - ${runtime}${degraded}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
74
|
/**
|
|
85
75
|
* The runner version behind whatever ACC wrote for one client.
|
|
86
76
|
*
|
|
@@ -117,25 +107,6 @@ async function findShims(root, depth) {
|
|
|
117
107
|
return found;
|
|
118
108
|
}
|
|
119
109
|
|
|
120
|
-
// One closed native-delivery report per adapter, built only from detection,
|
|
121
|
-
// ownership, and later the live binding facts - never inferred from a
|
|
122
|
-
// configured shim alone. eligibility is what the client could do; configured is
|
|
123
|
-
// whether a policy was recorded; policy is that recorded policy; runtime is
|
|
124
|
-
// filled in from current bindings; modes and reasonCode carry the closed
|
|
125
|
-
// detail. runtime "active" never means the model read anything.
|
|
126
|
-
function nativeState(detected, recordedPolicy) {
|
|
127
|
-
const native = detected ?? { state: "unsupported", reasonCode: "native_delivery_unsupported" };
|
|
128
|
-
const eligibility = native.state === "eligible" ? "eligible"
|
|
129
|
-
: native.state === "degraded" ? "degraded" : "unsupported";
|
|
130
|
-
const policy = recordedPolicy ?? "off";
|
|
131
|
-
const configured = policy !== "off";
|
|
132
|
-
const modes = native.state === "eligible" && Array.isArray(native.probe?.modes)
|
|
133
|
-
? [...native.probe.modes] : [];
|
|
134
|
-
const runtime = eligibility === "unsupported" ? "unsupported"
|
|
135
|
-
: !configured ? "inactive" : "waiting";
|
|
136
|
-
return { eligibility, configured, policy, runtime, modes, reasonCode: native.reasonCode ?? null };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
110
|
export async function diagnoseAdapters({ options, runtime, detect = detectInstallation }) {
|
|
140
111
|
// The same home `acc install --home` writes to, or the real one. Reading a
|
|
141
112
|
// different home than install wrote to reports every adapter as missing.
|
|
@@ -203,7 +174,9 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
|
|
|
203
174
|
// divergence legible rather than hidden behind a single reassuring number.
|
|
204
175
|
return { ...entry, stale, wired, bundleVersion, owned: { modified: owned.modified,
|
|
205
176
|
missing: owned.missing, intact: owned.intact.length },
|
|
206
|
-
nativeDelivery: nativeState(entry.nativeDelivery, livePolicyOf(installed)
|
|
177
|
+
nativeDelivery: nativeState(entry.nativeDelivery, livePolicyOf(installed), {
|
|
178
|
+
contract: adapters.find(adapter => adapter.id === entry.adapterId)?.nativeDelivery,
|
|
179
|
+
activation: installed?.nativeActivation }), remediation };
|
|
207
180
|
}));
|
|
208
181
|
}
|
|
209
182
|
|
|
@@ -259,22 +232,10 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
259
232
|
// front.
|
|
260
233
|
const service = context.service ?? await context.openService();
|
|
261
234
|
const status = await service.collectStatus({});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const bindingsByAdapter = new Map();
|
|
265
|
-
for (const binding of status.deliveryBindings ?? []) {
|
|
266
|
-
const existing = bindingsByAdapter.get(binding.adapterId);
|
|
267
|
-
if (existing === undefined || binding.reachable) bindingsByAdapter.set(binding.adapterId, binding);
|
|
268
|
-
}
|
|
235
|
+
updateNativeRuntime(adapters, status.deliveryBindings);
|
|
236
|
+
await updateNativeSessions(adapters, { service, status, root, now: clock.now() });
|
|
269
237
|
for (const adapter of adapters) {
|
|
270
|
-
|
|
271
|
-
if (native.eligibility === "unsupported") continue;
|
|
272
|
-
const binding = bindingsByAdapter.get(adapter.adapterId);
|
|
273
|
-
native.runtime = binding === undefined ? (native.configured ? "waiting" : "inactive")
|
|
274
|
-
: binding.reachable ? "active" : "degraded";
|
|
275
|
-
if (binding !== undefined && Array.isArray(binding.availableModes)) {
|
|
276
|
-
native.modes = binding.availableModes.filter(mode => mode !== "nextTurn");
|
|
277
|
-
}
|
|
238
|
+
adapter.remediation.push(...nativeRemediation(adapter));
|
|
278
239
|
}
|
|
279
240
|
|
|
280
241
|
const data = {
|
|
@@ -301,13 +262,19 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
301
262
|
const text = [`store healthy; ${describePresence(status.counts)}; `
|
|
302
263
|
+ `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
|
|
303
264
|
...adapters.filter(adapter => (adapter.present || adapter.installed)
|
|
265
|
+
&& adapter.nativeDelivery.reasonCode === "native_delivery_unsupported"
|
|
304
266
|
&& typeof adapter.deliveryDiagnostic === "string")
|
|
305
267
|
.map(adapter => ` ${adapter.deliveryDiagnostic}`),
|
|
306
268
|
// One concise native-delivery line per detected client, distinguishing
|
|
307
269
|
// eligibility, the recorded policy, and the live runtime state. It never
|
|
308
270
|
// claims that "active" means a model read anything.
|
|
309
271
|
...adapters.filter(adapter => adapter.present)
|
|
310
|
-
.map(adapter => ` ${adapter.displayName}
|
|
272
|
+
.map(adapter => ` ${adapter.displayName} live delivery: `
|
|
273
|
+
+ `${describeNative(adapter.nativeDelivery, { clientVersion: adapter.version })}; `
|
|
274
|
+
+ `fallback: ${describeDeliveryFallback(adapter)}`),
|
|
275
|
+
...nativeSessionLines(adapters),
|
|
276
|
+
...adapters.filter(adapter => adapter.present && adapter.outgoingDelivery)
|
|
277
|
+
.map(adapter => ` ${adapter.displayName} ${adapter.outgoingDelivery.diagnostic}`),
|
|
311
278
|
...(manager === null ? [] : [` automatic updates ${manager.auto ? "on" : "off"}; ACC ${manager.active.version}`
|
|
312
279
|
+ (manager.pin ? `; pinned to ${manager.pin}` : ""), ...(manager.notice ? [` ${manager.notice}`] : [])]),
|
|
313
280
|
...data.remediation.map(line => ` ${line}`),
|
|
@@ -148,11 +148,13 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
148
148
|
...operations.filter(operation => operation.applied)
|
|
149
149
|
.flatMap(operation => describeChanges(operation, home)),
|
|
150
150
|
...operations.filter(operation => operation.applied
|
|
151
|
-
&& typeof operation.deliveryDiagnostic === "string")
|
|
152
|
-
.map(operation => ` ${operation.deliveryDiagnostic}`),
|
|
151
|
+
&& typeof (operation.deliverySummary ?? operation.deliveryDiagnostic) === "string")
|
|
152
|
+
.map(operation => ` ${operation.deliverySummary ?? operation.deliveryDiagnostic}`),
|
|
153
153
|
...operations.filter(operation => operation.applied)
|
|
154
154
|
.flatMap(operation => (operation.needsAction ?? [])
|
|
155
155
|
.map(step => ` ${operation.adapterId}: ${step}`)),
|
|
156
|
+
...operations.filter(operation => operation.applied)
|
|
157
|
+
.flatMap(operation => (operation.setupNotes ?? []).map(note => ` ${note}`)),
|
|
156
158
|
...skipped.map(entry => ` skip ${entry.adapterId}: ${entry.reason}`),
|
|
157
159
|
...failed.map(entry => ` ${entry.adapterId}: ${entry.error}`),
|
|
158
160
|
// Said once, where it is needed: the reader has just been shown a list of
|
|
@@ -203,42 +205,22 @@ export function actedOn(result) {
|
|
|
203
205
|
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
204
206
|
&& runtime.isInteractive() === true;
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
* Written for someone who has never heard of this project. It used to open with
|
|
211
|
-
* "Enable native live delivery" over a list of internal artefact names, which
|
|
212
|
-
* told a first-time reader neither what they would gain nor what it would cost -
|
|
213
|
-
* and left out the part that matters most: saying yes makes their own `claude`
|
|
214
|
-
* start with the vendor's `--dangerously-load-development-channels` flag. A
|
|
215
|
-
* consent prompt that hides the loudest consequence is not consent.
|
|
216
|
-
*
|
|
217
|
-
* So the order is the reader's, not the installer's: what changes for them,
|
|
218
|
-
* what it costs, what it touches, and that "no" costs them nothing.
|
|
219
|
-
*/
|
|
220
|
-
function questionFor(entry, context, { onlyEligible = false } = {}) {
|
|
221
|
-
const bootstrap = entry.nativeDelivery.activationPlan.mechanisms
|
|
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
|
|
222
212
|
.find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
|
|
223
|
-
|
|
224
|
-
// why the client will warn at every start from now on. Which flag it is, and
|
|
225
|
-
// that a shim adds it, are answers to a question nobody is asking here - the
|
|
226
|
-
// shim `exec`s and is gone, so nothing runs "through" acc afterwards either.
|
|
227
|
-
const flags = (bootstrap?.prefixArgs ?? []).filter(argument => argument.startsWith("--"));
|
|
228
|
-
void context;
|
|
229
|
-
// Why the default is No, and why only one client is being asked about, are
|
|
230
|
-
// the two questions the bare prompt provokes and never answered: a `[y/N]`
|
|
231
|
-
// reads as "not recommended" on its own, and a single question among four
|
|
232
|
-
// installed clients reads as an omission. Both have the same short answer.
|
|
233
|
-
const aside = ["experimental", ...(onlyEligible ? [`${entry.displayName} only`] : [])]
|
|
234
|
-
.join("; ");
|
|
213
|
+
const warns = (bootstrap?.prefixArgs ?? []).some(argument => argument.startsWith("--"));
|
|
235
214
|
return [
|
|
236
|
-
`Let
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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.",
|
|
242
224
|
].join("\n");
|
|
243
225
|
}
|
|
244
226
|
|
|
@@ -250,22 +232,18 @@ function questionFor(entry, context, { onlyEligible = false } = {}) {
|
|
|
250
232
|
* client is off unless a person answers yes here, and a dry run or a
|
|
251
233
|
* non-interactive stdin/stdout makes no interactive choice at all.
|
|
252
234
|
*/
|
|
253
|
-
export async function decideDelivery({ options, detected, recorded, runtime, dryRun
|
|
235
|
+
export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
|
|
254
236
|
const explicit = options.delivery;
|
|
255
237
|
if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
|
|
256
238
|
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
257
239
|
}
|
|
258
240
|
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
259
|
-
// Whether this client is the only one that could be asked at all. When it is,
|
|
260
|
-
// the question says so, because otherwise being asked about one of four
|
|
261
|
-
// installed clients looks like the other three were forgotten.
|
|
262
|
-
const onlyEligible = detected
|
|
263
|
-
.filter(candidate => candidate.nativeDelivery?.state === "eligible").length === 1;
|
|
264
241
|
const deliveryByAdapter = {};
|
|
265
242
|
const asked = [];
|
|
266
243
|
let withheld = 0;
|
|
267
244
|
for (const entry of detected) {
|
|
268
|
-
const eligible = entry.nativeDelivery?.state === "eligible"
|
|
245
|
+
const eligible = entry.nativeDelivery?.state === "eligible"
|
|
246
|
+
|| entry.nativeDelivery?.consentAvailable === true;
|
|
269
247
|
const previous = livePolicyOf(recordedById.get(entry.adapterId));
|
|
270
248
|
if (explicit !== undefined) {
|
|
271
249
|
deliveryByAdapter[entry.adapterId] = explicit;
|
|
@@ -275,7 +253,7 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
|
|
|
275
253
|
deliveryByAdapter[entry.adapterId] = "off";
|
|
276
254
|
if (eligible) withheld += 1;
|
|
277
255
|
} else {
|
|
278
|
-
const yes = await runtime.confirm(questionFor(entry
|
|
256
|
+
const yes = await runtime.confirm(questionFor(entry),
|
|
279
257
|
{ input: runtime.input, output: runtime.output }) === true;
|
|
280
258
|
deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
|
|
281
259
|
asked.push(entry.adapterId);
|
|
@@ -333,7 +311,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
333
311
|
// Decided only after detection, from the operator's explicit answer, the
|
|
334
312
|
// recorded consent, or a default of off; never from inference.
|
|
335
313
|
const decided = action === "install"
|
|
336
|
-
? await decideDelivery({ options, detected, recorded, runtime, dryRun
|
|
314
|
+
? await decideDelivery({ options, detected, recorded, runtime, dryRun })
|
|
337
315
|
: { deliveryByAdapter: {}, asked: [], notes: [] };
|
|
338
316
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
339
317
|
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
@@ -137,7 +137,9 @@ function recordedText(message, delivery) {
|
|
|
137
137
|
const diagnostics = delivery.map(item => item.outcome === "offered"
|
|
138
138
|
? `live offered to ${item.recipientParticipantId} via ${item.transport}`
|
|
139
139
|
: item.outcome === "queued"
|
|
140
|
-
?
|
|
140
|
+
? item.errorCode === "transport_permission_denied"
|
|
141
|
+
? "live offer blocked by sender permissions; message remains queued; run acc doctor in the sending session"
|
|
142
|
+
: `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
|
|
141
143
|
: `delivery already ${item.outcome}`);
|
|
142
144
|
return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
|
|
143
145
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
2
|
+
|
|
3
|
+
export function describeNative(native, { clientVersion } = {}) {
|
|
4
|
+
const differs = native.runtime === "active" && native.sessionPolicy
|
|
5
|
+
&& native.sessionPolicy !== native.policy;
|
|
6
|
+
const enabled = (native.configured ? `enabled (${native.policy})` : "off")
|
|
7
|
+
+ (differs ? " for new sessions" : "");
|
|
8
|
+
const availability = native.eligibility === "eligible" ? "available"
|
|
9
|
+
: `${native.eligibility === "unsupported" ? "unavailable" : "readiness unverified"}: `
|
|
10
|
+
+ describeNativeReason(native.reasonCode, { clientVersion, minimumVersion: native.minimumVersion });
|
|
11
|
+
const runtime = native.runtime === "active"
|
|
12
|
+
? `local transport active${differs ? ` (session policy: ${native.sessionPolicy})` : ""}`
|
|
13
|
+
: native.runtime === "degraded" ? "channel unreachable"
|
|
14
|
+
: native.configured && native.runtime === "waiting"
|
|
15
|
+
? "no live channel bound in this workspace" : null;
|
|
16
|
+
return [availability, enabled, ...(runtime ? [runtime] : [])].join("; ");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function nativeRemediation(entry) {
|
|
20
|
+
const native = entry.nativeDelivery;
|
|
21
|
+
if (!(entry.present || entry.installed) || native.eligibility === "unsupported") return [];
|
|
22
|
+
const steps = [];
|
|
23
|
+
if (!native.configured && native.runtime !== "active") {
|
|
24
|
+
steps.push(`acc install --adapter ${entry.adapterId} --delivery actionable`
|
|
25
|
+
+ " # opt in to automatic peer requests; may spend tokens");
|
|
26
|
+
} else if (native.configured && native.activation === "missing") {
|
|
27
|
+
steps.push(`acc install --adapter ${entry.adapterId}`
|
|
28
|
+
+ " # complete the missing native launch setup from a supported shell");
|
|
29
|
+
} else if (native.configured && native.runtime !== "active") {
|
|
30
|
+
steps.push(`${entry.displayName}: no verified live channel in this workspace; `
|
|
31
|
+
+ "open a new terminal, start a new client session and check its integration/channel prompts; "
|
|
32
|
+
+ "then run acc doctor here");
|
|
33
|
+
}
|
|
34
|
+
if (native.reasonCode === "native_endpoint_unavailable") {
|
|
35
|
+
steps.push(entry.nativeSetup ?? `${entry.displayName}: check the client's local delivery service setup; `
|
|
36
|
+
+ "ACC does not start or restart that service");
|
|
37
|
+
} else if (native.reasonCode === "native_session_unavailable") {
|
|
38
|
+
steps.push(`${entry.displayName}: open a session connected to the client's local delivery service`);
|
|
39
|
+
}
|
|
40
|
+
return steps;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// One closed native-delivery report per adapter, built only from detection,
|
|
44
|
+
// ownership, and later the live binding facts - never inferred from a
|
|
45
|
+
// configured shim alone. eligibility is what the client could do; configured is
|
|
46
|
+
// whether a policy was recorded; policy is that recorded policy; runtime is
|
|
47
|
+
// filled in from current bindings; modes and reasonCode carry the closed
|
|
48
|
+
// detail. runtime "active" never means the model read anything.
|
|
49
|
+
export function nativeState(detected, recordedPolicy, { contract, activation } = {}) {
|
|
50
|
+
const native = detected ?? { state: "unsupported", reasonCode: "native_delivery_unsupported" };
|
|
51
|
+
const eligibility = native.state === "eligible" ? "eligible"
|
|
52
|
+
: native.state === "degraded" ? "degraded" : "unsupported";
|
|
53
|
+
const policy = recordedPolicy ?? "off";
|
|
54
|
+
const configured = policy !== "off";
|
|
55
|
+
const modes = native.state === "eligible" && Array.isArray(native.probe?.modes)
|
|
56
|
+
? [...native.probe.modes] : [];
|
|
57
|
+
const runtime = eligibility === "unsupported" ? "unsupported"
|
|
58
|
+
: !configured ? "inactive" : "waiting";
|
|
59
|
+
const setupRequired = contract?.activationKinds?.some(kind => kind !== "native-service") === true;
|
|
60
|
+
return { eligibility, configured, policy, runtime, modes, reasonCode: native.reasonCode ?? null,
|
|
61
|
+
minimumVersion: native.eligibility?.minimumVersion ?? null,
|
|
62
|
+
policySource: contract?.policySource ?? null,
|
|
63
|
+
activation: !setupRequired ? "not_required"
|
|
64
|
+
: activation?.mechanisms?.length > 0 ? "recorded" : "missing", sessionPolicy: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Native runtime is workspace-specific. A hook-only binding cannot prove it;
|
|
68
|
+
// legacy launch policies may outlive the policy selected for future launches.
|
|
69
|
+
export function updateNativeRuntime(adapters, bindings) {
|
|
70
|
+
const byAdapter = new Map();
|
|
71
|
+
for (const binding of bindings ?? []) {
|
|
72
|
+
if (!binding.availableModes?.includes("livePush")) continue;
|
|
73
|
+
const previous = byAdapter.get(binding.adapterId);
|
|
74
|
+
if (previous === undefined || binding.reachable) byAdapter.set(binding.adapterId, binding);
|
|
75
|
+
}
|
|
76
|
+
for (const entry of adapters) {
|
|
77
|
+
const native = entry.nativeDelivery;
|
|
78
|
+
if (native.eligibility === "unsupported") continue;
|
|
79
|
+
const binding = byAdapter.get(entry.adapterId);
|
|
80
|
+
const disabled = native.policySource === "installation-record" && !native.configured;
|
|
81
|
+
native.runtime = disabled ? "inactive"
|
|
82
|
+
: binding === undefined ? (native.configured ? "waiting" : "inactive")
|
|
83
|
+
: binding.reachable ? "active" : "degraded";
|
|
84
|
+
native.sessionPolicy = disabled || !binding ? null
|
|
85
|
+
: native.policySource === "installation-record" ? native.policy : binding.livePolicy;
|
|
86
|
+
if (binding) native.modes = binding.availableModes.filter(mode => mode !== "nextTurn");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { listSessionBindings, loadNativeAttempt } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
3
|
+
|
|
4
|
+
// Session diagnostics are read only here, never projected into agent context.
|
|
5
|
+
// Verify the owner generation against core before attributing any last attempt.
|
|
6
|
+
export async function updateNativeSessions(adapters, { service, status, root, now }) {
|
|
7
|
+
const owners = await listSessionBindings({ runtimeDir: root }).catch(() => []);
|
|
8
|
+
const transports = new Map();
|
|
9
|
+
for (const entry of adapters) {
|
|
10
|
+
const native = entry.nativeDelivery;
|
|
11
|
+
native.sessions = [];
|
|
12
|
+
if (native.reasonCode === "native_delivery_unsupported") continue;
|
|
13
|
+
for (const peer of status.participants.filter(p => p.harness === entry.adapterId)) {
|
|
14
|
+
const current = (await service.locateSession(peer.sessionId))?.record;
|
|
15
|
+
if (current?.state !== "open") continue;
|
|
16
|
+
const matches = owners.filter(owner => owner.accSessionId === peer.sessionId
|
|
17
|
+
&& owner.generation === current.generation);
|
|
18
|
+
const owner = matches.length === 1 ? matches[0] : null;
|
|
19
|
+
if (!transports.has(peer.participantId)) {
|
|
20
|
+
transports.set(peer.participantId, await service.listDeliveryBindings({
|
|
21
|
+
participantId: peer.participantId, now, includeExpired: true }));
|
|
22
|
+
}
|
|
23
|
+
const binding = transports.get(peer.participantId).find(b => b.sessionId === peer.sessionId
|
|
24
|
+
&& b.generation === current.generation && b.adapterId === entry.adapterId
|
|
25
|
+
&& b.availableModes.includes("livePush"));
|
|
26
|
+
const runtime = native.policySource === "installation-record" && !native.configured
|
|
27
|
+
? "inactive" : !binding ? "unbound"
|
|
28
|
+
: Date.parse(binding.leaseUntil) > Date.parse(now) ? "active" : "degraded";
|
|
29
|
+
native.sessions.push({ sessionId: peer.sessionId, participantId: peer.participantId,
|
|
30
|
+
presence: peer.presence, runtime,
|
|
31
|
+
lastAttempt: owner === null ? null : await loadNativeAttempt({ runtimeDir: root, ...owner }) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const failures = {
|
|
37
|
+
client_process_unknown: "the client process could not be identified; start a new client session",
|
|
38
|
+
handshake_failed: "the session handshake failed; check the client's integration/channel setup",
|
|
39
|
+
handshake_timeout: "the session handshake timed out; check the local channel and retry on the next turn",
|
|
40
|
+
handshake_version_mismatch: "the session and CLI versions differ; start a new client session",
|
|
41
|
+
session_generation_stale: "the session was replaced during the handshake",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function describeAttempt(attempt) {
|
|
45
|
+
if (attempt === null) return "no native binding attempt observed for this generation; "
|
|
46
|
+
+ "check client hooks; an older hook runtime or a failed diagnostic write can also leave no record";
|
|
47
|
+
let detail;
|
|
48
|
+
if (attempt.state === "off") {
|
|
49
|
+
const source = attempt.policySource === "bootstrap-environment" ? "launch" : "installation";
|
|
50
|
+
detail = attempt.policyStatus === "missing" ? `${source} delivery policy was absent`
|
|
51
|
+
: attempt.policyStatus === "invalid" ? `${source} delivery policy was invalid`
|
|
52
|
+
: attempt.policyStatus === "unavailable" ? `${source} delivery policy could not be read`
|
|
53
|
+
: `${source} delivery policy was off`;
|
|
54
|
+
} else if (attempt.state === "active") detail = "local handshake succeeded";
|
|
55
|
+
else detail = failures[attempt.reasonCode] ?? describeNativeReason(attempt.reasonCode);
|
|
56
|
+
return `last attempt ${attempt.at}: ${detail}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function nativeSessionLines(adapters) {
|
|
60
|
+
return adapters.flatMap(entry => (entry.nativeDelivery.sessions ?? []).map(session =>
|
|
61
|
+
` ${entry.displayName} session ${session.participantId} (${session.sessionId}): `
|
|
62
|
+
+ `${session.runtime === "active" ? "local transport active" : session.runtime}; `
|
|
63
|
+
+ describeAttempt(session.lastAttempt)));
|
|
64
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { refreshExpiredBinding } from "./refresh-binding.mjs";
|
|
2
2
|
|
|
3
3
|
const SAFE_ERRORS = new Set(["ambiguous_recipient_sessions", "delivery_disabled",
|
|
4
|
-
"recipient_busy", "recipient_unavailable", "transport_error", "transport_rejected",
|
|
4
|
+
"recipient_busy", "recipient_unavailable", "transport_error", "transport_rejected", "transport_permission_denied",
|
|
5
5
|
"unsupported_client_version"]);
|
|
6
6
|
const NAMED_LIVE_TRANSPORTS = new Set(["claude-channel", "codex-app-server"]);
|
|
7
7
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { storeNativeAttempt } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
import { readInstalledLivePolicyState } from "@agents-can-communicate/installer";
|
|
3
|
+
|
|
4
|
+
import { establishNativeBinding, LIVE_POLICIES, livePolicyFrom } from "./native-binding.mjs";
|
|
5
|
+
|
|
6
|
+
// Reserve time for turn projection, status and output before optional disk I/O.
|
|
7
|
+
export function nativeDiagnosticDeadline(hookDeadline) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
return hookDeadline - now >= 750 ? now + 250 : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The diagnostic has its own bounded writer; it never rewrites hook ownership.
|
|
13
|
+
// A failed or slow disk leaves the functional hook result intact.
|
|
14
|
+
export async function bindNative({ adapter, event, hookBinding, clientVersion, platform,
|
|
15
|
+
context, paths, deadline }) {
|
|
16
|
+
const assertBudget = () => {
|
|
17
|
+
if (Date.now() >= deadline) throw new Error("hook deadline expired");
|
|
18
|
+
};
|
|
19
|
+
assertBudget();
|
|
20
|
+
const policySource = adapter?.nativeDelivery?.policySource === "installation-record"
|
|
21
|
+
? "installation-record" : "bootstrap-environment";
|
|
22
|
+
const raw = context.env?.ACC_NATIVE_DELIVERY_POLICY;
|
|
23
|
+
const { policy, policyStatus } = policySource === "installation-record"
|
|
24
|
+
? await readInstalledLivePolicyState({ dataHome: context.dataHome, adapterId: adapter.id })
|
|
25
|
+
: { policy: livePolicyFrom(context.env), policyStatus: raw === undefined ? "missing"
|
|
26
|
+
: !LIVE_POLICIES.includes(raw) ? "invalid" : raw === "off" ? "off" : "enabled" };
|
|
27
|
+
assertBudget();
|
|
28
|
+
const result = await establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
|
|
29
|
+
livePolicy: policy, service: context.service, runtimeDir: paths.root,
|
|
30
|
+
clock: context.service.clock, env: context.env,
|
|
31
|
+
timeoutMs: Math.max(1, Math.min(750, deadline - Date.now())) });
|
|
32
|
+
const diagnosticDeadline = nativeDiagnosticDeadline(deadline);
|
|
33
|
+
if (adapter.nativeDelivery && diagnosticDeadline !== null) {
|
|
34
|
+
await storeNativeAttempt({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
35
|
+
accSessionId: hookBinding.accSessionId, generation: hookBinding.generation, deadlineAt: diagnosticDeadline, nativeAttempt: {
|
|
36
|
+
at: context.service.clock.now(), event: event.kind, state: result.state,
|
|
37
|
+
reasonCode: result.reasonCode, policy, policySource, policyStatus,
|
|
38
|
+
clientProcess: Number.isInteger(hookBinding.clientPid) && hookBinding.clientPid > 0
|
|
39
|
+
? "identified" : "unknown",
|
|
40
|
+
} }).catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|