agents-can-communicate 0.2.0 → 0.3.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 +91 -81
- package/SECURITY.md +7 -4
- 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 +57 -21
- package/docs/ARCHITECTURE.md +23 -15
- package/docs/CAPABILITIES.md +51 -18
- package/docs/CLI.md +31 -13
- package/docs/CONCEPTS.md +91 -94
- package/docs/CONFIGURATION.md +33 -23
- package/docs/DESIGN_DECISIONS.md +8 -4
- package/docs/GETTING_STARTED.md +76 -100
- package/docs/GLOSSARY.md +4 -4
- package/docs/HOW_IT_WORKS.md +163 -0
- package/docs/MCP.md +8 -8
- package/docs/PROTOCOL.md +13 -8
- package/docs/RELEASING.md +112 -82
- package/docs/SECURITY_MODEL.md +13 -8
- package/docs/TROUBLESHOOTING.md +30 -12
- package/docs/WHY_ACC.md +18 -16
- package/docs/index.md +26 -16
- 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 +10 -6
- 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 +9 -5
- 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 +9 -5
- 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 +9 -5
- 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 +9 -5
- 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 +133 -11
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +41 -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 +4 -2
- 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
|
@@ -1,112 +1,122 @@
|
|
|
1
1
|
# agents-can-communicate
|
|
2
2
|
|
|
3
|
+
**Open your agents. Let them coordinate.**
|
|
4
|
+
|
|
5
|
+
Give Codex the backend. Ask Claude Code to build the interface. Keep working as usual.
|
|
6
|
+
|
|
7
|
+
ACC makes supported sessions in the same project aware of each other and gives them
|
|
8
|
+
instructions for coordinating. Agents discover their peers, share what they're working
|
|
9
|
+
on, and decide when to ask questions, agree on changes, or avoid overlapping edits.
|
|
10
|
+
|
|
11
|
+
Think of the coordination you expect from subagents, extended across the independent
|
|
12
|
+
sessions you open yourself — with different clients and models. You keep giving them
|
|
13
|
+
ordinary tasks; they handle the conversations around their work.
|
|
14
|
+
|
|
15
|
+
[Try it](#try-it) · [Supported clients](#when-messages-arrive) · [Documentation](docs/index.md)
|
|
16
|
+
|
|
3
17
|
[](https://github.com/automatis-tools/agents-can-communicate/actions/workflows/ci.yml)
|
|
4
18
|
[](LICENSE)
|
|
5
19
|
[](https://nodejs.org)
|
|
6
20
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
It is a local-first communication layer for sessions you already run. Each session keeps
|
|
10
|
-
its own client, model, checkout, permissions, context, and human direction. ACC supplies a
|
|
11
|
-
shared room with presence, intent, narrow file claims, durable conversation threads, and
|
|
12
|
-
truthful per-recipient receipts. It does not launch, steer, supervise, or terminate agents.
|
|
21
|
+
## You describe the work. They work out the details.
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
state stays in platform app data outside the repository, and ACC never collects raw
|
|
16
|
-
transcripts.
|
|
23
|
+
Open two sessions in your project and give each its task:
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
you: copy it to the other window, copy the answer back, and repeat for every question.
|
|
25
|
+
| Session | Your prompt |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Codex | Build the backend for account registration. |
|
|
28
|
+
| Claude Code | Build the registration screen. |
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
ACC's integration tells the agents that peers are present and teaches them how to find
|
|
31
|
+
out what those peers are doing. As they work, they can notice the dependency and coordinate
|
|
32
|
+
on their own. An illustrative exchange:
|
|
27
33
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
U["You — direction and authority"] --> A
|
|
33
|
-
U --> B
|
|
34
|
+
```text
|
|
35
|
+
Claude → Codex I'm building the registration screen. What will your endpoint accept?
|
|
36
|
+
Codex → Claude Email and password. I'll return the new user and handle validation.
|
|
37
|
+
Claude → Codex I'll use that shape and keep my changes in the UI files.
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
The
|
|
37
|
-
|
|
40
|
+
The agents identify who to talk to and what to agree on. Your prompts stay focused on
|
|
41
|
+
the feature you want to build.
|
|
42
|
+
|
|
43
|
+
Each session keeps its own conversation and instructions. Response timing depends on the
|
|
44
|
+
receiving client: see [when messages arrive](#when-messages-arrive).
|
|
38
45
|
|
|
39
|
-
##
|
|
46
|
+
## Try it
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
You'll need **macOS or Linux, Node.js 24 or newer**, and two AI sessions in the same
|
|
49
|
+
project. They can use different clients or the same one.
|
|
42
50
|
|
|
43
51
|
```bash
|
|
44
52
|
npm install -g agents-can-communicate
|
|
45
53
|
acc install
|
|
46
54
|
```
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
independent and join the same ACC workspace. Use `acc doctor` to see exact versions,
|
|
51
|
-
installation health, and delivery downgrades.
|
|
56
|
+
The installer connects the clients it finds. Open a new terminal and restart your AI
|
|
57
|
+
clients to load the integration. If you use Codex, accept its plugin trust prompt.
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
Open two sessions in your project and give them ordinary tasks, as in the example above.
|
|
60
|
+
On supported clients, ACC introduces peer awareness through the client's own integration;
|
|
61
|
+
its installed instructions guide the agents to coordinate when their work overlaps.
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
Look for coordination in the agents' own activity: discovering a peer, checking who is
|
|
64
|
+
changing a file, or asking about a shared dependency. Each agent decides what is relevant
|
|
65
|
+
to its task; the integration provides awareness, not a guarantee that a model will
|
|
66
|
+
coordinate on every task. [Client support](#when-messages-arrive) determines how
|
|
67
|
+
automatically peers and messages reach it.
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
acc work --summary "changing the item schema" --mode edit --hint 'file:src/item.mjs'
|
|
62
|
-
acc claim --resource 'file:src/item.mjs' --reason "changing the item schema"
|
|
69
|
+
If the other session doesn't appear, run `acc doctor` from your project directory.
|
|
70
|
+
[Get help with setup](docs/TROUBLESHOOTING.md).
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
acc message --to models --type question --subject "item.drive" \
|
|
66
|
-
--body "Can your code stop reading item.drive before I remove it?"
|
|
72
|
+
## When messages arrive
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
acc reply --message message_x --body "Yes. Commit abc123 removes the final read."
|
|
74
|
+
ACC connects sessions **on the same machine** and saves messages locally, so a waiting
|
|
75
|
+
session can pick them up later. Delivery depends on the client:
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
| Client | How the agent receives a message |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Codex CLI, Claude Code, Gemini CLI, Kimi Code | At the next normal turn on verified versions and platforms; otherwise by checking its ACC inbox. |
|
|
80
|
+
| Grok or another client connected through [MCP](docs/MCP.md) | By checking its ACC inbox. |
|
|
81
|
+
|
|
82
|
+
**Claude Code can also reply while idle.** This optional mode is experimental and off by
|
|
83
|
+
default. It supports Apple Silicon Macs with zsh and Claude Code 2.1.258 or newer, subject
|
|
84
|
+
to compatibility checks. It can spend model tokens and requires accepting Claude's
|
|
85
|
+
development-channel warning at startup. Messages arriving mid-turn wait for it to finish.
|
|
86
|
+
|
|
87
|
+
Each direction follows the receiving client's rules: Claude's reply won't start a new
|
|
88
|
+
Codex turn. Codex can pick it up on a supported next turn or by checking its inbox.
|
|
89
|
+
|
|
90
|
+
[Full compatibility details](docs/CAPABILITIES.md).
|
|
91
|
+
|
|
92
|
+
## Keep the workflow you like
|
|
93
|
+
|
|
94
|
+
- **Your usual tools.** Start clients with their normal commands. You choose each
|
|
95
|
+
agent's task, model, and permissions.
|
|
96
|
+
- **Separate checkouts, one project.** Git worktrees share an ACC workspace.
|
|
97
|
+
Plain project folders work too; Git is optional.
|
|
98
|
+
- **Agree before editing.** Agents can reserve files and identify overlapping work.
|
|
99
|
+
Reservations are advisory unless the clients support enforcement.
|
|
100
|
+
[How reservations work](docs/CONCEPTS.md#intent-is-awareness-a-claim-commits).
|
|
101
|
+
- **Local coordination.** Messages live in app data outside your project. ACC doesn't
|
|
102
|
+
collect or share raw transcripts. Your clients use their usual model providers.
|
|
103
|
+
|
|
104
|
+
One npm package. No account or separate coordination service to set up.
|
|
105
|
+
|
|
106
|
+
To remove the integrations:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
acc uninstall
|
|
75
110
|
```
|
|
76
111
|
|
|
77
|
-
|
|
78
|
-
`acknowledged` describe different observable facts: an offer is not a read, retrieval is
|
|
79
|
-
not proof of model attention, and a reply resolves communication rather than proving a
|
|
80
|
-
requested task finished.
|
|
112
|
+
ACC preserves configuration changes you've made since installation.
|
|
81
113
|
|
|
82
|
-
|
|
83
|
-
reduce the polling, but only for the exact client version and platform captured by the
|
|
84
|
-
adapter. Native live push is opt-in and would also require a current reachable binding;
|
|
85
|
-
**no shipped adapter currently has passing live-push certification**. The failed Codex
|
|
86
|
-
0.152.0 and Claude Code 2.1.252 captures are shipped as evidence, so ACC reports fallback
|
|
87
|
-
instead of claiming realtime delivery.
|
|
114
|
+
## Go further
|
|
88
115
|
|
|
89
|
-
|
|
116
|
+
[Getting started](docs/GETTING_STARTED.md) · [CLI reference](docs/CLI.md) ·
|
|
117
|
+
[Connect an MCP client](docs/MCP.md) · [How ACC works](docs/HOW_IT_WORKS.md) ·
|
|
118
|
+
[Security](docs/SECURITY_MODEL.md)
|
|
90
119
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
| current intent and resource claims | prompts, permissions, or token budgets |
|
|
95
|
-
| messages, threads, replies, acknowledgements | work queues or execution state |
|
|
96
|
-
| delivery evidence and visible fallback | raw transcripts or shared model memory |
|
|
97
|
-
|
|
98
|
-
A peer message is untrusted input, never system authority. A guarded claim can stop only
|
|
99
|
-
the write paths a client actually exposes; `acc status` reports `advisory` when that cannot
|
|
100
|
-
be guaranteed.
|
|
101
|
-
|
|
102
|
-
## Documentation
|
|
103
|
-
|
|
104
|
-
Start with [Getting started](docs/GETTING_STARTED.md), then use the
|
|
105
|
-
[documentation map](docs/index.md). The exact surfaces are in the [CLI](docs/CLI.md),
|
|
106
|
-
[MCP](docs/MCP.md), [Protocol](docs/PROTOCOL.md), and
|
|
107
|
-
[Capabilities](docs/CAPABILITIES.md) references. Adapter evidence lives beside each
|
|
108
|
-
adapter in its `COMPATIBILITY.md` and `certification.json`.
|
|
109
|
-
|
|
110
|
-
Contributing starts with the repository's
|
|
111
|
-
[AGENTS.md](https://github.com/automatis-tools/agents-can-communicate/blob/main/AGENTS.md).
|
|
112
|
-
Node 24+, Git optional, MIT licensed.
|
|
120
|
+
Want to contribute or add a client?
|
|
121
|
+
Start with [AGENTS.md](https://github.com/automatis-tools/agents-can-communicate/blob/main/AGENTS.md)
|
|
122
|
+
and the [adapter guide](docs/ADAPTER_AUTHORING.md).
|
package/SECURITY.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Security policy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This page is for reporting a vulnerability. For system boundaries and threat reasoning,
|
|
4
|
+
read the [security model](docs/SECURITY_MODEL.md).
|
|
5
|
+
|
|
6
|
+
## Report privately
|
|
4
7
|
|
|
5
8
|
Report privately via [GitHub security advisories](https://github.com/automatis-tools/agents-can-communicate/security/advisories/new).
|
|
6
9
|
|
|
@@ -9,7 +12,7 @@ Please do not open a public issue first.
|
|
|
9
12
|
Include: what you ran, what happened, what you expected, and the client and
|
|
10
13
|
version. A reproduction against a throwaway `ACC_DATA_HOME` is ideal.
|
|
11
14
|
|
|
12
|
-
##
|
|
15
|
+
## Check the scope
|
|
13
16
|
|
|
14
17
|
| In scope | Out of scope |
|
|
15
18
|
|---|---|
|
|
@@ -21,11 +24,11 @@ version. A reproduction against a throwaway `ACC_DATA_HOME` is ideal.
|
|
|
21
24
|
|
|
22
25
|
Reasoning behind each: [docs/SECURITY_MODEL.md](docs/SECURITY_MODEL.md).
|
|
23
26
|
|
|
24
|
-
##
|
|
27
|
+
## Target a supported version
|
|
25
28
|
|
|
26
29
|
Pre-1.0: the latest release only.
|
|
27
30
|
|
|
28
|
-
##
|
|
31
|
+
## Expect this response
|
|
29
32
|
|
|
30
33
|
Acknowledge, reproduce, and tell you whether it is in scope. If it is, the fix
|
|
31
34
|
ships with a test in `tests/security/` so it cannot come back quietly.
|
|
@@ -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(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Writing an adapter
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Use this page to add one client without leaking vendor behavior into core. Start with the
|
|
4
|
+
[identity contract](PROTOCOL.md#identity), then compare the shipped evidence under
|
|
5
|
+
[certified support](CAPABILITIES.md#certified-support). The [documentation map](index.md)
|
|
6
|
+
links the surrounding concepts and runtime architecture.
|
|
7
7
|
|
|
8
8
|
```mermaid
|
|
9
9
|
graph LR
|
|
@@ -14,7 +14,7 @@ graph LR
|
|
|
14
14
|
R --> H
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Define the manifest
|
|
18
18
|
|
|
19
19
|
```js
|
|
20
20
|
export function createExampleAdapter() {
|
|
@@ -37,8 +37,8 @@ export function createExampleAdapter() {
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
`renderContextResult` is required wherever an adapter renders peer messages. It returns
|
|
40
|
-
`{ text,
|
|
41
|
-
lifecycle](PROTOCOL.md#
|
|
40
|
+
`{ text, offeredMessageIds, includedAttentionIds }`, and the [receipt
|
|
41
|
+
lifecycle](PROTOCOL.md#receipt-lifecycle) advances only from those ids — never by searching
|
|
42
42
|
`text` for one, because peer text is untrusted and can imitate another message's header.
|
|
43
43
|
`projectContextResult()` implements this contract; `projectContext()` remains the text-only
|
|
44
44
|
convenience API for adapters that don't need it. An adapter with only the older
|
|
@@ -53,7 +53,7 @@ actually runs as — `command: "claude"` for a client that really runs as `node`
|
|
|
53
53
|
nothing, and the failure is silent: the session gets `pid: null` and falls back to reading
|
|
54
54
|
presence by age alone, with nothing telling you why.
|
|
55
55
|
|
|
56
|
-
##
|
|
56
|
+
## Declare only proven capabilities
|
|
57
57
|
|
|
58
58
|
Fourteen booleans in four groups, declared in the manifest's `capabilities` object:
|
|
59
59
|
|
|
@@ -68,7 +68,8 @@ Fourteen booleans in four groups, declared in the manifest's `capabilities` obje
|
|
|
68
68
|
`defineAdapter` enforces the method — declaring `guards.beforeWrite: true` without
|
|
69
69
|
`guardWrite()` is a usage error at construction. It also requires a passing entry in the
|
|
70
70
|
validated `certification.json`; method existence is never evidence. What each shipped
|
|
71
|
-
client was actually observed doing against this list is
|
|
71
|
+
client was actually observed doing against this list is under
|
|
72
|
+
[certified support](CAPABILITIES.md#certified-support).
|
|
72
73
|
|
|
73
74
|
`lifecycle.heartbeat` is deliberately not a flavour of `delivery.nextTurn`. Next-turn
|
|
74
75
|
delivery happens only when the client reaches a normal turn boundary; heartbeat fires on a
|
|
@@ -90,20 +91,54 @@ Unreadable, unknown, or mismatched clients degrade every uncertified row to fals
|
|
|
90
91
|
The backing methods for delivery are `renderContextResult()` for `nextTurn`,
|
|
91
92
|
`offerMessage()` for `livePush`, and `routeReply()` for `replyRoute`.
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
### Native delivery contract
|
|
95
|
+
|
|
96
|
+
An adapter that can push a message into a running session declares `nativeDelivery` next
|
|
97
|
+
to its capabilities:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
nativeDelivery: {
|
|
101
|
+
minimumByPlatform: { "darwin-arm64": "2.1.258" },
|
|
102
|
+
anchors: [{ platform: "darwin-arm64", version: "2.1.258",
|
|
103
|
+
protocolContract: "claude-code-channel-mcp-v1" }],
|
|
104
|
+
knownBad: [],
|
|
105
|
+
activationKinds: ["shell-bootstrap"],
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The rules `defineAdapter` enforces, and the ones the runtime applies:
|
|
110
|
+
|
|
111
|
+
- A minimum is the **first passing capture** on that platform, never a guessed first vendor
|
|
112
|
+
release. Every anchor must have passing `delivery.livePush` evidence for the same client,
|
|
113
|
+
version, and platform, and each minimum must itself be an anchor.
|
|
114
|
+
- There is intentionally **no maximum version**. A newer stable release is admitted only
|
|
115
|
+
when a current read-only feature probe (`probeNativeDelivery()`) and a per-session
|
|
116
|
+
handshake (`bindNativeSession()`) both report the anchored `protocolContract`;
|
|
117
|
+
`evaluateNativeEligibility()` and `validateNativeHandshake()` are those two checks.
|
|
118
|
+
- Prereleases require their own passing capture; they are `prerelease_not_captured` even
|
|
119
|
+
when numerically newer. `knownBad` names exact versions or inclusive intervals.
|
|
120
|
+
- Exact-version certification still governs every non-native capability;
|
|
121
|
+
`effectiveCapabilities()` is unchanged. The native rule is used for live delivery alone.
|
|
122
|
+
- The three native methods return closed facts (`validateNativeActivationPlan()` closes the
|
|
123
|
+
activation plan) and never put vendor data - endpoints, sockets, raw errors - into core.
|
|
124
|
+
|
|
125
|
+
## Choose the integration depth
|
|
94
126
|
|
|
95
127
|
| Tier | You register | You get | You do not get |
|
|
96
128
|
|---|---|---|---|
|
|
97
129
|
| 0 | nothing — humans run `acc` | durable messages, status, claims | anything automatic |
|
|
98
130
|
| 1 | the MCP server | attach on first call, read, claim, message | guards, session end |
|
|
99
|
-
| 2 | hooks + skill |
|
|
100
|
-
| 3 | +
|
|
131
|
+
| 2 | hooks + skill | only the lifecycle, context, guard, and next-turn behaviors separately captured for this client | uncaptured hook behaviors |
|
|
132
|
+
| 3 | + native delivery surface | only captured live-push and reply-route modes | safe-point injection or child sessions unless separately captured |
|
|
101
133
|
|
|
102
134
|
Installed hook wiring may reach tier 2, but the effective capability is still limited to
|
|
103
|
-
an exact certified client/version/platform.
|
|
104
|
-
|
|
135
|
+
an exact certified client/version/platform. Tier 3 ships for one client: Claude Code, from
|
|
136
|
+
2.1.258 on macOS arm64, behind the native delivery contract above. Codex reached a passing
|
|
137
|
+
queue capture and was withdrawn anyway - the mode its transport needs reports the daemon's
|
|
138
|
+
workspace as the session's - which is the standard: a capture that works is not the same
|
|
139
|
+
claim as a capability that is safe to ship.
|
|
105
140
|
|
|
106
|
-
##
|
|
141
|
+
## Normalize hook input
|
|
107
142
|
|
|
108
143
|
Whitelist, never a filter. Every client hands hooks the prompt, the transcript path, or the
|
|
109
144
|
tool output; none of it may survive.
|
|
@@ -119,7 +154,7 @@ return normalizedEvent({
|
|
|
119
154
|
Refuse an unrecognised payload. Inventing a session attaches the wrong one, or a new one
|
|
120
155
|
every hook, and looks like it is working.
|
|
121
156
|
|
|
122
|
-
##
|
|
157
|
+
## Measure response contracts
|
|
123
158
|
|
|
124
159
|
Measure them. Every client differs, and a wrong shape fails **silently**:
|
|
125
160
|
|
|
@@ -134,10 +169,11 @@ Measure them. Every client differs, and a wrong shape fails **silently**:
|
|
|
134
169
|
`denyOutcome(reason)` returns `{ stdout, stderr, exitCode }`, so the runtime never has to
|
|
135
170
|
know which client it is talking to. This table is only the shape each shipped adapter
|
|
136
171
|
actually uses; the full experimental grid — every candidate shape tried against every
|
|
137
|
-
client, including which ones are silently ignored — is
|
|
138
|
-
|
|
172
|
+
client, including which ones are silently ignored — is recorded in each adapter's
|
|
173
|
+
`COMPATIBILITY.md` and certification fixtures. The cross-client summary is under
|
|
174
|
+
[guard limitations](CAPABILITIES.md#guard-limitations).
|
|
139
175
|
|
|
140
|
-
##
|
|
176
|
+
## Preserve install ownership
|
|
141
177
|
|
|
142
178
|
```mermaid
|
|
143
179
|
graph TB
|
|
@@ -156,7 +192,7 @@ Rules that are not negotiable:
|
|
|
156
192
|
`planInstall` must use the same path helpers as `install`. A conformance test compares
|
|
157
193
|
them, because a plan that drifts makes `--dry-run` a decoration.
|
|
158
194
|
|
|
159
|
-
##
|
|
195
|
+
## Run conformance
|
|
160
196
|
|
|
161
197
|
```bash
|
|
162
198
|
node --test tests/conformance/*.test.mjs
|
|
@@ -166,7 +202,7 @@ node --test tests/process/hook-wiring.test.mjs
|
|
|
166
202
|
The second one *executes* what your install wrote. Three adapters once shipped a hook
|
|
167
203
|
command that did not exist anywhere; every test was green.
|
|
168
204
|
|
|
169
|
-
## Record
|
|
205
|
+
## Record the evidence
|
|
170
206
|
|
|
171
207
|
One `COMPATIBILITY.md` per adapter: client version, event names, payload fields, the deny
|
|
172
208
|
matrix, and what you could **not** observe. The next person's alternative is guessing.
|