@anchrd/intel-contract 0.5.0 → 0.7.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.
@@ -496,18 +496,53 @@ export declare const AgentModel: z.ZodObject<{
496
496
  }, z.core.$strict>;
497
497
  export type AgentModel = z.infer<typeof AgentModel>;
498
498
  /**
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.
499
+ * One MCP server as the portal names it. The handle is what the portal puts in front of every tool
500
+ * that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
501
+ * Intel can both store and recognise again in a live `tools/list`.
503
502
  *
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.
503
+ * ⚠️ A handle is never invented from a tool name. Which servers exist is the portal's answer
504
+ * (`portal_list_servers`), and the prefix is only used to attribute a tool to a server that answer
505
+ * already named see `packages/api/src/tools/tool-servers` for why splitting on the underscore
506
+ * alone would be ambiguous.
509
507
  */
508
+ export declare const ToolServerHandle: z.ZodString;
509
+ export type ToolServerHandle = z.infer<typeof ToolServerHandle>;
510
+ /**
511
+ * What a caller may ASK for: whole MCP servers, and nothing about who delegates them (D30).
512
+ *
513
+ * ⚠️ The absence of `delegatedBy` is the point, and it is why the write shape differs from the read
514
+ * shape at all. Intel writes that field from the session it authorized; a caller who could name
515
+ * somebody else would be handing an agent a portal connection they do not have, and the agent would
516
+ * act on it unattended. Leaving the field out of the input makes that structural instead of a
517
+ * runtime overwrite: a body carrying it is a parse error at the boundary, on every surface, and no
518
+ * screen ever has to invent a value it has no business knowing.
519
+ */
520
+ export declare const AgentToolSelection: z.ZodObject<{
521
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
522
+ }, z.core.$strict>;
523
+ export type AgentToolSelection = z.infer<typeof AgentToolSelection>;
524
+ /**
525
+ * What is STORED and read back: the selection plus whose portal connection it came from (D30).
526
+ *
527
+ * ⚠️ This is a selection, not a permission. Nothing here grants anything: whether a server is
528
+ * reachable is still decided by one live `tools/list` with `delegatedBy`'s own portal token, so a
529
+ * delegator who loses the server or the connection takes it away from the agent at the next run
530
+ * with no edit to this document.
531
+ *
532
+ * ⚠️ Read shape only. It appears in `AgentDefinition` and never in an input — see
533
+ * `AgentToolSelection` for why the two are deliberately different documents rather than one schema
534
+ * with an optional field.
535
+ */
536
+ export declare const AgentToolDelegation: z.ZodObject<{
537
+ delegatedBy: z.ZodString;
538
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
539
+ }, z.core.$strict>;
540
+ export type AgentToolDelegation = z.infer<typeof AgentToolDelegation>;
510
541
  export declare const AgentDefinition: z.ZodObject<{
542
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
543
+ delegatedBy: z.ZodString;
544
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
545
+ }, z.core.$strict>>>;
511
546
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
512
547
  nodeId: z.ZodString;
513
548
  role: z.ZodEnum<{
@@ -535,10 +570,55 @@ export declare const AgentDefinition: z.ZodObject<{
535
570
  }, z.core.$strict>;
536
571
  }, z.core.$strict>;
537
572
  export type AgentDefinition = z.infer<typeof AgentDefinition>;
573
+ /**
574
+ * The same document as `AgentDefinition`, minus the one field a caller may not write.
575
+ *
576
+ * ⚠️ Two schemas rather than one, and the split is load-bearing (#208, D30). Everything an agent IS
577
+ * comes from whoever edits it; **whose portal connection it acts on** does not, because that is an
578
+ * authority the editor would be granting to themselves. So the write shape simply has no place to
579
+ * put it: `{ tools: { servers: [...] } }` is what a screen or an MCP client sends, Intel adds
580
+ * `delegatedBy` from the session, and a body that tries to name one is refused by the strict object
581
+ * before any of it is read. The reading shape keeps the field because a reader must be able to see
582
+ * whose connection an agent runs on.
583
+ */
584
+ export declare const AgentDefinitionInput: z.ZodObject<{
585
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
586
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
587
+ }, z.core.$strict>>>;
588
+ references: z.ZodDefault<z.ZodArray<z.ZodObject<{
589
+ nodeId: z.ZodString;
590
+ role: z.ZodEnum<{
591
+ "system-message": "system-message";
592
+ "semantic-context": "semantic-context";
593
+ memory: "memory";
594
+ }>;
595
+ }, z.core.$strict>>>;
596
+ schedules: z.ZodDefault<z.ZodArray<z.ZodObject<{
597
+ cron: z.ZodString;
598
+ target: z.ZodObject<{
599
+ kind: z.ZodEnum<{
600
+ document: "document";
601
+ flow: "flow";
602
+ }>;
603
+ id: z.ZodString;
604
+ }, z.core.$strict>;
605
+ }, z.core.$strict>>>;
606
+ model: z.ZodObject<{
607
+ provider: z.ZodEnum<{
608
+ "workers-ai": "workers-ai";
609
+ anthropic: "anthropic";
610
+ }>;
611
+ model: z.ZodString;
612
+ }, z.core.$strict>;
613
+ }, z.core.$strict>;
614
+ export type AgentDefinitionInput = z.infer<typeof AgentDefinitionInput>;
538
615
  export declare const SaveAgentDefinitionInput: z.ZodObject<{
539
616
  nodeId: z.ZodString;
540
617
  baseVersionId: z.ZodNullable<z.ZodString>;
541
618
  definition: z.ZodObject<{
619
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
620
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
621
+ }, z.core.$strict>>>;
542
622
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
543
623
  nodeId: z.ZodString;
544
624
  role: z.ZodEnum<{
@@ -597,6 +677,9 @@ export declare const CreateAgentInput: z.ZodObject<{
597
677
  title: z.ZodString;
598
678
  description: z.ZodDefault<z.ZodNullable<z.ZodString>>;
599
679
  definition: z.ZodObject<{
680
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
681
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
682
+ }, z.core.$strict>>>;
600
683
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
601
684
  nodeId: z.ZodString;
602
685
  role: z.ZodEnum<{
@@ -662,6 +745,10 @@ export declare const NodeAgent: z.ZodObject<{
662
745
  createdAt: z.ZodISODateTime;
663
746
  }, z.core.$strict>>;
664
747
  definition: z.ZodNullable<z.ZodObject<{
748
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
749
+ delegatedBy: z.ZodString;
750
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
751
+ }, z.core.$strict>>>;
665
752
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
666
753
  nodeId: z.ZodString;
667
754
  role: z.ZodEnum<{
@@ -692,36 +779,19 @@ export declare const NodeAgent: z.ZodObject<{
692
779
  }, z.core.$strict>;
693
780
  export type NodeAgent = z.infer<typeof NodeAgent>;
694
781
  /**
695
- * The single value in Intel's wire formats that IS a credential (#182, D27).
782
+ * ⚠️ There is NO key field in this file, and adding one back would be the regression (D29, #207).
696
783
  *
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.
784
+ * Until #207 the create answer carried the Application key in plain text, once, and a person had to
785
+ * carry it into a Worker secret by hand which is why an agent created through the screen could
786
+ * never run (#200). The key now goes from Gate straight into the agent runtime over Intel's service
787
+ * binding and is encrypted into that agent's Durable Object; it reaches no browser, no MCP tool
788
+ * result and no response body at all. `NodeAgent` is a `z.strictObject`, so a field named `key`
789
+ * added anywhere in this file is a parse error at the boundary rather than a leak somebody has to
790
+ * spot in review.
703
791
  *
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.
792
+ * What `POST /nodes/agents` and `agent_create` answer is therefore exactly what every read answers:
793
+ * the node, its first definition, and the `applicationId` that NAMES the principal without
794
+ * authenticating it.
725
795
  */
726
796
  export declare const CreatedAgent: z.ZodObject<{
727
797
  node: z.ZodObject<{
@@ -758,6 +828,10 @@ export declare const CreatedAgent: z.ZodObject<{
758
828
  createdAt: z.ZodISODateTime;
759
829
  }, z.core.$strict>>;
760
830
  definition: z.ZodNullable<z.ZodObject<{
831
+ tools: z.ZodDefault<z.ZodNullable<z.ZodObject<{
832
+ delegatedBy: z.ZodString;
833
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
834
+ }, z.core.$strict>>>;
761
835
  references: z.ZodDefault<z.ZodArray<z.ZodObject<{
762
836
  nodeId: z.ZodString;
763
837
  role: z.ZodEnum<{
@@ -785,14 +859,26 @@ export declare const CreatedAgent: z.ZodObject<{
785
859
  }, z.core.$strict>;
786
860
  }, z.core.$strict>>;
787
861
  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
862
  }, z.core.$strict>;
795
863
  export type CreatedAgent = z.infer<typeof CreatedAgent>;
864
+ export declare const RotateAgentKeyInput: z.ZodObject<{
865
+ nodeId: z.ZodString;
866
+ }, z.core.$strict>;
867
+ export type RotateAgentKeyInput = z.infer<typeof RotateAgentKeyInput>;
868
+ /**
869
+ * What replacing an agent's key answers.
870
+ *
871
+ * ⚠️ No key, and that is the whole shape of D29: Intel asks Gate for a new one, hands it to the
872
+ * runtime over the service binding, and forgets it inside the same call. What the caller gets is
873
+ * the fact that it happened, so a screen can say so — `applicationId` names the principal whose key
874
+ * was replaced, which is a name and not a credential.
875
+ */
876
+ export declare const AgentKeyRotated: z.ZodObject<{
877
+ nodeId: z.ZodString;
878
+ applicationId: z.ZodString;
879
+ rotatedAt: z.ZodISODateTime;
880
+ }, z.core.$strict>;
881
+ export type AgentKeyRotated = z.infer<typeof AgentKeyRotated>;
796
882
  export declare const AgentList: z.ZodObject<{
797
883
  items: z.ZodArray<z.ZodObject<{
798
884
  id: z.ZodString;
@@ -1141,6 +1227,53 @@ export declare const ToolCatalog: z.ZodObject<{
1141
1227
  }, z.core.$strict>>;
1142
1228
  }, z.core.$strict>;
1143
1229
  export type ToolCatalog = z.infer<typeof ToolCatalog>;
1230
+ /**
1231
+ * One MCP server the asking user reaches right now, as the portal itself names it (D30).
1232
+ *
1233
+ * ⚠️ `toolCount` is a fact about this moment and this user, not a size. It exists so a picker can
1234
+ * say "9 tools" instead of showing a handle alone, and it must never be read as what an agent will
1235
+ * get: the delegated run asks the portal again, with the delegator's token.
1236
+ */
1237
+ export declare const ToolServer: z.ZodObject<{
1238
+ handle: z.ZodString;
1239
+ name: z.ZodString;
1240
+ toolCount: z.ZodNumber;
1241
+ }, z.core.$strict>;
1242
+ export type ToolServer = z.infer<typeof ToolServer>;
1243
+ export declare const ToolServerCatalog: z.ZodObject<{
1244
+ portalConnected: z.ZodBoolean;
1245
+ items: z.ZodArray<z.ZodObject<{
1246
+ handle: z.ZodString;
1247
+ name: z.ZodString;
1248
+ toolCount: z.ZodNumber;
1249
+ }, z.core.$strict>>;
1250
+ }, z.core.$strict>;
1251
+ export type ToolServerCatalog = z.infer<typeof ToolServerCatalog>;
1252
+ /**
1253
+ * Which of the named servers a tool belongs to, or `null` for none of them.
1254
+ *
1255
+ * ⚠️ THE TRAP: a tool name does not say where its server name ends.
1256
+ *
1257
+ * The portal writes `<server>_<tool>`, and both halves may contain underscores — `intel_flow_get`
1258
+ * reads equally well as server `intel` with tool `flow_get` and as a server called `intel_flow`
1259
+ * with tool `get`. Splitting on the first underscore is therefore a guess that is wrong the day
1260
+ * somebody adds a server whose name contains one, and on the API side being wrong means an agent
1261
+ * delegated server A quietly reaching server B.
1262
+ *
1263
+ * So the prefix is never split. It is only ever MATCHED against handles the portal itself named,
1264
+ * and the longest match wins: with `intel` and `intel_flow` both declared, `intel_flow_get` belongs
1265
+ * to `intel_flow`, which is the only reading in which both declarations stay true.
1266
+ *
1267
+ * ⚠️ This lives in the contract because HOW A NAME IS READ is a property of the wire, and both
1268
+ * surfaces read the same wire: `packages/api` cuts a delegation with it, `packages/ui` groups the
1269
+ * tools screen with it (#212). A second implementation in the browser would be the third answer to
1270
+ * one question — the underscore rule has already been answered differently in two places once
1271
+ * (#106, #107), and the copies disagreed. What deliberately stays OUT of here is everything about
1272
+ * reach: which handles are declared, which are enabled, which may be delegated and which one owns
1273
+ * the portal's own management tools are decisions with consequences, and they belong to
1274
+ * `packages/api/src/tools/tool-servers`. This function only reads a name.
1275
+ */
1276
+ export declare function serverOf(toolName: string, handles: Iterable<string>): string | null;
1144
1277
  export declare const TestToolInput: z.ZodObject<{
1145
1278
  name: z.ZodString;
1146
1279
  arguments: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -292,10 +292,61 @@ export const AgentModel = z.strictObject({
292
292
  model: z.string().trim().min(1).max(120),
293
293
  });
294
294
  /**
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.
295
+ * One MCP server as the portal names it. The handle is what the portal puts in front of every tool
296
+ * that server offers (`notion_notion-search` belongs to `notion`), and it is the only identifier
297
+ * Intel can both store and recognise again in a live `tools/list`.
298
+ *
299
+ * ⚠️ A handle is never invented from a tool name. Which servers exist is the portal's answer
300
+ * (`portal_list_servers`), and the prefix is only used to attribute a tool to a server that answer
301
+ * already named — see `packages/api/src/tools/tool-servers` for why splitting on the underscore
302
+ * alone would be ambiguous.
303
+ */
304
+ export const ToolServerHandle = z
305
+ .string()
306
+ .trim()
307
+ .min(1)
308
+ .max(120)
309
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "A server handle is the portal's own identifier");
310
+ const ToolServerHandles = z.array(ToolServerHandle).max(32).default([]);
311
+ /**
312
+ * What a caller may ASK for: whole MCP servers, and nothing about who delegates them (D30).
313
+ *
314
+ * ⚠️ The absence of `delegatedBy` is the point, and it is why the write shape differs from the read
315
+ * shape at all. Intel writes that field from the session it authorized; a caller who could name
316
+ * somebody else would be handing an agent a portal connection they do not have, and the agent would
317
+ * act on it unattended. Leaving the field out of the input makes that structural instead of a
318
+ * runtime overwrite: a body carrying it is a parse error at the boundary, on every surface, and no
319
+ * screen ever has to invent a value it has no business knowing.
320
+ */
321
+ export const AgentToolSelection = z.strictObject({ servers: ToolServerHandles });
322
+ /**
323
+ * What is STORED and read back: the selection plus whose portal connection it came from (D30).
324
+ *
325
+ * ⚠️ This is a selection, not a permission. Nothing here grants anything: whether a server is
326
+ * reachable is still decided by one live `tools/list` with `delegatedBy`'s own portal token, so a
327
+ * delegator who loses the server or the connection takes it away from the agent at the next run
328
+ * with no edit to this document.
329
+ *
330
+ * ⚠️ Read shape only. It appears in `AgentDefinition` and never in an input — see
331
+ * `AgentToolSelection` for why the two are deliberately different documents rather than one schema
332
+ * with an optional field.
333
+ */
334
+ export const AgentToolDelegation = z.strictObject({
335
+ delegatedBy: IntelId,
336
+ servers: ToolServerHandles,
337
+ });
338
+ /**
339
+ * ⚠️ No accounts, no secrets and no channels — and the reason is mechanical rather than tidy
340
+ * (ADR-0005 §4): this body is read, shared, exported and put into model context, so a secret in it
341
+ * is a secret in a citation. Identity is Gate's, accounts are the portal's, channels are runtime
342
+ * configuration.
343
+ *
344
+ * ⚠️ `tools` is the one correction to that list (D30), and it is narrower than it looks. What is
345
+ * stored is a **selection of whole servers plus who delegated them**, never a mirrored permission
346
+ * and never a catalog: the catalog stays a live `tools/list` made with the delegator's token at the
347
+ * moment the agent runs. ADR-0005 §4's "no tools in the definition" forbade the mirror, and the
348
+ * mirror is still forbidden — a tool name, a schema or an account in here would be the thing that
349
+ * line was written against.
299
350
  *
300
351
  * ⚠️ Strict on purpose, and deliberately stricter than the runtime's own reader
301
352
  * (`packages/agent/src/definition/definition.ts`, which is `z.object`). Intel is the writer: an
@@ -303,15 +354,37 @@ export const AgentModel = z.strictObject({
303
354
  * reader and released separately, so it must keep starting agents when Intel adds a field
304
355
  * tomorrow. The asymmetry is the point, not an oversight.
305
356
  */
306
- export const AgentDefinition = z.strictObject({
357
+ const AgentBody = {
307
358
  references: z.array(AgentReference).max(200).default([]),
308
359
  schedules: z.array(AgentSchedule).max(50).default([]),
309
360
  model: AgentModel,
361
+ };
362
+ export const AgentDefinition = z.strictObject({
363
+ ...AgentBody,
364
+ // `null` is "this agent has no tools", and it is also what every definition written before D30
365
+ // parses to. An empty `servers` list means the same thing and is kept as its own state so
366
+ // removing the last server does not have to erase who was delegating.
367
+ tools: AgentToolDelegation.nullable().default(null),
368
+ });
369
+ /**
370
+ * The same document as `AgentDefinition`, minus the one field a caller may not write.
371
+ *
372
+ * ⚠️ Two schemas rather than one, and the split is load-bearing (#208, D30). Everything an agent IS
373
+ * comes from whoever edits it; **whose portal connection it acts on** does not, because that is an
374
+ * authority the editor would be granting to themselves. So the write shape simply has no place to
375
+ * put it: `{ tools: { servers: [...] } }` is what a screen or an MCP client sends, Intel adds
376
+ * `delegatedBy` from the session, and a body that tries to name one is refused by the strict object
377
+ * before any of it is read. The reading shape keeps the field because a reader must be able to see
378
+ * whose connection an agent runs on.
379
+ */
380
+ export const AgentDefinitionInput = z.strictObject({
381
+ ...AgentBody,
382
+ tools: AgentToolSelection.nullable().default(null),
310
383
  });
311
384
  export const SaveAgentDefinitionInput = z.strictObject({
312
385
  nodeId: IntelId,
313
386
  baseVersionId: IntelId.nullable(),
314
- definition: AgentDefinition,
387
+ definition: AgentDefinitionInput,
315
388
  idempotencyKey: z.string().min(8).max(200),
316
389
  });
317
390
  export const GetAgentInput = z.strictObject({ nodeId: IntelId });
@@ -339,7 +412,7 @@ export const CreateAgentInput = z.strictObject({
339
412
  parentId: IntelId.nullable().default(null),
340
413
  title: z.string().trim().min(1).max(240),
341
414
  description: z.string().trim().max(2_000).nullable().default(null),
342
- definition: AgentDefinition,
415
+ definition: AgentDefinitionInput,
343
416
  idempotencyKey: z.string().min(8).max(200),
344
417
  });
345
418
  // The ID of the Gate Application an agent runs as. Deliberately NOT an `IntelId`: it is Better
@@ -362,38 +435,37 @@ export const NodeAgent = z.strictObject({
362
435
  applicationId: GateApplicationId.nullable(),
363
436
  });
364
437
  /**
365
- * The single value in Intel's wire formats that IS a credential (#182, D27).
438
+ * ⚠️ There is NO key field in this file, and adding one back would be the regression (D29, #207).
366
439
  *
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.
440
+ * Until #207 the create answer carried the Application key in plain text, once, and a person had to
441
+ * carry it into a Worker secret by hand which is why an agent created through the screen could
442
+ * never run (#200). The key now goes from Gate straight into the agent runtime over Intel's service
443
+ * binding and is encrypted into that agent's Durable Object; it reaches no browser, no MCP tool
444
+ * result and no response body at all. `NodeAgent` is a `z.strictObject`, so a field named `key`
445
+ * added anywhere in this file is a parse error at the boundary rather than a leak somebody has to
446
+ * spot in review.
373
447
  *
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.
448
+ * What `POST /nodes/agents` and `agent_create` answer is therefore exactly what every read answers:
449
+ * the node, its first definition, and the `applicationId` that NAMES the principal without
450
+ * authenticating it.
377
451
  */
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
- });
452
+ export const CreatedAgent = NodeAgent;
453
+ // Which agent's key is being replaced. `nodeId` and not the Application ID: this addresses an agent
454
+ // in Intel's tree, and the Application behind it is Intel's to look up — a caller naming the
455
+ // principal directly would be rotating a key for an agent nobody checked they may edit.
456
+ export const RotateAgentKeyInput = z.strictObject({ nodeId: IntelId });
384
457
  /**
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.
458
+ * What replacing an agent's key answers.
387
459
  *
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.
460
+ * ⚠️ No key, and that is the whole shape of D29: Intel asks Gate for a new one, hands it to the
461
+ * runtime over the service binding, and forgets it inside the same call. What the caller gets is
462
+ * the fact that it happened, so a screen can say so `applicationId` names the principal whose key
463
+ * was replaced, which is a name and not a credential.
394
464
  */
395
- export const CreatedAgent = NodeAgent.extend({
396
- applicationKey: AgentApplicationKey.nullable(),
465
+ export const AgentKeyRotated = z.strictObject({
466
+ nodeId: IntelId,
467
+ applicationId: GateApplicationId,
468
+ rotatedAt: IsoDateTime,
397
469
  });
398
470
  export const AgentList = z.strictObject({ items: z.array(Node) });
399
471
  // ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
@@ -568,8 +640,11 @@ export const ToolSourceUrl = z.url().refine((value) => {
568
640
  return false;
569
641
  }
570
642
  }, "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.
643
+ // The portal namespaces every upstream tool, so the name alone identifies the target server. The
644
+ // portal is still the one that resolves it and attaches the credentials — Intel never holds an
645
+ // upstream credential. Since D30 Intel does read the namespace for one purpose: attributing a tool
646
+ // to a server the portal's own `portal_list_servers` already named, so a delegation can be cut to
647
+ // whole servers. That is attribution, not routing.
573
648
  export const ToolName = z.string().min(1).max(240);
574
649
  export const ToolAnnotations = z.strictObject({
575
650
  title: z.string().max(240).optional(),
@@ -593,6 +668,58 @@ export const ToolCatalog = z.strictObject({
593
668
  portalConnected: z.boolean(),
594
669
  items: z.array(ToolCapability),
595
670
  });
671
+ /**
672
+ * One MCP server the asking user reaches right now, as the portal itself names it (D30).
673
+ *
674
+ * ⚠️ `toolCount` is a fact about this moment and this user, not a size. It exists so a picker can
675
+ * say "9 tools" instead of showing a handle alone, and it must never be read as what an agent will
676
+ * get: the delegated run asks the portal again, with the delegator's token.
677
+ */
678
+ export const ToolServer = z.strictObject({
679
+ handle: ToolServerHandle,
680
+ name: z.string().min(1).max(240),
681
+ toolCount: z.number().int().min(0),
682
+ });
683
+ // The same live-query rule as the tool catalog, one level up. `portalConnected: false` is the state
684
+ // of somebody who has not signed into the portal yet, and it is not an error.
685
+ export const ToolServerCatalog = z.strictObject({
686
+ portalConnected: z.boolean(),
687
+ items: z.array(ToolServer),
688
+ });
689
+ /**
690
+ * Which of the named servers a tool belongs to, or `null` for none of them.
691
+ *
692
+ * ⚠️ THE TRAP: a tool name does not say where its server name ends.
693
+ *
694
+ * The portal writes `<server>_<tool>`, and both halves may contain underscores — `intel_flow_get`
695
+ * reads equally well as server `intel` with tool `flow_get` and as a server called `intel_flow`
696
+ * with tool `get`. Splitting on the first underscore is therefore a guess that is wrong the day
697
+ * somebody adds a server whose name contains one, and on the API side being wrong means an agent
698
+ * delegated server A quietly reaching server B.
699
+ *
700
+ * So the prefix is never split. It is only ever MATCHED against handles the portal itself named,
701
+ * and the longest match wins: with `intel` and `intel_flow` both declared, `intel_flow_get` belongs
702
+ * to `intel_flow`, which is the only reading in which both declarations stay true.
703
+ *
704
+ * ⚠️ This lives in the contract because HOW A NAME IS READ is a property of the wire, and both
705
+ * surfaces read the same wire: `packages/api` cuts a delegation with it, `packages/ui` groups the
706
+ * tools screen with it (#212). A second implementation in the browser would be the third answer to
707
+ * one question — the underscore rule has already been answered differently in two places once
708
+ * (#106, #107), and the copies disagreed. What deliberately stays OUT of here is everything about
709
+ * reach: which handles are declared, which are enabled, which may be delegated and which one owns
710
+ * the portal's own management tools are decisions with consequences, and they belong to
711
+ * `packages/api/src/tools/tool-servers`. This function only reads a name.
712
+ */
713
+ export function serverOf(toolName, handles) {
714
+ let best = null;
715
+ for (const handle of handles) {
716
+ if (!toolName.startsWith(`${handle}_`))
717
+ continue;
718
+ if (best === null || handle.length > best.length)
719
+ best = handle;
720
+ }
721
+ return best;
722
+ }
596
723
  export const TestToolInput = z.strictObject({
597
724
  name: ToolName,
598
725
  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.7.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {