@m6d/cortex-cli 0.14.0 → 0.16.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 +43 -19
- package/package.json +3 -13
- package/src/commands/apply.ts +171 -38
- package/src/commands/management.ts +73 -20
- package/src/commands/new.ts +8 -10
- package/src/commands/setup.ts +77 -37
- package/src/contracts/datasets/dataset-sql.ts +58 -7
- package/src/contracts/datasets/datasets.ts +220 -195
- package/src/contracts/egress-policy/scrub.ts +22 -10
- package/src/contracts/errors/index.ts +7 -0
- package/src/contracts/json/index.ts +45 -1
- package/src/contracts/manifest/index.ts +506 -134
- package/src/contracts/server/index.ts +45 -56
- package/src/contracts/triggers/index.ts +13 -9
- package/src/contracts/wire/index.ts +153 -18
- package/src/management/client.ts +79 -117
- package/src/management/profiles.ts +11 -22
- package/src/scaffold/files.ts +11 -9
- package/src/ui/prompts.ts +38 -8
- package/src/contracts/charts/chart-spec.ts +0 -217
- package/src/contracts/charts/index.ts +0 -8
- package/src/contracts/datasets/datasets.fixtures.json +0 -2849
- package/src/contracts/markdown/index.ts +0 -47
- package/src/contracts/pattern/index.ts +0 -276
- package/src/contracts/server/reserved-names.fixtures.json +0 -18
|
@@ -4,7 +4,7 @@ import { canCarrySecret } from "../egress-policy/index";
|
|
|
4
4
|
import {
|
|
5
5
|
agentSlugSchema,
|
|
6
6
|
EVENT_HEADER,
|
|
7
|
-
|
|
7
|
+
identifierSchema,
|
|
8
8
|
localeSchema,
|
|
9
9
|
SERVER_EVENTS,
|
|
10
10
|
refuseBlockedApprovalTitle,
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from "../server/index";
|
|
13
13
|
import { MESSAGE_TRIGGER_NAME } from "../triggers/index";
|
|
14
14
|
import {
|
|
15
|
+
BASE_LOCALE,
|
|
16
|
+
byLocale,
|
|
15
17
|
DEFAULT_MEDIA_FORMS,
|
|
16
18
|
MEDIA_FORMS,
|
|
17
19
|
TOOL_SURFACES,
|
|
@@ -46,14 +48,23 @@ export const TOOL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
|
|
|
46
48
|
*/
|
|
47
49
|
export const RESPONSE_WRAPS = ["items", "value"] as const;
|
|
48
50
|
|
|
51
|
+
/** How OpenAPI serializes a query parameter: `form` (the default), `spaceDelimited`, `pipeDelimited` or `deepObject`. */
|
|
52
|
+
export const QUERY_STYLES = ["form", "spaceDelimited", "pipeDelimited", "deepObject"] as const;
|
|
53
|
+
/** How OpenAPI serializes a path parameter: `simple` (the default), `label` or `matrix`. */
|
|
54
|
+
export const PATH_STYLES = ["simple", "label", "matrix"] as const;
|
|
55
|
+
|
|
49
56
|
export const SOAP_VERSIONS = ["1.1", "1.2"] as const;
|
|
50
57
|
|
|
58
|
+
/** An RFC 6901 pointer into a JSON document, naming a member: `/extra`, `/data/items`. */
|
|
59
|
+
const jsonPointerSchema = z.string().regex(/^\//).max(256);
|
|
60
|
+
|
|
51
61
|
type Bounds = { min: number; max: number; default: number };
|
|
52
62
|
|
|
53
63
|
/**
|
|
54
64
|
* The range of every number an author sets, in the console and in a
|
|
55
|
-
* manifest alike, and the value a manifest that leaves one out gets
|
|
56
|
-
* console builds its form schemas, with
|
|
65
|
+
* manifest alike, and the value a manifest that leaves one out gets, which
|
|
66
|
+
* a new row's column holds too. The console builds its form schemas, with
|
|
67
|
+
* its own messages, from these.
|
|
57
68
|
*/
|
|
58
69
|
export const SETTING_BOUNDS = {
|
|
59
70
|
tool: {
|
|
@@ -91,9 +102,36 @@ function setting({ min, max, default: fallback }: Bounds) {
|
|
|
91
102
|
return z.number().int().min(min).max(max).default(fallback);
|
|
92
103
|
}
|
|
93
104
|
|
|
105
|
+
/** One number per setting a group of `SETTING_BOUNDS` names. */
|
|
106
|
+
export type SettingValues<TBounds> = { [Name in keyof TBounds]: number };
|
|
107
|
+
|
|
108
|
+
/** Every setting of a group at its default, as a row that sets none stores them. */
|
|
109
|
+
export function settingDefaults<TBounds extends Record<string, Bounds>>(bounds: TBounds) {
|
|
110
|
+
return Object.fromEntries(
|
|
111
|
+
Object.entries(bounds).map(([name, bound]) => [name, bound.default]),
|
|
112
|
+
) as SettingValues<TBounds>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The fields of an entry users may change in Cortex after an apply, each
|
|
117
|
+
* once, from the ones `fields` allows; none when left out. Every other field
|
|
118
|
+
* is the manifest's alone. An apply keeps a live change to a listed field
|
|
119
|
+
* unless the manifest changed that field too (a conflict, see
|
|
120
|
+
* `manifestApplyInputSchema`). Identity, what is fixed at creation, the
|
|
121
|
+
* audience, credentials, and whatever an apply checks against the
|
|
122
|
+
* manifest's own value are never offered.
|
|
123
|
+
*/
|
|
124
|
+
function overridable<const TField extends string>(fields: readonly [TField, ...TField[]]) {
|
|
125
|
+
return z
|
|
126
|
+
.array(z.enum(fields))
|
|
127
|
+
.refine((listed) => new Set(listed).size === listed.length, "a field repeats")
|
|
128
|
+
.default([]);
|
|
129
|
+
}
|
|
130
|
+
|
|
94
131
|
/** Consumer-facing slugs: the manifest id, service keys, skill names and trigger names share it. */
|
|
95
132
|
export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
96
|
-
const
|
|
133
|
+
export const SLUG_MAX_LENGTH = 64;
|
|
134
|
+
const slugSchema = z.string().regex(SLUG_PATTERN).max(SLUG_MAX_LENGTH);
|
|
97
135
|
|
|
98
136
|
/** A DNS host name in lower case, the way `URL` reports one: dot-separated labels of letters, digits and inner hyphens. */
|
|
99
137
|
export const HOSTNAME_PATTERN =
|
|
@@ -175,6 +213,16 @@ export const soapActionSchema = z.string().regex(/^[^"\\\u0000-\u001f\u007f]*$/)
|
|
|
175
213
|
/** An envelope template: XML, so never only whitespace. */
|
|
176
214
|
const envelopeTemplateSchema = z.string().refine((template) => template.trim().length > 0);
|
|
177
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The media types a request body may be sent in besides JSON, the default.
|
|
218
|
+
* Each body field is written as its target says.
|
|
219
|
+
*/
|
|
220
|
+
export const FORM_MEDIA_TYPES = [
|
|
221
|
+
"application/x-www-form-urlencoded",
|
|
222
|
+
"multipart/form-data",
|
|
223
|
+
] as const;
|
|
224
|
+
export type FormMediaType = (typeof FORM_MEDIA_TYPES)[number];
|
|
225
|
+
|
|
178
226
|
/**
|
|
179
227
|
* The per-type payload a tool stores and publishes. Soap: the envelope
|
|
180
228
|
* template is the full request XML with `{{field}}` placeholders bound to
|
|
@@ -182,7 +230,12 @@ const envelopeTemplateSchema = z.string().refine((template) => template.trim().l
|
|
|
182
230
|
* acquires once per request.
|
|
183
231
|
*/
|
|
184
232
|
export const toolConfigByType = {
|
|
185
|
-
http: z.object({
|
|
233
|
+
http: z.object({
|
|
234
|
+
responseWrap: z.enum(RESPONSE_WRAPS).optional(),
|
|
235
|
+
requestMediaType: z.enum(FORM_MEDIA_TYPES).optional(),
|
|
236
|
+
/** Where a successful response keeps the result, for an API that wraps every answer (`{ success, extra }`); read before `responseWrap`. */
|
|
237
|
+
resultPath: jsonPointerSchema.optional(),
|
|
238
|
+
}),
|
|
186
239
|
embedded: z.object({ embedOrigin: embedOriginSchema, surface: z.enum(TOOL_SURFACES) }),
|
|
187
240
|
soap: z.object({
|
|
188
241
|
soapVersion: z.enum(SOAP_VERSIONS),
|
|
@@ -201,6 +254,92 @@ export const toolConfigByType = {
|
|
|
201
254
|
client: z.object({}),
|
|
202
255
|
};
|
|
203
256
|
|
|
257
|
+
/** Every tool type, in the order the database enum lists them; see `TOOL_TYPES`. */
|
|
258
|
+
export const TOOL_TYPE_NAMES = ["http", "embedded", "soap", "native", "client"] as const;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* What each tool type is, so no reader restates it: who answers its calls
|
|
262
|
+
* (`kind`: Cortex for a server tool, the browser for a client one), whether
|
|
263
|
+
* it calls an endpoint, whether a call may wait for the end user's
|
|
264
|
+
* approval, whether an input field may be filled from a source Cortex holds
|
|
265
|
+
* (`x-source`) or name its place in the request (`x-target`), whether an
|
|
266
|
+
* author creates one, and the `method` or `readOnly` the type pins whatever
|
|
267
|
+
* the author says. An embedded tool's initiate is never read-only, so a
|
|
268
|
+
* widget reload rejoins its session, and asks no approval, since the widget
|
|
269
|
+
* answers it. SOAP and native tools are POSTed, and a native tool comes from
|
|
270
|
+
* its server's manifest alone, with no field Cortex fills or places. A
|
|
271
|
+
* client tool calls nothing: the client answers it.
|
|
272
|
+
*/
|
|
273
|
+
export const TOOL_TYPES = {
|
|
274
|
+
http: {
|
|
275
|
+
kind: "server",
|
|
276
|
+
calls: true,
|
|
277
|
+
approval: true,
|
|
278
|
+
sources: true,
|
|
279
|
+
targets: true,
|
|
280
|
+
creatable: true,
|
|
281
|
+
},
|
|
282
|
+
embedded: {
|
|
283
|
+
kind: "client",
|
|
284
|
+
calls: true,
|
|
285
|
+
approval: false,
|
|
286
|
+
sources: true,
|
|
287
|
+
targets: false,
|
|
288
|
+
creatable: true,
|
|
289
|
+
readOnly: false,
|
|
290
|
+
},
|
|
291
|
+
soap: {
|
|
292
|
+
kind: "server",
|
|
293
|
+
calls: true,
|
|
294
|
+
approval: true,
|
|
295
|
+
sources: true,
|
|
296
|
+
targets: true,
|
|
297
|
+
creatable: true,
|
|
298
|
+
method: "POST",
|
|
299
|
+
},
|
|
300
|
+
native: {
|
|
301
|
+
kind: "server",
|
|
302
|
+
calls: true,
|
|
303
|
+
approval: true,
|
|
304
|
+
sources: false,
|
|
305
|
+
targets: false,
|
|
306
|
+
creatable: false,
|
|
307
|
+
method: "POST",
|
|
308
|
+
},
|
|
309
|
+
client: {
|
|
310
|
+
kind: "client",
|
|
311
|
+
calls: false,
|
|
312
|
+
approval: false,
|
|
313
|
+
sources: false,
|
|
314
|
+
targets: false,
|
|
315
|
+
creatable: true,
|
|
316
|
+
readOnly: true,
|
|
317
|
+
},
|
|
318
|
+
} as const satisfies Record<
|
|
319
|
+
(typeof TOOL_TYPE_NAMES)[number],
|
|
320
|
+
{
|
|
321
|
+
kind: "server" | "client";
|
|
322
|
+
calls: boolean;
|
|
323
|
+
approval: boolean;
|
|
324
|
+
sources: boolean;
|
|
325
|
+
targets: boolean;
|
|
326
|
+
creatable: boolean;
|
|
327
|
+
method?: (typeof TOOL_METHODS)[number];
|
|
328
|
+
readOnly?: boolean;
|
|
329
|
+
}
|
|
330
|
+
>;
|
|
331
|
+
|
|
332
|
+
type CreatableToolType = {
|
|
333
|
+
[TType in keyof typeof TOOL_TYPES]: (typeof TOOL_TYPES)[TType]["creatable"] extends true
|
|
334
|
+
? TType
|
|
335
|
+
: never;
|
|
336
|
+
}[keyof typeof TOOL_TYPES];
|
|
337
|
+
|
|
338
|
+
/** The types an author creates a tool as, in `TOOL_TYPE_NAMES` order; a native tool comes from its server's manifest. */
|
|
339
|
+
export const CREATABLE_TOOL_TYPES = TOOL_TYPE_NAMES.filter(
|
|
340
|
+
(type): type is CreatableToolType => TOOL_TYPES[type].creatable,
|
|
341
|
+
);
|
|
342
|
+
|
|
204
343
|
/** A `{name}` parameter in a tool's endpoint URL, filled from the input on each call. */
|
|
205
344
|
export const ENDPOINT_PARAM_PATTERN = /\{([^{}]+)\}/g;
|
|
206
345
|
|
|
@@ -219,9 +358,21 @@ export function confinesEndpointParams(url: string) {
|
|
|
219
358
|
/** An upstream Cortex calls: http or https. */
|
|
220
359
|
const upstreamUrlSchema = z.url({ protocol: /^https?$/ });
|
|
221
360
|
|
|
361
|
+
/**
|
|
362
|
+
* What a call presents: a header name and the prefix before the value in
|
|
363
|
+
* characters, and how many headers at most (a real upstream wants one or
|
|
364
|
+
* two). The console caps its credential rows at the same.
|
|
365
|
+
*/
|
|
366
|
+
export const CREDENTIAL_LIMITS = { headerLength: 120, prefixLength: 40, presentations: 8 } as const;
|
|
367
|
+
|
|
222
368
|
/** An HTTP header field name (RFC 9110 token). */
|
|
223
369
|
export const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
224
|
-
const headerNameSchema = z
|
|
370
|
+
const headerNameSchema = z
|
|
371
|
+
.string()
|
|
372
|
+
.trim()
|
|
373
|
+
.min(1)
|
|
374
|
+
.max(CREDENTIAL_LIMITS.headerLength)
|
|
375
|
+
.regex(HEADER_NAME_PATTERN);
|
|
225
376
|
|
|
226
377
|
/** Headers Cortex or fetch fills on every request. Lower-cased. */
|
|
227
378
|
export const transportSetHeaders = ["accept", "content-type", "content-length", "host"] as const;
|
|
@@ -250,11 +401,8 @@ export const transportOwnedHeaders = new Set<string>([
|
|
|
250
401
|
...transportForbiddenHeaders,
|
|
251
402
|
]);
|
|
252
403
|
|
|
253
|
-
/** At most this many headers a call presents; a real upstream wants one or two. */
|
|
254
|
-
export const CREDENTIAL_PRESENTATION_LIMIT = 8;
|
|
255
|
-
|
|
256
404
|
/** The index of each entry presenting a header an earlier entry already presents, compared case-insensitively. */
|
|
257
|
-
|
|
405
|
+
function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
|
|
258
406
|
const seen = new Set<string>();
|
|
259
407
|
return entries.flatMap((entry, index) => {
|
|
260
408
|
const name = entry.header.toLowerCase();
|
|
@@ -264,8 +412,10 @@ export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
|
|
|
264
412
|
});
|
|
265
413
|
}
|
|
266
414
|
|
|
415
|
+
/** The longest name Cortex holds a server, model provider, credential or secret under. */
|
|
416
|
+
export const NAME_MAX_LENGTH = 120;
|
|
267
417
|
/** A server, model provider, credential or secret, by the name Cortex holds it under. */
|
|
268
|
-
const nameSchema = z.string().trim().min(1).max(
|
|
418
|
+
const nameSchema = z.string().trim().min(1).max(NAME_MAX_LENGTH);
|
|
269
419
|
|
|
270
420
|
const presentedHeaderSchema = headerNameSchema.refine(
|
|
271
421
|
(name) => !transportOwnedHeaders.has(name.toLowerCase()),
|
|
@@ -274,30 +424,45 @@ const presentedHeaderSchema = headerNameSchema.refine(
|
|
|
274
424
|
|
|
275
425
|
/**
|
|
276
426
|
* What every call carries: each entry a header, what precedes the value in
|
|
277
|
-
* it (`"Bearer "`, say), and the credential by name
|
|
278
|
-
*
|
|
427
|
+
* it (`"Bearer "`, say), and the credential by name, as `entry` validates
|
|
428
|
+
* it, at most `CREDENTIAL_LIMITS.presentations` of them. A header is
|
|
429
|
+
* presented once; `repeated` words the refusal of a later one.
|
|
279
430
|
*/
|
|
280
|
-
function presentationsSchema<
|
|
281
|
-
|
|
282
|
-
|
|
431
|
+
export function presentationsSchema<TEntry extends z.ZodType<{ header: string }>>(
|
|
432
|
+
entry: TEntry,
|
|
433
|
+
repeated: () => string,
|
|
283
434
|
) {
|
|
284
435
|
return z
|
|
285
|
-
.array(
|
|
286
|
-
.max(
|
|
436
|
+
.array(entry)
|
|
437
|
+
.max(CREDENTIAL_LIMITS.presentations)
|
|
287
438
|
.superRefine((entries, context) => {
|
|
288
439
|
for (const index of repeatedHeaders(entries)) {
|
|
289
440
|
context.addIssue({
|
|
290
441
|
code: "custom",
|
|
291
442
|
path: [index, "header"],
|
|
292
|
-
message:
|
|
443
|
+
message: repeated(),
|
|
293
444
|
});
|
|
294
445
|
}
|
|
295
|
-
})
|
|
296
|
-
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The presentations a manifest entry declares: no unknown key, an omitted prefix or list empty. */
|
|
450
|
+
function declaredPresentationsSchema<TCredential extends string | null>(
|
|
451
|
+
header: z.ZodString,
|
|
452
|
+
credential: z.ZodType<TCredential, TCredential>,
|
|
453
|
+
) {
|
|
454
|
+
return presentationsSchema(
|
|
455
|
+
z.strictObject({
|
|
456
|
+
header,
|
|
457
|
+
prefix: z.string().max(CREDENTIAL_LIMITS.prefixLength).default(""),
|
|
458
|
+
credential,
|
|
459
|
+
}),
|
|
460
|
+
() => "a header presented twice",
|
|
461
|
+
).default([]);
|
|
297
462
|
}
|
|
298
463
|
|
|
299
464
|
/** A credential, or `null` for the run's own token (the end user's in a chat, the trigger's in a firing). */
|
|
300
|
-
const credentialsSchema =
|
|
465
|
+
const credentialsSchema = declaredPresentationsSchema(presentedHeaderSchema, nameSchema.nullable());
|
|
301
466
|
|
|
302
467
|
/**
|
|
303
468
|
* Where Cortex fills a top-level scalar input field from, out of the model's
|
|
@@ -306,7 +471,7 @@ const credentialsSchema = presentationsSchema(presentedHeaderSchema, nameSchema.
|
|
|
306
471
|
*/
|
|
307
472
|
export const fieldSourceSchema = z.discriminatedUnion("kind", [
|
|
308
473
|
z.object({ kind: z.literal("variable"), name: z.string().trim().min(1).max(64) }),
|
|
309
|
-
z.object({ kind: z.literal("secret"), name:
|
|
474
|
+
z.object({ kind: z.literal("secret"), name: nameSchema }),
|
|
310
475
|
z.object({
|
|
311
476
|
kind: z.literal("constant"),
|
|
312
477
|
value: z.union([z.string().max(4_000), z.number(), z.boolean()]),
|
|
@@ -321,7 +486,7 @@ export type FieldSource = z.infer<typeof fieldSourceSchema>;
|
|
|
321
486
|
* string only, and a constant must be a value of the field's type. Whether a
|
|
322
487
|
* variable is declared is the agent's server's to say.
|
|
323
488
|
*/
|
|
324
|
-
|
|
489
|
+
function sourceFits(
|
|
325
490
|
source: FieldSource,
|
|
326
491
|
field: { type: unknown; enum?: ReadonlyArray<string | null> },
|
|
327
492
|
) {
|
|
@@ -343,30 +508,67 @@ export function sourceFits(
|
|
|
343
508
|
}
|
|
344
509
|
|
|
345
510
|
/**
|
|
346
|
-
* Where a top-level
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
511
|
+
* Where a top-level input field travels in the upstream request; absent,
|
|
512
|
+
* path placeholders by name, then the query string on GET and the body
|
|
513
|
+
* otherwise. `name` renames a query parameter, header or cookie on the wire,
|
|
514
|
+
* and is the name `matrix` style writes into a path.
|
|
515
|
+
* `style` and `explode` write an array or object value as an OpenAPI
|
|
516
|
+
* parameter declares; left out, OpenAPI's defaults hold (`form` exploded for
|
|
517
|
+
* a query, `simple` unexploded for a path and a header). A cookie joins items
|
|
518
|
+
* with commas. In a form body (the tool's `requestMediaType`), a JSON
|
|
519
|
+
* `contentType` sends the value as JSON text, and `style` and `explode`
|
|
520
|
+
* write it as in a query; a multipart body repeats an array's items as
|
|
521
|
+
* parts, typing an object JSON unless `contentType` says text. A JSON body
|
|
522
|
+
* reads none of them.
|
|
350
523
|
*/
|
|
351
524
|
export const fieldTargetSchema = z.discriminatedUnion("kind", [
|
|
352
|
-
z.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
525
|
+
z.strictObject({
|
|
526
|
+
kind: z.literal("body"),
|
|
527
|
+
contentType: z.enum(["application/json", "text/plain"]).optional(),
|
|
528
|
+
style: z.enum(QUERY_STYLES).optional(),
|
|
529
|
+
explode: z.boolean().optional(),
|
|
530
|
+
}),
|
|
531
|
+
z.strictObject({
|
|
532
|
+
kind: z.literal("query"),
|
|
533
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
534
|
+
style: z.enum(QUERY_STYLES).optional(),
|
|
535
|
+
explode: z.boolean().optional(),
|
|
536
|
+
}),
|
|
537
|
+
z.strictObject({
|
|
538
|
+
kind: z.literal("path"),
|
|
539
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
540
|
+
style: z.enum(PATH_STYLES).optional(),
|
|
541
|
+
explode: z.boolean().optional(),
|
|
542
|
+
}),
|
|
543
|
+
z.strictObject({
|
|
544
|
+
kind: z.literal("header"),
|
|
545
|
+
name: headerNameSchema.optional(),
|
|
546
|
+
explode: z.boolean().optional(),
|
|
547
|
+
}),
|
|
548
|
+
z.strictObject({ kind: z.literal("cookie"), name: headerNameSchema.optional() }),
|
|
357
549
|
]);
|
|
358
550
|
export type FieldTarget = z.infer<typeof fieldTargetSchema>;
|
|
359
551
|
|
|
360
|
-
|
|
552
|
+
/** The types a field holds one value of. */
|
|
553
|
+
export const SCALAR_TYPES = ["string", "number", "integer", "boolean"] as const;
|
|
361
554
|
const schemaTypeSchema = z.enum([...SCALAR_TYPES, "object", "array"]);
|
|
362
555
|
|
|
556
|
+
/** A field's name and description in characters, and the values its enum lists. The console caps its field rows at the same. */
|
|
557
|
+
export const FIELD_LIMITS = { nameLength: 120, descriptionLength: 500, enumValues: 50 } as const;
|
|
558
|
+
|
|
363
559
|
/** Untrimmed: two names that differ only in surrounding spaces would collapse into one. */
|
|
364
560
|
const propertyNameSchema = z
|
|
365
561
|
.string()
|
|
366
562
|
.min(1)
|
|
367
|
-
.max(
|
|
563
|
+
.max(FIELD_LIMITS.nameLength)
|
|
368
564
|
.refine((name) => name === name.trim(), "no surrounding spaces");
|
|
369
565
|
|
|
566
|
+
/** The order an object's properties are shown in, each named once. */
|
|
567
|
+
const propertyOrderSchema = z
|
|
568
|
+
.array(z.string())
|
|
569
|
+
.refine((names) => new Set(names).size === names.length, "a name repeats")
|
|
570
|
+
.optional();
|
|
571
|
+
|
|
370
572
|
/** What an array holds: a scalar, or an object with properties of its own. */
|
|
371
573
|
const itemsSchema = z.strictObject({
|
|
372
574
|
type: z.enum([...SCALAR_TYPES, "object"]),
|
|
@@ -374,29 +576,32 @@ const itemsSchema = z.strictObject({
|
|
|
374
576
|
return z.record(propertyNameSchema, schemaPropertySchema).optional();
|
|
375
577
|
},
|
|
376
578
|
required: z.array(z.string()).optional(),
|
|
579
|
+
"x-order": propertyOrderSchema,
|
|
377
580
|
});
|
|
378
581
|
|
|
379
582
|
/**
|
|
380
583
|
* A JSON Schema property, in the part of JSON Schema a field editor holds:
|
|
381
584
|
* a type (with `"null"` beside it when the value may be null), an `enum` of
|
|
382
|
-
* strings for a string, an object's `properties` and
|
|
383
|
-
* array's `items`, a `description`, and
|
|
384
|
-
* (redacted from logs), `x-source`
|
|
385
|
-
* of a tool's input only)
|
|
585
|
+
* strings for a string, an object's `properties`, `required` and the
|
|
586
|
+
* `x-order` they are shown in, an array's `items`, a `description`, and
|
|
587
|
+
* the extensions `x-sensitive` (redacted from logs), `x-source` (top-level
|
|
588
|
+
* scalar fields of a tool's input only) and `x-target` (top-level fields of
|
|
589
|
+
* a tool's input only).
|
|
386
590
|
*/
|
|
387
591
|
const schemaPropertySchema = z.strictObject({
|
|
388
592
|
type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
|
|
389
|
-
description: z.string().trim().max(
|
|
593
|
+
description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
|
|
390
594
|
/** A nullable enum lists `null` beside its values, as the console stores one. */
|
|
391
595
|
enum: z
|
|
392
596
|
.array(z.union([z.string().trim().min(1), z.null()]))
|
|
393
597
|
.min(1)
|
|
394
|
-
.max(
|
|
598
|
+
.max(FIELD_LIMITS.enumValues + 1)
|
|
395
599
|
.optional(),
|
|
396
600
|
get properties() {
|
|
397
601
|
return z.record(propertyNameSchema, schemaPropertySchema).optional();
|
|
398
602
|
},
|
|
399
603
|
required: z.array(z.string()).optional(),
|
|
604
|
+
"x-order": propertyOrderSchema,
|
|
400
605
|
get items() {
|
|
401
606
|
return itemsSchema.optional();
|
|
402
607
|
},
|
|
@@ -414,24 +619,30 @@ function typeOf(node: { type: SchemaProperty["type"] }) {
|
|
|
414
619
|
/** A refusal below a node: where, relative to it, and why. */
|
|
415
620
|
type RowIssue = { at: Array<string | number>; message: string };
|
|
416
621
|
|
|
417
|
-
/**
|
|
418
|
-
|
|
622
|
+
/** A node holding properties: a property, an array's items, or an input or output schema. */
|
|
623
|
+
type ObjectNode = {
|
|
419
624
|
type: SchemaProperty["type"];
|
|
420
625
|
properties?: Record<string, SchemaProperty>;
|
|
421
626
|
required?: string[];
|
|
422
|
-
|
|
627
|
+
"x-order"?: string[];
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
/** What a node's own keywords break: `properties`, `required` or `x-order` off an object, or the last two naming no property. */
|
|
631
|
+
function nodeIssues(node: ObjectNode) {
|
|
423
632
|
const issues = new Array<RowIssue>();
|
|
424
633
|
const object = typeOf(node) === "object";
|
|
425
|
-
for (const keyword of ["properties", "required"] as const) {
|
|
634
|
+
for (const keyword of ["properties", "required", "x-order"] as const) {
|
|
426
635
|
if (!object && node[keyword] !== undefined) {
|
|
427
636
|
issues.push({ at: [keyword], message: "only an object has it" });
|
|
428
637
|
}
|
|
429
638
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
639
|
+
for (const keyword of ["required", "x-order"] as const) {
|
|
640
|
+
node[keyword]?.forEach((name, index) => {
|
|
641
|
+
if (!Object.hasOwn(node.properties ?? {}, name)) {
|
|
642
|
+
issues.push({ at: [keyword, index], message: "names no property" });
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
435
646
|
return issues;
|
|
436
647
|
}
|
|
437
648
|
|
|
@@ -444,20 +655,24 @@ function enumIssueOf(property: SchemaProperty, type: string | undefined) {
|
|
|
444
655
|
if (property.enum === undefined) return undefined;
|
|
445
656
|
if (type !== "string") return "an enum lists a string's values";
|
|
446
657
|
const values = property.enum.filter((value) => value !== null);
|
|
447
|
-
if (values.length === 0 || values.length >
|
|
658
|
+
if (values.length === 0 || values.length > FIELD_LIMITS.enumValues) {
|
|
659
|
+
return `an enum lists 1 to ${FIELD_LIMITS.enumValues} values`;
|
|
660
|
+
}
|
|
448
661
|
const nullable = Array.isArray(property.type);
|
|
449
662
|
if (nullable === property.enum.includes(null)) return undefined;
|
|
450
663
|
return nullable ? "a nullable enum lists null" : "only a nullable enum lists null";
|
|
451
664
|
}
|
|
452
665
|
|
|
666
|
+
/** The extensions a tool's input binds a field with: a source filling it, a place in the request. */
|
|
667
|
+
type Binding = "x-source" | "x-target";
|
|
668
|
+
|
|
453
669
|
/**
|
|
454
670
|
* What one property breaks of itself: an `enum` off a string, with no value,
|
|
455
671
|
* or listing `null` when the type does not allow it or the other way round;
|
|
456
|
-
* `items` off an array or an array without them; and
|
|
457
|
-
*
|
|
458
|
-
* fit the field.
|
|
672
|
+
* `items` off an array or an array without them; and what its bindings
|
|
673
|
+
* break (`bindingIssues`).
|
|
459
674
|
*/
|
|
460
|
-
function propertyIssues(property: SchemaProperty,
|
|
675
|
+
function propertyIssues(property: SchemaProperty, bindings: ReadonlyArray<Binding> | undefined) {
|
|
461
676
|
const issues = new Array<RowIssue>();
|
|
462
677
|
const type = typeOf(property);
|
|
463
678
|
const enumIssue = enumIssueOf(property, type);
|
|
@@ -468,17 +683,41 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
|
|
|
468
683
|
message: type === "array" ? "an array declares its items" : "only an array has it",
|
|
469
684
|
});
|
|
470
685
|
}
|
|
471
|
-
|
|
686
|
+
return [...issues, ...bindingIssues(property, type, bindings)];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* What a property's bindings break: one other than `bindings` directly under
|
|
691
|
+
* a tool's input (none anywhere else), an `x-source` off a scalar, or a
|
|
692
|
+
* source that does not fit the field.
|
|
693
|
+
*/
|
|
694
|
+
function bindingIssues(
|
|
695
|
+
property: SchemaProperty,
|
|
696
|
+
type: string | undefined,
|
|
697
|
+
bindings: ReadonlyArray<Binding> | undefined,
|
|
698
|
+
) {
|
|
699
|
+
const issues = new Array<RowIssue>();
|
|
700
|
+
const scalar = SCALAR_TYPES.some((scalar) => scalar === type);
|
|
701
|
+
// A source fills a scalar; a target places a field of any type.
|
|
702
|
+
const fields = { "x-source": "a top-level scalar field", "x-target": "a top-level field" };
|
|
472
703
|
for (const binding of ["x-source", "x-target"] as const) {
|
|
473
|
-
if (property[binding]
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
704
|
+
if (property[binding] === undefined) continue;
|
|
705
|
+
const reaches = bindings !== undefined && (binding === "x-target" || scalar);
|
|
706
|
+
if (reaches && bindings.includes(binding)) continue;
|
|
707
|
+
issues.push({
|
|
708
|
+
at: [binding],
|
|
709
|
+
message: reaches
|
|
710
|
+
? "this type of tool takes none"
|
|
711
|
+
: `only ${fields[binding]} of a tool's input has it`,
|
|
712
|
+
});
|
|
479
713
|
}
|
|
480
714
|
const source = property["x-source"];
|
|
481
|
-
if (
|
|
715
|
+
if (
|
|
716
|
+
scalar &&
|
|
717
|
+
bindings?.includes("x-source") &&
|
|
718
|
+
source &&
|
|
719
|
+
!sourceFits(source, { type, enum: property.enum })
|
|
720
|
+
) {
|
|
482
721
|
issues.push({ at: ["x-source"], message: "the source does not fit the field's type" });
|
|
483
722
|
}
|
|
484
723
|
return issues;
|
|
@@ -487,18 +726,15 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
|
|
|
487
726
|
/**
|
|
488
727
|
* Refuses, at every depth below `path`, what the console's field rows cannot
|
|
489
728
|
* hold, since it stores a schema as rows and would drop or change it in
|
|
490
|
-
* silence (`nodeIssues`, `propertyIssues`);
|
|
491
|
-
*
|
|
729
|
+
* silence (`nodeIssues`, `propertyIssues`); a binding only one of
|
|
730
|
+
* `bindings`, directly under the node they are given for, and `x-source`
|
|
731
|
+
* only on a scalar.
|
|
492
732
|
*/
|
|
493
733
|
function heldByRows(
|
|
494
|
-
node:
|
|
495
|
-
type: SchemaProperty["type"];
|
|
496
|
-
properties?: Record<string, SchemaProperty>;
|
|
497
|
-
required?: string[];
|
|
498
|
-
},
|
|
734
|
+
node: ObjectNode,
|
|
499
735
|
context: z.RefinementCtx,
|
|
500
736
|
path: Array<string | number>,
|
|
501
|
-
|
|
737
|
+
bindings: ReadonlyArray<Binding> | undefined,
|
|
502
738
|
) {
|
|
503
739
|
const report = (under: Array<string | number>, issues: ReadonlyArray<RowIssue>) => {
|
|
504
740
|
for (const { at, message } of issues) {
|
|
@@ -508,27 +744,29 @@ function heldByRows(
|
|
|
508
744
|
report(path, nodeIssues(node));
|
|
509
745
|
for (const [name, property] of Object.entries(node.properties ?? {})) {
|
|
510
746
|
const at = [...path, "properties", name];
|
|
511
|
-
report(at, propertyIssues(property,
|
|
512
|
-
heldByRows(property, context, at,
|
|
513
|
-
if (property.items) heldByRows(property.items, context, [...at, "items"],
|
|
747
|
+
report(at, propertyIssues(property, bindings));
|
|
748
|
+
heldByRows(property, context, at, undefined);
|
|
749
|
+
if (property.items) heldByRows(property.items, context, [...at, "items"], undefined);
|
|
514
750
|
}
|
|
515
751
|
}
|
|
516
752
|
|
|
517
753
|
/**
|
|
518
754
|
* An object's JSON Schema, the shape of a tool's or trigger's input and
|
|
519
|
-
* output
|
|
520
|
-
*
|
|
755
|
+
* output, and of what the console's field rows write. `bindings`, given for
|
|
756
|
+
* a tool's input, are the extensions its top-level fields may carry:
|
|
757
|
+
* `x-source` on a scalar, `x-target` on any field.
|
|
521
758
|
*/
|
|
522
|
-
function
|
|
759
|
+
export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
|
|
523
760
|
return z
|
|
524
761
|
.strictObject({
|
|
525
762
|
type: z.literal("object"),
|
|
526
763
|
properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
|
|
527
764
|
required: z.array(z.string()).optional(),
|
|
765
|
+
"x-order": propertyOrderSchema,
|
|
528
766
|
})
|
|
529
|
-
.superRefine((schema, context) => heldByRows(schema, context, [],
|
|
767
|
+
.superRefine((schema, context) => heldByRows(schema, context, [], bindings));
|
|
530
768
|
}
|
|
531
|
-
const objectSchema =
|
|
769
|
+
const objectSchema = objectJsonSchemaOf();
|
|
532
770
|
|
|
533
771
|
const EMPTY_OBJECT = { type: "object" } as const satisfies z.input<typeof objectSchema>;
|
|
534
772
|
|
|
@@ -544,23 +782,44 @@ const audienceSchema = z.union([
|
|
|
544
782
|
}),
|
|
545
783
|
]);
|
|
546
784
|
|
|
785
|
+
/** A model provider's base URL and each model it lists in characters, and how many it lists. The console caps a provider at the same. */
|
|
786
|
+
export const MODEL_PROVIDER_LIMITS = { urlLength: 2_048, modelLength: 200, models: 100 } as const;
|
|
787
|
+
|
|
547
788
|
/** A model a provider lists, and the reasoning effort sent with it. */
|
|
548
789
|
const modelSchema = z.strictObject({
|
|
549
790
|
provider: nameSchema,
|
|
550
|
-
model: z.string().trim().min(1).max(
|
|
791
|
+
model: z.string().trim().min(1).max(MODEL_PROVIDER_LIMITS.modelLength),
|
|
551
792
|
effort: z.string().trim().min(1).max(64).optional(),
|
|
552
793
|
});
|
|
553
794
|
|
|
795
|
+
/** A service's name and description, in characters per locale. The console caps its editor at the same lengths. */
|
|
796
|
+
export const SERVICE_LIMITS = { nameLength: 120, descriptionLength: 4_000 } as const;
|
|
797
|
+
|
|
554
798
|
/** One line per locale, trimmed. */
|
|
555
|
-
const localizedLineSchema = z.record(
|
|
556
|
-
|
|
799
|
+
const localizedLineSchema = z.record(
|
|
800
|
+
localeSchema,
|
|
801
|
+
z.string().trim().min(1).max(SERVICE_LIMITS.nameLength),
|
|
802
|
+
);
|
|
803
|
+
const localizedTextSchema = z.record(
|
|
804
|
+
localeSchema,
|
|
805
|
+
z.string().trim().min(1).max(SERVICE_LIMITS.descriptionLength),
|
|
806
|
+
);
|
|
557
807
|
|
|
558
808
|
const { tool: toolBounds } = SETTING_BOUNDS;
|
|
559
809
|
|
|
810
|
+
/** A `type` tool's input schema, its fields bound only as `TOOL_TYPES` lets the type: filled from a source, placed in the request. */
|
|
811
|
+
function toolInputSchemaOf(type: (typeof TOOL_TYPE_NAMES)[number]) {
|
|
812
|
+
const { sources, targets } = TOOL_TYPES[type];
|
|
813
|
+
return objectJsonSchemaOf([
|
|
814
|
+
...(sources ? ["x-source" as const] : []),
|
|
815
|
+
...(targets ? ["x-target" as const] : []),
|
|
816
|
+
]).default(EMPTY_OBJECT);
|
|
817
|
+
}
|
|
818
|
+
|
|
560
819
|
/** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, credentials, approval or bound fields. */
|
|
561
820
|
const toolBaseSchema = z.strictObject({
|
|
562
821
|
...serverToolSchema.pick({ name: true, description: true }).shape,
|
|
563
|
-
inputSchema:
|
|
822
|
+
inputSchema: toolInputSchemaOf("client"),
|
|
564
823
|
outputSchema: objectSchema.default(EMPTY_OBJECT),
|
|
565
824
|
timeoutMs: setting(toolBounds.timeoutMs),
|
|
566
825
|
maxResponseBytes: setting(toolBounds.maxResponseBytes),
|
|
@@ -572,11 +831,19 @@ const toolBaseSchema = z.strictObject({
|
|
|
572
831
|
pinned: z.boolean().default(false),
|
|
573
832
|
tags: toolTagsSchema.default([]),
|
|
574
833
|
audience: audienceSchema,
|
|
834
|
+
overridable: overridable([
|
|
835
|
+
"description",
|
|
836
|
+
"timeoutMs",
|
|
837
|
+
"maxResponseBytes",
|
|
838
|
+
"rateLimitRequests",
|
|
839
|
+
"rateLimitWindowSeconds",
|
|
840
|
+
"pinned",
|
|
841
|
+
"tags",
|
|
842
|
+
]),
|
|
575
843
|
});
|
|
576
844
|
|
|
577
845
|
/** A tool that calls an endpoint; with `requiresApproval`, every call waits for the end user to approve it. */
|
|
578
846
|
const calledToolSchema = toolBaseSchema.extend({
|
|
579
|
-
inputSchema: objectSchemaOf(true).default(EMPTY_OBJECT),
|
|
580
847
|
url: upstreamUrlSchema.refine(
|
|
581
848
|
confinesEndpointParams,
|
|
582
849
|
"a {parameter} outside the path or query",
|
|
@@ -589,28 +856,33 @@ const manifestToolSchema = z
|
|
|
589
856
|
.discriminatedUnion("type", [
|
|
590
857
|
calledToolSchema.extend({
|
|
591
858
|
type: z.literal("http"),
|
|
859
|
+
inputSchema: toolInputSchemaOf("http"),
|
|
592
860
|
method: z.enum(TOOL_METHODS),
|
|
593
861
|
readOnly: z.boolean(),
|
|
594
862
|
...toolConfigByType.http.shape,
|
|
595
863
|
}),
|
|
596
|
-
/**
|
|
597
|
-
|
|
864
|
+
/**
|
|
865
|
+
* The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it.
|
|
866
|
+
* No `requiresApproval`: the widget answers the call, so nothing would ask for it. No field
|
|
867
|
+
* names its place in the request (`x-target`).
|
|
868
|
+
*/
|
|
869
|
+
calledToolSchema.omit({ requiresApproval: true }).extend({
|
|
598
870
|
type: z.literal("embedded"),
|
|
871
|
+
inputSchema: toolInputSchemaOf("embedded"),
|
|
599
872
|
method: z.enum(TOOL_METHODS),
|
|
600
873
|
...toolConfigByType.embedded.shape,
|
|
601
874
|
}),
|
|
602
875
|
/** Always POSTed. */
|
|
603
876
|
calledToolSchema.extend({
|
|
604
877
|
type: z.literal("soap"),
|
|
878
|
+
inputSchema: toolInputSchemaOf("soap"),
|
|
605
879
|
readOnly: z.boolean(),
|
|
606
880
|
...toolConfigByType.soap.shape,
|
|
607
881
|
}),
|
|
608
882
|
/** Answered by your app: the model calls it and the widget hands the call over. */
|
|
609
883
|
toolBaseSchema.extend({ type: z.literal("client") }),
|
|
610
884
|
])
|
|
611
|
-
.superRefine((tool, context) =>
|
|
612
|
-
if (tool.type !== "client") refuseBlockedApprovalTitle(tool, context, []);
|
|
613
|
-
});
|
|
885
|
+
.superRefine((tool, context) => refuseBlockedApprovalTitle(tool, context, []));
|
|
614
886
|
export type ManifestTool = z.input<typeof manifestToolSchema>;
|
|
615
887
|
|
|
616
888
|
const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]);
|
|
@@ -629,6 +901,8 @@ const manifestOpenApiSchema = z.strictObject({
|
|
|
629
901
|
/** Where the document is read, with no credentials: OpenAPI 3 or Swagger 2, JSON or YAML. */
|
|
630
902
|
url: upstreamUrlSchema,
|
|
631
903
|
credentials: credentialsSchema,
|
|
904
|
+
/** Where every operation's successful response keeps its result, when the API wraps each answer. */
|
|
905
|
+
resultPath: jsonPointerSchema.optional(),
|
|
632
906
|
/** Tools listed in every turn of every agent they reach, whatever the question. */
|
|
633
907
|
pinned: toolNamesSchema,
|
|
634
908
|
approval: z
|
|
@@ -650,17 +924,22 @@ const manifestServiceSchema = z.strictObject({
|
|
|
650
924
|
/** Rich text: it may mention tools and skills. */
|
|
651
925
|
description: localizedTextSchema,
|
|
652
926
|
audience: audienceSchema,
|
|
927
|
+
overridable: overridable(["description"]),
|
|
653
928
|
});
|
|
654
929
|
export type ManifestService = z.input<typeof manifestServiceSchema>;
|
|
655
930
|
|
|
931
|
+
/** A skill's description in characters. The console caps its editor at the same. */
|
|
932
|
+
export const SKILL_LIMITS = { descriptionLength: 500 } as const;
|
|
933
|
+
|
|
656
934
|
const manifestSkillSchema = z.strictObject({
|
|
657
935
|
name: slugSchema,
|
|
658
|
-
description: z.string().trim().min(1).max(
|
|
936
|
+
description: z.string().trim().min(1).max(SKILL_LIMITS.descriptionLength),
|
|
659
937
|
/** Rich text: it may mention tools, skills and datasets, and read `{{variables}}`. */
|
|
660
938
|
body: z.string().trim().min(1),
|
|
661
939
|
/** The key of the service the skill renders inside; fixed once created. */
|
|
662
940
|
service: slugSchema.optional(),
|
|
663
941
|
audience: audienceSchema,
|
|
942
|
+
overridable: overridable(["description", "body"]),
|
|
664
943
|
});
|
|
665
944
|
export type ManifestSkill = z.input<typeof manifestSkillSchema>;
|
|
666
945
|
|
|
@@ -677,15 +956,18 @@ export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
|
|
|
677
956
|
z.object({ policy: z.literal("single") }),
|
|
678
957
|
z.object({ policy: z.literal("per-firing") }),
|
|
679
958
|
z.object({ policy: z.literal("ephemeral") }),
|
|
680
|
-
z.object({ policy: z.literal("keyed"), key:
|
|
959
|
+
z.object({ policy: z.literal("keyed"), key: jsonPointerSchema }),
|
|
681
960
|
]);
|
|
682
961
|
export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
|
|
683
962
|
|
|
963
|
+
/** A trigger's description and instructions in characters. The console caps its editor at the same. */
|
|
964
|
+
export const TRIGGER_LIMITS = { descriptionLength: 500, instructionsLength: 20_000 } as const;
|
|
965
|
+
|
|
684
966
|
/** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
|
|
685
967
|
const manifestTriggerSchema = z.strictObject({
|
|
686
968
|
/** `message` is the built-in trigger every agent has. */
|
|
687
969
|
name: slugSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
|
|
688
|
-
description: z.string().trim().min(1).max(
|
|
970
|
+
description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
|
|
689
971
|
inputSchema: objectSchema.default(EMPTY_OBJECT),
|
|
690
972
|
/** The shape the run answers in; absent, the run answers in free text. */
|
|
691
973
|
outputSchema: objectSchema
|
|
@@ -695,7 +977,7 @@ const manifestTriggerSchema = z.strictObject({
|
|
|
695
977
|
)
|
|
696
978
|
.optional(),
|
|
697
979
|
/** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
|
|
698
|
-
instructions: z.string().trim().min(1).max(
|
|
980
|
+
instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
|
|
699
981
|
threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
|
|
700
982
|
/** Firings a day per agent; absent, no limit. */
|
|
701
983
|
budgetPerDay: z.number().int().positive().optional(),
|
|
@@ -708,6 +990,14 @@ const manifestTriggerSchema = z.strictObject({
|
|
|
708
990
|
/** The model its runs talk to, over each agent's. */
|
|
709
991
|
model: modelSchema.optional(),
|
|
710
992
|
audience: audienceSchema,
|
|
993
|
+
overridable: overridable([
|
|
994
|
+
"description",
|
|
995
|
+
"instructions",
|
|
996
|
+
"threadPolicy",
|
|
997
|
+
"budgetPerDay",
|
|
998
|
+
"disableAfterFailures",
|
|
999
|
+
"model",
|
|
1000
|
+
]),
|
|
711
1001
|
});
|
|
712
1002
|
export type ManifestTrigger = z.input<typeof manifestTriggerSchema>;
|
|
713
1003
|
|
|
@@ -731,6 +1021,7 @@ const manifestDatasetHostSchema = z
|
|
|
731
1021
|
/** Among those, the ones listed in every turn, whatever the question. */
|
|
732
1022
|
pinned: z.array(datasetIdentifierSchema).max(50).default([]),
|
|
733
1023
|
audience: audienceSchema,
|
|
1024
|
+
overridable: overridable(["timeoutMs"]),
|
|
734
1025
|
})
|
|
735
1026
|
.superRefine((host, context) => {
|
|
736
1027
|
const unexposed = unexposedPins(host);
|
|
@@ -758,9 +1049,9 @@ export const mcpToolNamesSchema = z
|
|
|
758
1049
|
|
|
759
1050
|
const manifestMcpServerSchema = z.strictObject({
|
|
760
1051
|
/** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
|
|
761
|
-
name:
|
|
1052
|
+
name: identifierSchema,
|
|
762
1053
|
url: upstreamUrlSchema,
|
|
763
|
-
credentials:
|
|
1054
|
+
credentials: declaredPresentationsSchema(
|
|
764
1055
|
presentedHeaderSchema.refine(
|
|
765
1056
|
(name) => !mcpTransportHeaders.has(name.toLowerCase()),
|
|
766
1057
|
"a header the MCP transport sets",
|
|
@@ -771,6 +1062,7 @@ const manifestMcpServerSchema = z.strictObject({
|
|
|
771
1062
|
include: mcpToolNamesSchema.default([]),
|
|
772
1063
|
exclude: mcpToolNamesSchema.default([]),
|
|
773
1064
|
audience: audienceSchema,
|
|
1065
|
+
overridable: overridable(["include", "exclude"]),
|
|
774
1066
|
});
|
|
775
1067
|
export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
|
|
776
1068
|
|
|
@@ -784,7 +1076,7 @@ const manifestWebhookSchema = z.strictObject({
|
|
|
784
1076
|
description: z.string().trim().max(500).default(""),
|
|
785
1077
|
event: z.enum(SERVER_EVENTS),
|
|
786
1078
|
url: upstreamUrlSchema,
|
|
787
|
-
credentials:
|
|
1079
|
+
credentials: declaredPresentationsSchema(
|
|
788
1080
|
presentedHeaderSchema.refine(
|
|
789
1081
|
(name) => name.toLowerCase() !== EVENT_HEADER.toLowerCase(),
|
|
790
1082
|
"the header that names the event",
|
|
@@ -793,16 +1085,23 @@ const manifestWebhookSchema = z.strictObject({
|
|
|
793
1085
|
),
|
|
794
1086
|
timeoutMs: setting(SETTING_BOUNDS.webhook.timeoutMs),
|
|
795
1087
|
audience: audienceSchema,
|
|
1088
|
+
overridable: overridable(["description", "timeoutMs"]),
|
|
796
1089
|
});
|
|
797
1090
|
export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
|
|
798
1091
|
|
|
1092
|
+
/** A collection's name and description in characters. The console caps its editor at the same. */
|
|
1093
|
+
export const COLLECTION_LIMITS = { nameLength: 80, descriptionLength: 2_000 } as const;
|
|
1094
|
+
|
|
799
1095
|
/** Holds documents, which are uploaded to it apart from the manifest. */
|
|
800
1096
|
const manifestCollectionSchema = z.strictObject({
|
|
801
|
-
name: z.string().trim().min(1).max(
|
|
802
|
-
description: z.string().trim().max(
|
|
1097
|
+
name: z.string().trim().min(1).max(COLLECTION_LIMITS.nameLength),
|
|
1098
|
+
description: z.string().trim().max(COLLECTION_LIMITS.descriptionLength).default(""),
|
|
803
1099
|
/** The key of the service every document in the collection belongs to. */
|
|
804
1100
|
service: slugSchema.optional(),
|
|
1101
|
+
/** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
|
|
1102
|
+
citable: z.boolean().default(true),
|
|
805
1103
|
audience: audienceSchema,
|
|
1104
|
+
overridable: overridable(["description"]),
|
|
806
1105
|
});
|
|
807
1106
|
export type ManifestCollection = z.input<typeof manifestCollectionSchema>;
|
|
808
1107
|
|
|
@@ -816,9 +1115,9 @@ const tokenAudienceSchema = z.string().trim().min(1).max(2048).optional();
|
|
|
816
1115
|
* How the agent verifies end-user tokens: against the keys its issuer
|
|
817
1116
|
* publishes at a JWKS URI, with a shared secret named in Cortex's secret
|
|
818
1117
|
* store and the one algorithm tokens are signed with, through its server's
|
|
819
|
-
* `/verify`, or not at all.
|
|
1118
|
+
* `/verify`, or not at all. Cortex stores it on the agent as written here.
|
|
820
1119
|
*/
|
|
821
|
-
const agentAuthSchema = z.discriminatedUnion("kind", [
|
|
1120
|
+
export const agentAuthSchema = z.discriminatedUnion("kind", [
|
|
822
1121
|
z.strictObject({
|
|
823
1122
|
kind: z.literal("jwks"),
|
|
824
1123
|
jwksUri: tlsUrlSchema,
|
|
@@ -838,12 +1137,16 @@ const agentAuthSchema = z.discriminatedUnion("kind", [
|
|
|
838
1137
|
|
|
839
1138
|
const { agent: agentBounds, agentContext, agentLimits } = SETTING_BOUNDS;
|
|
840
1139
|
|
|
841
|
-
/**
|
|
842
|
-
|
|
843
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* How the agent runs; every setting left out keeps its default. The
|
|
1142
|
+
* console's settings input requires each of them.
|
|
1143
|
+
*/
|
|
1144
|
+
export const agentSettingsSchema = z.strictObject({
|
|
1145
|
+
/** Lets `searchRegistries` search the knowledge in the collections the agent sees. */
|
|
844
1146
|
ragEnabled: z.boolean().default(false),
|
|
845
|
-
/**
|
|
1147
|
+
/** Lets `searchRegistries` search the tools the agent sees, beyond the pinned and mentioned ones. */
|
|
846
1148
|
toolRegistryEnabled: z.boolean().default(false),
|
|
1149
|
+
/** Lets `searchRegistries` search the services the agent sees. */
|
|
847
1150
|
serviceRegistryEnabled: z.boolean().default(false),
|
|
848
1151
|
attachmentsEnabled: z.boolean().default(true),
|
|
849
1152
|
/** Hosts the built-in `fetchImage` tool may pull images from; none, no such tool. */
|
|
@@ -856,7 +1159,7 @@ const agentSettingsSchema = z.strictObject({
|
|
|
856
1159
|
media: z.array(z.enum(MEDIA_FORMS)).default([...DEFAULT_MEDIA_FORMS]),
|
|
857
1160
|
/** Off, end users cannot message the agent: only its triggers reach it. */
|
|
858
1161
|
messageTrigger: z.boolean().default(true),
|
|
859
|
-
defaultLocale: localeSchema.default(
|
|
1162
|
+
defaultLocale: localeSchema.default(BASE_LOCALE),
|
|
860
1163
|
rateLimitRequests: setting(agentBounds.rateLimitRequests),
|
|
861
1164
|
rateLimitWindowSeconds: setting(agentBounds.rateLimitWindowSeconds),
|
|
862
1165
|
dailyExecutionQuota: setting(agentBounds.dailyExecutionQuota),
|
|
@@ -881,9 +1184,12 @@ const agentSettingsSchema = z.strictObject({
|
|
|
881
1184
|
.prefault({}),
|
|
882
1185
|
});
|
|
883
1186
|
|
|
1187
|
+
/** An agent's name in characters. The console caps its editor at the same. */
|
|
1188
|
+
export const AGENT_LIMITS = { nameLength: 120 } as const;
|
|
1189
|
+
|
|
884
1190
|
const manifestAgentSchema = z.strictObject({
|
|
885
1191
|
slug: agentSlugSchema,
|
|
886
|
-
name: z.string().trim().min(1).max(
|
|
1192
|
+
name: z.string().trim().min(1).max(AGENT_LIMITS.nameLength),
|
|
887
1193
|
/** The server it belongs to, by name; a manifest never creates one. */
|
|
888
1194
|
server: nameSchema,
|
|
889
1195
|
/** Rich text: it may mention tools and skills, and read `{{variables}}`. */
|
|
@@ -901,55 +1207,95 @@ const manifestAgentSchema = z.strictObject({
|
|
|
901
1207
|
})
|
|
902
1208
|
.default({}),
|
|
903
1209
|
settings: agentSettingsSchema.prefault({}),
|
|
904
|
-
suggestions: suggestionsSchema.default(
|
|
905
|
-
greeting: greetingSchema.default(
|
|
1210
|
+
suggestions: suggestionsSchema.default(() => byLocale(() => [])),
|
|
1211
|
+
greeting: greetingSchema.default(() => byLocale(() => "")),
|
|
1212
|
+
overridable: overridable(["name", "prompt", "models", "settings", "suggestions", "greeting"]),
|
|
906
1213
|
});
|
|
907
1214
|
export type ManifestAgent = z.input<typeof manifestAgentSchema>;
|
|
908
1215
|
|
|
909
|
-
/**
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1216
|
+
/**
|
|
1217
|
+
* Every kind of entry a manifest owns a row for, in the order an apply
|
|
1218
|
+
* reports a name another row holds and a kind its actor may not write. An
|
|
1219
|
+
* OpenAPI source is none: its operations become `tools` entries.
|
|
1220
|
+
*/
|
|
1221
|
+
export const MANIFEST_KINDS = [
|
|
1222
|
+
"agents",
|
|
1223
|
+
"services",
|
|
1224
|
+
"tools",
|
|
1225
|
+
"skills",
|
|
1226
|
+
"triggers",
|
|
1227
|
+
"datasetHosts",
|
|
1228
|
+
"mcpServers",
|
|
1229
|
+
"webhooks",
|
|
1230
|
+
"collections",
|
|
1231
|
+
] as const;
|
|
1232
|
+
export type ManifestKind = (typeof MANIFEST_KINDS)[number];
|
|
913
1233
|
|
|
914
|
-
|
|
915
|
-
|
|
1234
|
+
/**
|
|
1235
|
+
* The field each kind's entries go by, which its row's name holds: an
|
|
1236
|
+
* agent's slug, a service's key, any other entry's name. A list holds each
|
|
1237
|
+
* once, so one entry means one row.
|
|
1238
|
+
*/
|
|
1239
|
+
export const MANIFEST_NAME_FIELDS = {
|
|
1240
|
+
agents: "slug",
|
|
1241
|
+
tools: "name",
|
|
1242
|
+
services: "key",
|
|
1243
|
+
skills: "name",
|
|
1244
|
+
triggers: "name",
|
|
1245
|
+
datasetHosts: "name",
|
|
1246
|
+
mcpServers: "name",
|
|
1247
|
+
webhooks: "name",
|
|
1248
|
+
collections: "name",
|
|
1249
|
+
} as const satisfies Record<ManifestKind, string>;
|
|
1250
|
+
|
|
1251
|
+
/** A list of entries named by their `field`, refusing a name they repeat. */
|
|
1252
|
+
function listOf<TField extends string, TEntry extends z.ZodType<Record<TField, string>>>(
|
|
1253
|
+
field: TField,
|
|
1254
|
+
entry: TEntry,
|
|
1255
|
+
) {
|
|
1256
|
+
return z
|
|
1257
|
+
.array(entry)
|
|
1258
|
+
.refine(
|
|
1259
|
+
(entries) => new Set(entries.map((value) => value[field])).size === entries.length,
|
|
1260
|
+
"a name repeats",
|
|
1261
|
+
)
|
|
1262
|
+
.default([]);
|
|
916
1263
|
}
|
|
917
1264
|
|
|
918
|
-
const byName = (entry: { name: string }) => entry.name;
|
|
919
|
-
|
|
920
1265
|
/** The id of Cortex's own manifest, which only Cortex applies: the management API refuses any other under it. */
|
|
921
1266
|
export const CORTEX_MANIFEST_ID = "cortex";
|
|
922
1267
|
|
|
923
1268
|
/**
|
|
924
1269
|
* A deployment's agents and components, owned by the manifest `id`: an apply
|
|
925
1270
|
* of it creates what is new, updates what changed, and deletes what it no
|
|
926
|
-
* longer lists, touching nothing it did not create.
|
|
1271
|
+
* longer lists, touching nothing it did not create. What an entry lists as
|
|
1272
|
+
* `overridable` may then change in Cortex too, and an apply keeps that
|
|
1273
|
+
* change unless the manifest changed the same field.
|
|
927
1274
|
*/
|
|
928
1275
|
export const manifestSchema = z.strictObject({
|
|
929
1276
|
/** Fixed for the deployment it describes: the rows an apply owns carry it. */
|
|
930
1277
|
id: slugSchema,
|
|
931
|
-
agents: listOf(
|
|
932
|
-
tools: listOf(
|
|
933
|
-
openapi: listOf(
|
|
934
|
-
services: listOf(
|
|
935
|
-
skills: listOf(
|
|
936
|
-
triggers: listOf(
|
|
937
|
-
datasetHosts: listOf(
|
|
938
|
-
mcpServers: listOf(
|
|
939
|
-
webhooks: listOf(
|
|
940
|
-
collections: listOf(
|
|
941
|
-
});
|
|
1278
|
+
agents: listOf(MANIFEST_NAME_FIELDS.agents, manifestAgentSchema),
|
|
1279
|
+
tools: listOf(MANIFEST_NAME_FIELDS.tools, manifestToolSchema),
|
|
1280
|
+
openapi: listOf("name", manifestOpenApiSchema),
|
|
1281
|
+
services: listOf(MANIFEST_NAME_FIELDS.services, manifestServiceSchema),
|
|
1282
|
+
skills: listOf(MANIFEST_NAME_FIELDS.skills, manifestSkillSchema),
|
|
1283
|
+
triggers: listOf(MANIFEST_NAME_FIELDS.triggers, manifestTriggerSchema),
|
|
1284
|
+
datasetHosts: listOf(MANIFEST_NAME_FIELDS.datasetHosts, manifestDatasetHostSchema),
|
|
1285
|
+
mcpServers: listOf(MANIFEST_NAME_FIELDS.mcpServers, manifestMcpServerSchema),
|
|
1286
|
+
webhooks: listOf(MANIFEST_NAME_FIELDS.webhooks, manifestWebhookSchema),
|
|
1287
|
+
collections: listOf(MANIFEST_NAME_FIELDS.collections, manifestCollectionSchema),
|
|
1288
|
+
} satisfies Record<ManifestKind | "id" | "openapi", z.ZodType>);
|
|
942
1289
|
/** A manifest as written: every field with a default may be left out. */
|
|
943
1290
|
export type Manifest = z.input<typeof manifestSchema>;
|
|
944
1291
|
|
|
1292
|
+
const manifestKindSchema = z.enum(MANIFEST_KINDS);
|
|
1293
|
+
|
|
945
1294
|
/**
|
|
946
|
-
* Entry names per kind
|
|
947
|
-
*
|
|
1295
|
+
* Entry names per kind, each by its `MANIFEST_NAME_FIELDS` field. An OpenAPI
|
|
1296
|
+
* source's operations are counted among `tools`.
|
|
948
1297
|
*/
|
|
949
|
-
const namesByKindSchema = z.record(
|
|
950
|
-
manifestSchema.keyof().exclude(["id", "openapi"]),
|
|
951
|
-
z.array(z.string()),
|
|
952
|
-
);
|
|
1298
|
+
const namesByKindSchema = z.record(manifestKindSchema, z.array(z.string()));
|
|
953
1299
|
|
|
954
1300
|
/** What an apply creates, updates and deletes, kind by kind; a preview answers the same without doing it. */
|
|
955
1301
|
export const manifestChangesSchema = z.object({
|
|
@@ -958,6 +1304,32 @@ export const manifestChangesSchema = z.object({
|
|
|
958
1304
|
delete: namesByKindSchema,
|
|
959
1305
|
});
|
|
960
1306
|
|
|
1307
|
+
/** A field of one entry that both the manifest and a live write changed since the last apply. */
|
|
1308
|
+
const manifestConflictSchema = z.strictObject({
|
|
1309
|
+
kind: manifestKindSchema,
|
|
1310
|
+
name: z.string(),
|
|
1311
|
+
field: z.string(),
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
/** Which value a conflict keeps: the one changed live in Cortex, or the manifest's. */
|
|
1315
|
+
const conflictSideSchema = z.enum(["live", "manifest"]);
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* What preview and sync take: the manifest, and how its conflicts resolve.
|
|
1319
|
+
* A conflict takes its own entry in `resolutions`, else `prefer`; a sync
|
|
1320
|
+
* left with one neither answers is refused as `unresolved_conflicts`,
|
|
1321
|
+
* naming them. A resolution for a field that no longer conflicts is ignored.
|
|
1322
|
+
*/
|
|
1323
|
+
export const manifestApplyInputSchema = manifestSchema.extend({
|
|
1324
|
+
prefer: conflictSideSchema.optional(),
|
|
1325
|
+
resolutions: z.array(manifestConflictSchema.extend({ keep: conflictSideSchema })).default([]),
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
/** What a preview answers: the changes, and the conflicts nothing resolved. */
|
|
1329
|
+
export const manifestPreviewSchema = manifestChangesSchema.extend({
|
|
1330
|
+
conflicts: z.array(manifestConflictSchema),
|
|
1331
|
+
});
|
|
1332
|
+
|
|
961
1333
|
/**
|
|
962
1334
|
* An apply's changes, and a fire secret, shown this once, for each trigger
|
|
963
1335
|
* it left with none anyone holds: one it created, or one whose
|