@frihet/mcp-server 1.16.6 → 2.0.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 (108) hide show
  1. package/README.md +168 -82
  2. package/dist/agent-onboarding.d.ts +98 -0
  3. package/dist/agent-onboarding.js +275 -0
  4. package/dist/api-origin.d.ts +9 -0
  5. package/dist/api-origin.js +41 -0
  6. package/dist/canary/mcp-harness.d.ts +77 -0
  7. package/dist/canary/mcp-harness.js +552 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +262 -0
  10. package/dist/claude-candidate-contract.d.ts +482 -0
  11. package/dist/claude-candidate-contract.js +87 -0
  12. package/dist/claude-output-privacy.d.ts +17 -0
  13. package/dist/claude-output-privacy.js +240 -0
  14. package/dist/claude-profile.d.ts +18 -0
  15. package/dist/claude-profile.js +260 -0
  16. package/dist/client-interface.d.ts +47 -33
  17. package/dist/client.d.ts +172 -40
  18. package/dist/client.js +708 -165
  19. package/dist/demo-client.d.ts +41 -32
  20. package/dist/demo-client.js +202 -31
  21. package/dist/demo-fixtures.d.ts +3 -3
  22. package/dist/demo-fixtures.js +7 -7
  23. package/dist/deposit-contract.d.ts +29 -0
  24. package/dist/deposit-contract.js +76 -0
  25. package/dist/fetch-no-redirect.d.ts +3 -0
  26. package/dist/fetch-no-redirect.js +16 -0
  27. package/dist/fiscal-aliases.d.ts +44 -0
  28. package/dist/fiscal-aliases.js +62 -0
  29. package/dist/fiscal-period.d.ts +26 -0
  30. package/dist/fiscal-period.js +31 -0
  31. package/dist/google-profile.d.ts +19 -0
  32. package/dist/google-profile.js +122 -0
  33. package/dist/index.js +68 -59
  34. package/dist/logger.d.ts +2 -2
  35. package/dist/logger.js +140 -8
  36. package/dist/muse-profile.d.ts +19 -0
  37. package/dist/muse-profile.js +122 -0
  38. package/dist/observability.d.ts +18 -29
  39. package/dist/observability.js +194 -106
  40. package/dist/openai-profile.d.ts +26 -22
  41. package/dist/openai-profile.js +1193 -157
  42. package/dist/openai-review-contract.d.ts +15 -6
  43. package/dist/openai-review-contract.js +714 -41
  44. package/dist/openai-review-oauth.d.ts +149 -2
  45. package/dist/openai-review-oauth.js +220 -6
  46. package/dist/prompts/register-all.d.ts +2 -0
  47. package/dist/prompts/register-all.js +34 -78
  48. package/dist/public-capability-contract.d.ts +39 -0
  49. package/dist/public-capability-contract.js +186 -0
  50. package/dist/redaction.d.ts +15 -0
  51. package/dist/redaction.js +83 -0
  52. package/dist/resources/register-all.js +154 -64
  53. package/dist/retry-after.d.ts +19 -0
  54. package/dist/retry-after.js +84 -0
  55. package/dist/server-composition.d.ts +19 -0
  56. package/dist/server-composition.js +74 -0
  57. package/dist/tool-exposure.d.ts +31 -27
  58. package/dist/tool-exposure.js +211 -87
  59. package/dist/tools/accountingClose.d.ts +14 -5
  60. package/dist/tools/accountingClose.js +41 -23
  61. package/dist/tools/audit_gl.js +8 -8
  62. package/dist/tools/backend-availability.d.ts +8 -29
  63. package/dist/tools/backend-availability.js +31 -30
  64. package/dist/tools/bank_rules.js +66 -15
  65. package/dist/tools/banking.js +5 -13
  66. package/dist/tools/clients.js +261 -15
  67. package/dist/tools/crm.js +22 -21
  68. package/dist/tools/deposits.js +114 -85
  69. package/dist/tools/einvoice.d.ts +16 -2
  70. package/dist/tools/einvoice.js +59 -12
  71. package/dist/tools/expenses.d.ts +16 -0
  72. package/dist/tools/expenses.js +47 -27
  73. package/dist/tools/fiscal.d.ts +3 -4
  74. package/dist/tools/fiscal.js +93 -60
  75. package/dist/tools/gestoria.js +15 -7
  76. package/dist/tools/hr.d.ts +2 -3
  77. package/dist/tools/hr.js +49 -20
  78. package/dist/tools/igic.d.ts +9 -8
  79. package/dist/tools/igic.js +34 -64
  80. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  81. package/dist/tools/impuesto_sociedades.js +16 -37
  82. package/dist/tools/intelligence.js +166 -12
  83. package/dist/tools/invoices.js +224 -54
  84. package/dist/tools/kitchen.js +19 -9
  85. package/dist/tools/payroll.d.ts +5 -8
  86. package/dist/tools/payroll.js +19 -32
  87. package/dist/tools/permissions.d.ts +4 -7
  88. package/dist/tools/permissions.js +11 -14
  89. package/dist/tools/products.js +6 -6
  90. package/dist/tools/quotes.js +172 -26
  91. package/dist/tools/recurring.js +26 -25
  92. package/dist/tools/register-all.d.ts +2 -2
  93. package/dist/tools/register-all.js +9 -6
  94. package/dist/tools/search.d.ts +19 -0
  95. package/dist/tools/search.js +84 -0
  96. package/dist/tools/shared.d.ts +1445 -109
  97. package/dist/tools/shared.js +478 -102
  98. package/dist/tools/team.js +24 -28
  99. package/dist/tools/time.js +4 -12
  100. package/dist/tools/vendors.js +249 -15
  101. package/dist/tools/webhooks.js +35 -30
  102. package/dist/types.d.ts +31 -5
  103. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  104. package/dist/workflows/daily-operational-brief.js +881 -0
  105. package/docs/agent-onboarding.json +391 -0
  106. package/package.json +49 -12
  107. package/dist/tools/onboarding.d.ts +0 -19
  108. package/dist/tools/onboarding.js +0 -64
@@ -2,25 +2,41 @@
2
2
  * Fail-closed contract capture for the ChatGPT-reviewed MCP surface.
3
3
  *
4
4
  * This intentionally uses a real McpServer, real Client, in-memory MCP
5
- * transport, and the production registration path. It therefore freezes what
5
+ * transport, and the production registration path. It therefore captures what
6
6
  * tools/list serializes, not an approximation of registration config objects.
7
7
  */
8
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
9
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
- import { applyOpenAIReviewProfiles, OPENAI_REVIEWED_TOOL_ALLOWLIST, } from "./openai-profile.js";
11
+ import { OPENAI_REVIEW_BUSINESS_CONTEXT_TOP_CLIENTS_MAX, OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX, OPENAI_REVIEW_FREE_TEXT_WARNING, OPENAI_REVIEW_FREE_TEXT_WARNING_PATHS, OPENAI_REVIEW_LIST_OUTPUT_FIELDS, OPENAI_REVIEW_OFFSET_MAX, OPENAI_REVIEW_PAGINATION_LIMITS, OPENAI_REVIEW_TEXT_INPUT_LIMITS, OPENAI_REVIEW_CONFIRM_REQUIRED_TOOLS, OPENAI_REVIEWED_TOOL_ALLOWLIST, OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS, } from "./openai-profile.js";
12
12
  import { SENSITIVE_FIELD_NAMES } from "./redaction.js";
13
- import { registerAllPrompts } from "./prompts/register-all.js";
14
- import { registerAllResources } from "./resources/register-all.js";
15
- import { registerAllTools } from "./tools/register-all.js";
16
- export const OPENAI_REVIEW_CONTRACT_VERSION = 1;
17
- export const OPENAI_REVIEW_BUSINESS_TOOL_COUNT = 53;
18
- export const OPENAI_REVIEW_DISCOVERY_TOOLS = [
19
- "describe_tool",
20
- "list_tool_groups",
21
- "search_tools",
13
+ import { hasExactOpenAIReviewScopes } from "./openai-review-oauth.js";
14
+ import { registerMcpSurface, remoteMcpSurfaceComposition, } from "./server-composition.js";
15
+ export const OPENAI_REVIEW_CONTRACT_VERSION = 7;
16
+ export const OPENAI_REVIEW_BUSINESS_TOOL_COUNT = 33;
17
+ export const OPENAI_REVIEW_TOTAL_TOOL_COUNT = OPENAI_REVIEW_BUSINESS_TOOL_COUNT;
18
+ const FORBIDDEN_REVIEW_TOOLS = [
19
+ "get_invoice_pdf",
20
+ "get_monthly_summary",
21
+ "apply_late_fee",
22
+ "update_invoice",
23
+ "mark_invoice_paid",
24
+ "delete_invoice",
25
+ "send_invoice",
26
+ "duplicate_invoice",
27
+ "create_credit_note",
28
+ "delete_client",
29
+ "delete_expense",
30
+ "delete_product",
31
+ "delete_vendor",
32
+ "send_quote",
33
+ "update_quote",
34
+ "list_webhooks",
35
+ "get_webhook",
36
+ "create_webhook",
37
+ "update_webhook",
38
+ "delete_webhook",
22
39
  ];
23
- export const OPENAI_REVIEW_TOTAL_TOOL_COUNT = 56;
24
40
  function makeRegistrationClient() {
25
41
  return new Proxy({}, {
26
42
  get: () => async () => ({ data: [], total: 0, limit: 0, offset: 0 }),
@@ -66,40 +82,62 @@ async function listResourcesOrEmpty(client) {
66
82
  }
67
83
  return resources;
68
84
  }
69
- /** Capture the exact OpenAI grouped surface over a real tools/list request. */
70
- export async function captureOpenAIReviewMcpSurface() {
71
- const server = new McpServer({
72
- name: "frihet-openai-review-freeze",
85
+ /** Capture the exact OpenAI full-description surface with a caller-selected SDK tree. */
86
+ export async function captureOpenAIReviewMcpSurfaceWithRuntime(sdkRuntime) {
87
+ const server = new sdkRuntime.McpServer({
88
+ name: "frihet-openai-review-contract",
73
89
  version: "1.0.0",
74
90
  });
75
- applyOpenAIReviewProfiles(server);
76
- registerAllTools(server, makeRegistrationClient());
77
- registerAllResources(server);
78
- registerAllPrompts(server);
79
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
80
- const client = new Client({ name: "frihet-openai-review-freeze-client", version: "1.0.0" }, { capabilities: {} });
91
+ registerMcpSurface(server, makeRegistrationClient(), remoteMcpSurfaceComposition(true, false));
92
+ const [clientTransport, serverTransport] = sdkRuntime.InMemoryTransport.createLinkedPair();
93
+ // The MCP SDK client parses tools/list through the generic MCP ToolSchema,
94
+ // which currently strips OpenAI's top-level `securitySchemes` extension.
95
+ // Capture the server's actual wire result so this contract proves what the
96
+ // review scanner receives, including both the standard OpenAI field and its
97
+ // legacy `_meta` mirror.
98
+ const wireTools = [];
99
+ const originalServerSend = serverTransport.send.bind(serverTransport);
100
+ serverTransport.send = async (message, options) => {
101
+ if (typeof message === "object"
102
+ && message !== null
103
+ && "result" in message
104
+ && typeof message.result === "object"
105
+ && message.result !== null
106
+ && "tools" in message.result
107
+ && Array.isArray(message.result.tools)) {
108
+ wireTools.push(...message.result.tools);
109
+ }
110
+ await originalServerSend(message, options);
111
+ };
112
+ const client = new sdkRuntime.Client({ name: "frihet-openai-review-contract-client", version: "1.0.0" }, { capabilities: {} });
81
113
  try {
82
114
  await Promise.all([
83
115
  server.connect(serverTransport),
84
116
  client.connect(clientTransport),
85
117
  ]);
86
- const tools = [];
87
118
  let cursor;
88
119
  do {
89
120
  const page = await client.listTools(cursor ? { cursor } : undefined);
90
- tools.push(...page.tools);
91
121
  cursor = page.nextCursor;
92
122
  } while (cursor);
93
123
  const [prompts, resources] = await Promise.all([
94
124
  listPromptsOrEmpty(client),
95
125
  listResourcesOrEmpty(client),
96
126
  ]);
97
- return { tools, prompts, resources };
127
+ return { tools: wireTools, prompts, resources };
98
128
  }
99
129
  finally {
100
130
  await Promise.allSettled([client.close(), server.close()]);
101
131
  }
102
132
  }
133
+ /** Capture the package-local surface; deploy gates inject the Worker runtime. */
134
+ export async function captureOpenAIReviewMcpSurface() {
135
+ return captureOpenAIReviewMcpSurfaceWithRuntime({
136
+ Client,
137
+ InMemoryTransport,
138
+ McpServer,
139
+ });
140
+ }
103
141
  export function buildOpenAIReviewContract(surface, oauth) {
104
142
  return {
105
143
  contractVersion: OPENAI_REVIEW_CONTRACT_VERSION,
@@ -129,14 +167,23 @@ export function serializeOpenAIReviewContract(contract) {
129
167
  return `${JSON.stringify(canonicalizeOpenAIReviewContract(contract), null, 2)}\n`;
130
168
  }
131
169
  function schemaSensitivePaths(tools) {
132
- const sensitive = new Set(SENSITIVE_FIELD_NAMES.map((field) => field.toLowerCase()));
133
- // `documentNumber` is an invoice sequence identifier on these two tools,
134
- // not the guest/customer identity-document field covered by the shared
135
- // redaction policy. Keep the exception exact so any future occurrence is
136
- // still rejected by default.
137
- const nonSensitiveBusinessPaths = new Set([
138
- "create_invoice.inputSchema.properties.documentNumber",
139
- "update_invoice.inputSchema.properties.documentNumber",
170
+ const sensitive = new Set([
171
+ ...SENSITIVE_FIELD_NAMES.map((field) => field.toLowerCase()),
172
+ // Opaque documents cannot be inspected by field-level redaction. A raw
173
+ // base64 payload on the reviewed surface can therefore smuggle regulated
174
+ // identifiers even when every surrounding JSON key is safe.
175
+ "base64",
176
+ // The reviewed connector does not request or echo precise address fields,
177
+ // nor does its monthly summary expose filing-estimate payloads.
178
+ "address",
179
+ "clientaddress",
180
+ "clientlocation",
181
+ "taxliability",
182
+ "estimatedmodel303",
183
+ "vatpayable",
184
+ "irpfretained",
185
+ "fiscalzone",
186
+ "series",
140
187
  ]);
141
188
  const paths = new Set();
142
189
  const visit = (value, path) => {
@@ -148,8 +195,7 @@ function schemaSensitivePaths(tools) {
148
195
  }
149
196
  for (const [key, child] of Object.entries(value)) {
150
197
  const childPath = `${path}.${key}`;
151
- if (sensitive.has(key.toLowerCase()) &&
152
- !nonSensitiveBusinessPaths.has(childPath)) {
198
+ if (sensitive.has(key.toLowerCase())) {
153
199
  paths.add(childPath);
154
200
  }
155
201
  visit(child, childPath);
@@ -197,6 +243,87 @@ function preview(value) {
197
243
  return "<missing>";
198
244
  return serialized.length > 180 ? `${serialized.slice(0, 177)}...` : serialized;
199
245
  }
246
+ function jsonObject(value) {
247
+ return value !== null && value !== undefined && !Array.isArray(value) && typeof value === "object"
248
+ ? value
249
+ : undefined;
250
+ }
251
+ function assertClosedReviewedOutputSchema(value, path) {
252
+ if (value === undefined || value === null || typeof value !== "object")
253
+ return;
254
+ if (Array.isArray(value)) {
255
+ value.forEach((child, index) => assertClosedReviewedOutputSchema(child, `${path}[${index}]`));
256
+ return;
257
+ }
258
+ if (value.type === "object") {
259
+ if (value.additionalProperties !== false) {
260
+ throw new Error(`${path} must set additionalProperties=false`);
261
+ }
262
+ const properties = value.properties;
263
+ if (properties === null
264
+ || Array.isArray(properties)
265
+ || typeof properties !== "object"
266
+ || Object.keys(properties).length === 0) {
267
+ throw new Error(`${path} must declare at least one reviewed property`);
268
+ }
269
+ }
270
+ for (const [key, child] of Object.entries(value)) {
271
+ assertClosedReviewedOutputSchema(child, `${path}.${key}`);
272
+ }
273
+ }
274
+ function assertClosedReviewedInputSchema(value, path) {
275
+ if (value === undefined || value === null || typeof value !== "object")
276
+ return;
277
+ if (Array.isArray(value)) {
278
+ value.forEach((child, index) => assertClosedReviewedInputSchema(child, `${path}[${index}]`));
279
+ return;
280
+ }
281
+ if (value.type === "object" && value.additionalProperties !== false) {
282
+ throw new Error(`${path} must set additionalProperties=false`);
283
+ }
284
+ for (const [key, child] of Object.entries(value)) {
285
+ assertClosedReviewedInputSchema(child, `${path}.${key}`);
286
+ }
287
+ }
288
+ function assertReviewedOAuthSecuritySchemes(value, path) {
289
+ if (!Array.isArray(value) || value.length !== 1) {
290
+ throw new Error(`${path} must contain exactly one reviewed OAuth scheme`);
291
+ }
292
+ const scheme = value[0];
293
+ if (scheme === null
294
+ || Array.isArray(scheme)
295
+ || typeof scheme !== "object"
296
+ || Object.keys(scheme).sort().join(",") !== "scopes,type"
297
+ || scheme.type !== "oauth2"
298
+ || !Array.isArray(scheme.scopes)
299
+ || !hasExactOpenAIReviewScopes(scheme.scopes)) {
300
+ throw new Error(`${path} must require exactly the reviewed OpenAI OAuth scopes`);
301
+ }
302
+ }
303
+ function reviewedInputFieldSchema(tool, path) {
304
+ let current = tool?.inputSchema;
305
+ for (const rawSegment of path.split(".")) {
306
+ const arraySegment = rawSegment.endsWith("[]");
307
+ const segment = arraySegment ? rawSegment.slice(0, -2) : rawSegment;
308
+ if (current === null || Array.isArray(current) || typeof current !== "object") {
309
+ return undefined;
310
+ }
311
+ const properties = current.properties;
312
+ if (properties === null || Array.isArray(properties) || typeof properties !== "object") {
313
+ return undefined;
314
+ }
315
+ current = properties[segment];
316
+ if (arraySegment) {
317
+ if (current === null || Array.isArray(current) || typeof current !== "object") {
318
+ return undefined;
319
+ }
320
+ current = current.items;
321
+ }
322
+ }
323
+ return current !== null && !Array.isArray(current) && typeof current === "object"
324
+ ? current
325
+ : undefined;
326
+ }
200
327
  /**
201
328
  * Fail closed on any semantic drift from the reviewed descriptor snapshot.
202
329
  */
@@ -211,19 +338,553 @@ export function assertOpenAIReviewContract(actual, expected) {
211
338
  if (new Set(names).size !== names.length) {
212
339
  throw new Error("OpenAI review surface contains duplicate tool names");
213
340
  }
214
- const discovery = new Set(OPENAI_REVIEW_DISCOVERY_TOOLS);
215
- const businessNames = names.filter((name) => !discovery.has(name));
341
+ const businessNames = names;
216
342
  if (businessNames.length !== OPENAI_REVIEW_BUSINESS_TOOL_COUNT) {
217
343
  throw new Error(`OpenAI review surface must expose exactly ${OPENAI_REVIEW_BUSINESS_TOOL_COUNT} business tools; got ${businessNames.length}`);
218
344
  }
219
- const missingDiscovery = OPENAI_REVIEW_DISCOVERY_TOOLS.filter((name) => !names.includes(name));
220
- if (missingDiscovery.length > 0) {
221
- throw new Error(`Missing discovery tools: ${missingDiscovery.join(", ")}`);
345
+ const forbidden = FORBIDDEN_REVIEW_TOOLS.filter((name) => names.includes(name));
346
+ if (forbidden.length > 0) {
347
+ throw new Error(`Unsafe tools are forbidden in the OpenAI review surface: ${forbidden.join(", ")}`);
222
348
  }
223
349
  const leaks = businessNames.filter((name) => !OPENAI_REVIEWED_TOOL_ALLOWLIST.has(name));
224
350
  if (leaks.length > 0) {
225
351
  throw new Error(`Non-reviewed tools leaked into OpenAI surface: ${leaks.join(", ")}`);
226
352
  }
353
+ const byName = new Map(actual.tools.map((tool) => [tool.name, tool]));
354
+ for (const tool of actual.tools) {
355
+ const annotations = tool.annotations;
356
+ if (annotations === null ||
357
+ Array.isArray(annotations) ||
358
+ typeof annotations !== "object") {
359
+ throw new Error(`${tool.name} must declare annotations`);
360
+ }
361
+ for (const hint of [
362
+ "readOnlyHint",
363
+ "destructiveHint",
364
+ "idempotentHint",
365
+ "openWorldHint",
366
+ ]) {
367
+ if (typeof annotations[hint] !== "boolean") {
368
+ throw new Error(`${tool.name}.${hint} must be an explicit boolean`);
369
+ }
370
+ }
371
+ assertReviewedOAuthSecuritySchemes(tool.securitySchemes, `${tool.name}.securitySchemes`);
372
+ const metadata = tool._meta;
373
+ assertReviewedOAuthSecuritySchemes(metadata !== null
374
+ && !Array.isArray(metadata)
375
+ && typeof metadata === "object"
376
+ ? metadata.securitySchemes
377
+ : undefined, `${tool.name}._meta.securitySchemes`);
378
+ if (!tool.outputSchema)
379
+ throw new Error(`${tool.name} must declare an output schema`);
380
+ assertClosedReviewedOutputSchema(tool.outputSchema, `${tool.name}.outputSchema`);
381
+ const inputSchema = tool.inputSchema;
382
+ if (inputSchema === null
383
+ || Array.isArray(inputSchema)
384
+ || typeof inputSchema !== "object"
385
+ || inputSchema.type !== "object"
386
+ || inputSchema.additionalProperties !== false) {
387
+ throw new Error(`${tool.name}.inputSchema must be a closed object`);
388
+ }
389
+ assertClosedReviewedInputSchema(inputSchema, `${tool.name}.inputSchema`);
390
+ }
391
+ for (const name of [
392
+ "list_invoices",
393
+ "search_invoices",
394
+ "list_expenses",
395
+ "list_clients",
396
+ "list_products",
397
+ "list_quotes",
398
+ "list_vendors",
399
+ ]) {
400
+ const output = byName.get(name)?.outputSchema;
401
+ const item = output
402
+ && !Array.isArray(output)
403
+ && typeof output === "object"
404
+ && output.properties
405
+ && !Array.isArray(output.properties)
406
+ && typeof output.properties === "object"
407
+ ? output.properties.data
408
+ : undefined;
409
+ const row = item
410
+ && !Array.isArray(item)
411
+ && typeof item === "object"
412
+ && item.items
413
+ && !Array.isArray(item.items)
414
+ && typeof item.items === "object"
415
+ ? item.items
416
+ : undefined;
417
+ if (!row || !Array.isArray(row.required) || !row.required.includes("id")) {
418
+ throw new Error(`${name} rows must require an id after fields projection is removed`);
419
+ }
420
+ }
421
+ for (const [name, expectedFields] of Object.entries(OPENAI_REVIEW_LIST_OUTPUT_FIELDS)) {
422
+ const tool = byName.get(name);
423
+ const outputSchema = tool?.outputSchema;
424
+ const outputProperties = outputSchema !== null
425
+ && !Array.isArray(outputSchema)
426
+ && typeof outputSchema === "object"
427
+ && outputSchema.properties !== null
428
+ && !Array.isArray(outputSchema.properties)
429
+ && typeof outputSchema.properties === "object"
430
+ ? outputSchema.properties
431
+ : undefined;
432
+ const data = outputProperties?.data;
433
+ const item = data !== null
434
+ && !Array.isArray(data)
435
+ && typeof data === "object"
436
+ && data.items !== null
437
+ && !Array.isArray(data.items)
438
+ && typeof data.items === "object"
439
+ ? data.items
440
+ : undefined;
441
+ const itemProperties = item?.properties !== null
442
+ && !Array.isArray(item?.properties)
443
+ && typeof item?.properties === "object"
444
+ ? item.properties
445
+ : undefined;
446
+ const actualFields = itemProperties ? Object.keys(itemProperties).sort() : [];
447
+ if (actualFields.length !== expectedFields.length
448
+ || actualFields.some((field, index) => field !== [...expectedFields].sort()[index])) {
449
+ throw new Error(`${name} must expose only its reviewed summary fields`);
450
+ }
451
+ }
452
+ for (const [name, maximum] of Object.entries(OPENAI_REVIEW_PAGINATION_LIMITS)) {
453
+ const tool = byName.get(name);
454
+ const inputSchema = tool?.inputSchema;
455
+ const inputProperties = inputSchema !== null
456
+ && !Array.isArray(inputSchema)
457
+ && typeof inputSchema === "object"
458
+ && inputSchema.properties !== null
459
+ && !Array.isArray(inputSchema.properties)
460
+ && typeof inputSchema.properties === "object"
461
+ ? inputSchema.properties
462
+ : undefined;
463
+ const limit = inputProperties?.limit;
464
+ const offset = inputProperties?.offset;
465
+ if (limit === null
466
+ || Array.isArray(limit)
467
+ || typeof limit !== "object"
468
+ || limit.maximum !== maximum
469
+ || offset === null
470
+ || Array.isArray(offset)
471
+ || typeof offset !== "object"
472
+ || offset.maximum !== OPENAI_REVIEW_OFFSET_MAX) {
473
+ throw new Error(`${name} must cap reviewed pagination at ${maximum} rows and offset ${OPENAI_REVIEW_OFFSET_MAX}`);
474
+ }
475
+ const outputSchema = tool?.outputSchema;
476
+ const outputProperties = outputSchema !== null
477
+ && !Array.isArray(outputSchema)
478
+ && typeof outputSchema === "object"
479
+ && outputSchema.properties !== null
480
+ && !Array.isArray(outputSchema.properties)
481
+ && typeof outputSchema.properties === "object"
482
+ ? outputSchema.properties
483
+ : undefined;
484
+ const data = outputProperties?.data;
485
+ if (data === null
486
+ || Array.isArray(data)
487
+ || typeof data !== "object"
488
+ || data.maxItems !== maximum) {
489
+ throw new Error(`${name} must cap reviewed structured output at ${maximum} rows`);
490
+ }
491
+ }
492
+ for (const [name, limits] of Object.entries(OPENAI_REVIEW_TEXT_INPUT_LIMITS)) {
493
+ const inputSchema = byName.get(name)?.inputSchema;
494
+ const properties = inputSchema !== null
495
+ && !Array.isArray(inputSchema)
496
+ && typeof inputSchema === "object"
497
+ && inputSchema.properties !== null
498
+ && !Array.isArray(inputSchema.properties)
499
+ && typeof inputSchema.properties === "object"
500
+ ? inputSchema.properties
501
+ : undefined;
502
+ for (const [field, maximum] of Object.entries(limits)) {
503
+ const fieldSchema = properties?.[field];
504
+ if (fieldSchema === null
505
+ || Array.isArray(fieldSchema)
506
+ || typeof fieldSchema !== "object"
507
+ || fieldSchema.maxLength !== maximum) {
508
+ throw new Error(`${name}.${field} must be capped at ${maximum} characters`);
509
+ }
510
+ }
511
+ }
512
+ for (const [name, paths] of Object.entries(OPENAI_REVIEW_FREE_TEXT_WARNING_PATHS)) {
513
+ for (const path of paths) {
514
+ const fieldSchema = reviewedInputFieldSchema(byName.get(name), path);
515
+ if (!fieldSchema
516
+ || typeof fieldSchema.description !== "string"
517
+ || !fieldSchema.description.includes(OPENAI_REVIEW_FREE_TEXT_WARNING)) {
518
+ throw new Error(`${name}.${path} must warn against sensitive free-text input`);
519
+ }
520
+ }
521
+ }
522
+ for (const name of ["create_invoice", "create_quote"]) {
523
+ const items = reviewedInputFieldSchema(byName.get(name), "items");
524
+ if (!items
525
+ || items.minItems !== 1
526
+ || items.maxItems !== OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX) {
527
+ throw new Error(`${name}.items must contain 1-${OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX} reviewed line items`);
528
+ }
529
+ }
530
+ const businessContextOutput = byName.get("get_business_context")?.outputSchema;
531
+ const businessContextProperties = businessContextOutput !== null
532
+ && !Array.isArray(businessContextOutput)
533
+ && typeof businessContextOutput === "object"
534
+ && businessContextOutput.properties !== null
535
+ && !Array.isArray(businessContextOutput.properties)
536
+ && typeof businessContextOutput.properties === "object"
537
+ ? businessContextOutput.properties
538
+ : undefined;
539
+ const topClients = businessContextProperties?.topClients;
540
+ if (topClients === null
541
+ || Array.isArray(topClients)
542
+ || typeof topClients !== "object"
543
+ || topClients.maxItems !== OPENAI_REVIEW_BUSINESS_CONTEXT_TOP_CLIENTS_MAX) {
544
+ throw new Error(`get_business_context.topClients must cap reviewed output at ${OPENAI_REVIEW_BUSINESS_CONTEXT_TOP_CLIENTS_MAX}`);
545
+ }
546
+ for (const name of ["get_invoice", "create_invoice", "get_quote", "create_quote"]) {
547
+ const output = byName.get(name)?.outputSchema;
548
+ const outputProperties = output !== null
549
+ && !Array.isArray(output)
550
+ && typeof output === "object"
551
+ && output.properties !== null
552
+ && !Array.isArray(output.properties)
553
+ && typeof output.properties === "object"
554
+ ? output.properties
555
+ : undefined;
556
+ const items = outputProperties?.items;
557
+ const lineItem = items !== null
558
+ && !Array.isArray(items)
559
+ && typeof items === "object"
560
+ && items.items !== null
561
+ && !Array.isArray(items.items)
562
+ && typeof items.items === "object"
563
+ ? items.items
564
+ : undefined;
565
+ const lineItemProperties = lineItem?.properties !== null
566
+ && !Array.isArray(lineItem?.properties)
567
+ && typeof lineItem?.properties === "object"
568
+ ? lineItem.properties
569
+ : undefined;
570
+ if (!lineItemProperties
571
+ || "id" in lineItemProperties
572
+ || items === null
573
+ || Array.isArray(items)
574
+ || typeof items !== "object"
575
+ || items.maxItems !== OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX) {
576
+ throw new Error(`${name} must omit unusable reviewed line-item ids and cap output at ${OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX} items`);
577
+ }
578
+ }
579
+ for (const name of ["list_client_activities", "log_client_activity"]) {
580
+ const output = byName.get(name)?.outputSchema;
581
+ const outputProperties = output !== null
582
+ && !Array.isArray(output)
583
+ && typeof output === "object"
584
+ && output.properties !== null
585
+ && !Array.isArray(output.properties)
586
+ && typeof output.properties === "object"
587
+ ? output.properties
588
+ : undefined;
589
+ const record = name === "list_client_activities"
590
+ ? (() => {
591
+ const data = outputProperties?.data;
592
+ return data !== null
593
+ && !Array.isArray(data)
594
+ && typeof data === "object"
595
+ && data.items !== null
596
+ && !Array.isArray(data.items)
597
+ && typeof data.items === "object"
598
+ ? data.items
599
+ : undefined;
600
+ })()
601
+ : output;
602
+ const recordProperties = record !== null
603
+ && !Array.isArray(record)
604
+ && typeof record === "object"
605
+ && record.properties !== null
606
+ && !Array.isArray(record.properties)
607
+ && typeof record.properties === "object"
608
+ ? record.properties
609
+ : undefined;
610
+ if (!recordProperties || "id" in recordProperties || "createdBy" in recordProperties) {
611
+ throw new Error(`${name} must omit unnecessary reviewed activity identifiers and creator markers`);
612
+ }
613
+ }
614
+ for (const name of OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS) {
615
+ const annotations = byName.get(name)?.annotations;
616
+ if (annotations?.destructiveHint !== true ||
617
+ annotations?.idempotentHint !== false ||
618
+ annotations?.openWorldHint !== true ||
619
+ annotations?.readOnlyHint !== false) {
620
+ throw new Error(`${name} may emit an irreversible workspace webhook and must be destructive, non-idempotent, open-world, and mutating`);
621
+ }
622
+ }
623
+ for (const name of OPENAI_REVIEW_CONFIRM_REQUIRED_TOOLS) {
624
+ const inputSchema = byName.get(name)?.inputSchema;
625
+ if (inputSchema === null ||
626
+ Array.isArray(inputSchema) ||
627
+ typeof inputSchema !== "object") {
628
+ throw new Error(`${name} must declare an input schema`);
629
+ }
630
+ const properties = inputSchema.properties;
631
+ const required = inputSchema.required;
632
+ const confirmSchema = properties &&
633
+ !Array.isArray(properties) &&
634
+ typeof properties === "object"
635
+ ? properties.confirm
636
+ : undefined;
637
+ if (properties === null ||
638
+ Array.isArray(properties) ||
639
+ typeof properties !== "object" ||
640
+ !("confirm" in properties) ||
641
+ !Array.isArray(required) ||
642
+ !required.includes("confirm") ||
643
+ confirmSchema === null ||
644
+ Array.isArray(confirmSchema) ||
645
+ typeof confirmSchema !== "object" ||
646
+ confirmSchema.const !== true) {
647
+ throw new Error(`${name} must expose confirm as a required literal-true reviewed input`);
648
+ }
649
+ }
650
+ for (const name of ["delete_quote"]) {
651
+ const tool = byName.get(name);
652
+ if (typeof tool?.description !== "string" || /permanently delete/i.test(tool.description)) {
653
+ throw new Error(`${name} must describe draft deletion versus non-draft cancellation truthfully`);
654
+ }
655
+ const outputSchema = jsonObject(tool.outputSchema);
656
+ const outputProperties = jsonObject(outputSchema?.properties);
657
+ const outputRequired = outputSchema?.required;
658
+ const success = jsonObject(outputProperties?.success);
659
+ const result = jsonObject(outputProperties?.result);
660
+ const branches = result?.anyOf;
661
+ const branchObjects = Array.isArray(branches)
662
+ ? branches.map((branch) => jsonObject(branch))
663
+ : [];
664
+ const branchByOutcome = new Map();
665
+ for (const branch of branchObjects) {
666
+ const properties = jsonObject(branch?.properties);
667
+ const outcome = jsonObject(properties?.outcome)?.const;
668
+ if (branch && typeof outcome === "string")
669
+ branchByOutcome.set(outcome, branch);
670
+ }
671
+ const deleted = branchByOutcome.get("deleted");
672
+ const deletedProperties = jsonObject(deleted?.properties);
673
+ const cancelled = branchByOutcome.get("cancelled");
674
+ const cancelledProperties = jsonObject(cancelled?.properties);
675
+ const deletedRequired = deleted?.required;
676
+ const cancelledRequired = cancelled?.required;
677
+ const cancelledStatus = jsonObject(cancelledProperties?.status);
678
+ const cancelledEffects = jsonObject(cancelledProperties?.externalEffects);
679
+ const cancelledEffectItems = jsonObject(cancelledEffects?.items);
680
+ if (!outputSchema ||
681
+ outputSchema.type !== "object" ||
682
+ outputSchema.additionalProperties !== false ||
683
+ !outputProperties ||
684
+ Object.keys(outputProperties).length !== 3 ||
685
+ !Array.isArray(outputRequired) ||
686
+ !["success", "id", "result"].every((field) => outputRequired.includes(field)) ||
687
+ outputRequired.length !== 3 ||
688
+ success?.const !== true ||
689
+ !result ||
690
+ branchObjects.length !== 2 ||
691
+ branchObjects.some((branch) => !branch) ||
692
+ branchByOutcome.size !== 2 ||
693
+ !deleted ||
694
+ deleted.type !== "object" ||
695
+ deleted.additionalProperties !== false ||
696
+ !deletedProperties ||
697
+ Object.keys(deletedProperties).length !== 1 ||
698
+ !Array.isArray(deletedRequired) ||
699
+ deletedRequired.length !== 1 ||
700
+ deletedRequired[0] !== "outcome" ||
701
+ !cancelled ||
702
+ cancelled.type !== "object" ||
703
+ cancelled.additionalProperties !== false ||
704
+ !cancelledProperties ||
705
+ Object.keys(cancelledProperties).some((field) => !["outcome", "status", "previousStatus", "externalEffects"].includes(field)) ||
706
+ !Array.isArray(cancelledRequired) ||
707
+ cancelledRequired.length !== 3 ||
708
+ !["outcome", "status", "externalEffects"].every((field) => cancelledRequired.includes(field)) ||
709
+ cancelledStatus?.const !== "cancelled" ||
710
+ cancelledEffects?.type !== "array" ||
711
+ cancelledEffectItems?.type !== "string") {
712
+ throw new Error(`${name} output schema must expose exactly two closed result branches: webhook-free deleted or status=cancelled with external effects`);
713
+ }
714
+ }
715
+ for (const [name, numberField] of [
716
+ ["create_invoice", "invoiceNumber"],
717
+ ["create_quote", "quoteNumber"],
718
+ ]) {
719
+ const outputSchema = byName.get(name)?.outputSchema;
720
+ const properties = outputSchema !== null
721
+ && !Array.isArray(outputSchema)
722
+ && typeof outputSchema === "object"
723
+ && outputSchema.properties !== null
724
+ && !Array.isArray(outputSchema.properties)
725
+ && typeof outputSchema.properties === "object"
726
+ ? outputSchema.properties
727
+ : undefined;
728
+ const required = outputSchema !== null
729
+ && !Array.isArray(outputSchema)
730
+ && typeof outputSchema === "object"
731
+ ? outputSchema.required
732
+ : undefined;
733
+ const status = properties?.status;
734
+ if (!properties
735
+ || !Array.isArray(required)
736
+ || !["id", numberField, "status", "externalEffects"].every((field) => required.includes(field))
737
+ || status === null
738
+ || Array.isArray(status)
739
+ || typeof status !== "object"
740
+ || status.const !== "draft") {
741
+ throw new Error(`${name} output schema must require its reserved number and literal draft status`);
742
+ }
743
+ }
744
+ const createOutputContracts = {
745
+ create_client: { required: ["id", "name", "externalEffects"], forbidden: ["stage"] },
746
+ create_expense: {
747
+ required: ["id", "description", "amount", "date", "taxDeductible", "externalEffects"],
748
+ forbidden: ["paidDate"],
749
+ },
750
+ create_invoice: {
751
+ required: ["id", "invoiceNumber", "clientId", "clientName", "items", "issueDate", "dueDate", "status", "externalEffects"],
752
+ forbidden: ["total"],
753
+ },
754
+ create_product: {
755
+ required: ["id", "name", "unitPrice", "externalEffects"],
756
+ forbidden: ["isActive"],
757
+ },
758
+ create_quote: {
759
+ required: ["id", "quoteNumber", "clientId", "clientName", "items", "issueDate", "status", "externalEffects"],
760
+ forbidden: ["total"],
761
+ },
762
+ };
763
+ for (const [name, contract] of Object.entries(createOutputContracts)) {
764
+ const output = jsonObject(byName.get(name)?.outputSchema);
765
+ const properties = jsonObject(output?.properties);
766
+ const required = output?.required;
767
+ if (!properties
768
+ || !Array.isArray(required)
769
+ || !contract.required.every((field) => required.includes(field))
770
+ || contract.forbidden.some((field) => field in properties)) {
771
+ throw new Error(`${name} output schema must require guaranteed create fields and omit impossible create-only fields`);
772
+ }
773
+ }
774
+ const createInvoice = byName.get("create_invoice");
775
+ const createInvoiceInput = createInvoice?.inputSchema;
776
+ const createInvoiceProperties = createInvoiceInput &&
777
+ !Array.isArray(createInvoiceInput) &&
778
+ typeof createInvoiceInput === "object" &&
779
+ createInvoiceInput.properties &&
780
+ !Array.isArray(createInvoiceInput.properties) &&
781
+ typeof createInvoiceInput.properties === "object"
782
+ ? createInvoiceInput.properties
783
+ : undefined;
784
+ if (!createInvoiceProperties) {
785
+ throw new Error("create_invoice must declare a reviewed input schema");
786
+ }
787
+ const forbiddenInvoiceInputs = [
788
+ "clientId",
789
+ "clientTaxId",
790
+ "clientAddress",
791
+ "clientLocation",
792
+ "status",
793
+ "irpfRate",
794
+ "equivalenceSurchargeRate",
795
+ "prepayment",
796
+ "seriesId",
797
+ "documentNumber",
798
+ "poNumber",
799
+ "operationType",
800
+ ].filter((field) => field in createInvoiceProperties);
801
+ if (forbiddenInvoiceInputs.length > 0) {
802
+ throw new Error(`create_invoice must remain draft-only and minimal; forbidden inputs: ${forbiddenInvoiceInputs.join(", ")}`);
803
+ }
804
+ if (typeof createInvoice?.description !== "string" ||
805
+ !/draft/i.test(createInvoice.description) ||
806
+ !/(reserves?|assigns?).*document number/i.test(createInvoice.description) ||
807
+ !/(does not|cannot).*(issue|email|submit|file)/i.test(createInvoice.description) ||
808
+ !/(create|link).*client/i.test(createInvoice.description)) {
809
+ throw new Error("create_invoice must disclose draft-only behavior, numbering, and possible client creation");
810
+ }
811
+ const createQuote = byName.get("create_quote");
812
+ const createQuoteInput = createQuote?.inputSchema;
813
+ const createQuoteProperties = createQuoteInput &&
814
+ !Array.isArray(createQuoteInput) &&
815
+ typeof createQuoteInput === "object" &&
816
+ createQuoteInput.properties &&
817
+ !Array.isArray(createQuoteInput.properties) &&
818
+ typeof createQuoteInput.properties === "object"
819
+ ? createQuoteInput.properties
820
+ : undefined;
821
+ if (!createQuoteProperties || "status" in createQuoteProperties) {
822
+ throw new Error("create_quote must remain draft-only and hide lifecycle status");
823
+ }
824
+ if (typeof createQuote?.description !== "string" ||
825
+ !/draft/i.test(createQuote.description) ||
826
+ !/(reserves?|assigns?).*document number/i.test(createQuote.description) ||
827
+ !/advances?.*numbering counter/i.test(createQuote.description) ||
828
+ !/(create|link).*client/i.test(createQuote.description) ||
829
+ !/(does not|cannot).*(send|accept)/i.test(createQuote.description)) {
830
+ throw new Error("create_quote must disclose draft-only behavior, numbering, and possible client creation");
831
+ }
832
+ for (const name of ["create_expense", "update_expense"]) {
833
+ const description = byName.get(name)?.description;
834
+ if (typeof description !== "string" ||
835
+ !/tax-deductible (?:classification|choice)[^.!?]{0,120}affect(?:s)? Frihet's (?:internal )?accounting/i.test(description) ||
836
+ !/(?:does not file anything|files nothing)/i.test(description)) {
837
+ throw new Error(`${name} must disclose the internal tax-classification effect without implying filing`);
838
+ }
839
+ }
840
+ const createExpenseInput = byName.get("create_expense")?.inputSchema;
841
+ const createExpenseProperties = createExpenseInput &&
842
+ !Array.isArray(createExpenseInput) &&
843
+ typeof createExpenseInput === "object" &&
844
+ createExpenseInput.properties &&
845
+ !Array.isArray(createExpenseInput.properties) &&
846
+ typeof createExpenseInput.properties === "object"
847
+ ? createExpenseInput.properties
848
+ : undefined;
849
+ const createExpenseRequired = createExpenseInput &&
850
+ !Array.isArray(createExpenseInput) &&
851
+ typeof createExpenseInput === "object" &&
852
+ Array.isArray(createExpenseInput.required)
853
+ ? createExpenseInput.required
854
+ : [];
855
+ if (!createExpenseProperties ||
856
+ "paidDate" in createExpenseProperties ||
857
+ !createExpenseRequired.includes("date") ||
858
+ !createExpenseRequired.includes("taxDeductible")) {
859
+ throw new Error("create_expense must require an explicit date and deductible choice without marking the expense paid");
860
+ }
861
+ if (typeof byName.get("create_expense")?.description !== "string" ||
862
+ !/explicit (?:expense )?date/i.test(byName.get("create_expense").description) ||
863
+ !/tax-deductible choice/i.test(byName.get("create_expense").description)) {
864
+ throw new Error("create_expense must disclose its explicit accounting choices");
865
+ }
866
+ const createExpenseConfirm = reviewedInputFieldSchema(byName.get("create_expense"), "confirm");
867
+ if (!createExpenseConfirm
868
+ || typeof createExpenseConfirm.description !== "string"
869
+ || !/separate step[^.!?]*may persist[^.!?]*expense write fails/i.test(createExpenseConfirm.description)) {
870
+ throw new Error("create_expense confirmation must disclose the separate vendor-write residual");
871
+ }
872
+ const updateExpenseInput = byName.get("update_expense")?.inputSchema;
873
+ const updateExpenseProperties = updateExpenseInput &&
874
+ !Array.isArray(updateExpenseInput) &&
875
+ typeof updateExpenseInput === "object" &&
876
+ updateExpenseInput.properties &&
877
+ !Array.isArray(updateExpenseInput.properties) &&
878
+ typeof updateExpenseInput.properties === "object"
879
+ ? updateExpenseInput.properties
880
+ : undefined;
881
+ if (!updateExpenseProperties || "vendor" in updateExpenseProperties) {
882
+ throw new Error("update_expense must not expose a supplier-name change without vendor identity re-resolution");
883
+ }
884
+ if (!("date" in updateExpenseProperties) ||
885
+ !("taxDeductible" in updateExpenseProperties)) {
886
+ throw new Error("update_expense must retain its reviewed date and deductible controls");
887
+ }
227
888
  if (actual.prompts.length !== 0 || actual.resources.length !== 0) {
228
889
  throw new Error(`OpenAI review surface must expose 0 prompts and 0 resources; got ${actual.prompts.length} prompts and ${actual.resources.length} resources`);
229
890
  }
@@ -233,7 +894,19 @@ export function assertOpenAIReviewContract(actual, expected) {
233
894
  }
234
895
  const expectedSensitivePaths = [...schemaSensitivePaths(expected.tools)];
235
896
  if (expectedSensitivePaths.length > 0) {
236
- throw new Error(`Frozen OpenAI review snapshot contains sensitive schema fields: ${expectedSensitivePaths.join(", ")}`);
897
+ throw new Error(`Reviewed OpenAI snapshot contains sensitive schema fields: ${expectedSensitivePaths.join(", ")}`);
898
+ }
899
+ const reviewedText = JSON.stringify(actual.tools);
900
+ const forbiddenText = [
901
+ /\btaxId\b/u,
902
+ /\bModelos?\s+\d+/iu,
903
+ /quarterly\s+tax/iu,
904
+ /gestor[ií]a/iu,
905
+ /recurring\s+invoices?/iu,
906
+ /webhook\s+(?:configuration|administration)/iu,
907
+ ].find((pattern) => pattern.test(reviewedText));
908
+ if (forbiddenText) {
909
+ throw new Error(`Reviewed descriptor contains excluded-surface prose: ${forbiddenText.source}`);
237
910
  }
238
911
  const canonicalExpected = canonicalizeOpenAIReviewContract(expected);
239
912
  const canonicalActual = canonicalizeOpenAIReviewContract(actual);