@mmerterden/multi-agent-pipeline 20.29.0 → 20.30.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 (146) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +5 -5
  3. package/README.tr.md +5 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +7 -7
  7. package/install/catalog-history.json +1 -1
  8. package/install/claude.mjs +10 -1
  9. package/install/codex.mjs +2 -0
  10. package/install/copilot.mjs +13 -0
  11. package/manifest.json +149 -48
  12. package/package.json +2 -1
  13. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
  14. package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
  15. package/pipeline/commands/multi-agent/office-on/SKILL.md +50 -0
  16. package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
  17. package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
  18. package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
  19. package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
  20. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  21. package/pipeline/contract/CHANGELOG.md +62 -0
  22. package/pipeline/contract/README.md +52 -6
  23. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  24. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  25. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  26. package/pipeline/contract/manifest.json +367 -22
  27. package/pipeline/contract/types/index.d.ts +530 -3
  28. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -4
  29. package/pipeline/multi-agent-refs/features/office-threat-model.md +91 -0
  30. package/pipeline/multi-agent-refs/help/en.md +3 -0
  31. package/pipeline/multi-agent-refs/help/tr.md +3 -0
  32. package/pipeline/office/NOTICE.md +42 -0
  33. package/pipeline/office/assets/BIN.png +0 -0
  34. package/pipeline/office/assets/BOOKSHELF.png +0 -0
  35. package/pipeline/office/assets/CACTUS.png +0 -0
  36. package/pipeline/office/assets/CLOCK.png +0 -0
  37. package/pipeline/office/assets/COFFEE.png +0 -0
  38. package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
  39. package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
  40. package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
  41. package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
  42. package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
  43. package/pipeline/office/assets/DESK_FRONT.png +0 -0
  44. package/pipeline/office/assets/DESK_SIDE.png +0 -0
  45. package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
  46. package/pipeline/office/assets/HANGING_PLANT.png +0 -0
  47. package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
  48. package/pipeline/office/assets/LARGE_PLANT.png +0 -0
  49. package/pipeline/office/assets/PC_BACK.png +0 -0
  50. package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
  51. package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
  52. package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
  53. package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
  54. package/pipeline/office/assets/PC_SIDE.png +0 -0
  55. package/pipeline/office/assets/PLANT.png +0 -0
  56. package/pipeline/office/assets/PLANT_2.png +0 -0
  57. package/pipeline/office/assets/POT.png +0 -0
  58. package/pipeline/office/assets/SMALL_PAINTING.png +0 -0
  59. package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
  60. package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
  61. package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
  62. package/pipeline/office/assets/SOFA_BACK.png +0 -0
  63. package/pipeline/office/assets/SOFA_FRONT.png +0 -0
  64. package/pipeline/office/assets/SOFA_SIDE.png +0 -0
  65. package/pipeline/office/assets/TABLE_FRONT.png +0 -0
  66. package/pipeline/office/assets/WHITEBOARD.png +0 -0
  67. package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
  68. package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
  69. package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
  70. package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
  71. package/pipeline/office/assets/char_0.png +0 -0
  72. package/pipeline/office/assets/char_1.png +0 -0
  73. package/pipeline/office/assets/char_2.png +0 -0
  74. package/pipeline/office/assets/char_3.png +0 -0
  75. package/pipeline/office/assets/char_4.png +0 -0
  76. package/pipeline/office/assets/char_5.png +0 -0
  77. package/pipeline/office/assets/floor_0.png +0 -0
  78. package/pipeline/office/assets/floor_1.png +0 -0
  79. package/pipeline/office/assets/floor_2.png +0 -0
  80. package/pipeline/office/assets/floor_3.png +0 -0
  81. package/pipeline/office/assets/floor_4.png +0 -0
  82. package/pipeline/office/assets/floor_5.png +0 -0
  83. package/pipeline/office/assets/floor_6.png +0 -0
  84. package/pipeline/office/assets/floor_7.png +0 -0
  85. package/pipeline/office/assets/floor_8.png +0 -0
  86. package/pipeline/office/assets/wall_0.png +0 -0
  87. package/pipeline/office/index.html +5319 -0
  88. package/pipeline/schemas/activity-chat.schema.json +78 -0
  89. package/pipeline/schemas/activity-roles.json +48 -0
  90. package/pipeline/schemas/activity-roles.schema.json +49 -0
  91. package/pipeline/schemas/activity.schema.json +429 -0
  92. package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
  93. package/pipeline/schemas/autopilot-on.schema.json +111 -0
  94. package/pipeline/schemas/contract-error.schema.json +33 -3
  95. package/pipeline/schemas/issue-maturity.schema.json +68 -0
  96. package/pipeline/schemas/ledger.schema.json +202 -0
  97. package/pipeline/schemas/permissions.schema.json +47 -0
  98. package/pipeline/schemas/prefs.schema.json +19 -0
  99. package/pipeline/schemas/prs.schema.json +73 -0
  100. package/pipeline/schemas/review-verdict-request.schema.json +20 -0
  101. package/pipeline/schemas/review-verdict.schema.json +62 -0
  102. package/pipeline/schemas/routine-delete.schema.json +59 -0
  103. package/pipeline/schemas/routines.schema.json +38 -0
  104. package/pipeline/schemas/run-evidence.schema.json +29 -0
  105. package/pipeline/schemas/run-results.schema.json +218 -0
  106. package/pipeline/schemas/service-read.schema.json +58 -0
  107. package/pipeline/schemas/stack-change.schema.json +82 -0
  108. package/pipeline/schemas/stack-request.schema.json +33 -0
  109. package/pipeline/schemas/stack.schema.json +59 -0
  110. package/pipeline/schemas/steer-request.schema.json +25 -0
  111. package/pipeline/schemas/steer.schema.json +59 -0
  112. package/pipeline/schemas/tool-call-request.schema.json +19 -0
  113. package/pipeline/schemas/tool-call.schema.json +47 -0
  114. package/pipeline/schemas/tools.schema.json +60 -0
  115. package/pipeline/scripts/_cost.mjs +66 -1
  116. package/pipeline/scripts/_office-smoke-lib.sh +115 -0
  117. package/pipeline/scripts/_spawn-async.mjs +73 -0
  118. package/pipeline/scripts/_transcript-tail.mjs +274 -0
  119. package/pipeline/scripts/activity-snapshot.mjs +1027 -0
  120. package/pipeline/scripts/autopilot-control.mjs +214 -9
  121. package/pipeline/scripts/contract-server.mjs +1178 -48
  122. package/pipeline/scripts/cost-analyze.mjs +2 -53
  123. package/pipeline/scripts/cost-table.json +1 -0
  124. package/pipeline/scripts/office-build.mjs +89 -0
  125. package/pipeline/scripts/office-control.mjs +383 -0
  126. package/pipeline/scripts/office-ledger.mjs +409 -0
  127. package/pipeline/scripts/routine-registry.mjs +42 -6
  128. package/pipeline/scripts/run-results.mjs +373 -0
  129. package/pipeline/scripts/service-read.mjs +244 -0
  130. package/pipeline/scripts/stack-toggle.mjs +285 -0
  131. package/pipeline/scripts/steer-run.mjs +97 -0
  132. package/pipeline/scripts/toolkit-tools.mjs +599 -0
  133. package/pipeline/scripts/uninstall.mjs +3 -0
  134. package/pipeline/scripts/work-items.mjs +304 -0
  135. package/pipeline/skills/.skill-manifest.json +19 -7
  136. package/pipeline/skills/.skills-index.json +34 -1
  137. package/pipeline/skills/shared/README.md +6 -3
  138. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
  139. package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
  140. package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +22 -0
  141. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
  142. package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
  143. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
  144. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
  145. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  146. package/pipeline/skills/skills-index.md +4 -1
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file toolkit-tools.mjs - the multi-agent-toolkit MCP server's tools, for
4
+ * a client that is not an MCP host: list them, and call one with arguments its
5
+ * own inputSchema accepts.
6
+ *
7
+ * The server is the one the user registered with Claude Code: the
8
+ * `mcpServers` entry whose name contains "toolkit" in ~/.claude.json, else in
9
+ * ~/.claude/settings.json (the lookup doctor.mjs makes). It is started as a
10
+ * stdio child with that entry's command, arguments and environment on top of
11
+ * this process's, so every guard the toolkit honours from the environment
12
+ * (MULTI_AGENT_UNATTENDED, its URL policy) applies unchanged, and a tool's own
13
+ * confirm or preview behaviour stays the tool's. One child per client, started
14
+ * on first use and closed with it.
15
+ *
16
+ * Each tool carries `risk` from its own annotations: readOnlyHint is read,
17
+ * destructiveHint is high, anything else is low.
18
+ *
19
+ * Calls are denied by default. A tool may be called only when it declares
20
+ * readOnlyHint and its inputSchema takes nothing that names a place: no
21
+ * property whose name speaks of a path, URL, file, directory, output, project
22
+ * or root, and no uri format, at any depth. Everything else is listed with
23
+ * `allowed: false` and refused with `tool-not-allowed`, because a read-only
24
+ * tool that is handed a location reads whatever the location points at (a
25
+ * local file, a loopback service). Arguments may not carry `confirm`, which
26
+ * would skip a tool's own preview.
27
+ *
28
+ * The child always runs with MULTI_AGENT_UNATTENDED=1 and
29
+ * MCP_TOOLKIT_URL_POLICY=strict, set after the registration's own environment
30
+ * so neither can be overridden. A registration whose package spec has no exact
31
+ * version (bare, a dist-tag such as `@latest`, or a range) is pinned to
32
+ * MA_TOOLKIT_VERSION or prefs.global.office.toolkitVersion, and refused when
33
+ * neither is set.
34
+ *
35
+ * No tool that is not read-only is allowed today. Opening one up means routing
36
+ * it through the server's two-step confirm, never a direct call.
37
+ *
38
+ * Arguments are checked against the tool's inputSchema before the call (the
39
+ * subset the toolkit's schemas use: type, enum, const, required, properties,
40
+ * additionalProperties, items, min/max, minLength/maxLength, pattern). A key
41
+ * the schema does not declare is refused at any depth, whatever
42
+ * additionalProperties says, and so is a `confirm` key. An unknown tool or
43
+ * invalid arguments never reach the server. Results come back as text
44
+ * (redacted), JSON (redacted, as text) and images (base64, image types only,
45
+ * size-capped); a tool's error message is redacted too.
46
+ *
47
+ * Usage:
48
+ * toolkit-tools.mjs list
49
+ * toolkit-tools.mjs call <name> '<json arguments>'
50
+ *
51
+ * MA_TOOLKIT_MCP sets the server as JSON {command, args, env} (tests).
52
+ *
53
+ * Exit: 0 printed, 1 refused or failed, 2 usage.
54
+ *
55
+ * @module pipeline/scripts/toolkit-tools
56
+ */
57
+
58
+ import { spawn } from "node:child_process";
59
+ import { readFileSync } from "node:fs";
60
+ import { homedir } from "node:os";
61
+ import { basename, join } from "node:path";
62
+ import { clearTimeout, setTimeout } from "node:timers";
63
+ import { createRedactor } from "./transcript-index.mjs";
64
+ import { runMain } from "../lib/fatal.mjs";
65
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
66
+
67
+ export const CONTRACT_VERSION = "1.0.0";
68
+ export const TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
69
+ const START_TIMEOUT_MS = 60_000;
70
+ const CALL_TIMEOUT_MS = 180_000;
71
+ const MAX_LINE_BYTES = 16 * 1024 * 1024;
72
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
73
+ const IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
74
+
75
+ export class ToolError extends Error {
76
+ constructor(code, message, extra = {}) {
77
+ super(message);
78
+ this.code = code;
79
+ this.extra = extra;
80
+ }
81
+ }
82
+
83
+ const readJson = (p) => {
84
+ try {
85
+ return JSON.parse(readFileSync(p, "utf8"));
86
+ } catch {
87
+ return null;
88
+ }
89
+ };
90
+
91
+ /** The registered toolkit server, or null. */
92
+ export function toolkitEntry(env = process.env) {
93
+ if (env.MA_TOOLKIT_MCP) {
94
+ try {
95
+ const e = JSON.parse(env.MA_TOOLKIT_MCP);
96
+ if (e && typeof e.command === "string") return e;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ const home = env.HOME || homedir();
102
+ for (const p of [join(home, ".claude.json"), join(home, ".claude", "settings.json")]) {
103
+ const j = readJson(p);
104
+ const key = j?.mcpServers && Object.keys(j.mcpServers).find((k) => /toolkit/i.test(k));
105
+ if (key && typeof j.mcpServers[key]?.command === "string") return j.mcpServers[key];
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Families refused whatever their schema says: `web` tools share a browser
112
+ * whose page may already point at a local file or service, and `context`
113
+ * tools search an index built from whatever files were indexed.
114
+ */
115
+ const DENIED_CATEGORIES = new Set(["web", "context"]);
116
+
117
+ /** Property names that hand a tool a location to read or write. */
118
+ const LOCATION_NAME =
119
+ /(path|url|uri|href|link|file|dir|folder|output|out_?dir|project|root|cwd|workspace|location|endpoint|host|address|socket|device|bundle|archive|repo)/i;
120
+ const LOCATION_FORMAT = /^(uri|uri-reference|iri|iri-reference|url|hostname|idn-hostname)$/i;
121
+
122
+ /** The location-shaped properties anywhere in a schema, by name or format. */
123
+ export function locationProperties(schema, prefix = "") {
124
+ const out = [];
125
+ if (!schema || typeof schema !== "object") return out;
126
+ for (const [k, v] of Object.entries(schema.properties ?? {})) {
127
+ const at = prefix ? `${prefix}.${k}` : k;
128
+ if (LOCATION_NAME.test(k) || LOCATION_FORMAT.test(String(v?.format ?? ""))) out.push(at);
129
+ out.push(...locationProperties(v, at));
130
+ if (v?.items) out.push(...locationProperties(v.items, `${at}[]`));
131
+ }
132
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
133
+ for (const alt of Array.isArray(schema[key]) ? schema[key] : []) {
134
+ out.push(...locationProperties(alt, prefix));
135
+ }
136
+ }
137
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
138
+ out.push(...locationProperties(schema.additionalProperties, `${prefix}*`));
139
+ }
140
+ return out;
141
+ }
142
+
143
+ /**
144
+ * Whether a tool may be called from this client, and why not. Only a
145
+ * read-only tool that takes no location is allowed.
146
+ */
147
+ export function callPolicy(tool) {
148
+ if (DENIED_CATEGORIES.has(categoryOf(String(tool?.name ?? "")))) {
149
+ return {
150
+ allowed: false,
151
+ reason:
152
+ "this family reads state shared with other sessions (a browser, an index of files); call it from a terminal session",
153
+ };
154
+ }
155
+ if (tool?.annotations?.readOnlyHint !== true || riskOf(tool) !== "read") {
156
+ return {
157
+ allowed: false,
158
+ reason: "only tools that declare readOnlyHint, and nothing destructive, can be called here",
159
+ };
160
+ }
161
+ const where = locationProperties(tool.inputSchema);
162
+ if (where.length) {
163
+ return {
164
+ allowed: false,
165
+ reason: `the tool takes a location (${where.slice(0, 3).join(", ")}); call it from a terminal session`,
166
+ };
167
+ }
168
+ return { allowed: true, reason: null };
169
+ }
170
+
171
+ /** Environment the toolkit child cannot override: unattended guards, strict URL policy. */
172
+ export const FORCED_ENV = Object.freeze({
173
+ MULTI_AGENT_UNATTENDED: "1",
174
+ MCP_TOOLKIT_URL_POLICY: "strict",
175
+ });
176
+
177
+ const SEMVER = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
178
+ const RUNNERS = new Set(["npx", "bunx", "pnpx"]);
179
+
180
+ /**
181
+ * The package specs a registration hands to a package runner (npx, bunx,
182
+ * pnpx, `npm exec`, `pnpm dlx`): the first positional argument, or each
183
+ * `-p` / `--package` value. A registration that runs a local file has none.
184
+ */
185
+ export function packageSpecs(entry) {
186
+ const cmd = basename(String(entry?.command ?? ""));
187
+ const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
188
+ let rest;
189
+ if (RUNNERS.has(cmd)) rest = args;
190
+ else if (cmd === "npm" && (args[0] === "exec" || args[0] === "x")) rest = args.slice(1);
191
+ else if (cmd === "pnpm" && args[0] === "dlx") rest = args.slice(1);
192
+ else return [];
193
+ const specs = [];
194
+ let named = false;
195
+ for (let i = 0; i < rest.length; i++) {
196
+ const a = rest[i];
197
+ if (a === "-p" || a === "--package") {
198
+ if (rest[i + 1] !== undefined) specs.push(rest[++i]);
199
+ named = true;
200
+ } else if (a.startsWith("--package=")) {
201
+ specs.push(a.slice("--package=".length));
202
+ named = true;
203
+ } else if (a === "--") {
204
+ if (!named && rest[i + 1] !== undefined) specs.push(rest[i + 1]);
205
+ break;
206
+ } else if (!a.startsWith("-")) {
207
+ if (!named) specs.push(a);
208
+ break;
209
+ }
210
+ }
211
+ return specs;
212
+ }
213
+
214
+ /** The name and version of a spec: `@scope/name@1.2.3`, `name@^1`, `name`. */
215
+ function splitSpec(spec) {
216
+ const at = spec.lastIndexOf("@");
217
+ return at > 0 ? [spec.slice(0, at), spec.slice(at + 1)] : [spec, null];
218
+ }
219
+
220
+ /**
221
+ * Whether a registration would resolve its package anew on every start: a
222
+ * spec without an exact version (bare, a dist-tag such as latest or next, or
223
+ * a range) is unpinned.
224
+ */
225
+ export function unpinned(entry) {
226
+ return packageSpecs(entry).some((s) => !SEMVER.test(splitSpec(s)[1] ?? ""));
227
+ }
228
+
229
+ /**
230
+ * The registration with every unpinned spec set to `<name>@<version>`, or
231
+ * null when it is unpinned and no exact version is known. The version comes
232
+ * from MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion.
233
+ */
234
+ export function pinnedEntry(entry, env = process.env) {
235
+ if (!unpinned(entry)) return entry;
236
+ const prefs = readJson(join(env.HOME || homedir(), ".claude", "multi-agent-preferences.json"));
237
+ const version = env.MA_TOOLKIT_VERSION || prefs?.global?.office?.toolkitVersion;
238
+ if (typeof version !== "string" || !SEMVER.test(version)) return null;
239
+ const loose = new Set(packageSpecs(entry).filter((s) => !SEMVER.test(splitSpec(s)[1] ?? "")));
240
+ const pin = (a) => {
241
+ if (typeof a !== "string") return a;
242
+ if (loose.has(a)) return `${splitSpec(a)[0]}@${version}`;
243
+ if (a.startsWith("--package=") && loose.has(a.slice(10))) {
244
+ return `--package=${splitSpec(a.slice(10))[0]}@${version}`;
245
+ }
246
+ return a;
247
+ };
248
+ return { ...entry, args: Array.isArray(entry.args) ? entry.args.map(pin) : entry.args };
249
+ }
250
+
251
+ /** read, low or high, from the tool's own annotations. */
252
+ export function riskOf(tool) {
253
+ const a = tool?.annotations ?? {};
254
+ if (a.destructiveHint === true) return "high";
255
+ if (a.readOnlyHint === true) return "read";
256
+ return "low";
257
+ }
258
+
259
+ /** The tool's category: the prefix before its first underscore. */
260
+ export const categoryOf = (name) => (name.includes("_") ? name.split("_")[0] : "other");
261
+
262
+ /**
263
+ * Every reason `value` does not satisfy `schema`, for the subset of JSON
264
+ * Schema the toolkit's input schemas use. Unknown keywords are ignored; the
265
+ * server validates again.
266
+ */
267
+ export function schemaErrors(schema, value, path = "", opts = {}) {
268
+ if (!schema || typeof schema !== "object") return [];
269
+ const at = path || "/";
270
+ const errors = [];
271
+ const types = schema.type === undefined ? null : [].concat(schema.type);
272
+ const typeOf = (v) =>
273
+ v === null ? "null" : Array.isArray(v) ? "array" : Number.isInteger(v) ? "integer" : typeof v;
274
+ if (types) {
275
+ const t = typeOf(value);
276
+ const ok = types.some((x) => x === t || (x === "number" && t === "integer"));
277
+ if (!ok) return [`${at} must be ${types.join(" or ")}`];
278
+ }
279
+ if (
280
+ Array.isArray(schema.enum) &&
281
+ !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))
282
+ ) {
283
+ errors.push(`${at} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(", ")}`);
284
+ }
285
+ if ("const" in schema && JSON.stringify(schema.const) !== JSON.stringify(value)) {
286
+ errors.push(`${at} must be ${JSON.stringify(schema.const)}`);
287
+ }
288
+ if (typeof value === "string") {
289
+ if (Number.isInteger(schema.minLength) && value.length < schema.minLength)
290
+ errors.push(`${at} is too short`);
291
+ if (Number.isInteger(schema.maxLength) && value.length > schema.maxLength)
292
+ errors.push(`${at} is too long`);
293
+ if (typeof schema.pattern === "string") {
294
+ try {
295
+ if (!new RegExp(schema.pattern, "u").test(value))
296
+ errors.push(`${at} does not match its pattern`);
297
+ } catch {
298
+ // A pattern this engine cannot compile is left to the server.
299
+ }
300
+ }
301
+ }
302
+ if (typeof value === "number") {
303
+ if (typeof schema.minimum === "number" && value < schema.minimum)
304
+ errors.push(`${at} is below ${schema.minimum}`);
305
+ if (typeof schema.maximum === "number" && value > schema.maximum)
306
+ errors.push(`${at} is above ${schema.maximum}`);
307
+ }
308
+ if (Array.isArray(value)) {
309
+ if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems)
310
+ errors.push(`${at} has too many items`);
311
+ if (Number.isInteger(schema.minItems) && value.length < schema.minItems)
312
+ errors.push(`${at} has too few items`);
313
+ if (schema.items && typeof schema.items === "object") {
314
+ value.forEach((v, i) => errors.push(...schemaErrors(schema.items, v, `${path}/${i}`, opts)));
315
+ }
316
+ }
317
+ if (value && typeof value === "object" && !Array.isArray(value)) {
318
+ const props = schema.properties ?? {};
319
+ for (const k of Array.isArray(schema.required) ? schema.required : []) {
320
+ if (!(k in value)) errors.push(`${path}/${k} is required`);
321
+ }
322
+ for (const [k, v] of Object.entries(value)) {
323
+ if (Object.hasOwn(props, k)) errors.push(...schemaErrors(props[k], v, `${path}/${k}`, opts));
324
+ else if (opts.strict) errors.push(`${path}/${k} is not declared by the tool's inputSchema`);
325
+ else if (schema.additionalProperties === false) errors.push(`${path}/${k} is not allowed`);
326
+ else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
327
+ errors.push(...schemaErrors(schema.additionalProperties, v, `${path}/${k}`, opts));
328
+ }
329
+ }
330
+ }
331
+ return errors;
332
+ }
333
+
334
+ /** Every `confirm` key in an argument value, at any depth, as JSON pointers. */
335
+ export function confirmPaths(value, path = "") {
336
+ if (Array.isArray(value)) return value.flatMap((v, i) => confirmPaths(v, `${path}/${i}`));
337
+ if (!value || typeof value !== "object") return [];
338
+ return Object.entries(value).flatMap(([k, v]) => [
339
+ ...(k === "confirm" ? [`${path}/confirm`] : []),
340
+ ...confirmPaths(v, `${path}/${k}`),
341
+ ]);
342
+ }
343
+
344
+ /**
345
+ * A stdio MCP client for the toolkit, started lazily, one per caller.
346
+ *
347
+ * @param {{env?: object}} [opts]
348
+ */
349
+ export function createToolkitClient({ env = process.env } = {}) {
350
+ let child = null;
351
+ let ready = null;
352
+ let nextId = 1;
353
+ let buf = "";
354
+ let tools = null;
355
+ const pending = new Map();
356
+
357
+ const failAll = (err) => {
358
+ for (const p of pending.values()) {
359
+ clearTimeout(p.timer);
360
+ p.reject(err);
361
+ }
362
+ pending.clear();
363
+ };
364
+
365
+ const rpc = (method, params, timeoutMs) =>
366
+ new Promise((resolve, reject) => {
367
+ const id = nextId++;
368
+ const timer = setTimeout(() => {
369
+ pending.delete(id);
370
+ reject(new ToolError("timeout", `the toolkit did not answer ${method} in time`));
371
+ }, timeoutMs);
372
+ pending.set(id, { resolve, reject, timer });
373
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
374
+ });
375
+
376
+ const start = () => {
377
+ if (ready) return ready;
378
+ const registered = toolkitEntry(env);
379
+ if (!registered) {
380
+ return Promise.reject(
381
+ new ToolError("not-registered", "multi-agent-toolkit is not registered as an MCP server"),
382
+ );
383
+ }
384
+ const entry = pinnedEntry(registered, env);
385
+ if (!entry) {
386
+ return Promise.reject(
387
+ new ToolError(
388
+ "unpinned",
389
+ "the toolkit registration starts a package without an exact version; register a pinned version or set prefs.global.office.toolkitVersion",
390
+ ),
391
+ );
392
+ }
393
+ ready = new Promise((resolve, reject) => {
394
+ child = spawn(entry.command, Array.isArray(entry.args) ? entry.args : [], {
395
+ env: { ...env, ...(entry.env ?? {}), ...FORCED_ENV },
396
+ stdio: ["pipe", "pipe", "ignore"],
397
+ });
398
+ child.on("error", (e) => {
399
+ ready = null;
400
+ reject(
401
+ new ToolError("not-started", `the toolkit could not start (${e.code ?? e.message})`),
402
+ );
403
+ });
404
+ child.on("exit", () => {
405
+ ready = null;
406
+ tools = null;
407
+ failAll(new ToolError("exited", "the toolkit exited"));
408
+ });
409
+ child.stdout.setEncoding("utf8");
410
+ child.stdout.on("data", (d) => {
411
+ buf += d;
412
+ if (buf.length > MAX_LINE_BYTES) {
413
+ buf = "";
414
+ return;
415
+ }
416
+ let nl;
417
+ while ((nl = buf.indexOf("\n")) !== -1) {
418
+ const line = buf.slice(0, nl);
419
+ buf = buf.slice(nl + 1);
420
+ let msg;
421
+ try {
422
+ msg = JSON.parse(line);
423
+ } catch {
424
+ continue;
425
+ }
426
+ const p = msg && pending.get(msg.id);
427
+ if (!p) continue;
428
+ pending.delete(msg.id);
429
+ clearTimeout(p.timer);
430
+ if (msg.error) {
431
+ const why = createRedactor().clean(String(msg.error.message ?? "the tool failed"));
432
+ p.reject(new ToolError("tool-error", why.slice(0, 2000)));
433
+ } else p.resolve(msg.result);
434
+ }
435
+ });
436
+ rpc(
437
+ "initialize",
438
+ {
439
+ protocolVersion: "2025-06-18",
440
+ capabilities: {},
441
+ clientInfo: { name: "multi-agent-contract-server", version: CONTRACT_VERSION },
442
+ },
443
+ START_TIMEOUT_MS,
444
+ )
445
+ .then(() => {
446
+ child.stdin.write(
447
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n",
448
+ );
449
+ resolve();
450
+ })
451
+ .catch((e) => {
452
+ ready = null;
453
+ reject(e);
454
+ });
455
+ });
456
+ return ready;
457
+ };
458
+
459
+ /** Every tool with its category, risk, description and inputSchema. */
460
+ async function listTools() {
461
+ if (tools) return tools;
462
+ await start();
463
+ const all = [];
464
+ let cursor;
465
+ for (let page = 0; page < 20; page++) {
466
+ const r = await rpc("tools/list", cursor ? { cursor } : {}, START_TIMEOUT_MS);
467
+ for (const t of Array.isArray(r?.tools) ? r.tools : []) {
468
+ if (typeof t?.name !== "string" || !TOOL_NAME.test(t.name)) continue;
469
+ all.push({
470
+ name: t.name,
471
+ title: typeof t.title === "string" ? t.title : (t.annotations?.title ?? null),
472
+ category: categoryOf(t.name),
473
+ description: typeof t.description === "string" ? t.description : "",
474
+ risk: riskOf(t),
475
+ ...callPolicy(t),
476
+ inputSchema:
477
+ t.inputSchema && typeof t.inputSchema === "object" ? t.inputSchema : { type: "object" },
478
+ });
479
+ }
480
+ cursor = r?.nextCursor;
481
+ if (!cursor) break;
482
+ }
483
+ tools = all.sort((a, b) => (a.name < b.name ? -1 : 1));
484
+ return tools;
485
+ }
486
+
487
+ /** Call one tool after checking its arguments; the result redacted. */
488
+ async function callTool(name, args) {
489
+ if (typeof name !== "string" || !TOOL_NAME.test(name))
490
+ throw new ToolError("unknown-tool", "no such tool");
491
+ const tool = (await listTools()).find((t) => t.name === name);
492
+ if (!tool) throw new ToolError("unknown-tool", "no such tool");
493
+ if (!tool.allowed) throw new ToolError("tool-not-allowed", tool.reason);
494
+ const input = args ?? {};
495
+ const confirms = confirmPaths(input);
496
+ if (confirms.length) {
497
+ throw new ToolError("invalid-arguments", "confirm is not accepted here", {
498
+ errors: confirms.map(
499
+ (p) => `${p} is not allowed: a tool's own preview is never skipped from here`,
500
+ ),
501
+ });
502
+ }
503
+ const errors = schemaErrors(tool.inputSchema, input, "", { strict: true });
504
+ if (errors.length)
505
+ throw new ToolError(
506
+ "invalid-arguments",
507
+ "the arguments do not match the tool's inputSchema",
508
+ { errors },
509
+ );
510
+ const r = await rpc("tools/call", { name, arguments: input }, CALL_TIMEOUT_MS);
511
+ return renderResult(tool, r);
512
+ }
513
+
514
+ function close() {
515
+ failAll(new ToolError("closed", "the client was closed"));
516
+ if (child && child.exitCode === null) child.kill("SIGTERM");
517
+ child = null;
518
+ ready = null;
519
+ tools = null;
520
+ }
521
+
522
+ return {
523
+ listTools,
524
+ callTool,
525
+ close,
526
+ get started() {
527
+ return Boolean(child);
528
+ },
529
+ };
530
+ }
531
+
532
+ /** A tools/call result as text, JSON text and images, every text redacted. */
533
+ export function renderResult(tool, result) {
534
+ const R = createRedactor();
535
+ const content = [];
536
+ for (const c of Array.isArray(result?.content) ? result.content : []) {
537
+ if (c?.type === "text") content.push({ kind: "text", text: R.clean(String(c.text ?? "")) });
538
+ else if (c?.type === "image" && IMAGE_TYPES.has(c.mimeType) && typeof c.data === "string") {
539
+ if (c.data.length * 0.75 <= MAX_IMAGE_BYTES)
540
+ content.push({ kind: "image", mime: c.mimeType, data: c.data });
541
+ else content.push({ kind: "text", text: "[image too large to show]" });
542
+ } else if (c?.type === "resource" && typeof c.resource?.text === "string") {
543
+ content.push({ kind: "text", text: R.clean(c.resource.text) });
544
+ } else if (c?.type)
545
+ content.push({ kind: "text", text: `[${String(c.type)} content not shown]` });
546
+ }
547
+ if (result?.structuredContent && typeof result.structuredContent === "object") {
548
+ content.push({
549
+ kind: "json",
550
+ text: R.clean(JSON.stringify(result.structuredContent, null, 2)),
551
+ });
552
+ }
553
+ return {
554
+ contractVersion: CONTRACT_VERSION,
555
+ tool: tool.name,
556
+ risk: tool.risk,
557
+ isError: result?.isError === true,
558
+ content,
559
+ };
560
+ }
561
+
562
+ async function main(argv) {
563
+ const [cmd, name, raw] = argv;
564
+ const client = createToolkitClient();
565
+ try {
566
+ let out;
567
+ if (cmd === "list" && !name) {
568
+ out = { contractVersion: CONTRACT_VERSION, tools: await client.listTools() };
569
+ } else if (cmd === "call" && name) {
570
+ let args = {};
571
+ if (raw !== undefined) {
572
+ try {
573
+ args = JSON.parse(raw);
574
+ } catch {
575
+ throw new ToolError("invalid-arguments", "the arguments are not JSON");
576
+ }
577
+ }
578
+ out = await client.callTool(name, args);
579
+ } else {
580
+ process.stderr.write("usage: toolkit-tools.mjs list | call <name> '<json arguments>'\n");
581
+ return 2;
582
+ }
583
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
584
+ return 0;
585
+ } catch (err) {
586
+ if (!(err instanceof ToolError)) throw err;
587
+ process.stderr.write(`toolkit-tools: ${err.message}\n`);
588
+ for (const e of err.extra?.errors ?? []) process.stderr.write(` ${e}\n`);
589
+ return 1;
590
+ } finally {
591
+ client.close();
592
+ }
593
+ }
594
+
595
+ if (invokedDirectly(import.meta.url)) {
596
+ runMain("toolkit-tools", async () => {
597
+ process.exitCode = await main(process.argv.slice(2));
598
+ });
599
+ }
@@ -997,6 +997,7 @@ export async function main() {
997
997
  rmIfExists(join(CLAUDE, "multi-agent-refs"));
998
998
  rmIfExists(join(CLAUDE, ".pipeline-swap"));
999
999
  rmIfExists(join(CLAUDE, "schemas"));
1000
+ rmIfExists(join(CLAUDE, "office"));
1000
1001
  if (!rmManifestFiles(CLAUDE, KEEP_IN_LIB)) {
1001
1002
  // An install that predates the manifest wiped lib/ and templates/ on
1002
1003
  // every run, so their shipped names are the pipeline's. The top-level
@@ -1053,6 +1054,7 @@ export async function main() {
1053
1054
  rmIfExists(join(COP, "scripts"));
1054
1055
  // Pipeline-managed trees the installer lays down alongside scripts/.
1055
1056
  rmIfExists(join(COP, "schemas"));
1057
+ rmIfExists(join(COP, "office"));
1056
1058
  rmDirExcept(join(COP, "lib"), KEEP_IN_LIB);
1057
1059
  rmIfExists(join(COP, ".pipeline-version"));
1058
1060
  const skills = join(COP, "skills");
@@ -1164,6 +1166,7 @@ export async function main() {
1164
1166
  rmIfExists(join(CODEX, "scripts"));
1165
1167
  rmDirExcept(join(CODEX, "lib"), KEEP_IN_LIB);
1166
1168
  rmIfExists(join(CODEX, "schemas"));
1169
+ rmIfExists(join(CODEX, "office"));
1167
1170
  rmIfExists(join(CODEX, "rules"));
1168
1171
  rmIfExists(join(CODEX, ".pipeline-version"));
1169
1172