@frihet/mcp-server 1.16.5 → 1.17.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 (68) hide show
  1. package/README.md +92 -31
  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 +549 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +254 -0
  10. package/dist/client-interface.d.ts +22 -14
  11. package/dist/client.d.ts +114 -18
  12. package/dist/client.js +520 -58
  13. package/dist/demo-client.d.ts +16 -13
  14. package/dist/demo-client.js +209 -29
  15. package/dist/demo-fixtures.d.ts +3 -3
  16. package/dist/demo-fixtures.js +7 -7
  17. package/dist/fiscal-aliases.d.ts +44 -0
  18. package/dist/fiscal-aliases.js +62 -0
  19. package/dist/index.js +41 -56
  20. package/dist/logger.d.ts +2 -2
  21. package/dist/logger.js +140 -8
  22. package/dist/observability.d.ts +18 -29
  23. package/dist/observability.js +192 -105
  24. package/dist/openai-profile.d.ts +27 -15
  25. package/dist/openai-profile.js +1191 -145
  26. package/dist/openai-review-contract.d.ts +49 -0
  27. package/dist/openai-review-contract.js +919 -0
  28. package/dist/openai-review-oauth.d.ts +98 -0
  29. package/dist/openai-review-oauth.js +136 -0
  30. package/dist/prompts/register-all.d.ts +2 -0
  31. package/dist/prompts/register-all.js +2 -0
  32. package/dist/public-capability-contract.d.ts +39 -0
  33. package/dist/public-capability-contract.js +186 -0
  34. package/dist/redaction.d.ts +10 -0
  35. package/dist/redaction.js +74 -0
  36. package/dist/resources/register-all.js +6 -12
  37. package/dist/server-composition.d.ts +16 -0
  38. package/dist/server-composition.js +44 -0
  39. package/dist/tool-exposure.d.ts +25 -22
  40. package/dist/tool-exposure.js +199 -77
  41. package/dist/tools/accountingClose.d.ts +14 -5
  42. package/dist/tools/accountingClose.js +41 -23
  43. package/dist/tools/backend-availability.d.ts +3 -3
  44. package/dist/tools/backend-availability.js +3 -3
  45. package/dist/tools/clients.js +9 -9
  46. package/dist/tools/crm.js +22 -21
  47. package/dist/tools/deposits.js +30 -11
  48. package/dist/tools/einvoice.d.ts +1 -1
  49. package/dist/tools/expenses.d.ts +16 -0
  50. package/dist/tools/expenses.js +47 -27
  51. package/dist/tools/hr.d.ts +2 -3
  52. package/dist/tools/hr.js +8 -9
  53. package/dist/tools/intelligence.js +95 -4
  54. package/dist/tools/invoices.js +150 -47
  55. package/dist/tools/payroll.d.ts +5 -8
  56. package/dist/tools/payroll.js +19 -32
  57. package/dist/tools/permissions.d.ts +4 -7
  58. package/dist/tools/permissions.js +11 -14
  59. package/dist/tools/products.js +6 -6
  60. package/dist/tools/quotes.js +109 -24
  61. package/dist/tools/shared.d.ts +1431 -75
  62. package/dist/tools/shared.js +386 -76
  63. package/dist/tools/team.js +18 -14
  64. package/dist/tools/vendors.js +9 -9
  65. package/dist/tools/webhooks.js +35 -30
  66. package/dist/types.d.ts +28 -4
  67. package/docs/agent-onboarding.json +381 -0
  68. package/package.json +32 -7
@@ -0,0 +1,919 @@
1
+ /**
2
+ * Fail-closed contract capture for the ChatGPT-reviewed MCP surface.
3
+ *
4
+ * This intentionally uses a real McpServer, real Client, in-memory MCP
5
+ * transport, and the production registration path. It therefore captures what
6
+ * tools/list serializes, not an approximation of registration config objects.
7
+ */
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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
+ import { SENSITIVE_FIELD_NAMES } from "./redaction.js";
13
+ import { FRIHET_CONNECTOR_SCOPE } from "./openai-review-oauth.js";
14
+ import { registerMcpSurface, remoteMcpSurfaceComposition, } from "./server-composition.js";
15
+ export const OPENAI_REVIEW_CONTRACT_VERSION = 6;
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",
39
+ ];
40
+ function makeRegistrationClient() {
41
+ return new Proxy({}, {
42
+ get: () => async () => ({ data: [], total: 0, limit: 0, offset: 0 }),
43
+ });
44
+ }
45
+ function isMethodNotFound(error) {
46
+ return (typeof error === "object" &&
47
+ error !== null &&
48
+ "code" in error &&
49
+ error.code === -32601);
50
+ }
51
+ async function listPromptsOrEmpty(client) {
52
+ const prompts = [];
53
+ let cursor;
54
+ try {
55
+ do {
56
+ const page = await client.listPrompts(cursor ? { cursor } : undefined);
57
+ prompts.push(...page.prompts);
58
+ cursor = page.nextCursor;
59
+ } while (cursor);
60
+ }
61
+ catch (error) {
62
+ if (isMethodNotFound(error))
63
+ return [];
64
+ throw error;
65
+ }
66
+ return prompts;
67
+ }
68
+ async function listResourcesOrEmpty(client) {
69
+ const resources = [];
70
+ let cursor;
71
+ try {
72
+ do {
73
+ const page = await client.listResources(cursor ? { cursor } : undefined);
74
+ resources.push(...page.resources);
75
+ cursor = page.nextCursor;
76
+ } while (cursor);
77
+ }
78
+ catch (error) {
79
+ if (isMethodNotFound(error))
80
+ return [];
81
+ throw error;
82
+ }
83
+ return resources;
84
+ }
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",
89
+ version: "1.0.0",
90
+ });
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: {} });
113
+ try {
114
+ await Promise.all([
115
+ server.connect(serverTransport),
116
+ client.connect(clientTransport),
117
+ ]);
118
+ let cursor;
119
+ do {
120
+ const page = await client.listTools(cursor ? { cursor } : undefined);
121
+ cursor = page.nextCursor;
122
+ } while (cursor);
123
+ const [prompts, resources] = await Promise.all([
124
+ listPromptsOrEmpty(client),
125
+ listResourcesOrEmpty(client),
126
+ ]);
127
+ return { tools: wireTools, prompts, resources };
128
+ }
129
+ finally {
130
+ await Promise.allSettled([client.close(), server.close()]);
131
+ }
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
+ }
141
+ export function buildOpenAIReviewContract(surface, oauth) {
142
+ return {
143
+ contractVersion: OPENAI_REVIEW_CONTRACT_VERSION,
144
+ tools: surface.tools,
145
+ prompts: surface.prompts,
146
+ resources: surface.resources,
147
+ oauth,
148
+ };
149
+ }
150
+ function canonicalizeValue(value) {
151
+ if (Array.isArray(value))
152
+ return value.map(canonicalizeValue);
153
+ if (value !== null && typeof value === "object") {
154
+ return Object.fromEntries(Object.entries(value)
155
+ .sort(([left], [right]) => left.localeCompare(right))
156
+ .map(([key, child]) => [key, canonicalizeValue(child)]));
157
+ }
158
+ return value;
159
+ }
160
+ /** Canonicalize object-key and tools/list order only; arrays remain semantic. */
161
+ export function canonicalizeOpenAIReviewContract(contract) {
162
+ const canonical = canonicalizeValue(contract);
163
+ canonical.tools.sort((left, right) => left.name.localeCompare(right.name));
164
+ return canonical;
165
+ }
166
+ export function serializeOpenAIReviewContract(contract) {
167
+ return `${JSON.stringify(canonicalizeOpenAIReviewContract(contract), null, 2)}\n`;
168
+ }
169
+ function schemaSensitivePaths(tools) {
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",
187
+ ]);
188
+ const paths = new Set();
189
+ const visit = (value, path) => {
190
+ if (value === undefined || value === null || typeof value !== "object")
191
+ return;
192
+ if (Array.isArray(value)) {
193
+ value.forEach((child, index) => visit(child, `${path}[${index}]`));
194
+ return;
195
+ }
196
+ for (const [key, child] of Object.entries(value)) {
197
+ const childPath = `${path}.${key}`;
198
+ if (sensitive.has(key.toLowerCase())) {
199
+ paths.add(childPath);
200
+ }
201
+ visit(child, childPath);
202
+ }
203
+ };
204
+ for (const tool of tools) {
205
+ visit(tool.inputSchema, `${tool.name}.inputSchema`);
206
+ visit(tool.outputSchema, `${tool.name}.outputSchema`);
207
+ }
208
+ return paths;
209
+ }
210
+ function firstDifference(expected, actual, path = "$") {
211
+ if (Object.is(expected, actual))
212
+ return undefined;
213
+ if (Array.isArray(expected) || Array.isArray(actual)) {
214
+ if (!Array.isArray(expected) || !Array.isArray(actual)) {
215
+ return { path, expected, actual };
216
+ }
217
+ if (expected.length !== actual.length)
218
+ return { path: `${path}.length`, expected: expected.length, actual: actual.length };
219
+ for (let index = 0; index < expected.length; index += 1) {
220
+ const difference = firstDifference(expected[index], actual[index], `${path}[${index}]`);
221
+ if (difference)
222
+ return difference;
223
+ }
224
+ return undefined;
225
+ }
226
+ if (expected !== null &&
227
+ actual !== null &&
228
+ typeof expected === "object" &&
229
+ typeof actual === "object") {
230
+ const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort();
231
+ for (const key of keys) {
232
+ const difference = firstDifference(expected[key], actual[key], `${path}.${key}`);
233
+ if (difference)
234
+ return difference;
235
+ }
236
+ return undefined;
237
+ }
238
+ return { path, expected, actual };
239
+ }
240
+ function preview(value) {
241
+ const serialized = JSON.stringify(value);
242
+ if (serialized === undefined)
243
+ return "<missing>";
244
+ return serialized.length > 180 ? `${serialized.slice(0, 177)}...` : serialized;
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
+ || scheme.scopes.length !== 1
300
+ || scheme.scopes[0] !== FRIHET_CONNECTOR_SCOPE) {
301
+ throw new Error(`${path} must require only the reviewed connector OAuth scope`);
302
+ }
303
+ }
304
+ function reviewedInputFieldSchema(tool, path) {
305
+ let current = tool?.inputSchema;
306
+ for (const rawSegment of path.split(".")) {
307
+ const arraySegment = rawSegment.endsWith("[]");
308
+ const segment = arraySegment ? rawSegment.slice(0, -2) : rawSegment;
309
+ if (current === null || Array.isArray(current) || typeof current !== "object") {
310
+ return undefined;
311
+ }
312
+ const properties = current.properties;
313
+ if (properties === null || Array.isArray(properties) || typeof properties !== "object") {
314
+ return undefined;
315
+ }
316
+ current = properties[segment];
317
+ if (arraySegment) {
318
+ if (current === null || Array.isArray(current) || typeof current !== "object") {
319
+ return undefined;
320
+ }
321
+ current = current.items;
322
+ }
323
+ }
324
+ return current !== null && !Array.isArray(current) && typeof current === "object"
325
+ ? current
326
+ : undefined;
327
+ }
328
+ /**
329
+ * Fail closed on any semantic drift from the reviewed descriptor snapshot.
330
+ */
331
+ export function assertOpenAIReviewContract(actual, expected) {
332
+ if (actual.contractVersion !== OPENAI_REVIEW_CONTRACT_VERSION) {
333
+ throw new Error(`Unsupported OpenAI review contract version: ${actual.contractVersion}`);
334
+ }
335
+ if (actual.tools.length !== OPENAI_REVIEW_TOTAL_TOOL_COUNT) {
336
+ throw new Error(`OpenAI review surface must expose exactly ${OPENAI_REVIEW_TOTAL_TOOL_COUNT} tools; got ${actual.tools.length}`);
337
+ }
338
+ const names = actual.tools.map((tool) => tool.name);
339
+ if (new Set(names).size !== names.length) {
340
+ throw new Error("OpenAI review surface contains duplicate tool names");
341
+ }
342
+ const businessNames = names;
343
+ if (businessNames.length !== OPENAI_REVIEW_BUSINESS_TOOL_COUNT) {
344
+ throw new Error(`OpenAI review surface must expose exactly ${OPENAI_REVIEW_BUSINESS_TOOL_COUNT} business tools; got ${businessNames.length}`);
345
+ }
346
+ const forbidden = FORBIDDEN_REVIEW_TOOLS.filter((name) => names.includes(name));
347
+ if (forbidden.length > 0) {
348
+ throw new Error(`Unsafe tools are forbidden in the OpenAI review surface: ${forbidden.join(", ")}`);
349
+ }
350
+ const leaks = businessNames.filter((name) => !OPENAI_REVIEWED_TOOL_ALLOWLIST.has(name));
351
+ if (leaks.length > 0) {
352
+ throw new Error(`Non-reviewed tools leaked into OpenAI surface: ${leaks.join(", ")}`);
353
+ }
354
+ const byName = new Map(actual.tools.map((tool) => [tool.name, tool]));
355
+ for (const tool of actual.tools) {
356
+ const annotations = tool.annotations;
357
+ if (annotations === null ||
358
+ Array.isArray(annotations) ||
359
+ typeof annotations !== "object") {
360
+ throw new Error(`${tool.name} must declare annotations`);
361
+ }
362
+ for (const hint of [
363
+ "readOnlyHint",
364
+ "destructiveHint",
365
+ "idempotentHint",
366
+ "openWorldHint",
367
+ ]) {
368
+ if (typeof annotations[hint] !== "boolean") {
369
+ throw new Error(`${tool.name}.${hint} must be an explicit boolean`);
370
+ }
371
+ }
372
+ assertReviewedOAuthSecuritySchemes(tool.securitySchemes, `${tool.name}.securitySchemes`);
373
+ const metadata = tool._meta;
374
+ assertReviewedOAuthSecuritySchemes(metadata !== null
375
+ && !Array.isArray(metadata)
376
+ && typeof metadata === "object"
377
+ ? metadata.securitySchemes
378
+ : undefined, `${tool.name}._meta.securitySchemes`);
379
+ if (!tool.outputSchema)
380
+ throw new Error(`${tool.name} must declare an output schema`);
381
+ assertClosedReviewedOutputSchema(tool.outputSchema, `${tool.name}.outputSchema`);
382
+ const inputSchema = tool.inputSchema;
383
+ if (inputSchema === null
384
+ || Array.isArray(inputSchema)
385
+ || typeof inputSchema !== "object"
386
+ || inputSchema.type !== "object"
387
+ || inputSchema.additionalProperties !== false) {
388
+ throw new Error(`${tool.name}.inputSchema must be a closed object`);
389
+ }
390
+ assertClosedReviewedInputSchema(inputSchema, `${tool.name}.inputSchema`);
391
+ }
392
+ for (const name of [
393
+ "list_invoices",
394
+ "search_invoices",
395
+ "list_expenses",
396
+ "list_clients",
397
+ "list_products",
398
+ "list_quotes",
399
+ "list_vendors",
400
+ ]) {
401
+ const output = byName.get(name)?.outputSchema;
402
+ const item = output
403
+ && !Array.isArray(output)
404
+ && typeof output === "object"
405
+ && output.properties
406
+ && !Array.isArray(output.properties)
407
+ && typeof output.properties === "object"
408
+ ? output.properties.data
409
+ : undefined;
410
+ const row = item
411
+ && !Array.isArray(item)
412
+ && typeof item === "object"
413
+ && item.items
414
+ && !Array.isArray(item.items)
415
+ && typeof item.items === "object"
416
+ ? item.items
417
+ : undefined;
418
+ if (!row || !Array.isArray(row.required) || !row.required.includes("id")) {
419
+ throw new Error(`${name} rows must require an id after fields projection is removed`);
420
+ }
421
+ }
422
+ for (const [name, expectedFields] of Object.entries(OPENAI_REVIEW_LIST_OUTPUT_FIELDS)) {
423
+ const tool = byName.get(name);
424
+ const outputSchema = tool?.outputSchema;
425
+ const outputProperties = outputSchema !== null
426
+ && !Array.isArray(outputSchema)
427
+ && typeof outputSchema === "object"
428
+ && outputSchema.properties !== null
429
+ && !Array.isArray(outputSchema.properties)
430
+ && typeof outputSchema.properties === "object"
431
+ ? outputSchema.properties
432
+ : undefined;
433
+ const data = outputProperties?.data;
434
+ const item = data !== null
435
+ && !Array.isArray(data)
436
+ && typeof data === "object"
437
+ && data.items !== null
438
+ && !Array.isArray(data.items)
439
+ && typeof data.items === "object"
440
+ ? data.items
441
+ : undefined;
442
+ const itemProperties = item?.properties !== null
443
+ && !Array.isArray(item?.properties)
444
+ && typeof item?.properties === "object"
445
+ ? item.properties
446
+ : undefined;
447
+ const actualFields = itemProperties ? Object.keys(itemProperties).sort() : [];
448
+ if (actualFields.length !== expectedFields.length
449
+ || actualFields.some((field, index) => field !== [...expectedFields].sort()[index])) {
450
+ throw new Error(`${name} must expose only its reviewed summary fields`);
451
+ }
452
+ }
453
+ for (const [name, maximum] of Object.entries(OPENAI_REVIEW_PAGINATION_LIMITS)) {
454
+ const tool = byName.get(name);
455
+ const inputSchema = tool?.inputSchema;
456
+ const inputProperties = inputSchema !== null
457
+ && !Array.isArray(inputSchema)
458
+ && typeof inputSchema === "object"
459
+ && inputSchema.properties !== null
460
+ && !Array.isArray(inputSchema.properties)
461
+ && typeof inputSchema.properties === "object"
462
+ ? inputSchema.properties
463
+ : undefined;
464
+ const limit = inputProperties?.limit;
465
+ const offset = inputProperties?.offset;
466
+ if (limit === null
467
+ || Array.isArray(limit)
468
+ || typeof limit !== "object"
469
+ || limit.maximum !== maximum
470
+ || offset === null
471
+ || Array.isArray(offset)
472
+ || typeof offset !== "object"
473
+ || offset.maximum !== OPENAI_REVIEW_OFFSET_MAX) {
474
+ throw new Error(`${name} must cap reviewed pagination at ${maximum} rows and offset ${OPENAI_REVIEW_OFFSET_MAX}`);
475
+ }
476
+ const outputSchema = tool?.outputSchema;
477
+ const outputProperties = outputSchema !== null
478
+ && !Array.isArray(outputSchema)
479
+ && typeof outputSchema === "object"
480
+ && outputSchema.properties !== null
481
+ && !Array.isArray(outputSchema.properties)
482
+ && typeof outputSchema.properties === "object"
483
+ ? outputSchema.properties
484
+ : undefined;
485
+ const data = outputProperties?.data;
486
+ if (data === null
487
+ || Array.isArray(data)
488
+ || typeof data !== "object"
489
+ || data.maxItems !== maximum) {
490
+ throw new Error(`${name} must cap reviewed structured output at ${maximum} rows`);
491
+ }
492
+ }
493
+ for (const [name, limits] of Object.entries(OPENAI_REVIEW_TEXT_INPUT_LIMITS)) {
494
+ const inputSchema = byName.get(name)?.inputSchema;
495
+ const properties = inputSchema !== null
496
+ && !Array.isArray(inputSchema)
497
+ && typeof inputSchema === "object"
498
+ && inputSchema.properties !== null
499
+ && !Array.isArray(inputSchema.properties)
500
+ && typeof inputSchema.properties === "object"
501
+ ? inputSchema.properties
502
+ : undefined;
503
+ for (const [field, maximum] of Object.entries(limits)) {
504
+ const fieldSchema = properties?.[field];
505
+ if (fieldSchema === null
506
+ || Array.isArray(fieldSchema)
507
+ || typeof fieldSchema !== "object"
508
+ || fieldSchema.maxLength !== maximum) {
509
+ throw new Error(`${name}.${field} must be capped at ${maximum} characters`);
510
+ }
511
+ }
512
+ }
513
+ for (const [name, paths] of Object.entries(OPENAI_REVIEW_FREE_TEXT_WARNING_PATHS)) {
514
+ for (const path of paths) {
515
+ const fieldSchema = reviewedInputFieldSchema(byName.get(name), path);
516
+ if (!fieldSchema
517
+ || typeof fieldSchema.description !== "string"
518
+ || !fieldSchema.description.includes(OPENAI_REVIEW_FREE_TEXT_WARNING)) {
519
+ throw new Error(`${name}.${path} must warn against sensitive free-text input`);
520
+ }
521
+ }
522
+ }
523
+ for (const name of ["create_invoice", "create_quote"]) {
524
+ const items = reviewedInputFieldSchema(byName.get(name), "items");
525
+ if (!items
526
+ || items.minItems !== 1
527
+ || items.maxItems !== OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX) {
528
+ throw new Error(`${name}.items must contain 1-${OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX} reviewed line items`);
529
+ }
530
+ }
531
+ const businessContextOutput = byName.get("get_business_context")?.outputSchema;
532
+ const businessContextProperties = businessContextOutput !== null
533
+ && !Array.isArray(businessContextOutput)
534
+ && typeof businessContextOutput === "object"
535
+ && businessContextOutput.properties !== null
536
+ && !Array.isArray(businessContextOutput.properties)
537
+ && typeof businessContextOutput.properties === "object"
538
+ ? businessContextOutput.properties
539
+ : undefined;
540
+ const topClients = businessContextProperties?.topClients;
541
+ if (topClients === null
542
+ || Array.isArray(topClients)
543
+ || typeof topClients !== "object"
544
+ || topClients.maxItems !== OPENAI_REVIEW_BUSINESS_CONTEXT_TOP_CLIENTS_MAX) {
545
+ throw new Error(`get_business_context.topClients must cap reviewed output at ${OPENAI_REVIEW_BUSINESS_CONTEXT_TOP_CLIENTS_MAX}`);
546
+ }
547
+ for (const name of ["get_invoice", "create_invoice", "get_quote", "create_quote"]) {
548
+ const output = byName.get(name)?.outputSchema;
549
+ const outputProperties = output !== null
550
+ && !Array.isArray(output)
551
+ && typeof output === "object"
552
+ && output.properties !== null
553
+ && !Array.isArray(output.properties)
554
+ && typeof output.properties === "object"
555
+ ? output.properties
556
+ : undefined;
557
+ const items = outputProperties?.items;
558
+ const lineItem = items !== null
559
+ && !Array.isArray(items)
560
+ && typeof items === "object"
561
+ && items.items !== null
562
+ && !Array.isArray(items.items)
563
+ && typeof items.items === "object"
564
+ ? items.items
565
+ : undefined;
566
+ const lineItemProperties = lineItem?.properties !== null
567
+ && !Array.isArray(lineItem?.properties)
568
+ && typeof lineItem?.properties === "object"
569
+ ? lineItem.properties
570
+ : undefined;
571
+ if (!lineItemProperties
572
+ || "id" in lineItemProperties
573
+ || items === null
574
+ || Array.isArray(items)
575
+ || typeof items !== "object"
576
+ || items.maxItems !== OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX) {
577
+ throw new Error(`${name} must omit unusable reviewed line-item ids and cap output at ${OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX} items`);
578
+ }
579
+ }
580
+ for (const name of ["list_client_activities", "log_client_activity"]) {
581
+ const output = byName.get(name)?.outputSchema;
582
+ const outputProperties = output !== null
583
+ && !Array.isArray(output)
584
+ && typeof output === "object"
585
+ && output.properties !== null
586
+ && !Array.isArray(output.properties)
587
+ && typeof output.properties === "object"
588
+ ? output.properties
589
+ : undefined;
590
+ const record = name === "list_client_activities"
591
+ ? (() => {
592
+ const data = outputProperties?.data;
593
+ return data !== null
594
+ && !Array.isArray(data)
595
+ && typeof data === "object"
596
+ && data.items !== null
597
+ && !Array.isArray(data.items)
598
+ && typeof data.items === "object"
599
+ ? data.items
600
+ : undefined;
601
+ })()
602
+ : output;
603
+ const recordProperties = record !== null
604
+ && !Array.isArray(record)
605
+ && typeof record === "object"
606
+ && record.properties !== null
607
+ && !Array.isArray(record.properties)
608
+ && typeof record.properties === "object"
609
+ ? record.properties
610
+ : undefined;
611
+ if (!recordProperties || "id" in recordProperties || "createdBy" in recordProperties) {
612
+ throw new Error(`${name} must omit unnecessary reviewed activity identifiers and creator markers`);
613
+ }
614
+ }
615
+ for (const name of OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS) {
616
+ const annotations = byName.get(name)?.annotations;
617
+ if (annotations?.destructiveHint !== true ||
618
+ annotations?.idempotentHint !== false ||
619
+ annotations?.openWorldHint !== true ||
620
+ annotations?.readOnlyHint !== false) {
621
+ throw new Error(`${name} may emit an irreversible workspace webhook and must be destructive, non-idempotent, open-world, and mutating`);
622
+ }
623
+ }
624
+ for (const name of OPENAI_REVIEW_CONFIRM_REQUIRED_TOOLS) {
625
+ const inputSchema = byName.get(name)?.inputSchema;
626
+ if (inputSchema === null ||
627
+ Array.isArray(inputSchema) ||
628
+ typeof inputSchema !== "object") {
629
+ throw new Error(`${name} must declare an input schema`);
630
+ }
631
+ const properties = inputSchema.properties;
632
+ const required = inputSchema.required;
633
+ const confirmSchema = properties &&
634
+ !Array.isArray(properties) &&
635
+ typeof properties === "object"
636
+ ? properties.confirm
637
+ : undefined;
638
+ if (properties === null ||
639
+ Array.isArray(properties) ||
640
+ typeof properties !== "object" ||
641
+ !("confirm" in properties) ||
642
+ !Array.isArray(required) ||
643
+ !required.includes("confirm") ||
644
+ confirmSchema === null ||
645
+ Array.isArray(confirmSchema) ||
646
+ typeof confirmSchema !== "object" ||
647
+ confirmSchema.const !== true) {
648
+ throw new Error(`${name} must expose confirm as a required literal-true reviewed input`);
649
+ }
650
+ }
651
+ for (const name of ["delete_quote"]) {
652
+ const tool = byName.get(name);
653
+ if (typeof tool?.description !== "string" || /permanently delete/i.test(tool.description)) {
654
+ throw new Error(`${name} must describe draft deletion versus non-draft cancellation truthfully`);
655
+ }
656
+ const outputSchema = jsonObject(tool.outputSchema);
657
+ const outputProperties = jsonObject(outputSchema?.properties);
658
+ const outputRequired = outputSchema?.required;
659
+ const success = jsonObject(outputProperties?.success);
660
+ const result = jsonObject(outputProperties?.result);
661
+ const branches = result?.anyOf;
662
+ const branchObjects = Array.isArray(branches)
663
+ ? branches.map((branch) => jsonObject(branch))
664
+ : [];
665
+ const branchByOutcome = new Map();
666
+ for (const branch of branchObjects) {
667
+ const properties = jsonObject(branch?.properties);
668
+ const outcome = jsonObject(properties?.outcome)?.const;
669
+ if (branch && typeof outcome === "string")
670
+ branchByOutcome.set(outcome, branch);
671
+ }
672
+ const deleted = branchByOutcome.get("deleted");
673
+ const deletedProperties = jsonObject(deleted?.properties);
674
+ const cancelled = branchByOutcome.get("cancelled");
675
+ const cancelledProperties = jsonObject(cancelled?.properties);
676
+ const deletedRequired = deleted?.required;
677
+ const cancelledRequired = cancelled?.required;
678
+ const cancelledStatus = jsonObject(cancelledProperties?.status);
679
+ const cancelledEffects = jsonObject(cancelledProperties?.externalEffects);
680
+ const cancelledEffectItems = jsonObject(cancelledEffects?.items);
681
+ if (!outputSchema ||
682
+ outputSchema.type !== "object" ||
683
+ outputSchema.additionalProperties !== false ||
684
+ !outputProperties ||
685
+ Object.keys(outputProperties).length !== 3 ||
686
+ !Array.isArray(outputRequired) ||
687
+ !["success", "id", "result"].every((field) => outputRequired.includes(field)) ||
688
+ outputRequired.length !== 3 ||
689
+ success?.const !== true ||
690
+ !result ||
691
+ branchObjects.length !== 2 ||
692
+ branchObjects.some((branch) => !branch) ||
693
+ branchByOutcome.size !== 2 ||
694
+ !deleted ||
695
+ deleted.type !== "object" ||
696
+ deleted.additionalProperties !== false ||
697
+ !deletedProperties ||
698
+ Object.keys(deletedProperties).length !== 1 ||
699
+ !Array.isArray(deletedRequired) ||
700
+ deletedRequired.length !== 1 ||
701
+ deletedRequired[0] !== "outcome" ||
702
+ !cancelled ||
703
+ cancelled.type !== "object" ||
704
+ cancelled.additionalProperties !== false ||
705
+ !cancelledProperties ||
706
+ Object.keys(cancelledProperties).some((field) => !["outcome", "status", "previousStatus", "externalEffects"].includes(field)) ||
707
+ !Array.isArray(cancelledRequired) ||
708
+ cancelledRequired.length !== 3 ||
709
+ !["outcome", "status", "externalEffects"].every((field) => cancelledRequired.includes(field)) ||
710
+ cancelledStatus?.const !== "cancelled" ||
711
+ cancelledEffects?.type !== "array" ||
712
+ cancelledEffectItems?.type !== "string") {
713
+ throw new Error(`${name} output schema must expose exactly two closed result branches: webhook-free deleted or status=cancelled with external effects`);
714
+ }
715
+ }
716
+ for (const [name, numberField] of [
717
+ ["create_invoice", "invoiceNumber"],
718
+ ["create_quote", "quoteNumber"],
719
+ ]) {
720
+ const outputSchema = byName.get(name)?.outputSchema;
721
+ const properties = outputSchema !== null
722
+ && !Array.isArray(outputSchema)
723
+ && typeof outputSchema === "object"
724
+ && outputSchema.properties !== null
725
+ && !Array.isArray(outputSchema.properties)
726
+ && typeof outputSchema.properties === "object"
727
+ ? outputSchema.properties
728
+ : undefined;
729
+ const required = outputSchema !== null
730
+ && !Array.isArray(outputSchema)
731
+ && typeof outputSchema === "object"
732
+ ? outputSchema.required
733
+ : undefined;
734
+ const status = properties?.status;
735
+ if (!properties
736
+ || !Array.isArray(required)
737
+ || !["id", numberField, "status", "externalEffects"].every((field) => required.includes(field))
738
+ || status === null
739
+ || Array.isArray(status)
740
+ || typeof status !== "object"
741
+ || status.const !== "draft") {
742
+ throw new Error(`${name} output schema must require its reserved number and literal draft status`);
743
+ }
744
+ }
745
+ const createOutputContracts = {
746
+ create_client: { required: ["id", "name", "externalEffects"], forbidden: ["stage"] },
747
+ create_expense: {
748
+ required: ["id", "description", "amount", "date", "taxDeductible", "externalEffects"],
749
+ forbidden: ["paidDate"],
750
+ },
751
+ create_invoice: {
752
+ required: ["id", "invoiceNumber", "clientId", "clientName", "items", "issueDate", "dueDate", "status", "externalEffects"],
753
+ forbidden: ["total"],
754
+ },
755
+ create_product: {
756
+ required: ["id", "name", "unitPrice", "externalEffects"],
757
+ forbidden: ["isActive"],
758
+ },
759
+ create_quote: {
760
+ required: ["id", "quoteNumber", "clientId", "clientName", "items", "issueDate", "status", "externalEffects"],
761
+ forbidden: ["total"],
762
+ },
763
+ };
764
+ for (const [name, contract] of Object.entries(createOutputContracts)) {
765
+ const output = jsonObject(byName.get(name)?.outputSchema);
766
+ const properties = jsonObject(output?.properties);
767
+ const required = output?.required;
768
+ if (!properties
769
+ || !Array.isArray(required)
770
+ || !contract.required.every((field) => required.includes(field))
771
+ || contract.forbidden.some((field) => field in properties)) {
772
+ throw new Error(`${name} output schema must require guaranteed create fields and omit impossible create-only fields`);
773
+ }
774
+ }
775
+ const createInvoice = byName.get("create_invoice");
776
+ const createInvoiceInput = createInvoice?.inputSchema;
777
+ const createInvoiceProperties = createInvoiceInput &&
778
+ !Array.isArray(createInvoiceInput) &&
779
+ typeof createInvoiceInput === "object" &&
780
+ createInvoiceInput.properties &&
781
+ !Array.isArray(createInvoiceInput.properties) &&
782
+ typeof createInvoiceInput.properties === "object"
783
+ ? createInvoiceInput.properties
784
+ : undefined;
785
+ if (!createInvoiceProperties) {
786
+ throw new Error("create_invoice must declare a reviewed input schema");
787
+ }
788
+ const forbiddenInvoiceInputs = [
789
+ "clientId",
790
+ "clientTaxId",
791
+ "clientAddress",
792
+ "clientLocation",
793
+ "status",
794
+ "irpfRate",
795
+ "equivalenceSurchargeRate",
796
+ "prepayment",
797
+ "seriesId",
798
+ "documentNumber",
799
+ "poNumber",
800
+ "operationType",
801
+ ].filter((field) => field in createInvoiceProperties);
802
+ if (forbiddenInvoiceInputs.length > 0) {
803
+ throw new Error(`create_invoice must remain draft-only and minimal; forbidden inputs: ${forbiddenInvoiceInputs.join(", ")}`);
804
+ }
805
+ if (typeof createInvoice?.description !== "string" ||
806
+ !/draft/i.test(createInvoice.description) ||
807
+ !/(reserves?|assigns?).*document number/i.test(createInvoice.description) ||
808
+ !/(does not|cannot).*(issue|email|submit|file)/i.test(createInvoice.description) ||
809
+ !/(create|link).*client/i.test(createInvoice.description)) {
810
+ throw new Error("create_invoice must disclose draft-only behavior, numbering, and possible client creation");
811
+ }
812
+ const createQuote = byName.get("create_quote");
813
+ const createQuoteInput = createQuote?.inputSchema;
814
+ const createQuoteProperties = createQuoteInput &&
815
+ !Array.isArray(createQuoteInput) &&
816
+ typeof createQuoteInput === "object" &&
817
+ createQuoteInput.properties &&
818
+ !Array.isArray(createQuoteInput.properties) &&
819
+ typeof createQuoteInput.properties === "object"
820
+ ? createQuoteInput.properties
821
+ : undefined;
822
+ if (!createQuoteProperties || "status" in createQuoteProperties) {
823
+ throw new Error("create_quote must remain draft-only and hide lifecycle status");
824
+ }
825
+ if (typeof createQuote?.description !== "string" ||
826
+ !/draft/i.test(createQuote.description) ||
827
+ !/(reserves?|assigns?).*document number/i.test(createQuote.description) ||
828
+ !/advances?.*numbering counter/i.test(createQuote.description) ||
829
+ !/(create|link).*client/i.test(createQuote.description) ||
830
+ !/(does not|cannot).*(send|accept)/i.test(createQuote.description)) {
831
+ throw new Error("create_quote must disclose draft-only behavior, numbering, and possible client creation");
832
+ }
833
+ for (const name of ["create_expense", "update_expense"]) {
834
+ const description = byName.get(name)?.description;
835
+ if (typeof description !== "string" ||
836
+ !/tax-deductible (?:classification|choice)[^.!?]{0,120}affect(?:s)? Frihet's (?:internal )?accounting/i.test(description) ||
837
+ !/(?:does not file anything|files nothing)/i.test(description)) {
838
+ throw new Error(`${name} must disclose the internal tax-classification effect without implying filing`);
839
+ }
840
+ }
841
+ const createExpenseInput = byName.get("create_expense")?.inputSchema;
842
+ const createExpenseProperties = createExpenseInput &&
843
+ !Array.isArray(createExpenseInput) &&
844
+ typeof createExpenseInput === "object" &&
845
+ createExpenseInput.properties &&
846
+ !Array.isArray(createExpenseInput.properties) &&
847
+ typeof createExpenseInput.properties === "object"
848
+ ? createExpenseInput.properties
849
+ : undefined;
850
+ const createExpenseRequired = createExpenseInput &&
851
+ !Array.isArray(createExpenseInput) &&
852
+ typeof createExpenseInput === "object" &&
853
+ Array.isArray(createExpenseInput.required)
854
+ ? createExpenseInput.required
855
+ : [];
856
+ if (!createExpenseProperties ||
857
+ "paidDate" in createExpenseProperties ||
858
+ !createExpenseRequired.includes("date") ||
859
+ !createExpenseRequired.includes("taxDeductible")) {
860
+ throw new Error("create_expense must require an explicit date and deductible choice without marking the expense paid");
861
+ }
862
+ if (typeof byName.get("create_expense")?.description !== "string" ||
863
+ !/explicit (?:expense )?date/i.test(byName.get("create_expense").description) ||
864
+ !/tax-deductible choice/i.test(byName.get("create_expense").description)) {
865
+ throw new Error("create_expense must disclose its explicit accounting choices");
866
+ }
867
+ const createExpenseConfirm = reviewedInputFieldSchema(byName.get("create_expense"), "confirm");
868
+ if (!createExpenseConfirm
869
+ || typeof createExpenseConfirm.description !== "string"
870
+ || !/separate step[^.!?]*may persist[^.!?]*expense write fails/i.test(createExpenseConfirm.description)) {
871
+ throw new Error("create_expense confirmation must disclose the separate vendor-write residual");
872
+ }
873
+ const updateExpenseInput = byName.get("update_expense")?.inputSchema;
874
+ const updateExpenseProperties = updateExpenseInput &&
875
+ !Array.isArray(updateExpenseInput) &&
876
+ typeof updateExpenseInput === "object" &&
877
+ updateExpenseInput.properties &&
878
+ !Array.isArray(updateExpenseInput.properties) &&
879
+ typeof updateExpenseInput.properties === "object"
880
+ ? updateExpenseInput.properties
881
+ : undefined;
882
+ if (!updateExpenseProperties || "vendor" in updateExpenseProperties) {
883
+ throw new Error("update_expense must not expose a supplier-name change without vendor identity re-resolution");
884
+ }
885
+ if (!("date" in updateExpenseProperties) ||
886
+ !("taxDeductible" in updateExpenseProperties)) {
887
+ throw new Error("update_expense must retain its reviewed date and deductible controls");
888
+ }
889
+ if (actual.prompts.length !== 0 || actual.resources.length !== 0) {
890
+ throw new Error(`OpenAI review surface must expose 0 prompts and 0 resources; got ${actual.prompts.length} prompts and ${actual.resources.length} resources`);
891
+ }
892
+ const sensitivePaths = [...schemaSensitivePaths(actual.tools)];
893
+ if (sensitivePaths.length > 0) {
894
+ throw new Error(`Sensitive schema fields are forbidden in the OpenAI review surface: ${sensitivePaths.join(", ")}`);
895
+ }
896
+ const expectedSensitivePaths = [...schemaSensitivePaths(expected.tools)];
897
+ if (expectedSensitivePaths.length > 0) {
898
+ throw new Error(`Reviewed OpenAI snapshot contains sensitive schema fields: ${expectedSensitivePaths.join(", ")}`);
899
+ }
900
+ const reviewedText = JSON.stringify(actual.tools);
901
+ const forbiddenText = [
902
+ /\btaxId\b/u,
903
+ /\bModelos?\s+\d+/iu,
904
+ /quarterly\s+tax/iu,
905
+ /gestor[ií]a/iu,
906
+ /recurring\s+invoices?/iu,
907
+ /webhook\s+(?:configuration|administration)/iu,
908
+ ].find((pattern) => pattern.test(reviewedText));
909
+ if (forbiddenText) {
910
+ throw new Error(`Reviewed descriptor contains excluded-surface prose: ${forbiddenText.source}`);
911
+ }
912
+ const canonicalExpected = canonicalizeOpenAIReviewContract(expected);
913
+ const canonicalActual = canonicalizeOpenAIReviewContract(actual);
914
+ const difference = firstDifference(canonicalExpected, canonicalActual);
915
+ if (difference) {
916
+ throw new Error(`OpenAI review descriptor drift at ${difference.path}: expected ${preview(difference.expected)}, got ${preview(difference.actual)}`);
917
+ }
918
+ }
919
+ //# sourceMappingURL=openai-review-contract.js.map