@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.
- package/README.md +168 -82
- package/dist/agent-onboarding.d.ts +98 -0
- package/dist/agent-onboarding.js +275 -0
- package/dist/api-origin.d.ts +9 -0
- package/dist/api-origin.js +41 -0
- package/dist/canary/mcp-harness.d.ts +77 -0
- package/dist/canary/mcp-harness.js +552 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +262 -0
- package/dist/claude-candidate-contract.d.ts +482 -0
- package/dist/claude-candidate-contract.js +87 -0
- package/dist/claude-output-privacy.d.ts +17 -0
- package/dist/claude-output-privacy.js +240 -0
- package/dist/claude-profile.d.ts +18 -0
- package/dist/claude-profile.js +260 -0
- package/dist/client-interface.d.ts +47 -33
- package/dist/client.d.ts +172 -40
- package/dist/client.js +708 -165
- package/dist/demo-client.d.ts +41 -32
- package/dist/demo-client.js +202 -31
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/deposit-contract.d.ts +29 -0
- package/dist/deposit-contract.js +76 -0
- package/dist/fetch-no-redirect.d.ts +3 -0
- package/dist/fetch-no-redirect.js +16 -0
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/fiscal-period.d.ts +26 -0
- package/dist/fiscal-period.js +31 -0
- package/dist/google-profile.d.ts +19 -0
- package/dist/google-profile.js +122 -0
- package/dist/index.js +68 -59
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/muse-profile.d.ts +19 -0
- package/dist/muse-profile.js +122 -0
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +194 -106
- package/dist/openai-profile.d.ts +26 -22
- package/dist/openai-profile.js +1193 -157
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +714 -41
- package/dist/openai-review-oauth.d.ts +149 -2
- package/dist/openai-review-oauth.js +220 -6
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +34 -78
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +15 -0
- package/dist/redaction.js +83 -0
- package/dist/resources/register-all.js +154 -64
- package/dist/retry-after.d.ts +19 -0
- package/dist/retry-after.js +84 -0
- package/dist/server-composition.d.ts +19 -0
- package/dist/server-composition.js +74 -0
- package/dist/tool-exposure.d.ts +31 -27
- package/dist/tool-exposure.js +211 -87
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/audit_gl.js +8 -8
- package/dist/tools/backend-availability.d.ts +8 -29
- package/dist/tools/backend-availability.js +31 -30
- package/dist/tools/bank_rules.js +66 -15
- package/dist/tools/banking.js +5 -13
- package/dist/tools/clients.js +261 -15
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +114 -85
- package/dist/tools/einvoice.d.ts +16 -2
- package/dist/tools/einvoice.js +59 -12
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/fiscal.d.ts +3 -4
- package/dist/tools/fiscal.js +93 -60
- package/dist/tools/gestoria.js +15 -7
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +49 -20
- package/dist/tools/igic.d.ts +9 -8
- package/dist/tools/igic.js +34 -64
- package/dist/tools/impuesto_sociedades.d.ts +4 -5
- package/dist/tools/impuesto_sociedades.js +16 -37
- package/dist/tools/intelligence.js +166 -12
- package/dist/tools/invoices.js +224 -54
- package/dist/tools/kitchen.js +19 -9
- package/dist/tools/payroll.d.ts +5 -8
- package/dist/tools/payroll.js +19 -32
- package/dist/tools/permissions.d.ts +4 -7
- package/dist/tools/permissions.js +11 -14
- package/dist/tools/products.js +6 -6
- package/dist/tools/quotes.js +172 -26
- package/dist/tools/recurring.js +26 -25
- package/dist/tools/register-all.d.ts +2 -2
- package/dist/tools/register-all.js +9 -6
- package/dist/tools/search.d.ts +19 -0
- package/dist/tools/search.js +84 -0
- package/dist/tools/shared.d.ts +1445 -109
- package/dist/tools/shared.js +478 -102
- package/dist/tools/team.js +24 -28
- package/dist/tools/time.js +4 -12
- package/dist/tools/vendors.js +249 -15
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +31 -5
- package/dist/workflows/daily-operational-brief.d.ts +163 -0
- package/dist/workflows/daily-operational-brief.js +881 -0
- package/docs/agent-onboarding.json +391 -0
- package/package.json +49 -12
- package/dist/tools/onboarding.d.ts +0 -19
- 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
|
|
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 {
|
|
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 {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
export const
|
|
17
|
-
export const
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
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
|
|
70
|
-
export async function
|
|
71
|
-
const server = new McpServer({
|
|
72
|
-
name: "frihet-openai-review-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
|
220
|
-
if (
|
|
221
|
-
throw new Error(`
|
|
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(`
|
|
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);
|