machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26

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 (102) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/GOVERNANCE.md +2 -2
  4. package/README.md +24 -6
  5. package/browser-extension/manifest.json +1 -1
  6. package/docs/AGENT_CONTEXT.md +10 -7
  7. package/docs/ARCHITECTURE.md +35 -22
  8. package/docs/AUDIT.md +85 -1
  9. package/docs/CLIENTS.md +6 -2
  10. package/docs/ENGINEERING.md +31 -9
  11. package/docs/LOCAL_AUTOMATION.md +4 -2
  12. package/docs/LOGGING.md +8 -8
  13. package/docs/OPERATIONS.md +43 -17
  14. package/docs/PRIVACY.md +18 -4
  15. package/docs/PROJECT_STANDARDS.md +2 -2
  16. package/docs/RELEASING.md +35 -11
  17. package/docs/TESTING.md +36 -16
  18. package/docs/THREAT_MODEL.md +20 -5
  19. package/docs/TOOL_REFERENCE.md +18 -12
  20. package/docs/UPGRADING.md +32 -0
  21. package/package.json +15 -6
  22. package/scripts/check-plan.mjs +8 -0
  23. package/scripts/coverage-check.mjs +30 -1
  24. package/scripts/foreground-daemon-recovery.mjs +88 -0
  25. package/scripts/github-release.mjs +22 -16
  26. package/scripts/install-published-prerelease.mjs +7 -7
  27. package/scripts/official-mcp-conformance.mjs +243 -0
  28. package/scripts/persistent-activation-process.mjs +36 -0
  29. package/scripts/release-candidate-manifest.mjs +12 -0
  30. package/scripts/release-publication-guard.mjs +65 -0
  31. package/scripts/release-state.mjs +1 -1
  32. package/scripts/sbom-check.mjs +99 -0
  33. package/scripts/start-release-candidate.mjs +39 -13
  34. package/src/local/agent-context-projection.mjs +26 -7
  35. package/src/local/agent-context.mjs +25 -4
  36. package/src/local/autostart-log-maintenance.mjs +36 -0
  37. package/src/local/capability-observer.mjs +5 -0
  38. package/src/local/child-process-settlement.mjs +103 -0
  39. package/src/local/cli-activate.mjs +42 -5
  40. package/src/local/cli-service.mjs +55 -5
  41. package/src/local/cli.mjs +59 -10
  42. package/src/local/daemon-process.mjs +24 -3
  43. package/src/local/delegated-process-sandbox.mjs +1 -0
  44. package/src/local/execution-routing.mjs +231 -0
  45. package/src/local/git-service.mjs +3 -1
  46. package/src/local/job-runner.mjs +55 -19
  47. package/src/local/macos-trust-broker.mjs +7 -0
  48. package/src/local/managed-job-runner-claim.mjs +54 -0
  49. package/src/local/managed-job-runner.mjs +13 -2
  50. package/src/local/process-execution.mjs +2 -2
  51. package/src/local/process-identity.mjs +11 -0
  52. package/src/local/process-tree-ownership-types.d.ts +37 -0
  53. package/src/local/process-tree-ownership.mjs +49 -41
  54. package/src/local/process-tree.mjs +1 -1
  55. package/src/local/relay-call-recovery.mjs +40 -21
  56. package/src/local/runtime-activation.mjs +357 -38
  57. package/src/local/runtime-capabilities.mjs +22 -6
  58. package/src/local/runtime-diagnostics.mjs +9 -2
  59. package/src/local/runtime.mjs +18 -4
  60. package/src/local/service-convergence.mjs +33 -0
  61. package/src/local/service-owner.mjs +147 -0
  62. package/src/local/service-restart-handoff.mjs +22 -8
  63. package/src/local/service-runtime.mjs +145 -0
  64. package/src/local/service.mjs +143 -25
  65. package/src/local/state.mjs +104 -7
  66. package/src/local/stdio.mjs +139 -45
  67. package/src/local/system-network-route.mjs +76 -0
  68. package/src/local/tool-executor.mjs +24 -6
  69. package/src/local/tools.mjs +6 -5
  70. package/src/local/windows-service-convergence.mjs +49 -0
  71. package/src/local/windows-service.mjs +30 -53
  72. package/src/shared/mcp-protocol.d.mts +27 -0
  73. package/src/shared/mcp-protocol.mjs +256 -0
  74. package/src/shared/mcp-subscriptions.d.mts +4 -0
  75. package/src/shared/mcp-subscriptions.mjs +59 -0
  76. package/src/shared/relay-contract.json +1 -0
  77. package/src/shared/result-projection.d.mts +2 -1
  78. package/src/shared/result-projection.mjs +13 -2
  79. package/src/shared/server-metadata.json +11 -4
  80. package/src/shared/tool-argument-validation.d.mts +17 -0
  81. package/src/shared/tool-argument-validation.mjs +325 -0
  82. package/src/shared/tool-catalog.json +18 -12
  83. package/src/worker/durable-stream-calls.ts +12 -24
  84. package/src/worker/http.ts +36 -2
  85. package/src/worker/index.ts +181 -165
  86. package/src/worker/mcp-http-contract.ts +276 -0
  87. package/src/worker/mcp-jsonrpc.ts +12 -6
  88. package/src/worker/mcp-legacy-dispatch.ts +104 -0
  89. package/src/worker/mcp-modern-controller.ts +199 -0
  90. package/src/worker/mcp-modern-proxy.ts +126 -0
  91. package/src/worker/mcp-modern-stream.ts +71 -0
  92. package/src/worker/mcp-session.ts +12 -3
  93. package/src/worker/mcp-stream-proxy-contract.ts +67 -0
  94. package/src/worker/mcp-stream-proxy.ts +17 -60
  95. package/src/worker/mcp-tool-call-input.ts +23 -0
  96. package/src/worker/tool-catalog.ts +29 -1
  97. package/src/worker/tool-timeout.ts +53 -12
  98. package/src/worker/worker-mcp-config.ts +23 -0
  99. package/src/worker/worker-metadata.ts +10 -1
  100. package/src/worker/worker-runtime-config.ts +19 -0
  101. package/src/worker/worker-static-routes.ts +7 -2
  102. package/tsconfig.local.json +7 -1
@@ -0,0 +1,325 @@
1
+ export const JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema";
2
+
3
+ const SUPPORTED_TYPES = new Set(["null", "boolean", "object", "array", "number", "integer", "string"]);
4
+ const SUPPORTED_KEYWORDS = new Set([
5
+ "$schema", "type", "enum", "const", "default", "title", "description",
6
+ "properties", "required", "additionalProperties", "items",
7
+ "minItems", "maxItems", "minLength", "maxLength", "pattern",
8
+ "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
9
+ "minProperties", "maxProperties", "allOf", "anyOf", "oneOf", "not",
10
+ "if", "then", "else", "x-mcp-header",
11
+ ]);
12
+ const DEFAULT_LIMITS = Object.freeze({
13
+ maximumDepth: 32, maximumNodes: 4096, maximumIssues: 16, maximumPatternLength: 2048, maximumValidationSteps: 65_536,
14
+ });
15
+
16
+ export class ToolSchemaContractError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "ToolSchemaContractError";
20
+ }
21
+ }
22
+
23
+ export class ToolArgumentValidationError extends Error {
24
+ constructor(tool, issues) {
25
+ super(`tool arguments do not match the input schema: ${tool}`);
26
+ this.name = "ToolArgumentValidationError";
27
+ this.code = "invalid_request";
28
+ this.retryable = false;
29
+ this.details = Object.freeze({
30
+ tool: String(tool),
31
+ validation_issues: Object.freeze(issues.map((issue) => Object.freeze({ ...issue }))),
32
+ });
33
+ }
34
+ }
35
+
36
+ export function compileToolArgumentValidators(tools, options = {}) {
37
+ if (!Array.isArray(tools)) throw new ToolSchemaContractError("tool catalog must be an array");
38
+ const limits = normalizeLimits(options);
39
+ const state = { nodes: 0, limits };
40
+ const validators = new Map();
41
+ for (const tool of tools) {
42
+ if (!isRecord(tool) || typeof tool.name !== "string" || !tool.name) {
43
+ throw new ToolSchemaContractError("tool catalog contains an invalid tool definition");
44
+ }
45
+ if (validators.has(tool.name)) throw new ToolSchemaContractError(`duplicate tool schema: ${tool.name}`);
46
+ const schema = tool.inputSchema ?? { type: "object" };
47
+ const compiled = compileSchema(schema, `tool ${tool.name}.inputSchema`, 0, state);
48
+ if (!compiled.types?.includes("object")) {
49
+ throw new ToolSchemaContractError(`tool ${tool.name} input schema must declare type object`);
50
+ }
51
+ validators.set(tool.name, compiled);
52
+ }
53
+ return Object.freeze({
54
+ names: Object.freeze([...validators.keys()]),
55
+ has(tool) { return validators.has(String(tool)); },
56
+ validate(tool, value) {
57
+ const name = String(tool || "");
58
+ const schema = validators.get(name);
59
+ if (!schema) return Object.freeze({ known: false, valid: false, issues: Object.freeze([]) });
60
+ const issues = [];
61
+ const budget = { remaining: limits.maximumValidationSteps };
62
+ try { validateNode(schema, value, "", issues, limits.maximumIssues, budget); }
63
+ catch (error) {
64
+ if (error !== VALIDATION_BUDGET_EXCEEDED) throw error;
65
+ pushIssue(issues, "", "validationBudget", "validation exceeded the bounded work budget", limits.maximumIssues);
66
+ }
67
+ return Object.freeze({ known: true, valid: issues.length === 0, issues: Object.freeze(issues) });
68
+ },
69
+ assert(tool, value) {
70
+ const result = this.validate(tool, value);
71
+ if (!result.known) throw new ToolSchemaContractError("unknown tool schema");
72
+ if (!result.valid) throw new ToolArgumentValidationError(tool, result.issues);
73
+ return value;
74
+ },
75
+ });
76
+ }
77
+
78
+ function compileSchema(schema, label, depth, state) {
79
+ if (schema === true) return { boolean: true };
80
+ if (schema === false) return { boolean: false };
81
+ if (!isRecord(schema)) throw new ToolSchemaContractError(`${label} must be an object or boolean schema`);
82
+ if (depth > state.limits.maximumDepth) throw new ToolSchemaContractError(`${label} exceeds maximum schema depth`);
83
+ state.nodes += 1;
84
+ if (state.nodes > state.limits.maximumNodes) throw new ToolSchemaContractError("tool catalog exceeds maximum schema node count");
85
+ for (const key of Object.keys(schema)) {
86
+ if (!SUPPORTED_KEYWORDS.has(key)) throw new ToolSchemaContractError(`${label} uses unsupported JSON Schema keyword: ${key}`);
87
+ }
88
+ validateDialect(schema.$schema, label);
89
+ const types = compileTypes(schema.type, label);
90
+ const node = { types };
91
+ if (Object.hasOwn(schema, "const")) node.constValue = structuredClone(schema.const);
92
+ if (Object.hasOwn(schema, "enum")) node.enumValues = compileEnum(schema.enum, label);
93
+ if (schema.pattern !== undefined) {
94
+ if (typeof schema.pattern !== "string" || schema.pattern.length > state.limits.maximumPatternLength) {
95
+ throw new ToolSchemaContractError(`${label}.pattern is invalid or too large`);
96
+ }
97
+ try { node.pattern = new RegExp(schema.pattern, "u"); }
98
+ catch { throw new ToolSchemaContractError(`${label}.pattern is not a valid ECMAScript regular expression`); }
99
+ }
100
+ for (const key of ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"]) {
101
+ if (schema[key] !== undefined) node[key] = nonNegativeInteger(schema[key], `${label}.${key}`);
102
+ }
103
+ for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
104
+ if (schema[key] !== undefined) node[key] = finiteNumber(schema[key], `${label}.${key}`);
105
+ }
106
+ if (schema.multipleOf !== undefined) {
107
+ node.multipleOf = finiteNumber(schema.multipleOf, `${label}.multipleOf`);
108
+ if (node.multipleOf <= 0) throw new ToolSchemaContractError(`${label}.multipleOf must be greater than zero`);
109
+ }
110
+ if (schema.properties !== undefined) {
111
+ if (!isRecord(schema.properties)) throw new ToolSchemaContractError(`${label}.properties must be an object`);
112
+ node.properties = new Map(Object.entries(schema.properties).map(([key, child]) => [
113
+ key, compileSchema(child, `${label}.properties.${key}`, depth + 1, state),
114
+ ]));
115
+ }
116
+ if (schema.required !== undefined) {
117
+ if (!Array.isArray(schema.required) || !schema.required.every((value) => typeof value === "string")) {
118
+ throw new ToolSchemaContractError(`${label}.required must be an array of strings`);
119
+ }
120
+ if (new Set(schema.required).size !== schema.required.length) throw new ToolSchemaContractError(`${label}.required contains duplicates`);
121
+ node.required = Object.freeze([...schema.required]);
122
+ }
123
+ if (schema.additionalProperties !== undefined) {
124
+ node.additionalProperties = typeof schema.additionalProperties === "boolean"
125
+ ? schema.additionalProperties
126
+ : compileSchema(schema.additionalProperties, `${label}.additionalProperties`, depth + 1, state);
127
+ }
128
+ if (schema.items !== undefined) node.items = compileSchema(schema.items, `${label}.items`, depth + 1, state);
129
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
130
+ if (schema[key] !== undefined) {
131
+ if (!Array.isArray(schema[key]) || schema[key].length === 0) throw new ToolSchemaContractError(`${label}.${key} must be a non-empty array`);
132
+ node[key] = schema[key].map((child, index) => compileSchema(child, `${label}.${key}[${index}]`, depth + 1, state));
133
+ }
134
+ }
135
+ for (const key of ["not", "if", "then", "else"]) {
136
+ if (schema[key] !== undefined) node[key] = compileSchema(schema[key], `${label}.${key}`, depth + 1, state);
137
+ }
138
+ return node;
139
+ }
140
+
141
+ const VALIDATION_BUDGET_EXCEEDED = Object.freeze({ code: "validation_budget_exceeded" });
142
+
143
+ function validateNode(node, value, instancePath, issues, maximumIssues, budget) {
144
+ if (issues.length >= maximumIssues) return;
145
+ if (budget.remaining <= 0) throw VALIDATION_BUDGET_EXCEEDED;
146
+ budget.remaining -= 1;
147
+ if (node.boolean === true) return;
148
+ if (node.boolean === false) return pushIssue(issues, instancePath, "falseSchema", "value is rejected by the schema", maximumIssues);
149
+ if (node.types && !node.types.some((type) => matchesType(type, value))) {
150
+ pushIssue(issues, instancePath, "type", `must be ${node.types.join(" or ")}`, maximumIssues);
151
+ return;
152
+ }
153
+ if (node.constValue !== undefined && !jsonEqual(value, node.constValue)) {
154
+ pushIssue(issues, instancePath, "const", "must equal the declared constant", maximumIssues);
155
+ }
156
+ if (node.enumValues && !node.enumValues.some((candidate) => jsonEqual(value, candidate))) {
157
+ pushIssue(issues, instancePath, "enum", "must equal one of the allowed values", maximumIssues);
158
+ }
159
+ if (typeof value === "string") validateString(node, value, instancePath, issues, maximumIssues);
160
+ if (typeof value === "number") validateNumber(node, value, instancePath, issues, maximumIssues);
161
+ if (Array.isArray(value)) validateArray(node, value, instancePath, issues, maximumIssues, budget);
162
+ else if (isRecord(value)) validateObject(node, value, instancePath, issues, maximumIssues, budget);
163
+ validateComposition(node, value, instancePath, issues, maximumIssues, budget);
164
+ }
165
+
166
+ function validateString(node, value, path, issues, maximumIssues) {
167
+ if (node.minLength !== undefined || node.maxLength !== undefined) {
168
+ const length = boundedCodePointLength(value, node.minLength, node.maxLength);
169
+ if (node.minLength !== undefined && length < node.minLength) pushIssue(issues, path, "minLength", `must contain at least ${node.minLength} characters`, maximumIssues);
170
+ if (node.maxLength !== undefined && length > node.maxLength) pushIssue(issues, path, "maxLength", `must contain at most ${node.maxLength} characters`, maximumIssues);
171
+ }
172
+ if (node.pattern && !node.pattern.test(value)) pushIssue(issues, path, "pattern", "must match the declared pattern", maximumIssues);
173
+ }
174
+
175
+ function boundedCodePointLength(value, minimum, maximum) {
176
+ const stopAt = maximum !== undefined ? maximum + 1 : minimum;
177
+ let length = 0;
178
+ for (let offset = 0; offset < value.length && (stopAt === undefined || length < stopAt); length += 1) {
179
+ const point = value.codePointAt(offset);
180
+ offset += point > 0xFFFF ? 2 : 1;
181
+ }
182
+ return length;
183
+ }
184
+
185
+ function validateNumber(node, value, path, issues, maximumIssues) {
186
+ if (!Number.isFinite(value)) return pushIssue(issues, path, "type", "must be a finite number", maximumIssues);
187
+ if (node.minimum !== undefined && value < node.minimum) pushIssue(issues, path, "minimum", `must be at least ${node.minimum}`, maximumIssues);
188
+ if (node.maximum !== undefined && value > node.maximum) pushIssue(issues, path, "maximum", `must be at most ${node.maximum}`, maximumIssues);
189
+ if (node.exclusiveMinimum !== undefined && value <= node.exclusiveMinimum) pushIssue(issues, path, "exclusiveMinimum", `must be greater than ${node.exclusiveMinimum}`, maximumIssues);
190
+ if (node.exclusiveMaximum !== undefined && value >= node.exclusiveMaximum) pushIssue(issues, path, "exclusiveMaximum", `must be less than ${node.exclusiveMaximum}`, maximumIssues);
191
+ if (node.multipleOf !== undefined && !isMultipleOf(value, node.multipleOf)) pushIssue(issues, path, "multipleOf", `must be a multiple of ${node.multipleOf}`, maximumIssues);
192
+ }
193
+
194
+ function validateArray(node, value, path, issues, maximumIssues, budget) {
195
+ if (node.minItems !== undefined && value.length < node.minItems) pushIssue(issues, path, "minItems", `must contain at least ${node.minItems} items`, maximumIssues);
196
+ if (node.maxItems !== undefined && value.length > node.maxItems) pushIssue(issues, path, "maxItems", `must contain at most ${node.maxItems} items`, maximumIssues);
197
+ if (node.items) value.forEach((item, index) => validateNode(node.items, item, `${path}/${index}`, issues, maximumIssues, budget));
198
+ }
199
+
200
+ function validateObject(node, value, path, issues, maximumIssues, budget) {
201
+ for (const required of node.required ?? []) {
202
+ if (!Object.hasOwn(value, required)) pushIssue(issues, path, "required", `missing required property ${required}`, maximumIssues);
203
+ }
204
+ let propertyCount = 0;
205
+ for (const key in value) {
206
+ if (!Object.hasOwn(value, key)) continue;
207
+ if (budget.remaining <= 0) throw VALIDATION_BUDGET_EXCEEDED;
208
+ budget.remaining -= 1;
209
+ propertyCount += 1;
210
+ const child = node.properties?.get(key);
211
+ if (child) validateNode(child, value[key], `${path}/${escapePointer(key)}`, issues, maximumIssues, budget);
212
+ else if (node.additionalProperties === false) pushIssue(issues, `${path}/${escapePointer(key)}`, "additionalProperties", "property is not allowed", maximumIssues);
213
+ else if (node.additionalProperties && node.additionalProperties !== true) validateNode(node.additionalProperties, value[key], `${path}/${escapePointer(key)}`, issues, maximumIssues, budget);
214
+ }
215
+ if (node.minProperties !== undefined && propertyCount < node.minProperties) pushIssue(issues, path, "minProperties", `must contain at least ${node.minProperties} properties`, maximumIssues);
216
+ if (node.maxProperties !== undefined && propertyCount > node.maxProperties) pushIssue(issues, path, "maxProperties", `must contain at most ${node.maxProperties} properties`, maximumIssues);
217
+ }
218
+
219
+ function validateComposition(node, value, path, issues, maximumIssues, budget) {
220
+ if (node.allOf) for (const child of node.allOf) validateNode(child, value, path, issues, maximumIssues, budget);
221
+ if (node.anyOf && !node.anyOf.some((child) => isValid(child, value, budget))) pushIssue(issues, path, "anyOf", "must match at least one schema", maximumIssues);
222
+ if (node.oneOf) {
223
+ const matches = node.oneOf.reduce((count, child) => count + Number(isValid(child, value, budget)), 0);
224
+ if (matches !== 1) pushIssue(issues, path, "oneOf", "must match exactly one schema", maximumIssues);
225
+ }
226
+ if (node.not && isValid(node.not, value, budget)) pushIssue(issues, path, "not", "must not match the excluded schema", maximumIssues);
227
+ if (node.if) {
228
+ const branch = isValid(node.if, value, budget) ? node.then : node.else;
229
+ if (branch) validateNode(branch, value, path, issues, maximumIssues, budget);
230
+ }
231
+ }
232
+
233
+ function isValid(node, value, budget) {
234
+ const issues = [];
235
+ validateNode(node, value, "", issues, 1, budget);
236
+ return issues.length === 0;
237
+ }
238
+
239
+ function compileTypes(value, label) {
240
+ if (value === undefined) return undefined;
241
+ const values = Array.isArray(value) ? value : [value];
242
+ if (values.length === 0 || !values.every((type) => typeof type === "string" && SUPPORTED_TYPES.has(type))) {
243
+ throw new ToolSchemaContractError(`${label}.type is invalid`);
244
+ }
245
+ if (new Set(values).size !== values.length) throw new ToolSchemaContractError(`${label}.type contains duplicates`);
246
+ return Object.freeze([...values]);
247
+ }
248
+
249
+ function compileEnum(value, label) {
250
+ if (!Array.isArray(value) || value.length === 0) throw new ToolSchemaContractError(`${label}.enum must be a non-empty array`);
251
+ for (let left = 0; left < value.length; left += 1) {
252
+ for (let right = left + 1; right < value.length; right += 1) {
253
+ if (jsonEqual(value[left], value[right])) throw new ToolSchemaContractError(`${label}.enum contains duplicate values`);
254
+ }
255
+ }
256
+ return structuredClone(value);
257
+ }
258
+
259
+ function validateDialect(value, label) {
260
+ if (value === undefined) return;
261
+ if (value !== JSON_SCHEMA_2020_12 && value !== `${JSON_SCHEMA_2020_12}#`) {
262
+ throw new ToolSchemaContractError(`${label} uses an unsupported JSON Schema dialect`);
263
+ }
264
+ }
265
+
266
+ function matchesType(type, value) {
267
+ if (type === "null") return value === null;
268
+ if (type === "array") return Array.isArray(value);
269
+ if (type === "object") return isRecord(value);
270
+ if (type === "integer") return typeof value === "number" && Number.isInteger(value);
271
+ if (type === "number") return typeof value === "number" && Number.isFinite(value);
272
+ return typeof value === type;
273
+ }
274
+
275
+ function jsonEqual(left, right) {
276
+ if (Object.is(left, right)) return true;
277
+ if (Array.isArray(left) && Array.isArray(right)) {
278
+ return left.length === right.length && left.every((value, index) => jsonEqual(value, right[index]));
279
+ }
280
+ if (isRecord(left) && isRecord(right)) {
281
+ const leftKeys = Object.keys(left);
282
+ const rightKeys = Object.keys(right);
283
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonEqual(left[key], right[key]));
284
+ }
285
+ return false;
286
+ }
287
+
288
+ function isMultipleOf(value, divisor) {
289
+ const quotient = value / divisor;
290
+ return Number.isFinite(quotient) && Math.abs(quotient - Math.round(quotient)) <= Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
291
+ }
292
+
293
+ function pushIssue(issues, instancePath, keyword, message, maximumIssues) {
294
+ if (issues.length >= maximumIssues) return;
295
+ issues.push(Object.freeze({ instancePath: instancePath || "", keyword, message }));
296
+ }
297
+
298
+ function normalizeLimits(value) {
299
+ const limits = {};
300
+ for (const [key, fallback] of Object.entries(DEFAULT_LIMITS)) {
301
+ const candidate = Number(value[key]);
302
+ limits[key] = Number.isInteger(candidate) && candidate > 0 ? candidate : fallback;
303
+ }
304
+ return Object.freeze(limits);
305
+ }
306
+
307
+ function nonNegativeInteger(value, label) {
308
+ if (!Number.isInteger(value) || value < 0) throw new ToolSchemaContractError(`${label} must be a non-negative integer`);
309
+ return value;
310
+ }
311
+
312
+ function finiteNumber(value, label) {
313
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ToolSchemaContractError(`${label} must be a finite number`);
314
+ return value;
315
+ }
316
+
317
+ function escapePointer(value) {
318
+ return String(value).replaceAll("~", "~0").replaceAll("/", "~1");
319
+ }
320
+
321
+ function isRecord(value) {
322
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
323
+ const prototype = Object.getPrototypeOf(value);
324
+ return prototype === Object.prototype || prototype === null;
325
+ }
@@ -2,7 +2,7 @@
2
2
  {
3
3
  "name": "server_info",
4
4
  "title": "Server information",
5
- "description": "Return authenticated account authority, effective policy/tools, daemon capability ceiling, runtime metadata, and protocol status. Treat authorization.effective_policy and authorization.effective_tools as authoritative; daemon.policy is only a ceiling.",
5
+ "description": "Return live authorization, effective tools, daemon/relay health, runtime state, and protocol status. Use this for authority or connectivity diagnosis, not for repository inventory. Treat authorization.effective_policy and authorization.effective_tools as authoritative; daemon.policy is only a ceiling.",
6
6
  "availability": "always",
7
7
  "annotations": {
8
8
  "readOnlyHint": true,
@@ -18,7 +18,7 @@
18
18
  {
19
19
  "name": "project_overview",
20
20
  "title": "Project overview",
21
- "description": "Summarize the connected workspace and repository. Remote responses report the authenticated account effective policy/tools at policy and tools, with the daemon capability ceiling preserved separately as daemonPolicy and daemonTools.",
21
+ "description": "Summarize the connected workspace, repository root, top-level entries, and project-facing runtime context. Use this for repository inventory, not as the primary live relay-health check. Remote responses report authenticated-account effective policy/tools separately from the daemon capability ceiling.",
22
22
  "availability": "always",
23
23
  "annotations": {
24
24
  "readOnlyHint": true,
@@ -34,7 +34,7 @@
34
34
  {
35
35
  "name": "session_bootstrap",
36
36
  "title": "Load session bootstrap",
37
- "description": "Load built-in working agreements, bounded automatic project facts, user-global and root workspace instructions, and capability refresh metadata for MCP session initialization.",
37
+ "description": "Load built-in working agreements, bounded automatic project facts, user-global and root workspace instructions, and capability refresh metadata. Modern clients call it explicitly; legacy initialize clients receive the same bounded guidance through the compatibility adapter.",
38
38
  "availability": "always",
39
39
  "annotations": {
40
40
  "readOnlyHint": true,
@@ -57,7 +57,7 @@
57
57
  {
58
58
  "name": "resolve_task_capabilities",
59
59
  "title": "Resolve task capabilities",
60
- "description": "Rescan built-in and automatic project context, local instruction files, skills, explicit and automatic package commands, installed applications, and browser capability metadata; rank the capabilities relevant to the current task and optionally load the best skill.",
60
+ "description": "Rescan task-relevant instructions, skills, registered commands, installed applications, browser metadata, and the effective tool catalog. Return bounded set-level execution routes, ranked tools, routing ambiguity, and an optional selected skill. Pass known_refresh_fingerprint to omit unchanged static instructions while recomputing task-specific ranking. The result is advisory: direct Bash and every other policy-allowed tool remain available.",
61
61
  "availability": "always",
62
62
  "annotations": {
63
63
  "readOnlyHint": true,
@@ -88,6 +88,12 @@
88
88
  "include_selected_skill": {
89
89
  "type": "boolean",
90
90
  "default": true
91
+ },
92
+ "known_refresh_fingerprint": {
93
+ "type": "string",
94
+ "pattern": "^[a-f0-9]{64}$",
95
+ "maxLength": 64,
96
+ "description": "Previous refresh fingerprint. When it still matches, unchanged static instructions are omitted while task-specific ranking is recomputed."
91
97
  }
92
98
  },
93
99
  "required": [
@@ -356,7 +362,7 @@
356
362
  {
357
363
  "name": "browser_list_tabs",
358
364
  "title": "List browser tabs",
359
- "description": "List tabs from the paired user's existing Chromium browser profile.",
365
+ "description": "Read the tab inventory from the paired existing Chromium profile without creating, activating, or closing a tab. Use browser_manage_tabs for tab mutations.",
360
366
  "availability": "full",
361
367
  "annotations": {
362
368
  "readOnlyHint": true,
@@ -388,7 +394,7 @@
388
394
  {
389
395
  "name": "browser_manage_tabs",
390
396
  "title": "Manage browser tabs",
391
- "description": "Create, activate, or close tabs in the paired existing browser profile.",
397
+ "description": "Create, activate, or close tabs in the paired existing Chromium profile. Use browser_list_tabs when only a read-only tab inventory is needed.",
392
398
  "availability": "full",
393
399
  "annotations": {
394
400
  "readOnlyHint": false,
@@ -437,7 +443,7 @@
437
443
  {
438
444
  "name": "browser_get_source",
439
445
  "title": "Read browser page source",
440
- "description": "Read bounded serialized current DOM HTML from the active or selected browser tab. max_bytes is one aggregate budget across at most 64 accessible frames, with explicit frame, node, and byte truncation metadata.",
446
+ "description": "Read bounded raw serialized DOM HTML from the active or selected tab when source markup is required. Use browser_inspect_page for semantic elements, actionability, reusable refs, and structured interaction planning. max_bytes is one aggregate budget across at most 64 accessible frames.",
441
447
  "availability": "full",
442
448
  "annotations": {
443
449
  "readOnlyHint": true,
@@ -479,7 +485,7 @@
479
485
  {
480
486
  "name": "browser_inspect_page",
481
487
  "title": "Inspect browser page",
482
- "description": "Inspect a bounded snapshot-version-2 semantic representation across at most 64 accessible frames, with one aggregate element budget, bounded reusable refs, actionability state, bounded page-controlled metadata, and explicit scan/frame truncation.",
488
+ "description": "Inspect a bounded semantic/actionability snapshot with reusable element refs for structured browser decisions and actions. This is not raw page source; use browser_get_source when serialized DOM HTML is required. The aggregate element budget spans at most 64 accessible frames.",
483
489
  "availability": "full",
484
490
  "annotations": {
485
491
  "readOnlyHint": true,
@@ -1103,7 +1109,7 @@
1103
1109
  {
1104
1110
  "name": "agent_context",
1105
1111
  "title": "Load agent context",
1106
- "description": "Discover built-in defaults, bounded automatic project facts, Codex-compatible global/root-to-target instruction precedence, progressively disclosed local skills, explicit commands, and safe automatic package-script command aliases for a target path.",
1112
+ "description": "Discover the instruction, skill, and registered-command inventory for a target path, including precedence and provenance. Use this when the caller needs the context itself; use resolve_task_capabilities when it needs task-specific ranking and execution-route advice.",
1107
1113
  "availability": "always",
1108
1114
  "annotations": {
1109
1115
  "readOnlyHint": true,
@@ -1230,7 +1236,7 @@
1230
1236
  {
1231
1237
  "name": "run_local_command",
1232
1238
  "title": "Run registered local command",
1233
- "description": "Run an effective manifest or automatic package-script command through its fixed argv, cwd, timeout ceiling, and extra-argument policy. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.",
1239
+ "description": "Prefer this when the repository already defines the desired operation as a registered command or package script. It runs the fixed argv/cwd/timeout contract without shell reinterpretation; use exec_command for ad hoc pipelines or run_process for an unregistered executable argv. Large output is retained for read_process.",
1234
1240
  "availability": "direct-exec",
1235
1241
  "annotations": {
1236
1242
  "readOnlyHint": false,
@@ -1665,7 +1671,7 @@
1665
1671
  {
1666
1672
  "name": "run_process",
1667
1673
  "title": "Run process directly",
1668
- "description": "Execute an argv array without a command shell. This avoids shell parsing but does not sandbox the executable or code it launches. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.",
1674
+ "description": "Run an explicit executable plus argv when no shell syntax is needed and no registered command fits. This avoids quoting, globbing, pipelines, and redirection, but it is not a sandbox; use exec_command when Bash composition is the convenient choice. Large output is retained for read_process.",
1669
1675
  "availability": "direct-exec",
1670
1676
  "annotations": {
1671
1677
  "readOnlyHint": false,
@@ -2382,7 +2388,7 @@
2382
2388
  {
2383
2389
  "name": "exec_command",
2384
2390
  "title": "Execute shell command",
2385
- "description": "Execute a shell command with workspace cwd. This is not a sandbox and has the operating-system authority of the local user. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.",
2391
+ "description": "Run Bash-compatible shell composition in the workspace: pipelines, redirection, globbing, conditionals, or compact multi-command probes. This is the convenient general escape hatch, not a sandbox, and has the local user's operating-system authority. Prefer run_local_command for an existing fixed project command and run_process when no shell syntax is needed. Large output is retained for read_process.",
2386
2392
  "availability": "shell-exec",
2387
2393
  "annotations": {
2388
2394
  "readOnlyHint": false,
@@ -1,4 +1,3 @@
1
- import { daemonLastSeenMs } from "./daemon-liveness.ts";
2
1
  import type { DaemonSocketRegistry } from "./daemon-sockets.ts";
3
2
  import { publicWorkerToolError, WorkerToolError } from "./errors.ts";
4
3
  import { workerErrorClass } from "./http.ts";
@@ -9,13 +8,6 @@ import { transformDurableStreamOutcome } from "./durable-stream-result.ts";
9
8
  import type { WorkerObservability } from "./observability.ts";
10
9
  import { sendWebSocketQuietly } from "./websocket-protocol.ts";
11
10
 
12
- type InvalidateDaemonSocket = (
13
- socket: WebSocket,
14
- message: string,
15
- closeReason: string,
16
- errorCode?: string,
17
- ) => Promise<void>;
18
-
19
11
  type TransientPendingSnapshot = {
20
12
  active: number;
21
13
  detached: number;
@@ -29,20 +21,17 @@ export class DurableStreamCallCoordinator {
29
21
  private readonly daemonRegistry: DaemonSocketRegistry;
30
22
  private readonly observability: WorkerObservability;
31
23
  private readonly maximumPendingCalls: number;
32
- private readonly invalidateDaemonSocket: InvalidateDaemonSocket;
33
24
 
34
25
  constructor(
35
26
  calls: McpPendingCallStore,
36
27
  daemonRegistry: DaemonSocketRegistry,
37
28
  observability: WorkerObservability,
38
29
  maximumPendingCalls: number,
39
- invalidateDaemonSocket: InvalidateDaemonSocket,
40
30
  ) {
41
31
  this.calls = calls;
42
32
  this.daemonRegistry = daemonRegistry;
43
33
  this.observability = observability;
44
34
  this.maximumPendingCalls = maximumPendingCalls;
45
- this.invalidateDaemonSocket = invalidateDaemonSocket;
46
35
  }
47
36
 
48
37
  async snapshot(transient: TransientPendingSnapshot): Promise<Record<string, unknown>> {
@@ -66,9 +55,10 @@ export class DurableStreamCallCoordinator {
66
55
  connectionId: string,
67
56
  outcome: PendingCallOutcome,
68
57
  knownCall?: PendingStreamCallView,
69
- ): Promise<boolean> {
58
+ ): Promise<"committed" | "missing" | "stale"> {
70
59
  const call = knownCall ?? await this.calls.get(callId);
71
- if (!call || call.connection_id !== connectionId) return false;
60
+ if (!call) return "missing";
61
+ if (call.connection_id !== connectionId) return "stale";
72
62
  const normalized = transformDurableStreamOutcome(call, outcome);
73
63
  const code = normalized.ok ? "" : publicWorkerToolError(normalized.error).code;
74
64
  try {
@@ -77,23 +67,21 @@ export class DurableStreamCallCoordinator {
77
67
  connectionId,
78
68
  streamTerminalMessage(call.requestId, normalized),
79
69
  );
80
- if (!completed) return false;
70
+ if (!completed) {
71
+ const current = await this.calls.get(callId);
72
+ return current ? "stale" : "missing";
73
+ }
81
74
  } catch (error) {
82
75
  this.observability.event("error", "mcp.stream.persist.failed", { error_class: workerErrorClass(error) });
76
+ throw error;
83
77
  }
84
78
  this.observability.callFinished(call.tool, code);
85
- return true;
79
+ return "committed";
86
80
  }
87
81
 
88
82
  async expire(call: PendingStreamCallView): Promise<void> {
89
83
  const socket = call.state === "attached" ? this.daemonRegistry.socketForConnectionId(call.connection_id) : undefined;
90
- if (socket) {
91
- sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
92
- const silentForMs = Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(socket));
93
- if (!Number.isFinite(silentForMs) || silentForMs > 45_000) {
94
- void this.invalidateDaemonSocket(socket, "daemon became unresponsive", "daemon liveness timeout");
95
- }
96
- }
84
+ if (socket) sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
97
85
  const error = call.state === "attached"
98
86
  ? new WorkerToolError("timeout", `daemon tool timed out: ${call.tool}`, true)
99
87
  : new WorkerToolError("unavailable", "daemon disconnected; reconnect grace expired", true);
@@ -111,12 +99,12 @@ export class DurableStreamCallCoordinator {
111
99
  if (!call) return false;
112
100
  const socket = call.state === "attached" ? this.daemonRegistry.socketForConnectionId(call.connection_id) : undefined;
113
101
  if (socket) sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
114
- return this.settle(
102
+ return (await this.settle(
115
103
  call.call_id,
116
104
  call.connection_id,
117
105
  { ok: false, error: new WorkerToolError("cancelled", "tool call cancelled by client") },
118
106
  call,
119
- );
107
+ )) === "committed";
120
108
  }
121
109
 
122
110
  detach(connectionId: string, graceMs: number): Promise<number> {
@@ -235,17 +235,30 @@ export function normalizeRedirectUri(value: string): string | null {
235
235
  }
236
236
  }
237
237
 
238
- export function corsPreflight(request: Request, base: string, configured: string): Response {
238
+ export function mcpOriginRejection(request: Request, base: string, configured: string): Response | null {
239
+ const origin = request.headers.get("Origin") ?? "";
240
+ if (!origin || isConfiguredOrSameOrigin(origin, base, configured)) return null;
241
+ return json({ error: "origin_not_allowed" }, 403);
242
+ }
243
+
244
+ export function corsPreflight(
245
+ request: Request, base: string, configured: string, allowedParameterHeaders: ReadonlySet<string> = EMPTY_CORS_HEADER_SET,
246
+ ): Response {
239
247
  const origin = request.headers.get("Origin") ?? "";
240
248
  if (!isConfiguredOrSameOrigin(origin, base, configured)) return json({ error: "origin_not_allowed" }, 403);
241
249
  const requestedMethod = (request.headers.get("Access-Control-Request-Method") ?? "").toUpperCase();
242
250
  if (requestedMethod && !["GET", "POST"].includes(requestedMethod)) return methodNotAllowed("GET, POST, OPTIONS");
251
+ const requestedHeaders = requestedCorsHeaders(request);
252
+ if (!requestedHeaders) return json({ error: "cors_header_invalid" }, 400);
253
+ const rejected = requestedHeaders.filter((name) => !isAllowedMcpCorsHeader(name, allowedParameterHeaders));
254
+ if (rejected.length > 0) return json({ error: "cors_header_not_allowed" }, 403);
255
+ const allowedHeaders = [...new Set([...DEFAULT_MCP_CORS_HEADERS, ...requestedHeaders])];
243
256
  return new Response(null, {
244
257
  status: 204,
245
258
  headers: {
246
259
  "access-control-allow-origin": origin,
247
260
  "access-control-allow-methods": "GET, POST, OPTIONS",
248
- "access-control-allow-headers": "authorization, content-type, dpop, last-event-id, mcp-protocol-version, mcp-session-id",
261
+ "access-control-allow-headers": allowedHeaders.join(", "),
249
262
  "access-control-max-age": "600",
250
263
  "cache-control": "no-store",
251
264
  "vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
@@ -253,6 +266,27 @@ export function corsPreflight(request: Request, base: string, configured: string
253
266
  });
254
267
  }
255
268
 
269
+ const DEFAULT_MCP_CORS_HEADERS = Object.freeze([
270
+ "authorization", "content-type", "dpop", "mcp-protocol-version", "mcp-method", "mcp-name",
271
+ "mcp-session-id", "last-event-id", "traceparent", "tracestate", "baggage",
272
+ ]);
273
+ const EMPTY_CORS_HEADER_SET: ReadonlySet<string> = new Set();
274
+ const CORS_HEADER_NAME = /^[!#$%&'*+.^_`|~0-9a-z-]+$/;
275
+ const MAX_CORS_REQUEST_HEADER_BYTES = 8192;
276
+ const MAX_CORS_REQUEST_HEADERS = 64;
277
+
278
+ function requestedCorsHeaders(request: Request): string[] | null {
279
+ const raw = request.headers.get("Access-Control-Request-Headers") ?? "";
280
+ if (new TextEncoder().encode(raw).byteLength > MAX_CORS_REQUEST_HEADER_BYTES) return null;
281
+ const values = [...new Set(raw.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean))];
282
+ if (values.length > MAX_CORS_REQUEST_HEADERS || values.some((name) => !CORS_HEADER_NAME.test(name))) return null;
283
+ return values;
284
+ }
285
+
286
+ function isAllowedMcpCorsHeader(name: string, allowedParameterHeaders: ReadonlySet<string>): boolean {
287
+ return DEFAULT_MCP_CORS_HEADERS.includes(name) || allowedParameterHeaders.has(name);
288
+ }
289
+
256
290
  export function applyCors(response: Response, request: Request, base: string, configured: string): Response {
257
291
  if (response.status === 101) return response;
258
292
  const origin = request.headers.get("Origin") ?? "";