@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
@@ -11,6 +11,7 @@ import type { Cursor, OrgId, TenantId, UserId } from '@kindgi/types';
11
11
  import { statusFor, toWireError } from '../errors.js';
12
12
  import type { Authorizer } from '../middleware/authorize.js';
13
13
  import type { AppEnv } from '../types.js';
14
+ import { orgDeleteSlugConflictError, slugConflictError } from './hierarchy-errors.js';
14
15
  import { clampLimit } from './pagination.js';
15
16
 
16
17
  /**
@@ -19,10 +20,13 @@ import { clampLimit } from './pagination.js';
19
20
  * Five endpoints on `/v1/orgs`:
20
21
  * - `GET /` — cursor-paginated list; optional
21
22
  * `?limit=` / `?cursor=` / `?nameContains=`.
22
- * - `POST /` — create; body `{ name, slug }`; 201 `{ id }`.
23
+ * - `POST /` — create; body `{ name, slug }`; 201 `{ id }`;
24
+ * 409 `slug-conflict` when the tenant has
25
+ * an org with that slug.
23
26
  * - `GET /:orgId` — 200 `Org`; 404 `org-not-found`.
24
27
  * - `PATCH /:orgId` — partial update via `OrgPatch`; 204 on
25
- * success; 404 `org-not-found`.
28
+ * success; 404 `org-not-found`; 409
29
+ * `slug-conflict`.
26
30
  * - `DELETE /:orgId` — 204 idempotent (unknown is 204 per the
27
31
  * binding's delete-is-a-no-op contract).
28
32
  *
@@ -142,17 +146,24 @@ export function orgsRouter(
142
146
  : ('00000000-0000-0000-0000-000000000000' as UserId);
143
147
  const result = await tenantHierarchy.createOrg({ tenantId, creatorUserId, spec });
144
148
  if (result.kind === 'err') {
145
- const code =
146
- result.error.code === 'slug-conflict' ? 'slug-conflict' : 'internal-server-error';
147
- c.status(statusFor(code) as never);
148
- return c.json(toWireError({ code, message: result.error.message }, requestId));
149
+ const error =
150
+ result.error.code === 'slug-conflict'
151
+ ? slugConflictError('org', spec.slug)
152
+ : { code: 'internal-server-error', message: result.error.message };
153
+ c.status(statusFor(error.code) as never);
154
+ return c.json(toWireError(error, requestId));
149
155
  }
150
156
  c.status(201);
151
157
  return c.json({ id: result.value.orgId as unknown as string });
152
158
  }
153
- const id = await binding.create(tenantId, spec);
159
+ const outcome = await binding.create(tenantId, spec);
160
+ if (outcome.kind === 'slug-conflict') {
161
+ const error = slugConflictError('org', outcome.slug);
162
+ c.status(statusFor(error.code) as never);
163
+ return c.json(toWireError(error, requestId));
164
+ }
154
165
  c.status(201);
155
- return c.json({ id: id as unknown as string });
166
+ return c.json({ id: outcome.orgId as unknown as string });
156
167
  });
157
168
 
158
169
  // ---------- GET /:orgId ----------
@@ -226,24 +237,24 @@ export function orgsRouter(
226
237
  shaped.slug = b.slug;
227
238
  }
228
239
 
229
- try {
230
- await binding.update(tenantId, orgId, shaped);
231
- } catch (cause) {
232
- const message = cause instanceof Error ? cause.message : String(cause);
233
- if (message.startsWith('org-not-found')) {
234
- c.status(statusFor('org-not-found') as never);
235
- return c.json(
236
- toWireError(
237
- {
238
- code: 'org-not-found',
239
- message: `No org with id "${orgId as unknown as string}"`,
240
- orgId: orgId as unknown as string,
241
- },
242
- requestId,
243
- ),
244
- );
245
- }
246
- throw cause;
240
+ const outcome = await binding.update(tenantId, orgId, shaped);
241
+ if (outcome.kind === 'org-not-found') {
242
+ c.status(statusFor('org-not-found') as never);
243
+ return c.json(
244
+ toWireError(
245
+ {
246
+ code: 'org-not-found',
247
+ message: `No org with id "${orgId as unknown as string}"`,
248
+ orgId: orgId as unknown as string,
249
+ },
250
+ requestId,
251
+ ),
252
+ );
253
+ }
254
+ if (outcome.kind === 'slug-conflict') {
255
+ const error = slugConflictError('org', outcome.slug);
256
+ c.status(statusFor(error.code) as never);
257
+ return c.json(toWireError(error, requestId));
247
258
  }
248
259
  c.status(204);
249
260
  return c.body(null);
@@ -251,9 +262,15 @@ export function orgsRouter(
251
262
 
252
263
  // ---------- DELETE /:orgId ----------
253
264
  r.delete('/:orgId', async (c) => {
265
+ const requestId = c.get('requestId');
254
266
  const tenantId = c.get('tenantId') as TenantId;
255
267
  const orgId = c.req.param('orgId') as OrgId;
256
- await binding.delete(tenantId, orgId);
268
+ const outcome = await binding.delete(tenantId, orgId);
269
+ if (outcome?.kind === 'slug-conflict') {
270
+ const error = orgDeleteSlugConflictError(outcome.slugs);
271
+ c.status(statusFor(error.code) as never);
272
+ return c.json(toWireError(error, requestId));
273
+ }
257
274
  c.status(204);
258
275
  return c.body(null);
259
276
  });
@@ -6,10 +6,12 @@ import { Hono } from 'hono';
6
6
  import type { Cursor, TenantId } from '@kindgi/types';
7
7
 
8
8
  import {
9
+ APPLIED_POLICY_KINDS,
9
10
  POLICY_KINDS,
10
11
  type Policy,
11
12
  type PolicyKind,
12
13
  type PolicyRegistryBinding,
14
+ isAppliedPolicyKind,
13
15
  validatePolicySpec,
14
16
  } from '@kindgi/policy-contract';
15
17
  import { statusFor, toWireError } from '../errors.js';
@@ -221,6 +223,23 @@ export function policiesRouter(binding: PolicyRegistryBinding): Hono<AppEnv> {
221
223
  );
222
224
  }
223
225
 
226
+ // A known kind no runtime consumer applies yet: publishing it would
227
+ // change nothing, silently.
228
+ if (!isAppliedPolicyKind(validation.value.kind)) {
229
+ c.status(statusFor('kind-not-applied') as never);
230
+ return c.json(
231
+ toWireError(
232
+ {
233
+ code: 'kind-not-applied',
234
+ message: `This runtime doesn't apply "${validation.value.kind}" policies yet, so publishing one would change nothing. Policy kinds it applies: ${APPLIED_POLICY_KINDS.join(', ')}.`,
235
+ kind: validation.value.kind,
236
+ appliedKinds: [...APPLIED_POLICY_KINDS],
237
+ },
238
+ requestId,
239
+ ),
240
+ );
241
+ }
242
+
224
243
  const outcome = await binding.publish({ tenantId, policy: validation.value });
225
244
  if (outcome.kind === 'already-registered') {
226
245
  c.status(statusFor('policy-already-registered') as never);
@@ -19,6 +19,11 @@ import type { Cursor, OrgId, ProjectId, TenantId, UserId } from '@kindgi/types';
19
19
  import { statusFor, toWireError } from '../errors.js';
20
20
  import type { Authorizer } from '../middleware/authorize.js';
21
21
  import type { AppEnv } from '../types.js';
22
+ import {
23
+ orgNotFoundError,
24
+ projectDefaultAlreadyExistsError,
25
+ slugConflictError,
26
+ } from './hierarchy-errors.js';
22
27
  import { clampLimit } from './pagination.js';
23
28
 
24
29
  /**
@@ -30,12 +35,20 @@ import { clampLimit } from './pagination.js';
30
35
  * `?nameContains=`.
31
36
  * - `POST /` — create; body `{ name, slug,
32
37
  * orgId?, description?, isDefault? }`;
33
- * 201 `{ id }`.
38
+ * 201 `{ id }`; 409 `slug-conflict`
39
+ * when its org (or, without an org,
40
+ * the tenant's projects without one)
41
+ * has a project with that slug, or
42
+ * `project-default-already-exists`
43
+ * for a second Default.
34
44
  * - `GET /default` — returns the tenant's Default
35
45
  * project or 404 `project-not-found`.
36
46
  * MUST be mounted BEFORE `/:projectId`.
37
47
  * - `GET /:projectId` — get; 200 or 404 `project-not-found`.
38
- * - `PATCH /:projectId` — partial update; 204 or 404.
48
+ * - `PATCH /:projectId` — partial update; 204, 404
49
+ * `project-not-found` or 409
50
+ * `slug-conflict` (a new slug, or a
51
+ * move to an org that has it).
39
52
  * - `DELETE /:projectId` — 204 idempotent.
40
53
  * - `GET /:projectId/memberships` — list; cursor-paginated.
41
54
  * - `POST /:projectId/memberships` — add member; body `{ userId, role }`;
@@ -235,23 +248,35 @@ export function projectsRouter(
235
248
  : ('00000000-0000-0000-0000-000000000000' as UserId);
236
249
  const result = await tenantHierarchy.createProject({ tenantId, creatorUserId, spec });
237
250
  if (result.kind === 'err') {
238
- const code =
251
+ const error =
239
252
  result.error.code === 'slug-conflict'
240
- ? 'slug-conflict'
241
- : result.error.code === 'default-conflict'
242
- ? 'project-default-already-exists'
243
- : 'internal-server-error';
244
- c.status(statusFor(code) as never);
245
- return c.json(toWireError({ code, message: result.error.message }, requestId));
253
+ ? slugConflictError('project', spec.slug)
254
+ : result.error.code === 'org-not-found'
255
+ ? orgNotFoundError(result.error.orgId as unknown as string)
256
+ : result.error.code === 'default-conflict'
257
+ ? projectDefaultAlreadyExistsError()
258
+ : { code: 'internal-server-error', message: result.error.message };
259
+ c.status(statusFor(error.code) as never);
260
+ return c.json(toWireError(error, requestId));
246
261
  }
247
262
  c.status(201);
248
263
  return c.json({ id: result.value.projectId as unknown as string });
249
264
  }
250
265
  // No authorizer configured — fall back to the raw binding create;
251
266
  // no FGA tuples written.
252
- const id = await binding.create(tenantId, spec);
267
+ const outcome = await binding.create(tenantId, spec);
268
+ if (outcome.kind !== 'ok') {
269
+ const error =
270
+ outcome.kind === 'slug-conflict'
271
+ ? slugConflictError('project', outcome.slug)
272
+ : outcome.kind === 'org-not-found'
273
+ ? orgNotFoundError(outcome.orgId as unknown as string)
274
+ : projectDefaultAlreadyExistsError();
275
+ c.status(statusFor(error.code) as never);
276
+ return c.json(toWireError(error, requestId));
277
+ }
253
278
  c.status(201);
254
- return c.json({ id: id as unknown as string });
279
+ return c.json({ id: outcome.projectId as unknown as string });
255
280
  });
256
281
 
257
282
  // ---------- GET /:projectId/memberships ----------
@@ -346,42 +371,41 @@ export function projectsRouter(
346
371
  ),
347
372
  );
348
373
  }
349
- try {
350
- // With an authorizer, add through the tenant-hierarchy binding — it
351
- // writes the FGA `<role>@project` tuple atomically so the granted
352
- // user's permissions land in FGA.
353
- if (authorizer !== undefined) {
354
- const res = await tenantHierarchy.addProjectMember({
355
- tenantId,
356
- projectId,
357
- userId: b.userId as unknown as UserId,
358
- role: b.role as ProjectRole,
359
- });
360
- if (res.kind === 'err') throw new Error(res.error.message);
361
- } else {
362
- await membershipBinding.add(tenantId, {
363
- projectId,
364
- userId: b.userId as unknown as UserId,
365
- role: b.role,
366
- });
367
- }
368
- } catch (cause) {
369
- const message = cause instanceof Error ? cause.message : String(cause);
370
- if (message.startsWith('project-not-found')) {
371
- // Racy — project was deleted between preliminary get and add.
372
- c.status(statusFor('project-not-found') as never);
373
- return c.json(
374
- toWireError(
375
- {
376
- code: 'project-not-found',
377
- message: `No project with id "${projectId as unknown as string}"`,
378
- projectId: projectId as unknown as string,
379
- },
380
- requestId,
381
- ),
382
- );
374
+ // Racy — the project can be deleted between the preliminary get and the add.
375
+ const projectGone = () => {
376
+ c.status(statusFor('project-not-found') as never);
377
+ return c.json(
378
+ toWireError(
379
+ {
380
+ code: 'project-not-found',
381
+ message: `No project with id "${projectId as unknown as string}"`,
382
+ projectId: projectId as unknown as string,
383
+ },
384
+ requestId,
385
+ ),
386
+ );
387
+ };
388
+ // With an authorizer, add through the tenant-hierarchy binding — it
389
+ // writes the FGA `<role>@project` tuple atomically so the granted
390
+ // user's permissions land in FGA.
391
+ if (authorizer !== undefined) {
392
+ const res = await tenantHierarchy.addProjectMember({
393
+ tenantId,
394
+ projectId,
395
+ userId: b.userId as unknown as UserId,
396
+ role: b.role as ProjectRole,
397
+ });
398
+ if (res.kind === 'err') {
399
+ if (res.error.code === 'project-not-found') return projectGone();
400
+ throw new Error(res.error.message, { cause: res.error });
383
401
  }
384
- throw cause;
402
+ } else {
403
+ const outcome = await membershipBinding.add(tenantId, {
404
+ projectId,
405
+ userId: b.userId as unknown as UserId,
406
+ role: b.role,
407
+ });
408
+ if (outcome.kind === 'project-not-found') return projectGone();
385
409
  }
386
410
  c.status(201);
387
411
  return c.json({
@@ -436,38 +460,33 @@ export function projectsRouter(
436
460
  ),
437
461
  );
438
462
  }
439
- try {
440
- await membershipBinding.updateRole(tenantId, projectId, userId, b.role);
441
- } catch (cause) {
442
- const message = cause instanceof Error ? cause.message : String(cause);
443
- if (message.startsWith('project-membership-not-found')) {
444
- c.status(statusFor('project-membership-not-found') as never);
445
- return c.json(
446
- toWireError(
447
- {
448
- code: 'project-membership-not-found',
449
- message: `No membership for project "${projectId as unknown as string}" + user "${userId as unknown as string}"`,
450
- projectId: projectId as unknown as string,
451
- userId: userId as unknown as string,
452
- },
453
- requestId,
454
- ),
455
- );
456
- }
457
- if (message.startsWith('project-not-found')) {
458
- c.status(statusFor('project-not-found') as never);
459
- return c.json(
460
- toWireError(
461
- {
462
- code: 'project-not-found',
463
- message: `No project with id "${projectId as unknown as string}"`,
464
- projectId: projectId as unknown as string,
465
- },
466
- requestId,
467
- ),
468
- );
469
- }
470
- throw cause;
463
+ const outcome = await membershipBinding.updateRole(tenantId, projectId, userId, b.role);
464
+ if (outcome.kind === 'project-membership-not-found') {
465
+ c.status(statusFor('project-membership-not-found') as never);
466
+ return c.json(
467
+ toWireError(
468
+ {
469
+ code: 'project-membership-not-found',
470
+ message: `No membership for project "${projectId as unknown as string}" + user "${userId as unknown as string}"`,
471
+ projectId: projectId as unknown as string,
472
+ userId: userId as unknown as string,
473
+ },
474
+ requestId,
475
+ ),
476
+ );
477
+ }
478
+ if (outcome.kind === 'project-not-found') {
479
+ c.status(statusFor('project-not-found') as never);
480
+ return c.json(
481
+ toWireError(
482
+ {
483
+ code: 'project-not-found',
484
+ message: `No project with id "${projectId as unknown as string}"`,
485
+ projectId: projectId as unknown as string,
486
+ },
487
+ requestId,
488
+ ),
489
+ );
471
490
  }
472
491
  c.status(204);
473
492
  return c.body(null);
@@ -571,24 +590,27 @@ export function projectsRouter(
571
590
  );
572
591
  }
573
592
  }
574
- try {
575
- await binding.update(tenantId, projectId, shaped);
576
- } catch (cause) {
577
- const message = cause instanceof Error ? cause.message : String(cause);
578
- if (message.startsWith('project-not-found')) {
579
- c.status(statusFor('project-not-found') as never);
580
- return c.json(
581
- toWireError(
582
- {
583
- code: 'project-not-found',
584
- message: `No project with id "${projectId as unknown as string}"`,
585
- projectId: projectId as unknown as string,
586
- },
587
- requestId,
588
- ),
589
- );
590
- }
591
- throw cause;
593
+ const outcome = await binding.update(tenantId, projectId, shaped);
594
+ if (outcome.kind === 'project-not-found') {
595
+ c.status(statusFor('project-not-found') as never);
596
+ return c.json(
597
+ toWireError(
598
+ {
599
+ code: 'project-not-found',
600
+ message: `No project with id "${projectId as unknown as string}"`,
601
+ projectId: projectId as unknown as string,
602
+ },
603
+ requestId,
604
+ ),
605
+ );
606
+ }
607
+ if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
608
+ const error =
609
+ outcome.kind === 'slug-conflict'
610
+ ? slugConflictError('project', outcome.slug)
611
+ : orgNotFoundError(outcome.orgId as unknown as string);
612
+ c.status(statusFor(error.code) as never);
613
+ return c.json(toWireError(error, requestId));
592
614
  }
593
615
  c.status(204);
594
616
  return c.body(null);
@@ -9,6 +9,7 @@ import {
9
9
  type Feature,
10
10
  type ModelInfo,
11
11
  type ProviderMetadata,
12
+ validateProviderLabels,
12
13
  } from '@kindgi/capabilities';
13
14
  import type { Cursor, TenantId } from '@kindgi/types';
14
15
 
@@ -316,6 +317,7 @@ function serializeProvider(m: ProviderMetadata): Record<string, unknown> {
316
317
  ...(m.description !== undefined && { description: m.description }),
317
318
  ...(m.capabilityKind !== undefined && { capabilityKind: m.capabilityKind }),
318
319
  ...(m.fallback !== undefined && { fallback: m.fallback }),
320
+ ...(m.labels !== undefined && { labels: m.labels }),
319
321
  };
320
322
  }
321
323
 
@@ -432,6 +434,8 @@ function validateProviderMetadata(
432
434
  },
433
435
  };
434
436
  }
437
+ const badLabels = validateProviderLabels(b.id, b.labels);
438
+ if (badLabels !== undefined) return { kind: 'err', error: badLabels };
435
439
  const value: ProviderMetadata = {
436
440
  id: b.id,
437
441
  region: b.region,
@@ -440,6 +444,7 @@ function validateProviderMetadata(
440
444
  ...(b.description !== undefined && { description: b.description }),
441
445
  ...(b.capabilityKind !== undefined && { capabilityKind: b.capabilityKind as string }),
442
446
  ...(b.fallback !== undefined && { fallback: b.fallback as boolean }),
447
+ ...(b.labels !== undefined && { labels: b.labels as Readonly<Record<string, string>> }),
443
448
  };
444
449
  return { kind: 'ok', value };
445
450
  }
@@ -654,7 +654,8 @@ function invokeFromBody(
654
654
  * Wire row for a run. `output` is the run's output once it completed;
655
655
  * single-run responses carry it, lists only with `?include=output`
656
656
  * (outputs can be large). Child runs carry their parent's run + node;
657
- * an agent's turns, the agent, its version and the conversation.
657
+ * an agent's turns, the agent, its version and the conversation; a
658
+ * replay run, the run it replays and its eval run.
658
659
  */
659
660
  function serializeRun(
660
661
  row: KernelRunRecord,
@@ -682,6 +683,9 @@ function serializeRun(
682
683
  conversationId: row.agent.conversationId as unknown as string,
683
684
  },
684
685
  }),
686
+ ...(row.replayOf != null && { replayOf: row.replayOf as unknown as string }),
687
+ ...(row.evalRunId != null && { evalRunId: row.evalRunId }),
688
+ ...(row.versions != null && { versions: row.versions }),
685
689
  };
686
690
  }
687
691
 
@@ -742,6 +746,8 @@ function listRunsInput(input: {
742
746
  ...(filter.parentRunId !== undefined && { parent: { runId: filter.parentRunId } }),
743
747
  ...(filter.topLevelOnly && { topLevelOnly: true }),
744
748
  ...(filter.agentId !== undefined && { agentId: filter.agentId }),
749
+ replays: filter.replays,
750
+ ...(filter.evalRunId !== undefined && { evalRunId: filter.evalRunId }),
745
751
  };
746
752
  }
747
753
 
@@ -749,17 +755,21 @@ interface RunListFilter {
749
755
  readonly parentRunId?: RunId;
750
756
  readonly topLevelOnly: boolean;
751
757
  readonly agentId?: string;
758
+ readonly replays: 'exclude' | 'include' | 'only';
759
+ readonly evalRunId?: string;
752
760
  readonly includeOutput: boolean;
753
761
  }
754
762
 
755
763
  /**
756
764
  * `?parentRunId=` (children of a run), `?topLevel=true`, `?agentId=` (an
757
- * agent's turns), `?include=output`.
765
+ * agent's turns), `?replays=exclude|include|only` (default `exclude`),
766
+ * `?evalRunId=` (one eval run's replays; implies they are included),
767
+ * `?include=output`.
758
768
  */
759
769
  function parseRunListFilter(
760
770
  query: Readonly<Record<string, string>>,
761
771
  ): { kind: 'ok'; value: RunListFilter } | { kind: 'err'; message: string } {
762
- const { parentRunId, topLevel, agentId, include } = query;
772
+ const { parentRunId, topLevel, agentId, replays, evalRunId, include } = query;
763
773
  if (agentId !== undefined && agentId.trim() === '') {
764
774
  return { kind: 'err', message: '`agentId` must not be empty' };
765
775
  }
@@ -773,6 +783,20 @@ function parseRunListFilter(
773
783
  if (parentRunId !== undefined && topLevelOnly) {
774
784
  return { kind: 'err', message: '`parentRunId` and `topLevel=true` cannot be combined' };
775
785
  }
786
+ if (
787
+ replays !== undefined &&
788
+ replays !== 'exclude' &&
789
+ replays !== 'include' &&
790
+ replays !== 'only'
791
+ ) {
792
+ return { kind: 'err', message: '`replays` must be `exclude`, `include` or `only`' };
793
+ }
794
+ if (evalRunId !== undefined && evalRunId.trim() === '') {
795
+ return { kind: 'err', message: '`evalRunId` must not be empty' };
796
+ }
797
+ if (evalRunId !== undefined && replays === 'exclude') {
798
+ return { kind: 'err', message: '`evalRunId` and `replays=exclude` cannot be combined' };
799
+ }
776
800
  const includes = include === undefined ? [] : include.split(',').map((i) => i.trim());
777
801
  const unknown = includes.filter((i) => i !== 'output');
778
802
  if (unknown.length > 0) {
@@ -784,6 +808,8 @@ function parseRunListFilter(
784
808
  ...(parentRunId !== undefined && { parentRunId: parentRunId as RunId }),
785
809
  topLevelOnly,
786
810
  ...(agentId !== undefined && { agentId }),
811
+ replays: replays ?? (evalRunId !== undefined ? 'include' : 'exclude'),
812
+ ...(evalRunId !== undefined && { evalRunId }),
787
813
  includeOutput: includes.includes('output'),
788
814
  },
789
815
  };