@m6d/cortex-cli 0.12.1 → 0.14.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 (76) hide show
  1. package/README.md +193 -128
  2. package/package.json +16 -5
  3. package/src/cli.ts +19 -12
  4. package/src/commands/apply.ts +186 -0
  5. package/src/commands/{cc.ts → management.ts} +173 -112
  6. package/src/commands/new.ts +83 -84
  7. package/src/commands/setup.ts +610 -0
  8. package/src/contracts/charts/chart-spec.ts +1 -1
  9. package/src/contracts/client-tools/index.ts +3 -3
  10. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  11. package/src/contracts/datasets/index.ts +3 -0
  12. package/src/contracts/egress-policy/index.ts +16 -0
  13. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  14. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  15. package/src/contracts/errors/index.ts +38 -0
  16. package/src/contracts/json/index.ts +5 -0
  17. package/src/contracts/manifest/index.ts +973 -0
  18. package/src/contracts/manifest-file/index.ts +16 -0
  19. package/src/contracts/markdown/index.ts +1 -1
  20. package/src/contracts/pattern/index.ts +276 -0
  21. package/src/contracts/server/index.ts +349 -0
  22. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  23. package/src/contracts/triggers/index.ts +112 -0
  24. package/src/contracts/wire/index.ts +67 -33
  25. package/src/{cc → management}/client.ts +33 -7
  26. package/src/{cc → management}/flags.ts +2 -1
  27. package/src/{cc → management}/profiles.ts +10 -8
  28. package/src/scaffold/files.ts +448 -284
  29. package/src/scaffold/ports.ts +12 -24
  30. package/src/scaffold/templates/cspell.json.txt +41 -0
  31. package/src/scaffold/templates/datasets.ts.txt +52 -0
  32. package/src/scaffold/templates/dockerignore.txt +6 -0
  33. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  34. package/src/scaffold/templates/gitignore.txt +1 -1
  35. package/src/scaffold/templates/server.ts.txt +40 -0
  36. package/src/scaffold/templates/setup.ts.txt +44 -0
  37. package/src/ui/prompts.ts +24 -38
  38. package/src/ui/report.ts +5 -15
  39. package/tsconfig.json +2 -3
  40. package/src/commands/graph.ts +0 -228
  41. package/src/commands/swagger.ts +0 -247
  42. package/src/config/load.ts +0 -122
  43. package/src/config/validate.ts +0 -46
  44. package/src/contracts/credentials/index.ts +0 -183
  45. package/src/contracts/egress/index.ts +0 -120
  46. package/src/contracts/graph/clients/embed.ts +0 -62
  47. package/src/contracts/graph/clients/neo4j.ts +0 -85
  48. package/src/contracts/graph/helpers.ts +0 -55
  49. package/src/contracts/graph/index.ts +0 -30
  50. package/src/contracts/graph/schema.ts +0 -65
  51. package/src/contracts/graph/types.ts +0 -125
  52. package/src/contracts/rich-text/index.ts +0 -220
  53. package/src/contracts/runtime/api.ts +0 -345
  54. package/src/contracts/runtime/auth.ts +0 -41
  55. package/src/contracts/runtime/grammar.ts +0 -169
  56. package/src/contracts/runtime/index.ts +0 -15
  57. package/src/contracts/runtime/models.ts +0 -50
  58. package/src/contracts/runtime/signature.ts +0 -255
  59. package/src/contracts/runtime/sync.ts +0 -196
  60. package/src/contracts/runtime/triggers.ts +0 -267
  61. package/src/graph/expand-domains.ts +0 -286
  62. package/src/graph/generate-cypher.ts +0 -201
  63. package/src/graph/seed.ts +0 -199
  64. package/src/graph/validate.ts +0 -78
  65. package/src/scaffold/features.ts +0 -190
  66. package/src/scaffold/templates/domains-README.md.txt +0 -16
  67. package/src/scaffold/templates/index.ts.txt +0 -18
  68. package/src/swagger/extract-endpoints.ts +0 -269
  69. package/src/swagger/generated-block.ts +0 -74
  70. package/src/swagger/openapi-schema.ts +0 -168
  71. package/src/swagger/parse-response.ts +0 -88
  72. package/src/swagger/types.ts +0 -25
  73. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  74. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  75. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  76. /package/src/{cc → management}/output.ts +0 -0
@@ -0,0 +1,973 @@
1
+ import { z } from "zod";
2
+ import { datasetIdentifierSchema } from "../datasets/index";
3
+ import { canCarrySecret } from "../egress-policy/index";
4
+ import {
5
+ agentSlugSchema,
6
+ EVENT_HEADER,
7
+ IDENTIFIER_PATTERN,
8
+ localeSchema,
9
+ SERVER_EVENTS,
10
+ refuseBlockedApprovalTitle,
11
+ serverToolSchema,
12
+ } from "../server/index";
13
+ import { MESSAGE_TRIGGER_NAME } from "../triggers/index";
14
+ import {
15
+ DEFAULT_MEDIA_FORMS,
16
+ MEDIA_FORMS,
17
+ TOOL_SURFACES,
18
+ type Greeting,
19
+ type Suggestions,
20
+ } from "../wire/index";
21
+
22
+ /*
23
+ * A manifest: the agents and components a code-first deployment declares,
24
+ * written by hand (`cortex.manifest.ts` or JSON) and applied in one
25
+ * transaction. Everything it points at, it names: a server, a model
26
+ * provider, a service by its key, an agent by its slug, and a stored secret
27
+ * or credential by the noun of what it names (`secret: "signing-key"`,
28
+ * `credential: "petstore"`). A tool input field's `x-source` is the one
29
+ * exception: `inputSchema` is the JSON Schema the tool stores, verbatim, so
30
+ * a secret there is `{ kind: "secret", name }`. No secret value ever
31
+ * appears in one. Prompts, skill bodies and trigger instructions mention a
32
+ * tool as `@[name]` and a skill as `/[name]`. Each kind's fields mirror what
33
+ * the console edits, and every bound and check below is Cortex's own, so
34
+ * Cortex accepts what this contract accepts, short of what only the
35
+ * deployment can judge: the variables a server declares, the names it
36
+ * holds, the egress policy, and a SOAP envelope's XML.
37
+ */
38
+
39
+ /** The HTTP methods a tool calls its endpoint with. */
40
+ export const TOOL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
41
+
42
+ /**
43
+ * How the executor folds an upstream body that is not a JSON object into the
44
+ * object the output schema describes: a top-level array becomes `{ items }`,
45
+ * a scalar becomes `{ value }`.
46
+ */
47
+ export const RESPONSE_WRAPS = ["items", "value"] as const;
48
+
49
+ export const SOAP_VERSIONS = ["1.1", "1.2"] as const;
50
+
51
+ type Bounds = { min: number; max: number; default: number };
52
+
53
+ /**
54
+ * 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. The
56
+ * console builds its form schemas, with its own messages, from these.
57
+ */
58
+ export const SETTING_BOUNDS = {
59
+ tool: {
60
+ timeoutMs: { min: 100, max: 120_000, default: 30_000 },
61
+ maxResponseBytes: { min: 1_024, max: 1_048_576, default: 262_144 },
62
+ rateLimitRequests: { min: 1, max: 10_000, default: 60 },
63
+ rateLimitWindowSeconds: { min: 1, max: 86_400, default: 60 },
64
+ },
65
+ agent: {
66
+ rateLimitRequests: { min: 1, max: 10_000, default: 60 },
67
+ rateLimitWindowSeconds: { min: 1, max: 86_400, default: 60 },
68
+ dailyExecutionQuota: { min: 1, max: 1_000_000, default: 10_000 },
69
+ },
70
+ /** How much of the conversation the agent's model sees. */
71
+ agentContext: {
72
+ maxContextTokens: { min: 4_000, max: 2_000_000, default: 120_000 },
73
+ /** A fraction of `maxContextTokens`, the one bound that is not an integer. */
74
+ summarizationThreshold: { min: 0.1, max: 0.95, default: 0.75 },
75
+ toolResultMaxTokens: { min: 500, max: 200_000, default: 10_000 },
76
+ recentMessagesToKeep: { min: 0, max: 100, default: 6 },
77
+ },
78
+ /** What Cortex's retrieval and tool gateway allow the agent. */
79
+ agentLimits: {
80
+ resolveTokenBudget: { min: 500, max: 64_000, default: 4_000 },
81
+ executeTimeoutMs: { min: 1_000, max: 300_000, default: 30_000 },
82
+ maxResponseBytes: { min: 1_024, max: 1_048_576, default: 262_144 },
83
+ },
84
+ trigger: { disableAfterFailures: { min: 1, max: 1_000, default: 5 } },
85
+ datasetHost: { timeoutMs: { min: 100, max: 120_000, default: 30_000 } },
86
+ webhook: { timeoutMs: { min: 1_000, max: 30_000, default: 10_000 } },
87
+ } as const satisfies Record<string, Record<string, Bounds>>;
88
+
89
+ /** An integer within `bounds`, their default when left out. */
90
+ function setting({ min, max, default: fallback }: Bounds) {
91
+ return z.number().int().min(min).max(max).default(fallback);
92
+ }
93
+
94
+ /** Consumer-facing slugs: the manifest id, service keys, skill names and trigger names share it. */
95
+ export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
96
+ const slugSchema = z.string().regex(SLUG_PATTERN).max(64);
97
+
98
+ /** A DNS host name in lower case, the way `URL` reports one: dot-separated labels of letters, digits and inner hyphens. */
99
+ export const HOSTNAME_PATTERN =
100
+ /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/;
101
+
102
+ /** At most this many hosts an agent's `fetchImage` tool pulls images from. */
103
+ export const FETCH_IMAGE_HOSTS_LIMIT = 20;
104
+
105
+ /**
106
+ * The widget's landing-page prompt chips, per locale: a short title the chip
107
+ * shows and the prompt it sends when clicked. The console stores this exact
108
+ * shape too, so the widget never sees a blank or unbounded chip. Pure
109
+ * validation, no transforms: a value that passes is served verbatim, so each
110
+ * source trims before it stores.
111
+ */
112
+ export const SUGGESTION_LIMITS = { perLocale: 6, titleLength: 120, promptLength: 2000 } as const;
113
+ const trimmedText = (max: number) =>
114
+ z
115
+ .string()
116
+ .min(1)
117
+ .max(max)
118
+ .refine((text) => text === text.trim(), "text has surrounding whitespace");
119
+ const suggestionsSchema = z.record(
120
+ localeSchema,
121
+ z
122
+ .array(
123
+ z.object({
124
+ title: trimmedText(SUGGESTION_LIMITS.titleLength),
125
+ prompt: trimmedText(SUGGESTION_LIMITS.promptLength),
126
+ }),
127
+ )
128
+ .max(SUGGESTION_LIMITS.perLocale),
129
+ ) satisfies z.ZodType<Suggestions>;
130
+
131
+ export const GREETING_MAX_LENGTH = 200;
132
+ /**
133
+ * The blank-chat landing's headline per locale. Empty means the widget's
134
+ * built-in default for that locale; like suggestions, served verbatim.
135
+ */
136
+ const greetingSchema = z.record(
137
+ localeSchema,
138
+ z
139
+ .string()
140
+ .max(GREETING_MAX_LENGTH)
141
+ .refine((text) => text === text.trim(), "greeting has surrounding whitespace"),
142
+ ) satisfies z.ZodType<Greeting>;
143
+
144
+ /**
145
+ * A tool's search tags. The console edits them as comma-separated entries,
146
+ * so no tag holds a comma.
147
+ */
148
+ export const toolTagsSchema = z
149
+ .array(
150
+ z
151
+ .string()
152
+ .trim()
153
+ .min(1)
154
+ .max(60)
155
+ .regex(/^[^,]*$/, "a tag holds no comma"),
156
+ )
157
+ .max(20);
158
+
159
+ /** https, or plain http on loopback for development. */
160
+ const tlsUrlSchema = z.url().refine((url) => canCarrySecret(url, []));
161
+
162
+ /**
163
+ * The origin an embedded tool's page is served from, normalized to a bare
164
+ * origin: it is what the widget compares postMessage events against. The
165
+ * browser dials it, so no host the egress policy admits counts.
166
+ */
167
+ export const embedOriginSchema = tlsUrlSchema.transform((value) => new URL(value).origin);
168
+
169
+ /**
170
+ * SOAPAction becomes a quoted HTTP header value verbatim: quotes,
171
+ * backslashes, and control characters would break the header syntax.
172
+ */
173
+ export const soapActionSchema = z.string().regex(/^[^"\\\u0000-\u001f\u007f]*$/);
174
+
175
+ /** An envelope template: XML, so never only whitespace. */
176
+ const envelopeTemplateSchema = z.string().refine((template) => template.trim().length > 0);
177
+
178
+ /**
179
+ * The per-type payload a tool stores and publishes. Soap: the envelope
180
+ * template is the full request XML with `{{field}}` placeholders bound to
181
+ * scalar input fields plus the reserved `{{token}}`, which `tokenCall`
182
+ * acquires once per request.
183
+ */
184
+ export const toolConfigByType = {
185
+ http: z.object({ responseWrap: z.enum(RESPONSE_WRAPS).optional() }),
186
+ embedded: z.object({ embedOrigin: embedOriginSchema, surface: z.enum(TOOL_SURFACES) }),
187
+ soap: z.object({
188
+ soapVersion: z.enum(SOAP_VERSIONS),
189
+ soapAction: soapActionSchema,
190
+ envelopeTemplate: envelopeTemplateSchema,
191
+ tokenCall: z
192
+ .object({
193
+ envelopeTemplate: envelopeTemplateSchema,
194
+ soapAction: soapActionSchema,
195
+ tokenPath: z.string().trim().min(1),
196
+ })
197
+ .optional(),
198
+ }),
199
+ /** Its server's URL and key are read off the server row on every call. */
200
+ native: z.object({}),
201
+ client: z.object({}),
202
+ };
203
+
204
+ /** A `{name}` parameter in a tool's endpoint URL, filled from the input on each call. */
205
+ export const ENDPOINT_PARAM_PATTERN = /\{([^{}]+)\}/g;
206
+
207
+ /**
208
+ * Whether every `{name}` parameter in a tool's endpoint URL sits in its path
209
+ * or query. One in the scheme, user info, host, port or fragment would let
210
+ * the call's input choose where the request and its credentials go.
211
+ */
212
+ export function confinesEndpointParams(url: string) {
213
+ const origin = /^[^:/?#]*:[/\\]*[^/\\?#]*/.exec(url)?.[0] ?? "";
214
+ const hash = url.indexOf("#", origin.length);
215
+ const fragment = hash < 0 ? "" : url.slice(hash);
216
+ return ![origin, fragment].some((part) => part.match(ENDPOINT_PARAM_PATTERN));
217
+ }
218
+
219
+ /** An upstream Cortex calls: http or https. */
220
+ const upstreamUrlSchema = z.url({ protocol: /^https?$/ });
221
+
222
+ /** An HTTP header field name (RFC 9110 token). */
223
+ export const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
224
+ const headerNameSchema = z.string().trim().min(1).max(120).regex(HEADER_NAME_PATTERN);
225
+
226
+ /** Headers Cortex or fetch fills on every request. Lower-cased. */
227
+ export const transportSetHeaders = ["accept", "content-type", "content-length", "host"] as const;
228
+
229
+ /** Headers fetch refuses to send on anyone's behalf. Lower-cased. */
230
+ export const transportForbiddenHeaders = [
231
+ "connection",
232
+ "transfer-encoding",
233
+ "upgrade",
234
+ "expect",
235
+ "te",
236
+ "trailer",
237
+ "proxy-authorization",
238
+ ] as const;
239
+
240
+ /** Headers the MCP transport sets on its own requests: no MCP server's credential travels under one. Lower-cased. */
241
+ export const mcpTransportHeaders = new Set<string>([
242
+ "mcp-session-id",
243
+ "mcp-protocol-version",
244
+ "last-event-id",
245
+ ]);
246
+
247
+ /** Headers no credential or field may travel under; test lower-cased names. */
248
+ export const transportOwnedHeaders = new Set<string>([
249
+ ...transportSetHeaders,
250
+ ...transportForbiddenHeaders,
251
+ ]);
252
+
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
+ /** The index of each entry presenting a header an earlier entry already presents, compared case-insensitively. */
257
+ export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
258
+ const seen = new Set<string>();
259
+ return entries.flatMap((entry, index) => {
260
+ const name = entry.header.toLowerCase();
261
+ const repeated = seen.has(name);
262
+ seen.add(name);
263
+ return repeated ? [index] : [];
264
+ });
265
+ }
266
+
267
+ /** A server, model provider, credential or secret, by the name Cortex holds it under. */
268
+ const nameSchema = z.string().trim().min(1).max(120);
269
+
270
+ const presentedHeaderSchema = headerNameSchema.refine(
271
+ (name) => !transportOwnedHeaders.has(name.toLowerCase()),
272
+ "a header the transport sets",
273
+ );
274
+
275
+ /**
276
+ * What every call carries: each entry a header, what precedes the value in
277
+ * it (`"Bearer "`, say), and the credential by name. A header is presented
278
+ * once.
279
+ */
280
+ function presentationsSchema<TCredential extends string | null>(
281
+ header: z.ZodString,
282
+ credential: z.ZodType<TCredential, TCredential>,
283
+ ) {
284
+ return z
285
+ .array(z.strictObject({ header, prefix: z.string().max(40).default(""), credential }))
286
+ .max(CREDENTIAL_PRESENTATION_LIMIT)
287
+ .superRefine((entries, context) => {
288
+ for (const index of repeatedHeaders(entries)) {
289
+ context.addIssue({
290
+ code: "custom",
291
+ path: [index, "header"],
292
+ message: "a header presented twice",
293
+ });
294
+ }
295
+ })
296
+ .default([]);
297
+ }
298
+
299
+ /** 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 = presentationsSchema(presentedHeaderSchema, nameSchema.nullable());
301
+
302
+ /**
303
+ * Where Cortex fills a top-level scalar input field from, out of the model's
304
+ * hands: one of the variables the agent's server declares, a stored secret,
305
+ * a constant, or the end user's bearer.
306
+ */
307
+ export const fieldSourceSchema = z.discriminatedUnion("kind", [
308
+ z.object({ kind: z.literal("variable"), name: z.string().trim().min(1).max(64) }),
309
+ z.object({ kind: z.literal("secret"), name: z.string().trim().min(1).max(120) }),
310
+ z.object({
311
+ kind: z.literal("constant"),
312
+ value: z.union([z.string().max(4_000), z.number(), z.boolean()]),
313
+ }),
314
+ z.object({ kind: z.literal("endUserToken") }),
315
+ ]);
316
+ export type FieldSource = z.infer<typeof fieldSourceSchema>;
317
+
318
+ /**
319
+ * Whether `source` can fill a field of `type` (and `enum`, for a string
320
+ * listing its values): a stored secret or the end user's token fills a
321
+ * string only, and a constant must be a value of the field's type. Whether a
322
+ * variable is declared is the agent's server's to say.
323
+ */
324
+ export function sourceFits(
325
+ source: FieldSource,
326
+ field: { type: unknown; enum?: ReadonlyArray<string | null> },
327
+ ) {
328
+ switch (source.kind) {
329
+ case "variable":
330
+ return true;
331
+ case "secret":
332
+ case "endUserToken":
333
+ return field.type === "string";
334
+ case "constant": {
335
+ const { value } = source;
336
+ if (field.enum) return typeof value === "string" && field.enum.includes(value);
337
+ if (field.type === "boolean") return typeof value === "boolean";
338
+ if (field.type === "number") return typeof value === "number" && Number.isFinite(value);
339
+ if (field.type === "integer") return Number.isInteger(value);
340
+ return field.type === "string" && typeof value === "string";
341
+ }
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Where a top-level scalar input field travels in the upstream request;
347
+ * absent, path placeholders by name, then the query string on GET and the
348
+ * body otherwise. `name` renames a query parameter, header or cookie on the
349
+ * wire.
350
+ */
351
+ export const fieldTargetSchema = z.discriminatedUnion("kind", [
352
+ z.object({ kind: z.literal("body") }),
353
+ z.object({ kind: z.literal("query"), name: z.string().trim().min(1).max(120).optional() }),
354
+ z.object({ kind: z.literal("path") }),
355
+ z.object({ kind: z.literal("header"), name: headerNameSchema.optional() }),
356
+ z.object({ kind: z.literal("cookie"), name: headerNameSchema.optional() }),
357
+ ]);
358
+ export type FieldTarget = z.infer<typeof fieldTargetSchema>;
359
+
360
+ const SCALAR_TYPES = ["string", "number", "integer", "boolean"] as const;
361
+ const schemaTypeSchema = z.enum([...SCALAR_TYPES, "object", "array"]);
362
+
363
+ /** Untrimmed: two names that differ only in surrounding spaces would collapse into one. */
364
+ const propertyNameSchema = z
365
+ .string()
366
+ .min(1)
367
+ .max(120)
368
+ .refine((name) => name === name.trim(), "no surrounding spaces");
369
+
370
+ /** What an array holds: a scalar, or an object with properties of its own. */
371
+ const itemsSchema = z.strictObject({
372
+ type: z.enum([...SCALAR_TYPES, "object"]),
373
+ get properties() {
374
+ return z.record(propertyNameSchema, schemaPropertySchema).optional();
375
+ },
376
+ required: z.array(z.string()).optional(),
377
+ });
378
+
379
+ /**
380
+ * A JSON Schema property, in the part of JSON Schema a field editor holds:
381
+ * 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 `required`, an
383
+ * array's `items`, a `description`, and the extensions `x-sensitive`
384
+ * (redacted from logs), `x-source` and `x-target` (top-level scalar fields
385
+ * of a tool's input only).
386
+ */
387
+ const schemaPropertySchema = z.strictObject({
388
+ type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
389
+ description: z.string().trim().max(500).optional(),
390
+ /** A nullable enum lists `null` beside its values, as the console stores one. */
391
+ enum: z
392
+ .array(z.union([z.string().trim().min(1), z.null()]))
393
+ .min(1)
394
+ .max(51)
395
+ .optional(),
396
+ get properties() {
397
+ return z.record(propertyNameSchema, schemaPropertySchema).optional();
398
+ },
399
+ required: z.array(z.string()).optional(),
400
+ get items() {
401
+ return itemsSchema.optional();
402
+ },
403
+ "x-sensitive": z.boolean().optional(),
404
+ "x-source": fieldSourceSchema.optional(),
405
+ "x-target": fieldTargetSchema.optional(),
406
+ });
407
+ type SchemaProperty = z.infer<typeof schemaPropertySchema>;
408
+
409
+ /** A node's own type, without the `"null"` beside it. */
410
+ function typeOf(node: { type: SchemaProperty["type"] }) {
411
+ return [node.type].flat()[0];
412
+ }
413
+
414
+ /** A refusal below a node: where, relative to it, and why. */
415
+ type RowIssue = { at: Array<string | number>; message: string };
416
+
417
+ /** What a node's own keywords break: `properties` or `required` off an object, or `required` naming no property. */
418
+ function nodeIssues(node: {
419
+ type: SchemaProperty["type"];
420
+ properties?: Record<string, SchemaProperty>;
421
+ required?: string[];
422
+ }) {
423
+ const issues = new Array<RowIssue>();
424
+ const object = typeOf(node) === "object";
425
+ for (const keyword of ["properties", "required"] as const) {
426
+ if (!object && node[keyword] !== undefined) {
427
+ issues.push({ at: [keyword], message: "only an object has it" });
428
+ }
429
+ }
430
+ node.required?.forEach((name, index) => {
431
+ if (!Object.hasOwn(node.properties ?? {}, name)) {
432
+ issues.push({ at: ["required", index], message: "names no property" });
433
+ }
434
+ });
435
+ return issues;
436
+ }
437
+
438
+ /**
439
+ * What an `enum` breaks, if anything: it lists a string's values, 1 to 50 of
440
+ * them, beside `null` exactly when the type allows null, as the console
441
+ * stores a nullable one.
442
+ */
443
+ function enumIssueOf(property: SchemaProperty, type: string | undefined) {
444
+ if (property.enum === undefined) return undefined;
445
+ if (type !== "string") return "an enum lists a string's values";
446
+ const values = property.enum.filter((value) => value !== null);
447
+ if (values.length === 0 || values.length > 50) return "an enum lists 1 to 50 values";
448
+ const nullable = Array.isArray(property.type);
449
+ if (nullable === property.enum.includes(null)) return undefined;
450
+ return nullable ? "a nullable enum lists null" : "only a nullable enum lists null";
451
+ }
452
+
453
+ /**
454
+ * What one property breaks of itself: an `enum` off a string, with no value,
455
+ * 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 `x-source` or
457
+ * `x-target` off a scalar of a `bindable` node, or a source that does not
458
+ * fit the field.
459
+ */
460
+ function propertyIssues(property: SchemaProperty, bindable: boolean) {
461
+ const issues = new Array<RowIssue>();
462
+ const type = typeOf(property);
463
+ const enumIssue = enumIssueOf(property, type);
464
+ if (enumIssue) issues.push({ at: ["enum"], message: enumIssue });
465
+ if ((property.items === undefined) === (type === "array")) {
466
+ issues.push({
467
+ at: ["items"],
468
+ message: type === "array" ? "an array declares its items" : "only an array has it",
469
+ });
470
+ }
471
+ const binds = bindable && SCALAR_TYPES.some((scalar) => scalar === type);
472
+ for (const binding of ["x-source", "x-target"] as const) {
473
+ if (property[binding] !== undefined && !binds) {
474
+ issues.push({
475
+ at: [binding],
476
+ message: "only a top-level scalar field of a tool's input has it",
477
+ });
478
+ }
479
+ }
480
+ const source = property["x-source"];
481
+ if (binds && source && !sourceFits(source, { type, enum: property.enum })) {
482
+ issues.push({ at: ["x-source"], message: "the source does not fit the field's type" });
483
+ }
484
+ return issues;
485
+ }
486
+
487
+ /**
488
+ * Refuses, at every depth below `path`, what the console's field rows cannot
489
+ * hold, since it stores a schema as rows and would drop or change it in
490
+ * silence (`nodeIssues`, `propertyIssues`); `x-source` and `x-target` only
491
+ * on a scalar directly under a node where `bindable`.
492
+ */
493
+ function heldByRows(
494
+ node: {
495
+ type: SchemaProperty["type"];
496
+ properties?: Record<string, SchemaProperty>;
497
+ required?: string[];
498
+ },
499
+ context: z.RefinementCtx,
500
+ path: Array<string | number>,
501
+ bindable: boolean,
502
+ ) {
503
+ const report = (under: Array<string | number>, issues: ReadonlyArray<RowIssue>) => {
504
+ for (const { at, message } of issues) {
505
+ context.addIssue({ code: "custom", path: [...under, ...at], message });
506
+ }
507
+ };
508
+ report(path, nodeIssues(node));
509
+ for (const [name, property] of Object.entries(node.properties ?? {})) {
510
+ const at = [...path, "properties", name];
511
+ report(at, propertyIssues(property, bindable));
512
+ heldByRows(property, context, at, false);
513
+ if (property.items) heldByRows(property.items, context, [...at, "items"], false);
514
+ }
515
+ }
516
+
517
+ /**
518
+ * An object's JSON Schema, the shape of a tool's or trigger's input and
519
+ * output. `bindable` admits `x-source` and `x-target` on its top-level
520
+ * scalar fields, as a tool's input has them.
521
+ */
522
+ function objectSchemaOf(bindable: boolean) {
523
+ return z
524
+ .strictObject({
525
+ type: z.literal("object"),
526
+ properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
527
+ required: z.array(z.string()).optional(),
528
+ })
529
+ .superRefine((schema, context) => heldByRows(schema, context, [], bindable));
530
+ }
531
+ const objectSchema = objectSchemaOf(false);
532
+
533
+ const EMPTY_OBJECT = { type: "object" } as const satisfies z.input<typeof objectSchema>;
534
+
535
+ /** Who a component reaches: every agent, every agent on one server, or the agents listed by slug. */
536
+ const audienceSchema = z.union([
537
+ z.literal("all"),
538
+ z.strictObject({ server: nameSchema }),
539
+ z.strictObject({
540
+ agents: z
541
+ .array(agentSlugSchema)
542
+ .min(1)
543
+ .refine((slugs) => new Set(slugs).size === slugs.length, "an agent repeats"),
544
+ }),
545
+ ]);
546
+
547
+ /** A model a provider lists, and the reasoning effort sent with it. */
548
+ const modelSchema = z.strictObject({
549
+ provider: nameSchema,
550
+ model: z.string().trim().min(1).max(200),
551
+ effort: z.string().trim().min(1).max(64).optional(),
552
+ });
553
+
554
+ /** One line per locale, trimmed. */
555
+ const localizedLineSchema = z.record(localeSchema, z.string().trim().min(1).max(120));
556
+ const localizedTextSchema = z.record(localeSchema, z.string().trim().min(1).max(4_000));
557
+
558
+ const { tool: toolBounds } = SETTING_BOUNDS;
559
+
560
+ /** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, credentials, approval or bound fields. */
561
+ const toolBaseSchema = z.strictObject({
562
+ ...serverToolSchema.pick({ name: true, description: true }).shape,
563
+ inputSchema: objectSchema.default(EMPTY_OBJECT),
564
+ outputSchema: objectSchema.default(EMPTY_OBJECT),
565
+ timeoutMs: setting(toolBounds.timeoutMs),
566
+ maxResponseBytes: setting(toolBounds.maxResponseBytes),
567
+ rateLimitRequests: setting(toolBounds.rateLimitRequests),
568
+ rateLimitWindowSeconds: setting(toolBounds.rateLimitWindowSeconds),
569
+ /** Trims the upstream's answer to what the output schema declares. */
570
+ projectOutput: z.boolean().default(false),
571
+ /** Listed in every turn of every agent the tool reaches, whatever the question. */
572
+ pinned: z.boolean().default(false),
573
+ tags: toolTagsSchema.default([]),
574
+ audience: audienceSchema,
575
+ });
576
+
577
+ /** A tool that calls an endpoint; with `requiresApproval`, every call waits for the end user to approve it. */
578
+ const calledToolSchema = toolBaseSchema.extend({
579
+ inputSchema: objectSchemaOf(true).default(EMPTY_OBJECT),
580
+ url: upstreamUrlSchema.refine(
581
+ confinesEndpointParams,
582
+ "a {parameter} outside the path or query",
583
+ ),
584
+ credentials: credentialsSchema,
585
+ requiresApproval: z.boolean().default(false),
586
+ });
587
+
588
+ const manifestToolSchema = z
589
+ .discriminatedUnion("type", [
590
+ calledToolSchema.extend({
591
+ type: z.literal("http"),
592
+ method: z.enum(TOOL_METHODS),
593
+ readOnly: z.boolean(),
594
+ ...toolConfigByType.http.shape,
595
+ }),
596
+ /** The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it. */
597
+ calledToolSchema.extend({
598
+ type: z.literal("embedded"),
599
+ method: z.enum(TOOL_METHODS),
600
+ ...toolConfigByType.embedded.shape,
601
+ }),
602
+ /** Always POSTed. */
603
+ calledToolSchema.extend({
604
+ type: z.literal("soap"),
605
+ readOnly: z.boolean(),
606
+ ...toolConfigByType.soap.shape,
607
+ }),
608
+ /** Answered by your app: the model calls it and the widget hands the call over. */
609
+ toolBaseSchema.extend({ type: z.literal("client") }),
610
+ ])
611
+ .superRefine((tool, context) => {
612
+ if (tool.type !== "client") refuseBlockedApprovalTitle(tool, context, []);
613
+ });
614
+ export type ManifestTool = z.input<typeof manifestToolSchema>;
615
+
616
+ const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]);
617
+
618
+ /**
619
+ * An OpenAPI document whose operations become HTTP tools this manifest owns.
620
+ * Each apply reads it, imports every operation Cortex supports, keeps those
621
+ * tools in step with the document and deletes the ones it no longer has.
622
+ * Every tool presents `credentials`. A GET call runs at once; any other
623
+ * method waits for the end user's approval, unless `approval` names the
624
+ * tool the other way.
625
+ */
626
+ const manifestOpenApiSchema = z.strictObject({
627
+ /** Names the source in what an apply reports. */
628
+ name: nameSchema,
629
+ /** Where the document is read, with no credentials: OpenAPI 3 or Swagger 2, JSON or YAML. */
630
+ url: upstreamUrlSchema,
631
+ credentials: credentialsSchema,
632
+ /** Tools listed in every turn of every agent they reach, whatever the question. */
633
+ pinned: toolNamesSchema,
634
+ approval: z
635
+ .strictObject({ never: toolNamesSchema, always: toolNamesSchema })
636
+ .prefault({})
637
+ .refine(
638
+ ({ never, always }) => !never.some((name) => always.includes(name)),
639
+ "a tool both never and always needs approval",
640
+ ),
641
+ audience: audienceSchema,
642
+ });
643
+ export type ManifestOpenApi = z.input<typeof manifestOpenApiSchema>;
644
+
645
+ /** Identified by its `key`, which skills and collections scope themselves by; `name` is its label. */
646
+ const manifestServiceSchema = z.strictObject({
647
+ /** What consumers parameterize their tools with; fixed once published. */
648
+ key: slugSchema,
649
+ name: localizedLineSchema,
650
+ /** Rich text: it may mention tools and skills. */
651
+ description: localizedTextSchema,
652
+ audience: audienceSchema,
653
+ });
654
+ export type ManifestService = z.input<typeof manifestServiceSchema>;
655
+
656
+ const manifestSkillSchema = z.strictObject({
657
+ name: slugSchema,
658
+ description: z.string().trim().min(1).max(500),
659
+ /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}`. */
660
+ body: z.string().trim().min(1),
661
+ /** The key of the service the skill renders inside; fixed once created. */
662
+ service: slugSchema.optional(),
663
+ audience: audienceSchema,
664
+ });
665
+ export type ManifestSkill = z.input<typeof manifestSkillSchema>;
666
+
667
+ /**
668
+ * Which thread a firing lands in. `single`: one thread for the trigger.
669
+ * `per-firing`: a fresh thread every time. `ephemeral`: a fresh thread that
670
+ * is deleted once the run completes, for one-shot calls: the answer reaches
671
+ * the caller as the firing's output or through the live stream, and nothing
672
+ * else outlives the run; a failed run keeps its thread for inspection. `keyed`:
673
+ * one thread per distinct value at a JSON pointer into the payload (e.g.
674
+ * `/pull_request/number`).
675
+ */
676
+ export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
677
+ z.object({ policy: z.literal("single") }),
678
+ z.object({ policy: z.literal("per-firing") }),
679
+ z.object({ policy: z.literal("ephemeral") }),
680
+ z.object({ policy: z.literal("keyed"), key: z.string().regex(/^\//).max(256) }),
681
+ ]);
682
+ export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
683
+
684
+ /** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
685
+ const manifestTriggerSchema = z.strictObject({
686
+ /** `message` is the built-in trigger every agent has. */
687
+ name: slugSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
688
+ description: z.string().trim().min(1).max(500),
689
+ inputSchema: objectSchema.default(EMPTY_OBJECT),
690
+ /** The shape the run answers in; absent, the run answers in free text. */
691
+ outputSchema: objectSchema
692
+ .refine(
693
+ (schema) => Object.keys(schema.properties ?? {}).length > 0,
694
+ "declares a property; leave it out for a free-text answer",
695
+ )
696
+ .optional(),
697
+ /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
698
+ instructions: z.string().trim().min(1).max(20_000),
699
+ threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
700
+ /** Firings a day per agent; absent, no limit. */
701
+ budgetPerDay: z.number().int().positive().optional(),
702
+ /** Consecutive failed or rejected firings before the trigger disables itself; it stays off until an operator enables it. */
703
+ disableAfterFailures: setting(SETTING_BOUNDS.trigger.disableAfterFailures),
704
+ /** The credential the run acts with, presented wherever the end user's token would go. */
705
+ credential: nameSchema.optional(),
706
+ /** A static credential whose value callers fire with, in place of a minted secret. */
707
+ fireCredential: nameSchema.optional(),
708
+ /** The model its runs talk to, over each agent's. */
709
+ model: modelSchema.optional(),
710
+ audience: audienceSchema,
711
+ });
712
+ export type ManifestTrigger = z.input<typeof manifestTriggerSchema>;
713
+
714
+ /** The datasets `pinned` lists that `expose` does not: a host pins only what it exposes. */
715
+ export function unexposedPins(host: {
716
+ expose: ReadonlyArray<string>;
717
+ pinned: ReadonlyArray<string>;
718
+ }) {
719
+ const exposed = new Set(host.expose);
720
+ return host.pinned.filter((name) => !exposed.has(name));
721
+ }
722
+
723
+ const manifestDatasetHostSchema = z
724
+ .strictObject({
725
+ name: datasetIdentifierSchema,
726
+ url: upstreamUrlSchema,
727
+ credentials: credentialsSchema,
728
+ timeoutMs: setting(SETTING_BOUNDS.datasetHost.timeoutMs),
729
+ /** The datasets the host serves that agents may query. */
730
+ expose: z.array(datasetIdentifierSchema).min(1).max(500),
731
+ /** Among those, the ones listed in every turn, whatever the question. */
732
+ pinned: z.array(datasetIdentifierSchema).max(50).default([]),
733
+ audience: audienceSchema,
734
+ })
735
+ .superRefine((host, context) => {
736
+ const unexposed = unexposedPins(host);
737
+ if (unexposed.length > 0) {
738
+ context.addIssue({
739
+ code: "custom",
740
+ path: ["pinned"],
741
+ message: `not exposed: ${unexposed.join(", ")}`,
742
+ });
743
+ }
744
+ });
745
+ export type ManifestDatasetHost = z.input<typeof manifestDatasetHostSchema>;
746
+
747
+ /** An MCP server's own tool names. The console edits the list as comma-separated entries, so no name holds a comma. */
748
+ export const mcpToolNamesSchema = z
749
+ .array(
750
+ z
751
+ .string()
752
+ .trim()
753
+ .min(1)
754
+ .max(128)
755
+ .regex(/^[^,]*$/, "a tool name holds no comma"),
756
+ )
757
+ .max(200);
758
+
759
+ const manifestMcpServerSchema = z.strictObject({
760
+ /** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
761
+ name: z.string().regex(IDENTIFIER_PATTERN).max(64),
762
+ url: upstreamUrlSchema,
763
+ credentials: presentationsSchema(
764
+ presentedHeaderSchema.refine(
765
+ (name) => !mcpTransportHeaders.has(name.toLowerCase()),
766
+ "a header the MCP transport sets",
767
+ ),
768
+ nameSchema.nullable(),
769
+ ),
770
+ /** The server's own tool names to offer; empty offers every tool. */
771
+ include: mcpToolNamesSchema.default([]),
772
+ exclude: mcpToolNamesSchema.default([]),
773
+ audience: audienceSchema,
774
+ });
775
+ export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
776
+
777
+ /**
778
+ * Delivers its event to a receiver while it is declared. A delivery carries
779
+ * no run, so every credential it presents is named, and none under the
780
+ * header that names the event.
781
+ */
782
+ const manifestWebhookSchema = z.strictObject({
783
+ name: slugSchema,
784
+ description: z.string().trim().max(500).default(""),
785
+ event: z.enum(SERVER_EVENTS),
786
+ url: upstreamUrlSchema,
787
+ credentials: presentationsSchema(
788
+ presentedHeaderSchema.refine(
789
+ (name) => name.toLowerCase() !== EVENT_HEADER.toLowerCase(),
790
+ "the header that names the event",
791
+ ),
792
+ nameSchema,
793
+ ),
794
+ timeoutMs: setting(SETTING_BOUNDS.webhook.timeoutMs),
795
+ audience: audienceSchema,
796
+ });
797
+ export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
798
+
799
+ /** Holds documents, which are uploaded to it apart from the manifest. */
800
+ const manifestCollectionSchema = z.strictObject({
801
+ name: z.string().trim().min(1).max(80),
802
+ description: z.string().trim().max(2_000).default(""),
803
+ /** The key of the service every document in the collection belongs to. */
804
+ service: slugSchema.optional(),
805
+ audience: audienceSchema,
806
+ });
807
+ export type ManifestCollection = z.input<typeof manifestCollectionSchema>;
808
+
809
+ export const AUTH_JWT_SECRET_ALGORITHMS = ["HS256", "HS384", "HS512"] as const;
810
+
811
+ const issuerSchema = z.string().trim().min(1).max(2048);
812
+ /** When set, the token's `aud` must contain it, so a token minted for another audience under the same issuer cannot be replayed here. */
813
+ const tokenAudienceSchema = z.string().trim().min(1).max(2048).optional();
814
+
815
+ /**
816
+ * How the agent verifies end-user tokens: against the keys its issuer
817
+ * publishes at a JWKS URI, with a shared secret named in Cortex's secret
818
+ * store and the one algorithm tokens are signed with, through its server's
819
+ * `/verify`, or not at all.
820
+ */
821
+ const agentAuthSchema = z.discriminatedUnion("kind", [
822
+ z.strictObject({
823
+ kind: z.literal("jwks"),
824
+ jwksUri: tlsUrlSchema,
825
+ issuer: issuerSchema,
826
+ audience: tokenAudienceSchema,
827
+ }),
828
+ z.strictObject({
829
+ kind: z.literal("jwt-secret"),
830
+ secret: nameSchema,
831
+ algorithm: z.enum(AUTH_JWT_SECRET_ALGORITHMS),
832
+ issuer: issuerSchema,
833
+ audience: tokenAudienceSchema,
834
+ }),
835
+ z.strictObject({ kind: z.literal("anonymous") }),
836
+ z.strictObject({ kind: z.literal("server") }),
837
+ ]);
838
+
839
+ const { agent: agentBounds, agentContext, agentLimits } = SETTING_BOUNDS;
840
+
841
+ /** How the agent runs; every setting left out keeps its default. */
842
+ const agentSettingsSchema = z.strictObject({
843
+ /** Offers `searchKnowledge` over the collections the agent sees. */
844
+ ragEnabled: z.boolean().default(false),
845
+ /** Offers `searchTools` over the tools the agent sees, beyond the pinned and mentioned ones. */
846
+ toolRegistryEnabled: z.boolean().default(false),
847
+ serviceRegistryEnabled: z.boolean().default(false),
848
+ attachmentsEnabled: z.boolean().default(true),
849
+ /** Hosts the built-in `fetchImage` tool may pull images from; none, no such tool. */
850
+ fetchImageHostnames: z
851
+ .array(z.string().max(253).regex(HOSTNAME_PATTERN))
852
+ .max(FETCH_IMAGE_HOSTS_LIMIT)
853
+ .default([]),
854
+ locationPicker: z.boolean().default(false),
855
+ /** The fenced media forms the agent may draw, which its prompt then describes. */
856
+ media: z.array(z.enum(MEDIA_FORMS)).default([...DEFAULT_MEDIA_FORMS]),
857
+ /** Off, end users cannot message the agent: only its triggers reach it. */
858
+ messageTrigger: z.boolean().default(true),
859
+ defaultLocale: localeSchema.default("en"),
860
+ rateLimitRequests: setting(agentBounds.rateLimitRequests),
861
+ rateLimitWindowSeconds: setting(agentBounds.rateLimitWindowSeconds),
862
+ dailyExecutionQuota: setting(agentBounds.dailyExecutionQuota),
863
+ context: z
864
+ .strictObject({
865
+ maxContextTokens: setting(agentContext.maxContextTokens),
866
+ summarizationThreshold: z
867
+ .number()
868
+ .min(agentContext.summarizationThreshold.min)
869
+ .max(agentContext.summarizationThreshold.max)
870
+ .default(agentContext.summarizationThreshold.default),
871
+ toolResultMaxTokens: setting(agentContext.toolResultMaxTokens),
872
+ recentMessagesToKeep: setting(agentContext.recentMessagesToKeep),
873
+ })
874
+ .prefault({}),
875
+ limits: z
876
+ .strictObject({
877
+ resolveTokenBudget: setting(agentLimits.resolveTokenBudget),
878
+ executeTimeoutMs: setting(agentLimits.executeTimeoutMs),
879
+ maxResponseBytes: setting(agentLimits.maxResponseBytes),
880
+ })
881
+ .prefault({}),
882
+ });
883
+
884
+ const manifestAgentSchema = z.strictObject({
885
+ slug: agentSlugSchema,
886
+ name: z.string().trim().min(1).max(120),
887
+ /** The server it belongs to, by name; a manifest never creates one. */
888
+ server: nameSchema,
889
+ /** Rich text: it may mention tools and skills, and read `{{variables}}`. */
890
+ prompt: z.string().trim().min(1),
891
+ auth: agentAuthSchema,
892
+ /**
893
+ * `chat` left out runs on the server's pick; `vision` and
894
+ * `summarization` left out use none of their own.
895
+ */
896
+ models: z
897
+ .strictObject({
898
+ chat: modelSchema.optional(),
899
+ vision: modelSchema.omit({ effort: true }).optional(),
900
+ summarization: modelSchema.omit({ effort: true }).optional(),
901
+ })
902
+ .default({}),
903
+ settings: agentSettingsSchema.prefault({}),
904
+ suggestions: suggestionsSchema.default({ en: [], ar: [] }),
905
+ greeting: greetingSchema.default({ en: "", ar: "" }),
906
+ });
907
+ export type ManifestAgent = z.input<typeof manifestAgentSchema>;
908
+
909
+ /** Every name in a list once, so one entry means one row. */
910
+ function uniqueBy<TEntry>(key: (entry: TEntry) => string) {
911
+ return (entries: ReadonlyArray<TEntry>) => new Set(entries.map(key)).size === entries.length;
912
+ }
913
+
914
+ function listOf<TEntry extends z.ZodType>(entry: TEntry, key: (value: z.output<TEntry>) => string) {
915
+ return z.array(entry).refine(uniqueBy(key), "a name repeats").default([]);
916
+ }
917
+
918
+ const byName = (entry: { name: string }) => entry.name;
919
+
920
+ /** The id of Cortex's own manifest, which only Cortex applies: the management API refuses any other under it. */
921
+ export const CORTEX_MANIFEST_ID = "cortex";
922
+
923
+ /**
924
+ * A deployment's agents and components, owned by the manifest `id`: an apply
925
+ * of it creates what is new, updates what changed, and deletes what it no
926
+ * longer lists, touching nothing it did not create.
927
+ */
928
+ export const manifestSchema = z.strictObject({
929
+ /** Fixed for the deployment it describes: the rows an apply owns carry it. */
930
+ id: slugSchema,
931
+ agents: listOf(manifestAgentSchema, (agent) => agent.slug),
932
+ tools: listOf(manifestToolSchema, byName),
933
+ openapi: listOf(manifestOpenApiSchema, byName),
934
+ services: listOf(manifestServiceSchema, (service) => service.key),
935
+ skills: listOf(manifestSkillSchema, byName),
936
+ triggers: listOf(manifestTriggerSchema, byName),
937
+ datasetHosts: listOf(manifestDatasetHostSchema, byName),
938
+ mcpServers: listOf(manifestMcpServerSchema, byName),
939
+ webhooks: listOf(manifestWebhookSchema, byName),
940
+ collections: listOf(manifestCollectionSchema, byName),
941
+ });
942
+ /** A manifest as written: every field with a default may be left out. */
943
+ export type Manifest = z.input<typeof manifestSchema>;
944
+
945
+ /**
946
+ * Entry names per kind: agents by slug, services by key, everything else by
947
+ * name. An OpenAPI source's operations are counted among `tools`.
948
+ */
949
+ const namesByKindSchema = z.record(
950
+ manifestSchema.keyof().exclude(["id", "openapi"]),
951
+ z.array(z.string()),
952
+ );
953
+
954
+ /** What an apply creates, updates and deletes, kind by kind; a preview answers the same without doing it. */
955
+ export const manifestChangesSchema = z.object({
956
+ create: namesByKindSchema,
957
+ update: namesByKindSchema,
958
+ delete: namesByKindSchema,
959
+ });
960
+
961
+ /**
962
+ * An apply's changes, and a fire secret, shown this once, for each trigger
963
+ * it left with none anyone holds: one it created, or one whose
964
+ * `fireCredential` it dropped.
965
+ */
966
+ export const appliedManifestSchema = manifestChangesSchema.extend({
967
+ triggerSecrets: z.record(z.string(), z.string()),
968
+ });
969
+
970
+ /** Types a manifest as it is written; `cortex apply` validates and sends it. */
971
+ export function defineManifest(manifest: Manifest) {
972
+ return manifest;
973
+ }