agents-can-communicate 0.7.1 → 0.8.1
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/bin/entrypoints/acc-mcp.mjs +0 -1
- package/bin/entrypoints/acc.mjs +0 -1
- package/docs/ADAPTER_AUTHORING.md +74 -24
- package/docs/ARCHITECTURE.md +31 -18
- package/docs/CAPABILITIES.md +38 -25
- package/docs/CLI.md +51 -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 +8 -5
- package/docs/SECURITY_MODEL.md +14 -1
- package/docs/TROUBLESHOOTING.md +66 -29
- 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-antigravity/src/adapter.mjs +2 -3
- 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 +26 -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 +249 -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-codex/src/adapter.mjs +3 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +5 -5
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -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/certification.mjs +2 -2
- 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 +4 -3
- 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 +104 -84
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +8 -4
- 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 +24 -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/maintenance.mjs +9 -15
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
- 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/receipts.mjs +17 -2
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +31 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +33 -48
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -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 +27 -15
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +4 -2
- 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/delivery-diagnostics.mjs +0 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +6 -11
- 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
package/README.md
CHANGED
|
@@ -98,7 +98,9 @@ provide messages at the next normal turn; other sessions read their ACC inbox ex
|
|
|
98
98
|
|
|
99
99
|
Experimental live delivery can wake eligible Claude Code and Codex sessions on Apple Silicon
|
|
100
100
|
macOS. It requires opt-in and an active verified connection, is off by default, and can spend
|
|
101
|
-
model tokens.
|
|
101
|
+
model tokens. Claude Code sessions wake through the inbox that Claude Code itself opens, so
|
|
102
|
+
you start Claude Code with your ordinary command. A busy Claude Code session gets the message
|
|
103
|
+
between two tool calls. A busy Codex session gets it when the current turn ends.
|
|
102
104
|
|
|
103
105
|
See [client capabilities](docs/CAPABILITIES.md) for exact support. Run `acc doctor` from your
|
|
104
106
|
project if a peer is missing or delivery differs from what you expect; see
|
|
@@ -1,51 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// empty unless ACC_BOOTSTRAP_DEBUG=1 asks for one safe line.
|
|
8
|
-
import { checkNativeBootstrap } from "@agents-can-communicate/installer";
|
|
9
|
-
|
|
10
|
-
import { createAntigravityAdapter } from "@agents-can-communicate/adapter-antigravity";
|
|
11
|
-
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
12
|
-
import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
13
|
-
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
14
|
-
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
15
|
-
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
16
|
-
|
|
17
|
-
import { parseBootstrapOptions } from "@agents-can-communicate/cli/managed-entry";
|
|
18
|
-
|
|
19
|
-
function debug(line) {
|
|
20
|
-
if (process.env.ACC_BOOTSTRAP_DEBUG === "1") process.stderr.write(`acc-bootstrap: ${line}\n`);
|
|
21
|
-
}
|
|
22
|
-
|
|
2
|
+
// Kept only for installs made by ACC 0.7.x or earlier. A 0.7.x updater refuses a
|
|
3
|
+
// downloaded package without this file, and a `claude` shell shim written by
|
|
4
|
+
// 0.7.x still runs it through its launcher until `acc install` or `acc update`
|
|
5
|
+
// retires the shim. A shim reads any exit other than 0 as "launch the vendor
|
|
6
|
+
// command untouched", so this answers 1 and writes nothing.
|
|
23
7
|
export async function main() {
|
|
24
|
-
|
|
25
|
-
const options = parseBootstrapOptions(process.argv.slice(2));
|
|
26
|
-
if (options === null) {
|
|
27
|
-
debug("usage: --adapter <id> --real-executable <path> --data-home <path>");
|
|
28
|
-
process.exit(2);
|
|
29
|
-
}
|
|
30
|
-
const registry = {
|
|
31
|
-
claude_code: createClaudeCodeAdapter, codex: createCodexAdapter,
|
|
32
|
-
gemini_cli: createGeminiCliAdapter, grok: createGrokAdapter, kimi: createKimiAdapter,
|
|
33
|
-
// No native delivery contract: this reports the client unsupported rather
|
|
34
|
-
// than unknown, which is a different and more useful thing to read.
|
|
35
|
-
antigravity: createAntigravityAdapter,
|
|
36
|
-
};
|
|
37
|
-
const adapter = registry[options.adapter]?.();
|
|
38
|
-
if (adapter === undefined) {
|
|
39
|
-
debug(`${options.adapter}: unknown adapter`);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
const result = await checkNativeBootstrap({ adapter, realExecutable: options.realExecutable,
|
|
43
|
-
platform: `${process.platform}-${process.arch}`, dataHome: options.dataHome, timeoutMs: 750 });
|
|
44
|
-
debug(`${options.adapter}: ${result.supported ? "supported" : result.reasonCode}`);
|
|
45
|
-
process.exit(result.supported ? 0 : 1);
|
|
46
|
-
} catch (error) {
|
|
47
|
-
debug(`failed: ${String(error?.code ?? error?.name ?? "error")}`);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
8
|
+
process.exitCode = 1;
|
|
51
9
|
}
|
|
@@ -1,166 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
|
|
16
|
-
import { recordAndOffer } from "@agents-can-communicate/mcp-server";
|
|
17
|
-
import { resolveClientPid } from "@agents-can-communicate/hook-runner/client-pid";
|
|
18
|
-
import { readProcessTable } from "@agents-can-communicate/hook-runner/process-table";
|
|
19
|
-
import { createId } from "@agents-can-communicate/protocol";
|
|
20
|
-
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
21
|
-
import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
22
|
-
from "@agents-can-communicate/cli";
|
|
23
|
-
|
|
24
|
-
import { createAccChannel, endpointDir, routeAck, routeReply }
|
|
25
|
-
from "@agents-can-communicate/adapter-claude-code/channel";
|
|
26
|
-
|
|
27
|
-
import { pump, startInertChannel } from "./claude-channel-stdio.mjs";
|
|
28
|
-
import { activateClaudeChannel } from "./claude-channel-binding.mjs";
|
|
29
|
-
|
|
30
|
-
const clock = { now: () => new Date().toISOString() };
|
|
31
|
-
const ids = { next: kind => createId(kind, randomBytes) };
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* The ACC session this Claude process opened - decided by which client process
|
|
35
|
-
* this Channel is running under, never by how many sessions happen to be
|
|
36
|
-
* visible.
|
|
37
|
-
*
|
|
38
|
-
* There used to be a fallback here: if exactly one session was live, it was
|
|
39
|
-
* taken to be this one. On a single-session machine that looked safe, and with
|
|
40
|
-
* two sessions it silently handed one client the other's identity. The Channel
|
|
41
|
-
* starts while its own SessionStart hook is still writing its binding, so the
|
|
42
|
-
* only binding a second client can see at that moment is the *first* client's -
|
|
43
|
-
* and the wait that was added to survive that race then locks the wrong answer
|
|
44
|
-
* in on the first attempt, because it is a complete, live, plausible binding.
|
|
45
|
-
*
|
|
46
|
-
* Measured on two real 2.1.259 sessions: both Channels registered an endpoint
|
|
47
|
-
* under the first client's pid. `bindNativeSession` refuses two registrations
|
|
48
|
-
* for one pid, so the session that had been receiving live messages dropped
|
|
49
|
-
* back to the durable inbox - and an `acc_reply` from the second window would
|
|
50
|
-
* have been recorded as the first session's answer, which is a false record
|
|
51
|
-
* rather than a lost message.
|
|
52
|
-
*
|
|
53
|
-
* This process was spawned by its client, so its own ancestry names that
|
|
54
|
-
* client, and no race can change that. Ownership is decided by it.
|
|
55
|
-
*/
|
|
56
|
-
export async function resolveSession({ runtimeDir, service, env, ownClientPid,
|
|
57
|
-
listBindings = listSessionBindings }) {
|
|
58
|
-
if (!Number.isInteger(ownClientPid)) return null;
|
|
59
|
-
const bindings = await listBindings({ runtimeDir });
|
|
60
|
-
if (bindings.length === 0) return null;
|
|
61
|
-
const status = await service.collectStatus({});
|
|
62
|
-
const live = new Set(status.participants
|
|
63
|
-
.filter(participant => participant.presence !== "offline")
|
|
64
|
-
.map(participant => participant.sessionId));
|
|
65
|
-
const mine = bindings.filter(binding => live.has(binding.accSessionId)
|
|
66
|
-
&& binding.clientPid === ownClientPid);
|
|
67
|
-
// One client process can outlive a session and open another, so the pid alone
|
|
68
|
-
// can still name two. The harness session id the client exports separates
|
|
69
|
-
// them; with nothing to separate them the answer is nobody, as before.
|
|
70
|
-
const exported = new Set(Object.values(env ?? {}).filter(value => typeof value === "string"));
|
|
71
|
-
const matched = mine.filter(binding => exported.has(binding.harnessSessionId));
|
|
72
|
-
const chosen = mine.length === 1 ? mine[0] : matched.length === 1 ? matched[0] : null;
|
|
73
|
-
return chosen === null ? null
|
|
74
|
-
: { sessionId: chosen.accSessionId, generation: chosen.generation, clientPid: chosen.clientPid,
|
|
75
|
-
harnessSessionId: chosen.harnessSessionId };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The client process this Channel was spawned by, or null when nobody knows. */
|
|
79
|
-
export async function ownClient({ table = null, from = process.pid,
|
|
80
|
-
command = createClaudeCodeAdapter().client.command } = {}) {
|
|
81
|
-
return resolveClientPid({ table: table ?? await readProcessTable(), from, command });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// How long the Channel will wait for the hook to publish this session's
|
|
85
|
-
// binding. Claude spawns this child while SessionStart is still running - both
|
|
86
|
-
// landed in the same second on a real 2.1.259 launch - and a single lookup
|
|
87
|
-
// loses that race often enough to make native delivery activate or not per
|
|
88
|
-
// launch. Bounded, because a session with no ACC binding must still be answered.
|
|
89
|
-
const BINDING_WAIT_MS = 5_000;
|
|
90
|
-
const BINDING_POLL_MS = 100;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The session binding, once it exists - or null when the wait runs out.
|
|
94
|
-
*
|
|
95
|
-
* A binding with no `clientPid` names no process, and the Channel binds an
|
|
96
|
-
* exact one, so it does not count as resolved: the hook writes the identity
|
|
97
|
-
* first and the certified facts after, and reading in between must keep waiting
|
|
98
|
-
* rather than settle for the half-written answer.
|
|
99
|
-
*/
|
|
100
|
-
export async function resolveWithin({ resolve, deadline, intervalMs = BINDING_POLL_MS,
|
|
101
|
-
now = () => Date.now(), sleep = ms => new Promise(resolve_ => setTimeout(resolve_, ms)) }) {
|
|
102
|
-
for (;;) {
|
|
103
|
-
const session = await resolve();
|
|
104
|
-
if (session !== null && Number.isInteger(session?.clientPid)) return session;
|
|
105
|
-
if (now() >= deadline) return null;
|
|
106
|
-
await sleep(intervalMs);
|
|
2
|
+
// Kept only for installs made by ACC 0.7.x or earlier, which wired a Claude Channel MCP
|
|
3
|
+
// server. A 0.7.x updater refuses a downloaded package without this file, and a
|
|
4
|
+
// plugin copy written by 0.7.x can still ask Claude Code to spawn it until
|
|
5
|
+
// `acc install` or `acc update` lays the plugin out again.
|
|
6
|
+
//
|
|
7
|
+
// A child that answers nothing is reported to the user as a server that failed
|
|
8
|
+
// to connect, so this finishes the MCP handshake, declares no Channel and
|
|
9
|
+
// offers no tools, and exits when Claude closes its stdin or stops it with a signal.
|
|
10
|
+
|
|
11
|
+
function answer(message) {
|
|
12
|
+
if (message.method === "initialize") {
|
|
13
|
+
return { protocolVersion: message.params?.protocolVersion, capabilities: { tools: {} },
|
|
14
|
+
serverInfo: { name: "agents-can-communicate", version: "0.2.0" } };
|
|
107
15
|
}
|
|
16
|
+
if (message.method === "tools/list") return { tools: [] };
|
|
17
|
+
if (message.method === "ping") return {};
|
|
18
|
+
return undefined;
|
|
108
19
|
}
|
|
109
20
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const paths = runtimePaths({ dataHome, workspaceId: descriptor.id, workspaceRoots: descriptor.roots });
|
|
115
|
-
const store = await openFilesystemStore({ root: paths.root, clock, ids,
|
|
116
|
-
workspaceId: descriptor.id });
|
|
117
|
-
const service = createCoordinationService({ store, clock, ids });
|
|
118
|
-
const write = payload => process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
119
|
-
// Read once: the ancestry that decides ownership cannot change while this
|
|
120
|
-
// process runs, and re-reading it on every poll would spawn `ps` in a loop.
|
|
121
|
-
const clientPid = await ownClient();
|
|
122
|
-
const session = await resolveWithin({
|
|
123
|
-
resolve: () => resolveSession({ runtimeDir: paths.root, service, env: process.env,
|
|
124
|
-
ownClientPid: clientPid }),
|
|
125
|
-
deadline: Date.now() + BINDING_WAIT_MS });
|
|
126
|
-
// No binding to serve - but Claude is already speaking MCP to this child, and
|
|
127
|
-
// returning here left the event loop empty: the process exited in 75ms
|
|
128
|
-
// without answering `initialize`, and Claude reported the server as failed to
|
|
129
|
-
// connect on every session that enables the plugin without ACC's shim.
|
|
130
|
-
if (session === null || !Number.isInteger(session.clientPid)) {
|
|
131
|
-
return startInertChannel({ write });
|
|
21
|
+
function handle(line) {
|
|
22
|
+
let message;
|
|
23
|
+
try { message = JSON.parse(line); } catch {
|
|
24
|
+
return { jsonrpc: "2.0", error: { code: -32700, message: "parse error" } };
|
|
132
25
|
}
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
clientPid: session.clientPid,
|
|
139
|
-
write,
|
|
140
|
-
// The Channel tool records through its own bound session, then uses the
|
|
141
|
-
// same durable-first offer as ordinary MCP. A successful reply record alone
|
|
142
|
-
// does not deliver it to the peer, and a failed offer must not lose it.
|
|
143
|
-
routeReply: ({ messageId, body }) => recordAndOffer({ router: deliveryRouter,
|
|
144
|
-
selectMessage: value => value.reply,
|
|
145
|
-
record: () => routeReply({ service, session, messageId, body }) }),
|
|
146
|
-
routeAck: ({ messageId }) => routeAck({ service, session, messageId }),
|
|
147
|
-
// This process is the only party that knows the endpoint is still being
|
|
148
|
-
// served, and Claude publishes no heartbeat, so nothing else would move
|
|
149
|
-
// ACC's lease while the session sits idle - which is when live delivery is
|
|
150
|
-
// worth having.
|
|
151
|
-
refreshBinding: leaseUntil => service.refreshDeliveryBinding({
|
|
152
|
-
sessionId: session.sessionId, generation: session.generation, leaseUntil }),
|
|
153
|
-
});
|
|
154
|
-
await channel.listen();
|
|
155
|
-
pump(channel.handleLine, () => { channel.close(); process.exit(0); });
|
|
156
|
-
// Serve MCP immediately; a slow or failed ACC binding must not stop the
|
|
157
|
-
// client connecting. The listening endpoint lets an in-flight hook finish
|
|
158
|
-
// before this owner acquires the same lifecycle lock and completes startup.
|
|
159
|
-
await activateClaudeChannel({ session, service, runtimeDir: paths.root, dataHome,
|
|
160
|
-
env: process.env }).catch(() => {});
|
|
161
|
-
return channel;
|
|
26
|
+
if (message.id === undefined || message.id === null) return null;
|
|
27
|
+
const result = answer(message);
|
|
28
|
+
return result === undefined
|
|
29
|
+
? { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `unknown method: ${message.method}` } }
|
|
30
|
+
: { jsonrpc: "2.0", id: message.id, result };
|
|
162
31
|
}
|
|
163
32
|
|
|
164
33
|
export async function main() {
|
|
165
|
-
|
|
34
|
+
let buffer = "";
|
|
35
|
+
process.stdin.setEncoding("utf8");
|
|
36
|
+
process.stdin.on("data", chunk => {
|
|
37
|
+
buffer += chunk;
|
|
38
|
+
let newline = buffer.indexOf("\n");
|
|
39
|
+
while (newline !== -1) {
|
|
40
|
+
const line = buffer.slice(0, newline).trim();
|
|
41
|
+
buffer = buffer.slice(newline + 1);
|
|
42
|
+
newline = buffer.indexOf("\n");
|
|
43
|
+
const reply = line === "" ? null : handle(line);
|
|
44
|
+
if (reply !== null) process.stdout.write(`${JSON.stringify(reply)}\n`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
await new Promise(resolve => {
|
|
48
|
+
process.stdin.once("end", resolve);
|
|
49
|
+
process.once("SIGTERM", resolve);
|
|
50
|
+
process.once("SIGINT", resolve);
|
|
51
|
+
process.stdin.resume();
|
|
52
|
+
});
|
|
53
|
+
// A signal can arrive while Claude still holds the pipe open; a stdin left
|
|
54
|
+
// reading would keep this process alive after main() returns.
|
|
55
|
+
process.stdin.destroy();
|
|
166
56
|
}
|
|
@@ -54,7 +54,6 @@ await serve({
|
|
|
54
54
|
context: {
|
|
55
55
|
service,
|
|
56
56
|
deliveryRouter: createDeliveryRouter({ service, adapters, clock,
|
|
57
|
-
platform: `${process.platform}-${process.arch}`,
|
|
58
57
|
readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
|
|
59
58
|
adapterId: adapter.id }) }),
|
|
60
59
|
workspaceId: descriptor.id,
|
package/bin/entrypoints/acc.mjs
CHANGED
|
@@ -25,7 +25,6 @@ const runtime = {
|
|
|
25
25
|
ids: { next: kind => createId(kind, randomBytes) },
|
|
26
26
|
createDeliveryRouter: ({ service, clock, dataHome }) =>
|
|
27
27
|
createDeliveryRouter({ service, adapters, clock,
|
|
28
|
-
platform: `${process.platform}-${process.arch}`,
|
|
29
28
|
readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
|
|
30
29
|
adapterId: adapter.id }) }),
|
|
31
30
|
// Asked only by `acc config init`, and only when stdout is a terminal. There
|
|
@@ -81,10 +81,15 @@ is not evidence for compaction. Counts add no ids to `offeredMessageIds` or
|
|
|
81
81
|
|
|
82
82
|
`client.command` does double duty. `detect.mjs` uses it as the version-probe binary, and
|
|
83
83
|
presence liveness separately walks the hook's process ancestry for the first ancestor whose
|
|
84
|
-
executable basename matches it, to learn the client's own pid.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
executable basename matches it, to learn the client's own pid. A client shipped as a node
|
|
85
|
+
script, such as Gemini CLI, runs as `node`; for a `node` ancestor the walk also matches the
|
|
86
|
+
script it runs: the first argument after any interpreter options, by basename, with a
|
|
87
|
+
`.js`, `.mjs` or `.cjs` extension ignored. A word right after an option written without `=`
|
|
88
|
+
may be that option's value (`--require ./preload.cjs`), so it is also tried and the script
|
|
89
|
+
after it still counts. Declare the name the client actually runs as. A
|
|
90
|
+
command that matches neither resolves nothing, and the failure is silent: the session gets
|
|
91
|
+
`pid: null`, falls back to reading presence by age alone, and is not pinned to its
|
|
92
|
+
generation, with nothing telling you why.
|
|
88
93
|
|
|
89
94
|
## Declare only proven capabilities
|
|
90
95
|
|
|
@@ -148,35 +153,71 @@ to its capabilities:
|
|
|
148
153
|
|
|
149
154
|
```js
|
|
150
155
|
nativeDelivery: {
|
|
151
|
-
|
|
152
|
-
anchors: [{
|
|
153
|
-
protocolContract: "claude-code-channel-mcp-v1" }],
|
|
156
|
+
minimum: "2.1.282",
|
|
157
|
+
anchors: [{ version: "2.1.282", protocolContract: "claude-code-inbox-socket-v1" }],
|
|
154
158
|
knownBad: [],
|
|
155
|
-
activationKinds: ["
|
|
159
|
+
activationKinds: ["native-service"],
|
|
160
|
+
offerKind: "wake",
|
|
156
161
|
}
|
|
157
162
|
```
|
|
158
163
|
|
|
159
164
|
The rules `defineAdapter` enforces, and the ones the runtime applies:
|
|
160
165
|
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
`
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
166
|
+
- The minimum is the **first passing capture**, never a guessed first vendor release. Every
|
|
167
|
+
anchor must have passing `delivery.livePush` evidence for the same client and version, and
|
|
168
|
+
the minimum must itself be an anchor. The platform an evidence row names is where the
|
|
169
|
+
capture was taken; it is provenance, and the anchor names no platform.
|
|
170
|
+
- There is intentionally **no maximum version** and **no platform**. A newer release is
|
|
171
|
+
admitted, on every platform, only when a current read-only feature probe
|
|
172
|
+
(`probeNativeDelivery()`) and a per-session handshake (`bindNativeSession()`) both report
|
|
173
|
+
the anchored `protocolContract`; `evaluateNativeEligibility()` and
|
|
174
|
+
`validateNativeHandshake()` are those two checks.
|
|
175
|
+
- A prerelease is judged by its release triple, as hook capabilities are: `2.2.0-beta.1` is
|
|
176
|
+
`2.2.0` against the minimum and `knownBad`. `knownBad` names exact versions or inclusive
|
|
177
|
+
intervals. A version that cannot be read at all is `version_unavailable`.
|
|
178
|
+
- Certification evidence still governs every non-native capability;
|
|
171
179
|
`effectiveCapabilities()` is unchanged. The native rule is used for live delivery alone.
|
|
180
|
+
|
|
181
|
+
What may refuse live delivery is exactly three things:
|
|
182
|
+
|
|
183
|
+
- the machine's own probe or per-session handshake failing;
|
|
184
|
+
- a client older than the first passing capture;
|
|
185
|
+
- a regression recorded in `knownBad`.
|
|
186
|
+
|
|
187
|
+
Nothing else: not the platform, not a prerelease suffix, not where the evidence was taken. A
|
|
188
|
+
transport that is technically different on some platform is the probe's fact to report, as
|
|
189
|
+
`native_delivery_unsupported`, never a gap in the captures. The 0.8.0 shape,
|
|
190
|
+
`minimumByPlatform` with a platform on each anchor, is refused by name; the design record is
|
|
191
|
+
`docs/design/2026-09-26-native-delivery-across-platforms.md` in the repository.
|
|
172
192
|
- Native methods return closed facts (`validateNativeActivationPlan()` closes the
|
|
173
193
|
activation plan) and never put vendor data - endpoints, sockets, raw errors - into core.
|
|
174
194
|
|
|
175
|
-
`nativeDelivery.policySource`
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
`nativeDelivery.policySource` accepts only `"installation-record"`, which is also the
|
|
196
|
+
default. Hooks and senders read the current recorded recipient consent, independently of
|
|
197
|
+
the vendor process environment. Installation records the requested policy even when
|
|
198
|
+
current activation is unavailable. An absent or invalid record means `off`. A previously
|
|
199
|
+
published binding cannot override it.
|
|
200
|
+
|
|
201
|
+
`nativeDelivery.offerKind` says what an accepted offer puts in front of the model. It is
|
|
202
|
+
`"message"` (the default) or `"wake"`:
|
|
203
|
+
|
|
204
|
+
- `"message"` carries the body. The router records the receipt as `offered` with the
|
|
205
|
+
transport name and returns the outcome `offered`.
|
|
206
|
+
- `"wake"` carries only a notice that makes the client run a turn. The router records no
|
|
207
|
+
offer and returns the outcome `woken`. The receipt stays `queued`. The next-turn hook
|
|
208
|
+
records `offered` via `next-turn` after its output carried the body. Use `"wake"` only
|
|
209
|
+
where the turn that the wake starts runs the client's next-turn hook, as Claude Code's
|
|
210
|
+
`UserPromptSubmit` does. When the client takes the wake but holds it for its user's
|
|
211
|
+
approval, return `pendingApproval: true` with the acceptance; the outcome carries it so
|
|
212
|
+
the sender is told the truth.
|
|
213
|
+
|
|
214
|
+
A failed offer of either kind is recorded as a failed offer and leaves the receipt queued.
|
|
215
|
+
|
|
216
|
+
`nativeDelivery.activationKinds` names the mechanisms the adapter may plan:
|
|
217
|
+
`"native-config"` or `"native-service"`. A `native-service` mechanism with
|
|
218
|
+
`preExisting: true` and no commands describes a service that the client itself runs, such
|
|
219
|
+
as the Claude Code inbox. A `shell-bootstrap` mechanism can no longer be planned. Install
|
|
220
|
+
records of one stay readable, so that `acc install` and `acc update` can retire it.
|
|
180
221
|
|
|
181
222
|
The optional `refreshNativeSession()` method may revalidate an expired lease before
|
|
182
223
|
an offer. It returns the same closed handshake shape as `bindNativeSession()` and
|
|
@@ -204,7 +245,7 @@ earlier failures without duplicating a certification tuple.
|
|
|
204
245
|
|
|
205
246
|
Installed hook wiring may reach tier 2, but each effective hook capability still
|
|
206
247
|
requires exact client/version/platform evidence. Tier 3 has separate native
|
|
207
|
-
contracts: Claude Code
|
|
248
|
+
contracts: the Claude Code inbox wake from 2.1.282 and Codex LocalDaemon from 0.152.1 on
|
|
208
249
|
macOS arm64, with current probes and exact session handshakes. Codex's earlier
|
|
209
250
|
remote-wrapper failure remains historical evidence. Ordinary launch now preserves
|
|
210
251
|
workspace identity; the full installed product matrix, including negative controls
|
|
@@ -220,9 +261,18 @@ return normalizedEvent({
|
|
|
220
261
|
kind, sessionId, cwd, model, parentSessionId, tool,
|
|
221
262
|
targets, // paths this call would WRITE. For a shell call, pass the command to
|
|
222
263
|
// shellWriteTargets() — it reads write positions only, never reads.
|
|
264
|
+
permissionMode, // the client's own name for the session's permission mode, or null
|
|
265
|
+
endReason, // on sessionEnd, the client's own word for why, or null
|
|
223
266
|
});
|
|
224
267
|
```
|
|
225
268
|
|
|
269
|
+
`permissionMode` and `endReason` are optional and default to `null`. Pass `permissionMode`
|
|
270
|
+
when the client's hook reports one: a native binding reads it to tell a sender whether the
|
|
271
|
+
client will hold a wake for approval. Claude Code reports it on most hooks but not on
|
|
272
|
+
`SessionStart`. Pass `endReason` on `sessionEnd` when the client says why, as Claude Code's
|
|
273
|
+
`reason` does (`"clear"` after `/clear`). ACC keeps it in the closed session record's
|
|
274
|
+
`extensions`, and a sender is told that the conversation was cleared.
|
|
275
|
+
|
|
226
276
|
Refuse an unrecognised payload. Inventing a session attaches the wrong one, or a new one
|
|
227
277
|
every hook, and looks like it is working.
|
|
228
278
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -48,6 +48,11 @@ The order is part of the public guarantee:
|
|
|
48
48
|
4. ask one adapter to cross its transport boundary;
|
|
49
49
|
5. only after acceptance, commit `offered` and an immutable success event.
|
|
50
50
|
|
|
51
|
+
An adapter whose contract declares `offerKind: "wake"` puts only a notice in front of the
|
|
52
|
+
client, never the body. The router records no offer for an accepted wake and returns the
|
|
53
|
+
outcome `woken`. The receipt stays `queued` until the recipient's next-turn hook output
|
|
54
|
+
carried the body. That hook then commits `offered` via `next-turn`.
|
|
55
|
+
|
|
51
56
|
A failed offer records the selected recipient session id and generation with a safe
|
|
52
57
|
diagnostic code, then leaves the receipt queued. Core validates that target provenance and
|
|
53
58
|
derives the event actor from it. A crash after bytes cross the boundary but before the
|
|
@@ -78,12 +83,16 @@ or projected into agent context; session restart replaces it and SessionEnd atte
|
|
|
78
83
|
cleanup. Doctor ignores closed or superseded generations even if cleanup could not finish. Diagnostic disk I/O runs in an
|
|
79
84
|
unreferenced worker, so a stalled write cannot hold the hook process open.
|
|
80
85
|
|
|
81
|
-
Claude's
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
Claude Code's inbox binding runs inside ACC's own hooks, with no ACC process in the
|
|
87
|
+
session. Each turn hook reads the session's inbox socket from `CLAUDE_CODE_MESSAGING_SOCKET`
|
|
88
|
+
and verifies it against Claude Code's session registry `<config>/sessions/<pid>.json`. The
|
|
89
|
+
pid and the socket must match; the session id is checked from the first offer on, because
|
|
90
|
+
Claude Code rewrites the entry for a `/resume` only after SessionStart. The hook then writes
|
|
91
|
+
a private endpoint record and publishes the binding with a 120-second lease. The router refreshes an expired
|
|
92
|
+
lease with the same registry check, so an idle session stays reachable between turns. The
|
|
93
|
+
adapter repeats the check before each offer. A registry that disappeared, or that names
|
|
94
|
+
another session or socket, refuses the offer, so a wake never reaches a reused pid. This
|
|
95
|
+
does not create a session or revive a closed one.
|
|
87
96
|
|
|
88
97
|
## Certified capability versus current reachability
|
|
89
98
|
|
|
@@ -96,14 +105,16 @@ a third certification check. A binding says what that current generation
|
|
|
96
105
|
exposes and whether its lease is current, while recipient policy says whether it may spend a
|
|
97
106
|
turn.
|
|
98
107
|
|
|
99
|
-
Claude Code
|
|
100
|
-
|
|
101
|
-
the agent starts in its own shell. All three use a captured
|
|
102
|
-
current probe and exact session handshake
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
and
|
|
106
|
-
|
|
108
|
+
The Claude Code inbox wake has installed-client evidence on 2.1.282. Codex LocalDaemon
|
|
109
|
+
has it on 0.152.1 and 0.153.4. Antigravity CLI has it on 1.2.7 and later through a relay
|
|
110
|
+
the agent starts in its own shell. All three use a captured minimum that applies on every
|
|
111
|
+
platform, a current probe and an exact session handshake; a prerelease is judged by its
|
|
112
|
+
release triple. Claude Code and Codex preserve the ordinary
|
|
113
|
+
client launch. Claude Code verifies its session registry, and Codex verifies its registered
|
|
114
|
+
thread and workspace. Native delivery owns no vendor daemon lifecycle. Gemini CLI and Kimi
|
|
115
|
+
Code have exact-version next-turn evidence only. Grok and generic MCP use inbox polling.
|
|
116
|
+
Antigravity CLI additionally attaches a session only where the client reports an open
|
|
117
|
+
workspace.
|
|
107
118
|
|
|
108
119
|
## Storage and workspace identity
|
|
109
120
|
|
|
@@ -216,10 +227,12 @@ modes. It preserves explicit declarations and does not rewrite live leases. Miss
|
|
|
216
227
|
changed generation files and unknown native bindings remain unverified holds.
|
|
217
228
|
A session is also pinned to the generation
|
|
218
229
|
it started with: its hooks delegate to that generation while the pin declares the same
|
|
219
|
-
store contract this one does, and run the active generation otherwise.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
store contract this one does, and run the active generation otherwise. Apart from
|
|
231
|
+
`SessionEnd`, only the client's confirmed death releases a pin, so a session whose client
|
|
232
|
+
process cannot be found is not pinned, and a pin that names no client is reaped. A
|
|
233
|
+
generation directory is reclaimed once no control pointer, live lease, live pin or staging
|
|
234
|
+
hold names it. Parsed help, version, and update recovery remain available when workspace
|
|
235
|
+
admission is unavailable.
|
|
223
236
|
|
|
224
237
|
An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
|
|
225
238
|
stable package identity and the discovered integrity, and health-checks the staged runtime.
|