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.
- package/README.md +76 -138
- package/SECURITY.md +31 -0
- package/bin/acc-hook.mjs +96 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +6 -1
- package/docs/ADAPTER_AUTHORING.md +172 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +105 -197
- package/docs/CLI.md +157 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +143 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +109 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +102 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +42 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +15 -5
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +21 -12
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +10 -4
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/hooks/hooks.json +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +152 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +127 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +101 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +10 -4
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +139 -224
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +13 -29
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +5 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +109 -109
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +1 -1
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +118 -0
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +14 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +70 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -258
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +156 -60
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +109 -71
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +74 -93
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +86 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +121 -27
- package/package.json +22 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readdir, readFile, rename, rm, writeFile }
|
|
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
|
-
|
|
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
|
|
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")
|
|
@@ -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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 {
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
|
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)
|
|
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,
|
|
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 "
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|