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.
Files changed (96) hide show
  1. package/README.md +91 -81
  2. package/SECURITY.md +7 -4
  3. package/bin/acc-bootstrap.mjs +56 -0
  4. package/bin/acc-claude-channel.mjs +177 -0
  5. package/bin/acc.mjs +7 -2
  6. package/docs/ADAPTER_AUTHORING.md +57 -21
  7. package/docs/ARCHITECTURE.md +23 -15
  8. package/docs/CAPABILITIES.md +51 -18
  9. package/docs/CLI.md +31 -13
  10. package/docs/CONCEPTS.md +91 -94
  11. package/docs/CONFIGURATION.md +33 -23
  12. package/docs/DESIGN_DECISIONS.md +8 -4
  13. package/docs/GETTING_STARTED.md +76 -100
  14. package/docs/GLOSSARY.md +4 -4
  15. package/docs/HOW_IT_WORKS.md +163 -0
  16. package/docs/MCP.md +8 -8
  17. package/docs/PROTOCOL.md +13 -8
  18. package/docs/RELEASING.md +112 -82
  19. package/docs/SECURITY_MODEL.md +13 -8
  20. package/docs/TROUBLESHOOTING.md +30 -12
  21. package/docs/WHY_ACC.md +18 -16
  22. package/docs/index.md +26 -16
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +196 -46
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +237 -45
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +3 -1
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +10 -6
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +34 -2
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
  35. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -5
  36. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +171 -38
  37. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +3 -1
  38. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
  39. package/node_modules/@agents-can-communicate/adapter-codex/package.json +4 -2
  40. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +9 -5
  41. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +42 -7
  42. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +44 -28
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  47. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +9 -5
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeAgent.json → BeforeAgent-0.57.0.json} +2 -2
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool.json → BeforeTool-0.57.0.json} +2 -2
  50. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool-shell.json → BeforeTool-shell-0.57.0.json} +2 -2
  51. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionEnd.json → SessionEnd-0.57.0.json} +2 -2
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionStart.json → SessionStart-0.57.0.json} +2 -2
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +266 -39
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +6 -6
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +25 -11
  56. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  57. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +9 -5
  58. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +3 -1
  59. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  60. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +9 -5
  61. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  62. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +18 -0
  63. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +2 -0
  64. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +6 -2
  65. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
  66. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
  67. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
  68. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +17 -2
  69. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/cli/src/args.mjs +2 -2
  71. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +67 -5
  72. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +133 -11
  73. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +41 -1
  75. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +52 -2
  76. package/node_modules/@agents-can-communicate/core/src/service.mjs +10 -0
  77. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  78. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +26 -12
  79. package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
  80. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
  81. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +123 -90
  82. package/node_modules/@agents-can-communicate/installer/package.json +4 -2
  83. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +48 -4
  84. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
  85. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +72 -3
  86. package/node_modules/@agents-can-communicate/installer/src/index.mjs +7 -0
  87. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
  88. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -2
  89. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +41 -6
  90. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
  91. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  92. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  93. package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
  94. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +18 -3
  95. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  96. 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
  [![CI](https://github.com/automatis-tools/agents-can-communicate/actions/workflows/ci.yml/badge.svg)](https://github.com/automatis-tools/agents-can-communicate/actions/workflows/ci.yml)
4
18
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
19
  [![Node](https://img.shields.io/badge/node-%E2%89%A524-brightgreen.svg)](https://nodejs.org)
6
20
 
7
- ACC connects independently opened AI sessions so they can discover, ask, answer, acknowledge, and hand off without becoming one managed agent team.
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
- The project uses Node's standard library with **zero runtime dependencies**. Coordination
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
- ## Stop relaying between windows
19
-
20
- One session finds that removing `item.drive` will break another area. A second session is
21
- working there, but neither client knows the other exists. Without ACC, the warning stops at
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
- With ACC, the first session sends an attributed question. The second retrieves it, replies
25
- in the same thread, and thereby acknowledges the original. The first retrieves the answer.
26
- The message is durable throughout; neither model gains authority over the other.
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
- ```mermaid
29
- flowchart LR
30
- A["session A — independently opened"] <--> R["ACC room<br/>presence · messages · receipts · claims"]
31
- B["session B — independently opened"] <--> R
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 product's canonical activation event is simple: a second independently opened session
37
- completes a useful acknowledged interaction without the human copying peer message content.
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
- ## Install
46
+ ## Try it
40
47
 
41
- On macOS or Linux with Node 24 or newer:
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
- Restart the clients whose hooks were installed; Codex also requires trusting the plugin.
49
- Then open two sessions in the same repository or plain directory as usual. They remain
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
- Runtime state lives in `~/Library/Application Support/acc` on macOS or the XDG data
54
- directory on Linux. `ACC_DATA_HOME` can override it, but ACC refuses a location inside a
55
- workspace. Git is optional; worktrees of one repository share awareness.
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
- ## The communication loop
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
- ```bash
60
- # Session A publishes awareness and reserves only what it will edit.
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
- # Session A asks a participant named by `acc status`.
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
- # Session B reads and answers the exact message.
69
- acc inbox --message message_x
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
- # Either session records a handoff while context is still available.
73
- acc finish --goal "remove item.drive" --status complete \
74
- --completed "schema and reader updated" --remaining "none"
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
- Messages commit before any delivery attempt. `queued`, `offered`, `retrieved`, and
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
- Durable inbox recovery is the baseline for every client. Certified next-turn injection can
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
- ## What ACC owns
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
- | ACC owns | ACC does not own |
92
- |---|---|
93
- | participant and session presence | process or model lifecycle |
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
- ## Reporting
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
- ## Scope
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
- ## Supported versions
27
+ ## Target a supported version
25
28
 
26
29
  Pre-1.0: the latest release only.
27
30
 
28
- ## What we will do
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 => askConfirmation(question,
29
- { input: process.stdin, output: process.stdout }),
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
- An adapter teaches ACC one client. Nothing else in ACC knows that client exists. This page
4
- assumes the vocabulary in [Protocol](PROTOCOL.md#identity-hierarchy) — session, participant,
5
- claim — and points to [Capabilities](CAPABILITIES.md) for what was actually measured per
6
- client; see the [documentation map](index.md) for where both fit among the rest.
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
- ## The manifest
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, includedMessageIds, includedAttentionIds }`, and the [delivery
41
- lifecycle](PROTOCOL.md#delivery-lifecycle) advances only from those ids — never by searching
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
- ## Capabilities
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 [Capabilities](CAPABILITIES.md#matrix).
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
- ## How far you can get
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 | automatic attach, turn context, write guards, cleanup | realtime |
100
- | 3 | + realtime surface | delivery receipts, safe-point injection, child sessions | — |
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. No client currently certifies native live push
104
- or native reply routing.
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
- ## normalizeHook
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
- ## Response contracts do not port
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 measured in
138
- [Capabilities](CAPABILITIES.md#response-contracts-which-do-not-port).
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
- ## Install and ownership
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
- ## Conformance
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 what you learned
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.