@kindgi/api 0.1.3 → 0.1.4-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/dist/agent-binding.d.ts +26 -4
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/agent-pins.d.ts +48 -0
  4. package/dist/agent-pins.d.ts.map +1 -0
  5. package/dist/agent-pins.js +102 -0
  6. package/dist/agent-pins.js.map +1 -0
  7. package/dist/app.d.ts +22 -0
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +27 -3
  10. package/dist/app.js.map +1 -1
  11. package/dist/block-binding.d.ts +132 -0
  12. package/dist/block-binding.d.ts.map +1 -0
  13. package/dist/block-binding.js +4 -0
  14. package/dist/block-binding.js.map +1 -0
  15. package/dist/block-pins.d.ts +22 -0
  16. package/dist/block-pins.d.ts.map +1 -0
  17. package/dist/block-pins.js +112 -0
  18. package/dist/block-pins.js.map +1 -0
  19. package/dist/cost-binding.d.ts +20 -1
  20. package/dist/cost-binding.d.ts.map +1 -1
  21. package/dist/cost-binding.js +4 -0
  22. package/dist/cost-binding.js.map +1 -1
  23. package/dist/deploy-versions.d.ts +60 -0
  24. package/dist/deploy-versions.d.ts.map +1 -0
  25. package/dist/deploy-versions.js +91 -0
  26. package/dist/deploy-versions.js.map +1 -0
  27. package/dist/deployment-binding.d.ts +24 -3
  28. package/dist/deployment-binding.d.ts.map +1 -1
  29. package/dist/derive-agent-version.d.ts +77 -0
  30. package/dist/derive-agent-version.d.ts.map +1 -0
  31. package/dist/derive-agent-version.js +149 -0
  32. package/dist/derive-agent-version.js.map +1 -0
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +18 -0
  35. package/dist/errors.js.map +1 -1
  36. package/dist/eval-case-binding.d.ts +65 -0
  37. package/dist/eval-case-binding.d.ts.map +1 -0
  38. package/dist/eval-case-binding.js +4 -0
  39. package/dist/eval-case-binding.js.map +1 -0
  40. package/dist/eval-run-binding.d.ts +37 -0
  41. package/dist/eval-run-binding.d.ts.map +1 -1
  42. package/dist/eval-run-binding.js.map +1 -1
  43. package/dist/eval-run-dispatcher.d.ts +41 -3
  44. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  45. package/dist/eval-run-dispatcher.js +21 -15
  46. package/dist/eval-run-dispatcher.js.map +1 -1
  47. package/dist/eval-suite-binding.d.ts +1 -1
  48. package/dist/eval-suite-binding.d.ts.map +1 -1
  49. package/dist/eval-suite-binding.js +2 -0
  50. package/dist/eval-suite-binding.js.map +1 -1
  51. package/dist/flow-binding.d.ts +18 -4
  52. package/dist/flow-binding.d.ts.map +1 -1
  53. package/dist/flow-pins.d.ts +36 -0
  54. package/dist/flow-pins.d.ts.map +1 -0
  55. package/dist/flow-pins.js +81 -0
  56. package/dist/flow-pins.js.map +1 -0
  57. package/dist/guardrail-binding.d.ts +8 -0
  58. package/dist/guardrail-binding.d.ts.map +1 -1
  59. package/dist/handler-binding.d.ts +3 -0
  60. package/dist/handler-binding.d.ts.map +1 -1
  61. package/dist/index.d.ts +18 -6
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +8 -2
  64. package/dist/index.js.map +1 -1
  65. package/dist/judged-dispatcher.d.ts +138 -0
  66. package/dist/judged-dispatcher.d.ts.map +1 -0
  67. package/dist/judged-dispatcher.js +308 -0
  68. package/dist/judged-dispatcher.js.map +1 -0
  69. package/dist/judged-items.d.ts +86 -0
  70. package/dist/judged-items.d.ts.map +1 -0
  71. package/dist/judged-items.js +184 -0
  72. package/dist/judged-items.js.map +1 -0
  73. package/dist/judgment-binding.d.ts +316 -0
  74. package/dist/judgment-binding.d.ts.map +1 -0
  75. package/dist/judgment-binding.js +19 -0
  76. package/dist/judgment-binding.js.map +1 -0
  77. package/dist/openapi/generate.d.ts.map +1 -1
  78. package/dist/openapi/generate.js +4 -1
  79. package/dist/openapi/generate.js.map +1 -1
  80. package/dist/openapi/operations.d.ts.map +1 -1
  81. package/dist/openapi/operations.js +482 -13
  82. package/dist/openapi/operations.js.map +1 -1
  83. package/dist/openapi/schemas.d.ts +46 -0
  84. package/dist/openapi/schemas.d.ts.map +1 -1
  85. package/dist/openapi/schemas.js +1350 -175
  86. package/dist/openapi/schemas.js.map +1 -1
  87. package/dist/provider-binding.d.ts +12 -7
  88. package/dist/provider-binding.d.ts.map +1 -1
  89. package/dist/registry-read-only.d.ts +32 -0
  90. package/dist/registry-read-only.d.ts.map +1 -0
  91. package/dist/registry-read-only.js +22 -0
  92. package/dist/registry-read-only.js.map +1 -0
  93. package/dist/routes/agents.d.ts +9 -1
  94. package/dist/routes/agents.d.ts.map +1 -1
  95. package/dist/routes/agents.js +181 -11
  96. package/dist/routes/agents.js.map +1 -1
  97. package/dist/routes/blocks.d.ts +19 -0
  98. package/dist/routes/blocks.d.ts.map +1 -0
  99. package/dist/routes/blocks.js +306 -0
  100. package/dist/routes/blocks.js.map +1 -0
  101. package/dist/routes/cost.d.ts.map +1 -1
  102. package/dist/routes/cost.js +47 -2
  103. package/dist/routes/cost.js.map +1 -1
  104. package/dist/routes/deployments.d.ts +3 -0
  105. package/dist/routes/deployments.d.ts.map +1 -1
  106. package/dist/routes/deployments.js +224 -55
  107. package/dist/routes/deployments.js.map +1 -1
  108. package/dist/routes/eval-comparison.d.ts +15 -0
  109. package/dist/routes/eval-comparison.d.ts.map +1 -0
  110. package/dist/routes/eval-comparison.js +123 -0
  111. package/dist/routes/eval-comparison.js.map +1 -0
  112. package/dist/routes/eval-runs.d.ts +7 -1
  113. package/dist/routes/eval-runs.d.ts.map +1 -1
  114. package/dist/routes/eval-runs.js +42 -3
  115. package/dist/routes/eval-runs.js.map +1 -1
  116. package/dist/routes/eval-versions.d.ts +25 -0
  117. package/dist/routes/eval-versions.d.ts.map +1 -0
  118. package/dist/routes/eval-versions.js +66 -0
  119. package/dist/routes/eval-versions.js.map +1 -0
  120. package/dist/routes/flows.d.ts +13 -1
  121. package/dist/routes/flows.d.ts.map +1 -1
  122. package/dist/routes/flows.js +48 -3
  123. package/dist/routes/flows.js.map +1 -1
  124. package/dist/routes/guardrails.d.ts.map +1 -1
  125. package/dist/routes/guardrails.js +4 -0
  126. package/dist/routes/guardrails.js.map +1 -1
  127. package/dist/routes/hierarchy-errors.d.ts +45 -0
  128. package/dist/routes/hierarchy-errors.d.ts.map +1 -0
  129. package/dist/routes/hierarchy-errors.js +47 -0
  130. package/dist/routes/hierarchy-errors.js.map +1 -0
  131. package/dist/routes/judged-suites.d.ts +20 -0
  132. package/dist/routes/judged-suites.d.ts.map +1 -0
  133. package/dist/routes/judged-suites.js +272 -0
  134. package/dist/routes/judged-suites.js.map +1 -0
  135. package/dist/routes/judgment-context.d.ts +22 -0
  136. package/dist/routes/judgment-context.d.ts.map +1 -0
  137. package/dist/routes/judgment-context.js +88 -0
  138. package/dist/routes/judgment-context.js.map +1 -0
  139. package/dist/routes/judgment-flow-context.d.ts +32 -0
  140. package/dist/routes/judgment-flow-context.d.ts.map +1 -0
  141. package/dist/routes/judgment-flow-context.js +195 -0
  142. package/dist/routes/judgment-flow-context.js.map +1 -0
  143. package/dist/routes/judgments.d.ts +41 -0
  144. package/dist/routes/judgments.d.ts.map +1 -0
  145. package/dist/routes/judgments.js +566 -0
  146. package/dist/routes/judgments.js.map +1 -0
  147. package/dist/routes/orgs.d.ts +5 -2
  148. package/dist/routes/orgs.d.ts.map +1 -1
  149. package/dist/routes/orgs.js +38 -22
  150. package/dist/routes/orgs.js.map +1 -1
  151. package/dist/routes/policies.d.ts.map +1 -1
  152. package/dist/routes/policies.js +12 -1
  153. package/dist/routes/policies.js.map +1 -1
  154. package/dist/routes/projects.d.ts +10 -2
  155. package/dist/routes/projects.d.ts.map +1 -1
  156. package/dist/routes/projects.js +94 -80
  157. package/dist/routes/projects.js.map +1 -1
  158. package/dist/routes/providers.d.ts.map +1 -1
  159. package/dist/routes/providers.js +6 -1
  160. package/dist/routes/providers.js.map +1 -1
  161. package/dist/routes/runs.js +25 -3
  162. package/dist/routes/runs.js.map +1 -1
  163. package/dist/routes/teams.d.ts +6 -2
  164. package/dist/routes/teams.d.ts.map +1 -1
  165. package/dist/routes/teams.js +83 -73
  166. package/dist/routes/teams.js.map +1 -1
  167. package/dist/routes/tools.d.ts.map +1 -1
  168. package/dist/routes/tools.js +4 -0
  169. package/dist/routes/tools.js.map +1 -1
  170. package/dist/tool-binding.d.ts +8 -0
  171. package/dist/tool-binding.d.ts.map +1 -1
  172. package/openapi.json +14253 -10099
  173. package/package.json +21 -21
  174. package/src/agent-binding.ts +28 -4
  175. package/src/agent-pins.ts +147 -0
  176. package/src/app.ts +81 -3
  177. package/src/block-binding.ts +137 -0
  178. package/src/block-pins.ts +148 -0
  179. package/src/cost-binding.ts +21 -1
  180. package/src/deploy-versions.ts +157 -0
  181. package/src/deployment-binding.ts +27 -3
  182. package/src/derive-agent-version.ts +217 -0
  183. package/src/errors.ts +18 -0
  184. package/src/eval-case-binding.ts +71 -0
  185. package/src/eval-run-binding.ts +40 -0
  186. package/src/eval-run-dispatcher.ts +60 -16
  187. package/src/eval-suite-binding.ts +2 -0
  188. package/src/flow-binding.ts +20 -4
  189. package/src/flow-pins.ts +113 -0
  190. package/src/guardrail-binding.ts +9 -0
  191. package/src/handler-binding.ts +3 -0
  192. package/src/index.ts +86 -2
  193. package/src/judged-dispatcher.ts +530 -0
  194. package/src/judged-items.ts +263 -0
  195. package/src/judgment-binding.ts +349 -0
  196. package/src/openapi/generate.ts +7 -1
  197. package/src/openapi/operations.ts +579 -13
  198. package/src/openapi/schemas.ts +1408 -128
  199. package/src/provider-binding.ts +12 -7
  200. package/src/registry-read-only.ts +43 -0
  201. package/src/routes/agents.ts +252 -19
  202. package/src/routes/blocks.ts +387 -0
  203. package/src/routes/cost.ts +55 -1
  204. package/src/routes/deployments.ts +291 -56
  205. package/src/routes/eval-comparison.ts +135 -0
  206. package/src/routes/eval-runs.ts +57 -3
  207. package/src/routes/eval-versions.ts +110 -0
  208. package/src/routes/flows.ts +70 -5
  209. package/src/routes/guardrails.ts +7 -0
  210. package/src/routes/hierarchy-errors.ts +60 -0
  211. package/src/routes/judged-suites.ts +363 -0
  212. package/src/routes/judgment-context.ts +128 -0
  213. package/src/routes/judgment-flow-context.ts +245 -0
  214. package/src/routes/judgments.ts +743 -0
  215. package/src/routes/orgs.ts +44 -27
  216. package/src/routes/policies.ts +19 -0
  217. package/src/routes/projects.ts +118 -96
  218. package/src/routes/providers.ts +5 -0
  219. package/src/routes/runs.ts +29 -3
  220. package/src/routes/teams.ts +104 -90
  221. package/src/routes/tools.ts +7 -0
  222. package/src/tool-binding.ts +9 -0
@@ -5,9 +5,10 @@ import { createHash } from 'node:crypto';
5
5
 
6
6
  import { Hono } from 'hono';
7
7
 
8
- import type { AgentId } from '@kindgi/agents';
8
+ import type { Agent, AgentId, AgentPins } from '@kindgi/agents';
9
9
  import { tuplesForCreate } from '@kindgi/authz';
10
10
  import { parsePublicKeyPem, verifyEd25519 } from '@kindgi/crypto';
11
+ import type { Flow, FlowPins } from '@kindgi/flow';
11
12
  import { type Guardrail, validateGuardrailSpec } from '@kindgi/guardrails';
12
13
  import type { ProjectBinding, Scope } from '@kindgi/platform';
13
14
  import { validateToolManifest } from '@kindgi/tools';
@@ -16,13 +17,20 @@ import type {
16
17
  FlowId,
17
18
  GuardrailId,
18
19
  ProjectId,
20
+ Semver,
19
21
  SigningKeyId,
20
22
  TenantId,
21
23
  ToolId,
22
24
  } from '@kindgi/types';
23
25
 
24
26
  import type { AgentRegistryBinding } from '../agent-binding.js';
27
+ import { type UnpinnableRef, publishDeployedAgent, resolveAgentPins } from '../agent-pins.js';
28
+ import type { BlockRegistryBinding } from '../block-binding.js';
29
+ import type { DeployedVersionOutcome } from '../deploy-versions.js';
25
30
  import type {
31
+ DeployedAgent,
32
+ DeployedFlow,
33
+ DeployedVersion,
26
34
  Deployment,
27
35
  DeploymentBinding,
28
36
  DeploymentPrimitiveCounts,
@@ -30,8 +38,10 @@ import type {
30
38
  } from '../deployment-binding.js';
31
39
  import { statusFor, toWireError } from '../errors.js';
32
40
  import type { FlowRegistryBinding } from '../flow-binding.js';
41
+ import { publishDeployedFlow, resolveFlowPins } from '../flow-pins.js';
33
42
  import type { GuardrailRegistryBinding } from '../guardrail-binding.js';
34
43
  import type { ImageRegistryBinding } from '../image-registry-binding.js';
44
+ import { type RegistryReadOnly, refuseReadOnly } from '../registry-read-only.js';
35
45
  import type { SecretBinding } from '../secrets-binding.js';
36
46
  import type { SigningKeyBinding as SigningKeyRegistryBinding } from '../signing-key-binding.js';
37
47
  import type { ToolRegistryBinding } from '../tool-binding.js';
@@ -112,6 +122,8 @@ export interface DeploymentsRouterBindings {
112
122
  readonly guardrailRegistry?: GuardrailRegistryBinding;
113
123
  readonly agentRegistry?: AgentRegistryBinding;
114
124
  readonly flowRegistry?: FlowRegistryBinding;
125
+ /** Data blocks: an agent's block references are pinned at deploy with its tools. */
126
+ readonly blockRegistry?: BlockRegistryBinding;
115
127
  /**
116
128
  * REQUIRED at runtime for `POST /v1/deployments/:deploymentId/secrets`.
117
129
  * Optional at the type level so app compositions without a secrets
@@ -476,6 +488,11 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
476
488
  }
477
489
  const validated = validation.value;
478
490
 
491
+ // A read-only registry (under `kindgi dev`, the pack's files) takes
492
+ // nothing a deployment brings: refuse before any write.
493
+ const readOnly = readOnlyTarget(validated, bindings);
494
+ if (readOnly !== undefined) return refuseReadOnly(c, readOnly);
495
+
479
496
  // ---- Registry upserts (with rollback tracking) ----
480
497
  //
481
498
  // Agent-registry, flow-registry, tool-registry, and
@@ -529,6 +546,10 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
529
546
  }
530
547
 
531
548
  const rolled: RollbackAction[] = [];
549
+ // The version each agent is registered under: its definition's, or
550
+ // the one a deploy registered in its place (`publishDeployedAgent`).
551
+ const deployedAgents: DeployedAgent[] = [];
552
+ const deployedFlows: DeployedFlow[] = [];
532
553
  try {
533
554
  if (bindings.toolRegistry !== undefined && defaultProjectId !== undefined) {
534
555
  for (const tool of validated.tools) {
@@ -578,58 +599,50 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
578
599
  }
579
600
  }
580
601
  if (bindings.agentRegistry !== undefined && defaultProjectId !== undefined) {
581
- for (const agent of validated.agents) {
582
- const projectIdForAgent = defaultProjectId;
583
- const outcome = await bindings.agentRegistry.publish({
602
+ deployedAgents.push(
603
+ ...(await registerAgents({
604
+ agents: bindings.agentRegistry,
605
+ tools: bindings.toolRegistry,
606
+ blocks: bindings.blockRegistry,
584
607
  tenantId,
585
- projectId: projectIdForAgent,
586
- agent,
587
- // Signed deploys have no per-request principal — pass parent-
588
- // only tuples (no owner grant). Tenant admins keep access via
589
- // `admin from parent` cascade.
590
- enqueueTuples: (agentId) =>
591
- tuplesForCreate({
592
- kind: 'agent',
593
- id: agentId as AgentId,
594
- tenantId,
595
- projectId: projectIdForAgent,
596
- }),
597
- });
598
- if (outcome.kind === 'ok') {
599
- const agentId = outcome.agentId;
600
- const version = outcome.version;
601
- rolled.push(async () => {
602
- await bindings.agentRegistry?.unregister({ tenantId, agentId, version });
603
- });
604
- }
605
- }
608
+ projectId: defaultProjectId,
609
+ defined: validated.agents,
610
+ rolled,
611
+ })),
612
+ );
613
+ } else {
614
+ deployedAgents.push(...validated.agents.map(deployedPrimitive));
606
615
  }
607
616
  if (bindings.flowRegistry !== undefined && defaultProjectId !== undefined) {
608
- for (const flow of validated.flows) {
609
- const projectIdForGraph = defaultProjectId;
610
- const outcome = await bindings.flowRegistry.publish({
617
+ deployedFlows.push(
618
+ ...(await registerFlows({
619
+ flows: bindings.flowRegistry,
620
+ tools: bindings.toolRegistry,
621
+ agents: bindings.agentRegistry,
611
622
  tenantId,
612
- projectId: projectIdForGraph,
613
- flow,
614
- enqueueTuples: (flowId) =>
615
- tuplesForCreate({
616
- kind: 'flow',
617
- id: flowId as FlowId,
618
- tenantId,
619
- projectId: projectIdForGraph,
620
- }),
621
- });
622
- if (outcome.kind === 'ok') {
623
- const flowId = outcome.flowId;
624
- const version = outcome.version;
625
- rolled.push(async () => {
626
- await bindings.flowRegistry?.unregister({ tenantId, flowId, version });
627
- });
628
- }
629
- }
623
+ projectId: defaultProjectId,
624
+ defined: validated.flows,
625
+ rolled,
626
+ })),
627
+ );
628
+ } else {
629
+ deployedFlows.push(...validated.flows.map(deployedPrimitive));
630
630
  }
631
631
  } catch (cause) {
632
632
  await rollback(rolled);
633
+ if (cause instanceof UnpinnableDeploy) {
634
+ c.status(statusFor('invalid-agent') as never);
635
+ return c.json(
636
+ toWireError(
637
+ {
638
+ code: 'validation-failed',
639
+ message: `The deployment's agents or flows use tool or agent versions that aren't published (${cause.issues.length} issue${cause.issues.length === 1 ? '' : 's'}); nothing was deployed`,
640
+ issues: cause.issues as unknown as Record<string, unknown>[],
641
+ },
642
+ requestId,
643
+ ),
644
+ );
645
+ }
633
646
  c.status(statusFor('internal-server-error') as never);
634
647
  return c.json(
635
648
  toWireError(
@@ -669,14 +682,8 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
669
682
  version: t.version,
670
683
  })),
671
684
  guardrails: validated.guardrails.map((g) => ({ id: g.id as unknown as string })),
672
- agents: validated.agents.map((a) => ({
673
- id: a.id as unknown as string,
674
- version: a.version as unknown as string,
675
- })),
676
- flows: validated.flows.map((f) => ({
677
- id: f.id as unknown as string,
678
- version: f.version as unknown as string,
679
- })),
685
+ agents: deployedAgents,
686
+ flows: deployedFlows,
680
687
  },
681
688
  });
682
689
  } catch (cause) {
@@ -1374,8 +1381,18 @@ function validateAgentIndexShape(
1374
1381
  if (typeof spec.name !== 'string' || (spec.name as string).length === 0) {
1375
1382
  out.push({ path: '/name', message: 'agent.name must be a non-empty string' });
1376
1383
  }
1377
- if (typeof spec.instructions !== 'string' || (spec.instructions as string).length === 0) {
1378
- out.push({ path: '/instructions', message: 'agent.instructions must be a non-empty string' });
1384
+ const instructions = spec.instructions as { prompt?: unknown; version?: unknown } | string;
1385
+ const promptRef =
1386
+ typeof instructions === 'object' &&
1387
+ instructions !== null &&
1388
+ typeof instructions.prompt === 'string' &&
1389
+ typeof instructions.version === 'string';
1390
+ if (!promptRef && (typeof instructions !== 'string' || instructions.length === 0)) {
1391
+ out.push({
1392
+ path: '/instructions',
1393
+ message:
1394
+ 'agent.instructions must be a non-empty string, or a prompt block { prompt, version }',
1395
+ });
1379
1396
  }
1380
1397
  if (!Array.isArray(spec.capabilities)) {
1381
1398
  out.push({ path: '/capabilities', message: 'agent.capabilities must be an array' });
@@ -1411,6 +1428,205 @@ function validateFlowIndexShape(
1411
1428
 
1412
1429
  type RollbackAction = () => Promise<void>;
1413
1430
 
1431
+ /** A deploy whose agents or flows use a tool or agent with no published version in range. */
1432
+ class UnpinnableDeploy extends Error {
1433
+ constructor(readonly issues: readonly UnpinnableRef[]) {
1434
+ super('the deployment uses tools or agents that are not published');
1435
+ }
1436
+ }
1437
+
1438
+ /** A deployed agent or flow registered under its definition's version. */
1439
+ function deployedPrimitive(definition: { readonly id: string; readonly version: string }): {
1440
+ id: string;
1441
+ version: string;
1442
+ } {
1443
+ return { id: definition.id, version: definition.version };
1444
+ }
1445
+
1446
+ /** A deployed agent or flow, under the version the deploy rule registered it as. */
1447
+ function deployedVersion(
1448
+ definition: { readonly id: string; readonly version: string },
1449
+ outcome: DeployedVersionOutcome,
1450
+ ): DeployedVersion {
1451
+ return outcome.kind === 'reused' || outcome.kind === 'renumbered'
1452
+ ? {
1453
+ id: definition.id,
1454
+ version: outcome.version,
1455
+ authoredVersion: definition.version,
1456
+ reason: outcome.reason,
1457
+ newVersion: outcome.kind === 'renumbered',
1458
+ ...(outcome.pinChanges !== undefined && { pinChanges: outcome.pinChanges }),
1459
+ }
1460
+ : deployedPrimitive(definition);
1461
+ }
1462
+
1463
+ /**
1464
+ * Register a deployment's agents, each under the version it's registered
1465
+ * as (`DeployedAgent`), pushing a rollback for each version it writes.
1466
+ *
1467
+ * With a tool registry, every agent is pinned first (the pack's tools
1468
+ * are registered by now): one whose range matches no published version
1469
+ * throws `UnpinnableDeploy` before any agent is written, and the deploy
1470
+ * rolls back. Each is then registered by `publishDeployedAgent`, which
1471
+ * never keeps a version's old pins. Without one, agents register
1472
+ * unpinned, as before pins existed.
1473
+ */
1474
+ async function registerAgents(input: {
1475
+ readonly agents: AgentRegistryBinding;
1476
+ readonly tools: ToolRegistryBinding | undefined;
1477
+ readonly blocks: BlockRegistryBinding | undefined;
1478
+ readonly tenantId: TenantId;
1479
+ readonly projectId: ProjectId;
1480
+ readonly defined: readonly Agent[];
1481
+ readonly rolled: RollbackAction[];
1482
+ }): Promise<DeployedAgent[]> {
1483
+ const { agents, tools, blocks, tenantId, projectId, rolled } = input;
1484
+ // Signed deploys have no per-request principal — pass parent-only
1485
+ // tuples (no owner grant). Tenant admins keep access via
1486
+ // `admin from parent` cascade.
1487
+ const enqueueTuples = (agentId: string) =>
1488
+ tuplesForCreate({ kind: 'agent', id: agentId as AgentId, tenantId, projectId });
1489
+ const written = (agentId: AgentId, version: Semver) =>
1490
+ rolled.push(async () => {
1491
+ await agents.unregister({ tenantId, agentId, version });
1492
+ });
1493
+
1494
+ if (tools === undefined) {
1495
+ for (const agent of input.defined) {
1496
+ const outcome = await agents.publish({ tenantId, projectId, agent, enqueueTuples });
1497
+ if (outcome.kind === 'ok') written(outcome.agentId, outcome.version);
1498
+ }
1499
+ return input.defined.map(deployedPrimitive);
1500
+ }
1501
+
1502
+ const pinsOf = await pinAgents(tools, blocks, tenantId, input.defined);
1503
+ const deployed: DeployedAgent[] = [];
1504
+ for (const [i, agent] of input.defined.entries()) {
1505
+ const pins = pinsOf[i] as AgentPins;
1506
+ const outcome = await publishDeployedAgent({
1507
+ agents,
1508
+ tenantId,
1509
+ projectId,
1510
+ agent,
1511
+ pins,
1512
+ enqueueTuples,
1513
+ });
1514
+ if (outcome.kind === 'registered' || outcome.kind === 'renumbered') {
1515
+ written(agent.id, outcome.version as unknown as Semver);
1516
+ }
1517
+ deployed.push(deployedVersion(agent, outcome));
1518
+ }
1519
+ return deployed;
1520
+ }
1521
+
1522
+ /**
1523
+ * Register a deployment's flows, each under the version it's registered
1524
+ * as, pushing a rollback for each version it writes. Runs after the
1525
+ * agents, so a flow's agent pins see the versions this deploy
1526
+ * registered: a tool change cascades through an agent into a flow
1527
+ * within the one deploy, each derived once.
1528
+ *
1529
+ * With the tool and agent registries, every flow is pinned first; one
1530
+ * that runs a tool or agent with no published version throws
1531
+ * `UnpinnableDeploy` before any flow is written, and the deploy rolls
1532
+ * back. Without them, flows register unpinned, as before pins existed.
1533
+ */
1534
+ async function registerFlows(input: {
1535
+ readonly flows: FlowRegistryBinding;
1536
+ readonly tools: ToolRegistryBinding | undefined;
1537
+ readonly agents: AgentRegistryBinding | undefined;
1538
+ readonly tenantId: TenantId;
1539
+ readonly projectId: ProjectId;
1540
+ readonly defined: readonly Flow[];
1541
+ readonly rolled: RollbackAction[];
1542
+ }): Promise<DeployedFlow[]> {
1543
+ const { flows, tools, agents, tenantId, projectId, rolled } = input;
1544
+ const enqueueTuples = (flowId: string) =>
1545
+ tuplesForCreate({ kind: 'flow', id: flowId as FlowId, tenantId, projectId });
1546
+ const written = (flowId: FlowId, version: string) =>
1547
+ rolled.push(async () => {
1548
+ await flows.unregister({ tenantId, flowId, version: version as never });
1549
+ });
1550
+
1551
+ if (tools === undefined || agents === undefined) {
1552
+ for (const flow of input.defined) {
1553
+ const outcome = await flows.publish({ tenantId, projectId, flow, enqueueTuples });
1554
+ if (outcome.kind === 'ok') written(outcome.flowId, outcome.version as unknown as string);
1555
+ }
1556
+ return input.defined.map(deployedPrimitive);
1557
+ }
1558
+
1559
+ const pinsOf = await pinFlows(tools, agents, tenantId, input.defined);
1560
+ const deployed: DeployedFlow[] = [];
1561
+ for (const [i, flow] of input.defined.entries()) {
1562
+ const outcome = await publishDeployedFlow({
1563
+ flows,
1564
+ tenantId,
1565
+ projectId,
1566
+ flow,
1567
+ pins: pinsOf[i] as FlowPins,
1568
+ enqueueTuples,
1569
+ });
1570
+ if (outcome.kind === 'registered' || outcome.kind === 'renumbered') {
1571
+ written(flow.id, outcome.version);
1572
+ }
1573
+ deployed.push(deployedVersion(flow, outcome));
1574
+ }
1575
+ return deployed;
1576
+ }
1577
+
1578
+ /** Each flow's pins, in order; throws `UnpinnableDeploy` naming every tool or agent with no published version. */
1579
+ async function pinFlows(
1580
+ tools: ToolRegistryBinding,
1581
+ agents: AgentRegistryBinding,
1582
+ tenantId: TenantId,
1583
+ defined: readonly Flow[],
1584
+ ): Promise<FlowPins[]> {
1585
+ const pins: FlowPins[] = [];
1586
+ const unpinnable: UnpinnableRef[] = [];
1587
+ for (const [i, flow] of defined.entries()) {
1588
+ const resolved = await resolveFlowPins(tools, agents, tenantId, flow);
1589
+ if (resolved.kind === 'ok') {
1590
+ pins.push(resolved.pins);
1591
+ continue;
1592
+ }
1593
+ for (const issue of resolved.issues) {
1594
+ unpinnable.push({
1595
+ path: `/flows/${i}${issue.path}`,
1596
+ message: `flow "${flow.id as unknown as string}": ${issue.message}`,
1597
+ });
1598
+ }
1599
+ }
1600
+ if (unpinnable.length > 0) throw new UnpinnableDeploy(unpinnable);
1601
+ return pins;
1602
+ }
1603
+
1604
+ /** Each agent's pins, in order; throws `UnpinnableDeploy` naming every range that matches nothing. */
1605
+ async function pinAgents(
1606
+ tools: ToolRegistryBinding,
1607
+ blocks: BlockRegistryBinding | undefined,
1608
+ tenantId: TenantId,
1609
+ defined: readonly Agent[],
1610
+ ): Promise<AgentPins[]> {
1611
+ const pins: AgentPins[] = [];
1612
+ const unpinnable: UnpinnableRef[] = [];
1613
+ for (const [i, agent] of defined.entries()) {
1614
+ const resolved = await resolveAgentPins(tools, tenantId, agent, blocks);
1615
+ if (resolved.kind === 'ok') {
1616
+ pins.push(resolved.pins);
1617
+ continue;
1618
+ }
1619
+ for (const issue of resolved.issues) {
1620
+ unpinnable.push({
1621
+ path: `/agents/${i}${issue.path}`,
1622
+ message: `agent "${agent.id as unknown as string}": ${issue.message}`,
1623
+ });
1624
+ }
1625
+ }
1626
+ if (unpinnable.length > 0) throw new UnpinnableDeploy(unpinnable);
1627
+ return pins;
1628
+ }
1629
+
1414
1630
  async function rollback(actions: readonly RollbackAction[]): Promise<void> {
1415
1631
  // Reverse order — last-in, first-out — so registrations are undone in
1416
1632
  // the mirror sequence of their creation.
@@ -1491,3 +1707,22 @@ export function sha256HexPrefixed(bytes: Uint8Array): string {
1491
1707
  const hex = createHash('sha256').update(bytes).digest('hex');
1492
1708
  return `sha256:${hex}`;
1493
1709
  }
1710
+
1711
+ /** The first read-only registry a deployment would publish into, if any. */
1712
+ function readOnlyTarget(
1713
+ validated: {
1714
+ readonly tools: readonly unknown[];
1715
+ readonly agents: readonly unknown[];
1716
+ readonly flows: readonly unknown[];
1717
+ readonly guardrails: readonly unknown[];
1718
+ },
1719
+ bindings: DeploymentsRouterBindings,
1720
+ ): RegistryReadOnly | undefined {
1721
+ const targets = [
1722
+ [validated.tools, bindings.toolRegistry?.readOnly],
1723
+ [validated.agents, bindings.agentRegistry?.readOnly],
1724
+ [validated.flows, bindings.flowRegistry?.readOnly],
1725
+ [validated.guardrails, bindings.guardrailRegistry?.readOnly],
1726
+ ] as const;
1727
+ return targets.find(([brought, readOnly]) => brought.length > 0 && readOnly !== undefined)?.[1];
1728
+ }
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { AgentId } from '@kindgi/agents';
5
+ import type { FlowVersionOverrides } from '@kindgi/flow';
6
+ import type { ProjectId, Semver } from '@kindgi/types';
7
+
8
+ import type { EvalBaseline, EvalComparison } from '../eval-run-binding.js';
9
+ import { DEFAULT_COMPARISON } from '../judged-dispatcher.js';
10
+
11
+ const MAX_REPETITIONS = 10;
12
+ const MAX_K = 100;
13
+
14
+ function obj(value: unknown): Record<string, unknown> | undefined {
15
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
16
+ ? (value as Record<string, unknown>)
17
+ : undefined;
18
+ }
19
+
20
+ function parseLive(raw: unknown): EvalBaseline | string {
21
+ const live = obj(raw);
22
+ if (live === undefined) return '`baseline.live` must be an object';
23
+ const segments = live.segments === undefined ? undefined : obj(live.segments);
24
+ const stringsOnly =
25
+ segments !== undefined && Object.values(segments).every((v) => typeof v === 'string');
26
+ if (live.segments !== undefined && !stringsOnly) {
27
+ return '`baseline.live.segments` must be an object of strings';
28
+ }
29
+ if (live.projectId !== undefined && typeof live.projectId !== 'string') {
30
+ return '`baseline.live.projectId` must be a string';
31
+ }
32
+ return {
33
+ live: {
34
+ ...(typeof live.projectId === 'string' && { projectId: live.projectId as ProjectId }),
35
+ ...(segments !== undefined && { segments: segments as Record<string, string> }),
36
+ },
37
+ };
38
+ }
39
+
40
+ function parseBaseline(raw: unknown): EvalBaseline | string {
41
+ if (raw === 'recorded') return 'recorded';
42
+ const o = obj(raw);
43
+ if (o === undefined) {
44
+ return "`baseline` must be 'recorded', { agentId, version } or { live: { projectId?, segments? } }";
45
+ }
46
+ if (o.live !== undefined) return parseLive(o.live);
47
+ if (typeof o.agentId !== 'string' || o.agentId === '' || typeof o.version !== 'string') {
48
+ return '`baseline` as a version needs `agentId` and `version`';
49
+ }
50
+ return { agentId: o.agentId as AgentId, version: o.version as Semver };
51
+ }
52
+
53
+ /** `{ id: version }` with non-empty strings; an error message otherwise. */
54
+ function versionMap(raw: unknown, name: string): Readonly<Record<string, string>> | string {
55
+ const o = obj(raw);
56
+ const valid =
57
+ o !== undefined &&
58
+ Object.entries(o).every(([id, v]) => id !== '' && typeof v === 'string' && v !== '');
59
+ return valid
60
+ ? (o as Record<string, string>)
61
+ : `\`${name}\` must be an object of { id: version } with non-empty strings`;
62
+ }
63
+
64
+ /** `versions: { agents?, tools? }`; `undefined` when it names nothing. */
65
+ function parseVersions(raw: unknown): FlowVersionOverrides | undefined | string {
66
+ const o = obj(raw);
67
+ if (o === undefined) return '`versions` must be an object: { agents?, tools? }';
68
+ const extra = Object.keys(o).filter((key) => key !== 'agents' && key !== 'tools');
69
+ if (extra.length > 0) return `\`versions\` takes \`agents\` and \`tools\`, not \`${extra[0]}\``;
70
+ const agents = o.agents === undefined ? {} : versionMap(o.agents, 'versions.agents');
71
+ if (typeof agents === 'string') return agents;
72
+ const tools = o.tools === undefined ? {} : versionMap(o.tools, 'versions.tools');
73
+ if (typeof tools === 'string') return tools;
74
+ if (Object.keys(agents).length === 0 && Object.keys(tools).length === 0) return undefined;
75
+ return {
76
+ ...(Object.keys(agents).length > 0 && { agents }),
77
+ ...(Object.keys(tools).length > 0 && { tools }),
78
+ };
79
+ }
80
+
81
+ function integerIn(raw: unknown, name: string, max: number): number | string {
82
+ return typeof raw === 'number' && Number.isInteger(raw) && raw >= 1 && raw <= max
83
+ ? raw
84
+ : `\`${name}\` must be an integer from 1 to ${max}`;
85
+ }
86
+
87
+ /**
88
+ * A comparison eval run's settings from the start body: `baseline`,
89
+ * `reads`, `repetitions`, `k` and a flow candidate's `versions`.
90
+ * `undefined` when none is given (the defaults apply); an error message
91
+ * for a bad one.
92
+ */
93
+ export function parseComparison(
94
+ b: Readonly<Record<string, unknown>>,
95
+ ):
96
+ | { readonly kind: 'ok'; readonly value?: EvalComparison }
97
+ | { readonly kind: 'err'; readonly message: string } {
98
+ const { baseline, reads, repetitions, k } = b;
99
+ const versions = b.versions === undefined ? undefined : parseVersions(b.versions);
100
+ if (typeof versions === 'string') return { kind: 'err', message: versions };
101
+ if (
102
+ baseline === undefined &&
103
+ reads === undefined &&
104
+ repetitions === undefined &&
105
+ k === undefined &&
106
+ versions === undefined
107
+ ) {
108
+ return { kind: 'ok' };
109
+ }
110
+ const parsedBaseline =
111
+ baseline === undefined ? DEFAULT_COMPARISON.baseline : parseBaseline(baseline);
112
+ if (typeof parsedBaseline === 'string' && parsedBaseline !== 'recorded') {
113
+ return { kind: 'err', message: parsedBaseline };
114
+ }
115
+ if (reads !== undefined && reads !== 'recorded' && reads !== 'live') {
116
+ return { kind: 'err', message: "`reads` must be 'recorded' or 'live'" };
117
+ }
118
+ const reps =
119
+ repetitions === undefined
120
+ ? DEFAULT_COMPARISON.repetitions
121
+ : integerIn(repetitions, 'repetitions', MAX_REPETITIONS);
122
+ if (typeof reps === 'string') return { kind: 'err', message: reps };
123
+ const topK = k === undefined ? DEFAULT_COMPARISON.k : integerIn(k, 'k', MAX_K);
124
+ if (typeof topK === 'string') return { kind: 'err', message: topK };
125
+ return {
126
+ kind: 'ok',
127
+ value: {
128
+ baseline: parsedBaseline,
129
+ reads: reads ?? DEFAULT_COMPARISON.reads,
130
+ repetitions: reps,
131
+ k: topK,
132
+ ...(versions !== undefined && { versions }),
133
+ },
134
+ };
135
+ }