@anchrd/intel-contract 0.5.0 → 0.8.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.
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  export declare const IntelId: z.ZodString;
3
3
  export declare const IsoDateTime: z.ZodISODateTime;
4
+ export declare const UI_LANGUAGES: readonly ["en", "de", "es"];
5
+ export type UiLanguage = (typeof UI_LANGUAGES)[number];
4
6
  export declare const ProblemDetails: z.ZodObject<{
5
7
  type: z.ZodString;
6
8
  status: z.ZodNumber;
@@ -478,6 +480,7 @@ export declare const AgentScheduleTarget: z.ZodObject<{
478
480
  export type AgentScheduleTarget = z.infer<typeof AgentScheduleTarget>;
479
481
  export declare const AgentSchedule: z.ZodObject<{
480
482
  cron: z.ZodString;
483
+ timezone: z.ZodDefault<z.ZodString>;
481
484
  target: z.ZodObject<{
482
485
  kind: z.ZodEnum<{
483
486
  document: "document";
@@ -496,18 +499,53 @@ export declare const AgentModel: z.ZodObject<{
496
499
  }, z.core.$strict>;
497
500
  export type AgentModel = z.infer<typeof AgentModel>;
498
501
  /**
499
- * ⚠️ No accounts, no secrets, no channels, and no tools and the reason is mechanical rather than
500
- * tidy (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret
501
- * in it is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are
502
- * runtime configuration, and the tool catalog is a live `tools/list` that is never mirrored.
502
+ * One MCP server as the portal names it. The handle is what the portal puts in front of every tool
503
+ * that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
504
+ * Intel can both store and recognise again in a live `tools/list`.
503
505
  *
504
- * ⚠️ Strict on purpose, and deliberately stricter than the runtime's own reader
505
- * (`packages/agent/src/definition/definition.ts`, which is `z.object`). Intel is the writer: an
506
- * unknown field here is a caller's mistake and is refused at the boundary. The runtime is the
507
- * reader and released separately, so it must keep starting agents when Intel adds a field
508
- * tomorrow. The asymmetry is the point, not an oversight.
506
+ * ⚠️ A handle is never invented from a tool name. Which servers exist is the portal's answer
507
+ * (`portal_list_servers`), and the prefix is only used to attribute a tool to a server that answer
508
+ * already named see `packages/api/src/tools/tool-servers` for why splitting on the underscore
509
+ * alone would be ambiguous.
509
510
  */
511
+ export declare const ToolServerHandle: z.ZodString;
512
+ export type ToolServerHandle = z.infer<typeof ToolServerHandle>;
513
+ /**
514
+ * What a caller may ASK for: whole MCP servers, and nothing about who delegates them (D30).
515
+ *
516
+ * ⚠️ The absence of `delegatedBy` is the point, and it is why the write shape differs from the read
517
+ * shape at all. Intel writes that field from the session it authorized; a caller who could name
518
+ * somebody else would be handing an agent a portal connection they do not have, and the agent would
519
+ * act on it unattended. Leaving the field out of the input makes that structural instead of a
520
+ * runtime overwrite: a body carrying it is a parse error at the boundary, on every surface, and no
521
+ * screen ever has to invent a value it has no business knowing.
522
+ */
523
+ export declare const AgentToolSelection: z.ZodObject<{
524
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
525
+ }, z.core.$strict>;
526
+ export type AgentToolSelection = z.infer<typeof AgentToolSelection>;
527
+ /**
528
+ * What is STORED and read back: the selection plus whose portal connection it came from (D30).
529
+ *
530
+ * ⚠️ This is a selection, not a permission. Nothing here grants anything: whether a server is
531
+ * reachable is still decided by one live `tools/list` with `delegatedBy`'s own portal token, so a
532
+ * delegator who loses the server or the connection takes it away from the agent at the next run
533
+ * with no edit to this document.
534
+ *
535
+ * ⚠️ Read shape only. It appears in `AgentDefinition` and never in an input — see
536
+ * `AgentToolSelection` for why the two are deliberately different documents rather than one schema
537
+ * with an optional field.
538
+ */
539
+ export declare const AgentToolDelegation: z.ZodObject<{
540
+ delegatedBy: z.ZodString;
541
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
542
+ }, z.core.$strict>;
543
+ export type AgentToolDelegation = z.infer<typeof AgentToolDelegation>;
510
544
  export declare const AgentDefinition: z.ZodObject<{
545
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
546
+ delegatedBy: z.ZodString;
547
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
548
+ }, z.core.$strict>>>;
511
549
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
512
550
  nodeId: z.ZodString;
513
551
  role: z.ZodEnum<{
@@ -518,6 +556,7 @@ export declare const AgentDefinition: z.ZodObject<{
518
556
  }, z.core.$strict>>>;
519
557
  schedules: z.ZodDefault<z.ZodArray<z.ZodObject<{
520
558
  cron: z.ZodString;
559
+ timezone: z.ZodDefault<z.ZodString>;
521
560
  target: z.ZodObject<{
522
561
  kind: z.ZodEnum<{
523
562
  document: "document";
@@ -535,10 +574,56 @@ export declare const AgentDefinition: z.ZodObject<{
535
574
  }, z.core.$strict>;
536
575
  }, z.core.$strict>;
537
576
  export type AgentDefinition = z.infer<typeof AgentDefinition>;
577
+ /**
578
+ * The same document as `AgentDefinition`, minus the one field a caller may not write.
579
+ *
580
+ * ⚠️ Two schemas rather than one, and the split is load-bearing (#208, D30). Everything an agent IS
581
+ * comes from whoever edits it; **whose portal connection it acts on** does not, because that is an
582
+ * authority the editor would be granting to themselves. So the write shape simply has no place to
583
+ * put it: `{ tools: { servers: [...] } }` is what a screen or an MCP client sends, Intel adds
584
+ * `delegatedBy` from the session, and a body that tries to name one is refused by the strict object
585
+ * before any of it is read. The reading shape keeps the field because a reader must be able to see
586
+ * whose connection an agent runs on.
587
+ */
588
+ export declare const AgentDefinitionInput: z.ZodObject<{
589
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
590
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
591
+ }, z.core.$strict>>>;
592
+ references: z.ZodDefault<z.ZodArray<z.ZodObject<{
593
+ nodeId: z.ZodString;
594
+ role: z.ZodEnum<{
595
+ "system-message": "system-message";
596
+ "semantic-context": "semantic-context";
597
+ memory: "memory";
598
+ }>;
599
+ }, z.core.$strict>>>;
600
+ schedules: z.ZodDefault<z.ZodArray<z.ZodObject<{
601
+ cron: z.ZodString;
602
+ timezone: z.ZodDefault<z.ZodString>;
603
+ target: z.ZodObject<{
604
+ kind: z.ZodEnum<{
605
+ document: "document";
606
+ flow: "flow";
607
+ }>;
608
+ id: z.ZodString;
609
+ }, z.core.$strict>;
610
+ }, z.core.$strict>>>;
611
+ model: z.ZodObject<{
612
+ provider: z.ZodEnum<{
613
+ "workers-ai": "workers-ai";
614
+ anthropic: "anthropic";
615
+ }>;
616
+ model: z.ZodString;
617
+ }, z.core.$strict>;
618
+ }, z.core.$strict>;
619
+ export type AgentDefinitionInput = z.infer<typeof AgentDefinitionInput>;
538
620
  export declare const SaveAgentDefinitionInput: z.ZodObject<{
539
621
  nodeId: z.ZodString;
540
622
  baseVersionId: z.ZodNullable<z.ZodString>;
541
623
  definition: z.ZodObject<{
624
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
625
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
626
+ }, z.core.$strict>>>;
542
627
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
543
628
  nodeId: z.ZodString;
544
629
  role: z.ZodEnum<{
@@ -549,6 +634,7 @@ export declare const SaveAgentDefinitionInput: z.ZodObject<{
549
634
  }, z.core.$strict>>>;
550
635
  schedules: z.ZodDefault<z.ZodArray<z.ZodObject<{
551
636
  cron: z.ZodString;
637
+ timezone: z.ZodDefault<z.ZodString>;
552
638
  target: z.ZodObject<{
553
639
  kind: z.ZodEnum<{
554
640
  document: "document";
@@ -597,6 +683,9 @@ export declare const CreateAgentInput: z.ZodObject<{
597
683
  title: z.ZodString;
598
684
  description: z.ZodDefault<z.ZodNullable<z.ZodString>>;
599
685
  definition: z.ZodObject<{
686
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
687
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
688
+ }, z.core.$strict>>>;
600
689
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
601
690
  nodeId: z.ZodString;
602
691
  role: z.ZodEnum<{
@@ -607,6 +696,7 @@ export declare const CreateAgentInput: z.ZodObject<{
607
696
  }, z.core.$strict>>>;
608
697
  schedules: z.ZodDefault<z.ZodArray<z.ZodObject<{
609
698
  cron: z.ZodString;
699
+ timezone: z.ZodDefault<z.ZodString>;
610
700
  target: z.ZodObject<{
611
701
  kind: z.ZodEnum<{
612
702
  document: "document";
@@ -662,6 +752,10 @@ export declare const NodeAgent: z.ZodObject<{
662
752
  createdAt: z.ZodISODateTime;
663
753
  }, z.core.$strict>>;
664
754
  definition: z.ZodNullable<z.ZodObject<{
755
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
756
+ delegatedBy: z.ZodString;
757
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
758
+ }, z.core.$strict>>>;
665
759
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
666
760
  nodeId: z.ZodString;
667
761
  role: z.ZodEnum<{
@@ -672,6 +766,7 @@ export declare const NodeAgent: z.ZodObject<{
672
766
  }, z.core.$strict>>>;
673
767
  schedules: z.ZodDefault<z.ZodArray<z.ZodObject<{
674
768
  cron: z.ZodString;
769
+ timezone: z.ZodDefault<z.ZodString>;
675
770
  target: z.ZodObject<{
676
771
  kind: z.ZodEnum<{
677
772
  document: "document";
@@ -692,36 +787,19 @@ export declare const NodeAgent: z.ZodObject<{
692
787
  }, z.core.$strict>;
693
788
  export type NodeAgent = z.infer<typeof NodeAgent>;
694
789
  /**
695
- * The single value in Intel's wire formats that IS a credential (#182, D27).
790
+ * ⚠️ There is NO key field in this file, and adding one back would be the regression (D29, #207).
696
791
  *
697
- * ⚠️ It exists for exactly one response — the one that created the agent and for no other. Gate
698
- * returns an Application key once in plain text and stores only its hash, so this is not a value
699
- * Intel could fetch again later even if it wanted to; nothing in Intel writes it to D1, to R2, to
700
- * an audit event or to a log, and no read surface has a field it could travel in. What the caller
701
- * does with it is named in `notice` rather than left to them: it belongs in the agent runtime's
702
- * `AGENT_APPLICATION_KEYS` secret, keyed by `agentId`, and nowhere else.
792
+ * Until #207 the create answer carried the Application key in plain text, once, and a person had to
793
+ * carry it into a Worker secret by hand which is why an agent created through the screen could
794
+ * never run (#200). The key now goes from Gate straight into the agent runtime over Intel's service
795
+ * binding and is encrypted into that agent's Durable Object; it reaches no browser, no MCP tool
796
+ * result and no response body at all. `NodeAgent` is a `z.strictObject`, so a field named `key`
797
+ * added anywhere in this file is a parse error at the boundary rather than a leak somebody has to
798
+ * spot in review.
703
799
  *
704
- * ⚠️ `agentId` is the Intel node ID and not the Application ID, because that is the key the runtime
705
- * looks an entry up by. Writing the Application ID into the runtime's map would produce a
706
- * deployment that parses, starts, and then cannot find a single agent.
707
- */
708
- export declare const AgentApplicationKey: z.ZodObject<{
709
- agentId: z.ZodString;
710
- applicationId: z.ZodString;
711
- key: z.ZodString;
712
- notice: z.ZodString;
713
- }, z.core.$strict>;
714
- export type AgentApplicationKey = z.infer<typeof AgentApplicationKey>;
715
- /**
716
- * What `POST /nodes/agents` and `agent_create` answer, and the only shape carrying a key. Every
717
- * other agent read answers with the plain `NodeAgent` above.
718
- *
719
- * ⚠️ `applicationKey` is `null` on a REPLAY, and that is the honest answer rather than a gap. An
720
- * idempotency key repeated after the first response was lost still returns the agent that exists —
721
- * the promise every create in Intel makes — but the key belonged to the one response that created
722
- * it and is gone from Gate. Minting a second principal to fill this field would leave the
723
- * installation with two machine accounts for one agent, one of which nobody would ever switch off.
724
- * `application_rotate_key` in Gate is the way to a new key, and it is a deliberate act.
800
+ * What `POST /nodes/agents` and `agent_create` answer is therefore exactly what every read answers:
801
+ * the node, its first definition, and the `applicationId` that NAMES the principal without
802
+ * authenticating it.
725
803
  */
726
804
  export declare const CreatedAgent: z.ZodObject<{
727
805
  node: z.ZodObject<{
@@ -758,6 +836,10 @@ export declare const CreatedAgent: z.ZodObject<{
758
836
  createdAt: z.ZodISODateTime;
759
837
  }, z.core.$strict>>;
760
838
  definition: z.ZodNullable<z.ZodObject<{
839
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
840
+ delegatedBy: z.ZodString;
841
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
842
+ }, z.core.$strict>>>;
761
843
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
762
844
  nodeId: z.ZodString;
763
845
  role: z.ZodEnum<{
@@ -768,6 +850,7 @@ export declare const CreatedAgent: z.ZodObject<{
768
850
  }, z.core.$strict>>>;
769
851
  schedules: z.ZodDefault<z.ZodArray<z.ZodObject<{
770
852
  cron: z.ZodString;
853
+ timezone: z.ZodDefault<z.ZodString>;
771
854
  target: z.ZodObject<{
772
855
  kind: z.ZodEnum<{
773
856
  document: "document";
@@ -785,14 +868,26 @@ export declare const CreatedAgent: z.ZodObject<{
785
868
  }, z.core.$strict>;
786
869
  }, z.core.$strict>>;
787
870
  applicationId: z.ZodNullable<z.ZodString>;
788
- applicationKey: z.ZodNullable<z.ZodObject<{
789
- agentId: z.ZodString;
790
- applicationId: z.ZodString;
791
- key: z.ZodString;
792
- notice: z.ZodString;
793
- }, z.core.$strict>>;
794
871
  }, z.core.$strict>;
795
872
  export type CreatedAgent = z.infer<typeof CreatedAgent>;
873
+ export declare const RotateAgentKeyInput: z.ZodObject<{
874
+ nodeId: z.ZodString;
875
+ }, z.core.$strict>;
876
+ export type RotateAgentKeyInput = z.infer<typeof RotateAgentKeyInput>;
877
+ /**
878
+ * What replacing an agent's key answers.
879
+ *
880
+ * ⚠️ No key, and that is the whole shape of D29: Intel asks Gate for a new one, hands it to the
881
+ * runtime over the service binding, and forgets it inside the same call. What the caller gets is
882
+ * the fact that it happened, so a screen can say so — `applicationId` names the principal whose key
883
+ * was replaced, which is a name and not a credential.
884
+ */
885
+ export declare const AgentKeyRotated: z.ZodObject<{
886
+ nodeId: z.ZodString;
887
+ applicationId: z.ZodString;
888
+ rotatedAt: z.ZodISODateTime;
889
+ }, z.core.$strict>;
890
+ export type AgentKeyRotated = z.infer<typeof AgentKeyRotated>;
796
891
  export declare const AgentList: z.ZodObject<{
797
892
  items: z.ZodArray<z.ZodObject<{
798
893
  id: z.ZodString;
@@ -1141,6 +1236,53 @@ export declare const ToolCatalog: z.ZodObject<{
1141
1236
  }, z.core.$strict>>;
1142
1237
  }, z.core.$strict>;
1143
1238
  export type ToolCatalog = z.infer<typeof ToolCatalog>;
1239
+ /**
1240
+ * One MCP server the asking user reaches right now, as the portal itself names it (D30).
1241
+ *
1242
+ * ⚠️ `toolCount` is a fact about this moment and this user, not a size. It exists so a picker can
1243
+ * say "9 tools" instead of showing a handle alone, and it must never be read as what an agent will
1244
+ * get: the delegated run asks the portal again, with the delegator's token.
1245
+ */
1246
+ export declare const ToolServer: z.ZodObject<{
1247
+ handle: z.ZodString;
1248
+ name: z.ZodString;
1249
+ toolCount: z.ZodNumber;
1250
+ }, z.core.$strict>;
1251
+ export type ToolServer = z.infer<typeof ToolServer>;
1252
+ export declare const ToolServerCatalog: z.ZodObject<{
1253
+ portalConnected: z.ZodBoolean;
1254
+ items: z.ZodArray<z.ZodObject<{
1255
+ handle: z.ZodString;
1256
+ name: z.ZodString;
1257
+ toolCount: z.ZodNumber;
1258
+ }, z.core.$strict>>;
1259
+ }, z.core.$strict>;
1260
+ export type ToolServerCatalog = z.infer<typeof ToolServerCatalog>;
1261
+ /**
1262
+ * Which of the named servers a tool belongs to, or `null` for none of them.
1263
+ *
1264
+ * ⚠️ THE TRAP: a tool name does not say where its server name ends.
1265
+ *
1266
+ * The portal writes `<server>_<tool>`, and both halves may contain underscores — `intel_flow_get`
1267
+ * reads equally well as server `intel` with tool `flow_get` and as a server called `intel_flow`
1268
+ * with tool `get`. Splitting on the first underscore is therefore a guess that is wrong the day
1269
+ * somebody adds a server whose name contains one, and on the API side being wrong means an agent
1270
+ * delegated server A quietly reaching server B.
1271
+ *
1272
+ * So the prefix is never split. It is only ever MATCHED against handles the portal itself named,
1273
+ * and the longest match wins: with `intel` and `intel_flow` both declared, `intel_flow_get` belongs
1274
+ * to `intel_flow`, which is the only reading in which both declarations stay true.
1275
+ *
1276
+ * ⚠️ This lives in the contract because HOW A NAME IS READ is a property of the wire, and both
1277
+ * surfaces read the same wire: `packages/api` cuts a delegation with it, `packages/ui` groups the
1278
+ * tools screen with it (#212). A second implementation in the browser would be the third answer to
1279
+ * one question — the underscore rule has already been answered differently in two places once
1280
+ * (#106, #107), and the copies disagreed. What deliberately stays OUT of here is everything about
1281
+ * reach: which handles are declared, which are enabled, which may be delegated and which one owns
1282
+ * the portal's own management tools are decisions with consequences, and they belong to
1283
+ * `packages/api/src/tools/tool-servers`. This function only reads a name.
1284
+ */
1285
+ export declare function serverOf(toolName: string, handles: Iterable<string>): string | null;
1144
1286
  export declare const TestToolInput: z.ZodObject<{
1145
1287
  name: z.ZodString;
1146
1288
  arguments: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -1,6 +1,14 @@
1
1
  import { z } from "zod";
2
2
  export const IntelId = z.string().min(1).max(128);
3
3
  export const IsoDateTime = z.iso.datetime({ offset: true });
4
+ // The languages whose UI catalog ships inside intel-ui. They live here rather than only in the UI
5
+ // because `intel build` has to know them: a built-in language may be chosen as `ui.defaultLanguage`
6
+ // without the customer listing a copy under `ui.languages` that would rot at every UI update.
7
+ //
8
+ // ⚠️ The list is a claim about the contents of packages/ui/src/i18n and cannot be checked here.
9
+ // `i18n.unit.ts` compares it against the catalogs actually built in; a language listed here without
10
+ // a file gives a red run there instead of a UI that starts on a catalog which does not exist.
11
+ export const UI_LANGUAGES = ["en", "de", "es"];
4
12
  export const ProblemDetails = z.strictObject({
5
13
  type: z.string(),
6
14
  status: z.number().int().min(400).max(599),
@@ -283,8 +291,42 @@ export const AgentScheduleTarget = z.strictObject({
283
291
  kind: z.enum(["document", "flow"]),
284
292
  id: IntelId,
285
293
  });
294
+ /**
295
+ * ⚠️ `timezone` is what the cron expression is READ IN, and it belongs to the schedule rather than
296
+ * to whoever is looking at it (#228). "Every morning at eight" means eight o'clock where the person
297
+ * who wrote it sits — in Berlin that is 06:00 UTC in summer and 07:00 in winter, and a field that
298
+ * does not carry the zone cannot express that difference. A UTC cron is an hour wrong twice a year
299
+ * and nobody sees why.
300
+ *
301
+ * The UI suggests the reader's own zone when a schedule is created, but it is not a per-user
302
+ * setting: an agent's schedule would otherwise move whenever its owner travelled, and it would mean
303
+ * different times to two people reading the same definition. What is stored is the answer.
304
+ *
305
+ * ⚠️ The default is `"UTC"`, and it is load-bearing rather than tidy: every definition written
306
+ * before this field parses to it and therefore keeps firing exactly when it did. A default of
307
+ * "whatever the writer's browser says" would silently move every existing schedule at the next save.
308
+ *
309
+ * The name is validated against this runtime's own tz database rather than a pattern. A regular
310
+ * expression would accept `Mars/Olympus`, and the failure would surface inside a Durable Object
311
+ * alarm — the place where nobody is watching.
312
+ */
313
+ const IanaTimezone = z
314
+ .string()
315
+ .trim()
316
+ .min(1)
317
+ .max(64)
318
+ .refine((zone) => {
319
+ try {
320
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
321
+ return true;
322
+ }
323
+ catch {
324
+ return false;
325
+ }
326
+ }, { message: "must be an IANA timezone name this runtime knows, for example Europe/Berlin" });
286
327
  export const AgentSchedule = z.strictObject({
287
328
  cron: z.string().trim().min(1).max(120),
329
+ timezone: IanaTimezone.default("UTC"),
288
330
  target: AgentScheduleTarget,
289
331
  });
290
332
  export const AgentModel = z.strictObject({
@@ -292,10 +334,61 @@ export const AgentModel = z.strictObject({
292
334
  model: z.string().trim().min(1).max(120),
293
335
  });
294
336
  /**
295
- * ⚠️ No accounts, no secrets, no channels, and no tools and the reason is mechanical rather than
296
- * tidy (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret
297
- * in it is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are
298
- * runtime configuration, and the tool catalog is a live `tools/list` that is never mirrored.
337
+ * One MCP server as the portal names it. The handle is what the portal puts in front of every tool
338
+ * that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
339
+ * Intel can both store and recognise again in a live `tools/list`.
340
+ *
341
+ * ⚠️ A handle is never invented from a tool name. Which servers exist is the portal's answer
342
+ * (`portal_list_servers`), and the prefix is only used to attribute a tool to a server that answer
343
+ * already named — see `packages/api/src/tools/tool-servers` for why splitting on the underscore
344
+ * alone would be ambiguous.
345
+ */
346
+ export const ToolServerHandle = z
347
+ .string()
348
+ .trim()
349
+ .min(1)
350
+ .max(120)
351
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "A server handle is the portal's own identifier");
352
+ const ToolServerHandles = z.array(ToolServerHandle).max(32).default([]);
353
+ /**
354
+ * What a caller may ASK for: whole MCP servers, and nothing about who delegates them (D30).
355
+ *
356
+ * ⚠️ The absence of `delegatedBy` is the point, and it is why the write shape differs from the read
357
+ * shape at all. Intel writes that field from the session it authorized; a caller who could name
358
+ * somebody else would be handing an agent a portal connection they do not have, and the agent would
359
+ * act on it unattended. Leaving the field out of the input makes that structural instead of a
360
+ * runtime overwrite: a body carrying it is a parse error at the boundary, on every surface, and no
361
+ * screen ever has to invent a value it has no business knowing.
362
+ */
363
+ export const AgentToolSelection = z.strictObject({ servers: ToolServerHandles });
364
+ /**
365
+ * What is STORED and read back: the selection plus whose portal connection it came from (D30).
366
+ *
367
+ * ⚠️ This is a selection, not a permission. Nothing here grants anything: whether a server is
368
+ * reachable is still decided by one live `tools/list` with `delegatedBy`'s own portal token, so a
369
+ * delegator who loses the server or the connection takes it away from the agent at the next run
370
+ * with no edit to this document.
371
+ *
372
+ * ⚠️ Read shape only. It appears in `AgentDefinition` and never in an input — see
373
+ * `AgentToolSelection` for why the two are deliberately different documents rather than one schema
374
+ * with an optional field.
375
+ */
376
+ export const AgentToolDelegation = z.strictObject({
377
+ delegatedBy: IntelId,
378
+ servers: ToolServerHandles,
379
+ });
380
+ /**
381
+ * ⚠️ No accounts, no secrets and no channels — and the reason is mechanical rather than tidy
382
+ * (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret in it
383
+ * is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are runtime
384
+ * configuration.
385
+ *
386
+ * ⚠️ `tools` is the one correction to that list (D30), and it is narrower than it looks. What is
387
+ * stored is a **selection of whole servers plus who delegated them**, never a mirrored permission
388
+ * and never a catalog: the catalog stays a live `tools/list` made with the delegator's token at the
389
+ * moment the agent runs. ADR-0005 §4's "no tools in the definition" forbade the mirror, and the
390
+ * mirror is still forbidden — a tool name, a schema or an account in here would be the thing that
391
+ * line was written against.
299
392
  *
300
393
  * ⚠️ Strict on purpose, and deliberately stricter than the runtime's own reader
301
394
  * (`packages/agent/src/definition/definition.ts`, which is `z.object`). Intel is the writer: an
@@ -303,15 +396,37 @@ export const AgentModel = z.strictObject({
303
396
  * reader and released separately, so it must keep starting agents when Intel adds a field
304
397
  * tomorrow. The asymmetry is the point, not an oversight.
305
398
  */
306
- export const AgentDefinition = z.strictObject({
399
+ const AgentBody = {
307
400
  references: z.array(AgentReference).max(200).default([]),
308
401
  schedules: z.array(AgentSchedule).max(50).default([]),
309
402
  model: AgentModel,
403
+ };
404
+ export const AgentDefinition = z.strictObject({
405
+ ...AgentBody,
406
+ // `null` is "this agent has no tools", and it is also what every definition written before D30
407
+ // parses to. An empty `servers` list means the same thing and is kept as its own state so
408
+ // removing the last server does not have to erase who was delegating.
409
+ tools: AgentToolDelegation.nullable().default(null),
410
+ });
411
+ /**
412
+ * The same document as `AgentDefinition`, minus the one field a caller may not write.
413
+ *
414
+ * ⚠️ Two schemas rather than one, and the split is load-bearing (#208, D30). Everything an agent IS
415
+ * comes from whoever edits it; **whose portal connection it acts on** does not, because that is an
416
+ * authority the editor would be granting to themselves. So the write shape simply has no place to
417
+ * put it: `{ tools: { servers: [...] } }` is what a screen or an MCP client sends, Intel adds
418
+ * `delegatedBy` from the session, and a body that tries to name one is refused by the strict object
419
+ * before any of it is read. The reading shape keeps the field because a reader must be able to see
420
+ * whose connection an agent runs on.
421
+ */
422
+ export const AgentDefinitionInput = z.strictObject({
423
+ ...AgentBody,
424
+ tools: AgentToolSelection.nullable().default(null),
310
425
  });
311
426
  export const SaveAgentDefinitionInput = z.strictObject({
312
427
  nodeId: IntelId,
313
428
  baseVersionId: IntelId.nullable(),
314
- definition: AgentDefinition,
429
+ definition: AgentDefinitionInput,
315
430
  idempotencyKey: z.string().min(8).max(200),
316
431
  });
317
432
  export const GetAgentInput = z.strictObject({ nodeId: IntelId });
@@ -339,7 +454,7 @@ export const CreateAgentInput = z.strictObject({
339
454
  parentId: IntelId.nullable().default(null),
340
455
  title: z.string().trim().min(1).max(240),
341
456
  description: z.string().trim().max(2_000).nullable().default(null),
342
- definition: AgentDefinition,
457
+ definition: AgentDefinitionInput,
343
458
  idempotencyKey: z.string().min(8).max(200),
344
459
  });
345
460
  // The ID of the Gate Application an agent runs as. Deliberately NOT an `IntelId`: it is Better
@@ -362,38 +477,37 @@ export const NodeAgent = z.strictObject({
362
477
  applicationId: GateApplicationId.nullable(),
363
478
  });
364
479
  /**
365
- * The single value in Intel's wire formats that IS a credential (#182, D27).
480
+ * ⚠️ There is NO key field in this file, and adding one back would be the regression (D29, #207).
366
481
  *
367
- * ⚠️ It exists for exactly one response — the one that created the agent and for no other. Gate
368
- * returns an Application key once in plain text and stores only its hash, so this is not a value
369
- * Intel could fetch again later even if it wanted to; nothing in Intel writes it to D1, to R2, to
370
- * an audit event or to a log, and no read surface has a field it could travel in. What the caller
371
- * does with it is named in `notice` rather than left to them: it belongs in the agent runtime's
372
- * `AGENT_APPLICATION_KEYS` secret, keyed by `agentId`, and nowhere else.
482
+ * Until #207 the create answer carried the Application key in plain text, once, and a person had to
483
+ * carry it into a Worker secret by hand which is why an agent created through the screen could
484
+ * never run (#200). The key now goes from Gate straight into the agent runtime over Intel's service
485
+ * binding and is encrypted into that agent's Durable Object; it reaches no browser, no MCP tool
486
+ * result and no response body at all. `NodeAgent` is a `z.strictObject`, so a field named `key`
487
+ * added anywhere in this file is a parse error at the boundary rather than a leak somebody has to
488
+ * spot in review.
373
489
  *
374
- * ⚠️ `agentId` is the Intel node ID and not the Application ID, because that is the key the runtime
375
- * looks an entry up by. Writing the Application ID into the runtime's map would produce a
376
- * deployment that parses, starts, and then cannot find a single agent.
490
+ * What `POST /nodes/agents` and `agent_create` answer is therefore exactly what every read answers:
491
+ * the node, its first definition, and the `applicationId` that NAMES the principal without
492
+ * authenticating it.
377
493
  */
378
- export const AgentApplicationKey = z.strictObject({
379
- agentId: IntelId,
380
- applicationId: GateApplicationId,
381
- key: z.string().min(1),
382
- notice: z.string().min(1),
383
- });
494
+ export const CreatedAgent = NodeAgent;
495
+ // Which agent's key is being replaced. `nodeId` and not the Application ID: this addresses an agent
496
+ // in Intel's tree, and the Application behind it is Intel's to look up — a caller naming the
497
+ // principal directly would be rotating a key for an agent nobody checked they may edit.
498
+ export const RotateAgentKeyInput = z.strictObject({ nodeId: IntelId });
384
499
  /**
385
- * What `POST /nodes/agents` and `agent_create` answer, and the only shape carrying a key. Every
386
- * other agent read answers with the plain `NodeAgent` above.
500
+ * What replacing an agent's key answers.
387
501
  *
388
- * ⚠️ `applicationKey` is `null` on a REPLAY, and that is the honest answer rather than a gap. An
389
- * idempotency key repeated after the first response was lost still returns the agent that exists —
390
- * the promise every create in Intel makes but the key belonged to the one response that created
391
- * it and is gone from Gate. Minting a second principal to fill this field would leave the
392
- * installation with two machine accounts for one agent, one of which nobody would ever switch off.
393
- * `application_rotate_key` in Gate is the way to a new key, and it is a deliberate act.
502
+ * ⚠️ No key, and that is the whole shape of D29: Intel asks Gate for a new one, hands it to the
503
+ * runtime over the service binding, and forgets it inside the same call. What the caller gets is
504
+ * the fact that it happened, so a screen can say so `applicationId` names the principal whose key
505
+ * was replaced, which is a name and not a credential.
394
506
  */
395
- export const CreatedAgent = NodeAgent.extend({
396
- applicationKey: AgentApplicationKey.nullable(),
507
+ export const AgentKeyRotated = z.strictObject({
508
+ nodeId: IntelId,
509
+ applicationId: GateApplicationId,
510
+ rotatedAt: IsoDateTime,
397
511
  });
398
512
  export const AgentList = z.strictObject({ items: z.array(Node) });
399
513
  // ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
@@ -568,8 +682,11 @@ export const ToolSourceUrl = z.url().refine((value) => {
568
682
  return false;
569
683
  }
570
684
  }, "The portal must use an approved public HTTPS host without embedded credentials");
571
- // The portal namespaces every upstream tool, so the name alone identifies the target server. Intel
572
- // never learns which server that is the portal resolves it and attaches the credentials.
685
+ // The portal namespaces every upstream tool, so the name alone identifies the target server. The
686
+ // portal is still the one that resolves it and attaches the credentials — Intel never holds an
687
+ // upstream credential. Since D30 Intel does read the namespace for one purpose: attributing a tool
688
+ // to a server the portal's own `portal_list_servers` already named, so a delegation can be cut to
689
+ // whole servers. That is attribution, not routing.
573
690
  export const ToolName = z.string().min(1).max(240);
574
691
  export const ToolAnnotations = z.strictObject({
575
692
  title: z.string().max(240).optional(),
@@ -593,6 +710,58 @@ export const ToolCatalog = z.strictObject({
593
710
  portalConnected: z.boolean(),
594
711
  items: z.array(ToolCapability),
595
712
  });
713
+ /**
714
+ * One MCP server the asking user reaches right now, as the portal itself names it (D30).
715
+ *
716
+ * ⚠️ `toolCount` is a fact about this moment and this user, not a size. It exists so a picker can
717
+ * say "9 tools" instead of showing a handle alone, and it must never be read as what an agent will
718
+ * get: the delegated run asks the portal again, with the delegator's token.
719
+ */
720
+ export const ToolServer = z.strictObject({
721
+ handle: ToolServerHandle,
722
+ name: z.string().min(1).max(240),
723
+ toolCount: z.number().int().min(0),
724
+ });
725
+ // The same live-query rule as the tool catalog, one level up. `portalConnected: false` is the state
726
+ // of somebody who has not signed into the portal yet, and it is not an error.
727
+ export const ToolServerCatalog = z.strictObject({
728
+ portalConnected: z.boolean(),
729
+ items: z.array(ToolServer),
730
+ });
731
+ /**
732
+ * Which of the named servers a tool belongs to, or `null` for none of them.
733
+ *
734
+ * ⚠️ THE TRAP: a tool name does not say where its server name ends.
735
+ *
736
+ * The portal writes `<server>_<tool>`, and both halves may contain underscores — `intel_flow_get`
737
+ * reads equally well as server `intel` with tool `flow_get` and as a server called `intel_flow`
738
+ * with tool `get`. Splitting on the first underscore is therefore a guess that is wrong the day
739
+ * somebody adds a server whose name contains one, and on the API side being wrong means an agent
740
+ * delegated server A quietly reaching server B.
741
+ *
742
+ * So the prefix is never split. It is only ever MATCHED against handles the portal itself named,
743
+ * and the longest match wins: with `intel` and `intel_flow` both declared, `intel_flow_get` belongs
744
+ * to `intel_flow`, which is the only reading in which both declarations stay true.
745
+ *
746
+ * ⚠️ This lives in the contract because HOW A NAME IS READ is a property of the wire, and both
747
+ * surfaces read the same wire: `packages/api` cuts a delegation with it, `packages/ui` groups the
748
+ * tools screen with it (#212). A second implementation in the browser would be the third answer to
749
+ * one question — the underscore rule has already been answered differently in two places once
750
+ * (#106, #107), and the copies disagreed. What deliberately stays OUT of here is everything about
751
+ * reach: which handles are declared, which are enabled, which may be delegated and which one owns
752
+ * the portal's own management tools are decisions with consequences, and they belong to
753
+ * `packages/api/src/tools/tool-servers`. This function only reads a name.
754
+ */
755
+ export function serverOf(toolName, handles) {
756
+ let best = null;
757
+ for (const handle of handles) {
758
+ if (!toolName.startsWith(`${handle}_`))
759
+ continue;
760
+ if (best === null || handle.length > best.length)
761
+ best = handle;
762
+ }
763
+ return best;
764
+ }
596
765
  export const TestToolInput = z.strictObject({
597
766
  name: ToolName,
598
767
  arguments: z.record(z.string(), z.unknown()).default({}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-contract",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {