@kindgi/api 0.1.4-rc.1 → 0.1.4-rc.3

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 (220) hide show
  1. package/dist/agent-binding.d.ts +12 -2
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/agent-pins.d.ts.map +1 -1
  4. package/dist/agent-pins.js +4 -1
  5. package/dist/agent-pins.js.map +1 -1
  6. package/dist/app.d.ts +8 -0
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +23 -4
  9. package/dist/app.js.map +1 -1
  10. package/dist/deploy-versions.d.ts +8 -5
  11. package/dist/deploy-versions.d.ts.map +1 -1
  12. package/dist/deploy-versions.js +3 -9
  13. package/dist/deploy-versions.js.map +1 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +25 -0
  16. package/dist/errors.js.map +1 -1
  17. package/dist/eval-case-binding.d.ts +10 -0
  18. package/dist/eval-case-binding.d.ts.map +1 -1
  19. package/dist/eval-run-binding.d.ts +14 -3
  20. package/dist/eval-run-binding.d.ts.map +1 -1
  21. package/dist/eval-run-binding.js.map +1 -1
  22. package/dist/flow-pins.d.ts +20 -7
  23. package/dist/flow-pins.d.ts.map +1 -1
  24. package/dist/flow-pins.js +36 -11
  25. package/dist/flow-pins.js.map +1 -1
  26. package/dist/gate-policy-binding.d.ts +164 -0
  27. package/dist/gate-policy-binding.d.ts.map +1 -0
  28. package/dist/gate-policy-binding.js +12 -0
  29. package/dist/gate-policy-binding.js.map +1 -0
  30. package/dist/gate.d.ts +56 -0
  31. package/dist/gate.d.ts.map +1 -0
  32. package/dist/gate.js +359 -0
  33. package/dist/gate.js.map +1 -0
  34. package/dist/handler-binding.d.ts +14 -2
  35. package/dist/handler-binding.d.ts.map +1 -1
  36. package/dist/hitl-binding.d.ts +5 -0
  37. package/dist/hitl-binding.d.ts.map +1 -1
  38. package/dist/index.d.ts +10 -4
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/judged-dispatcher.d.ts +16 -1
  43. package/dist/judged-dispatcher.d.ts.map +1 -1
  44. package/dist/judged-dispatcher.js +45 -6
  45. package/dist/judged-dispatcher.js.map +1 -1
  46. package/dist/judgment-binding.d.ts +38 -0
  47. package/dist/judgment-binding.d.ts.map +1 -1
  48. package/dist/judgment-binding.js +19 -0
  49. package/dist/judgment-binding.js.map +1 -1
  50. package/dist/live-version-binding.d.ts +206 -0
  51. package/dist/live-version-binding.d.ts.map +1 -0
  52. package/dist/live-version-binding.js +4 -0
  53. package/dist/live-version-binding.js.map +1 -0
  54. package/dist/middleware/authorize.d.ts.map +1 -1
  55. package/dist/middleware/authorize.js +13 -2
  56. package/dist/middleware/authorize.js.map +1 -1
  57. package/dist/middleware/project-ref.d.ts +25 -0
  58. package/dist/middleware/project-ref.d.ts.map +1 -0
  59. package/dist/middleware/project-ref.js +72 -0
  60. package/dist/middleware/project-ref.js.map +1 -0
  61. package/dist/openapi/generate.d.ts.map +1 -1
  62. package/dist/openapi/generate.js +4 -0
  63. package/dist/openapi/generate.js.map +1 -1
  64. package/dist/openapi/operations.d.ts +6 -0
  65. package/dist/openapi/operations.d.ts.map +1 -1
  66. package/dist/openapi/operations.js +512 -22
  67. package/dist/openapi/operations.js.map +1 -1
  68. package/dist/openapi/schemas.d.ts +37 -0
  69. package/dist/openapi/schemas.d.ts.map +1 -1
  70. package/dist/openapi/schemas.js +709 -5
  71. package/dist/openapi/schemas.js.map +1 -1
  72. package/dist/publish-refused.d.ts +18 -0
  73. package/dist/publish-refused.d.ts.map +1 -0
  74. package/dist/publish-refused.js +22 -0
  75. package/dist/publish-refused.js.map +1 -0
  76. package/dist/retention-binding.d.ts +29 -0
  77. package/dist/retention-binding.d.ts.map +1 -1
  78. package/dist/routes/agent-releases.d.ts +40 -0
  79. package/dist/routes/agent-releases.d.ts.map +1 -0
  80. package/dist/routes/agent-releases.js +462 -0
  81. package/dist/routes/agent-releases.js.map +1 -0
  82. package/dist/routes/agents.d.ts +7 -1
  83. package/dist/routes/agents.d.ts.map +1 -1
  84. package/dist/routes/agents.js +31 -3
  85. package/dist/routes/agents.js.map +1 -1
  86. package/dist/routes/approvals.d.ts.map +1 -1
  87. package/dist/routes/approvals.js +40 -9
  88. package/dist/routes/approvals.js.map +1 -1
  89. package/dist/routes/audit.d.ts.map +1 -1
  90. package/dist/routes/audit.js +7 -0
  91. package/dist/routes/audit.js.map +1 -1
  92. package/dist/routes/auth.js +1 -1
  93. package/dist/routes/auth.js.map +1 -1
  94. package/dist/routes/conversations.d.ts +0 -7
  95. package/dist/routes/conversations.d.ts.map +1 -1
  96. package/dist/routes/conversations.js +19 -3
  97. package/dist/routes/conversations.js.map +1 -1
  98. package/dist/routes/deployments.d.ts +6 -0
  99. package/dist/routes/deployments.d.ts.map +1 -1
  100. package/dist/routes/deployments.js +33 -5
  101. package/dist/routes/deployments.js.map +1 -1
  102. package/dist/routes/env.d.ts.map +1 -1
  103. package/dist/routes/env.js +1 -0
  104. package/dist/routes/env.js.map +1 -1
  105. package/dist/routes/eval-comparison.d.ts +2 -1
  106. package/dist/routes/eval-comparison.d.ts.map +1 -1
  107. package/dist/routes/eval-comparison.js +22 -9
  108. package/dist/routes/eval-comparison.js.map +1 -1
  109. package/dist/routes/flows.d.ts +14 -7
  110. package/dist/routes/flows.d.ts.map +1 -1
  111. package/dist/routes/flows.js +10 -8
  112. package/dist/routes/flows.js.map +1 -1
  113. package/dist/routes/gate-policies.d.ts +19 -0
  114. package/dist/routes/gate-policies.d.ts.map +1 -0
  115. package/dist/routes/gate-policies.js +191 -0
  116. package/dist/routes/gate-policies.js.map +1 -0
  117. package/dist/routes/gate-policy-spec.d.ts +17 -0
  118. package/dist/routes/gate-policy-spec.d.ts.map +1 -0
  119. package/dist/routes/gate-policy-spec.js +192 -0
  120. package/dist/routes/gate-policy-spec.js.map +1 -0
  121. package/dist/routes/gate-policy-wire.d.ts +3 -0
  122. package/dist/routes/gate-policy-wire.d.ts.map +1 -0
  123. package/dist/routes/gate-policy-wire.js +18 -0
  124. package/dist/routes/gate-policy-wire.js.map +1 -0
  125. package/dist/routes/hierarchy-errors.d.ts +11 -0
  126. package/dist/routes/hierarchy-errors.d.ts.map +1 -1
  127. package/dist/routes/hierarchy-errors.js +13 -0
  128. package/dist/routes/hierarchy-errors.js.map +1 -1
  129. package/dist/routes/judged-suites.js +7 -0
  130. package/dist/routes/judged-suites.js.map +1 -1
  131. package/dist/routes/judgments.d.ts +4 -1
  132. package/dist/routes/judgments.d.ts.map +1 -1
  133. package/dist/routes/judgments.js +92 -19
  134. package/dist/routes/judgments.js.map +1 -1
  135. package/dist/routes/live-scope-wire.d.ts +32 -0
  136. package/dist/routes/live-scope-wire.d.ts.map +1 -0
  137. package/dist/routes/live-scope-wire.js +71 -0
  138. package/dist/routes/live-scope-wire.js.map +1 -0
  139. package/dist/routes/policies.d.ts +6 -3
  140. package/dist/routes/policies.d.ts.map +1 -1
  141. package/dist/routes/policies.js +55 -3
  142. package/dist/routes/policies.js.map +1 -1
  143. package/dist/routes/projects.d.ts.map +1 -1
  144. package/dist/routes/projects.js +47 -3
  145. package/dist/routes/projects.js.map +1 -1
  146. package/dist/routes/retention.d.ts.map +1 -1
  147. package/dist/routes/retention.js +18 -2
  148. package/dist/routes/retention.js.map +1 -1
  149. package/dist/routes/reviewers.d.ts +8 -1
  150. package/dist/routes/reviewers.d.ts.map +1 -1
  151. package/dist/routes/reviewers.js +20 -3
  152. package/dist/routes/reviewers.js.map +1 -1
  153. package/dist/routes/runs.d.ts.map +1 -1
  154. package/dist/routes/runs.js +22 -13
  155. package/dist/routes/runs.js.map +1 -1
  156. package/dist/routes/secrets.d.ts.map +1 -1
  157. package/dist/routes/secrets.js +2 -0
  158. package/dist/routes/secrets.js.map +1 -1
  159. package/dist/routes/segments.d.ts +17 -0
  160. package/dist/routes/segments.d.ts.map +1 -0
  161. package/dist/routes/segments.js +68 -0
  162. package/dist/routes/segments.js.map +1 -0
  163. package/dist/routes/teams.d.ts.map +1 -1
  164. package/dist/routes/teams.js +42 -3
  165. package/dist/routes/teams.js.map +1 -1
  166. package/dist/routes/uuid-param.d.ts +11 -0
  167. package/dist/routes/uuid-param.d.ts.map +1 -0
  168. package/dist/routes/uuid-param.js +19 -0
  169. package/dist/routes/uuid-param.js.map +1 -0
  170. package/openapi.json +7008 -3624
  171. package/package.json +21 -21
  172. package/src/agent-binding.ts +12 -2
  173. package/src/agent-pins.ts +3 -1
  174. package/src/app.ts +37 -3
  175. package/src/deploy-versions.ts +11 -14
  176. package/src/errors.ts +25 -0
  177. package/src/eval-case-binding.ts +7 -0
  178. package/src/eval-run-binding.ts +24 -3
  179. package/src/flow-pins.ts +49 -9
  180. package/src/gate-policy-binding.ts +171 -0
  181. package/src/gate.ts +467 -0
  182. package/src/handler-binding.ts +14 -2
  183. package/src/hitl-binding.ts +5 -0
  184. package/src/index.ts +46 -1
  185. package/src/judged-dispatcher.ts +74 -6
  186. package/src/judgment-binding.ts +62 -0
  187. package/src/live-version-binding.ts +237 -0
  188. package/src/middleware/authorize.ts +18 -2
  189. package/src/middleware/project-ref.ts +88 -0
  190. package/src/openapi/generate.ts +5 -0
  191. package/src/openapi/operations.ts +619 -22
  192. package/src/openapi/schemas.ts +788 -5
  193. package/src/publish-refused.ts +31 -0
  194. package/src/retention-binding.ts +30 -0
  195. package/src/routes/agent-releases.ts +581 -0
  196. package/src/routes/agents.ts +43 -2
  197. package/src/routes/approvals.ts +58 -8
  198. package/src/routes/audit.ts +10 -0
  199. package/src/routes/auth.ts +1 -1
  200. package/src/routes/conversations.ts +31 -3
  201. package/src/routes/deployments.ts +50 -5
  202. package/src/routes/env.ts +1 -0
  203. package/src/routes/eval-comparison.ts +24 -10
  204. package/src/routes/flows.ts +27 -11
  205. package/src/routes/gate-policies.ts +226 -0
  206. package/src/routes/gate-policy-spec.ts +229 -0
  207. package/src/routes/gate-policy-wire.ts +20 -0
  208. package/src/routes/hierarchy-errors.ts +14 -0
  209. package/src/routes/judged-suites.ts +5 -0
  210. package/src/routes/judgments.ts +107 -23
  211. package/src/routes/live-scope-wire.ts +90 -0
  212. package/src/routes/policies.ts +61 -3
  213. package/src/routes/projects.ts +54 -2
  214. package/src/routes/retention.ts +21 -2
  215. package/src/routes/reviewers.ts +23 -4
  216. package/src/routes/runs.ts +33 -20
  217. package/src/routes/secrets.ts +2 -0
  218. package/src/routes/segments.ts +75 -0
  219. package/src/routes/teams.ts +53 -3
  220. package/src/routes/uuid-param.ts +28 -0
@@ -23,10 +23,18 @@ import {
23
23
  nextFreeAgentVersion,
24
24
  } from '../derive-agent-version.js';
25
25
  import { statusFor, toWireError } from '../errors.js';
26
+ import type { AgentReleaseBindings } from '../live-version-binding.js';
26
27
  import type { Authorizer } from '../middleware/authorize.js';
27
28
  import { refuseWritesWhenReadOnly } from '../registry-read-only.js';
28
29
  import type { ToolRegistryBinding } from '../tool-binding.js';
29
30
  import type { AppEnv } from '../types.js';
31
+ import {
32
+ type AgentReleaseGateDeps,
33
+ isPromotionCheck,
34
+ isPromotionWrite,
35
+ mountAgentReleaseRoutes,
36
+ } from './agent-releases.js';
37
+ import { liveScopeToWire } from './live-scope-wire.js';
30
38
  import { clampLimit } from './pagination.js';
31
39
  import { parseScopeParams } from './scope-params.js';
32
40
 
@@ -52,6 +60,10 @@ export function agentsRouter(
52
60
  authorizer?: Authorizer,
53
61
  toolRegistry?: ToolRegistryBinding,
54
62
  blockRegistry?: BlockRegistryBinding,
63
+ /** Live versions per scope and promotions (evals step 4); absent → those routes aren't mounted. */
64
+ releases?: AgentReleaseBindings,
65
+ /** What a promotion's gate reads besides the releases (evals step 4b). */
66
+ gateDeps?: AgentReleaseGateDeps,
55
67
  ): Hono<AppEnv> {
56
68
  const r = new Hono<AppEnv>();
57
69
  // A read-only registry (under `kindgi dev`, the pack's files) refuses
@@ -71,6 +83,9 @@ export function agentsRouter(
71
83
  // POST /:agentId/versions → publish on the agent (derive)
72
84
  // POST /:agentId/versions/:version/unregister → admin on the agent
73
85
  // POST /:agentId/versions/:version/reinstate → admin on the agent
86
+ // POST /:agentId/promotions, /live/rollback, /live/unpin → promote on the agent
87
+ // POST /:agentId/promotions/check → read on the agent (changes nothing)
88
+ // GET /:agentId/live, /live-versions, /promotions[/…] → read on the agent
74
89
  // GET / (list) → tenant-scoped fetch;
75
90
  // with a project scope
76
91
  // (?scopeKind=project&scopeId=),
@@ -108,9 +123,16 @@ export function agentsRouter(
108
123
  });
109
124
  r.use('/:agentId/*', async (c, next) => {
110
125
  // Deriving a version (POST …/versions) is `publish` on the agent;
111
- // unregister and reinstate are `admin`.
126
+ // changing what's live is its own permission (`promote`, granted to
127
+ // the agent's admins); unregister and reinstate are `admin`.
112
128
  const action =
113
- c.req.method === 'GET' ? 'read' : c.req.path.endsWith('/versions') ? 'publish' : 'admin';
129
+ c.req.method === 'GET' || isPromotionCheck(c.req.method, c.req.path)
130
+ ? 'read'
131
+ : isPromotionWrite(c.req.method, c.req.path)
132
+ ? 'promote'
133
+ : c.req.path.endsWith('/versions')
134
+ ? 'publish'
135
+ : 'admin';
114
136
  const agentId = c.req.param('agentId') ?? '';
115
137
  const mw = authorizer.authorize(action, () => ref('agent', agentId));
116
138
  return mw(c, next);
@@ -434,6 +456,23 @@ export function agentsRouter(
434
456
  const version = c.req.param('version') as Semver;
435
457
 
436
458
  const outcome = await binding.unregister({ tenantId, agentId, version });
459
+ if (!outcome.unregistered && outcome.live !== undefined && outcome.live.length > 0) {
460
+ c.status(statusFor('agent-version-live') as never);
461
+ return c.json(
462
+ toWireError(
463
+ {
464
+ code: 'agent-version-live',
465
+ message: `${agentId as unknown as string} ${version as unknown as string} is live in ${
466
+ outcome.live.length === 1 ? 'a scope' : `${outcome.live.length} scopes`
467
+ }: roll back, unpin, or promote another version there first, then unregister it.`,
468
+ agentId: agentId as unknown as string,
469
+ version: version as unknown as string,
470
+ scopes: outcome.live.map(liveScopeToWire),
471
+ },
472
+ requestId,
473
+ ),
474
+ );
475
+ }
437
476
  if (!outcome.unregistered) {
438
477
  c.status(statusFor('agent-not-found') as never);
439
478
  return c.json(
@@ -489,6 +528,8 @@ export function agentsRouter(
489
528
  });
490
529
  });
491
530
 
531
+ if (releases !== undefined) mountAgentReleaseRoutes(r, binding, releases, gateDeps);
532
+
492
533
  return r;
493
534
  }
494
535
 
@@ -57,6 +57,10 @@ const DECISION_KINDS: ReadonlySet<ReviewDecisionKind> = new Set([
57
57
 
58
58
  const ROLE_VALUES: ReadonlySet<ReviewerRole> = new Set(['standard', 'senior', 'admin']);
59
59
 
60
+ /** How many `waitTokenId` values a list takes, and how long each may be. */
61
+ const MAX_WAIT_TOKEN_IDS = 50;
62
+ const MAX_WAIT_TOKEN_ID_LENGTH = 512;
63
+
60
64
  /**
61
65
  * Terminal approval statuses — the ones an audit bundle can be exported for.
62
66
  * An `expired` approval has no recorded decision.
@@ -214,6 +218,23 @@ export function approvalsRouter(
214
218
  createdAfterIso = parsed.toISOString();
215
219
  }
216
220
 
221
+ const waitTokenIds = c.req.queries('waitTokenId') ?? [];
222
+ if (
223
+ waitTokenIds.length > MAX_WAIT_TOKEN_IDS ||
224
+ waitTokenIds.some((t) => t.length === 0 || t.length > MAX_WAIT_TOKEN_ID_LENGTH)
225
+ ) {
226
+ c.status(statusFor('bad-input') as never);
227
+ return c.json(
228
+ toWireError(
229
+ {
230
+ code: 'bad-input',
231
+ message: `\`waitTokenId\` takes at most ${MAX_WAIT_TOKEN_IDS} values, each 1–${MAX_WAIT_TOKEN_ID_LENGTH} characters`,
232
+ },
233
+ requestId,
234
+ ),
235
+ );
236
+ }
237
+
217
238
  const cursor = c.req.query('cursor');
218
239
  if (cursor !== undefined && cursor.length > 0) {
219
240
  const parsed = new Date(cursor);
@@ -238,6 +259,7 @@ export function approvalsRouter(
238
259
  ...(requiredRoleFilter !== undefined && { requiredRole: requiredRoleFilter }),
239
260
  ...(createdAfterIso !== undefined && { since: createdAfterIso as unknown as Timestamp }),
240
261
  ...(cursor !== undefined && cursor.length > 0 && { cursor: cursor as Cursor }),
262
+ ...(waitTokenIds.length > 0 && { waitTokenIds }),
241
263
  };
242
264
  const listed = await hitlBinding.listApprovals(listInput);
243
265
  if (listed.kind === 'err') {
@@ -406,6 +428,7 @@ export function approvalsRouter(
406
428
  // override it, when the resume payload must carry more than the
407
429
  // decision (an agent gate refuses one, above).
408
430
  let waitpointResolved = false;
431
+ let resume: ResumeReport | undefined;
409
432
  if (
410
433
  approval.waitTokenId !== undefined &&
411
434
  approval.provenanceRef?.runId !== undefined &&
@@ -440,15 +463,12 @@ export function approvalsRouter(
440
463
  // journaled `wait.resumed`. The approval-complete surface reports
441
464
  // the decision as successful; a run that stayed suspended because
442
465
  // the inline resume failed can be resumed later.
466
+ //
467
+ // The response says how the resume went (`resume`), so a decision
468
+ // whose run couldn't go on (a tool version it started with is gone,
469
+ // say) isn't reported as plain success.
443
470
  if (runHandler !== undefined) {
444
- try {
445
- await runHandler.resumeRun({
446
- tenantId,
447
- runId: approval.provenanceRef.runId as RunId,
448
- });
449
- } catch {
450
- // Soft-fail — the decision is durable; the run can be resumed later.
451
- }
471
+ resume = await resumeInline(runHandler, tenantId, approval.provenanceRef.runId as RunId);
452
472
  }
453
473
  }
454
474
 
@@ -461,6 +481,7 @@ export function approvalsRouter(
461
481
  nextApproval: serializeApproval(result.nextApproval),
462
482
  }),
463
483
  waitpointResolved,
484
+ ...(resume !== undefined && { resume }),
464
485
  });
465
486
  });
466
487
 
@@ -808,3 +829,32 @@ function parseAuditBundleBody(
808
829
  },
809
830
  };
810
831
  }
832
+
833
+ /** How the inline resume after a decision went. */
834
+ type ResumeReport =
835
+ | { readonly kind: 'ok' }
836
+ | { readonly kind: 'failed'; readonly code: string; readonly message: string };
837
+
838
+ /**
839
+ * Resume the run a decision released, in the same request. A resume that
840
+ * fails is reported, not raised: the decision is durable either way, and
841
+ * the runtime ends a run that can't go on, or resumes it later.
842
+ */
843
+ async function resumeInline(
844
+ runHandler: RunHandlerBinding,
845
+ tenantId: TenantId,
846
+ runId: RunId,
847
+ ): Promise<ResumeReport> {
848
+ try {
849
+ const outcome = await runHandler.resumeRun({ tenantId, runId });
850
+ return outcome.kind === 'ok'
851
+ ? { kind: 'ok' }
852
+ : { kind: 'failed', code: outcome.error.code, message: outcome.error.message };
853
+ } catch (cause) {
854
+ return {
855
+ kind: 'failed',
856
+ code: 'resume-failed',
857
+ message: cause instanceof Error ? cause.message : String(cause),
858
+ };
859
+ }
860
+ }
@@ -21,6 +21,7 @@
21
21
  // ?from=<iso>&to=<iso>
22
22
  // ?runId=<id>
23
23
  // ?limit=<n>&cursor=<opaque>
24
+ // ?order=asc|desc (asc, oldest first, by default)
24
25
  //
25
26
 
26
27
  import { Hono } from 'hono';
@@ -83,6 +84,14 @@ export function auditRouter(binding: AuditEventBinding, authorizer?: Authorizer)
83
84
  const from = c.req.query('from');
84
85
  const to = c.req.query('to');
85
86
  const runId = c.req.query('runId');
87
+ const orderRaw = c.req.query('order');
88
+
89
+ if (orderRaw !== undefined && orderRaw !== 'asc' && orderRaw !== 'desc') {
90
+ c.status(statusFor('bad-input') as never);
91
+ return c.json(
92
+ toWireError({ code: 'bad-input', message: '`order` must be "asc" or "desc"' }, requestId),
93
+ );
94
+ }
86
95
 
87
96
  let outcome: 'allowed' | 'denied' | undefined;
88
97
  if (outcomeRaw === 'allowed' || outcomeRaw === 'denied') outcome = outcomeRaw;
@@ -130,6 +139,7 @@ export function auditRouter(binding: AuditEventBinding, authorizer?: Authorizer)
130
139
  ...(to !== undefined && to.length > 0 && { to }),
131
140
  },
132
141
  ...(cursor !== undefined && cursor.length > 0 && { cursor }),
142
+ ...(orderRaw !== undefined && { order: orderRaw }),
133
143
  limit,
134
144
  });
135
145
 
@@ -76,7 +76,7 @@ export function authRouters(options: AuthRouterOptions): {
76
76
  authed.get('/providers', async (c) => {
77
77
  const tenantId = c.get('tenantId') as TenantId;
78
78
  const page = await identityProvider.list({ tenantId });
79
- return c.json({ data: page.data.map(serializeProviderConfig) });
79
+ return c.json({ data: page.data.map(serializeProviderConfig), hasMore: false });
80
80
  });
81
81
 
82
82
  // ---------- POST /providers ----------
@@ -18,6 +18,7 @@ import { statusFor, toWireError } from '../errors.js';
18
18
  import type { AppEnv } from '../types.js';
19
19
  import { clampLimit, decodeCursor, encodeCursor } from './pagination.js';
20
20
  import { parseListScope } from './scope-params.js';
21
+ import { refuseMalformedUuidParam } from './uuid-param.js';
21
22
 
22
23
  const PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23
24
 
@@ -28,6 +29,15 @@ const PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]
28
29
  * order); conversation list sort is `openedAt DESC, id DESC`
29
30
  * (most-recently-opened first).
30
31
  */
32
+ /**
33
+ * A `:conversationId` that isn't a conversation id is a 400
34
+ * (`uuid-param.ts`), not a 500 from the uuid cast (as `runs` does).
35
+ */
36
+ const refuseMalformedConversationId = refuseMalformedUuidParam(
37
+ 'conversationId',
38
+ 'a conversation id',
39
+ );
40
+
31
41
  export function conversationsRouter(
32
42
  conversationBinding: ConversationBinding,
33
43
  runBinding: RunBinding,
@@ -62,6 +72,23 @@ export function conversationsRouter(
62
72
 
63
73
  const agentIdRaw = c.req.query('agentId');
64
74
 
75
+ // Replay conversations (a comparison's replays) are left out unless asked for.
76
+ const replaysRaw = c.req.query('replays');
77
+ if (
78
+ replaysRaw !== undefined &&
79
+ replaysRaw !== 'exclude' &&
80
+ replaysRaw !== 'include' &&
81
+ replaysRaw !== 'only'
82
+ ) {
83
+ c.status(statusFor('bad-input') as never);
84
+ return c.json(
85
+ toWireError(
86
+ { code: 'bad-input', message: '`replays` must be `exclude`, `include` or `only`' },
87
+ requestId,
88
+ ),
89
+ );
90
+ }
91
+
65
92
  const scopeParsed = parseListScope(c.req.query(), { tenantId });
66
93
  if (scopeParsed.kind === 'err') {
67
94
  c.status(statusFor('scope-invalid') as never);
@@ -88,6 +115,7 @@ export function conversationsRouter(
88
115
  ...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
89
116
  ...(agentIdRaw !== undefined && agentIdRaw.length > 0 && { agentId: agentIdRaw as AgentId }),
90
117
  ...(statusFilter !== undefined && { status: statusFilter }),
118
+ replays: replaysRaw ?? 'exclude',
91
119
  ...(before !== undefined && { before }),
92
120
  limit,
93
121
  });
@@ -112,7 +140,7 @@ export function conversationsRouter(
112
140
  });
113
141
 
114
142
  // ---------- GET /:conversationId ----------
115
- r.get('/:conversationId', async (c) => {
143
+ r.get('/:conversationId', refuseMalformedConversationId, async (c) => {
116
144
  const requestId = c.get('requestId');
117
145
  const tenantId = c.get('tenantId') as TenantId;
118
146
  const conversationId = c.req.param('conversationId') as ConversationId;
@@ -186,7 +214,7 @@ export function conversationsRouter(
186
214
  });
187
215
 
188
216
  // ---------- POST /:conversationId/close (idempotent) ----------
189
- r.post('/:conversationId/close', async (c) => {
217
+ r.post('/:conversationId/close', refuseMalformedConversationId, async (c) => {
190
218
  const requestId = c.get('requestId');
191
219
  const tenantId = c.get('tenantId') as TenantId;
192
220
  const conversationId = c.req.param('conversationId') as ConversationId;
@@ -214,7 +242,7 @@ export function conversationsRouter(
214
242
  });
215
243
 
216
244
  // ---------- GET /:conversationId/messages (cursor-paginated, sequence asc) ----------
217
- r.get('/:conversationId/messages', async (c) => {
245
+ r.get('/:conversationId/messages', refuseMalformedConversationId, async (c) => {
218
246
  const requestId = c.get('requestId');
219
247
  const tenantId = c.get('tenantId') as TenantId;
220
248
  const conversationId = c.req.param('conversationId') as ConversationId;
@@ -38,9 +38,11 @@ import type {
38
38
  } from '../deployment-binding.js';
39
39
  import { statusFor, toWireError } from '../errors.js';
40
40
  import type { FlowRegistryBinding } from '../flow-binding.js';
41
- import { publishDeployedFlow, resolveFlowPins } from '../flow-pins.js';
41
+ import { type FlowPinsLive, publishDeployedFlow, resolveFlowPins } from '../flow-pins.js';
42
42
  import type { GuardrailRegistryBinding } from '../guardrail-binding.js';
43
43
  import type { ImageRegistryBinding } from '../image-registry-binding.js';
44
+ import type { LiveVersionBinding } from '../live-version-binding.js';
45
+ import { PublishRefused } from '../publish-refused.js';
44
46
  import { type RegistryReadOnly, refuseReadOnly } from '../registry-read-only.js';
45
47
  import type { SecretBinding } from '../secrets-binding.js';
46
48
  import type { SigningKeyBinding as SigningKeyRegistryBinding } from '../signing-key-binding.js';
@@ -122,6 +124,11 @@ export interface DeploymentsRouterBindings {
122
124
  readonly guardrailRegistry?: GuardrailRegistryBinding;
123
125
  readonly agentRegistry?: AgentRegistryBinding;
124
126
  readonly flowRegistry?: FlowRegistryBinding;
127
+ /**
128
+ * The agents' live versions: a deployed flow's agent step that names no
129
+ * version pins the one live for the deploy's project, else the latest.
130
+ */
131
+ readonly liveVersions?: LiveVersionBinding;
125
132
  /** Data blocks: an agent's block references are pinned at deploy with its tools. */
126
133
  readonly blockRegistry?: BlockRegistryBinding;
127
134
  /**
@@ -572,6 +579,8 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
572
579
  rolled.push(async () => {
573
580
  await bindings.toolRegistry?.unregister({ tenantId, toolId, version });
574
581
  });
582
+ } else if (outcome.kind !== 'already-registered') {
583
+ throw new PublishRefused('tool', `${tool.id}@${tool.version}`, outcome.kind);
575
584
  }
576
585
  }
577
586
  }
@@ -595,6 +604,8 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
595
604
  rolled.push(async () => {
596
605
  await bindings.guardrailRegistry?.unregister({ tenantId, guardrailId });
597
606
  });
607
+ } else if (outcome.kind !== 'already-registered') {
608
+ throw new PublishRefused('guardrail', guardrail.id, outcome.kind);
598
609
  }
599
610
  }
600
611
  }
@@ -619,6 +630,7 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
619
630
  flows: bindings.flowRegistry,
620
631
  tools: bindings.toolRegistry,
621
632
  agents: bindings.agentRegistry,
633
+ ...(bindings.liveVersions !== undefined && { live: bindings.liveVersions }),
622
634
  tenantId,
623
635
  projectId: defaultProjectId,
624
636
  defined: validated.flows,
@@ -630,6 +642,22 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
630
642
  }
631
643
  } catch (cause) {
632
644
  await rollback(rolled);
645
+ // A primitive refused with a typed outcome is the caller's to fix:
646
+ // that outcome's own status and code. Anything thrown is a 500.
647
+ if (cause instanceof PublishRefused) {
648
+ c.status(statusFor(cause.code) as never);
649
+ return c.json(
650
+ toWireError(
651
+ {
652
+ code: cause.code,
653
+ message: `${cause.message}; nothing was deployed`,
654
+ primitive: cause.primitive,
655
+ id: cause.id,
656
+ },
657
+ requestId,
658
+ ),
659
+ );
660
+ }
633
661
  if (cause instanceof UnpinnableDeploy) {
634
662
  c.status(statusFor('invalid-agent') as never);
635
663
  return c.json(
@@ -1495,6 +1523,9 @@ async function registerAgents(input: {
1495
1523
  for (const agent of input.defined) {
1496
1524
  const outcome = await agents.publish({ tenantId, projectId, agent, enqueueTuples });
1497
1525
  if (outcome.kind === 'ok') written(outcome.agentId, outcome.version);
1526
+ else if (outcome.kind !== 'already-registered') {
1527
+ throw new PublishRefused('agent', `${agent.id}@${agent.version}`, outcome.kind);
1528
+ }
1498
1529
  }
1499
1530
  return input.defined.map(deployedPrimitive);
1500
1531
  }
@@ -1524,7 +1555,10 @@ async function registerAgents(input: {
1524
1555
  * as, pushing a rollback for each version it writes. Runs after the
1525
1556
  * agents, so a flow's agent pins see the versions this deploy
1526
1557
  * registered: a tool change cascades through an agent into a flow
1527
- * within the one deploy, each derived once.
1558
+ * within the one deploy, each derived once. An agent step that names no
1559
+ * version pins what a run in the project gets: the agent's live version
1560
+ * there, with `live`, else the latest (this deploy's, when it registered
1561
+ * one).
1528
1562
  *
1529
1563
  * With the tool and agent registries, every flow is pinned first; one
1530
1564
  * that runs a tool or agent with no published version throws
@@ -1535,12 +1569,13 @@ async function registerFlows(input: {
1535
1569
  readonly flows: FlowRegistryBinding;
1536
1570
  readonly tools: ToolRegistryBinding | undefined;
1537
1571
  readonly agents: AgentRegistryBinding | undefined;
1572
+ readonly live?: LiveVersionBinding;
1538
1573
  readonly tenantId: TenantId;
1539
1574
  readonly projectId: ProjectId;
1540
1575
  readonly defined: readonly Flow[];
1541
1576
  readonly rolled: RollbackAction[];
1542
1577
  }): Promise<DeployedFlow[]> {
1543
- const { flows, tools, agents, tenantId, projectId, rolled } = input;
1578
+ const { flows, tools, agents, live, tenantId, projectId, rolled } = input;
1544
1579
  const enqueueTuples = (flowId: string) =>
1545
1580
  tuplesForCreate({ kind: 'flow', id: flowId as FlowId, tenantId, projectId });
1546
1581
  const written = (flowId: FlowId, version: string) =>
@@ -1552,11 +1587,20 @@ async function registerFlows(input: {
1552
1587
  for (const flow of input.defined) {
1553
1588
  const outcome = await flows.publish({ tenantId, projectId, flow, enqueueTuples });
1554
1589
  if (outcome.kind === 'ok') written(outcome.flowId, outcome.version as unknown as string);
1590
+ else if (outcome.kind !== 'already-registered') {
1591
+ throw new PublishRefused('flow', `${flow.id}@${flow.version}`, outcome.kind);
1592
+ }
1555
1593
  }
1556
1594
  return input.defined.map(deployedPrimitive);
1557
1595
  }
1558
1596
 
1559
- const pinsOf = await pinFlows(tools, agents, tenantId, input.defined);
1597
+ const pinsOf = await pinFlows(
1598
+ tools,
1599
+ agents,
1600
+ tenantId,
1601
+ input.defined,
1602
+ live !== undefined ? { binding: live, projectId } : undefined,
1603
+ );
1560
1604
  const deployed: DeployedFlow[] = [];
1561
1605
  for (const [i, flow] of input.defined.entries()) {
1562
1606
  const outcome = await publishDeployedFlow({
@@ -1581,11 +1625,12 @@ async function pinFlows(
1581
1625
  agents: AgentRegistryBinding,
1582
1626
  tenantId: TenantId,
1583
1627
  defined: readonly Flow[],
1628
+ live: FlowPinsLive | undefined,
1584
1629
  ): Promise<FlowPins[]> {
1585
1630
  const pins: FlowPins[] = [];
1586
1631
  const unpinnable: UnpinnableRef[] = [];
1587
1632
  for (const [i, flow] of defined.entries()) {
1588
- const resolved = await resolveFlowPins(tools, agents, tenantId, flow);
1633
+ const resolved = await resolveFlowPins(tools, agents, tenantId, flow, live);
1589
1634
  if (resolved.kind === 'ok') {
1590
1635
  pins.push(resolved.pins);
1591
1636
  continue;
package/src/routes/env.ts CHANGED
@@ -87,6 +87,7 @@ export function envRouter(envBinding: EnvBinding, authorizer?: Authorizer): Hono
87
87
  });
88
88
  return c.json({
89
89
  data: page.data.map(serializeEnvRecord),
90
+ hasMore: page.nextCursor !== undefined,
90
91
  ...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
91
92
  });
92
93
  });
@@ -7,6 +7,7 @@ import type { ProjectId, Semver } from '@kindgi/types';
7
7
 
8
8
  import type { EvalBaseline, EvalComparison } from '../eval-run-binding.js';
9
9
  import { DEFAULT_COMPARISON } from '../judged-dispatcher.js';
10
+ import { parseSegmentsBody } from './segments.js';
10
11
 
11
12
  const MAX_REPETITIONS = 10;
12
13
  const MAX_K = 100;
@@ -20,19 +21,18 @@ function obj(value: unknown): Record<string, unknown> | undefined {
20
21
  function parseLive(raw: unknown): EvalBaseline | string {
21
22
  const live = obj(raw);
22
23
  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
24
  if (live.projectId !== undefined && typeof live.projectId !== 'string') {
30
25
  return '`baseline.live.projectId` must be a string';
31
26
  }
27
+ const path = parseSegmentsBody(live.segments, 'baseline.live.segments');
28
+ if (path.kind === 'err') return path.message;
29
+ if (path.segments !== undefined && live.projectId === undefined) {
30
+ return '`baseline.live.segments` is a segment path in a project: it needs `baseline.live.projectId`';
31
+ }
32
32
  return {
33
33
  live: {
34
34
  ...(typeof live.projectId === 'string' && { projectId: live.projectId as ProjectId }),
35
- ...(segments !== undefined && { segments: segments as Record<string, string> }),
35
+ ...(path.segments !== undefined && { segments: path.segments }),
36
36
  },
37
37
  };
38
38
  }
@@ -86,7 +86,8 @@ function integerIn(raw: unknown, name: string, max: number): number | string {
86
86
 
87
87
  /**
88
88
  * A comparison eval run's settings from the start body: `baseline`,
89
- * `reads`, `repetitions`, `k` and a flow candidate's `versions`.
89
+ * `reads`, `repetitions`, `k`, `classWeights` and a flow candidate's
90
+ * `versions`.
90
91
  * `undefined` when none is given (the defaults apply); an error message
91
92
  * for a bad one.
92
93
  */
@@ -95,7 +96,7 @@ export function parseComparison(
95
96
  ):
96
97
  | { readonly kind: 'ok'; readonly value?: EvalComparison }
97
98
  | { readonly kind: 'err'; readonly message: string } {
98
- const { baseline, reads, repetitions, k } = b;
99
+ const { baseline, reads, repetitions, k, classWeights } = b;
99
100
  const versions = b.versions === undefined ? undefined : parseVersions(b.versions);
100
101
  if (typeof versions === 'string') return { kind: 'err', message: versions };
101
102
  if (
@@ -103,10 +104,22 @@ export function parseComparison(
103
104
  reads === undefined &&
104
105
  repetitions === undefined &&
105
106
  k === undefined &&
106
- versions === undefined
107
+ versions === undefined &&
108
+ classWeights === undefined
107
109
  ) {
108
110
  return { kind: 'ok' };
109
111
  }
112
+ if (
113
+ classWeights !== undefined &&
114
+ classWeights !== 'as-recorded' &&
115
+ classWeights !== 'restricted-only'
116
+ ) {
117
+ return {
118
+ kind: 'err',
119
+ message:
120
+ "`classWeights` must be 'as-recorded' (every judgment at its class's weight) or 'restricted-only' (only judgments recorded under a restricted class)",
121
+ };
122
+ }
110
123
  const parsedBaseline =
111
124
  baseline === undefined ? DEFAULT_COMPARISON.baseline : parseBaseline(baseline);
112
125
  if (typeof parsedBaseline === 'string' && parsedBaseline !== 'recorded') {
@@ -130,6 +143,7 @@ export function parseComparison(
130
143
  repetitions: reps,
131
144
  k: topK,
132
145
  ...(versions !== undefined && { versions }),
146
+ ...(classWeights !== undefined && { classWeights }),
133
147
  },
134
148
  };
135
149
  }
@@ -12,6 +12,7 @@ import type { UnpinnableRef } from '../agent-pins.js';
12
12
  import { statusFor, toWireError } from '../errors.js';
13
13
  import type { FlowRegistryBinding, FlowVersionRecord } from '../flow-binding.js';
14
14
  import { resolveFlowPins } from '../flow-pins.js';
15
+ import type { LiveVersionBinding } from '../live-version-binding.js';
15
16
  import type { Authorizer } from '../middleware/authorize.js';
16
17
  import { refuseWritesWhenReadOnly } from '../registry-read-only.js';
17
18
  import type { ToolRegistryBinding } from '../tool-binding.js';
@@ -37,16 +38,18 @@ import { parseScopeParams } from './scope-params.js';
37
38
  * `/v1/flows/:flowId/run` route is added here.
38
39
  *
39
40
  * With `pinning` (the tool and agent registries), a published version is
40
- * pinned: each tool it runs, and each agent it runs at no named version,
41
- * resolves once, at publish, to the latest version, which every run of
42
- * that version uses (`pins`, see `resolveFlowPins`); one with no
43
- * published version refuses the publish. Without it, versions carry no
44
- * pins and bind the latest versions per run.
41
+ * pinned: each tool it runs resolves once, at publish, to its latest
42
+ * version, and each agent it runs at no named version to what a run in
43
+ * the flow's project would get, its live version there with
44
+ * `pinning.live`, else its latest. Every run of that version uses those
45
+ * (`pins`, see `resolveFlowPins`); one with no published version refuses
46
+ * the publish. Without `pinning`, versions carry no pins and bind the
47
+ * latest versions per run.
45
48
  */
46
49
  export function flowsRouter(
47
50
  binding: FlowRegistryBinding,
48
51
  authorizer?: Authorizer,
49
- pinning?: { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding },
52
+ pinning?: FlowPinning,
50
53
  ): Hono<AppEnv> {
51
54
  const r = new Hono<AppEnv>();
52
55
  // A read-only registry (under `kindgi dev`, the pack's files) refuses
@@ -300,7 +303,7 @@ export function flowsRouter(
300
303
  // Pin the version: every tool and unversioned agent resolves now,
301
304
  // once, to the version its runs use. One with no published version
302
305
  // refuses the publish rather than store a partly pinned version.
303
- const pinned = await pinFlow(pinning, tenantId, loaded.value);
306
+ const pinned = await pinFlow(pinning, tenantId, projectId, loaded.value);
304
307
  if (pinned.kind === 'unpinnable') {
305
308
  c.status(statusFor('validation-failed') as never);
306
309
  return c.json(toWireError(unpinnableFlow(loaded.value, pinned.issues), requestId));
@@ -415,19 +418,32 @@ export function flowsRouter(
415
418
  return r;
416
419
  }
417
420
 
421
+ /** The registries a published flow version is pinned against. */
422
+ export interface FlowPinning {
423
+ readonly tools: ToolRegistryBinding;
424
+ readonly agents: AgentRegistryBinding;
425
+ /** The agents' live versions: an agent step pins the one live for the flow's project. */
426
+ readonly live?: LiveVersionBinding;
427
+ }
428
+
418
429
  /** The flow with its pins, or the references with no published version; as it is without `pinning`. */
419
430
  async function pinFlow(
420
- pinning:
421
- | { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding }
422
- | undefined,
431
+ pinning: FlowPinning | undefined,
423
432
  tenantId: TenantId,
433
+ projectId: ProjectId,
424
434
  flow: Flow,
425
435
  ): Promise<
426
436
  | { readonly kind: 'ok'; readonly flow: Flow }
427
437
  | { readonly kind: 'unpinnable'; readonly issues: readonly UnpinnableRef[] }
428
438
  > {
429
439
  if (pinning === undefined) return { kind: 'ok', flow };
430
- const resolved = await resolveFlowPins(pinning.tools, pinning.agents, tenantId, flow);
440
+ const resolved = await resolveFlowPins(
441
+ pinning.tools,
442
+ pinning.agents,
443
+ tenantId,
444
+ flow,
445
+ pinning.live !== undefined ? { binding: pinning.live, projectId } : undefined,
446
+ );
431
447
  if (resolved.kind === 'unpinnable') return resolved;
432
448
  return {
433
449
  kind: 'ok',