@m6d/cortex-cli 0.20.0 → 0.21.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.
@@ -2,10 +2,11 @@ import { z } from "zod";
2
2
  import { datasetIdentifierSchema } from "../datasets/index";
3
3
  import { canCarrySecret } from "../egress-policy/index";
4
4
  import {
5
- agentSlugSchema,
5
+ displayNameSchema,
6
6
  EVENT_HEADER,
7
7
  identifierSchema,
8
8
  localeSchema,
9
+ nameSchema,
9
10
  SERVER_EVENTS,
10
11
  refuseBlockedApprovalTitle,
11
12
  serverToolSchema,
@@ -25,7 +26,7 @@ import {
25
26
  * A manifest: the agents and components a code-first deployment declares,
26
27
  * written by hand (`cortex.manifest.ts` or JSON) and applied in one
27
28
  * transaction. Everything it points at, it names: a server, a model
28
- * provider, a service by its key, an agent by its slug, and a stored secret
29
+ * provider, a service, an agent, and a stored secret
29
30
  * or credential by the noun of what it names (`secret: "signing-key"`,
30
31
  * `credential: "petstore"`). A tool input field's `x-source` is the one
31
32
  * exception: `inputSchema` is the JSON Schema the tool stores, verbatim, so
@@ -187,11 +188,6 @@ function overridable<const TField extends string>(fields: readonly [TField, ...T
187
188
  .default([]);
188
189
  }
189
190
 
190
- /** Consumer-facing slugs: the manifest id, service keys, skill names and trigger names share it. */
191
- export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
192
- export const SLUG_MAX_LENGTH = 64;
193
- const slugSchema = z.string().regex(SLUG_PATTERN).max(SLUG_MAX_LENGTH);
194
-
195
191
  /** A DNS host name in lower case, the way `URL` reports one: dot-separated labels of letters, digits and inner hyphens. */
196
192
  export const HOSTNAME_PATTERN =
197
193
  /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/;
@@ -477,11 +473,6 @@ function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
477
473
  });
478
474
  }
479
475
 
480
- /** The longest name Cortex holds a server, model provider, credential or secret under. */
481
- export const NAME_MAX_LENGTH = 120;
482
- /** A server, model provider, credential or secret, by the name Cortex holds it under. */
483
- const nameSchema = z.string().trim().min(1).max(NAME_MAX_LENGTH);
484
-
485
476
  const presentedHeaderSchema = headerNameSchema.refine(
486
477
  (name) => !transportOwnedHeaders.has(name.toLowerCase()),
487
478
  "a header the transport sets",
@@ -518,7 +509,10 @@ const variableSourceSchema = z.object({
518
509
  });
519
510
 
520
511
  /** A credential Cortex holds or acquires, by name. */
521
- const credentialSourceSchema = z.object({ kind: z.literal("credential"), name: nameSchema });
512
+ const credentialSourceSchema = z.object({
513
+ kind: z.literal("credential"),
514
+ name: nameSchema,
515
+ });
522
516
 
523
517
  /** A value written as is. */
524
518
  const constantHeaderSourceSchema = z.object({
@@ -883,15 +877,15 @@ const objectSchema = objectJsonSchemaOf();
883
877
 
884
878
  const EMPTY_OBJECT = { type: "object" } as const satisfies z.input<typeof objectSchema>;
885
879
 
886
- /** Who a component reaches: every agent, every agent on one server, or the agents listed by slug. */
880
+ /** Who a component reaches: every agent, every agent on one server, or the agents listed by name. */
887
881
  const audienceSchema = z.union([
888
882
  z.literal("all"),
889
883
  z.strictObject({ server: nameSchema }),
890
884
  z.strictObject({
891
885
  agents: z
892
- .array(agentSlugSchema)
886
+ .array(nameSchema)
893
887
  .min(1)
894
- .refine((slugs) => new Set(slugs).size === slugs.length, "an agent repeats"),
888
+ .refine((names) => new Set(names).size === names.length, "an agent repeats"),
895
889
  }),
896
890
  ]);
897
891
 
@@ -905,14 +899,9 @@ const modelSchema = z.strictObject({
905
899
  effort: z.string().trim().min(1).max(64).optional(),
906
900
  });
907
901
 
908
- /** A service's name and description, in characters per locale. The console caps its editor at the same lengths. */
909
- export const SERVICE_LIMITS = { nameLength: 120, descriptionLength: 4_000 } as const;
902
+ /** A service's description, in characters per locale. The console caps its editor at the same length. */
903
+ export const SERVICE_LIMITS = { descriptionLength: 4_000 } as const;
910
904
 
911
- /** One line per locale, trimmed. */
912
- const localizedLineSchema = z.record(
913
- localeSchema,
914
- z.string().trim().min(1).max(SERVICE_LIMITS.nameLength),
915
- );
916
905
  const localizedTextSchema = z.record(
917
906
  localeSchema,
918
907
  z.string().trim().min(1).max(SERVICE_LIMITS.descriptionLength),
@@ -932,6 +921,7 @@ function toolInputSchemaOf(type: (typeof TOOL_TYPE_NAMES)[number]) {
932
921
 
933
922
  /** What a live write may change on a manifest's tool, and keep through the next apply. */
934
923
  const TOOL_OVERRIDABLES = [
924
+ "displayName",
935
925
  "description",
936
926
  "timeoutMs",
937
927
  "maxResponseBytes",
@@ -946,6 +936,7 @@ const CALLED_TOOL_OVERRIDABLES = [...TOOL_OVERRIDABLES, "headers"] as const;
946
936
  /** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, headers, approval or bound fields. */
947
937
  const toolBaseSchema = z.strictObject({
948
938
  ...serverToolSchema.pick({ name: true, description: true }).shape,
939
+ displayName: displayNameSchema.default({}),
949
940
  inputSchema: toolInputSchemaOf("client"),
950
941
  outputSchema: objectSchema.default(EMPTY_OBJECT),
951
942
  timeoutMs: setting(toolBounds.timeoutMs),
@@ -1050,15 +1041,15 @@ const manifestOpenApiSchema = z.strictObject({
1050
1041
  });
1051
1042
  export type ManifestOpenApi = z.input<typeof manifestOpenApiSchema>;
1052
1043
 
1053
- /** Identified by its `key`, which skills and collections scope themselves by; `name` is its label. */
1044
+ /** Skills and collections scope themselves by a service's `name`. */
1054
1045
  const manifestServiceSchema = z.strictObject({
1055
- /** What consumers parameterize their tools with; fixed once published. */
1056
- key: slugSchema,
1057
- name: localizedLineSchema,
1046
+ /** What consumers parameterize their tools with. */
1047
+ name: nameSchema,
1048
+ displayName: displayNameSchema.default({}),
1058
1049
  /** Rich text: it may mention tools and skills. */
1059
1050
  description: localizedTextSchema,
1060
1051
  audience: audienceSchema,
1061
- overridable: overridable(["description"]),
1052
+ overridable: overridable(["displayName", "description"]),
1062
1053
  });
1063
1054
  export type ManifestService = z.input<typeof manifestServiceSchema>;
1064
1055
 
@@ -1066,14 +1057,15 @@ export type ManifestService = z.input<typeof manifestServiceSchema>;
1066
1057
  export const SKILL_LIMITS = { descriptionLength: 500 } as const;
1067
1058
 
1068
1059
  const manifestSkillSchema = z.strictObject({
1069
- name: slugSchema,
1060
+ name: nameSchema,
1061
+ displayName: displayNameSchema.default({}),
1070
1062
  description: z.string().trim().min(1).max(SKILL_LIMITS.descriptionLength),
1071
1063
  /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}`. */
1072
1064
  body: z.string().trim().min(1),
1073
- /** The key of the service the skill renders inside; fixed once created. */
1074
- service: slugSchema.optional(),
1065
+ /** The name of the service the skill renders inside; fixed once created. */
1066
+ service: nameSchema.optional(),
1075
1067
  audience: audienceSchema,
1076
- overridable: overridable(["description", "body"]),
1068
+ overridable: overridable(["displayName", "description", "body"]),
1077
1069
  });
1078
1070
  export type ManifestSkill = z.input<typeof manifestSkillSchema>;
1079
1071
 
@@ -1100,7 +1092,8 @@ export const TRIGGER_LIMITS = { descriptionLength: 500, instructionsLength: 20_0
1100
1092
  /** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
1101
1093
  const manifestTriggerSchema = z.strictObject({
1102
1094
  /** `message` is the built-in trigger every agent has. */
1103
- name: slugSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
1095
+ name: nameSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
1096
+ displayName: displayNameSchema.default({}),
1104
1097
  description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
1105
1098
  inputSchema: objectSchema.default(EMPTY_OBJECT),
1106
1099
  /** The shape the run answers in; absent, the run answers in free text. */
@@ -1125,6 +1118,7 @@ const manifestTriggerSchema = z.strictObject({
1125
1118
  model: modelSchema.optional(),
1126
1119
  audience: audienceSchema,
1127
1120
  overridable: overridable([
1121
+ "displayName",
1128
1122
  "description",
1129
1123
  "instructions",
1130
1124
  "threadPolicy",
@@ -1147,6 +1141,7 @@ export function unexposedPins(host: {
1147
1141
  const manifestDatasetHostSchema = z
1148
1142
  .strictObject({
1149
1143
  name: datasetIdentifierSchema,
1144
+ displayName: displayNameSchema.default({}),
1150
1145
  url: upstreamUrlSchema,
1151
1146
  headers: headersSchema,
1152
1147
  timeoutMs: setting(SETTING_BOUNDS.datasetHost.timeoutMs),
@@ -1175,7 +1170,7 @@ const manifestDatasetHostSchema = z
1175
1170
  /** Among those, the ones listed in every turn, whatever the question. */
1176
1171
  pinned: z.array(datasetIdentifierSchema).max(50).default([]),
1177
1172
  audience: audienceSchema,
1178
- overridable: overridable(["timeoutMs", "rateLimits", "headers"]),
1173
+ overridable: overridable(["displayName", "timeoutMs", "rateLimits", "headers"]),
1179
1174
  })
1180
1175
  .superRefine((host, context) => {
1181
1176
  const unexposed = unexposedPins(host);
@@ -1204,6 +1199,7 @@ export const mcpToolNamesSchema = z
1204
1199
  const manifestMcpServerSchema = z.strictObject({
1205
1200
  /** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
1206
1201
  name: identifierSchema,
1202
+ displayName: displayNameSchema.default({}),
1207
1203
  url: upstreamUrlSchema,
1208
1204
  headers: declaredHeadersSchema(
1209
1205
  presentedHeaderSchema.refine(
@@ -1216,7 +1212,7 @@ const manifestMcpServerSchema = z.strictObject({
1216
1212
  include: mcpToolNamesSchema.default([]),
1217
1213
  exclude: mcpToolNamesSchema.default([]),
1218
1214
  audience: audienceSchema,
1219
- overridable: overridable(["include", "exclude", "headers"]),
1215
+ overridable: overridable(["displayName", "include", "exclude", "headers"]),
1220
1216
  });
1221
1217
  export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
1222
1218
 
@@ -1226,7 +1222,8 @@ export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
1226
1222
  * travels under the header that names the event.
1227
1223
  */
1228
1224
  const manifestWebhookSchema = z.strictObject({
1229
- name: slugSchema,
1225
+ name: nameSchema,
1226
+ displayName: displayNameSchema.default({}),
1230
1227
  description: z.string().trim().max(500).default(""),
1231
1228
  event: z.enum(SERVER_EVENTS),
1232
1229
  url: upstreamUrlSchema,
@@ -1239,19 +1236,15 @@ const manifestWebhookSchema = z.strictObject({
1239
1236
  ),
1240
1237
  timeoutMs: setting(SETTING_BOUNDS.webhook.timeoutMs),
1241
1238
  audience: audienceSchema,
1242
- overridable: overridable(["description", "timeoutMs", "headers"]),
1239
+ overridable: overridable(["displayName", "description", "timeoutMs", "headers"]),
1243
1240
  });
1244
1241
  export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
1245
1242
 
1246
1243
  /**
1247
- * A collection's name and description in characters, and how many documents
1248
- * a manifest lists in one. The console caps its editor at the same.
1244
+ * A collection's description in characters, and how many documents a
1245
+ * manifest lists in one. The console caps its editor at the same.
1249
1246
  */
1250
- export const COLLECTION_LIMITS = {
1251
- nameLength: 80,
1252
- descriptionLength: 2_000,
1253
- documents: 100,
1254
- } as const;
1247
+ export const COLLECTION_LIMITS = { descriptionLength: 2_000, documents: 100 } as const;
1255
1248
 
1256
1249
  /** A knowledge document's name in characters and its size in bytes, however it arrives. */
1257
1250
  export const DOCUMENT_LIMITS = { nameLength: 255, bytes: 10 * 1024 * 1024 } as const;
@@ -1377,15 +1370,16 @@ function collectionSchema<TDocument extends z.ZodType<Parameters<typeof document
1377
1370
  document: TDocument,
1378
1371
  ) {
1379
1372
  return z.strictObject({
1380
- name: z.string().trim().min(1).max(COLLECTION_LIMITS.nameLength),
1373
+ name: nameSchema,
1374
+ displayName: displayNameSchema.default({}),
1381
1375
  description: z.string().trim().max(COLLECTION_LIMITS.descriptionLength).default(""),
1382
- /** The key of the service every document in the collection belongs to. */
1383
- service: slugSchema.optional(),
1376
+ /** The name of the service every document in the collection belongs to. */
1377
+ service: nameSchema.optional(),
1384
1378
  /** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
1385
1379
  citable: z.boolean().default(true),
1386
1380
  documents: documentsSchema(document),
1387
1381
  audience: audienceSchema,
1388
- overridable: overridable(["description", "documents"]),
1382
+ overridable: overridable(["displayName", "description", "documents"]),
1389
1383
  });
1390
1384
  }
1391
1385
 
@@ -1497,13 +1491,10 @@ export const agentSettingsSchema = z.strictObject({
1497
1491
  .prefault({}),
1498
1492
  });
1499
1493
 
1500
- /** An agent's name in characters. The console caps its editor at the same. */
1501
- export const AGENT_LIMITS = { nameLength: 120 } as const;
1502
-
1503
1494
  const manifestAgentSchema = z
1504
1495
  .strictObject({
1505
- slug: agentSlugSchema,
1506
- name: z.string().trim().min(1).max(AGENT_LIMITS.nameLength),
1496
+ name: nameSchema,
1497
+ displayName: displayNameSchema.default({}),
1507
1498
  /**
1508
1499
  * The server it belongs to, by name; a manifest never creates one. Left
1509
1500
  * out, the agent has no server: no native tools, no server variables,
@@ -1545,7 +1536,7 @@ const manifestAgentSchema = z
1545
1536
  suggestions: suggestionsSchema.default(() => byLocale(() => [])),
1546
1537
  greeting: greetingSchema.default(() => byLocale(() => "")),
1547
1538
  overridable: overridable([
1548
- "name",
1539
+ "displayName",
1549
1540
  "prompt",
1550
1541
  "models",
1551
1542
  "settings",
@@ -1592,31 +1583,14 @@ export const MANIFEST_KINDS = [
1592
1583
  export type ManifestKind = (typeof MANIFEST_KINDS)[number];
1593
1584
 
1594
1585
  /**
1595
- * The field each kind's entries go by, which its row's name holds: an
1596
- * agent's slug, a service's key, any other entry's name. A list holds each
1597
- * once, so one entry means one row.
1586
+ * A list of entries, each going by the name its row holds, refusing a name
1587
+ * they repeat: one entry means one row.
1598
1588
  */
1599
- export const MANIFEST_NAME_FIELDS = {
1600
- agents: "slug",
1601
- tools: "name",
1602
- services: "key",
1603
- skills: "name",
1604
- triggers: "name",
1605
- datasetHosts: "name",
1606
- mcpServers: "name",
1607
- webhooks: "name",
1608
- collections: "name",
1609
- } as const satisfies Record<ManifestKind, string>;
1610
-
1611
- /** A list of entries named by their `field`, refusing a name they repeat. */
1612
- function listOf<TField extends string, TEntry extends z.ZodType<Record<TField, string>>>(
1613
- field: TField,
1614
- entry: TEntry,
1615
- ) {
1589
+ function listOf<TEntry extends z.ZodType<{ name: string }>>(entry: TEntry) {
1616
1590
  return z
1617
1591
  .array(entry)
1618
1592
  .refine(
1619
- (entries) => new Set(entries.map((value) => value[field])).size === entries.length,
1593
+ (entries) => new Set(entries.map((value) => value.name)).size === entries.length,
1620
1594
  "a name repeats",
1621
1595
  )
1622
1596
  .default([]);
@@ -1634,17 +1608,17 @@ export const CORTEX_MANIFEST_ID = "cortex";
1634
1608
  */
1635
1609
  export const manifestSchema = z.strictObject({
1636
1610
  /** Fixed for the deployment it describes: the rows an apply owns carry it. */
1637
- id: slugSchema,
1638
- agents: listOf(MANIFEST_NAME_FIELDS.agents, manifestAgentSchema),
1639
- tools: listOf(MANIFEST_NAME_FIELDS.tools, manifestToolSchema),
1640
- openapi: listOf("name", manifestOpenApiSchema),
1641
- services: listOf(MANIFEST_NAME_FIELDS.services, manifestServiceSchema),
1642
- skills: listOf(MANIFEST_NAME_FIELDS.skills, manifestSkillSchema),
1643
- triggers: listOf(MANIFEST_NAME_FIELDS.triggers, manifestTriggerSchema),
1644
- datasetHosts: listOf(MANIFEST_NAME_FIELDS.datasetHosts, manifestDatasetHostSchema),
1645
- mcpServers: listOf(MANIFEST_NAME_FIELDS.mcpServers, manifestMcpServerSchema),
1646
- webhooks: listOf(MANIFEST_NAME_FIELDS.webhooks, manifestWebhookSchema),
1647
- collections: listOf(MANIFEST_NAME_FIELDS.collections, manifestCollectionSchema),
1611
+ id: nameSchema,
1612
+ agents: listOf(manifestAgentSchema),
1613
+ tools: listOf(manifestToolSchema),
1614
+ openapi: listOf(manifestOpenApiSchema),
1615
+ services: listOf(manifestServiceSchema),
1616
+ skills: listOf(manifestSkillSchema),
1617
+ triggers: listOf(manifestTriggerSchema),
1618
+ datasetHosts: listOf(manifestDatasetHostSchema),
1619
+ mcpServers: listOf(manifestMcpServerSchema),
1620
+ webhooks: listOf(manifestWebhookSchema),
1621
+ collections: listOf(manifestCollectionSchema),
1648
1622
  } satisfies Record<ManifestKind | "id" | "openapi", z.ZodType>);
1649
1623
  /** A manifest as written: every field with a default may be left out. */
1650
1624
  export type Manifest = z.input<typeof manifestSchema>;
@@ -1655,15 +1629,12 @@ export type Manifest = z.input<typeof manifestSchema>;
1655
1629
  * beside it.
1656
1630
  */
1657
1631
  export const resolvedManifestSchema = manifestSchema.extend({
1658
- collections: listOf(MANIFEST_NAME_FIELDS.collections, resolvedCollectionSchema),
1632
+ collections: listOf(resolvedCollectionSchema),
1659
1633
  });
1660
1634
 
1661
1635
  const manifestKindSchema = z.enum(MANIFEST_KINDS);
1662
1636
 
1663
- /**
1664
- * Entry names per kind, each by its `MANIFEST_NAME_FIELDS` field. An OpenAPI
1665
- * source's operations are counted among `tools`.
1666
- */
1637
+ /** Entry names per kind. An OpenAPI source's operations are counted among `tools`. */
1667
1638
  const namesByKindSchema = z.record(manifestKindSchema, z.array(z.string()));
1668
1639
 
1669
1640
  /** What an apply creates, updates and deletes, kind by kind; a preview answers the same without doing it. */
@@ -18,10 +18,6 @@ import { APPROVAL_TITLE_FIELD, LOCALES } from "../wire/index";
18
18
  /** The run's end-user token, which Cortex forwards verbatim to a tool's backend, a dataset host or the agent's server. */
19
19
  export const END_USER_TOKEN_HEADER = "X-End-User-Token";
20
20
 
21
- /** The agent slug Cortex's routes name an agent by. */
22
- export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
23
- export const agentSlugSchema = z.string().regex(AGENT_SLUG_PATTERN);
24
-
25
21
  /** The longest end-user id Cortex stores. */
26
22
  export const USER_ID_MAX_LENGTH = 255;
27
23
 
@@ -40,9 +36,35 @@ export const IDENTIFIER_MAX_LENGTH = 64;
40
36
 
41
37
  export const identifierSchema = z.string().regex(IDENTIFIER_PATTERN).max(IDENTIFIER_MAX_LENGTH);
42
38
 
39
+ /**
40
+ * A kebab name: lowercase letters and digits in words joined by single
41
+ * hyphens. What the manifest id and every name Cortex holds must be, save a
42
+ * tool's, an MCP server's and a dataset host's, and what its routes name an
43
+ * agent by.
44
+ */
45
+ export const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
46
+
47
+ /** The longest kebab name Cortex stores. */
48
+ export const NAME_MAX_LENGTH = 64;
49
+
50
+ export const nameSchema = z.string().regex(NAME_PATTERN).max(NAME_MAX_LENGTH);
51
+
43
52
  /** A localized value's key. */
44
53
  export const localeSchema = z.enum(LOCALES);
45
54
 
55
+ /** The longest display name Cortex stores, in any locale. */
56
+ export const DISPLAY_NAME_MAX_LENGTH = 120;
57
+
58
+ /**
59
+ * What a console shows an entity as, per locale; a locale without one falls
60
+ * back as `localized` says, and an entity with none goes by its name. Each
61
+ * value is trimmed, and a blank one is dropped as absent.
62
+ */
63
+ export const displayNameSchema = z
64
+ .partialRecord(localeSchema, z.string().trim().max(DISPLAY_NAME_MAX_LENGTH))
65
+ .transform((value) => Object.fromEntries(Object.entries(value).filter(([, text]) => text)))
66
+ .pipe(z.partialRecord(localeSchema, z.string()));
67
+
46
68
  /** How many variables and tools a server's manifest declares at most, and how long each of their texts runs. */
47
69
  export const SERVER_MANIFEST_LIMITS = {
48
70
  variables: 100,
@@ -146,12 +168,14 @@ export type ServerVariable = z.infer<typeof serverVariableSchema>;
146
168
 
147
169
  /**
148
170
  * A tool the server runs. Its input is an object, the `input` its calls
149
- * carry. `requiresApproval` parks every call for the end user's approval;
150
- * such a tool's input cannot declare `APPROVAL_TITLE_FIELD`, which the
151
- * model fills for the approval card.
171
+ * carry. `displayName` is what the console shows it as; the model only ever
172
+ * sees its name. `requiresApproval` parks every call for the end user's
173
+ * approval; such a tool's input cannot declare `APPROVAL_TITLE_FIELD`, which
174
+ * the model fills for the approval card.
152
175
  */
153
176
  export const serverToolSchema = z.object({
154
177
  name: declaredToolNameSchema,
178
+ displayName: displayNameSchema.optional(),
155
179
  description: z.string().trim().min(1).max(SERVER_MANIFEST_LIMITS.description),
156
180
  inputSchema: jsonSchemaSchema.refine(
157
181
  (schema) => schema.type === "object",
@@ -179,7 +203,7 @@ export const EVENT_HEADER = "X-Cortex-Event";
179
203
  */
180
204
  export const approvalRequestedEventSchema = z.object({
181
205
  event: z.literal("approval_requested"),
182
- agentSlug: agentSlugSchema,
206
+ agentName: nameSchema,
183
207
  agentId: z.uuid(),
184
208
  threadId: z.string(),
185
209
  runId: z.string(),
@@ -285,7 +309,7 @@ export const serverManifestSchema = z.object({
285
309
  export type ServerManifest = z.infer<typeof serverManifestSchema>;
286
310
 
287
311
  /** The agent a call is for. */
288
- const agentSchema = z.object({ id: z.uuid(), slug: agentSlugSchema });
312
+ const agentSchema = z.object({ id: z.uuid(), name: nameSchema });
289
313
 
290
314
  /** The end user a run acts for, as their token was verified; null for a trigger's run. */
291
315
  const serverUserSchema = z
@@ -6,7 +6,7 @@ import { TRIGGER_DROP_REASONS, TRIGGER_REASONS } from "../wire/index";
6
6
  * Triggers: named entry points on an agent. A firing is one invocation with a
7
7
  * payload and starts at most one run. The end user's message is the built-in
8
8
  * `message` trigger; every other trigger is fired by a caller over
9
- * `POST /agents/:agentSlug/triggers/:name` with the trigger's secret as its
9
+ * `POST /agents/:agentName/triggers/:name` with the trigger's secret as its
10
10
  * bearer. These are the shapes Cortex answers that caller with.
11
11
  */
12
12
 
@@ -26,14 +26,14 @@ export const TRIGGER_OUTCOMES = [
26
26
  ] as const;
27
27
  export type TriggerOutcome = (typeof TRIGGER_OUTCOMES)[number];
28
28
 
29
- /** The engine route of agent `agentSlug`, which the widget chats with and its triggers sit under. */
30
- export function agentRoute(agentSlug: string) {
31
- return `/agents/${encodeURIComponent(agentSlug)}`;
29
+ /** The engine route of agent `agentName`, which the widget chats with and its triggers sit under. */
30
+ export function agentRoute(agentName: string) {
31
+ return `/agents/${encodeURIComponent(agentName)}`;
32
32
  }
33
33
 
34
- /** The engine route a trigger is fired at on agent `agentSlug`; its firings are read and answered under it. */
35
- export function triggerRoute(agentSlug: string, name: string) {
36
- return `${agentRoute(agentSlug)}/triggers/${encodeURIComponent(name)}`;
34
+ /** The engine route a trigger is fired at on agent `agentName`; its firings are read and answered under it. */
35
+ export function triggerRoute(agentName: string, name: string) {
36
+ return `${agentRoute(agentName)}/triggers/${encodeURIComponent(name)}`;
37
37
  }
38
38
 
39
39
  /** One server tool call a parked run waits on; `POST .../firings/:id/answer` decides it. */
@@ -88,7 +88,7 @@ export const triggerFiringResponseSchema = z.discriminatedUnion("outcome", [
88
88
  export type TriggerFiringResponse = z.infer<typeof triggerFiringResponseSchema>;
89
89
 
90
90
  /**
91
- * `POST /agents/:agentSlug/triggers/:name/firings/:id/answer`: the decision on one parked call.
91
+ * `POST /agents/:agentName/triggers/:name/firings/:id/answer`: the decision on one parked call.
92
92
  * A call id is as long as the model provider made it (some carry a signature), so only the
93
93
  * request's body limit bounds it.
94
94
  */
@@ -98,7 +98,7 @@ export const triggerFiringAnswerSchema = z.object({
98
98
  });
99
99
  export type TriggerFiringAnswer = z.infer<typeof triggerFiringAnswerSchema>;
100
100
 
101
- /** One row of `GET /agents/:agentSlug/triggers/:name/firings`, and the answer of `.../firings/:id`. */
101
+ /** One row of `GET /agents/:agentName/triggers/:name/firings`, and the answer of `.../firings/:id`. */
102
102
  export const triggerFiringRecordSchema = z.object({
103
103
  id: z.uuid(),
104
104
  triggerName: z.string(),
@@ -169,6 +169,8 @@ export type ClientToolBinding = {
169
169
  * The widget hides UI for anything absent; Cortex enforces each.
170
170
  */
171
171
  export type AgentCapabilities = {
172
+ /** The agent's display name per locale, which titles a new chat; `{}` for an agent without one. */
173
+ displayName: Partial<LocalizedText>;
172
174
  attachments: boolean;
173
175
  /**
174
176
  * False when this caller may not send to the chat: the agent's `message`
@@ -214,6 +216,15 @@ export type Greeting = Record<Locale, string>;
214
216
  /** Author-written copy the widget shows verbatim, one string per locale. */
215
217
  export type LocalizedText = Record<Locale, string>;
216
218
 
219
+ /**
220
+ * Resolves a per-locale value against `locale`: exact match, then English,
221
+ * then any entry, so a sparse map still renders something. An entity's
222
+ * label is `localized(entity.displayName, locale) ?? entity.name`.
223
+ */
224
+ export function localized<T>(value: Partial<Record<Locale, T>>, locale: Locale) {
225
+ return value[locale] ?? value.en ?? Object.values(value)[0];
226
+ }
227
+
217
228
  /**
218
229
  * Cortex's own tools whose input carries `status`: the model's one-line
219
230
  * statement of the call, which the widget draws in place of a generic label.
@@ -8,8 +8,6 @@
8
8
  * a standalone scaffold has no workspace.
9
9
  */
10
10
 
11
- import { NAME_MAX_LENGTH } from "@cortex/contracts/manifest";
12
-
13
11
  import pkg from "../../package.json";
14
12
  import serverTs from "@/scaffold/templates/server.ts.txt";
15
13
  import setupTs from "@/scaffold/templates/setup.ts.txt";
@@ -30,7 +28,7 @@ import { applyPorts, type Ports } from "@/scaffold/ports";
30
28
  * `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
31
29
  * before the CLI publishes, so a scaffold pins what was current then.
32
30
  */
33
- const CORTEX_VERSION = "0.19.0";
31
+ const CORTEX_VERSION = "0.20.0";
34
32
 
35
33
  /** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
36
34
  export const DOCKER_HOST = "host.docker.internal";
@@ -40,9 +38,9 @@ const SERVER_URL = `http://${DOCKER_HOST}:{{port.server}}`;
40
38
 
41
39
  /**
42
40
  * The agent the manifest declares and the README sends you to chat with.
43
- * Not `assistant`: Cortex holds that slug for its console's own assistant.
41
+ * Not `assistant`: Cortex holds that name for its console's own assistant.
44
42
  */
45
- export const AGENT_SLUG = "helper";
43
+ export const AGENT_NAME = "helper";
46
44
 
47
45
  /**
48
46
  * The embedding model the stack asks for, and the dimension the image's
@@ -111,7 +109,7 @@ function runAll(runner: "mprocs" | "concurrently", processes: Record<string, str
111
109
  return `${runner} --names ${Object.keys(processes).join(",")} ${commands.join(" ")}`;
112
110
  }
113
111
 
114
- /** The name as the manifest `id` takes it: lowercase words and hyphens. */
112
+ /** The name as the manifest `id` and the server's name take it: lowercase words and hyphens. */
115
113
  export function manifestIdOf(name: string) {
116
114
  return name
117
115
  .replace(/[^a-z0-9]+/g, "-")
@@ -149,7 +147,7 @@ function packageJson({ name, datasets, setup }: Project) {
149
147
  verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
150
148
  },
151
149
  dependencies: {
152
- "@m6d/cortex": "~0.7.0",
150
+ "@m6d/cortex": "~0.8.0",
153
151
  // drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
154
152
  ...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
155
153
  hono: "^4.12.3",
@@ -218,8 +216,8 @@ export default defineManifest({
218
216
  id: ${JSON.stringify(manifestIdOf(name))},
219
217
  agents: [
220
218
  {
221
- slug: "${AGENT_SLUG}",
222
- name: "Helper",
219
+ name: "${AGENT_NAME}",
220
+ displayName: { en: "Helper" },
223
221
  server,
224
222
  prompt: "You are a helpful assistant. Address the user as {{userName}}.",
225
223
  // No identity provider yet: each browser is a visitor of its own.
@@ -460,7 +458,7 @@ function readme({ name, server, datasets, setup }: Project) {
460
458
  const gettingStarted = setup
461
459
  ? `That starts Cortex (\`docker compose up\`), this backend, and \`cortex setup\` in one dashboard.
462
460
  Setup waits for Cortex, registers this backend as the server \`${server}\`, adds the model provider
463
- in \`.env\`, and applies the manifest. Then it prints where to chat with \`${AGENT_SLUG}\`, and the first
461
+ in \`.env\`, and applies the manifest. Then it prints where to chat with \`${AGENT_NAME}\`, and the first
464
462
  time, how to sign in. It adds only what is missing, so every \`bun run dev\` runs it again.
465
463
 
466
464
  While \`MODEL_PROVIDER_*\` in \`.env\` is blank, setup registers the server and applies nothing:
@@ -487,7 +485,7 @@ A backend for Cortex agents, and Cortex itself to run them on.
487
485
  ? "\n- `datasets.ts` serves an `orders` table as a dataset host, for agents to ask grouped\n questions of."
488
486
  : ""
489
487
  }
490
- - \`cortex.manifest.ts\` declares the \`${AGENT_SLUG}\` agent on this server${
488
+ - \`cortex.manifest.ts\` declares the \`${AGENT_NAME}\` agent on this server${
491
489
  datasets ? " and the dataset host" : ""
492
490
  }. \`cortex apply\` makes Cortex match it.
493
491
  - \`docker-compose.yml\` runs Cortex: its app, worker and engine, and the stores they use.
@@ -507,7 +505,7 @@ bun run dev
507
505
 
508
506
  ${gettingStarted}
509
507
 
510
- Then chat with \`${AGENT_SLUG}\` on the console's **Playground** page. Edit \`cortex.manifest.ts\` and run
508
+ Then chat with \`${AGENT_NAME}\` on the console's **Playground** page. Edit \`cortex.manifest.ts\` and run
511
509
  \`bun cortex apply\` to change it; an apply changes only what the edit changed. The CLI reaches this
512
510
  checkout's Cortex through \`CORTEX_URL\` and \`CORTEX_KEY\` in \`.env\`, so it needs no login, and
513
511
  \`-p <profile>\` picks a saved one instead.
@@ -538,7 +536,7 @@ pre-commit hook or CI job should run. Each is also available on its own: \`forma
538
536
  * are drawn by the caller.
539
537
  */
540
538
  export function scaffoldFiles(input: Input) {
541
- const project = { ...input, server: input.name.slice(0, NAME_MAX_LENGTH) };
539
+ const project = { ...input, server: manifestIdOf(input.name) };
542
540
  const files = {
543
541
  "server.ts": serverTs,
544
542
  "setup.ts": setupTs,