agents-can-communicate 0.1.17 → 0.2.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 (137) hide show
  1. package/README.md +76 -138
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-hook.mjs +96 -12
  4. package/bin/acc-mcp.mjs +6 -2
  5. package/bin/acc.mjs +6 -1
  6. package/docs/ADAPTER_AUTHORING.md +172 -0
  7. package/docs/ARCHITECTURE.md +131 -0
  8. package/docs/CAPABILITIES.md +105 -197
  9. package/docs/CLI.md +157 -0
  10. package/docs/CONCEPTS.md +134 -0
  11. package/docs/CONFIGURATION.md +143 -0
  12. package/docs/DESIGN_DECISIONS.md +89 -0
  13. package/docs/GETTING_STARTED.md +145 -0
  14. package/docs/GLOSSARY.md +26 -0
  15. package/docs/MCP.md +94 -0
  16. package/docs/PROTOCOL.md +200 -0
  17. package/docs/RELEASING.md +109 -0
  18. package/docs/SECURITY_MODEL.md +131 -0
  19. package/docs/TROUBLESHOOTING.md +102 -0
  20. package/docs/WHY_ACC.md +61 -0
  21. package/docs/index.md +42 -0
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +80 -160
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +15 -5
  33. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
  34. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
  35. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
  36. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
  37. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
  38. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
  41. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
  42. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +80 -160
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +21 -12
  44. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +80 -160
  47. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
  50. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
  51. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +10 -4
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
  56. package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
  57. package/node_modules/@agents-can-communicate/adapter-grok/package.json +14 -0
  58. package/node_modules/@agents-can-communicate/adapter-grok/plugin/hooks/hooks.json +61 -0
  59. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +152 -0
  60. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +61 -0
  61. package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +127 -0
  62. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +101 -0
  63. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  64. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  65. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  66. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  67. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  68. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  69. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  70. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  71. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +80 -160
  72. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +10 -4
  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 +34 -18
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +139 -224
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
  80. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  81. package/node_modules/@agents-can-communicate/cli/src/args.mjs +13 -29
  82. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
  83. package/node_modules/@agents-can-communicate/cli/src/help.mjs +5 -6
  84. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +12 -3
  85. package/node_modules/@agents-can-communicate/cli/src/main.mjs +109 -109
  86. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +1 -1
  87. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  88. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  89. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  90. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
  91. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  92. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +118 -0
  93. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -2
  94. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  95. package/node_modules/@agents-can-communicate/core/src/ports.mjs +3 -2
  96. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  97. package/node_modules/@agents-can-communicate/core/src/service.mjs +14 -10
  98. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +70 -20
  99. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  100. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -258
  101. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  102. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  103. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
  104. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  105. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  106. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +156 -60
  107. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
  109. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
  110. package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
  111. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
  112. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
  113. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  114. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  115. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  116. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +109 -71
  117. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +74 -93
  118. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  119. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  120. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  121. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  122. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
  123. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  124. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  129. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  130. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  131. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +86 -28
  132. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +121 -27
  133. package/package.json +22 -1
  134. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  135. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  136. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  137. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { lstat, mkdir, readdir, readFile, rename, rm, rmdir, writeFile }
3
+ from "node:fs/promises";
3
4
  import path from "node:path";
4
5
 
5
6
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
@@ -109,7 +110,7 @@ async function saveOwnership({ dataHome, record }) {
109
110
  * runtime, and leaves the bundle inside the client exactly where it was.
110
111
  */
111
112
  export async function recordInstall({ dataHome, adapterId, version, accVersion = null,
112
- artifacts }) {
113
+ artifacts, createdDirectories = [] }) {
113
114
  const stamped = await Promise.all(artifacts.map(async artifact => ({
114
115
  path: artifact.path,
115
116
  kind: artifact.kind ?? "file",
@@ -118,14 +119,99 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
118
119
  sha256: artifact.kind === "merge" ? null : await fingerprintFor(artifact),
119
120
  })));
120
121
  const record = await loadOwnership({ dataHome });
122
+ const previous = record.installs.find(install => install.adapterId === adapterId);
123
+ const directories = [...new Set([
124
+ ...(previous?.createdDirectories ?? []), ...createdDirectories,
125
+ ])].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length
126
+ || left.localeCompare(right));
121
127
  await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
122
128
  installs: [...record.installs.filter(install => install.adapterId !== adapterId),
123
- { adapterId, version, accVersion, artifacts: stamped }] } });
129
+ { adapterId, version, accVersion, artifacts: stamped,
130
+ ...(directories.length === 0 ? {} : { createdDirectories: directories }) }] } });
124
131
  }
125
132
 
126
133
  const installFor = (record, adapterId) =>
127
134
  record.installs.find(install => install.adapterId === adapterId) ?? null;
128
135
 
136
+ const inside = (home, candidate) => {
137
+ const relative = path.relative(home, candidate);
138
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`)
139
+ && !path.isAbsolute(relative);
140
+ };
141
+
142
+ async function hasSymlinkAncestor(home, candidate) {
143
+ let current = candidate;
144
+ while (inside(home, current)) {
145
+ const stat = await lstat(current).catch(error => {
146
+ if (error.code === "ENOENT") return null;
147
+ throw error;
148
+ });
149
+ if (stat?.isSymbolicLink()) return true;
150
+ current = path.dirname(current);
151
+ }
152
+ return false;
153
+ }
154
+
155
+ /** Return planned artifact parents that do not yet exist under the client home. */
156
+ export async function missingArtifactParents({ home, artifacts }) {
157
+ if (typeof home !== "string") return [];
158
+ const root = path.resolve(home);
159
+ const missing = new Set();
160
+ for (const artifact of artifacts) {
161
+ let directory = path.dirname(path.resolve(artifact.path));
162
+ while (inside(root, directory)) {
163
+ try {
164
+ await lstat(directory);
165
+ break;
166
+ } catch (error) {
167
+ if (error.code !== "ENOENT") throw error;
168
+ missing.add(directory);
169
+ directory = path.dirname(directory);
170
+ }
171
+ }
172
+ }
173
+ return [...missing].sort((left, right) =>
174
+ left.split(path.sep).length - right.split(path.sep).length || left.localeCompare(right));
175
+ }
176
+
177
+ /** Remove recorded parents deepest-first, but only while each remains an empty directory. */
178
+ export async function removeEmptyOwnedDirectories({ home, directories = [] }) {
179
+ const result = { removed: [], kept: [], missing: [] };
180
+ const root = typeof home === "string" ? path.resolve(home) : null;
181
+ const ordered = [...new Set(directories)].sort((left, right) =>
182
+ right.split(path.sep).length - left.split(path.sep).length || left.localeCompare(right));
183
+ for (const directory of ordered) {
184
+ if (root === null || !inside(root, path.resolve(directory))) {
185
+ result.kept.push(directory);
186
+ continue;
187
+ }
188
+ let stat;
189
+ try {
190
+ stat = await lstat(directory);
191
+ } catch (error) {
192
+ if (error.code === "ENOENT") { result.missing.push(directory); continue; }
193
+ throw error;
194
+ }
195
+ if (!stat.isDirectory() || stat.isSymbolicLink()
196
+ || await hasSymlinkAncestor(root, path.dirname(directory))) {
197
+ result.kept.push(directory);
198
+ continue;
199
+ }
200
+ try {
201
+ await rmdir(directory);
202
+ result.removed.push(directory);
203
+ } catch (error) {
204
+ if (["ENOTEMPTY", "EEXIST"].includes(error.code)) {
205
+ result.kept.push(directory);
206
+ continue;
207
+ }
208
+ if (error.code === "ENOENT") { result.missing.push(directory); continue; }
209
+ throw error;
210
+ }
211
+ }
212
+ return result;
213
+ }
214
+
129
215
  /** Compare what was written against what is there now. Read-only. */
130
216
  export async function verifyOwned({ dataHome, adapterId }) {
131
217
  const install = installFor(await loadOwnership({ dataHome }), adapterId);
@@ -141,17 +227,12 @@ export async function verifyOwned({ dataHome, adapterId }) {
141
227
  return result;
142
228
  }
143
229
 
144
- /**
145
- * Remove the files this adapter's install wrote, and only those.
146
- *
147
- * A modified file is kept and reported. A merge artifact is never deleted at
148
- * all: the user owns that file and ACC owns some entries inside it, which is the
149
- * adapter's own uninstall to unpick because it knows the format.
150
- */
151
- export async function removeOwned({ dataHome, adapterId }) {
230
+ /** Remove owned artifacts while retaining the record as retry authority. */
231
+ export async function removeOwnedArtifacts({ dataHome, adapterId }) {
152
232
  const record = await loadOwnership({ dataHome });
153
233
  const install = installFor(record, adapterId);
154
- const result = { adapterId, removed: [], kept: [], missing: [], delegated: [] };
234
+ const result = { adapterId, removed: [], kept: [], missing: [], delegated: [],
235
+ createdDirectories: install?.createdDirectories ?? [] };
155
236
  if (install === null) return result;
156
237
 
157
238
  for (const artifact of install.artifacts) {
@@ -163,7 +244,22 @@ export async function removeOwned({ dataHome, adapterId }) {
163
244
  result.removed.push(artifact.path);
164
245
  }
165
246
 
247
+ return result;
248
+ }
249
+
250
+ /** Forget one install only after every adapter-owned cleanup step succeeded. */
251
+ export async function finalizeRemoval({ dataHome, adapterId }) {
252
+ const record = await loadOwnership({ dataHome });
253
+ if (installFor(record, adapterId) === null) return false;
254
+
166
255
  await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
167
256
  installs: record.installs.filter(entry => entry.adapterId !== adapterId) } });
257
+ return true;
258
+ }
259
+
260
+ /** Remove and finalize for callers that perform no delegated adapter cleanup. */
261
+ export async function removeOwned(options) {
262
+ const result = await removeOwnedArtifacts(options);
263
+ await finalizeRemoval(options);
168
264
  return result;
169
265
  }
@@ -8,10 +8,14 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
8
8
  * it previews is a decoration, and the operator would find out only afterwards.
9
9
  */
10
10
  export function planInstallation({ adapters, detected, context, action = "install",
11
- recorded = [], accVersion = null, allowDowngrade = false, requested = [] }) {
11
+ recorded = [], accVersion = null, allowDowngrade = false, requested = [],
12
+ delivery = "off" }) {
12
13
  if (!["install", "uninstall"].includes(action)) {
13
14
  throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
14
15
  }
16
+ if (!["off", "actionable", "all"].includes(delivery)) {
17
+ throw new AccError(EXIT.USAGE, `unknown delivery policy: ${delivery}`, { delivery });
18
+ }
15
19
  const byId = new Map(adapters.map(adapter => [adapter.id, adapter]));
16
20
  // What ACC recorded writing, by client. For an uninstall this is the
17
21
  // authority rather than detection: the record is the only account of what was
@@ -82,7 +86,16 @@ export function planInstallation({ adapters, detected, context, action = "instal
82
86
  // From the record when the client is gone, because that is what was written
83
87
  // and so what will be removed. Asking the adapter instead would describe an
84
88
  // install for a machine this one no longer is.
85
- const artifacts = (record?.artifacts ?? adapter.planInstall(context))
89
+ const liveDeliverySupported = entry.capabilities?.delivery?.livePush === true;
90
+ const effectiveLivePolicy = liveDeliverySupported ? delivery : "off";
91
+ const deliveryDiagnostic = action === "install" && delivery !== "off"
92
+ && !liveDeliverySupported
93
+ ? entry.deliveryDiagnostic ?? adapter.deliveryFallback?.diagnostic
94
+ ?? `${adapter.displayName ?? adapter.id} has no certified live delivery for this client; durable fallback remains active`
95
+ : null;
96
+ const installContext = { ...context, requestedLivePolicy: delivery,
97
+ livePolicy: effectiveLivePolicy };
98
+ const artifacts = (record?.artifacts ?? adapter.planInstall(installContext))
86
99
  .map(artifact => ({ path: artifact.path, kind: artifact.kind ?? "file" }))
87
100
  .sort((a, b) => a.path.localeCompare(b.path));
88
101
 
@@ -96,12 +109,16 @@ export function planInstallation({ adapters, detected, context, action = "instal
96
109
  // to be inferred from a client version that is null.
97
110
  clientPresent: entry.present === true,
98
111
  alreadyInstalled: entry.installed === true,
112
+ livePolicy: delivery,
113
+ effectiveLivePolicy,
114
+ ...(deliveryDiagnostic === null ? {} : { deliveryDiagnostic }),
99
115
  artifacts,
100
116
  // Said in the operator's terms, not in paths: which files ACC creates
101
117
  // outright and which belong to the user and are only edited.
102
118
  summary: [
103
119
  ...(entry.present ? [] : [`${adapter.displayName ?? adapter.id} is no longer on `
104
120
  + "this machine; removing what ACC recorded writing"]),
121
+ ...(deliveryDiagnostic === null ? [] : [deliveryDiagnostic]),
105
122
  ...artifacts.filter(a => a.kind === "tree")
106
123
  .map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
107
124
  ...artifacts.filter(a => a.kind === "merge")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.1.17",
3
+ "version": "0.2.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,13 +1,18 @@
1
- import { AccError, EXIT } from "@agents-can-communicate/protocol";
1
+ import { createRequire } from "node:module";
2
+
3
+ import { AccError, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIONS }
4
+ from "@agents-can-communicate/protocol";
2
5
  import { clearSessionBinding, loadSessionBinding, storeSessionBinding }
3
6
  from "@agents-can-communicate/adapter-sdk";
4
7
 
5
8
  import { readResource } from "./resources.mjs";
9
+ import { validateToolInput } from "./input-validator.mjs";
6
10
  import { MCP_CAPABILITIES, PUBLIC_TOOLS, RESOURCES } from "./tools.mjs";
7
11
 
8
12
  export const PROTOCOL_VERSION = "2026-07-28";
9
13
  export const SUPPORTED_VERSIONS = Object.freeze([PROTOCOL_VERSION]);
10
- 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 });
11
16
 
12
17
  const META = "io.modelcontextprotocol";
13
18
  const HEARTBEAT_CADENCE_MS = 60_000;
@@ -71,12 +76,48 @@ async function resolveSession(context) {
71
76
  return session;
72
77
  }
73
78
 
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}`);
111
+ }
112
+ return obligation;
113
+ }
114
+
74
115
  /**
75
116
  * A poll is this client's turn.
76
117
  *
77
118
  * The hook runtime hands a session its pending messages when it builds a turn,
78
119
  * and marks them delivered. An MCP client has no turn and no hook, and no tool
79
- * 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
80
121
  * an id, and to read what a peer had actually said it had to ask for the whole
81
122
  * snapshot and search every message in the workspace for its own name.
82
123
  *
@@ -85,29 +126,10 @@ async function resolveSession(context) {
85
126
  * that had answered it.
86
127
  *
87
128
  * Returning them here is delivery, in the same sense and with the same honesty
88
- * 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.
89
130
  * Acknowledgement stays a separate act, because being shown something is not
90
131
  * agreeing to it.
91
132
  */
92
- async function syncWithMail(service, owner, context, args) {
93
- const sync = await service.sync({ ...owner, cursor: args.cursor ?? null,
94
- scope: args.scope, limit: args.limit });
95
- const messages = await service.pendingMessages({
96
- workspaceId: context.workspaceId,
97
- participantId: context.participantId,
98
- exceptSessionId: owner.sessionId });
99
- if (messages.length === 0) return sync;
100
-
101
- for (const message of messages) {
102
- // One failure must not swallow the rest: the client is holding the message
103
- // either way, and a receipt that cannot be written is not a reason to hide
104
- // what a peer said.
105
- await service.markDelivery({ ...owner, messageId: message.messageId,
106
- state: "injected" }).catch(() => null);
107
- }
108
- return { ...sync, messages };
109
- }
110
-
111
133
  async function callTool(name, args, context) {
112
134
  const session = await resolveSession(context);
113
135
  const owner = { sessionId: session.sessionId, generation: session.generation,
@@ -115,62 +137,66 @@ async function callTool(name, args, context) {
115
137
  const service = context.service;
116
138
 
117
139
  switch (name) {
140
+ case "acc_status":
141
+ return service.collectStatus({});
118
142
  case "acc_sync":
119
- return syncWithMail(service, owner, context, args);
143
+ return service.sync({ ...owner, cursor: args.cursor ?? null,
144
+ scope: args.scope, limit: args.limit });
120
145
  case "acc_work":
121
- if (args.clear === true) return service.clearIntent({ ...owner });
146
+ if (args.clear === true) {
147
+ await service.clearIntent({ ...owner });
148
+ return { cleared: true };
149
+ }
122
150
  return service.setIntent({ ...owner, summary: args.summary, mode: args.mode,
123
- state: args.state, workstreamId: args.workstreamId ?? null,
124
- resourceHints: args.resourceHints ?? [] });
151
+ state: args.state, resourceHints: args.resourceHints ?? [] });
125
152
  case "acc_claim":
126
- if (args.action === "release") return service.releaseClaim({ ...owner,
127
- claimId: args.claimId }) ?? { released: args.claimId };
128
153
  if (args.action === "renew") return service.renewClaim({ ...owner,
129
154
  claimId: args.claimId, leaseSeconds: args.leaseSeconds });
130
155
  return service.acquireClaim({ ...owner, resource: args.resource,
131
156
  mode: args.mode ?? "exclusive", enforcement: "advisory",
132
157
  reason: args.reason ?? "unspecified", leaseSeconds: args.leaseSeconds });
133
- case "acc_message":
134
- return service.sendMessage({ ...owner, toParticipantIds: args.to ?? [],
135
- subject: args.subject, body: args.body, type: args.type ?? "note",
136
- priority: args.priority, requiresAck: args.requiresAck === true,
137
- workstreamId: args.workstreamId ?? null });
138
- case "acc_task":
139
- if (args.action === "claim") return service.claimTask({ ...owner,
140
- taskId: args.taskId, force: args.force === true });
141
- if (args.action === "decline") return service.declineTask({ ...owner,
142
- taskId: args.taskId, reason: args.reason });
143
- if (args.action === "transition") return service.transitionTask({ ...owner,
144
- taskId: args.taskId, state: args.state });
145
- return service.createTask({ ...owner, workstreamId: args.workstreamId,
146
- title: args.title, detail: args.detail, taskId: args.taskId,
147
- assigneeParticipantId: args.assigneeParticipantId,
148
- dependsOn: args.dependsOn ?? [] });
149
- case "acc_request":
150
- return service.requestWork({ ...owner, toParticipantId: args.toParticipantId,
151
- title: args.title, detail: args.detail, workstreamId: args.workstreamId,
152
- priority: args.priority, dependsOn: args.dependsOn ?? [] });
158
+ case "acc_release":
159
+ await service.releaseClaim({ ...owner, claimId: args.claimId });
160
+ return { released: args.claimId };
161
+ case "acc_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 };
170
+ }
171
+ case "acc_inbox":
172
+ return service.readInbox({ ...owner, messageId: args.messageId });
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
+ }
153
189
  case "acc_ack":
154
- return service.markDelivery({ ...owner, messageId: args.messageId,
155
- state: args.state ?? "acknowledged" });
156
- case "acc_decide":
157
- return service.recordDecision({ ...owner, title: args.title, outcome: args.outcome,
158
- authority: args.authority ?? "workstream", workstreamId: args.workstreamId ?? null,
159
- decidedBy: args.decidedBy, supersedes: args.supersedes ?? null,
160
- humanConfirmed: args.humanConfirmed === true });
161
- case "acc_workstream":
162
- if (args.action === "coordinate") {
163
- return service.acquireCoordinator({ ...owner, workstreamId: args.workstreamId });
164
- }
165
- if (args.action === "release") {
166
- return service.releaseCoordinator({ ...owner, workstreamId: args.workstreamId });
167
- }
168
- return service.createWorkstream({ ...owner, title: args.title,
169
- objective: args.objective });
170
- case "acc_finish":
171
- 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,
172
196
  completed: args.completed ?? [], remaining: args.remaining ?? [],
173
- blockers: args.blockers ?? [], toParticipantId: args.toParticipantId ?? null });
197
+ blockers: args.blockers ?? [], toParticipantId: args.toParticipantId }) });
198
+ return { message: routed.recorded.message, delivery: routed.delivery };
199
+ }
174
200
  default:
175
201
  throw new AccError(EXIT.USAGE, `unknown tool: ${name}`, { name });
176
202
  }
@@ -190,15 +216,27 @@ async function handle(message, context) {
190
216
  case "resources/list":
191
217
  return complete({ resources: [...RESOURCES] });
192
218
  case "resources/read": {
193
- 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);
194
226
  return complete({ contents: [{ uri: params.uri, mimeType: "application/json",
195
227
  text: JSON.stringify(value, null, 2) }] });
196
228
  }
197
229
  case "tools/call": {
198
230
  try {
199
- 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);
200
238
  return complete({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
201
- structuredContent: JSON.stringify(value) });
239
+ structuredContent: value });
202
240
  } catch (error) {
203
241
  // A failing operation is a tool result, not a transport failure: the
204
242
  // model must see it and be able to react.