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.
Files changed (155) hide show
  1. package/README.md +87 -69
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-bootstrap.mjs +56 -0
  4. package/bin/acc-claude-channel.mjs +177 -0
  5. package/bin/acc-hook.mjs +94 -12
  6. package/bin/acc-mcp.mjs +6 -2
  7. package/bin/acc.mjs +13 -3
  8. package/docs/ADAPTER_AUTHORING.md +204 -0
  9. package/docs/ARCHITECTURE.md +131 -0
  10. package/docs/CAPABILITIES.md +117 -214
  11. package/docs/CLI.md +164 -0
  12. package/docs/CONCEPTS.md +134 -0
  13. package/docs/CONFIGURATION.md +147 -0
  14. package/docs/DESIGN_DECISIONS.md +89 -0
  15. package/docs/GETTING_STARTED.md +145 -0
  16. package/docs/GLOSSARY.md +26 -0
  17. package/docs/HOW_IT_WORKS.md +277 -0
  18. package/docs/MCP.md +94 -0
  19. package/docs/PROTOCOL.md +200 -0
  20. package/docs/RELEASING.md +115 -0
  21. package/docs/SECURITY_MODEL.md +131 -0
  22. package/docs/TROUBLESHOOTING.md +108 -0
  23. package/docs/WHY_ACC.md +61 -0
  24. package/docs/index.md +44 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +228 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +269 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +21 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +13 -2
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +22 -22
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +45 -5
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +150 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +199 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +21 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/package.json +11 -2
  51. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +22 -22
  53. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +54 -12
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
  57. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +68 -0
  58. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +22 -22
  60. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent-0.57.0.json +8 -0
  61. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-0.57.0.json +12 -0
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell-0.57.0.json +12 -0
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd-0.57.0.json +8 -0
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart-0.57.0.json +8 -0
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +293 -0
  66. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
  67. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +31 -13
  68. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
  69. package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
  70. package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
  71. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +22 -22
  72. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +11 -9
  73. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  74. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  75. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  76. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  77. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  78. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  79. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  80. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  81. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +22 -22
  82. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
  83. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  84. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +52 -18
  85. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  86. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
  87. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +9 -1
  88. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +7 -2
  89. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
  90. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
  91. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
  92. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +28 -4
  93. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/cli/src/args.mjs +12 -31
  95. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +70 -5
  96. package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
  97. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +111 -12
  98. package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
  99. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  100. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  101. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  102. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +131 -0
  103. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  104. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
  105. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
  106. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  107. package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
  108. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  109. package/node_modules/@agents-can-communicate/core/src/service.mjs +21 -10
  110. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
  111. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  112. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
  113. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  114. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  115. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +131 -0
  116. package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
  117. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  118. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
  119. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +190 -105
  120. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  121. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +70 -10
  122. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
  123. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +89 -5
  124. package/node_modules/@agents-can-communicate/installer/src/index.mjs +10 -2
  125. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
  126. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +112 -12
  127. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +54 -2
  128. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
  129. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  130. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  131. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  132. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
  133. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
  134. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  135. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  137. package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
  138. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  139. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +64 -90
  140. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  141. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  143. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  144. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  145. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  146. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  147. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  148. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
  149. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
  150. package/package.json +20 -1
  151. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  152. package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
  153. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  154. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  155. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
@@ -0,0 +1,210 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmod, mkdir, readdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protocol";
6
+
7
+ // The reversible shell half of native delivery: one marked PATH block in the
8
+ // user's zsh rc and one per-command shim in an ACC-owned directory. The shim
9
+ // keeps the user's command name, runs the bounded bootstrap check, and then
10
+ // replaces itself with the real vendor executable through `exec`; ACC is never
11
+ // the parent of a model session. Every byte ACC writes is recorded with its
12
+ // hash so uninstall removes only what is still ACC's and refuses a block or
13
+ // shim someone has edited.
14
+
15
+ export const BLOCK_BEGIN = "# >>> agents-can-communicate native delivery >>>";
16
+ export const BLOCK_END = "# <<< agents-can-communicate native delivery <<<";
17
+ export const SHIM_MARKER = "# agents-can-communicate native delivery shim";
18
+ export const SUPPORTED_SHELLS = Object.freeze(["zsh"]);
19
+ export const SHIM_POLICIES = Object.freeze(["actionable", "all"]);
20
+ const COMMAND_NAME = /^[a-z][a-z0-9_.-]*$/;
21
+
22
+ const usage = (message, details = {}) => { throw new AccError(EXIT.USAGE, message, details); };
23
+ const sha256 = text => createHash("sha256").update(text).digest("hex");
24
+ // Single-quoted POSIX literal: the only escaping is the quote itself, so no
25
+ // byte of a path or argument is ever interpreted by the shell.
26
+ export const shellLiteral = value => `'${String(value).replaceAll("'", "'\\''")}'`;
27
+ const isAbsolute = value => typeof value === "string" && path.isAbsolute(value);
28
+ const inside = (root, candidate) => {
29
+ const relative = path.relative(root, candidate);
30
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
31
+ };
32
+
33
+ export function renderPathBlock(shimDir) {
34
+ return `${BLOCK_BEGIN}\nexport PATH=${shellLiteral(shimDir)}:"$PATH"\n${BLOCK_END}\n`;
35
+ }
36
+
37
+ export function validateShimEntry(entry) {
38
+ if (entry === null || typeof entry !== "object") usage("a shim entry must be an object");
39
+ for (const key of Object.keys(entry)) {
40
+ if (!["adapterId", "command", "realExecutable", "prefixArgs", "livePolicy"].includes(key)) {
41
+ usage(`unknown shim entry field ${key}`, { key });
42
+ }
43
+ }
44
+ assertPortableId(entry.adapterId, "shim adapter id");
45
+ if (typeof entry.command !== "string" || !COMMAND_NAME.test(entry.command)) {
46
+ usage("a shim command must be a bare command name", { command: entry.command });
47
+ }
48
+ if (!isAbsolute(entry.realExecutable)) usage("a shim realExecutable must be an absolute path");
49
+ if (!Array.isArray(entry.prefixArgs) || entry.prefixArgs.some(arg => typeof arg !== "string"
50
+ || arg === "" || /[\0\n]/.test(arg))) {
51
+ usage("shim prefixArgs must be non-empty argument strings without NUL or newline");
52
+ }
53
+ if (!SHIM_POLICIES.includes(entry.livePolicy)) {
54
+ usage("a shim livePolicy must be actionable or all; off means no shim", { livePolicy: entry.livePolicy });
55
+ }
56
+ return Object.freeze({ ...entry, prefixArgs: Object.freeze([...entry.prefixArgs]) });
57
+ }
58
+
59
+ export function renderCommandShim({ node, bootstrap, dataHome, entry }) {
60
+ const shim = validateShimEntry(entry);
61
+ for (const [name, value] of Object.entries({ node, bootstrap, dataHome })) {
62
+ if (!isAbsolute(value)) usage(`shim ${name} must be an absolute path`, { [name]: value });
63
+ }
64
+ const real = shellLiteral(shim.realExecutable);
65
+ const check = [node, bootstrap, "--adapter", shim.adapterId, "--real-executable",
66
+ shim.realExecutable, "--data-home", dataHome].map(shellLiteral).join(" ");
67
+ const prefix = shim.prefixArgs.map(shellLiteral).join(" ");
68
+ return [
69
+ "#!/bin/sh",
70
+ `${SHIM_MARKER} for ${shellLiteral(shim.command)}. Generated; do not edit.`,
71
+ "# ACC_BYPASS=1 runs the vendor command untouched. A failed or missing check",
72
+ "# does the same: the vendor command is never blocked on ACC.",
73
+ 'if [ "${ACC_BYPASS-}" = "1" ]; then',
74
+ " unset ACC_NATIVE_DELIVERY_POLICY",
75
+ ` exec ${real} "$@"`,
76
+ "fi",
77
+ `if ${check} </dev/null >/dev/null 2>&1; then`,
78
+ ` ACC_NATIVE_DELIVERY_POLICY=${shellLiteral(shim.livePolicy)}`,
79
+ " export ACC_NATIVE_DELIVERY_POLICY",
80
+ ` exec ${real}${prefix === "" ? "" : ` ${prefix}`} "$@"`,
81
+ "fi",
82
+ "unset ACC_NATIVE_DELIVERY_POLICY",
83
+ `exec ${real} "$@"`,
84
+ "",
85
+ ].join("\n");
86
+ }
87
+
88
+ export function planShellBootstrap({ shell, rcFile, shimDir, entries = [], runtime = null }) {
89
+ if (!SUPPORTED_SHELLS.includes(shell)) {
90
+ return Object.freeze({ eligible: false, reasonCode: "unsupported_shell", shell: shell ?? null,
91
+ rcFile: null, shimDir: null, block: null, shims: Object.freeze([]), runtime: null });
92
+ }
93
+ if (!isAbsolute(rcFile) || !isAbsolute(shimDir)) usage("rcFile and shimDir must be absolute");
94
+ const validated = entries.map(validateShimEntry);
95
+ const commands = new Set();
96
+ for (const entry of validated) {
97
+ if (commands.has(entry.command)) usage(`duplicate shim command ${entry.command}`);
98
+ commands.add(entry.command);
99
+ // A shim that resolved to itself would loop forever; the real executable
100
+ // is resolved before the shim directory is ever on PATH.
101
+ if (inside(shimDir, entry.realExecutable)) {
102
+ usage("a shim realExecutable cannot live inside the shim directory",
103
+ { realExecutable: entry.realExecutable });
104
+ }
105
+ }
106
+ return Object.freeze({ eligible: true, reasonCode: null, shell, rcFile, shimDir,
107
+ block: renderPathBlock(shimDir), runtime,
108
+ shims: Object.freeze(validated.map(entry => Object.freeze({
109
+ path: path.join(shimDir, entry.command), entry }))) });
110
+ }
111
+
112
+ const defaultIo = Object.freeze({ readFile, writeFile, mkdir, chmod, rename, rm, rmdir, readdir });
113
+
114
+ async function readText(io, file) {
115
+ try {
116
+ return await io.readFile(file, "utf8");
117
+ } catch (error) {
118
+ if (error.code === "ENOENT") return null;
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ async function writeAtomic(io, file, content, mode) {
124
+ const temporary = `${file}.${process.pid}.tmp`;
125
+ await io.writeFile(temporary, content, { mode });
126
+ await io.rename(temporary, file);
127
+ await io.chmod(file, mode);
128
+ }
129
+
130
+ // The block's exact span inside the rc text, or null. The block is matched by
131
+ // its two sentinel lines so a user's other lines are never touched.
132
+ export function locateBlock(text) {
133
+ if (typeof text !== "string") return null;
134
+ const begin = text.indexOf(BLOCK_BEGIN);
135
+ if (begin === -1 || (begin > 0 && text[begin - 1] !== "\n")) return null;
136
+ const endLine = text.indexOf(BLOCK_END, begin);
137
+ if (endLine === -1) return null;
138
+ const end = text.indexOf("\n", endLine);
139
+ return { start: begin, end: end === -1 ? text.length : end + 1 };
140
+ }
141
+
142
+ export async function installShellBootstrap({ plan, io = defaultIo }) {
143
+ if (!plan?.eligible) return { ok: false, reasonCode: plan?.reasonCode ?? "unsupported_shell" };
144
+ const { node, bootstrap, dataHome } = plan.runtime ?? {};
145
+ const current = await readText(io, plan.rcFile);
146
+ const span = locateBlock(current);
147
+ const existing = span === null ? null : current.slice(span.start, span.end);
148
+ if (existing !== null && existing !== plan.block) {
149
+ return { ok: false, reasonCode: "rc_block_modified", rcFile: plan.rcFile };
150
+ }
151
+ await io.mkdir(plan.shimDir, { recursive: true, mode: 0o700 });
152
+ await io.chmod(plan.shimDir, 0o700);
153
+ const shims = [];
154
+ for (const shim of plan.shims) {
155
+ const content = renderCommandShim({ node, bootstrap, dataHome, entry: shim.entry });
156
+ await writeAtomic(io, shim.path, content, 0o700);
157
+ shims.push({ path: shim.path, command: shim.entry.command, sha256: sha256(content) });
158
+ }
159
+ let appended = false;
160
+ if (existing === null) {
161
+ const base = current ?? "";
162
+ const separator = base === "" || base.endsWith("\n") ? "" : "\n";
163
+ await writeAtomic(io, plan.rcFile, `${base}${separator}${plan.block}`, 0o600);
164
+ appended = true;
165
+ }
166
+ return { ok: true, reasonCode: null, shell: plan.shell, shimDir: plan.shimDir,
167
+ rcFile: { path: plan.rcFile, blockSha256: sha256(plan.block), appended },
168
+ shims };
169
+ }
170
+
171
+ export async function uninstallShellBootstrap({ ownership, io = defaultIo }) {
172
+ const result = { ok: true, reasonCode: null, removedShims: [], keptShims: [],
173
+ missingShims: [], rcBlock: "absent" };
174
+ for (const shim of ownership?.shims ?? []) {
175
+ const content = await readText(io, shim.path);
176
+ if (content === null) { result.missingShims.push(shim.path); continue; }
177
+ if (sha256(content) !== shim.sha256) { result.keptShims.push(shim.path); continue; }
178
+ await io.rm(shim.path, { force: true });
179
+ result.removedShims.push(shim.path);
180
+ }
181
+ const shimDir = ownership?.shimDir;
182
+ let remaining = [];
183
+ if (typeof shimDir === "string") {
184
+ const names = await io.readdir(shimDir).catch(() => []);
185
+ for (const name of names) {
186
+ const content = await readText(io, path.join(shimDir, name));
187
+ if (content !== null && content.includes(SHIM_MARKER)) remaining.push(name);
188
+ }
189
+ }
190
+ const rcFile = ownership?.rcFile;
191
+ const text = typeof rcFile?.path === "string" ? await readText(io, rcFile.path) : null;
192
+ const span = locateBlock(text);
193
+ if (span !== null) {
194
+ const block = text.slice(span.start, span.end);
195
+ if (sha256(block) !== rcFile.blockSha256) {
196
+ result.rcBlock = "modified";
197
+ result.ok = false;
198
+ result.reasonCode = "rc_block_modified";
199
+ } else if (remaining.length > 0) {
200
+ result.rcBlock = "kept";
201
+ } else {
202
+ await writeAtomic(io, rcFile.path, text.slice(0, span.start) + text.slice(span.end), 0o600);
203
+ result.rcBlock = "removed";
204
+ }
205
+ }
206
+ if (typeof shimDir === "string" && remaining.length === 0 && result.keptShims.length === 0) {
207
+ await io.rmdir(shimDir).catch(() => null);
208
+ }
209
+ return result;
210
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.1.18",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,79 @@
1
+ class InputValidationError extends Error {}
2
+
3
+ const fail = (path, message) => {
4
+ throw new InputValidationError(`${path} ${message}`);
5
+ };
6
+
7
+ const isObject = value => value !== null && typeof value === "object"
8
+ && !Array.isArray(value);
9
+
10
+ function matches(schema, value, path) {
11
+ try {
12
+ validateSchema(schema, value, path);
13
+ return true;
14
+ } catch (error) {
15
+ if (!(error instanceof InputValidationError)) throw error;
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function validateSchema(schema, value, path) {
21
+ if (schema.const !== undefined && !Object.is(value, schema.const)) {
22
+ fail(path, `must equal ${JSON.stringify(schema.const)}`);
23
+ }
24
+ if (schema.enum !== undefined && !schema.enum.some(item => Object.is(item, value))) {
25
+ fail(path, `must be one of ${schema.enum.map(item => JSON.stringify(item)).join(", ")}`);
26
+ }
27
+
28
+ const objectKeywords = schema.type === "object" || schema.properties !== undefined
29
+ || schema.required !== undefined || schema.additionalProperties !== undefined;
30
+ if (objectKeywords) {
31
+ if (!isObject(value)) fail(path, "must be an object");
32
+ const properties = schema.properties ?? {};
33
+ for (const key of schema.required ?? []) {
34
+ if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, "is required");
35
+ }
36
+ if (schema.additionalProperties === false) {
37
+ for (const key of Object.keys(value)) {
38
+ if (!Object.hasOwn(properties, key)) fail(`${path}.${key}`, "is not a known field");
39
+ }
40
+ }
41
+ for (const [key, child] of Object.entries(properties)) {
42
+ if (Object.hasOwn(value, key)) validateSchema(child, value[key], `${path}.${key}`);
43
+ }
44
+ } else if (schema.type === "array") {
45
+ if (!Array.isArray(value)) fail(path, "must be an array");
46
+ if (schema.items !== undefined) {
47
+ value.forEach((item, index) => validateSchema(schema.items, item, `${path}[${index}]`));
48
+ }
49
+ } else if (schema.type === "string" && typeof value !== "string") {
50
+ fail(path, "must be a string");
51
+ } else if (schema.type === "boolean" && typeof value !== "boolean") {
52
+ fail(path, "must be a boolean");
53
+ } else if (schema.type === "integer" && !Number.isInteger(value)) {
54
+ fail(path, "must be an integer");
55
+ }
56
+
57
+ if (schema.minimum !== undefined && value < schema.minimum) {
58
+ fail(path, `must be at least ${schema.minimum}`);
59
+ }
60
+ if (schema.maximum !== undefined && value > schema.maximum) {
61
+ fail(path, `must be at most ${schema.maximum}`);
62
+ }
63
+ if (schema.anyOf !== undefined
64
+ && !schema.anyOf.some(branch => matches(branch, value, path))) {
65
+ fail(path, "must match an accepted shape");
66
+ }
67
+ if (schema.oneOf !== undefined
68
+ && schema.oneOf.filter(branch => matches(branch, value, path)).length !== 1) {
69
+ fail(path, "must match exactly one accepted shape");
70
+ }
71
+ if (schema.not !== undefined && matches(schema.not, value, path)) {
72
+ fail(path, "contains fields that cannot be combined");
73
+ }
74
+ return value;
75
+ }
76
+
77
+ export function validateToolInput(schema, value) {
78
+ return validateSchema(schema, value, "arguments");
79
+ }
@@ -15,42 +15,37 @@ function escapeText(value) {
15
15
  return result;
16
16
  }
17
17
 
18
+ function escapePeerValue(value) {
19
+ if (typeof value === "string") return escapeText(value);
20
+ if (Array.isArray(value)) return value.map(escapePeerValue);
21
+ if (value !== null && typeof value === "object") {
22
+ return Object.fromEntries(Object.entries(value)
23
+ .map(([key, child]) => [key, escapePeerValue(child)]));
24
+ }
25
+ return value;
26
+ }
27
+
18
28
  const attributedMessage = message => ({
19
- messageId: message.messageId,
20
- from: message.fromSessionId,
21
- type: message.type,
22
- priority: message.priority,
23
- requiresAck: message.requiresAck,
24
- sentAt: message.sentAt,
29
+ ...escapePeerValue(message),
25
30
  trust: "untrusted peer content",
26
- subject: escapeText(message.subject),
27
- body: escapeText(message.body),
28
31
  });
29
32
 
30
- export async function readResource(uri, { service, participantId, workspaceId }) {
31
- const snapshot = await service.store.snapshot(workspaceId);
33
+ export async function readResource(uri, { service, participantId, workspaceId, session }) {
32
34
  switch (uri) {
33
- case "acc://snapshot":
34
- return { ...snapshot,
35
- messages: snapshot.messages.map(attributedMessage) };
35
+ case "acc://snapshot": {
36
+ const { snapshot } = await service.sync({ workspaceId, scope: "full" });
37
+ return { ...snapshot, messages: snapshot.messages.map(attributedMessage) };
38
+ }
36
39
  case "acc://roster":
37
40
  return (await service.sync({ workspaceId })).roster;
38
- case "acc://workstreams":
39
- return snapshot.workstreams;
40
- case "acc://tasks":
41
- return snapshot.tasks;
42
41
  case "acc://inbox": {
43
- const mine = new Set(snapshot.receipts
44
- .filter(receipt => receipt.recipientParticipantId === participantId)
45
- .map(receipt => receipt.messageId));
46
- // A participant sees what was addressed to it, plus what it sent, so a
47
- // fresh reader can follow its own thread.
48
- return snapshot.messages
49
- .filter(message => mine.has(message.messageId)
50
- || message.toParticipantIds.includes(participantId)
51
- || snapshot.sessions.some(session => session.sessionId === message.fromSessionId
52
- && session.participantId === participantId))
53
- .map(attributedMessage);
42
+ if (session === undefined) {
43
+ throw new AccError(EXIT.DATA, "the inbox resource requires a resolved session",
44
+ { participantId });
45
+ }
46
+ const inbox = await service.readInbox({ workspaceId, sessionId: session.sessionId,
47
+ generation: session.generation });
48
+ return inbox.map(item => attributedMessage(item.message));
54
49
  }
55
50
  default:
56
51
  throw new AccError(EXIT.DATA, `unknown resource: ${uri}`, { uri });
@@ -1,14 +1,18 @@
1
- import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
- import { noteNudge } from "@agents-can-communicate/core";
1
+ import { createRequire } from "node:module";
2
+
3
+ import { AccError, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIONS }
4
+ from "@agents-can-communicate/protocol";
3
5
  import { clearSessionBinding, loadSessionBinding, storeSessionBinding }
4
6
  from "@agents-can-communicate/adapter-sdk";
5
7
 
6
8
  import { readResource } from "./resources.mjs";
9
+ import { validateToolInput } from "./input-validator.mjs";
7
10
  import { MCP_CAPABILITIES, PUBLIC_TOOLS, RESOURCES } from "./tools.mjs";
8
11
 
9
12
  export const PROTOCOL_VERSION = "2026-07-28";
10
13
  export const SUPPORTED_VERSIONS = Object.freeze([PROTOCOL_VERSION]);
11
- const SERVER_INFO = Object.freeze({ name: "agents-can-communicate", version: "0.0.0" });
14
+ const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version;
15
+ const SERVER_INFO = Object.freeze({ name: "agents-can-communicate", version: PACKAGE_VERSION });
12
16
 
13
17
  const META = "io.modelcontextprotocol";
14
18
  const HEARTBEAT_CADENCE_MS = 60_000;
@@ -72,20 +76,40 @@ async function resolveSession(context) {
72
76
  return session;
73
77
  }
74
78
 
75
- // Kept for clients released before acc_inbox existed. New clients should use
76
- // the narrow inbox tool, but removing mail from acc_sync would strand deployed
77
- // clients that only know this response field. Returning a body is delivery, so
78
- // only those returned messages advance to injected.
79
- async function syncWithMail(service, owner, context, args) {
80
- const sync = await service.sync({ ...owner, cursor: args.cursor ?? null,
81
- scope: args.scope, limit: args.limit });
82
- const messages = await service.pendingMessages({ workspaceId: context.workspaceId,
83
- participantId: context.participantId, exceptSessionId: owner.sessionId });
84
- for (const message of messages) {
85
- await service.markDelivery({ ...owner, messageId: message.messageId,
86
- state: "injected" }).catch(() => null);
79
+ export async function recordAndOffer({ record, router, selectMessage = value => value }) {
80
+ const recorded = await record();
81
+ const message = selectMessage(recorded);
82
+ if (router === null || router === undefined
83
+ || !Array.isArray(message?.toParticipantIds) || message.toParticipantIds.length === 0) {
84
+ return { recorded, delivery: [] };
85
+ }
86
+ try {
87
+ return { recorded, delivery: await router.offer(message) };
88
+ } catch {
89
+ return { recorded, delivery: message.toParticipantIds.map(recipientParticipantId => ({
90
+ recipientParticipantId, outcome: "queued", transport: "durable",
91
+ errorCode: "transport_error",
92
+ })) };
93
+ }
94
+ }
95
+
96
+ const clientMessageId = (args, service) =>
97
+ args.clientMessageId ?? service.ids.next("client");
98
+
99
+ function obligationFor(kind, explicit, addressed) {
100
+ if (!GENERIC_MESSAGE_KINDS.includes(kind)) {
101
+ const command = kind === "answer"
102
+ ? "acc_reply" : kind === "handoff" ? "acc_finish" : null;
103
+ throw new AccError(EXIT.USAGE, command === null
104
+ ? `unknown message kind: ${kind}` : `${kind} messages require ${command}`);
105
+ }
106
+ const obligation = explicit ?? VALID_OBLIGATIONS[kind][0];
107
+ if (!VALID_OBLIGATIONS[kind].includes(obligation)
108
+ || (!addressed && obligation !== "none")) {
109
+ throw new AccError(EXIT.USAGE,
110
+ `message obligation ${obligation} is invalid for ${kind}`);
87
111
  }
88
- return messages.length === 0 ? sync : { ...sync, messages };
112
+ return obligation;
89
113
  }
90
114
 
91
115
  /**
@@ -93,7 +117,7 @@ async function syncWithMail(service, owner, context, args) {
93
117
  *
94
118
  * The hook runtime hands a session its pending messages when it builds a turn,
95
119
  * and marks them delivered. An MCP client has no turn and no hook, and no tool
96
- * ever handed it anything: it saw a `direct_request` line carrying a subject and
120
+ * ever handed it anything: it saw a `reply_required` line carrying a subject and
97
121
  * an id, and to read what a peer had actually said it had to ask for the whole
98
122
  * snapshot and search every message in the workspace for its own name.
99
123
  *
@@ -102,7 +126,7 @@ async function syncWithMail(service, owner, context, args) {
102
126
  * that had answered it.
103
127
  *
104
128
  * Returning them here is delivery, in the same sense and with the same honesty
105
- * as the turn: what is handed over is marked `injected`, and nothing else is.
129
+ * as the turn: what is handed over is marked `retrieved`, and nothing else is.
106
130
  * Acknowledgement stays a separate act, because being shown something is not
107
131
  * agreeing to it.
108
132
  */
@@ -113,72 +137,66 @@ async function callTool(name, args, context) {
113
137
  const service = context.service;
114
138
 
115
139
  switch (name) {
140
+ case "acc_status":
141
+ return service.collectStatus({});
116
142
  case "acc_sync":
117
- return syncWithMail(service, owner, context, args);
143
+ return service.sync({ ...owner, cursor: args.cursor ?? null,
144
+ scope: args.scope, limit: args.limit });
118
145
  case "acc_work":
119
- if (args.clear === true) return service.clearIntent({ ...owner });
146
+ if (args.clear === true) {
147
+ await service.clearIntent({ ...owner });
148
+ return { cleared: true };
149
+ }
120
150
  return service.setIntent({ ...owner, summary: args.summary, mode: args.mode,
121
- state: args.state, workstreamId: args.workstreamId ?? null,
122
- resourceHints: args.resourceHints ?? [] });
151
+ state: args.state, resourceHints: args.resourceHints ?? [] });
123
152
  case "acc_claim":
124
- if (args.action === "release") return service.releaseClaim({ ...owner,
125
- claimId: args.claimId }) ?? { released: args.claimId };
126
153
  if (args.action === "renew") return service.renewClaim({ ...owner,
127
154
  claimId: args.claimId, leaseSeconds: args.leaseSeconds });
128
155
  return service.acquireClaim({ ...owner, resource: args.resource,
129
156
  mode: args.mode ?? "exclusive", enforcement: "advisory",
130
157
  reason: args.reason ?? "unspecified", leaseSeconds: args.leaseSeconds });
158
+ case "acc_release":
159
+ await service.releaseClaim({ ...owner, claimId: args.claimId });
160
+ return { released: args.claimId };
131
161
  case "acc_message": {
132
- const message = await service.sendMessage({ ...owner, toParticipantIds: args.to ?? [],
133
- subject: args.subject, body: args.body, type: args.type ?? "note",
134
- priority: args.priority, requiresAck: args.requiresAck === true,
135
- workstreamId: args.workstreamId ?? null });
136
- // A note that reads like it wants a reply was sent fire-and-forget; hand
137
- // the nudge back with the message so the model that sent it can reconsider.
138
- const advice = noteNudge(message);
139
- return advice ? { ...message, advice } : message;
162
+ const kind = args.kind ?? "note";
163
+ const toParticipantIds = args.to ?? [];
164
+ const routed = await recordAndOffer({ router: context.deliveryRouter, record: () =>
165
+ service.sendMessage({ ...owner, clientMessageId: clientMessageId(args, service),
166
+ toParticipantIds, subject: args.subject, body: args.body, kind,
167
+ obligation: obligationFor(kind, args.obligation, toParticipantIds.length > 0) }) });
168
+ const message = routed.recorded;
169
+ return { message, delivery: routed.delivery };
140
170
  }
141
171
  case "acc_inbox":
142
172
  return service.readInbox({ ...owner, messageId: args.messageId });
143
- case "acc_reply":
144
- return service.replyToMessage({ ...owner, messageId: args.messageId,
145
- body: args.body, subject: args.subject, type: args.type, priority: args.priority });
146
- case "acc_task":
147
- if (args.action === "claim") return service.claimTask({ ...owner,
148
- taskId: args.taskId, force: args.force === true });
149
- if (args.action === "decline") return service.declineTask({ ...owner,
150
- taskId: args.taskId, reason: args.reason });
151
- if (args.action === "transition") return service.transitionTask({ ...owner,
152
- taskId: args.taskId, state: args.state });
153
- return service.createTask({ ...owner, workstreamId: args.workstreamId,
154
- title: args.title, detail: args.detail, taskId: args.taskId,
155
- assigneeParticipantId: args.assigneeParticipantId,
156
- dependsOn: args.dependsOn ?? [] });
157
- case "acc_request":
158
- return service.requestWork({ ...owner, toParticipantId: args.toParticipantId,
159
- title: args.title, detail: args.detail, workstreamId: args.workstreamId,
160
- priority: args.priority, dependsOn: args.dependsOn ?? [] });
173
+ case "acc_reply": {
174
+ const routed = await recordAndOffer({ router: context.deliveryRouter,
175
+ selectMessage: value => value.reply,
176
+ record: () => service.replyToMessage({ ...owner, messageId: args.messageId,
177
+ body: args.body, subject: args.subject,
178
+ clientMessageId: clientMessageId(args, service) }) });
179
+ return { message: routed.recorded.reply, delivery: routed.delivery };
180
+ }
181
+ case "acc_request": {
182
+ const routed = await recordAndOffer({ router: context.deliveryRouter,
183
+ record: () => service.sendMessage({ ...owner,
184
+ clientMessageId: clientMessageId(args, service),
185
+ toParticipantIds: [args.toParticipantId], kind: "request", obligation: "reply",
186
+ subject: args.title, body: args.detail ?? args.title }) });
187
+ return { message: routed.recorded, delivery: routed.delivery };
188
+ }
161
189
  case "acc_ack":
162
- return service.markDelivery({ ...owner, messageId: args.messageId,
163
- state: args.state ?? "acknowledged" });
164
- case "acc_decide":
165
- return service.recordDecision({ ...owner, title: args.title, outcome: args.outcome,
166
- authority: args.authority ?? "workstream", workstreamId: args.workstreamId ?? null,
167
- decidedBy: args.decidedBy, supersedes: args.supersedes ?? null,
168
- humanConfirmed: args.humanConfirmed === true });
169
- case "acc_workstream":
170
- if (args.action === "coordinate") {
171
- return service.acquireCoordinator({ ...owner, workstreamId: args.workstreamId });
172
- }
173
- if (args.action === "release") {
174
- return service.releaseCoordinator({ ...owner, workstreamId: args.workstreamId });
175
- }
176
- return service.createWorkstream({ ...owner, title: args.title,
177
- objective: args.objective });
178
- case "acc_finish":
179
- return service.finishSession({ ...owner, goal: args.goal, status: args.status,
190
+ return service.acknowledgeMessage({ ...owner, messageId: args.messageId });
191
+ case "acc_finish": {
192
+ const routed = await recordAndOffer({ router: context.deliveryRouter,
193
+ selectMessage: value => value.message,
194
+ record: () => service.finishSession({ ...owner,
195
+ clientMessageId: clientMessageId(args, service), goal: args.goal, status: args.status,
180
196
  completed: args.completed ?? [], remaining: args.remaining ?? [],
181
- blockers: args.blockers ?? [], toParticipantId: args.toParticipantId ?? null });
197
+ blockers: args.blockers ?? [], toParticipantId: args.toParticipantId }) });
198
+ return { message: routed.recorded.message, delivery: routed.delivery };
199
+ }
182
200
  default:
183
201
  throw new AccError(EXIT.USAGE, `unknown tool: ${name}`, { name });
184
202
  }
@@ -198,15 +216,27 @@ async function handle(message, context) {
198
216
  case "resources/list":
199
217
  return complete({ resources: [...RESOURCES] });
200
218
  case "resources/read": {
201
- const value = await readResource(params.uri, context);
219
+ // Snapshot and roster are observation-only. Inbox is a delivery boundary:
220
+ // resolve this configured participant's durable session and let the core
221
+ // inbox service record that the returned bodies were retrieved.
222
+ const resourceContext = params.uri === "acc://inbox"
223
+ ? { ...context, session: await resolveSession(context) }
224
+ : context;
225
+ const value = await readResource(params.uri, resourceContext);
202
226
  return complete({ contents: [{ uri: params.uri, mimeType: "application/json",
203
227
  text: JSON.stringify(value, null, 2) }] });
204
228
  }
205
229
  case "tools/call": {
206
230
  try {
207
- const value = await callTool(params.name, params.arguments ?? {}, context);
231
+ const args = params.arguments === undefined ? {} : params.arguments;
232
+ const tool = PUBLIC_TOOLS.find(candidate => candidate.name === params.name);
233
+ if (tool === undefined) {
234
+ throw new AccError(EXIT.USAGE, `unknown tool: ${params.name}`, { name: params.name });
235
+ }
236
+ validateToolInput(tool.inputSchema, args);
237
+ const value = await callTool(params.name, args, context);
208
238
  return complete({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
209
- structuredContent: JSON.stringify(value) });
239
+ structuredContent: value });
210
240
  } catch (error) {
211
241
  // A failing operation is a tool result, not a transport failure: the
212
242
  // model must see it and be able to react.