agents-can-communicate 0.2.0 → 0.3.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 +17 -7
- package/bin/acc-bootstrap.mjs +56 -0
- package/bin/acc-claude-channel.mjs +177 -0
- package/bin/acc.mjs +7 -2
- package/docs/ADAPTER_AUTHORING.md +34 -2
- package/docs/CAPABILITIES.md +25 -10
- package/docs/CLI.md +10 -3
- package/docs/CONFIGURATION.md +4 -0
- package/docs/HOW_IT_WORKS.md +277 -0
- package/docs/PROTOCOL.md +1 -1
- package/docs/RELEASING.md +7 -1
- package/docs/TROUBLESHOOTING.md +7 -1
- package/docs/index.md +3 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +196 -46
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +237 -45
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +3 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +34 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -5
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +171 -38
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +3 -1
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +4 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +42 -7
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +44 -28
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeAgent.json → BeforeAgent-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool.json → BeforeTool-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool-shell.json → BeforeTool-shell-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionEnd.json → SessionEnd-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionStart.json → SessionStart-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +266 -39
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +6 -6
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +25 -11
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +18 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +2 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +6 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +17 -2
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +2 -2
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +67 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +103 -11
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +52 -2
- package/node_modules/@agents-can-communicate/core/src/service.mjs +10 -0
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +26 -12
- package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +123 -90
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +48 -4
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +72 -3
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +7 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -2
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +41 -6
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +18 -3
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -79,12 +79,21 @@ Messages commit before any delivery attempt. `queued`, `offered`, `retrieved`, a
|
|
|
79
79
|
not proof of model attention, and a reply resolves communication rather than proving a
|
|
80
80
|
requested task finished.
|
|
81
81
|
|
|
82
|
-
Durable inbox recovery is the baseline for every client. Certified next-turn injection
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
Durable inbox recovery is the baseline for every client. Certified next-turn injection
|
|
83
|
+
reduces the polling for the exact captured client version and platform. Native live push is
|
|
84
|
+
opt-in, experimental, and off until you choose it: **Claude Code 2.1.258 and Codex 0.152.1
|
|
85
|
+
on macOS arm64 have passing live-push captures** and deliver an addressed message into a
|
|
86
|
+
running session through the vendor's own transport, falling back to the durable inbox on any
|
|
87
|
+
failure. Every other client, and any client below the captured minimum, stays next-turn or
|
|
88
|
+
inbox only. The retained failed captures are shipped beside the passing ones as honest
|
|
89
|
+
evidence.
|
|
90
|
+
|
|
91
|
+
| Client | Native delivery |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Claude Code 2.1.258 (darwin-arm64) | experimental Channel: idle offer, queue-after-turn, native reply, off until opt-in |
|
|
94
|
+
| Codex 0.152.1 (darwin-arm64) | experimental App Server queue: idle/after-turn, reply via `acc reply`, off until opt-in |
|
|
95
|
+
| Gemini CLI, Kimi Code | next-turn or inbox only; no captured transparent transport |
|
|
96
|
+
| Grok | awaiting a compatibility capture; inbox only |
|
|
88
97
|
|
|
89
98
|
## What ACC owns
|
|
90
99
|
|
|
@@ -101,7 +110,8 @@ be guaranteed.
|
|
|
101
110
|
|
|
102
111
|
## Documentation
|
|
103
112
|
|
|
104
|
-
Start with [Getting started](docs/GETTING_STARTED.md), then
|
|
113
|
+
Start with [Getting started](docs/GETTING_STARTED.md), then read
|
|
114
|
+
[How ACC works](docs/HOW_IT_WORKS.md) for the end-to-end engineering tour or use the
|
|
105
115
|
[documentation map](docs/index.md). The exact surfaces are in the [CLI](docs/CLI.md),
|
|
106
116
|
[MCP](docs/MCP.md), [Protocol](docs/PROTOCOL.md), and
|
|
107
117
|
[Capabilities](docs/CAPABILITIES.md) references. Adapter evidence lives beside each
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Internal entry point behind an ACC-owned shell shim: is this exact vendor
|
|
3
|
+
// executable eligible for native delivery right now? Exit 0 means yes and the
|
|
4
|
+
// shim exports its owned policy before `exec`; any other exit - including a
|
|
5
|
+
// crash, a missing adapter, or a damaged install - means the shim launches the
|
|
6
|
+
// vendor command untouched. Nothing is ever written to stdout, and stderr stays
|
|
7
|
+
// empty unless ACC_BOOTSTRAP_DEBUG=1 asks for one safe line.
|
|
8
|
+
import { checkNativeBootstrap } from "@agents-can-communicate/installer";
|
|
9
|
+
|
|
10
|
+
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
11
|
+
import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
12
|
+
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
13
|
+
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
14
|
+
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
15
|
+
|
|
16
|
+
const FLAGS = new Map([["--adapter", "adapter"], ["--real-executable", "realExecutable"],
|
|
17
|
+
["--data-home", "dataHome"]]);
|
|
18
|
+
|
|
19
|
+
function debug(line) {
|
|
20
|
+
if (process.env.ACC_BOOTSTRAP_DEBUG === "1") process.stderr.write(`acc-bootstrap: ${line}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parse(args) {
|
|
24
|
+
const options = {};
|
|
25
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
26
|
+
const key = FLAGS.get(args[index]);
|
|
27
|
+
const value = args[index + 1];
|
|
28
|
+
if (key === undefined || typeof value !== "string" || value === "" || key in options) return null;
|
|
29
|
+
options[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return FLAGS.values().every(key => key in options) ? options : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const options = parse(process.argv.slice(2));
|
|
36
|
+
if (options === null) {
|
|
37
|
+
debug("usage: --adapter <id> --real-executable <path> --data-home <path>");
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
const registry = {
|
|
41
|
+
claude_code: createClaudeCodeAdapter, codex: createCodexAdapter,
|
|
42
|
+
gemini_cli: createGeminiCliAdapter, grok: createGrokAdapter, kimi: createKimiAdapter,
|
|
43
|
+
};
|
|
44
|
+
const adapter = registry[options.adapter]?.();
|
|
45
|
+
if (adapter === undefined) {
|
|
46
|
+
debug(`${options.adapter}: unknown adapter`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const result = await checkNativeBootstrap({ adapter, realExecutable: options.realExecutable,
|
|
50
|
+
platform: `${process.platform}-${process.arch}`, dataHome: options.dataHome, timeoutMs: 750 });
|
|
51
|
+
debug(`${options.adapter}: ${result.supported ? "supported" : result.reasonCode}`);
|
|
52
|
+
process.exit(result.supported ? 0 : 1);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
debug(`failed: ${String(error?.code ?? error?.name ?? "error")}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The ACC Channel binary Claude Code spawns as a stdio MCP child. It composes
|
|
3
|
+
// the same workspace discovery, session binding, core service, and local
|
|
4
|
+
// data-home rules as acc-hook, opens a session-scoped endpoint for the delivery
|
|
5
|
+
// router, and turns the model's explicit acc_reply / acc_ack into real ACC
|
|
6
|
+
// records. It never collects a transcript or runs a model, and it fails open:
|
|
7
|
+
// any composition error leaves Claude a plain MCP server that simply offers no
|
|
8
|
+
// tools rather than crashing the session.
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { realpathSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
15
|
+
import { listSessionBindings } from "@agents-can-communicate/adapter-sdk";
|
|
16
|
+
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
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 { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
22
|
+
from "@agents-can-communicate/cli";
|
|
23
|
+
|
|
24
|
+
import { createAccChannel, createInertChannel, endpointDir, routeAck, routeReply }
|
|
25
|
+
from "@agents-can-communicate/adapter-claude-code/channel";
|
|
26
|
+
|
|
27
|
+
const clock = { now: () => new Date().toISOString() };
|
|
28
|
+
const ids = { next: kind => createId(kind, randomBytes) };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The ACC session this Claude process opened - decided by which client process
|
|
32
|
+
* this Channel is running under, never by how many sessions happen to be
|
|
33
|
+
* visible.
|
|
34
|
+
*
|
|
35
|
+
* There used to be a fallback here: if exactly one session was live, it was
|
|
36
|
+
* taken to be this one. On a single-session machine that looked safe, and with
|
|
37
|
+
* two sessions it silently handed one client the other's identity. The Channel
|
|
38
|
+
* starts while its own SessionStart hook is still writing its binding, so the
|
|
39
|
+
* only binding a second client can see at that moment is the *first* client's -
|
|
40
|
+
* and the wait that was added to survive that race then locks the wrong answer
|
|
41
|
+
* in on the first attempt, because it is a complete, live, plausible binding.
|
|
42
|
+
*
|
|
43
|
+
* Measured on two real 2.1.259 sessions: both Channels registered an endpoint
|
|
44
|
+
* under the first client's pid. `bindNativeSession` refuses two registrations
|
|
45
|
+
* for one pid, so the session that had been receiving live messages dropped
|
|
46
|
+
* back to the durable inbox - and an `acc_reply` from the second window would
|
|
47
|
+
* have been recorded as the first session's answer, which is a false record
|
|
48
|
+
* rather than a lost message.
|
|
49
|
+
*
|
|
50
|
+
* This process was spawned by its client, so its own ancestry names that
|
|
51
|
+
* client, and no race can change that. Ownership is decided by it.
|
|
52
|
+
*/
|
|
53
|
+
export async function resolveSession({ runtimeDir, service, env, ownClientPid,
|
|
54
|
+
listBindings = listSessionBindings }) {
|
|
55
|
+
if (!Number.isInteger(ownClientPid)) return null;
|
|
56
|
+
const bindings = await listBindings({ runtimeDir });
|
|
57
|
+
if (bindings.length === 0) return null;
|
|
58
|
+
const status = await service.collectStatus({});
|
|
59
|
+
const live = new Set(status.participants
|
|
60
|
+
.filter(participant => participant.presence !== "offline")
|
|
61
|
+
.map(participant => participant.sessionId));
|
|
62
|
+
const mine = bindings.filter(binding => live.has(binding.accSessionId)
|
|
63
|
+
&& binding.clientPid === ownClientPid);
|
|
64
|
+
// One client process can outlive a session and open another, so the pid alone
|
|
65
|
+
// can still name two. The harness session id the client exports separates
|
|
66
|
+
// them; with nothing to separate them the answer is nobody, as before.
|
|
67
|
+
const exported = new Set(Object.values(env ?? {}).filter(value => typeof value === "string"));
|
|
68
|
+
const matched = mine.filter(binding => exported.has(binding.harnessSessionId));
|
|
69
|
+
const chosen = mine.length === 1 ? mine[0] : matched.length === 1 ? matched[0] : null;
|
|
70
|
+
return chosen === null ? null
|
|
71
|
+
: { sessionId: chosen.accSessionId, generation: chosen.generation, clientPid: chosen.clientPid };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The client process this Channel was spawned by, or null when nobody knows. */
|
|
75
|
+
export async function ownClient({ table = null, from = process.pid,
|
|
76
|
+
command = createClaudeCodeAdapter().client.command } = {}) {
|
|
77
|
+
return resolveClientPid({ table: table ?? await readProcessTable(), from, command });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// How long the Channel will wait for the hook to publish this session's
|
|
81
|
+
// binding. Claude spawns this child while SessionStart is still running - both
|
|
82
|
+
// landed in the same second on a real 2.1.259 launch - and a single lookup
|
|
83
|
+
// loses that race often enough to make native delivery activate or not per
|
|
84
|
+
// launch. Bounded, because a session with no ACC binding must still be answered.
|
|
85
|
+
const BINDING_WAIT_MS = 5_000;
|
|
86
|
+
const BINDING_POLL_MS = 100;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The session binding, once it exists - or null when the wait runs out.
|
|
90
|
+
*
|
|
91
|
+
* A binding with no `clientPid` names no process, and the Channel binds an
|
|
92
|
+
* exact one, so it does not count as resolved: the hook writes the identity
|
|
93
|
+
* first and the certified facts after, and reading in between must keep waiting
|
|
94
|
+
* rather than settle for the half-written answer.
|
|
95
|
+
*/
|
|
96
|
+
export async function resolveWithin({ resolve, deadline, intervalMs = BINDING_POLL_MS,
|
|
97
|
+
now = () => Date.now(), sleep = ms => new Promise(resolve_ => setTimeout(resolve_, ms)) }) {
|
|
98
|
+
for (;;) {
|
|
99
|
+
const session = await resolve();
|
|
100
|
+
if (session !== null && Number.isInteger(session?.clientPid)) return session;
|
|
101
|
+
if (now() >= deadline) return null;
|
|
102
|
+
await sleep(intervalMs);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main() {
|
|
107
|
+
const descriptor = await discoverWorkspace({ cwd: process.cwd(), env: process.env,
|
|
108
|
+
gitProbe: createGitProbe() });
|
|
109
|
+
const paths = runtimePaths({ dataHome: platformDataHome({ platform: process.platform,
|
|
110
|
+
env: process.env }), workspaceId: descriptor.id, workspaceRoots: descriptor.roots });
|
|
111
|
+
const store = await openFilesystemStore({ root: paths.root, clock, ids,
|
|
112
|
+
workspaceId: descriptor.id });
|
|
113
|
+
const service = createCoordinationService({ store, clock, ids });
|
|
114
|
+
const write = payload => process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
115
|
+
// Read once: the ancestry that decides ownership cannot change while this
|
|
116
|
+
// process runs, and re-reading it on every poll would spawn `ps` in a loop.
|
|
117
|
+
const clientPid = await ownClient();
|
|
118
|
+
const session = await resolveWithin({
|
|
119
|
+
resolve: () => resolveSession({ runtimeDir: paths.root, service, env: process.env,
|
|
120
|
+
ownClientPid: clientPid }),
|
|
121
|
+
deadline: Date.now() + BINDING_WAIT_MS });
|
|
122
|
+
// No binding to serve - but Claude is already speaking MCP to this child, and
|
|
123
|
+
// returning here left the event loop empty: the process exited in 75ms
|
|
124
|
+
// without answering `initialize`, and Claude reported the server as failed to
|
|
125
|
+
// connect on every session that enables the plugin without ACC's shim.
|
|
126
|
+
if (session === null || !Number.isInteger(session.clientPid)) {
|
|
127
|
+
const inert = createInertChannel({ write });
|
|
128
|
+
pump(inert.handleLine, () => process.exit(0));
|
|
129
|
+
return inert;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const channel = createAccChannel({
|
|
133
|
+
endpointDir: endpointDir(paths.root),
|
|
134
|
+
clientPid: session.clientPid,
|
|
135
|
+
write,
|
|
136
|
+
routeReply: ({ messageId, body }) => routeReply({ service, session, messageId, body }),
|
|
137
|
+
routeAck: ({ messageId }) => routeAck({ service, session, messageId }),
|
|
138
|
+
// This process is the only party that knows the endpoint is still being
|
|
139
|
+
// served, and Claude publishes no heartbeat, so nothing else would move
|
|
140
|
+
// ACC's lease while the session sits idle - which is when live delivery is
|
|
141
|
+
// worth having.
|
|
142
|
+
refreshBinding: leaseUntil => service.refreshDeliveryBinding({
|
|
143
|
+
sessionId: session.sessionId, generation: session.generation, leaseUntil }),
|
|
144
|
+
});
|
|
145
|
+
await channel.listen();
|
|
146
|
+
pump(channel.handleLine, () => { channel.close(); process.exit(0); });
|
|
147
|
+
return channel;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** One line-delimited JSON pump, for the bound server and the unbound one alike. */
|
|
151
|
+
function pump(handleLine, shutdown) {
|
|
152
|
+
let buffer = "";
|
|
153
|
+
process.stdin.setEncoding("utf8");
|
|
154
|
+
process.stdin.on("data", async chunk => {
|
|
155
|
+
buffer += chunk;
|
|
156
|
+
let newline = buffer.indexOf("\n");
|
|
157
|
+
while (newline !== -1) {
|
|
158
|
+
const line = buffer.slice(0, newline).trim();
|
|
159
|
+
buffer = buffer.slice(newline + 1);
|
|
160
|
+
newline = buffer.indexOf("\n");
|
|
161
|
+
if (line !== "") await handleLine(line);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
process.stdin.on("end", shutdown);
|
|
165
|
+
process.once("SIGTERM", shutdown);
|
|
166
|
+
process.once("SIGINT", shutdown);
|
|
167
|
+
// Keeps the child answerable for as long as Claude holds the transport open.
|
|
168
|
+
process.stdin.resume();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const invokedDirectly = typeof process.argv[1] === "string"
|
|
172
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
173
|
+
if (invokedDirectly) {
|
|
174
|
+
// Fail open: a Channel that cannot compose is simply absent, never a crash in
|
|
175
|
+
// front of the user's session.
|
|
176
|
+
await main().catch(() => { process.stdin.resume(); });
|
|
177
|
+
}
|
package/bin/acc.mjs
CHANGED
|
@@ -25,8 +25,13 @@ const runtime = {
|
|
|
25
25
|
// answers no: in a real terminal the command printed "not written" and never
|
|
26
26
|
// said why, and `--yes` - the flag documented for runs with nobody to ask -
|
|
27
27
|
// was the only way to write the file.
|
|
28
|
-
confirm: question
|
|
29
|
-
|
|
28
|
+
confirm: (question, io = { input: process.stdin, output: process.stdout }) =>
|
|
29
|
+
askConfirmation(question, io),
|
|
30
|
+
// The two streams and the fact that a person is at both ends, for the
|
|
31
|
+
// per-client native delivery question `acc install` asks only interactively.
|
|
32
|
+
input: process.stdin,
|
|
33
|
+
output: process.stdout,
|
|
34
|
+
isInteractive: () => process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
30
35
|
// Asked for only by `acc version`, so a package missing its own manifest
|
|
31
36
|
// fails that one command rather than every command.
|
|
32
37
|
version: async () => JSON.parse(
|
|
@@ -90,6 +90,37 @@ Unreadable, unknown, or mismatched clients degrade every uncertified row to fals
|
|
|
90
90
|
The backing methods for delivery are `renderContextResult()` for `nextTurn`,
|
|
91
91
|
`offerMessage()` for `livePush`, and `routeReply()` for `replyRoute`.
|
|
92
92
|
|
|
93
|
+
### Native delivery contract
|
|
94
|
+
|
|
95
|
+
An adapter that can push a message into a running session declares `nativeDelivery` next
|
|
96
|
+
to its capabilities:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
nativeDelivery: {
|
|
100
|
+
minimumByPlatform: { "darwin-arm64": "2.1.258" },
|
|
101
|
+
anchors: [{ platform: "darwin-arm64", version: "2.1.258",
|
|
102
|
+
protocolContract: "claude-code-channel-mcp-v1" }],
|
|
103
|
+
knownBad: [],
|
|
104
|
+
activationKinds: ["shell-bootstrap"],
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The rules `defineAdapter` enforces, and the ones the runtime applies:
|
|
109
|
+
|
|
110
|
+
- A minimum is the **first passing capture** on that platform, never a guessed first vendor
|
|
111
|
+
release. Every anchor must have passing `delivery.livePush` evidence for the same client,
|
|
112
|
+
version, and platform, and each minimum must itself be an anchor.
|
|
113
|
+
- There is intentionally **no maximum version**. A newer stable release is admitted only
|
|
114
|
+
when a current read-only feature probe (`probeNativeDelivery()`) and a per-session
|
|
115
|
+
handshake (`bindNativeSession()`) both report the anchored `protocolContract`;
|
|
116
|
+
`evaluateNativeEligibility()` and `validateNativeHandshake()` are those two checks.
|
|
117
|
+
- Prereleases require their own passing capture; they are `prerelease_not_captured` even
|
|
118
|
+
when numerically newer. `knownBad` names exact versions or inclusive intervals.
|
|
119
|
+
- Exact-version certification still governs every non-native capability;
|
|
120
|
+
`effectiveCapabilities()` is unchanged. The native rule is used for live delivery alone.
|
|
121
|
+
- The three native methods return closed facts (`validateNativeActivationPlan()` closes the
|
|
122
|
+
activation plan) and never put vendor data - endpoints, sockets, raw errors - into core.
|
|
123
|
+
|
|
93
124
|
## How far you can get
|
|
94
125
|
|
|
95
126
|
| Tier | You register | You get | You do not get |
|
|
@@ -100,8 +131,9 @@ The backing methods for delivery are `renderContextResult()` for `nextTurn`,
|
|
|
100
131
|
| 3 | + realtime surface | delivery receipts, safe-point injection, child sessions | — |
|
|
101
132
|
|
|
102
133
|
Installed hook wiring may reach tier 2, but the effective capability is still limited to
|
|
103
|
-
an exact certified client/version/platform.
|
|
104
|
-
|
|
134
|
+
an exact certified client/version/platform. Passing native captures exist for Claude Code
|
|
135
|
+
2.1.258 and Codex 0.152.1 on macOS arm64; they become live capabilities only when the
|
|
136
|
+
production transports ship behind the native delivery contract above.
|
|
105
137
|
|
|
106
138
|
## normalizeHook
|
|
107
139
|
|
package/docs/CAPABILITIES.md
CHANGED
|
@@ -17,7 +17,7 @@ platforms degrade to false. No weaker session inherits a stronger peer's capabil
|
|
|
17
17
|
|
|
18
18
|
Passing evidence currently ships for these exact versions on `darwin-arm64`:
|
|
19
19
|
|
|
20
|
-
| Capability | Codex 0.147.0 | Claude Code 2.1.233 | Gemini CLI 0.
|
|
20
|
+
| Capability | Codex 0.147.0 | Claude Code 2.1.233 | Gemini CLI 0.57.0 | Grok 1.0.13 | Kimi 0.36.1 |
|
|
21
21
|
|---|---:|---:|---:|---:|---:|
|
|
22
22
|
| `lifecycle.sessionStart` | yes | yes | yes | no | yes |
|
|
23
23
|
| `lifecycle.sessionEnd` | yes | yes | yes | no | no |
|
|
@@ -32,15 +32,21 @@ Passing evidence currently ships for these exact versions on `darwin-arm64`:
|
|
|
32
32
|
Every other capability in the closed shape defaults to false, including session resume,
|
|
33
33
|
child sessions, startup or safe-point injection, and before-read guards.
|
|
34
34
|
|
|
35
|
+
The `delivery.livePush` and `delivery.replyRoute` row is `no` for the exact hook versions
|
|
36
|
+
this matrix is keyed to. Native live delivery was captured on newer clients - Claude Code
|
|
37
|
+
2.1.258 (livePush and replyRoute) and Codex 0.152.1 (livePush) - and is admitted through the
|
|
38
|
+
native delivery contract rather than exact-version certification: it is off until a per-client
|
|
39
|
+
opt-in, experimental, and never turns on for a client below the captured minimum.
|
|
40
|
+
|
|
35
41
|
The limitations belong next to the adapters they affect:
|
|
36
42
|
|
|
37
43
|
| Adapter | Exact limitation and evidence |
|
|
38
44
|
|---|---|
|
|
39
|
-
| Codex | 0.147.0 next-turn stdout arrives as unwrapped developer-role context and requires plugin trust.
|
|
40
|
-
| Claude Code | 2.1.233 next-turn delivery waits for the next user prompt.
|
|
41
|
-
| Gemini CLI | Only 0.
|
|
42
|
-
| Grok | Documentation-shaped payloads do not count as real captures.
|
|
43
|
-
| Kimi Code | 0.36.1 has next-turn and guard evidence, plus a 60-second heartbeat.
|
|
45
|
+
| Codex | 0.147.0 next-turn stdout arrives as unwrapped developer-role context and requires plugin trust. A 0.152.1 capture proved the App Server queue transport, but the release capture then measured that native delivery there requires `codex --remote unix://`, and in that mode the session runs inside the daemon: both the hook payload and the App Server's own thread record report the daemon's directory instead of the session's, so ACC cannot tell which workspace the session is in. Nothing ACC can reach carries the real one, and placing a session in the wrong workspace is worse than not placing it, so `delivery.livePush` is **not** claimed - the probe and the handshake both refuse with `workspace_identity_unavailable`. `replyRoute` stays false. |
|
|
46
|
+
| Claude Code | 2.1.233 next-turn delivery waits for the next user prompt. A 2.1.258 Channel capture proved idle offer, busy queue-after-turn, explicit reply, duplicate suppression, and durable fallback, so `delivery.livePush` and `delivery.replyRoute` are live capabilities behind the native contract (experimental, off until opted in; Claude's development-channel warning is vendor-owned and visible). |
|
|
47
|
+
| Gemini CLI | Only 0.57.0 has package-shipped next-turn certification. Its TUI has no captured external wake or queue interface and `--acp` changes launch ownership, so native delivery is fallback-only; live push and reply routing remain false. |
|
|
48
|
+
| Grok | Documentation-shaped payloads do not count as real captures. The public leader surface exposed no proven addressed injection into an ordinary TUI session, so native delivery is `awaiting_compatibility_capture`; all capabilities remain false. |
|
|
49
|
+
| Kimi Code | 0.36.1 has next-turn and guard evidence, plus a 60-second heartbeat. Its server/queue APIs do not prove a transparent binding to an independently opened session, so native delivery is fallback-only. |
|
|
44
50
|
| Generic MCP | Tool polling is not next-turn injection, live push, or a native reply route. It has no write guard or client-lifecycle evidence. |
|
|
45
51
|
|
|
46
52
|
`certification.json` beside each adapter is machine-readable. `COMPATIBILITY.md` records the
|
|
@@ -57,8 +63,17 @@ The router requires exactly one current eligible generation. No binding, an expi
|
|
|
57
63
|
several live sessions for one participant, a busy target, or a version that does not match
|
|
58
64
|
passing evidence all stay on durable fallback.
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
The lease is extended by whoever serves the endpoint, because only that process knows it is
|
|
67
|
+
still alive. A client that publishes no heartbeat - Claude Code among them - would otherwise
|
|
68
|
+
let the lease run out under an idle session, which is exactly when live delivery is worth
|
|
69
|
+
having. Giving a binding up is a separate, final fact rather than an expired lease, so a
|
|
70
|
+
channel that has not yet noticed cannot extend something the session already retired.
|
|
71
|
+
|
|
72
|
+
Current shipped reality: Claude Code on darwin-arm64 has a passing experimental `livePush`
|
|
73
|
+
capture behind the native delivery contract, off until a per-client opt-in. Codex is
|
|
74
|
+
next-turn and inbox only: its queue transport works, but the mode that makes a session
|
|
75
|
+
reachable is the mode that hides which workspace it is in. Every other client is next-turn or
|
|
76
|
+
inbox only;
|
|
62
77
|
Gemini CLI and Kimi Code are next-turn only at their exact captured versions; Grok and MCP
|
|
63
78
|
poll inbox.
|
|
64
79
|
|
|
@@ -70,7 +85,7 @@ policy:
|
|
|
70
85
|
| Policy | Meaning |
|
|
71
86
|
|---|---|
|
|
72
87
|
| `off` | normal next-turn and inbox only |
|
|
73
|
-
| `actionable` | questions, requests, and addressed handoffs may use live push |
|
|
88
|
+
| `actionable` | questions, requests, answers, decisions, and addressed handoffs may use live push; notes wait for the next turn |
|
|
74
89
|
| `all` | every addressed message kind may use live push |
|
|
75
90
|
|
|
76
91
|
The default is `off`. `acc install --delivery actionable|all` is an explicit request, not
|
|
@@ -84,7 +99,7 @@ Room messages are never live-push candidates.
|
|
|
84
99
|
|---|---|
|
|
85
100
|
| exact-certified Codex 0.147.0 | complete peer body at the next normal turn; `acc inbox` remains recoverable |
|
|
86
101
|
| exact-certified Claude Code 2.1.233 | complete peer body at the next normal prompt; `acc inbox` remains recoverable |
|
|
87
|
-
| exact-certified Gemini CLI 0.
|
|
102
|
+
| exact-certified Gemini CLI 0.57.0 | complete peer body at the next normal turn; `acc inbox` remains recoverable |
|
|
88
103
|
| exact-certified Kimi Code 0.36.1 | complete peer body at the next normal turn; `acc inbox` remains recoverable |
|
|
89
104
|
| Grok, generic MCP, unknown version, other platform | explicit `acc inbox` polling |
|
|
90
105
|
|
package/docs/CLI.md
CHANGED
|
@@ -129,9 +129,16 @@ external AI client. An addressed handoff requires acknowledgement; a room handof
|
|
|
129
129
|
| `acc help` | — |
|
|
130
130
|
| `acc version` | — |
|
|
131
131
|
|
|
132
|
-
`--delivery` is a recipient policy request, not a capability
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
`--delivery off|actionable|all` is a per-client recipient policy request, not a capability
|
|
133
|
+
switch, and the default is `off`. `--adapter` is repeatable to name several clients. An
|
|
134
|
+
explicit `--delivery` applies uniformly and never prompts; omitting it on an interactive
|
|
135
|
+
terminal asks one default-No question per eligible client, and on a non-interactive run or a
|
|
136
|
+
`--dry-run` it keeps fresh clients off. A recorded opt-in is kept on upgrade. If the detected
|
|
137
|
+
client cannot receive native delivery - unsupported, below the captured minimum, a
|
|
138
|
+
prerelease, known-bad, a wrong platform, or an unsupported shell - installation keeps the
|
|
139
|
+
effective policy off and prints the reason. A live install writes an owned zsh PATH block and
|
|
140
|
+
a per-command shim that keeps your command name and `exec`s the real client; `ACC_BYPASS=1`
|
|
141
|
+
runs the unmodified client, and ACC is never the parent of the session after that `exec`.
|
|
135
142
|
|
|
136
143
|
Only `update` touches the network. `ACC_NO_UPDATE_CHECK=1` disables update checks. Hooks
|
|
137
144
|
never perform them.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -141,3 +141,7 @@ that resolves inside a workspace.
|
|
|
141
141
|
| `ACC_MCP_WORKSPACE` | The project `acc-mcp` joins. Without it the server takes the directory the client launched it in, which is rarely the project |
|
|
142
142
|
| `ACC_NO_UPDATE_CHECK=1` | Never ask npm whether a newer ACC exists. `acc update` then says it is off, which is a different answer from "nothing is newer" |
|
|
143
143
|
| `ACC_PROBE_TIMEOUT_MS` | How long to wait for a client to print its version. Three seconds by default: generous on an idle machine, and not always enough on a busy one, where a client that overruns it is reported as not installed |
|
|
144
|
+
| `ACC_NATIVE_DELIVERY_POLICY` | Set only by an ACC-owned shell shim to the consented live policy (`off`, `actionable`, or `all`) before it `exec`s the real client, so the session's hook knows a native transport was activated. Not for a person to set: an ordinary or `ACC_BYPASS=1` launch leaves it unset, and the hook treats missing or invalid values as `off` |
|
|
145
|
+
| `ACC_BYPASS=1` | Runs the unmodified client through an ACC shim: no launch-time check, no native flags, and the reserved policy variable is unset. The escape hatch when you want the vendor command exactly as it was |
|
|
146
|
+
| `ACC_BOOTSTRAP_DEBUG=1` | Lets the internal `acc-bootstrap` check write one safe diagnostic line to stderr. Off, it is silent, and it never writes to stdout |
|
|
147
|
+
| `CODEX_HOME` | Codex's own home, honoured when locating the Codex App Server daemon's control socket for native delivery. Codex sets it; ACC only reads it |
|