agents-can-communicate 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/entrypoints/acc-bootstrap.mjs +6 -48
- package/bin/entrypoints/acc-claude-channel.mjs +47 -157
- package/docs/ADAPTER_AUTHORING.md +41 -10
- package/docs/ARCHITECTURE.md +23 -13
- package/docs/CAPABILITIES.md +19 -16
- package/docs/CLI.md +48 -13
- package/docs/CONCEPTS.md +5 -4
- package/docs/CONFIGURATION.md +18 -11
- package/docs/GETTING_STARTED.md +15 -12
- package/docs/GLOSSARY.md +2 -1
- package/docs/HOW_IT_WORKS.md +8 -4
- package/docs/PROTOCOL.md +25 -6
- package/docs/RELEASING.md +3 -2
- package/docs/SECURITY_MODEL.md +14 -1
- package/docs/TROUBLESHOOTING.md +64 -27
- package/docs/UPGRADING.md +49 -8
- package/docs/WHY_ACC.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +25 -30
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +254 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- 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/events.mjs +16 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +23 -11
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +5 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +25 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +19 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +24 -7
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +24 -11
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +3 -7
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
- 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/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
- package/package.json +1 -1
- package/bin/acc-bootstrap.mjs +0 -7
- package/bin/acc-claude-channel.mjs +0 -10
- package/bin/entrypoints/claude-channel-binding.mjs +0 -46
- package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
|
@@ -7,13 +7,14 @@ import path from "node:path";
|
|
|
7
7
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
-
import { acccreatedFile, bakeSkillCommand, blankJson,
|
|
10
|
+
import { acccreatedFile, bakeSkillCommand, blankJson,
|
|
11
11
|
mergeOwnedEntries, ownedEntries,
|
|
12
12
|
keepVersions, ownVersion, stampPluginVersion,
|
|
13
13
|
removeIfEmpty,
|
|
14
14
|
removeInstalledTree,
|
|
15
15
|
removeOwnedEntries, writeCliShim, writeForeignJson, writeHookShim }
|
|
16
16
|
from "@agents-can-communicate/adapter-sdk";
|
|
17
|
+
import { MANAGED_SETTINGS, inboundStatus } from "./inbox-settings.mjs";
|
|
17
18
|
|
|
18
19
|
const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
|
|
19
20
|
const manifest = fileURLToPath(new URL("../plugin/.claude-plugin/plugin.json",
|
|
@@ -22,9 +23,6 @@ const manifest = fileURLToPath(new URL("../plugin/.claude-plugin/plugin.json",
|
|
|
22
23
|
const PLUGIN_NAME = "agents-can-communicate";
|
|
23
24
|
const MARKETPLACE = "acc-local";
|
|
24
25
|
const QUALIFIED = `${PLUGIN_NAME}@${MARKETPLACE}`;
|
|
25
|
-
const CHANNEL_ACTIVATION_CHECK = "Check Claude's Channels startup notice for ACC; "
|
|
26
|
-
+ "an MCP connection does not verify inbound delivery. If Channels are unavailable "
|
|
27
|
-
+ "or blocked, resolve that client warning before expecting live messages.";
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
28
|
* How this client actually installs a plugin.
|
|
@@ -147,24 +145,21 @@ const marketplaceManifest = () => ({
|
|
|
147
145
|
}],
|
|
148
146
|
});
|
|
149
147
|
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// removed entirely for a non-live install so a plain launch spawns nothing.
|
|
148
|
+
// Up to 0.7.x a live install wrote the Channel MCP server here, and Claude
|
|
149
|
+
// Code spawned it for every session that enabled the plugin. The inbox wake
|
|
150
|
+
// needs no MCP server, so no plugin copy ACC keeps may name one.
|
|
154
151
|
const mcpPath = target => path.join(target, ".mcp.json");
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
|
|
153
|
+
async function removeChannelConfig(versionsRoot) {
|
|
154
|
+
const versions = await readdir(versionsRoot).catch(() => []);
|
|
155
|
+
await Promise.all(versions.map(version => rm(mcpPath(path.join(versionsRoot, version)),
|
|
156
|
+
{ force: true })));
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
/** A plugin tree with the shim written and the skill's command baked in. */
|
|
161
|
-
async function layOutPlugin(target, { runner, node, cli
|
|
160
|
+
async function layOutPlugin(target, { runner, node, cli }) {
|
|
162
161
|
await rm(target, { recursive: true, force: true });
|
|
163
162
|
await cp(bundle, target, { recursive: true });
|
|
164
|
-
// The bundle ships a placeholder .mcp.json; the real one is written only for a
|
|
165
|
-
// live install, and a non-live tree carries none.
|
|
166
|
-
await rm(mcpPath(target), { force: true });
|
|
167
|
-
if (live) await writeChannelMcp(target, { node, channel });
|
|
168
163
|
// The skill ships with a placeholder where the command belongs: `acc` is not
|
|
169
164
|
// on PATH everywhere, and an agent that cannot run it improvises. The shim
|
|
170
165
|
// carries the pinning so each example can name one path. No data home is
|
|
@@ -181,9 +176,8 @@ async function layOutPlugin(target, { runner, node, cli, channel, live }) {
|
|
|
181
176
|
version: await pluginVersion(), io: { readFile, writeFile } });
|
|
182
177
|
}
|
|
183
178
|
|
|
184
|
-
export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null,
|
|
185
|
-
|
|
186
|
-
const live = livePolicy === "actionable" || livePolicy === "all";
|
|
179
|
+
export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null,
|
|
180
|
+
node = process.execPath, now = new Date() }) {
|
|
187
181
|
// Everything this will merge into, read before a byte is written. A settings
|
|
188
182
|
// file that will not parse used to be discovered after the plugin tree was
|
|
189
183
|
// already on disk, and the install then failed with nineteen files left
|
|
@@ -200,12 +194,12 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
|
|
|
200
194
|
const source = sourceDir(configDir);
|
|
201
195
|
const cached = cachePath(configDir, version);
|
|
202
196
|
|
|
203
|
-
await layOutPlugin(source, { runner, node, cli
|
|
197
|
+
await layOutPlugin(source, { runner, node, cli });
|
|
204
198
|
await writeJson(marketplaceFile(configDir), marketplaceManifest());
|
|
205
199
|
// The copy the client runs from. Written here rather than asking the user to
|
|
206
200
|
// run `claude plugin install`, exactly as the Codex adapter does, because the
|
|
207
201
|
// command's only effect is this copy plus the two registry entries below.
|
|
208
|
-
await layOutPlugin(cached, { runner, node, cli
|
|
202
|
+
await layOutPlugin(cached, { runner, node, cli });
|
|
209
203
|
// The copy just written, plus the one an upgrade moved off. A client caches a
|
|
210
204
|
// plugin under its version, so every upgrade would otherwise leave the previous
|
|
211
205
|
// release's tree beside this one - invisible while the version never moved,
|
|
@@ -214,6 +208,10 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
|
|
|
214
208
|
// anything older than that holds no session and is litter.
|
|
215
209
|
await keepVersions({ root: path.dirname(cached), keep: [version, keepPreviousVersion],
|
|
216
210
|
io: { readdir, rm } });
|
|
211
|
+
// The copy an upgrade moved off stays for sessions already running from it,
|
|
212
|
+
// but its Channel config would still start a Channel for any session that
|
|
213
|
+
// reads it. Those sessions keep what they loaded; nothing new starts one.
|
|
214
|
+
await removeChannelConfig(path.dirname(cached));
|
|
217
215
|
|
|
218
216
|
await writeClientJson(knownMarketplacesPath(configDir), {
|
|
219
217
|
...known,
|
|
@@ -254,12 +252,9 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
|
|
|
254
252
|
|
|
255
253
|
return { ok: true,
|
|
256
254
|
changes: [source, cached, marketplaceFile(configDir),
|
|
257
|
-
knownMarketplacesPath(configDir), installedPluginsPath(configDir), file,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
diagnostics: live
|
|
261
|
-
? ["native channel wired; Claude's experimental development-channel warning still applies"]
|
|
262
|
-
: [] };
|
|
255
|
+
knownMarketplacesPath(configDir), installedPluginsPath(configDir), file],
|
|
256
|
+
needsAction: [],
|
|
257
|
+
diagnostics: [] };
|
|
263
258
|
}
|
|
264
259
|
|
|
265
260
|
export async function uninstallClaudePlugin({ configDir, keep = [] }) {
|
|
@@ -297,16 +292,15 @@ export async function uninstallClaudePlugin({ configDir, keep = [] }) {
|
|
|
297
292
|
return { ok: true, changes, diagnostics: [] };
|
|
298
293
|
}
|
|
299
294
|
|
|
300
|
-
export async function detectClaude({ configDir }) {
|
|
295
|
+
export async function detectClaude({ configDir, managedSettingsPath = MANAGED_SETTINGS[process.platform] }) {
|
|
301
296
|
const settings = await readJson(settingsPath(configDir), null);
|
|
302
297
|
const enabled = settings?.enabledPlugins?.[QUALIFIED] === true;
|
|
303
298
|
const registered = Object.hasOwn(
|
|
304
299
|
(await readJson(installedPluginsPath(configDir), { plugins: {} })).plugins ?? {},
|
|
305
300
|
QUALIFIED);
|
|
306
|
-
const channelConfigured = enabled && registered
|
|
307
|
-
&& (await readJson(mcpPath(sourceDir(configDir)), null))?.mcpServers?.["acc-channel"] != null;
|
|
308
301
|
return { ok: true, changes: [],
|
|
309
|
-
|
|
302
|
+
inboundDelivery: await inboundStatus({ configDir, managedSettingsPath }),
|
|
303
|
+
needsAction: [],
|
|
310
304
|
diagnostics: [enabled && registered
|
|
311
305
|
? "acc plugin registered and enabled"
|
|
312
306
|
: "acc plugin not registered"] };
|
|
@@ -4,7 +4,7 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
4
4
|
// so the four cannot drift: a consumer that has to ask which adapter produced an
|
|
5
5
|
// event is a consumer that will eventually get it wrong.
|
|
6
6
|
export const NORMALIZED_EVENT_KEYS = Object.freeze(["kind", "sessionId", "cwd", "model",
|
|
7
|
-
"parentSessionId", "tool", "targets"]);
|
|
7
|
+
"parentSessionId", "tool", "targets", "permissionMode", "endReason"]);
|
|
8
8
|
|
|
9
9
|
export const EVENT_KINDS = Object.freeze(["sessionStart", "sessionEnd", "heartbeat",
|
|
10
10
|
"beforeTurn", "beforeTool", "afterTool", "turnEnd", "childStart", "childEnd", "other"]);
|
|
@@ -34,7 +34,7 @@ export function normalizedEvent(fields) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
const { kind, sessionId, cwd, model = null, parentSessionId = null, tool = null,
|
|
37
|
-
targets = [] } = fields;
|
|
37
|
+
targets = [], permissionMode = null, endReason = null } = fields;
|
|
38
38
|
|
|
39
39
|
if (!EVENT_KINDS.includes(kind)) data("unknown normalised event kind", { kind });
|
|
40
40
|
if (typeof sessionId !== "string" || sessionId === "") {
|
|
@@ -50,6 +50,19 @@ export function normalizedEvent(fields) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// The client's own name for the session's permission mode, when its hook
|
|
54
|
+
// reports one. Only a native binding reads it, to tell a sender whether the
|
|
55
|
+
// client will hold a wake for approval.
|
|
56
|
+
if (permissionMode !== null && (typeof permissionMode !== "string" || permissionMode === "")) {
|
|
57
|
+
data("normalised event permission mode must be a non-empty string", { kind });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Why the client ended the session, in its own words, when it says. A sender
|
|
61
|
+
// is told a conversation was cleared rather than that its peer vanished.
|
|
62
|
+
if (endReason !== null && (typeof endReason !== "string" || endReason === "")) {
|
|
63
|
+
data("normalised event end reason must be a non-empty string", { kind });
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
return Object.freeze({ kind, sessionId, cwd, model, parentSessionId, tool,
|
|
54
|
-
targets: Object.freeze([...targets]) });
|
|
67
|
+
targets: Object.freeze([...targets]), permissionMode, endReason });
|
|
55
68
|
}
|
|
@@ -58,8 +58,6 @@ const ownBinary = name => {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
export const defaultRunner = () => ownBinary("acc-hook.mjs");
|
|
61
|
-
export const defaultBootstrap = () => ownBinary("acc-bootstrap.mjs");
|
|
62
|
-
export const defaultChannel = () => ownBinary("acc-claude-channel.mjs");
|
|
63
61
|
export const defaultAntigravityRelay = () => ownBinary("acc-antigravity-relay.mjs");
|
|
64
62
|
|
|
65
63
|
export const runnerExists = async runner => {
|
|
@@ -199,7 +197,10 @@ export async function writeCliShim({ dir, cli = defaultCli(), node = process.exe
|
|
|
199
197
|
* created, and a recognised path is not a reason to delete it.
|
|
200
198
|
*/
|
|
201
199
|
export async function removeInstalledTree(target, keep = []) {
|
|
202
|
-
|
|
200
|
+
// A kept tree holds someone's own work. Removing a directory inside it, or one
|
|
201
|
+
// that contains it, would delete that work as surely as removing it.
|
|
202
|
+
const within = (inner, outer) => inner === outer || inner.startsWith(`${outer}${path.sep}`);
|
|
203
|
+
if (keep.some(kept => within(target, kept) || within(kept, target))) return false;
|
|
203
204
|
try {
|
|
204
205
|
await lstat(target);
|
|
205
206
|
} catch (error) {
|
|
@@ -8,7 +8,7 @@ export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE
|
|
|
8
8
|
validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
|
|
9
9
|
from "./native-delivery.mjs";
|
|
10
10
|
export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
|
|
11
|
-
export { assertRunner, bakeSkillCommand, defaultAntigravityRelay,
|
|
11
|
+
export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli,
|
|
12
12
|
defaultRunner, removeInstalledTree, runnerExists, writeCliShim, writeHookShim }
|
|
13
13
|
from "./hook-shim.mjs";
|
|
14
14
|
export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { IDENTIFIER, NATIVE_ACTIVATION_KINDS, SHELL_SOURCE, assertReasonCode,
|
|
4
4
|
closed, deepFreeze, isPlainObject, isText, usage } from "./native-vocabulary.mjs";
|
|
5
5
|
|
|
6
6
|
// A native activation plan is what an adapter hands the installer: closed
|
|
@@ -27,19 +27,9 @@ function validateMechanism(mechanism, index) {
|
|
|
27
27
|
usage(`${label} kind must be one of ${NATIVE_ACTIVATION_KINDS.join(", ")}`);
|
|
28
28
|
}
|
|
29
29
|
if (mechanism.kind === "shell-bootstrap") {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
if (!isText(mechanism.realExecutable) || !path.isAbsolute(mechanism.realExecutable)) {
|
|
35
|
-
usage(`${label} realExecutable must be an absolute path`);
|
|
36
|
-
}
|
|
37
|
-
if (!Array.isArray(mechanism.prefixArgs) || mechanism.prefixArgs.some(arg => !isText(arg)
|
|
38
|
-
|| SHELL_SOURCE.test(arg))) {
|
|
39
|
-
usage(`${label} prefixArgs must be plain argument strings, never shell source`);
|
|
40
|
-
}
|
|
41
|
-
return { kind: mechanism.kind, command: mechanism.command,
|
|
42
|
-
realExecutable: mechanism.realExecutable, prefixArgs: [...mechanism.prefixArgs] };
|
|
30
|
+
// ACC 0.7.x put a `claude` shim on PATH this way. Install records of it are
|
|
31
|
+
// still read so it can be retired; a plan can no longer create one.
|
|
32
|
+
usage(`${label}: shell-bootstrap activation was removed; no adapter may plan one`);
|
|
43
33
|
}
|
|
44
34
|
if (mechanism.kind === "native-config") {
|
|
45
35
|
closed(mechanism, ["kind", "artifactIds"], label);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON,
|
|
2
|
-
|
|
1
|
+
import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_BINDING_MODES,
|
|
2
|
+
NATIVE_PLATFORMS, PLANNABLE_ACTIVATION_KINDS, PROBE_KEYS, TIMESTAMP, assertModes,
|
|
3
3
|
assertReasonCode, closed, compareStableVersions, deepFreeze, isPlainObject, isText,
|
|
4
4
|
parseStableVersion, usage } from "./native-vocabulary.mjs";
|
|
5
5
|
|
|
@@ -12,19 +12,31 @@ import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_ACTI
|
|
|
12
12
|
// capability; this rule is used for native live delivery alone.
|
|
13
13
|
|
|
14
14
|
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE_REASON_CODES,
|
|
15
|
-
compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
|
|
15
|
+
PLANNABLE_ACTIVATION_KINDS, compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
|
|
16
16
|
export { validateNativeActivationPlan } from "./native-activation.mjs";
|
|
17
17
|
|
|
18
18
|
const orderedModes = modes => NATIVE_BINDING_MODES.filter(mode => modes.includes(mode));
|
|
19
19
|
|
|
20
20
|
export function validateNativeDeliveryContract(value, { certification, client }) {
|
|
21
|
+
// Consent to live delivery is what `acc install` recorded. Up to 0.7.x a
|
|
22
|
+
// Claude shell shim could export it instead ("bootstrap-environment"); the
|
|
23
|
+
// shim is gone, and the installation record is the one source left.
|
|
21
24
|
const policySource = Object.hasOwn(value ?? {}, "policySource")
|
|
22
|
-
? value.policySource : "
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
? value.policySource : "installation-record";
|
|
26
|
+
// What a successful offer put in front of the model. "message" carries the
|
|
27
|
+
// body, so the router records the receipt as offered. "wake" carries only a
|
|
28
|
+
// notice that makes the client run a turn; the body reaches the model through
|
|
29
|
+
// the next-turn hook, which records the offer itself once its stdout carried
|
|
30
|
+
// the body. Recording a wake as offered would hide the body from that hook.
|
|
31
|
+
const offerKind = Object.hasOwn(value ?? {}, "offerKind") ? value.offerKind : "message";
|
|
32
|
+
closed({ ...value, policySource, offerKind },
|
|
33
|
+
["minimumByPlatform", "anchors", "knownBad", "activationKinds", "policySource", "offerKind"],
|
|
25
34
|
"nativeDelivery");
|
|
26
|
-
if (
|
|
27
|
-
usage("nativeDelivery.policySource must be installation-record
|
|
35
|
+
if (policySource !== "installation-record") {
|
|
36
|
+
usage("nativeDelivery.policySource must be installation-record");
|
|
37
|
+
}
|
|
38
|
+
if (!["message", "wake"].includes(offerKind)) {
|
|
39
|
+
usage("nativeDelivery.offerKind must be message or wake");
|
|
28
40
|
}
|
|
29
41
|
const minimums = value.minimumByPlatform;
|
|
30
42
|
if (!isPlainObject(minimums) || Object.keys(minimums).length === 0) {
|
|
@@ -89,11 +101,11 @@ export function validateNativeDeliveryContract(value, { certification, client })
|
|
|
89
101
|
});
|
|
90
102
|
const kinds = value.activationKinds;
|
|
91
103
|
if (!Array.isArray(kinds) || kinds.length === 0 || new Set(kinds).size !== kinds.length
|
|
92
|
-
|| kinds.some(kind => !
|
|
93
|
-
usage(`nativeDelivery.activationKinds must be unique entries of ${
|
|
104
|
+
|| kinds.some(kind => !PLANNABLE_ACTIVATION_KINDS.includes(kind))) {
|
|
105
|
+
usage(`nativeDelivery.activationKinds must be unique entries of ${PLANNABLE_ACTIVATION_KINDS.join(", ")}`);
|
|
94
106
|
}
|
|
95
107
|
return deepFreeze({ minimumByPlatform: { ...minimums }, anchors, knownBad,
|
|
96
|
-
activationKinds: [...kinds], policySource });
|
|
108
|
+
activationKinds: [...kinds], policySource, offerKind });
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
function knownBadHit(contract, version) {
|
|
@@ -9,10 +9,15 @@ export const NATIVE_BINDING_MODES = Object.freeze([
|
|
|
9
9
|
"livePush", "idleWake", "busyQueue", "replyRoute",
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
|
+
// Every kind an install record may carry. "shell-bootstrap" is the `claude`
|
|
13
|
+
// shim ACC 0.7.x wrote; records of it stay readable so it can be retired.
|
|
12
14
|
export const NATIVE_ACTIVATION_KINDS = Object.freeze([
|
|
13
15
|
"shell-bootstrap", "native-config", "native-service",
|
|
14
16
|
]);
|
|
15
17
|
|
|
18
|
+
// The kinds an adapter may still ask for.
|
|
19
|
+
export const PLANNABLE_ACTIVATION_KINDS = Object.freeze(["native-config", "native-service"]);
|
|
20
|
+
|
|
16
21
|
export const NATIVE_PLATFORMS = Object.freeze([
|
|
17
22
|
"darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-x64",
|
|
18
23
|
]);
|
|
@@ -34,7 +39,6 @@ export const NATIVE_REASON_CODES = Object.freeze([
|
|
|
34
39
|
export const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
|
|
35
40
|
export const CONTRACT_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
36
41
|
export const IDENTIFIER = /^[a-z][a-z0-9_-]*$/;
|
|
37
|
-
export const COMMAND_NAME = /^[a-z][a-z0-9_.-]*$/;
|
|
38
42
|
export const FINGERPRINT = /^sha256:[0-9a-f]{64}$/;
|
|
39
43
|
export const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
40
44
|
export const SHELL_SOURCE = /[;&|<>`\n\0]|\$\(/;
|
|
@@ -2,7 +2,7 @@ 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 { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnership,
|
|
5
|
+
import { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnership, verifyOwned }
|
|
6
6
|
from "@agents-can-communicate/installer";
|
|
7
7
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
8
8
|
|
|
@@ -114,12 +114,9 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
|
|
|
114
114
|
const home = options?.home ?? runtime?.env?.HOME ?? homedir();
|
|
115
115
|
const { data: dataHome } = platformPaths({ platform: runtime?.platform,
|
|
116
116
|
env: runtime?.env ?? {} });
|
|
117
|
-
// The same
|
|
118
|
-
// shell bootstrap it would apply. Omitting them left detection with a null
|
|
119
|
-
// shell, which degrades every shell-bootstrap client to `unsupported_shell` -
|
|
120
|
-
// so a zsh machine with a working shim read as degraded on every run.
|
|
117
|
+
// The same environment install reads, so detection probes the same client.
|
|
121
118
|
const clients = clientContext(home, path.join(dataHome, "acc"),
|
|
122
|
-
{
|
|
119
|
+
{ env: runtime?.env ?? {} });
|
|
123
120
|
const adapters = ALL_ADAPTERS();
|
|
124
121
|
const detected = await detect({ adapters, context: clients,
|
|
125
122
|
probeTimeoutMs: probeTimeout(runtime?.env) });
|
|
@@ -297,6 +294,11 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
297
294
|
...nativeSessionLines(adapters),
|
|
298
295
|
...adapters.filter(adapter => adapter.present && adapter.outgoingDelivery?.state === "configured")
|
|
299
296
|
.map(adapter => ` ${adapter.displayName} ${adapter.outgoingDelivery.diagnostic}`),
|
|
297
|
+
// The client's own inbound control decides whether a wake reaches the model.
|
|
298
|
+
// Only someone who turned live delivery on needs to hear about it.
|
|
299
|
+
...adapters.filter(adapter => adapter.present && adapter.nativeDelivery.configured
|
|
300
|
+
&& typeof adapter.inboundDelivery?.diagnostic === "string")
|
|
301
|
+
.map(adapter => ` ${adapter.displayName} inbound: ${adapter.inboundDelivery.diagnostic}`),
|
|
300
302
|
...(manager === null ? [] : [` automatic updates ${manager.auto ? "on" : "off"}; ACC ${manager.active.version}`
|
|
301
303
|
+ (manager.pin ? `; pinned to ${manager.pin}` : ""), ...(update.notice ? [` ${update.notice}`] : [])]),
|
|
302
304
|
...data.remediation.map(line => ` ${line}`),
|
|
@@ -7,8 +7,7 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
|
7
7
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
8
8
|
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
9
9
|
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
10
|
-
import { applyPlan, detectInstallation,
|
|
11
|
-
loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
|
|
10
|
+
import { applyPlan, detectInstallation, loadOwnership, planInstallation }
|
|
12
11
|
from "@agents-can-communicate/installer";
|
|
13
12
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
14
13
|
|
|
@@ -29,7 +28,7 @@ export { decideDelivery } from "./install-delivery-consent.mjs";
|
|
|
29
28
|
// pointed at the home itself writes beside them rather than inside them. That
|
|
30
29
|
// install reports success and the client never reads a byte of it.
|
|
31
30
|
export const clientContext = (home, stateRoot,
|
|
32
|
-
{
|
|
31
|
+
{ env = {}, dataHome, cwd = process.cwd() } = {}) => ({
|
|
33
32
|
home,
|
|
34
33
|
...(dataHome === undefined ? {} : { dataHome }),
|
|
35
34
|
configDir: path.join(home, ".claude"),
|
|
@@ -39,10 +38,8 @@ export const clientContext = (home, stateRoot,
|
|
|
39
38
|
kimiHome: path.join(home, ".kimi-code"),
|
|
40
39
|
grokHome: typeof env.GROK_HOME === "string" && env.GROK_HOME !== ""
|
|
41
40
|
? env.GROK_HOME : path.join(home, ".grok"),
|
|
42
|
-
// The user's
|
|
43
|
-
//
|
|
44
|
-
// shim would exec. Detection reads them; nothing here writes.
|
|
45
|
-
shell,
|
|
41
|
+
// The user's PATH, for finding the real client executable to probe.
|
|
42
|
+
// Detection reads it; nothing here writes.
|
|
46
43
|
env,
|
|
47
44
|
// Antigravity CLI keeps its hooks in the `~/.gemini` tree Gemini CLI also
|
|
48
45
|
// uses, and reads none of the same files. Two locations load - the
|
|
@@ -200,8 +197,7 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
200
197
|
export function failureOf({ action, acted, failed = [], operations = [], skipped = [], home }) {
|
|
201
198
|
if (failed.length === 0) return null;
|
|
202
199
|
return new AccError(EXIT.DATA,
|
|
203
|
-
|
|
204
|
-
...reloadAdvice(operations)].join("\n"), { failed });
|
|
200
|
+
describeOutcome({ action, acted, failed, operations, skipped, home }), { failed });
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
/**
|
|
@@ -220,23 +216,13 @@ export function actedOn(result) {
|
|
|
220
216
|
+ (operation.changes?.length ?? 0) > 0)).length;
|
|
221
217
|
}
|
|
222
218
|
|
|
223
|
-
// Said once, after the first PATH block is written: a running shell and a
|
|
224
|
-
// running client know nothing about it.
|
|
225
|
-
function reloadAdvice(operations) {
|
|
226
|
-
const appended = operations.filter(operation => operation.appendedRcBlock === true);
|
|
227
|
-
if (appended.length === 0) return [];
|
|
228
|
-
const rc = appended[0].nativeActivation?.rcFile ?? "your shell rc file";
|
|
229
|
-
return ["", `native delivery is wired for new sessions: open a new terminal (or reload ${rc}), `
|
|
230
|
-
+ "then start the client normally; a session already running keeps durable delivery"];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
219
|
export async function runInstallCommand({ options, runtime, action = "install" }) {
|
|
234
220
|
const adapters = selectAdapters(options.adapter);
|
|
235
221
|
const home = options.home ?? runtime.env?.HOME ?? homedir();
|
|
236
222
|
const { data: dataHome } = platformPaths({ platform: runtime.platform,
|
|
237
223
|
env: runtime.env ?? {} });
|
|
238
224
|
const context = clientContext(home, path.join(dataHome, "acc"),
|
|
239
|
-
{
|
|
225
|
+
{ env: runtime.env ?? {}, dataHome });
|
|
240
226
|
|
|
241
227
|
const detected = await detectInstallation({ adapters, context,
|
|
242
228
|
probeTimeoutMs: probeTimeout(runtime.env) });
|
|
@@ -273,7 +259,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
273
259
|
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
|
|
274
260
|
allowServiceSetup: action === "install" });
|
|
275
261
|
const apply = (paths = {}) => applyPlan({ plan, adapters, context: { ...context, ...paths },
|
|
276
|
-
dataHome, dryRun, accVersion
|
|
262
|
+
dataHome, dryRun, accVersion });
|
|
277
263
|
const result = action === "install" && !dryRun && runtime.packageRoot
|
|
278
264
|
? await installManaged({ packageRoot: runtime.packageRoot,
|
|
279
265
|
managerRoot: path.join(dataHome, "acc", "runtime"), dataHome, home, cwd: options.cwd ?? runtime.cwd,
|
|
@@ -293,9 +279,8 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
293
279
|
|
|
294
280
|
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
|
|
295
281
|
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter, asked: decided.asked },
|
|
296
|
-
text:
|
|
282
|
+
text: describeOutcome({ action, acted, failed: result.failed,
|
|
297
283
|
skipped: plan.skipped, operations: result.operations, home }),
|
|
298
|
-
...reloadAdvice(result.operations)].join("\n"),
|
|
299
284
|
error: failureOf({ action, acted, failed: result.failed, operations: result.operations,
|
|
300
285
|
skipped: plan.skipped, home }) };
|
|
301
286
|
}
|
|
@@ -57,9 +57,6 @@ function questionFor(entries) {
|
|
|
57
57
|
const names = entries.map(entry => entry.displayName ?? entry.adapterId).join(", ");
|
|
58
58
|
const setup = [...new Set(entries.map(entry => entry.outgoingDelivery?.setup)
|
|
59
59
|
.filter(value => typeof value === "string"))];
|
|
60
|
-
const channels = entries.some(entry => (entry.nativeDelivery?.activationPlan?.mechanisms ?? [])
|
|
61
|
-
.some(mechanism => mechanism.kind === "shell-bootstrap"
|
|
62
|
-
&& (mechanism.prefixArgs ?? []).some(argument => argument.startsWith("--"))));
|
|
63
60
|
return [
|
|
64
61
|
`Complete automatic peer-request setup for ${names}? (experimental)`,
|
|
65
62
|
" Yes: automatic turns can spend tokens without waiting for you.",
|
|
@@ -69,7 +66,9 @@ function questionFor(entries) {
|
|
|
69
66
|
...entries.filter(needsPrerequisite).map(entry =>
|
|
70
67
|
` Download the official Codex ${entry.nativeServiceSetup.cliVersion} standalone package for its service; retain your existing codex command.`),
|
|
71
68
|
...setup.map(note => ` ${note}`),
|
|
72
|
-
...(
|
|
69
|
+
...(entries.some(entry => entry.adapterId === "claude_code")
|
|
70
|
+
? [" Claude Code sessions that bypass permission prompts ask before each ACC wake;",
|
|
71
|
+
" set Claude Code's crossSessionInbound to accept to take wakes without asking."] : []),
|
|
73
72
|
" No: keep current delivery policies and use available next-turn hooks or acc inbox.",
|
|
74
73
|
].join("\n");
|
|
75
74
|
}
|
|
@@ -138,13 +138,26 @@ function obligationFor(kind, explicit, addressed) {
|
|
|
138
138
|
return obligation;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function recordedText(message, delivery) {
|
|
141
|
+
export function recordedText(message, delivery) {
|
|
142
142
|
const diagnostics = delivery.map(item => item.outcome === "offered"
|
|
143
143
|
? `live offered to ${item.recipientParticipantId} via ${item.transport}`
|
|
144
|
+
: item.outcome === "woken" && item.pendingApproval === true
|
|
145
|
+
? `sent a wake to ${item.recipientParticipantId} via ${item.transport}, which its session holds `
|
|
146
|
+
+ "for approval because it bypasses permission prompts; the message arrives with its next turn"
|
|
147
|
+
: item.outcome === "woken"
|
|
148
|
+
? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
|
|
144
149
|
: item.outcome === "queued"
|
|
145
150
|
? item.errorCode === "transport_permission_denied"
|
|
146
151
|
? "live offer blocked by sender permissions; message remains queued; run acc doctor in the sending session"
|
|
147
|
-
:
|
|
152
|
+
: item.errorCode === "no_live_transport"
|
|
153
|
+
? `${item.recipientParticipantId} has no live transport; the message waits in its inbox`
|
|
154
|
+
: item.errorCode === "recipient_offline" && item.endReason === "clear"
|
|
155
|
+
? `${item.recipientParticipantId}'s conversation was cleared (/clear); the message waits `
|
|
156
|
+
+ "until that conversation resumes"
|
|
157
|
+
: item.errorCode === "recipient_offline"
|
|
158
|
+
? `${item.recipientParticipantId} has no open session; the message waits until it starts `
|
|
159
|
+
+ "or resumes one"
|
|
160
|
+
: `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
|
|
148
161
|
: `delivery already ${item.outcome}`);
|
|
149
162
|
return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
|
|
150
163
|
}
|
|
@@ -185,7 +198,16 @@ export function describeStatus(status) {
|
|
|
185
198
|
// A transport accepting a message is not a model reading it. Said only when
|
|
186
199
|
// it is true of something, so the everyday line stays the everyday line.
|
|
187
200
|
const offered = status.counts.unretrieved?.offered ?? 0;
|
|
188
|
-
|
|
201
|
+
const withOffers = offered === 0 ? line : `${line}; ${offered} offered, not retrieved`;
|
|
202
|
+
// Status lists a participant with no open session only while messages wait
|
|
203
|
+
// for it; each is named here, with the client's reason when it gave one.
|
|
204
|
+
const waiting = (status.participants ?? []).filter(item => item.presence === "offline")
|
|
205
|
+
.map(item => {
|
|
206
|
+
const count = (item.unretrieved?.queued ?? 0) + (item.unretrieved?.offered ?? 0);
|
|
207
|
+
return `${item.participantId} (${item.endReason === "clear" ? "cleared, " : ""}${count})`;
|
|
208
|
+
});
|
|
209
|
+
return waiting.length === 0 ? withOffers
|
|
210
|
+
: `${withOffers}; waiting for a closed session: ${waiting.join(", ")}`;
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
const HANDLERS = Object.freeze({
|