@agent-native/core 0.209.0-nightly-20261014153330 → 0.209.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 (44) hide show
  1. package/dist/agent/production-agent.d.ts +8 -0
  2. package/dist/agent/production-agent.js +68 -3
  3. package/dist/agent/strict-tool-input.d.ts +4 -0
  4. package/dist/agent/strict-tool-input.js +26 -0
  5. package/dist/agent/types.d.ts +7 -0
  6. package/dist/automation/index.d.ts +1 -1
  7. package/dist/cli/skills-content/turn-into-app-attachments.d.ts +1 -1
  8. package/dist/cli/skills-content/turn-into-app-attachments.js +1 -3
  9. package/dist/cli/skills-content/turn-into-app-skill.d.ts +2 -2
  10. package/dist/cli/skills-content/turn-into-app-skill.js +15 -10
  11. package/dist/client/chat/agentkit-protocol.js +2 -0
  12. package/dist/client/chat/runtime.d.ts +1 -0
  13. package/dist/client/chat/runtime.js +35 -3
  14. package/dist/collab/routes.d.ts +1 -1
  15. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  16. package/dist/observability/metrics.d.ts +2 -0
  17. package/dist/observability/metrics.js +15 -1
  18. package/dist/observability/routes.d.ts +7 -7
  19. package/dist/observability/traces.js +8 -1
  20. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  21. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  22. package/dist/resources/handlers.d.ts +1 -1
  23. package/dist/secrets/routes.d.ts +10 -10
  24. package/dist/server/agent-chat/additional-actions.d.ts +45 -0
  25. package/dist/server/agent-chat/additional-actions.js +707 -0
  26. package/dist/server/agent-chat/plugin-options.d.ts +21 -0
  27. package/dist/server/agent-chat-plugin.js +54 -13
  28. package/dist/server/better-auth-instance.js +108 -18
  29. package/dist/server/builder-browser.d.ts +4 -2
  30. package/dist/server/builder-browser.js +21 -16
  31. package/dist/server/index.d.ts +2 -1
  32. package/dist/server/index.js +1 -1
  33. package/dist/shared/data-url.d.ts +2 -0
  34. package/dist/shared/data-url.js +12 -0
  35. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/attachments.md +1 -3
  36. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -8
  37. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/source-brief.md +2 -2
  38. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  39. package/docs/content/agent-surfaces.mdx +61 -0
  40. package/docs/content/observability.mdx +2 -0
  41. package/package.json +5 -4
  42. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/attachments.md +1 -3
  43. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -8
  44. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/source-brief.md +2 -2
@@ -0,0 +1,707 @@
1
+ import Ajv from "ajv";
2
+ import Ajv2020 from "ajv/dist/2020.js";
3
+ import { registerStrictToolInput } from "../../agent/strict-tool-input.js";
4
+ import { assertRegisteredActionAccess } from "../../authorization/action-access-runtime.js";
5
+ import { normalizeMcpPrincipal } from "../../mcp-client/principal.js";
6
+ import { isMcpToolAllowedForRequest } from "../../mcp-client/visibility.js";
7
+ import { ForbiddenError } from "../../sharing/access.js";
8
+ import { getRequestOrgId, getRequestUserEmail } from "../request-context.js";
9
+ const EXTERNAL_SURFACES = ["a2a", "mcp"];
10
+ // The shape every model provider accepts for a tool name.
11
+ const ACTION_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
12
+ const MAX_TITLE_CHARS = 120;
13
+ const MAX_DESCRIPTION_CHARS = 2_000;
14
+ const MAX_SCHEMA_JSON_CHARS = 64_000;
15
+ const MAX_SCHEMA_DEPTH = 24;
16
+ // Every entry is compiled on every request, so the whole registry is bounded,
17
+ // not just each schema.
18
+ const MAX_ADDITIONAL_ACTIONS = 128;
19
+ const MAX_TOTAL_SCHEMA_JSON_CHARS = 512_000;
20
+ const MAX_REPORTED_SKIPS = 1_000;
21
+ // Validation can evaluate a shared `$ref` target once per use, so the cap is on
22
+ // subschemas after expansion, not on the schema's written size.
23
+ const MAX_SCHEMA_EXPANDED_NODES = 5_000;
24
+ const MAX_TOTAL_SCHEMA_EXPANDED_NODES = 20_000;
25
+ // Without recursion, validation evaluates each expanded subschema at most once
26
+ // per input value, so a call is refused before Ajv runs when the product of the
27
+ // two passes this. Ajv validates synchronously; nothing can interrupt it.
28
+ const MAX_VALIDATION_WORK = 20_000_000;
29
+ // Every length check scans the whole string, so a few characters cost as much
30
+ // as one value.
31
+ const INPUT_STRING_CHARS_PER_UNIT = 4;
32
+ // C0/C1 controls other than tab and newline, zero-width characters, and bidi
33
+ // overrides: invisible in logs and tool lists, but read by the model.
34
+ const HIDDEN_TEXT_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F​-‏‪-‮⁠-⁩]/g;
35
+ // Formats are annotations here: no format validators are installed.
36
+ const strictAjvOptions = {
37
+ strict: false,
38
+ allErrors: true,
39
+ coerceTypes: false,
40
+ useDefaults: false,
41
+ removeAdditional: false,
42
+ validateFormats: false,
43
+ };
44
+ const strictAjv = new Ajv(strictAjvOptions);
45
+ const strictAjv2020 = new Ajv2020(strictAjvOptions);
46
+ // A cached validator's own instance skips the meta-schemas, which cost about
47
+ // 30 ms to set up per instance; the shared instances above check each schema
48
+ // against its meta-schema instead, which retains nothing per schema.
49
+ const compileAjvOptions = {
50
+ ...strictAjvOptions,
51
+ meta: false,
52
+ validateSchema: false,
53
+ };
54
+ const validateAnyObject = strictAjv.compile({ type: "object" });
55
+ // allErrors reports every failure; the model needs the first few.
56
+ const MAX_REPORTED_INPUT_ERRORS = 10;
57
+ // Compiling is synchronous and costs about as much as the schema is large, and
58
+ // a caller's tools rarely change between runs, so validators are reused by
59
+ // checked schema text. Each is compiled in its own Ajv instance, because Ajv
60
+ // keeps every schema it compiles in instance-wide state; evicting an entry
61
+ // frees it. The cache is bounded by expanded subschemas and by entries.
62
+ const MAX_CACHED_VALIDATOR_NODES = 60_000;
63
+ const MAX_CACHED_VALIDATORS = 256;
64
+ const validatorCache = new Map();
65
+ let cachedValidatorNodes = 0;
66
+ // The keywords app schemas may use, by the shape of their value. Every other
67
+ // key in a schema is refused by name rather than ignored. Ajv's identifier scan
68
+ // walks every key it does not know, so an unknown key can hide a `$id` that
69
+ // rebases `$ref`s onto parts of the schema nobody checked. Keywords left out on
70
+ // purpose: `pattern`/`patternProperties` (regexes run synchronously),
71
+ // `uniqueItems` (quadratic in the input), `$async` (validation that returns a
72
+ // promise), identifiers and anchors (registered in Ajv's shared state), and
73
+ // the `unevaluated*` and `dependent*` keywords.
74
+ const SCHEMA_KEYWORDS = new Set([
75
+ "additionalItems",
76
+ "additionalProperties",
77
+ "contains",
78
+ "else",
79
+ "if",
80
+ "items",
81
+ "not",
82
+ "propertyNames",
83
+ "then",
84
+ ]);
85
+ const SCHEMA_LIST_KEYWORDS = new Set([
86
+ "allOf",
87
+ "anyOf",
88
+ "items",
89
+ "oneOf",
90
+ "prefixItems",
91
+ ]);
92
+ const SCHEMA_MAP_KEYWORDS = new Set([
93
+ "$defs",
94
+ "definitions",
95
+ "dependencies",
96
+ "properties",
97
+ ]);
98
+ const VALUE_KEYWORDS = new Set([
99
+ "$comment",
100
+ "$ref",
101
+ "$schema",
102
+ "const",
103
+ "contentEncoding",
104
+ "contentMediaType",
105
+ "default",
106
+ "deprecated",
107
+ "description",
108
+ "enum",
109
+ "examples",
110
+ "exclusiveMaximum",
111
+ "exclusiveMinimum",
112
+ "format",
113
+ "maxContains",
114
+ "maxItems",
115
+ "maxLength",
116
+ "maxProperties",
117
+ "maximum",
118
+ "minContains",
119
+ "minItems",
120
+ "minLength",
121
+ "minProperties",
122
+ "minimum",
123
+ "multipleOf",
124
+ "readOnly",
125
+ "required",
126
+ "title",
127
+ "type",
128
+ "writeOnly",
129
+ ]);
130
+ // Each dialect ignores the other's array keywords instead of enforcing them:
131
+ // schemas without a 2020-12 `$schema` compile as draft-07.
132
+ const DRAFT_2020_ONLY_KEYWORDS = new Set([
133
+ "maxContains",
134
+ "minContains",
135
+ "prefixItems",
136
+ ]);
137
+ const DRAFT_07_ONLY_KEYWORDS = new Set(["additionalItems"]);
138
+ const DRAFT_2020_DIALECT = "https://json-schema.org/draft/2020-12/schema";
139
+ // Insertion-ordered, so the oldest key is evicted first once the cap is hit.
140
+ const reportedSkips = new Set();
141
+ export function normalizeAdditionalActionSurfaces(surfaces) {
142
+ if (surfaces === undefined)
143
+ return [];
144
+ if (!Array.isArray(surfaces)) {
145
+ throw new TypeError("additionalActionSurfaces must be an array");
146
+ }
147
+ const normalized = new Set();
148
+ for (const surface of surfaces) {
149
+ if (!EXTERNAL_SURFACES.includes(surface)) {
150
+ throw new TypeError(`additionalActionSurfaces contains "${String(surface)}"; expected "a2a" or "mcp". Interactive chat always receives additional actions.`);
151
+ }
152
+ normalized.add(surface);
153
+ }
154
+ return [...normalized];
155
+ }
156
+ /**
157
+ * Runs the app's `resolveAdditionalActions` once for one request and returns
158
+ * the entries that may join that request's registry.
159
+ *
160
+ * Everything the app returns is untrusted: it may carry names, descriptions,
161
+ * and schemas a user registered. An entry is dropped (and reported once) when
162
+ * its name is malformed or reserved, when it is an `mcp__` name this caller
163
+ * cannot see, or when its tool definition is unusable. Each kept entry is bound
164
+ * to the caller it was resolved for and its `run` is wrapped so every call,
165
+ * from any dispatcher, rechecks that caller, enforces the entry's `access`
166
+ * policy, and validates the arguments against its schema without coercion.
167
+ */
168
+ export async function resolveAppAdditionalActions(config, details, registered) {
169
+ const { resolver } = config;
170
+ const { ownerEmail, surface } = details;
171
+ // Anonymous callers get no app actions, by the same rule as MCP tools.
172
+ if (!resolver ||
173
+ !ownerEmail ||
174
+ !normalizeMcpPrincipal({ userEmail: ownerEmail, orgId: details.orgId })) {
175
+ return {};
176
+ }
177
+ if (surface !== "chat" && !config.externalSurfaces.includes(surface)) {
178
+ return {};
179
+ }
180
+ const principal = { ownerEmail, orgId: details.orgId ?? null };
181
+ const entries = await resolver({
182
+ surface,
183
+ event: details.event,
184
+ ...principal,
185
+ });
186
+ if (!isPlainRecord(entries)) {
187
+ throw new TypeError("resolveAdditionalActions must return an object of action entries");
188
+ }
189
+ const names = Object.keys(entries);
190
+ if (names.length > MAX_ADDITIONAL_ACTIONS) {
191
+ reportSkip(names[MAX_ADDITIONAL_ACTIONS], `is the first of ${names.length - MAX_ADDITIONAL_ACTIONS} entries past the ${MAX_ADDITIONAL_ACTIONS} a resolver may return`);
192
+ }
193
+ const added = {};
194
+ const schemaBudget = {
195
+ remaining: MAX_TOTAL_SCHEMA_JSON_CHARS,
196
+ expandedRemaining: MAX_TOTAL_SCHEMA_EXPANDED_NODES,
197
+ };
198
+ for (const name of names.slice(0, MAX_ADDITIONAL_ACTIONS)) {
199
+ const entry = entries[name];
200
+ const rejection = rejectActionName(name, config, registered) ?? rejectActionEntry(entry);
201
+ if (rejection) {
202
+ reportSkip(name, rejection);
203
+ continue;
204
+ }
205
+ const prepared = prepareActionEntry(name, entry, principal, schemaBudget);
206
+ if (typeof prepared === "string") {
207
+ reportSkip(name, prepared);
208
+ continue;
209
+ }
210
+ added[name] = prepared;
211
+ }
212
+ return added;
213
+ }
214
+ function rejectActionName(name, config, registered) {
215
+ if (!ACTION_NAME_PATTERN.test(name)) {
216
+ return "is not a valid action name (letters, digits, '_' and '-', at most 64 characters)";
217
+ }
218
+ // Assigning it to a registry object sets the prototype instead of a key.
219
+ if (name === "__proto__")
220
+ return "is not a valid action name";
221
+ if (Object.hasOwn(registered, name) || config.isReservedName(name)) {
222
+ return "is a registered action name";
223
+ }
224
+ if (name.startsWith("mcp__") && !isMcpToolAllowedForRequest(name)) {
225
+ return "is an MCP tool name this caller cannot use";
226
+ }
227
+ return null;
228
+ }
229
+ function rejectActionEntry(entry) {
230
+ if (!isPlainRecord(entry))
231
+ return "is not an action entry";
232
+ if (typeof entry.run !== "function")
233
+ return "has no run function";
234
+ if (!isPlainRecord(entry.tool))
235
+ return "has no tool definition";
236
+ if (typeof entry.tool.description !== "string") {
237
+ return "has no tool description";
238
+ }
239
+ return null;
240
+ }
241
+ function prepareActionEntry(name, entry, principal, schemaBudget) {
242
+ // Read each field once: a getter could return one value to the checks and
243
+ // another to the copy the model and dispatcher use.
244
+ const { title: rawTitle, description: rawDescription, parameters: rawParameters, ...tool } = entry.tool;
245
+ if (typeof rawDescription !== "string")
246
+ return "has no tool description";
247
+ const description = cleanText(rawDescription, MAX_DESCRIPTION_CHARS);
248
+ if (!description)
249
+ return "has an empty tool description";
250
+ const title = typeof rawTitle === "string"
251
+ ? cleanText(rawTitle, MAX_TITLE_CHARS)
252
+ : undefined;
253
+ let validate = validateAnyObject;
254
+ let expandedSize = 1;
255
+ let parameters;
256
+ if (rawParameters !== undefined) {
257
+ const checked = checkParameterSchema(rawParameters, schemaBudget);
258
+ if (typeof checked === "string")
259
+ return checked;
260
+ ({ schema: parameters, expanded: expandedSize } = checked);
261
+ try {
262
+ validate = compileStrict(parameters, expandedSize);
263
+ }
264
+ catch (error) {
265
+ return `has a tool schema that does not compile: ${error instanceof Error ? error.message : String(error)}`;
266
+ }
267
+ }
268
+ const run = entry.run;
269
+ const access = entry.access;
270
+ const maxInputUnits = Math.floor(MAX_VALIDATION_WORK / expandedSize);
271
+ const inputError = (input) => {
272
+ if (!inputFitsUnits(input, maxInputUnits)) {
273
+ return `arguments are too large to validate against this tool's schema; send less data`;
274
+ }
275
+ if (validate(input))
276
+ return null;
277
+ const errors = validate.errors ?? [];
278
+ const shown = strictAjv.errorsText(errors.slice(0, MAX_REPORTED_INPUT_ERRORS), { dataVar: "input", separator: "; " });
279
+ return errors.length > MAX_REPORTED_INPUT_ERRORS
280
+ ? `${shown}; and ${errors.length - MAX_REPORTED_INPUT_ERRORS} more`
281
+ : shown;
282
+ };
283
+ const prepared = {
284
+ ...entry,
285
+ // The model sees exactly the schema that was checked and compiled.
286
+ tool: {
287
+ ...tool,
288
+ ...(parameters
289
+ ? {
290
+ parameters: parameters,
291
+ }
292
+ : {}),
293
+ ...(title ? { title } : {}),
294
+ description,
295
+ },
296
+ run: async (args, ctx) => {
297
+ const caller = assertBoundCaller(name, principal, ctx);
298
+ const input = args === undefined ? {} : args;
299
+ const error = inputError(input);
300
+ if (error) {
301
+ throw new TypeError(`Invalid arguments for ${name}: ${error}`);
302
+ }
303
+ if (access) {
304
+ await assertRegisteredActionAccess(access, input, {
305
+ ...ctx,
306
+ caller: ctx?.caller ?? "tool",
307
+ userEmail: caller.userEmail,
308
+ orgId: caller.orgId,
309
+ });
310
+ }
311
+ return run(input, ctx);
312
+ },
313
+ };
314
+ // The agent loop would otherwise repair and coerce the model's input before
315
+ // `run` sees it, so a string "3" would reach a number parameter as 3.
316
+ registerStrictToolInput(prepared, (input) => inputError(input === undefined ? {} : input));
317
+ return prepared;
318
+ }
319
+ /**
320
+ * The entry was resolved for one caller. Whatever dispatcher reaches it — the
321
+ * agent loop, tool orchestration, run-code, a sub-agent — must be running for
322
+ * that same caller, so an entry that escapes its request still cannot act for
323
+ * anyone else.
324
+ */
325
+ function assertBoundCaller(name, principal, ctx) {
326
+ const userEmail = ctx?.userEmail ?? getRequestUserEmail();
327
+ const orgId = ctx && Object.hasOwn(ctx, "orgId")
328
+ ? (ctx.orgId ?? null)
329
+ : (getRequestOrgId() ?? null);
330
+ if (!userEmail ||
331
+ userEmail.trim().toLowerCase() !==
332
+ principal.ownerEmail.trim().toLowerCase() ||
333
+ orgId !== principal.orgId ||
334
+ (name.startsWith("mcp__") && !isMcpToolAllowedForRequest(name))) {
335
+ throw new ForbiddenError(`${name} is not available to this caller.`);
336
+ }
337
+ return { userEmail, orgId };
338
+ }
339
+ /**
340
+ * Copies the app's schema into plain JSON data, checks it, and returns the copy
341
+ * with its size after `$ref` expansion. Everything after this point, the
342
+ * keyword check, Ajv, and the tool list the model sees, uses the copy, so a
343
+ * getter, prototype, or proxy in the app's object cannot show the checks one
344
+ * schema and Ajv another.
345
+ */
346
+ function checkParameterSchema(parameters, schemaBudget) {
347
+ // Both request budgets are charged for the work done, accepted or not, so
348
+ // entries that each fail late cannot each spend a full per-schema budget.
349
+ const limit = Math.min(MAX_SCHEMA_JSON_CHARS, schemaBudget.remaining);
350
+ const copied = copyJsonValue(parameters, limit);
351
+ schemaBudget.remaining -= copied.size;
352
+ if (copied.failure === "too-deep") {
353
+ return `has a tool schema nested deeper than ${MAX_SCHEMA_DEPTH} levels`;
354
+ }
355
+ if (copied.failure === "too-large") {
356
+ return limit < MAX_SCHEMA_JSON_CHARS
357
+ ? `has a tool schema that exceeds the ${MAX_TOTAL_SCHEMA_JSON_CHARS}-character budget shared by this request's schemas`
358
+ : `has a tool schema larger than ${MAX_SCHEMA_JSON_CHARS} characters`;
359
+ }
360
+ if (copied.failure) {
361
+ return `has a tool schema that is not plain JSON data (${copied.failure})`;
362
+ }
363
+ const schema = copied.value;
364
+ if (!isPlainRecord(schema))
365
+ return "has a tool schema that is not an object";
366
+ if (schema.type !== "object") {
367
+ return 'has a tool schema whose root type is not "object"';
368
+ }
369
+ if (schema.properties !== undefined && !isPlainRecord(schema.properties)) {
370
+ return "has a tool schema whose properties are not an object";
371
+ }
372
+ const walk = {
373
+ expanded: 0,
374
+ limit: Math.min(MAX_SCHEMA_EXPANDED_NODES, schemaBudget.expandedRemaining),
375
+ refStack: new Set([schema]),
376
+ draft2020: isDraft2020(schema),
377
+ };
378
+ const rejection = rejectSchemaKeywords(schema, schema, walk);
379
+ schemaBudget.expandedRemaining -= walk.expanded;
380
+ if (rejection === "too-expanded") {
381
+ return walk.limit < MAX_SCHEMA_EXPANDED_NODES
382
+ ? `has a tool schema that exceeds the ${MAX_TOTAL_SCHEMA_EXPANDED_NODES}-subschema budget shared by this request's schemas`
383
+ : `has a tool schema that expands to more than ${MAX_SCHEMA_EXPANDED_NODES} subschemas through $ref`;
384
+ }
385
+ if (rejection)
386
+ return rejection;
387
+ return { schema, expanded: walk.expanded };
388
+ }
389
+ /**
390
+ * Copies own data properties of plain objects and dense arrays, approximating
391
+ * the JSON length as it goes and stopping as soon as it passes `limit` or
392
+ * `MAX_SCHEMA_DEPTH`, so an oversized or cyclic value costs no more than the
393
+ * budget it is rejected for.
394
+ */
395
+ function copyJsonValue(value, limit) {
396
+ let size = 0;
397
+ let failure = null;
398
+ const fail = (reason) => {
399
+ failure = reason;
400
+ return undefined;
401
+ };
402
+ const copy = (node, depth) => {
403
+ if (depth > MAX_SCHEMA_DEPTH)
404
+ return fail("too-deep");
405
+ if (node === null || typeof node === "boolean") {
406
+ size += 5;
407
+ return node;
408
+ }
409
+ if (typeof node === "number") {
410
+ if (!Number.isFinite(node))
411
+ return fail("a value that is not JSON");
412
+ size += 24;
413
+ return node;
414
+ }
415
+ if (typeof node === "string") {
416
+ size += node.length + 2;
417
+ return size > limit ? fail("too-large") : node;
418
+ }
419
+ if (typeof node !== "object")
420
+ return fail("a value that is not JSON");
421
+ const prototype = Object.getPrototypeOf(node);
422
+ size += 2;
423
+ if (Array.isArray(node)) {
424
+ if (prototype !== Array.prototype) {
425
+ return fail("an object with a custom prototype");
426
+ }
427
+ const out = [];
428
+ for (let index = 0; index < node.length; index += 1) {
429
+ size += 1;
430
+ if (size > limit)
431
+ return fail("too-large");
432
+ const descriptor = Object.getOwnPropertyDescriptor(node, index);
433
+ if (!descriptor)
434
+ return fail("an array with holes");
435
+ if (!("value" in descriptor))
436
+ return fail("a getter or setter");
437
+ out.push(copy(descriptor.value, depth + 1));
438
+ if (failure)
439
+ return undefined;
440
+ }
441
+ return out;
442
+ }
443
+ if (prototype !== Object.prototype && prototype !== null) {
444
+ return fail("an object with a custom prototype");
445
+ }
446
+ const out = {};
447
+ for (const key in node) {
448
+ if (!Object.hasOwn(node, key))
449
+ continue;
450
+ if (key === "__proto__")
451
+ return fail("a __proto__ key");
452
+ const descriptor = Object.getOwnPropertyDescriptor(node, key);
453
+ if (!("value" in descriptor))
454
+ return fail("a getter or setter");
455
+ if (descriptor.value === undefined)
456
+ continue;
457
+ size += key.length + 4;
458
+ if (size > limit)
459
+ return fail("too-large");
460
+ out[key] = copy(descriptor.value, depth + 1);
461
+ if (failure)
462
+ return undefined;
463
+ }
464
+ return out;
465
+ };
466
+ const copied = copy(value, 0);
467
+ return failure ? { size, failure } : { value: copied, size, failure: null };
468
+ }
469
+ /**
470
+ * Walks every location Ajv evaluates as a schema, refusing keys outside the
471
+ * allowlist and following each local `$ref`.
472
+ *
473
+ * Each `$ref` is expanded where it is used, as validation evaluates it, so a
474
+ * small schema that references the same definition several times per level
475
+ * shows its exponential validation cost here and is rejected. A `$ref` back
476
+ * into a schema it is already inside is refused outright: even when each
477
+ * level steps into a property or item, a recursive schema with more than one
478
+ * reference per level costs validation exponential work in the input's
479
+ * nesting depth, which the expansion count cannot see.
480
+ */
481
+ function rejectSchemaKeywords(schema, root, walk) {
482
+ // Boolean subschemas are compiled and evaluated too, so they count.
483
+ walk.expanded += 1;
484
+ if (walk.expanded > walk.limit)
485
+ return "too-expanded";
486
+ if (typeof schema === "boolean")
487
+ return null;
488
+ if (!isPlainRecord(schema)) {
489
+ return "has a tool schema with a subschema that is not an object or boolean";
490
+ }
491
+ for (const [keyword, value] of Object.entries(schema)) {
492
+ let rejection = null;
493
+ if (walk.draft2020
494
+ ? DRAFT_07_ONLY_KEYWORDS.has(keyword)
495
+ : DRAFT_2020_ONLY_KEYWORDS.has(keyword)) {
496
+ rejection = walk.draft2020
497
+ ? `has a tool schema that uses "${keyword}", which draft 2020-12 does not enforce`
498
+ : `has a tool schema that uses "${keyword}" without "$schema": "${DRAFT_2020_DIALECT}", so it would not be enforced`;
499
+ }
500
+ else if (keyword === "items" && Array.isArray(value)) {
501
+ rejection = rejectEach(value, root, walk);
502
+ }
503
+ else if (SCHEMA_KEYWORDS.has(keyword)) {
504
+ rejection = rejectSchemaKeywords(value, root, walk);
505
+ }
506
+ else if (SCHEMA_LIST_KEYWORDS.has(keyword)) {
507
+ rejection = Array.isArray(value)
508
+ ? rejectEach(value, root, walk)
509
+ : `has a tool schema whose "${keyword}" is not an array`;
510
+ }
511
+ else if (SCHEMA_MAP_KEYWORDS.has(keyword)) {
512
+ if (!isPlainRecord(value)) {
513
+ rejection = `has a tool schema whose "${keyword}" is not an object`;
514
+ }
515
+ else {
516
+ // `dependencies` maps a property to a schema or to required names,
517
+ // which are counted like `required`.
518
+ const children = [];
519
+ for (const child of Object.values(value)) {
520
+ if (keyword === "dependencies" && Array.isArray(child)) {
521
+ walk.expanded += countJsonNodes(child);
522
+ }
523
+ else {
524
+ children.push(child);
525
+ }
526
+ }
527
+ rejection =
528
+ walk.expanded > walk.limit
529
+ ? "too-expanded"
530
+ : rejectEach(children, root, walk);
531
+ }
532
+ }
533
+ else if (keyword === "$ref") {
534
+ rejection = rejectRef(value, root, walk);
535
+ }
536
+ else if (keyword === "$schema" && schema !== root) {
537
+ rejection = 'has a tool schema that sets "$schema" below its root';
538
+ }
539
+ else if (keyword === "enum" ||
540
+ keyword === "const" ||
541
+ keyword === "required") {
542
+ // Each listed value is compared against, or looked up in, the input.
543
+ walk.expanded += countJsonNodes(value);
544
+ if (walk.expanded > walk.limit)
545
+ return "too-expanded";
546
+ }
547
+ else if (!VALUE_KEYWORDS.has(keyword)) {
548
+ rejection = `has a tool schema that uses "${keyword.slice(0, 80)}", which app actions may not use`;
549
+ }
550
+ if (rejection)
551
+ return rejection;
552
+ }
553
+ return null;
554
+ }
555
+ function rejectEach(schemas, root, walk) {
556
+ for (const child of schemas) {
557
+ const rejection = rejectSchemaKeywords(child, root, walk);
558
+ if (rejection)
559
+ return rejection;
560
+ }
561
+ return null;
562
+ }
563
+ function rejectRef(ref, root, walk) {
564
+ const target = resolveLocalRef(root, ref);
565
+ if (target === undefined) {
566
+ return 'has a tool schema with a $ref that is not "#" or an unencoded JSON pointer inside the schema';
567
+ }
568
+ // Only objects can recurse, but every target, boolean ones included, is
569
+ // evaluated where the $ref is used and counts toward the budget.
570
+ const recursive = isPlainRecord(target);
571
+ if (recursive && walk.refStack.has(target)) {
572
+ return "has a tool schema with a recursive $ref";
573
+ }
574
+ if (recursive)
575
+ walk.refStack.add(target);
576
+ const rejection = rejectSchemaKeywords(target, root, walk);
577
+ if (recursive)
578
+ walk.refStack.delete(target);
579
+ return rejection;
580
+ }
581
+ function countJsonNodes(value) {
582
+ if (!value || typeof value !== "object")
583
+ return 1;
584
+ let count = 1;
585
+ for (const child of Object.values(value))
586
+ count += countJsonNodes(child);
587
+ return count;
588
+ }
589
+ /**
590
+ * Ajv percent-decodes the whole fragment before splitting it into pointer
591
+ * segments, so an encoded `/` addresses a different location than a
592
+ * segment-by-segment reading would. Encoded refs are refused rather than
593
+ * reimplementing that decoding.
594
+ */
595
+ function resolveLocalRef(root, ref) {
596
+ if (ref === "#")
597
+ return root;
598
+ if (typeof ref !== "string" || !ref.startsWith("#/") || ref.includes("%")) {
599
+ return undefined;
600
+ }
601
+ let target = root;
602
+ for (const raw of ref.slice(2).split("/")) {
603
+ const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
604
+ if (!target ||
605
+ typeof target !== "object" ||
606
+ !Object.hasOwn(target, segment)) {
607
+ return undefined;
608
+ }
609
+ target = target[segment];
610
+ }
611
+ return target;
612
+ }
613
+ /**
614
+ * Counts the input's values and object keys (long strings count once per
615
+ * `INPUT_STRING_CHARS_PER_UNIT` characters; `propertyNames` checks keys like
616
+ * strings) and stops at `maxUnits`. The walk is iterative because the model
617
+ * controls how deeply its arguments nest.
618
+ */
619
+ function inputFitsUnits(input, maxUnits) {
620
+ let units = 0;
621
+ const pending = [input];
622
+ while (pending.length > 0) {
623
+ const node = pending.pop();
624
+ units +=
625
+ typeof node === "string"
626
+ ? 1 + Math.floor(node.length / INPUT_STRING_CHARS_PER_UNIT)
627
+ : 1;
628
+ if (units > maxUnits)
629
+ return false;
630
+ // Every queued node costs at least one unit, so stop queuing as soon as
631
+ // the queue alone is over budget rather than after copying a huge node.
632
+ if (Array.isArray(node)) {
633
+ for (let i = 0; i < node.length; i += 1) {
634
+ pending.push(node[i]);
635
+ if (units + pending.length > maxUnits)
636
+ return false;
637
+ }
638
+ }
639
+ else if (node && typeof node === "object") {
640
+ for (const key in node) {
641
+ if (!Object.hasOwn(node, key))
642
+ continue;
643
+ pending.push(key, node[key]);
644
+ if (units + pending.length > maxUnits)
645
+ return false;
646
+ }
647
+ }
648
+ }
649
+ return true;
650
+ }
651
+ function compileStrict(schema, expanded) {
652
+ const is2020 = isDraft2020(schema);
653
+ const key = `${is2020 ? "2020" : "7"}${JSON.stringify(schema)}`;
654
+ const cached = validatorCache.get(key);
655
+ if (cached) {
656
+ validatorCache.delete(key);
657
+ validatorCache.set(key, cached);
658
+ return cached.validate;
659
+ }
660
+ const metaAjv = is2020 ? strictAjv2020 : strictAjv;
661
+ if (!metaAjv.validateSchema(schema)) {
662
+ throw new Error(`schema is invalid: ${metaAjv.errorsText(metaAjv.errors)}`);
663
+ }
664
+ const ajv = is2020
665
+ ? new Ajv2020(compileAjvOptions)
666
+ : new Ajv(compileAjvOptions);
667
+ const validate = ajv.compile(schema);
668
+ if (validate.$async) {
669
+ throw new Error("the schema validates asynchronously");
670
+ }
671
+ validatorCache.set(key, { validate, expanded });
672
+ cachedValidatorNodes += expanded;
673
+ for (const [oldest, entry] of validatorCache) {
674
+ if (cachedValidatorNodes <= MAX_CACHED_VALIDATOR_NODES &&
675
+ validatorCache.size <= MAX_CACHED_VALIDATORS) {
676
+ break;
677
+ }
678
+ validatorCache.delete(oldest);
679
+ cachedValidatorNodes -= entry.expanded;
680
+ }
681
+ return validate;
682
+ }
683
+ function isDraft2020(schema) {
684
+ const declared = schema.$schema;
685
+ return (typeof declared === "string" &&
686
+ declared.replace(/#$/, "") === DRAFT_2020_DIALECT);
687
+ }
688
+ function cleanText(value, maxChars) {
689
+ const cleaned = value.replace(HIDDEN_TEXT_PATTERN, "").trim();
690
+ return cleaned.length > maxChars
691
+ ? `${cleaned.slice(0, maxChars - 1)}…`
692
+ : cleaned;
693
+ }
694
+ function isPlainRecord(value) {
695
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
696
+ }
697
+ function reportSkip(name, reason) {
698
+ const shown = JSON.stringify(name.slice(0, 80));
699
+ const key = `${shown}\u0000${reason.slice(0, 200)}`;
700
+ if (reportedSkips.has(key))
701
+ return;
702
+ if (reportedSkips.size >= MAX_REPORTED_SKIPS) {
703
+ reportedSkips.delete(reportedSkips.values().next().value);
704
+ }
705
+ reportedSkips.add(key);
706
+ console.warn(`[agent-chat] resolveAdditionalActions returned ${shown}, which ${reason}; ignoring the app entry.`);
707
+ }