agents-can-communicate 0.1.18 → 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 +87 -69
- package/SECURITY.md +31 -0
- package/bin/acc-bootstrap.mjs +56 -0
- package/bin/acc-claude-channel.mjs +177 -0
- package/bin/acc-hook.mjs +94 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +13 -3
- package/docs/ADAPTER_AUTHORING.md +204 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +117 -214
- package/docs/CLI.md +164 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +147 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/HOW_IT_WORKS.md +277 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +115 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +108 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +228 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +269 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +21 -0
- 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 +13 -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 +22 -22
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +45 -5
- 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 +150 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +199 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +21 -0
- 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 +11 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +54 -12
- 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 +68 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +293 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +31 -13
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +11 -9
- package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +52 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +7 -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 +28 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +12 -31
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +70 -5
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +111 -12
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +131 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +21 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +131 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- 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 +190 -105
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +70 -10
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +89 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +10 -2
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +112 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +54 -2
- 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/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +64 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
- package/package.json +20 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
package/README.md
CHANGED
|
@@ -4,101 +4,119 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
between them.**
|
|
7
|
+
ACC connects independently opened AI sessions so they can discover, ask, answer, acknowledge, and hand off without becoming one managed agent team.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
dependencies**, and your transcripts never leave the client.
|
|
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.
|
|
15
13
|
|
|
16
|
-
|
|
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.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
file you're not looking at, still reads it. Neither terminal knows the other exists — so
|
|
20
|
-
either you carry the warning across by hand, or it ships broken.
|
|
18
|
+
## Stop relaying between windows
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
23
|
+
|
|
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.
|
|
26
27
|
|
|
27
28
|
```mermaid
|
|
28
|
-
flowchart
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
R["ACC shared room<br/>presence · claims · messages · handoffs"]
|
|
34
|
-
Y --> A
|
|
35
|
-
Y --> B
|
|
36
|
-
Y --> C
|
|
37
|
-
A <--> R
|
|
38
|
-
B <--> R
|
|
39
|
-
C <--> R
|
|
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
|
|
40
34
|
```
|
|
41
35
|
|
|
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.
|
|
38
|
+
|
|
42
39
|
## Install
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
On macOS or Linux with Node 24 or newer:
|
|
45
42
|
|
|
46
43
|
```bash
|
|
47
44
|
npm install -g agents-can-communicate
|
|
48
45
|
acc install
|
|
49
46
|
```
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`acc doctor` shows what's active, `acc update --apply` keeps it current, `acc uninstall`
|
|
56
|
-
removes only what ACC wrote.
|
|
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.
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
61
56
|
|
|
62
|
-
##
|
|
57
|
+
## The communication loop
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
| **messages** | questions, decisions, notes — attributed, delivered on the recipient's next turn, and data they weigh rather than orders. |
|
|
69
|
-
| **requests & handoffs** | work addressed to a participant, so it waits across restarts and carries the answer back. |
|
|
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"
|
|
70
63
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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?"
|
|
74
67
|
|
|
75
|
-
|
|
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."
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
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"
|
|
75
|
+
```
|
|
79
76
|
|
|
80
|
-
|
|
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.
|
|
81
|
+
|
|
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 |
|
|
81
92
|
|---|---|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
| `acc update --apply` | install the latest release and refresh integrations |
|
|
87
|
-
| `acc uninstall` | remove ACC's integrations — settings you changed stay yours |
|
|
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
|
|
99
|
+
|
|
100
|
+
| ACC owns | ACC does not own |
|
|
101
|
+
|---|---|
|
|
102
|
+
| participant and session presence | process or model lifecycle |
|
|
103
|
+
| current intent and resource claims | prompts, permissions, or token budgets |
|
|
104
|
+
| messages, threads, replies, acknowledgements | work queues or execution state |
|
|
105
|
+
| delivery evidence and visible fallback | raw transcripts or shared model memory |
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
[protocol](docs/PROTOCOL.md) / [capabilities](docs/CAPABILITIES.md) reference, and
|
|
95
|
-
[adapter authoring](docs/ADAPTER_AUTHORING.md) to extend it. New to the vocabulary? The
|
|
96
|
-
[glossary](docs/GLOSSARY.md) defines every term in one line.
|
|
107
|
+
A peer message is untrusted input, never system authority. A guarded claim can stop only
|
|
108
|
+
the write paths a client actually exposes; `acc status` reports `advisory` when that cannot
|
|
109
|
+
be guaranteed.
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
folder](examples/non-git-research.md). Contributing starts with [AGENTS.md](AGENTS.md).
|
|
111
|
+
## Documentation
|
|
100
112
|
|
|
101
|
-
|
|
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
|
|
115
|
+
[documentation map](docs/index.md). The exact surfaces are in the [CLI](docs/CLI.md),
|
|
116
|
+
[MCP](docs/MCP.md), [Protocol](docs/PROTOCOL.md), and
|
|
117
|
+
[Capabilities](docs/CAPABILITIES.md) references. Adapter evidence lives beside each
|
|
118
|
+
adapter in its `COMPATIBILITY.md` and `certification.json`.
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
Contributing starts with the repository's
|
|
121
|
+
[AGENTS.md](https://github.com/automatis-tools/agents-can-communicate/blob/main/AGENTS.md).
|
|
122
|
+
Node 24+, Git optional, MIT licensed.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting
|
|
4
|
+
|
|
5
|
+
Report privately via [GitHub security advisories](https://github.com/automatis-tools/agents-can-communicate/security/advisories/new).
|
|
6
|
+
|
|
7
|
+
Please do not open a public issue first.
|
|
8
|
+
|
|
9
|
+
Include: what you ran, what happened, what you expected, and the client and
|
|
10
|
+
version. A reproduction against a throwaway `ACC_DATA_HOME` is ideal.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
| In scope | Out of scope |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Peer text escaping its quoted block | An attacker who already has write access to your data home |
|
|
17
|
+
| Any path escaping the managed root | A model choosing to obey persuasive peer text |
|
|
18
|
+
| Uninstall deleting files ACC did not write | Vulnerabilities in Codex, Claude Code, Gemini, Grok, or Kimi themselves |
|
|
19
|
+
| ACC writing into a repository | Denial of service by a trusted peer |
|
|
20
|
+
| Session impersonation across MCP | |
|
|
21
|
+
|
|
22
|
+
Reasoning behind each: [docs/SECURITY_MODEL.md](docs/SECURITY_MODEL.md).
|
|
23
|
+
|
|
24
|
+
## Supported versions
|
|
25
|
+
|
|
26
|
+
Pre-1.0: the latest release only.
|
|
27
|
+
|
|
28
|
+
## What we will do
|
|
29
|
+
|
|
30
|
+
Acknowledge, reproduce, and tell you whether it is in scope. If it is, the fix
|
|
31
|
+
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-hook.mjs
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// someone's session stops working. Unknown adapter, malformed payload, broken
|
|
7
7
|
// store, missing binding - all of them end in "allow, exit 0".
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { realpathSync } from "node:fs";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
9
11
|
|
|
10
12
|
import { createId } from "@agents-can-communicate/protocol";
|
|
11
13
|
import { runHook } from "@agents-can-communicate/hook-runner";
|
|
@@ -36,20 +38,100 @@ const readStdin = () => new Promise(resolve => {
|
|
|
36
38
|
return undefined;
|
|
37
39
|
});
|
|
38
40
|
|
|
39
|
-
const
|
|
41
|
+
export const writeOutput = (stream, output, { deadlineAt } = {}) => {
|
|
42
|
+
if (output === "") return Promise.resolve();
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let settled = false;
|
|
45
|
+
let timer;
|
|
46
|
+
const finish = error => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
50
|
+
if (error === undefined || error === null) resolve();
|
|
51
|
+
else reject(error);
|
|
52
|
+
};
|
|
53
|
+
if (deadlineAt !== undefined) {
|
|
54
|
+
const remaining = deadlineAt - Date.now();
|
|
55
|
+
if (remaining <= 0) {
|
|
56
|
+
finish(new Error("hook budget exhausted before stdout write"));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
timer = setTimeout(() => finish(
|
|
60
|
+
new Error("hook budget exhausted waiting for stdout callback")), remaining);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
stream.write(output, finish);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
finish(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const DIAGNOSTIC_BYTES = 512;
|
|
71
|
+
|
|
72
|
+
function boundedDiagnostic(label, error) {
|
|
73
|
+
const detail = String(error?.message ?? error).replace(/[\u0000-\u001f\u007f]/g, " ");
|
|
74
|
+
let line = `acc: ${label}: ${detail}`;
|
|
75
|
+
while (Buffer.byteLength(`${line}\n`, "utf8") > DIAGNOSTIC_BYTES && line.length > 0) {
|
|
76
|
+
line = line.slice(0, -1);
|
|
77
|
+
}
|
|
78
|
+
return `${line}\n`;
|
|
79
|
+
}
|
|
40
80
|
|
|
41
|
-
|
|
81
|
+
function tryWrite(stream, output) {
|
|
82
|
+
if (output === "") return;
|
|
83
|
+
try {
|
|
84
|
+
stream.write(output, () => {});
|
|
85
|
+
} catch {
|
|
86
|
+
// A broken diagnostic stream must not turn a failed-open hook into a crash.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function completeHookOutput(result,
|
|
91
|
+
{ stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
92
|
+
try {
|
|
93
|
+
await writeOutput(stdout, result.stdout ?? "", { deadlineAt: result.deadlineAt });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
tryWrite(stderr, boundedDiagnostic("stdout write failed", error));
|
|
96
|
+
return { exitCode: 0, wroteStdout: false, committedOffers: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let committedOffers = true;
|
|
100
|
+
try {
|
|
101
|
+
await result.commitOffers?.();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
committedOffers = false;
|
|
104
|
+
tryWrite(stderr, boundedDiagnostic("offer commit failed", error));
|
|
105
|
+
}
|
|
106
|
+
if (result.stderr) tryWrite(stderr, `${result.stderr}\n`);
|
|
107
|
+
return { exitCode: 0, wroteStdout: true, committedOffers };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
const [adapterId] = process.argv.slice(2);
|
|
112
|
+
|
|
113
|
+
let payload = null;
|
|
114
|
+
try {
|
|
115
|
+
payload = JSON.parse(await readStdin());
|
|
116
|
+
} catch {
|
|
117
|
+
payload = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = await runHook({ adapterId, payload, adapters,
|
|
121
|
+
runtime: { clock: { now: () => new Date().toISOString() },
|
|
122
|
+
ids: { next: kind => createId(kind, randomBytes) } },
|
|
123
|
+
env: process.env });
|
|
124
|
+
|
|
125
|
+
const completed = await completeHookOutput(result);
|
|
126
|
+
process.exitCode = result.exitCode ?? completed.exitCode;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let isMain = false;
|
|
42
130
|
try {
|
|
43
|
-
|
|
131
|
+
isMain = process.argv[1] !== undefined
|
|
132
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
44
133
|
} catch {
|
|
45
|
-
|
|
134
|
+
isMain = false;
|
|
46
135
|
}
|
|
47
136
|
|
|
48
|
-
|
|
49
|
-
runtime: { clock: { now: () => new Date().toISOString() },
|
|
50
|
-
ids: { next: kind => createId(kind, randomBytes) } },
|
|
51
|
-
env: process.env });
|
|
52
|
-
|
|
53
|
-
if (result.stdout !== "") process.stdout.write(result.stdout);
|
|
54
|
-
if (result.stderr) process.stderr.write(`${result.stderr}\n`);
|
|
55
|
-
process.exitCode = result.exitCode ?? 0;
|
|
137
|
+
if (isMain) await main();
|
package/bin/acc-mcp.mjs
CHANGED
|
@@ -3,8 +3,9 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
|
|
4
4
|
import { EXIT, createId } from "@agents-can-communicate/protocol";
|
|
5
5
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
6
|
+
import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
|
|
6
7
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
7
|
-
import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
8
|
+
import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
8
9
|
from "@agents-can-communicate/cli";
|
|
9
10
|
import { serve } from "@agents-can-communicate/mcp-server";
|
|
10
11
|
|
|
@@ -40,13 +41,16 @@ const paths = runtimePaths({
|
|
|
40
41
|
});
|
|
41
42
|
const store = await openFilesystemStore({ root: paths.root, clock, ids,
|
|
42
43
|
workspaceId: descriptor.id });
|
|
44
|
+
const service = createCoordinationService({ store, clock, ids });
|
|
45
|
+
const adapters = Object.fromEntries(ALL_ADAPTERS().map(adapter => [adapter.id, adapter]));
|
|
43
46
|
|
|
44
47
|
await serve({
|
|
45
48
|
input: process.stdin,
|
|
46
49
|
output: process.stdout,
|
|
47
50
|
log: message => process.stderr.write(`acc-mcp: ${message}\n`),
|
|
48
51
|
context: {
|
|
49
|
-
service
|
|
52
|
+
service,
|
|
53
|
+
deliveryRouter: createDeliveryRouter({ service, adapters, clock }),
|
|
50
54
|
workspaceId: descriptor.id,
|
|
51
55
|
participantId,
|
|
52
56
|
descriptor,
|
package/bin/acc.mjs
CHANGED
|
@@ -3,7 +3,10 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
|
|
5
5
|
import { createId } from "@agents-can-communicate/protocol";
|
|
6
|
-
import {
|
|
6
|
+
import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
|
|
7
|
+
import { ALL_ADAPTERS, askConfirmation, main } from "@agents-can-communicate/cli";
|
|
8
|
+
|
|
9
|
+
const adapters = Object.fromEntries(ALL_ADAPTERS().map(adapter => [adapter.id, adapter]));
|
|
7
10
|
|
|
8
11
|
// The composition root is the only place allowed to reach for ambient time and
|
|
9
12
|
// randomness; everything below it receives them as ports.
|
|
@@ -15,13 +18,20 @@ const runtime = {
|
|
|
15
18
|
stderr: process.stderr,
|
|
16
19
|
clock: { now: () => new Date().toISOString() },
|
|
17
20
|
ids: { next: kind => createId(kind, randomBytes) },
|
|
21
|
+
createDeliveryRouter: ({ service, clock }) =>
|
|
22
|
+
createDeliveryRouter({ service, adapters, clock }),
|
|
18
23
|
// Asked only by `acc config init`, and only when stdout is a terminal. There
|
|
19
24
|
// was no port here at all, so the question went to the fallback that always
|
|
20
25
|
// answers no: in a real terminal the command printed "not written" and never
|
|
21
26
|
// said why, and `--yes` - the flag documented for runs with nobody to ask -
|
|
22
27
|
// was the only way to write the file.
|
|
23
|
-
confirm: question
|
|
24
|
-
|
|
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,
|
|
25
35
|
// Asked for only by `acc version`, so a package missing its own manifest
|
|
26
36
|
// fails that one command rather than every command.
|
|
27
37
|
version: async () => JSON.parse(
|