agents-can-communicate 0.3.1 → 0.4.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.
Files changed (157) hide show
  1. package/README.md +28 -7
  2. package/SECURITY.md +1 -1
  3. package/bin/acc-bootstrap.mjs +5 -54
  4. package/bin/acc-claude-channel.mjs +8 -175
  5. package/bin/acc-hook.mjs +4 -133
  6. package/bin/acc-mcp.mjs +4 -59
  7. package/bin/acc-update-worker.mjs +8 -0
  8. package/bin/acc.mjs +5 -39
  9. package/bin/entrypoints/acc-bootstrap.mjs +47 -0
  10. package/bin/entrypoints/acc-claude-channel.mjs +149 -0
  11. package/bin/entrypoints/acc-hook.mjs +58 -0
  12. package/bin/entrypoints/acc-mcp.mjs +70 -0
  13. package/bin/entrypoints/acc.mjs +51 -0
  14. package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
  15. package/bin/entrypoints/hook-output.mjs +73 -0
  16. package/docs/ADAPTER_AUTHORING.md +41 -7
  17. package/docs/ARCHITECTURE.md +62 -11
  18. package/docs/CAPABILITIES.md +40 -36
  19. package/docs/CLI.md +143 -25
  20. package/docs/CONCEPTS.md +13 -6
  21. package/docs/CONFIGURATION.md +37 -22
  22. package/docs/DESIGN_DECISIONS.md +16 -11
  23. package/docs/GETTING_STARTED.md +37 -7
  24. package/docs/GLOSSARY.md +9 -3
  25. package/docs/HOW_IT_WORKS.md +32 -11
  26. package/docs/MCP.md +91 -24
  27. package/docs/PROTOCOL.md +69 -12
  28. package/docs/RELEASING.md +28 -10
  29. package/docs/SECURITY_MODEL.md +45 -13
  30. package/docs/TROUBLESHOOTING.md +76 -18
  31. package/docs/UPGRADING.md +119 -0
  32. package/docs/WHY_ACC.md +5 -4
  33. package/docs/index.md +8 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +106 -5
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
  38. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
  41. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
  51. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
  52. package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
  53. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +106 -5
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
  57. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
  58. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
  59. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
  60. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
  61. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -2
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +106 -5
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
  66. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  67. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +106 -5
  68. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
  69. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +106 -5
  71. package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
  72. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
  73. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
  80. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
  81. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
  82. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
  83. package/node_modules/@agents-can-communicate/cli/package.json +4 -2
  84. package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
  85. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
  86. package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
  87. package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
  88. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +22 -4
  89. package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
  90. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
  91. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +63 -0
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
  94. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
  95. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +73 -0
  96. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
  97. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
  98. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
  99. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
  100. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
  101. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
  102. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
  103. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +122 -0
  104. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
  105. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
  106. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
  107. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
  109. package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
  110. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +9 -25
  111. package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
  112. package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
  113. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
  114. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
  115. package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
  116. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
  117. package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
  118. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
  119. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
  120. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
  121. package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
  122. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  123. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
  124. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
  125. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
  127. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
  128. package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
  129. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
  130. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -67
  131. package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
  132. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  133. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
  134. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
  135. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
  137. package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
  138. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
  139. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
  140. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
  141. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
  143. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
  144. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
  145. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
  146. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  147. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
  148. package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
  149. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
  150. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
  151. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  152. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
  153. package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
  154. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
  155. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
  156. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
  157. package/package.json +2 -1
@@ -0,0 +1,149 @@
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
+
11
+ import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
12
+ import { listSessionBindings } from "@agents-can-communicate/adapter-sdk";
13
+ import { createCoordinationService } from "@agents-can-communicate/core";
14
+ import { resolveClientPid } from "@agents-can-communicate/hook-runner/client-pid";
15
+ import { readProcessTable } from "@agents-can-communicate/hook-runner/process-table";
16
+ import { createId } from "@agents-can-communicate/protocol";
17
+ import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
18
+ import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
19
+ from "@agents-can-communicate/cli";
20
+
21
+ import { createAccChannel, endpointDir, routeAck, routeReply }
22
+ from "@agents-can-communicate/adapter-claude-code/channel";
23
+
24
+ import { pump, startInertChannel } from "./claude-channel-stdio.mjs";
25
+
26
+ const clock = { now: () => new Date().toISOString() };
27
+ const ids = { next: kind => createId(kind, randomBytes) };
28
+
29
+ /**
30
+ * The ACC session this Claude process opened - decided by which client process
31
+ * this Channel is running under, never by how many sessions happen to be
32
+ * visible.
33
+ *
34
+ * There used to be a fallback here: if exactly one session was live, it was
35
+ * taken to be this one. On a single-session machine that looked safe, and with
36
+ * two sessions it silently handed one client the other's identity. The Channel
37
+ * starts while its own SessionStart hook is still writing its binding, so the
38
+ * only binding a second client can see at that moment is the *first* client's -
39
+ * and the wait that was added to survive that race then locks the wrong answer
40
+ * in on the first attempt, because it is a complete, live, plausible binding.
41
+ *
42
+ * Measured on two real 2.1.259 sessions: both Channels registered an endpoint
43
+ * under the first client's pid. `bindNativeSession` refuses two registrations
44
+ * for one pid, so the session that had been receiving live messages dropped
45
+ * back to the durable inbox - and an `acc_reply` from the second window would
46
+ * have been recorded as the first session's answer, which is a false record
47
+ * rather than a lost message.
48
+ *
49
+ * This process was spawned by its client, so its own ancestry names that
50
+ * client, and no race can change that. Ownership is decided by it.
51
+ */
52
+ export async function resolveSession({ runtimeDir, service, env, ownClientPid,
53
+ listBindings = listSessionBindings }) {
54
+ if (!Number.isInteger(ownClientPid)) return null;
55
+ const bindings = await listBindings({ runtimeDir });
56
+ if (bindings.length === 0) return null;
57
+ const status = await service.collectStatus({});
58
+ const live = new Set(status.participants
59
+ .filter(participant => participant.presence !== "offline")
60
+ .map(participant => participant.sessionId));
61
+ const mine = bindings.filter(binding => live.has(binding.accSessionId)
62
+ && binding.clientPid === ownClientPid);
63
+ // One client process can outlive a session and open another, so the pid alone
64
+ // can still name two. The harness session id the client exports separates
65
+ // them; with nothing to separate them the answer is nobody, as before.
66
+ const exported = new Set(Object.values(env ?? {}).filter(value => typeof value === "string"));
67
+ const matched = mine.filter(binding => exported.has(binding.harnessSessionId));
68
+ const chosen = mine.length === 1 ? mine[0] : matched.length === 1 ? matched[0] : null;
69
+ return chosen === null ? null
70
+ : { sessionId: chosen.accSessionId, generation: chosen.generation, clientPid: chosen.clientPid };
71
+ }
72
+
73
+ /** The client process this Channel was spawned by, or null when nobody knows. */
74
+ export async function ownClient({ table = null, from = process.pid,
75
+ command = createClaudeCodeAdapter().client.command } = {}) {
76
+ return resolveClientPid({ table: table ?? await readProcessTable(), from, command });
77
+ }
78
+
79
+ // How long the Channel will wait for the hook to publish this session's
80
+ // binding. Claude spawns this child while SessionStart is still running - both
81
+ // landed in the same second on a real 2.1.259 launch - and a single lookup
82
+ // loses that race often enough to make native delivery activate or not per
83
+ // launch. Bounded, because a session with no ACC binding must still be answered.
84
+ const BINDING_WAIT_MS = 5_000;
85
+ const BINDING_POLL_MS = 100;
86
+
87
+ /**
88
+ * The session binding, once it exists - or null when the wait runs out.
89
+ *
90
+ * A binding with no `clientPid` names no process, and the Channel binds an
91
+ * exact one, so it does not count as resolved: the hook writes the identity
92
+ * first and the certified facts after, and reading in between must keep waiting
93
+ * rather than settle for the half-written answer.
94
+ */
95
+ export async function resolveWithin({ resolve, deadline, intervalMs = BINDING_POLL_MS,
96
+ now = () => Date.now(), sleep = ms => new Promise(resolve_ => setTimeout(resolve_, ms)) }) {
97
+ for (;;) {
98
+ const session = await resolve();
99
+ if (session !== null && Number.isInteger(session?.clientPid)) return session;
100
+ if (now() >= deadline) return null;
101
+ await sleep(intervalMs);
102
+ }
103
+ }
104
+
105
+ async function compose() {
106
+ const descriptor = await discoverWorkspace({ cwd: process.cwd(), env: process.env,
107
+ gitProbe: createGitProbe() });
108
+ const paths = runtimePaths({ dataHome: platformDataHome({ platform: process.platform,
109
+ env: process.env }), workspaceId: descriptor.id, workspaceRoots: descriptor.roots });
110
+ const store = await openFilesystemStore({ root: paths.root, clock, ids,
111
+ workspaceId: descriptor.id });
112
+ const service = createCoordinationService({ store, clock, ids });
113
+ const write = payload => process.stdout.write(`${JSON.stringify(payload)}\n`);
114
+ // Read once: the ancestry that decides ownership cannot change while this
115
+ // process runs, and re-reading it on every poll would spawn `ps` in a loop.
116
+ const clientPid = await ownClient();
117
+ const session = await resolveWithin({
118
+ resolve: () => resolveSession({ runtimeDir: paths.root, service, env: process.env,
119
+ ownClientPid: clientPid }),
120
+ deadline: Date.now() + BINDING_WAIT_MS });
121
+ // No binding to serve - but Claude is already speaking MCP to this child, and
122
+ // returning here left the event loop empty: the process exited in 75ms
123
+ // without answering `initialize`, and Claude reported the server as failed to
124
+ // connect on every session that enables the plugin without ACC's shim.
125
+ if (session === null || !Number.isInteger(session.clientPid)) {
126
+ return startInertChannel({ write });
127
+ }
128
+
129
+ const channel = createAccChannel({
130
+ endpointDir: endpointDir(paths.root),
131
+ clientPid: session.clientPid,
132
+ write,
133
+ routeReply: ({ messageId, body }) => routeReply({ service, session, messageId, body }),
134
+ routeAck: ({ messageId }) => routeAck({ service, session, messageId }),
135
+ // This process is the only party that knows the endpoint is still being
136
+ // served, and Claude publishes no heartbeat, so nothing else would move
137
+ // ACC's lease while the session sits idle - which is when live delivery is
138
+ // worth having.
139
+ refreshBinding: leaseUntil => service.refreshDeliveryBinding({
140
+ sessionId: session.sessionId, generation: session.generation, leaseUntil }),
141
+ });
142
+ await channel.listen();
143
+ pump(channel.handleLine, () => { channel.close(); process.exit(0); });
144
+ return channel;
145
+ }
146
+
147
+ export async function main() {
148
+ await compose().catch(() => startInertChannel());
149
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // Hook entry point: `acc-hook <adapter-id> <kind>`, payload on stdin.
3
+ //
4
+ // Every harness runs this as a short-lived child process in front of the user's
5
+ // turn. The one rule that overrides all others here: never be the reason
6
+ // someone's session stops working. Unknown adapter, malformed payload, broken
7
+ // store, missing binding - all of them end in "allow, exit 0".
8
+ import { randomBytes } from "node:crypto";
9
+
10
+ import { createId } from "@agents-can-communicate/protocol";
11
+ import { runHook } from "@agents-can-communicate/hook-runner";
12
+
13
+ import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
14
+ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
15
+ import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
16
+ import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
17
+ import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
18
+
19
+ const adapters = {
20
+ claude_code: createClaudeCodeAdapter(),
21
+ codex: createCodexAdapter(),
22
+ gemini_cli: createGeminiCliAdapter(),
23
+ grok: createGrokAdapter(),
24
+ kimi: createKimiAdapter(),
25
+ };
26
+
27
+ const readStdin = () => new Promise(resolve => {
28
+ // A hook is always given its payload on stdin, but a client that closes it
29
+ // without writing must not leave this process hanging in front of a turn.
30
+ if (process.stdin.isTTY) return resolve("");
31
+ let raw = "";
32
+ process.stdin.setEncoding("utf8");
33
+ process.stdin.on("data", chunk => { raw += chunk; });
34
+ process.stdin.on("end", () => resolve(raw));
35
+ process.stdin.on("error", () => resolve(""));
36
+ return undefined;
37
+ });
38
+
39
+ import { completeHookOutput } from "./hook-output.mjs";
40
+
41
+ export async function main() {
42
+ const [adapterId] = process.argv.slice(2);
43
+
44
+ let payload = null;
45
+ try {
46
+ payload = JSON.parse(await readStdin());
47
+ } catch {
48
+ payload = null;
49
+ }
50
+
51
+ const result = await runHook({ adapterId, payload, adapters,
52
+ runtime: { clock: { now: () => new Date().toISOString() },
53
+ ids: { next: kind => createId(kind, randomBytes) } },
54
+ env: process.env });
55
+
56
+ const completed = await completeHookOutput(result);
57
+ process.exitCode = result.exitCode ?? completed.exitCode;
58
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { randomBytes } from "node:crypto";
3
+
4
+ import { EXIT, createId } from "@agents-can-communicate/protocol";
5
+ import { createCoordinationService } from "@agents-can-communicate/core";
6
+ import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
7
+ import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
8
+ import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
9
+ import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
10
+ from "@agents-can-communicate/cli";
11
+ import { serve } from "@agents-can-communicate/mcp-server";
12
+
13
+ export async function main({ managerRoot = null, packageRoot } = {}) {
14
+ // The composition root is the only place allowed to reach for ambient time and
15
+ // randomness. The participant name comes from configuration, never from the
16
+ // client: the protocol says clientInfo is self-reported and must not drive
17
+ // behaviour, and the session is derived from this configuration alone.
18
+ // Nothing is read from the command line, so nothing may be passed on it. It
19
+ // used to accept and ignore anything: writing `acc-mcp --cwd <project>` - the
20
+ // habit `acc` teaches - started a server rooted wherever the client happened to
21
+ // launch it, alone in a workspace nobody else was in, with no warning at all.
22
+ if (process.argv.length > 2) {
23
+ process.stderr.write("acc-mcp takes no arguments. It is configured by environment:\n"
24
+ + " ACC_MCP_PARTICIPANT who this server takes part as (default: mcp)\n"
25
+ + " ACC_MCP_WORKSPACE the project it joins (default: the working directory)\n"
26
+ + `refusing: ${process.argv.slice(2).join(" ")}\n`);
27
+ process.exit(EXIT.USAGE);
28
+ }
29
+
30
+ const participantId = process.env.ACC_MCP_PARTICIPANT ?? "mcp";
31
+ const clock = { now: () => new Date().toISOString() };
32
+ const ids = { next: kind => createId(kind, randomBytes) };
33
+
34
+ const descriptor = await discoverWorkspace({
35
+ cwd: process.env.ACC_MCP_WORKSPACE ?? process.cwd(),
36
+ env: process.env,
37
+ gitProbe: createGitProbe(),
38
+ });
39
+ const dataHome = platformDataHome({ platform: process.platform, env: process.env });
40
+ const paths = runtimePaths({
41
+ dataHome,
42
+ workspaceId: descriptor.id,
43
+ workspaceRoots: descriptor.roots,
44
+ });
45
+ const store = await openFilesystemStore({ root: paths.root, clock, ids,
46
+ workspaceId: descriptor.id });
47
+ const service = createCoordinationService({ store, clock, ids });
48
+ const adapters = Object.fromEntries(ALL_ADAPTERS().map(adapter => [adapter.id, adapter]));
49
+
50
+ await serve({
51
+ input: process.stdin,
52
+ output: process.stdout,
53
+ log: message => process.stderr.write(`acc-mcp: ${message}\n`),
54
+ context: {
55
+ service,
56
+ deliveryRouter: createDeliveryRouter({ service, adapters, clock,
57
+ platform: `${process.platform}-${process.arch}`,
58
+ readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
59
+ adapterId: adapter.id }) }),
60
+ workspaceId: descriptor.id,
61
+ participantId,
62
+ descriptor,
63
+ runtimeDir: paths.root,
64
+ },
65
+ });
66
+
67
+ // Exiting on stdin EOF is the only portable graceful shutdown in this binding.
68
+ process.exitCode = 0;
69
+
70
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { randomBytes } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+
5
+ import { createId } from "@agents-can-communicate/protocol";
6
+ import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
7
+ import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
8
+ import { ALL_ADAPTERS, askConfirmation, main as runCli } from "@agents-can-communicate/cli";
9
+
10
+ export async function main({ managerRoot = null, packageRoot, managementOnly = false } = {}) {
11
+ const adapters = Object.fromEntries(ALL_ADAPTERS().map(adapter => [adapter.id, adapter]));
12
+
13
+ // The composition root is the only place allowed to reach for ambient time and
14
+ // randomness; everything below it receives them as ports.
15
+ const runtime = {
16
+ cwd: process.cwd(),
17
+ managerRoot,
18
+ managementOnly,
19
+ packageRoot,
20
+ env: process.env,
21
+ platform: process.platform,
22
+ stdout: process.stdout,
23
+ stderr: process.stderr,
24
+ clock: { now: () => new Date().toISOString() },
25
+ ids: { next: kind => createId(kind, randomBytes) },
26
+ createDeliveryRouter: ({ service, clock, dataHome }) =>
27
+ createDeliveryRouter({ service, adapters, clock,
28
+ platform: `${process.platform}-${process.arch}`,
29
+ readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
30
+ adapterId: adapter.id }) }),
31
+ // Asked only by `acc config init`, and only when stdout is a terminal. There
32
+ // was no port here at all, so the question went to the fallback that always
33
+ // answers no: in a real terminal the command printed "not written" and never
34
+ // said why, and `--yes` - the flag documented for runs with nobody to ask -
35
+ // was the only way to write the file.
36
+ confirm: (question, io = { input: process.stdin, output: process.stdout }) =>
37
+ askConfirmation(question, io),
38
+ // The two streams and the fact that a person is at both ends, for the
39
+ // per-client native delivery question `acc install` asks only interactively.
40
+ input: process.stdin,
41
+ output: process.stdout,
42
+ isInteractive: () => process.stdin.isTTY === true && process.stdout.isTTY === true,
43
+ // Asked for only by `acc version`, so a package missing its own manifest
44
+ // fails that one command rather than every command.
45
+ version: async () => JSON.parse(
46
+ await readFile(new URL("../../package.json", import.meta.url), "utf8")).version,
47
+ };
48
+
49
+ process.exitCode = await runCli(process.argv.slice(2), runtime);
50
+
51
+ }
@@ -0,0 +1,29 @@
1
+ // Workspace-free MCP fallback shared by composition and managed admission failure.
2
+ import { createInertChannel } from "@agents-can-communicate/adapter-claude-code/channel";
3
+
4
+ export function startInertChannel({ write = payload => process.stdout.write(JSON.stringify(payload) + "\n") } = {}) {
5
+ const inert = createInertChannel({ write });
6
+ pump(inert.handleLine, () => process.exit(0));
7
+ return inert;
8
+ }
9
+
10
+ /** One line-delimited JSON pump, for the bound server and the unbound one alike. */
11
+ export function pump(handleLine, shutdown) {
12
+ let buffer = "";
13
+ process.stdin.setEncoding("utf8");
14
+ process.stdin.on("data", async chunk => {
15
+ buffer += chunk;
16
+ let newline = buffer.indexOf("\n");
17
+ while (newline !== -1) {
18
+ const line = buffer.slice(0, newline).trim();
19
+ buffer = buffer.slice(newline + 1);
20
+ newline = buffer.indexOf("\n");
21
+ if (line !== "") await handleLine(line);
22
+ }
23
+ });
24
+ process.stdin.on("end", shutdown);
25
+ process.once("SIGTERM", shutdown);
26
+ process.once("SIGINT", shutdown);
27
+ // Keeps the child answerable for as long as Claude holds the transport open.
28
+ process.stdin.resume();
29
+ }
@@ -0,0 +1,73 @@
1
+ export const writeOutput = (stream, output, { deadlineAt } = {}) => {
2
+ if (output === "") return Promise.resolve();
3
+ return new Promise((resolve, reject) => {
4
+ let settled = false;
5
+ let timer;
6
+ const finish = error => {
7
+ if (settled) return;
8
+ settled = true;
9
+ if (timer !== undefined) clearTimeout(timer);
10
+ if (error === undefined || error === null) resolve();
11
+ else reject(error);
12
+ };
13
+ if (deadlineAt !== undefined) {
14
+ const remaining = deadlineAt - Date.now();
15
+ if (remaining <= 0) {
16
+ finish(new Error("hook budget exhausted before stdout write"));
17
+ return;
18
+ }
19
+ timer = setTimeout(() => finish(
20
+ new Error("hook budget exhausted waiting for stdout callback")), remaining);
21
+ }
22
+ try {
23
+ stream.write(output, finish);
24
+ } catch (error) {
25
+ finish(error);
26
+ }
27
+ });
28
+ };
29
+
30
+ const DIAGNOSTIC_BYTES = 512;
31
+
32
+ function boundedDiagnostic(label, error) {
33
+ const detail = String(error?.message ?? error).replace(/[\u0000-\u001f\u007f]/g, " ");
34
+ let line = `acc: ${label}: ${detail}`;
35
+ while (Buffer.byteLength(`${line}\n`, "utf8") > DIAGNOSTIC_BYTES && line.length > 0) {
36
+ line = line.slice(0, -1);
37
+ }
38
+ return `${line}\n`;
39
+ }
40
+
41
+ function tryWrite(stream, output) {
42
+ if (output === "") return;
43
+ try {
44
+ stream.write(output, () => {});
45
+ } catch {
46
+ // A broken diagnostic stream must not turn a failed-open hook into a crash.
47
+ }
48
+ }
49
+
50
+ export async function completeHookOutput(result,
51
+ { stdout = process.stdout, stderr = process.stderr } = {}) {
52
+ if (result.failed || result.timedOut) {
53
+ // Hook failures can include arbitrary payload or filesystem text. Report
54
+ // degradation without reflecting those details into the client's output.
55
+ tryWrite(stderr, "acc: coordination unavailable; hook continued without context\n");
56
+ }
57
+ try {
58
+ await writeOutput(stdout, result.stdout ?? "", { deadlineAt: result.deadlineAt });
59
+ } catch (error) {
60
+ tryWrite(stderr, boundedDiagnostic("stdout write failed", error));
61
+ return { exitCode: 0, wroteStdout: false, committedOffers: false };
62
+ }
63
+
64
+ let committedOffers = true;
65
+ try {
66
+ await result.commitOffers?.();
67
+ } catch (error) {
68
+ committedOffers = false;
69
+ tryWrite(stderr, boundedDiagnostic("offer commit failed", error));
70
+ }
71
+ if (result.stderr) tryWrite(stderr, `${result.stderr}\n`);
72
+ return { exitCode: 0, wroteStdout: true, committedOffers };
73
+ }
@@ -28,6 +28,7 @@ export function createExampleAdapter() {
28
28
 
29
29
  detect, install, uninstall, doctor,
30
30
  planInstall, // what install would write
31
+ preflightUninstall, // optional read-only removal validation
31
32
  normalizeHook, // client payload -> normalised event
32
33
  renderContext, // SyncResult -> text
33
34
  renderContextResult, // text + ids of complete rendered groups
@@ -36,6 +37,11 @@ export function createExampleAdapter() {
36
37
  }
37
38
  ```
38
39
 
40
+ `preflightUninstall(context)` is optional and read-only. Throw to refuse unsafe
41
+ removal before the installer deactivates native integration or deletes recorded
42
+ artifacts. `uninstall` should retain its own validation for direct adapter callers;
43
+ recorded artifact fingerprint checks still determine the `keep` paths it receives.
44
+
39
45
  `renderContextResult` is required wherever an adapter renders peer messages. It returns
40
46
  `{ text, offeredMessageIds, includedAttentionIds }`, and the [receipt
41
47
  lifecycle](PROTOCOL.md#receipt-lifecycle) advances only from those ids — never by searching
@@ -46,6 +52,12 @@ convenience API for adapters that don't need it. An adapter with only the older
46
52
  instead: repeating an untracked body every turn would be quieter in code and dishonest about
47
53
  what was actually delivered.
48
54
 
55
+ The hook also supplies `reminderMessageIds`, derived from this participant's `offered` or
56
+ `retrieved` receipts with unresolved obligations. The standard projector combines those
57
+ reply/acknowledgement attention items into counts after new bodies. A missing body alone
58
+ is not evidence for compaction. Counts add no ids to `offeredMessageIds` or
59
+ `includedAttentionIds`; complete inbox and status results remain available.
60
+
49
61
  `client.command` does double duty. `detect.mjs` uses it as the version-probe binary, and
50
62
  presence liveness separately walks the hook's process ancestry for the first ancestor whose
51
63
  executable basename matches it, to learn the client's own pid. Declare the binary the client
@@ -119,9 +131,30 @@ The rules `defineAdapter` enforces, and the ones the runtime applies:
119
131
  when numerically newer. `knownBad` names exact versions or inclusive intervals.
120
132
  - Exact-version certification still governs every non-native capability;
121
133
  `effectiveCapabilities()` is unchanged. The native rule is used for live delivery alone.
122
- - The three native methods return closed facts (`validateNativeActivationPlan()` closes the
134
+ - Native methods return closed facts (`validateNativeActivationPlan()` closes the
123
135
  activation plan) and never put vendor data - endpoints, sockets, raw errors - into core.
124
136
 
137
+ `nativeDelivery.policySource` defaults to the session environment. An adapter may
138
+ declare `"installation-record"` when hooks and senders must read current recorded
139
+ recipient consent independently of the vendor process environment. Installation
140
+ records the requested policy even when current activation is unavailable. An absent
141
+ or invalid record means `off`; a previously published binding cannot override it.
142
+
143
+ The optional `refreshNativeSession()` method may revalidate an expired lease before
144
+ an offer. It returns the same closed handshake shape as `bindNativeSession()` and
145
+ must preserve the exact endpoint and protocol identity. The router rechecks the
146
+ current session, generation, retirement, uniqueness and policy before publishing
147
+ the refreshed lease. Refresh is not a heartbeat and cannot extend presence beyond
148
+ its own expiry. Optional `retireNativeSession()` cleans adapter-owned endpoint state
149
+ after confirmed core retirement; it must remain bounded and fail open for hooks.
150
+
151
+ A package-shipped passing Codex installed-hook capture additionally references its
152
+ complete real product matrix through the selected provenance record's
153
+ `productEvidence: { fixture, sha256 }`. Package verification checks the actual file,
154
+ raw digest and client/version/platform/package identity. A transport-only capture
155
+ cannot certify the installed product route. `historicalFixtures` may retain hashed
156
+ earlier failures without duplicating a certification tuple.
157
+
125
158
  ## Choose the integration depth
126
159
 
127
160
  | Tier | You register | You get | You do not get |
@@ -131,12 +164,13 @@ The rules `defineAdapter` enforces, and the ones the runtime applies:
131
164
  | 2 | hooks + skill | only the lifecycle, context, guard, and next-turn behaviors separately captured for this client | uncaptured hook behaviors |
132
165
  | 3 | + native delivery surface | only captured live-push and reply-route modes | safe-point injection or child sessions unless separately captured |
133
166
 
134
- Installed hook wiring may reach tier 2, but the effective capability is still limited to
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.
167
+ Installed hook wiring may reach tier 2, but each effective hook capability still
168
+ requires exact client/version/platform evidence. Tier 3 has separate native
169
+ contracts: Claude Code Channel from 2.1.258 and Codex LocalDaemon from 0.152.1 on
170
+ macOS arm64, with current probes and exact session handshakes. Codex's earlier
171
+ remote-wrapper failure remains historical evidence. Ordinary launch now preserves
172
+ workspace identity; the full installed product matrix, including negative controls
173
+ and fallback, backs its native claim.
140
174
 
141
175
  ## Normalize hook input
142
176
 
@@ -81,12 +81,12 @@ a third exact-version certification check. A binding says what that current gene
81
81
  exposes and whether its lease is current, while recipient policy says whether it may spend a
82
82
  turn.
83
83
 
84
- Claude Code is the one client that proved the live seam: a Channel capture on 2.1.258,
85
- confirmed on 2.1.260 through the installed package, admitted by minimum-plus-probe rather
86
- than an exact version. Codex's queue transport passed its capture and the capability was
87
- withdrawn, because the mode it needs hides the session's workspace. Gemini CLI and Kimi
88
- Code have exact-version next-turn evidence only; Grok and generic MCP use inbox polling.
89
- The seam exists for every adapter; only one has earned it.
84
+ Claude Code Channel has installed-client evidence on 2.1.258 and 2.1.260; Codex
85
+ LocalDaemon has it on 0.152.1 and 0.153.4. Both use a captured platform minimum,
86
+ current probe and exact session handshake. Codex preserves the ordinary client
87
+ launch and verifies its registered thread and workspace; ACC owns no vendor daemon
88
+ lifecycle. Gemini CLI and Kimi Code have exact-version next-turn evidence only;
89
+ Grok and generic MCP use inbox polling.
90
90
 
91
91
  ## Storage and workspace identity
92
92
 
@@ -101,17 +101,29 @@ file ACC writes is an optional `acc.workspace.json` explicitly requested through
101
101
  `acc config init`.
102
102
 
103
103
  A lone session can remain ephemeral. Durable state materialises when a second live session
104
- appears or the first claim, message, or handoff is committed. This makes “silent when
105
- alone” an architectural behavior, not a UI preference.
104
+ appears or the first claim, message, or handoff is committed. Solo presence therefore
105
+ does not require durable workspace history. Native turn hooks still supply the session's
106
+ own CLI arguments, so a peer joining later in the same turn does not require reattachment.
107
+ Without relevant coordination context, that identity header is the only projected content.
106
108
 
107
109
  ## Inbox, attention, and projection
108
110
 
109
- Inbox reads only messages addressed to the calling participant and advances that
110
- participant's receipt to `retrieved`. Reply validates ownership, creates an `answer` in the
111
+ Public inbox discovery uses core `listInbox`: bounded, read-only message summaries
112
+ addressed to the current participant. Exact body reads use `readInbox` and advance only
113
+ that participant's receipt to `retrieved`; the embedded core API still supports bulk
114
+ retrieval. History sync reads message records without receipt changes, using the same
115
+ summary pager or one exact id. Reply validates ownership, creates an `answer` in the
111
116
  same thread, and acknowledges the original atomically. No participant can advance another
112
117
  participant's receipt.
113
118
 
114
- Attention is computed from six explicit rules:
119
+ Decision lifecycle is derived from immutable message links, without a new record kind
120
+ or mutable decision store. Read copies carry bounded status; raw forensic snapshots stay
121
+ unchanged. Non-head decisions leave pending inbox, bulk delivery, and attention while
122
+ exact reads retain them. Replacement/withdrawal records inherit recipients before commit,
123
+ so offline readers receive the change without a forged acknowledgement. See
124
+ [the protocol contract](PROTOCOL.md#decision-lifecycle).
125
+
126
+ Attention is computed from six explicit rules for current obligations:
115
127
 
116
128
  | Priority | Kind | Observable trigger |
117
129
  |---|---|---|
@@ -129,6 +141,14 @@ Adapters project peer bodies in an attributed untrusted frame. If a complete bod
129
141
  fit, the projection keeps the message id and points to `acc inbox --message <id>` instead
130
142
  of silently truncating it.
131
143
 
144
+ For this participant's unresolved messages already `offered` or `retrieved`, the standard
145
+ projector combines reply and acknowledgement reminders into a compact count after new
146
+ message bodies. Queued obligations and claim conflicts remain individual attention items.
147
+ All pending headers stay discoverable through `acc inbox` pages; owned `acc status` retains
148
+ its complete attention list. Full bodies require an exact inbox read; projection
149
+ does not acknowledge or delete records. Explicit decision links, rather than the
150
+ projector or message age, retire old decision obligations.
151
+
132
152
  ## Hooks fail open
133
153
 
134
154
  Hook execution is bounded. If coordination state cannot be read or a decision cannot be
@@ -137,3 +157,34 @@ it, but a coordination tool must not be the reason a session stops working.
137
157
 
138
158
  Next: [Protocol](PROTOCOL.md) · [Capabilities](CAPABILITIES.md) ·
139
159
  [Security model](SECURITY_MODEL.md)
160
+
161
+ ## Managed runtime activation
162
+
163
+ The CLI owns installation generations under `<dataHome>/acc/runtime`, outside workspaces.
164
+ All five launch paths select a generation and publish an actual-process lease under one
165
+ admission mutex before loading workspace-capable code. Immutable launcher modules and
166
+ runtime directories preserve in-progress imports. Leases survive `main()` returning and
167
+ are removed only after confirmed process death. Parsed help, version, and update recovery
168
+ remain available when workspace admission is unavailable.
169
+
170
+ An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
171
+ stable package identity and the discovered integrity, and health-checks the staged runtime.
172
+ Installer detection and planning happen before exclusive admission. The worker then checks
173
+ ACC process leases and native binding PIDs, records `activating`, applies every integration,
174
+ and only publishes the new active pointer after all applications succeed. Partial refresh
175
+ remains fenced and repairs forward, with the old active pointer retained but normal
176
+ workspace admission unavailable until recovery completes. Native bindings cease holding
177
+ after observed SessionEnd cleanup or confirmed process death; the vendor daemon may remain
178
+ running. Unknown PIDs remain holds until lifecycle cleanup. Presence TTL, `acc finish`,
179
+ and delivery off alone do not prove native lifecycle end. ACC never manages the daemon.
180
+
181
+ Lock ownership never expires by age. Retained nonempty tombstones prevent delayed observers
182
+ from reclaiming a successor. Under an acquired lock, maintenance removes historical
183
+ bookkeeping only when no live or unknown contender could still reference it; a stopped
184
+ contender postpones cleanup. Admission also removes confirmed-dead process leases. None of
185
+ this metadata is agent conversation history or injected context.
186
+
187
+ The first upgrade from an unmanaged binary still requires a restart. ACC cannot fence old
188
+ direct binaries or external applications importing core. Native trust and client-side cache
189
+ activation remain vendor responsibilities; refreshed files do not establish observed
190
+ capabilities.