@kindgi/api 0.1.4-rc.0 → 0.1.4-rc.2

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 (262) hide show
  1. package/dist/agent-binding.d.ts +20 -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 +30 -5
  9. package/dist/app.js.map +1 -1
  10. package/dist/deploy-versions.d.ts +10 -5
  11. package/dist/deploy-versions.d.ts.map +1 -1
  12. package/dist/deploy-versions.js +4 -10
  13. package/dist/deploy-versions.js.map +1 -1
  14. package/dist/derive-agent-version.d.ts +9 -1
  15. package/dist/derive-agent-version.d.ts.map +1 -1
  16. package/dist/derive-agent-version.js +11 -1
  17. package/dist/derive-agent-version.js.map +1 -1
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +26 -0
  20. package/dist/errors.js.map +1 -1
  21. package/dist/eval-case-binding.d.ts +10 -0
  22. package/dist/eval-case-binding.d.ts.map +1 -1
  23. package/dist/eval-run-binding.d.ts +21 -3
  24. package/dist/eval-run-binding.d.ts.map +1 -1
  25. package/dist/eval-run-binding.js.map +1 -1
  26. package/dist/eval-run-dispatcher.d.ts +3 -0
  27. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  28. package/dist/eval-run-dispatcher.js.map +1 -1
  29. package/dist/flow-binding.d.ts +8 -0
  30. package/dist/flow-binding.d.ts.map +1 -1
  31. package/dist/flow-pins.d.ts +20 -7
  32. package/dist/flow-pins.d.ts.map +1 -1
  33. package/dist/flow-pins.js +36 -11
  34. package/dist/flow-pins.js.map +1 -1
  35. package/dist/gate-policy-binding.d.ts +164 -0
  36. package/dist/gate-policy-binding.d.ts.map +1 -0
  37. package/dist/gate-policy-binding.js +12 -0
  38. package/dist/gate-policy-binding.js.map +1 -0
  39. package/dist/gate.d.ts +56 -0
  40. package/dist/gate.d.ts.map +1 -0
  41. package/dist/gate.js +359 -0
  42. package/dist/gate.js.map +1 -0
  43. package/dist/guardrail-binding.d.ts +8 -0
  44. package/dist/guardrail-binding.d.ts.map +1 -1
  45. package/dist/handler-binding.d.ts +17 -2
  46. package/dist/handler-binding.d.ts.map +1 -1
  47. package/dist/index.d.ts +11 -4
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/judged-dispatcher.d.ts +20 -1
  52. package/dist/judged-dispatcher.d.ts.map +1 -1
  53. package/dist/judged-dispatcher.js +57 -7
  54. package/dist/judged-dispatcher.js.map +1 -1
  55. package/dist/judgment-binding.d.ts +38 -0
  56. package/dist/judgment-binding.d.ts.map +1 -1
  57. package/dist/judgment-binding.js +19 -0
  58. package/dist/judgment-binding.js.map +1 -1
  59. package/dist/live-version-binding.d.ts +206 -0
  60. package/dist/live-version-binding.d.ts.map +1 -0
  61. package/dist/live-version-binding.js +4 -0
  62. package/dist/live-version-binding.js.map +1 -0
  63. package/dist/middleware/authorize.d.ts.map +1 -1
  64. package/dist/middleware/authorize.js +13 -2
  65. package/dist/middleware/authorize.js.map +1 -1
  66. package/dist/middleware/project-ref.d.ts +25 -0
  67. package/dist/middleware/project-ref.d.ts.map +1 -0
  68. package/dist/middleware/project-ref.js +72 -0
  69. package/dist/middleware/project-ref.js.map +1 -0
  70. package/dist/openapi/generate.d.ts.map +1 -1
  71. package/dist/openapi/generate.js +4 -0
  72. package/dist/openapi/generate.js.map +1 -1
  73. package/dist/openapi/operations.d.ts +6 -0
  74. package/dist/openapi/operations.d.ts.map +1 -1
  75. package/dist/openapi/operations.js +524 -27
  76. package/dist/openapi/operations.js.map +1 -1
  77. package/dist/openapi/schemas.d.ts +43 -0
  78. package/dist/openapi/schemas.d.ts.map +1 -1
  79. package/dist/openapi/schemas.js +1050 -7
  80. package/dist/openapi/schemas.js.map +1 -1
  81. package/dist/publish-refused.d.ts +18 -0
  82. package/dist/publish-refused.d.ts.map +1 -0
  83. package/dist/publish-refused.js +22 -0
  84. package/dist/publish-refused.js.map +1 -0
  85. package/dist/registry-read-only.d.ts +32 -0
  86. package/dist/registry-read-only.d.ts.map +1 -0
  87. package/dist/registry-read-only.js +22 -0
  88. package/dist/registry-read-only.js.map +1 -0
  89. package/dist/retention-binding.d.ts +29 -0
  90. package/dist/retention-binding.d.ts.map +1 -1
  91. package/dist/routes/agent-releases.d.ts +40 -0
  92. package/dist/routes/agent-releases.d.ts.map +1 -0
  93. package/dist/routes/agent-releases.js +462 -0
  94. package/dist/routes/agent-releases.js.map +1 -0
  95. package/dist/routes/agents.d.ts +7 -1
  96. package/dist/routes/agents.d.ts.map +1 -1
  97. package/dist/routes/agents.js +38 -4
  98. package/dist/routes/agents.js.map +1 -1
  99. package/dist/routes/approvals.d.ts.map +1 -1
  100. package/dist/routes/approvals.js +27 -9
  101. package/dist/routes/approvals.js.map +1 -1
  102. package/dist/routes/audit.d.ts.map +1 -1
  103. package/dist/routes/audit.js +7 -0
  104. package/dist/routes/audit.js.map +1 -1
  105. package/dist/routes/auth.js +1 -1
  106. package/dist/routes/auth.js.map +1 -1
  107. package/dist/routes/blocks.d.ts.map +1 -1
  108. package/dist/routes/blocks.js +37 -12
  109. package/dist/routes/blocks.js.map +1 -1
  110. package/dist/routes/conversations.d.ts +0 -7
  111. package/dist/routes/conversations.d.ts.map +1 -1
  112. package/dist/routes/conversations.js +19 -3
  113. package/dist/routes/conversations.js.map +1 -1
  114. package/dist/routes/deployments.d.ts +6 -0
  115. package/dist/routes/deployments.d.ts.map +1 -1
  116. package/dist/routes/deployments.js +49 -5
  117. package/dist/routes/deployments.js.map +1 -1
  118. package/dist/routes/env.d.ts.map +1 -1
  119. package/dist/routes/env.js +1 -0
  120. package/dist/routes/env.js.map +1 -1
  121. package/dist/routes/eval-comparison.d.ts +4 -2
  122. package/dist/routes/eval-comparison.d.ts.map +1 -1
  123. package/dist/routes/eval-comparison.js +59 -10
  124. package/dist/routes/eval-comparison.js.map +1 -1
  125. package/dist/routes/eval-runs.d.ts +7 -1
  126. package/dist/routes/eval-runs.d.ts.map +1 -1
  127. package/dist/routes/eval-runs.js +34 -3
  128. package/dist/routes/eval-runs.js.map +1 -1
  129. package/dist/routes/eval-versions.d.ts +25 -0
  130. package/dist/routes/eval-versions.d.ts.map +1 -0
  131. package/dist/routes/eval-versions.js +66 -0
  132. package/dist/routes/eval-versions.js.map +1 -0
  133. package/dist/routes/flows.d.ts +14 -7
  134. package/dist/routes/flows.d.ts.map +1 -1
  135. package/dist/routes/flows.js +14 -8
  136. package/dist/routes/flows.js.map +1 -1
  137. package/dist/routes/gate-policies.d.ts +19 -0
  138. package/dist/routes/gate-policies.d.ts.map +1 -0
  139. package/dist/routes/gate-policies.js +191 -0
  140. package/dist/routes/gate-policies.js.map +1 -0
  141. package/dist/routes/gate-policy-spec.d.ts +17 -0
  142. package/dist/routes/gate-policy-spec.d.ts.map +1 -0
  143. package/dist/routes/gate-policy-spec.js +192 -0
  144. package/dist/routes/gate-policy-spec.js.map +1 -0
  145. package/dist/routes/gate-policy-wire.d.ts +3 -0
  146. package/dist/routes/gate-policy-wire.d.ts.map +1 -0
  147. package/dist/routes/gate-policy-wire.js +18 -0
  148. package/dist/routes/gate-policy-wire.js.map +1 -0
  149. package/dist/routes/guardrails.d.ts.map +1 -1
  150. package/dist/routes/guardrails.js +4 -0
  151. package/dist/routes/guardrails.js.map +1 -1
  152. package/dist/routes/hierarchy-errors.d.ts +21 -0
  153. package/dist/routes/hierarchy-errors.d.ts.map +1 -1
  154. package/dist/routes/hierarchy-errors.js +21 -0
  155. package/dist/routes/hierarchy-errors.js.map +1 -1
  156. package/dist/routes/judged-suites.js +7 -0
  157. package/dist/routes/judged-suites.js.map +1 -1
  158. package/dist/routes/judgments.d.ts +4 -1
  159. package/dist/routes/judgments.d.ts.map +1 -1
  160. package/dist/routes/judgments.js +92 -19
  161. package/dist/routes/judgments.js.map +1 -1
  162. package/dist/routes/live-scope-wire.d.ts +32 -0
  163. package/dist/routes/live-scope-wire.d.ts.map +1 -0
  164. package/dist/routes/live-scope-wire.js +71 -0
  165. package/dist/routes/live-scope-wire.js.map +1 -0
  166. package/dist/routes/policies.d.ts +6 -3
  167. package/dist/routes/policies.d.ts.map +1 -1
  168. package/dist/routes/policies.js +55 -3
  169. package/dist/routes/policies.js.map +1 -1
  170. package/dist/routes/projects.d.ts.map +1 -1
  171. package/dist/routes/projects.js +59 -9
  172. package/dist/routes/projects.js.map +1 -1
  173. package/dist/routes/retention.d.ts.map +1 -1
  174. package/dist/routes/retention.js +18 -2
  175. package/dist/routes/retention.js.map +1 -1
  176. package/dist/routes/reviewers.d.ts +8 -1
  177. package/dist/routes/reviewers.d.ts.map +1 -1
  178. package/dist/routes/reviewers.js +20 -3
  179. package/dist/routes/reviewers.js.map +1 -1
  180. package/dist/routes/runs.d.ts.map +1 -1
  181. package/dist/routes/runs.js +23 -13
  182. package/dist/routes/runs.js.map +1 -1
  183. package/dist/routes/secrets.d.ts.map +1 -1
  184. package/dist/routes/secrets.js +2 -0
  185. package/dist/routes/secrets.js.map +1 -1
  186. package/dist/routes/segments.d.ts +17 -0
  187. package/dist/routes/segments.d.ts.map +1 -0
  188. package/dist/routes/segments.js +68 -0
  189. package/dist/routes/segments.js.map +1 -0
  190. package/dist/routes/teams.d.ts.map +1 -1
  191. package/dist/routes/teams.js +53 -8
  192. package/dist/routes/teams.js.map +1 -1
  193. package/dist/routes/tools.d.ts.map +1 -1
  194. package/dist/routes/tools.js +4 -0
  195. package/dist/routes/tools.js.map +1 -1
  196. package/dist/routes/uuid-param.d.ts +11 -0
  197. package/dist/routes/uuid-param.d.ts.map +1 -0
  198. package/dist/routes/uuid-param.js +19 -0
  199. package/dist/routes/uuid-param.js.map +1 -0
  200. package/dist/tool-binding.d.ts +8 -0
  201. package/dist/tool-binding.d.ts.map +1 -1
  202. package/openapi.json +6484 -2402
  203. package/package.json +21 -21
  204. package/src/agent-binding.ts +21 -2
  205. package/src/agent-pins.ts +3 -1
  206. package/src/app.ts +47 -4
  207. package/src/deploy-versions.ts +12 -15
  208. package/src/derive-agent-version.ts +12 -1
  209. package/src/errors.ts +26 -0
  210. package/src/eval-case-binding.ts +7 -0
  211. package/src/eval-run-binding.ts +31 -3
  212. package/src/eval-run-dispatcher.ts +3 -0
  213. package/src/flow-binding.ts +9 -0
  214. package/src/flow-pins.ts +49 -9
  215. package/src/gate-policy-binding.ts +171 -0
  216. package/src/gate.ts +467 -0
  217. package/src/guardrail-binding.ts +9 -0
  218. package/src/handler-binding.ts +17 -2
  219. package/src/index.ts +47 -1
  220. package/src/judged-dispatcher.ts +100 -9
  221. package/src/judgment-binding.ts +62 -0
  222. package/src/live-version-binding.ts +237 -0
  223. package/src/middleware/authorize.ts +18 -2
  224. package/src/middleware/project-ref.ts +88 -0
  225. package/src/openapi/generate.ts +5 -0
  226. package/src/openapi/operations.ts +664 -27
  227. package/src/openapi/schemas.ts +1175 -33
  228. package/src/publish-refused.ts +31 -0
  229. package/src/registry-read-only.ts +43 -0
  230. package/src/retention-binding.ts +30 -0
  231. package/src/routes/agent-releases.ts +581 -0
  232. package/src/routes/agents.ts +53 -3
  233. package/src/routes/approvals.ts +36 -8
  234. package/src/routes/audit.ts +10 -0
  235. package/src/routes/auth.ts +1 -1
  236. package/src/routes/blocks.ts +39 -14
  237. package/src/routes/conversations.ts +31 -3
  238. package/src/routes/deployments.ts +75 -5
  239. package/src/routes/env.ts +1 -0
  240. package/src/routes/eval-comparison.ts +59 -11
  241. package/src/routes/eval-runs.ts +46 -3
  242. package/src/routes/eval-versions.ts +110 -0
  243. package/src/routes/flows.ts +34 -11
  244. package/src/routes/gate-policies.ts +226 -0
  245. package/src/routes/gate-policy-spec.ts +229 -0
  246. package/src/routes/gate-policy-wire.ts +20 -0
  247. package/src/routes/guardrails.ts +7 -0
  248. package/src/routes/hierarchy-errors.ts +23 -0
  249. package/src/routes/judged-suites.ts +5 -0
  250. package/src/routes/judgments.ts +107 -23
  251. package/src/routes/live-scope-wire.ts +90 -0
  252. package/src/routes/policies.ts +61 -3
  253. package/src/routes/projects.ts +72 -9
  254. package/src/routes/retention.ts +21 -2
  255. package/src/routes/reviewers.ts +23 -4
  256. package/src/routes/runs.ts +34 -20
  257. package/src/routes/secrets.ts +2 -0
  258. package/src/routes/segments.ts +75 -0
  259. package/src/routes/teams.ts +66 -8
  260. package/src/routes/tools.ts +7 -0
  261. package/src/routes/uuid-param.ts +28 -0
  262. package/src/tool-binding.ts +9 -0
@@ -406,6 +406,7 @@ export function approvalsRouter(
406
406
  // override it, when the resume payload must carry more than the
407
407
  // decision (an agent gate refuses one, above).
408
408
  let waitpointResolved = false;
409
+ let resume: ResumeReport | undefined;
409
410
  if (
410
411
  approval.waitTokenId !== undefined &&
411
412
  approval.provenanceRef?.runId !== undefined &&
@@ -440,15 +441,12 @@ export function approvalsRouter(
440
441
  // journaled `wait.resumed`. The approval-complete surface reports
441
442
  // the decision as successful; a run that stayed suspended because
442
443
  // the inline resume failed can be resumed later.
444
+ //
445
+ // The response says how the resume went (`resume`), so a decision
446
+ // whose run couldn't go on (a tool version it started with is gone,
447
+ // say) isn't reported as plain success.
443
448
  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
- }
449
+ resume = await resumeInline(runHandler, tenantId, approval.provenanceRef.runId as RunId);
452
450
  }
453
451
  }
454
452
 
@@ -461,6 +459,7 @@ export function approvalsRouter(
461
459
  nextApproval: serializeApproval(result.nextApproval),
462
460
  }),
463
461
  waitpointResolved,
462
+ ...(resume !== undefined && { resume }),
464
463
  });
465
464
  });
466
465
 
@@ -808,3 +807,32 @@ function parseAuditBundleBody(
808
807
  },
809
808
  };
810
809
  }
810
+
811
+ /** How the inline resume after a decision went. */
812
+ type ResumeReport =
813
+ | { readonly kind: 'ok' }
814
+ | { readonly kind: 'failed'; readonly code: string; readonly message: string };
815
+
816
+ /**
817
+ * Resume the run a decision released, in the same request. A resume that
818
+ * fails is reported, not raised: the decision is durable either way, and
819
+ * the runtime ends a run that can't go on, or resumes it later.
820
+ */
821
+ async function resumeInline(
822
+ runHandler: RunHandlerBinding,
823
+ tenantId: TenantId,
824
+ runId: RunId,
825
+ ): Promise<ResumeReport> {
826
+ try {
827
+ const outcome = await runHandler.resumeRun({ tenantId, runId });
828
+ return outcome.kind === 'ok'
829
+ ? { kind: 'ok' }
830
+ : { kind: 'failed', code: outcome.error.code, message: outcome.error.message };
831
+ } catch (cause) {
832
+ return {
833
+ kind: 'failed',
834
+ code: 'resume-failed',
835
+ message: cause instanceof Error ? cause.message : String(cause),
836
+ };
837
+ }
838
+ }
@@ -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 ----------
@@ -184,14 +184,20 @@ export function blocksRouter(binding: BlockRegistryBinding, authorizer?: Authori
184
184
  return validationFailed(c, validated.error.message, validated.error.issues);
185
185
  const block = validated.value;
186
186
 
187
- // A block keeps its kind, and a settings block's latest schema holds.
187
+ // A block keeps its kind. A settings version without a schema keeps
188
+ // the latest version's (stored on it, so the check carries forward to
189
+ // every later version); one that gives a schema replaces it.
188
190
  const latest = await binding.get({ tenantId, blockId: block.id });
189
191
  const continuity = continuityIssues(latest, block);
190
192
  if (continuity.length > 0) {
191
193
  return validationFailed(c, `Block "${block.id}" can't take this version`, continuity);
192
194
  }
193
195
 
194
- const outcome = await binding.publish({ tenantId, projectId: projectId as ProjectId, block });
196
+ const outcome = await binding.publish({
197
+ tenantId,
198
+ projectId: projectId as ProjectId,
199
+ block: withCarriedSchema(latest, block),
200
+ });
195
201
  return published(c, outcome);
196
202
  });
197
203
 
@@ -243,7 +249,11 @@ function isBlockKind(value: string): value is BlockKind {
243
249
  return (BLOCK_KINDS as readonly string[]).includes(value);
244
250
  }
245
251
 
246
- /** A new version against the block's latest: same kind, and a settings block's schema holds. */
252
+ /**
253
+ * A new version against the block's latest: the same kind, and a
254
+ * settings version without a schema of its own satisfies the schema it
255
+ * keeps (`carriedSchema`).
256
+ */
247
257
  function continuityIssues(
248
258
  latest: BlockRecord | null,
249
259
  block: BlockDefinition,
@@ -257,17 +267,32 @@ function continuityIssues(
257
267
  },
258
268
  ];
259
269
  }
260
- if (
261
- latest.kind === 'settings' &&
262
- block.kind === 'settings' &&
263
- latest.content.schema !== undefined
264
- ) {
265
- return settingsSchemaIssues(block.content.values, latest.content.schema).map((i) => ({
266
- ...i,
267
- message: `${i.message} (the schema of version ${latest.version})`,
268
- }));
269
- }
270
- return [];
270
+ const carried = carriedSchema(latest, block);
271
+ if (block.kind !== 'settings' || carried === undefined) return [];
272
+ return settingsSchemaIssues(block.content.values, carried).map((i) => ({
273
+ ...i,
274
+ message: `${i.message} (the schema of version ${latest.version})`,
275
+ }));
276
+ }
277
+
278
+ /**
279
+ * The schema a settings version that gives none keeps: the latest
280
+ * version's. A version that gives a schema replaces it (`{}` drops the
281
+ * check on purpose).
282
+ */
283
+ function carriedSchema(
284
+ latest: BlockRecord | null,
285
+ block: BlockDefinition,
286
+ ): Readonly<Record<string, unknown>> | undefined {
287
+ if (latest?.kind !== 'settings' || block.kind !== 'settings') return undefined;
288
+ return block.content.schema === undefined ? latest.content.schema : undefined;
289
+ }
290
+
291
+ /** The version as stored: with the schema it keeps, if any. */
292
+ function withCarriedSchema(latest: BlockRecord | null, block: BlockDefinition): BlockDefinition {
293
+ const schema = carriedSchema(latest, block);
294
+ if (schema === undefined || block.kind !== 'settings') return block;
295
+ return { ...block, content: { ...block.content, schema } };
271
296
  }
272
297
 
273
298
  /** The response to a publish outcome. */
@@ -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,12 @@ 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';
46
+ import { type RegistryReadOnly, refuseReadOnly } from '../registry-read-only.js';
44
47
  import type { SecretBinding } from '../secrets-binding.js';
45
48
  import type { SigningKeyBinding as SigningKeyRegistryBinding } from '../signing-key-binding.js';
46
49
  import type { ToolRegistryBinding } from '../tool-binding.js';
@@ -121,6 +124,11 @@ export interface DeploymentsRouterBindings {
121
124
  readonly guardrailRegistry?: GuardrailRegistryBinding;
122
125
  readonly agentRegistry?: AgentRegistryBinding;
123
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;
124
132
  /** Data blocks: an agent's block references are pinned at deploy with its tools. */
125
133
  readonly blockRegistry?: BlockRegistryBinding;
126
134
  /**
@@ -487,6 +495,11 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
487
495
  }
488
496
  const validated = validation.value;
489
497
 
498
+ // A read-only registry (under `kindgi dev`, the pack's files) takes
499
+ // nothing a deployment brings: refuse before any write.
500
+ const readOnly = readOnlyTarget(validated, bindings);
501
+ if (readOnly !== undefined) return refuseReadOnly(c, readOnly);
502
+
490
503
  // ---- Registry upserts (with rollback tracking) ----
491
504
  //
492
505
  // Agent-registry, flow-registry, tool-registry, and
@@ -566,6 +579,8 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
566
579
  rolled.push(async () => {
567
580
  await bindings.toolRegistry?.unregister({ tenantId, toolId, version });
568
581
  });
582
+ } else if (outcome.kind !== 'already-registered') {
583
+ throw new PublishRefused('tool', `${tool.id}@${tool.version}`, outcome.kind);
569
584
  }
570
585
  }
571
586
  }
@@ -589,6 +604,8 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
589
604
  rolled.push(async () => {
590
605
  await bindings.guardrailRegistry?.unregister({ tenantId, guardrailId });
591
606
  });
607
+ } else if (outcome.kind !== 'already-registered') {
608
+ throw new PublishRefused('guardrail', guardrail.id, outcome.kind);
592
609
  }
593
610
  }
594
611
  }
@@ -613,6 +630,7 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
613
630
  flows: bindings.flowRegistry,
614
631
  tools: bindings.toolRegistry,
615
632
  agents: bindings.agentRegistry,
633
+ ...(bindings.liveVersions !== undefined && { live: bindings.liveVersions }),
616
634
  tenantId,
617
635
  projectId: defaultProjectId,
618
636
  defined: validated.flows,
@@ -624,6 +642,22 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
624
642
  }
625
643
  } catch (cause) {
626
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
+ }
627
661
  if (cause instanceof UnpinnableDeploy) {
628
662
  c.status(statusFor('invalid-agent') as never);
629
663
  return c.json(
@@ -1489,6 +1523,9 @@ async function registerAgents(input: {
1489
1523
  for (const agent of input.defined) {
1490
1524
  const outcome = await agents.publish({ tenantId, projectId, agent, enqueueTuples });
1491
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
+ }
1492
1529
  }
1493
1530
  return input.defined.map(deployedPrimitive);
1494
1531
  }
@@ -1518,7 +1555,10 @@ async function registerAgents(input: {
1518
1555
  * as, pushing a rollback for each version it writes. Runs after the
1519
1556
  * agents, so a flow's agent pins see the versions this deploy
1520
1557
  * registered: a tool change cascades through an agent into a flow
1521
- * 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).
1522
1562
  *
1523
1563
  * With the tool and agent registries, every flow is pinned first; one
1524
1564
  * that runs a tool or agent with no published version throws
@@ -1529,12 +1569,13 @@ async function registerFlows(input: {
1529
1569
  readonly flows: FlowRegistryBinding;
1530
1570
  readonly tools: ToolRegistryBinding | undefined;
1531
1571
  readonly agents: AgentRegistryBinding | undefined;
1572
+ readonly live?: LiveVersionBinding;
1532
1573
  readonly tenantId: TenantId;
1533
1574
  readonly projectId: ProjectId;
1534
1575
  readonly defined: readonly Flow[];
1535
1576
  readonly rolled: RollbackAction[];
1536
1577
  }): Promise<DeployedFlow[]> {
1537
- const { flows, tools, agents, tenantId, projectId, rolled } = input;
1578
+ const { flows, tools, agents, live, tenantId, projectId, rolled } = input;
1538
1579
  const enqueueTuples = (flowId: string) =>
1539
1580
  tuplesForCreate({ kind: 'flow', id: flowId as FlowId, tenantId, projectId });
1540
1581
  const written = (flowId: FlowId, version: string) =>
@@ -1546,11 +1587,20 @@ async function registerFlows(input: {
1546
1587
  for (const flow of input.defined) {
1547
1588
  const outcome = await flows.publish({ tenantId, projectId, flow, enqueueTuples });
1548
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
+ }
1549
1593
  }
1550
1594
  return input.defined.map(deployedPrimitive);
1551
1595
  }
1552
1596
 
1553
- 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
+ );
1554
1604
  const deployed: DeployedFlow[] = [];
1555
1605
  for (const [i, flow] of input.defined.entries()) {
1556
1606
  const outcome = await publishDeployedFlow({
@@ -1575,11 +1625,12 @@ async function pinFlows(
1575
1625
  agents: AgentRegistryBinding,
1576
1626
  tenantId: TenantId,
1577
1627
  defined: readonly Flow[],
1628
+ live: FlowPinsLive | undefined,
1578
1629
  ): Promise<FlowPins[]> {
1579
1630
  const pins: FlowPins[] = [];
1580
1631
  const unpinnable: UnpinnableRef[] = [];
1581
1632
  for (const [i, flow] of defined.entries()) {
1582
- const resolved = await resolveFlowPins(tools, agents, tenantId, flow);
1633
+ const resolved = await resolveFlowPins(tools, agents, tenantId, flow, live);
1583
1634
  if (resolved.kind === 'ok') {
1584
1635
  pins.push(resolved.pins);
1585
1636
  continue;
@@ -1701,3 +1752,22 @@ export function sha256HexPrefixed(bytes: Uint8Array): string {
1701
1752
  const hex = createHash('sha256').update(bytes).digest('hex');
1702
1753
  return `sha256:${hex}`;
1703
1754
  }
1755
+
1756
+ /** The first read-only registry a deployment would publish into, if any. */
1757
+ function readOnlyTarget(
1758
+ validated: {
1759
+ readonly tools: readonly unknown[];
1760
+ readonly agents: readonly unknown[];
1761
+ readonly flows: readonly unknown[];
1762
+ readonly guardrails: readonly unknown[];
1763
+ },
1764
+ bindings: DeploymentsRouterBindings,
1765
+ ): RegistryReadOnly | undefined {
1766
+ const targets = [
1767
+ [validated.tools, bindings.toolRegistry?.readOnly],
1768
+ [validated.agents, bindings.agentRegistry?.readOnly],
1769
+ [validated.flows, bindings.flowRegistry?.readOnly],
1770
+ [validated.guardrails, bindings.guardrailRegistry?.readOnly],
1771
+ ] as const;
1772
+ return targets.find(([brought, readOnly]) => brought.length > 0 && readOnly !== undefined)?.[1];
1773
+ }
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
  });
@@ -2,10 +2,12 @@
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
4
  import type { AgentId } from '@kindgi/agents';
5
+ import type { FlowVersionOverrides } from '@kindgi/flow';
5
6
  import type { ProjectId, Semver } from '@kindgi/types';
6
7
 
7
8
  import type { EvalBaseline, EvalComparison } from '../eval-run-binding.js';
8
9
  import { DEFAULT_COMPARISON } from '../judged-dispatcher.js';
10
+ import { parseSegmentsBody } from './segments.js';
9
11
 
10
12
  const MAX_REPETITIONS = 10;
11
13
  const MAX_K = 100;
@@ -19,19 +21,18 @@ function obj(value: unknown): Record<string, unknown> | undefined {
19
21
  function parseLive(raw: unknown): EvalBaseline | string {
20
22
  const live = obj(raw);
21
23
  if (live === undefined) return '`baseline.live` must be an object';
22
- const segments = live.segments === undefined ? undefined : obj(live.segments);
23
- const stringsOnly =
24
- segments !== undefined && Object.values(segments).every((v) => typeof v === 'string');
25
- if (live.segments !== undefined && !stringsOnly) {
26
- return '`baseline.live.segments` must be an object of strings';
27
- }
28
24
  if (live.projectId !== undefined && typeof live.projectId !== 'string') {
29
25
  return '`baseline.live.projectId` must be a string';
30
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
+ }
31
32
  return {
32
33
  live: {
33
34
  ...(typeof live.projectId === 'string' && { projectId: live.projectId as ProjectId }),
34
- ...(segments !== undefined && { segments: segments as Record<string, string> }),
35
+ ...(path.segments !== undefined && { segments: path.segments }),
35
36
  },
36
37
  };
37
38
  }
@@ -49,6 +50,34 @@ function parseBaseline(raw: unknown): EvalBaseline | string {
49
50
  return { agentId: o.agentId as AgentId, version: o.version as Semver };
50
51
  }
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
+
52
81
  function integerIn(raw: unknown, name: string, max: number): number | string {
53
82
  return typeof raw === 'number' && Number.isInteger(raw) && raw >= 1 && raw <= max
54
83
  ? raw
@@ -57,23 +86,40 @@ function integerIn(raw: unknown, name: string, max: number): number | string {
57
86
 
58
87
  /**
59
88
  * A comparison eval run's settings from the start body: `baseline`,
60
- * `reads`, `repetitions` and `k`. `undefined` when none is given (the
61
- * defaults apply); an error message for a bad one.
89
+ * `reads`, `repetitions`, `k`, `classWeights` and a flow candidate's
90
+ * `versions`.
91
+ * `undefined` when none is given (the defaults apply); an error message
92
+ * for a bad one.
62
93
  */
63
94
  export function parseComparison(
64
95
  b: Readonly<Record<string, unknown>>,
65
96
  ):
66
97
  | { readonly kind: 'ok'; readonly value?: EvalComparison }
67
98
  | { readonly kind: 'err'; readonly message: string } {
68
- const { baseline, reads, repetitions, k } = b;
99
+ const { baseline, reads, repetitions, k, classWeights } = b;
100
+ const versions = b.versions === undefined ? undefined : parseVersions(b.versions);
101
+ if (typeof versions === 'string') return { kind: 'err', message: versions };
69
102
  if (
70
103
  baseline === undefined &&
71
104
  reads === undefined &&
72
105
  repetitions === undefined &&
73
- k === undefined
106
+ k === undefined &&
107
+ versions === undefined &&
108
+ classWeights === undefined
74
109
  ) {
75
110
  return { kind: 'ok' };
76
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
+ }
77
123
  const parsedBaseline =
78
124
  baseline === undefined ? DEFAULT_COMPARISON.baseline : parseBaseline(baseline);
79
125
  if (typeof parsedBaseline === 'string' && parsedBaseline !== 'recorded') {
@@ -96,6 +142,8 @@ export function parseComparison(
96
142
  reads: reads ?? DEFAULT_COMPARISON.reads,
97
143
  repetitions: reps,
98
144
  k: topK,
145
+ ...(versions !== undefined && { versions }),
146
+ ...(classWeights !== undefined && { classWeights }),
99
147
  },
100
148
  };
101
149
  }
@@ -17,8 +17,10 @@ import {
17
17
  type EvalRunStatus,
18
18
  type FlowRef,
19
19
  } from '../eval-run-binding.js';
20
+ import { VERSIONS_NEED_A_FLOW } from '../judged-dispatcher.js';
20
21
  import type { AppEnv } from '../types.js';
21
22
  import { parseComparison } from './eval-comparison.js';
23
+ import { type FlowVersionsCheck, checkFlowVersions } from './eval-versions.js';
22
24
  import { clampLimit } from './pagination.js';
23
25
  import { parseScopeParams } from './scope-params.js';
24
26
  import { formatSseFrame } from './sse.js';
@@ -47,16 +49,49 @@ export interface EvalRunsRouters {
47
49
  readonly readback: Hono<AppEnv>;
48
50
  }
49
51
 
50
- export function evalRunsRouters(binding: EvalRunBinding): EvalRunsRouters {
52
+ /**
53
+ * `versionsCheck`: where a flow candidate's `versions` are checked
54
+ * against the flow version at start (without it, they're applied as
55
+ * given).
56
+ */
57
+ export function evalRunsRouters(
58
+ binding: EvalRunBinding,
59
+ versionsCheck?: FlowVersionsCheck,
60
+ ): EvalRunsRouters {
51
61
  return {
52
- start: startRouter(binding),
62
+ start: startRouter(binding, versionsCheck),
53
63
  readback: readbackRouter(binding),
54
64
  };
55
65
  }
56
66
 
57
67
  // ---------- POST /v1/eval-suites/:suiteId/runs ----------
58
68
 
59
- function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
69
+ /** The `validation-failed` error for a flow candidate's `versions` that don't fit the flow; `undefined` when they do. */
70
+ async function versionsRefusal(
71
+ check: FlowVersionsCheck | undefined,
72
+ tenantId: TenantId,
73
+ start: ParsedStartBody,
74
+ ) {
75
+ const versions = start.comparison?.versions;
76
+ const flowRef = start.flowRef;
77
+ if (check === undefined || versions === undefined || flowRef?.version === undefined) {
78
+ return undefined;
79
+ }
80
+ const issues = await checkFlowVersions(
81
+ check,
82
+ tenantId,
83
+ { flowId: flowRef.flowId as unknown as string, version: flowRef.version },
84
+ versions,
85
+ );
86
+ if (issues.length === 0) return undefined;
87
+ return {
88
+ code: 'validation-failed' as const,
89
+ message: `The versions don't fit flow ${flowRef.flowId as unknown as string} ${flowRef.version} (${issues.length} issue${issues.length === 1 ? '' : 's'})`,
90
+ issues: issues as unknown as Record<string, unknown>[],
91
+ };
92
+ }
93
+
94
+ function startRouter(binding: EvalRunBinding, versionsCheck?: FlowVersionsCheck): Hono<AppEnv> {
60
95
  const r = new Hono<AppEnv>();
61
96
 
62
97
  r.post('/:suiteId/runs', async (c) => {
@@ -84,6 +119,11 @@ function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
84
119
  c.status(statusFor(parsed.error.code) as never);
85
120
  return c.json(toWireError(parsed.error, requestId));
86
121
  }
122
+ const refusal = await versionsRefusal(versionsCheck, tenantId, parsed.value);
123
+ if (refusal !== undefined) {
124
+ c.status(statusFor(refusal.code) as never);
125
+ return c.json(toWireError(refusal, requestId));
126
+ }
87
127
 
88
128
  const outcome = await binding.start({
89
129
  tenantId,
@@ -627,6 +667,9 @@ function parseStartBody(
627
667
  if (comparison.kind === 'err') {
628
668
  return { kind: 'err', error: { code: 'bad-input', message: comparison.message } };
629
669
  }
670
+ if (comparison.value?.versions !== undefined && agentRef !== undefined) {
671
+ return { kind: 'err', error: { code: 'bad-input', message: VERSIONS_NEED_A_FLOW } };
672
+ }
630
673
 
631
674
  return {
632
675
  kind: 'ok',