@kindgi/api 0.1.2 → 0.1.4-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/agent-binding.d.ts +18 -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 +22 -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 +100 -5
  20. package/dist/cost-binding.d.ts.map +1 -1
  21. package/dist/cost-binding.js +10 -0
  22. package/dist/cost-binding.js.map +1 -1
  23. package/dist/deploy-versions.d.ts +58 -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 +69 -0
  30. package/dist/derive-agent-version.d.ts.map +1 -0
  31. package/dist/derive-agent-version.js +139 -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 +17 -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 +30 -0
  41. package/dist/eval-run-binding.d.ts.map +1 -1
  42. package/dist/eval-run-dispatcher.d.ts +38 -3
  43. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  44. package/dist/eval-run-dispatcher.js +21 -15
  45. package/dist/eval-run-dispatcher.js.map +1 -1
  46. package/dist/eval-suite-binding.d.ts +1 -1
  47. package/dist/eval-suite-binding.d.ts.map +1 -1
  48. package/dist/eval-suite-binding.js +2 -0
  49. package/dist/eval-suite-binding.js.map +1 -1
  50. package/dist/flow-binding.d.ts +10 -4
  51. package/dist/flow-binding.d.ts.map +1 -1
  52. package/dist/flow-pins.d.ts +36 -0
  53. package/dist/flow-pins.d.ts.map +1 -0
  54. package/dist/flow-pins.js +81 -0
  55. package/dist/flow-pins.js.map +1 -0
  56. package/dist/hitl-binding.d.ts +20 -5
  57. package/dist/hitl-binding.d.ts.map +1 -1
  58. package/dist/index.d.ts +19 -8
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +8 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/judged-dispatcher.d.ts +134 -0
  63. package/dist/judged-dispatcher.d.ts.map +1 -0
  64. package/dist/judged-dispatcher.js +297 -0
  65. package/dist/judged-dispatcher.js.map +1 -0
  66. package/dist/judged-items.d.ts +86 -0
  67. package/dist/judged-items.d.ts.map +1 -0
  68. package/dist/judged-items.js +184 -0
  69. package/dist/judged-items.js.map +1 -0
  70. package/dist/judgment-binding.d.ts +316 -0
  71. package/dist/judgment-binding.d.ts.map +1 -0
  72. package/dist/judgment-binding.js +19 -0
  73. package/dist/judgment-binding.js.map +1 -0
  74. package/dist/middleware/auth.d.ts +3 -2
  75. package/dist/middleware/auth.d.ts.map +1 -1
  76. package/dist/middleware/auth.js.map +1 -1
  77. package/dist/middleware/idempotency.d.ts +5 -1
  78. package/dist/middleware/idempotency.d.ts.map +1 -1
  79. package/dist/middleware/idempotency.js +8 -1
  80. package/dist/middleware/idempotency.js.map +1 -1
  81. package/dist/openapi/generate.d.ts.map +1 -1
  82. package/dist/openapi/generate.js +4 -1
  83. package/dist/openapi/generate.js.map +1 -1
  84. package/dist/openapi/operations.d.ts.map +1 -1
  85. package/dist/openapi/operations.js +543 -24
  86. package/dist/openapi/operations.js.map +1 -1
  87. package/dist/openapi/schemas.d.ts +58 -0
  88. package/dist/openapi/schemas.d.ts.map +1 -1
  89. package/dist/openapi/schemas.js +1058 -27
  90. package/dist/openapi/schemas.js.map +1 -1
  91. package/dist/provenance-binding.d.ts +27 -1
  92. package/dist/provenance-binding.d.ts.map +1 -1
  93. package/dist/provenance-binding.js.map +1 -1
  94. package/dist/provider-binding.d.ts +12 -7
  95. package/dist/provider-binding.d.ts.map +1 -1
  96. package/dist/reviewer-binding.d.ts +10 -2
  97. package/dist/reviewer-binding.d.ts.map +1 -1
  98. package/dist/reviewer-role.d.ts +13 -0
  99. package/dist/reviewer-role.d.ts.map +1 -0
  100. package/dist/reviewer-role.js +27 -0
  101. package/dist/reviewer-role.js.map +1 -0
  102. package/dist/routes/agents.d.ts +9 -1
  103. package/dist/routes/agents.d.ts.map +1 -1
  104. package/dist/routes/agents.js +175 -11
  105. package/dist/routes/agents.js.map +1 -1
  106. package/dist/routes/approvals.d.ts +5 -4
  107. package/dist/routes/approvals.d.ts.map +1 -1
  108. package/dist/routes/approvals.js +52 -16
  109. package/dist/routes/approvals.js.map +1 -1
  110. package/dist/routes/blocks.d.ts +19 -0
  111. package/dist/routes/blocks.d.ts.map +1 -0
  112. package/dist/routes/blocks.js +281 -0
  113. package/dist/routes/blocks.js.map +1 -0
  114. package/dist/routes/conversations.d.ts +7 -1
  115. package/dist/routes/conversations.d.ts.map +1 -1
  116. package/dist/routes/conversations.js +41 -1
  117. package/dist/routes/conversations.js.map +1 -1
  118. package/dist/routes/cost.d.ts.map +1 -1
  119. package/dist/routes/cost.js +142 -11
  120. package/dist/routes/cost.js.map +1 -1
  121. package/dist/routes/deployments.d.ts +3 -0
  122. package/dist/routes/deployments.d.ts.map +1 -1
  123. package/dist/routes/deployments.js +208 -55
  124. package/dist/routes/deployments.js.map +1 -1
  125. package/dist/routes/eval-comparison.d.ts +14 -0
  126. package/dist/routes/eval-comparison.d.ts.map +1 -0
  127. package/dist/routes/eval-comparison.js +87 -0
  128. package/dist/routes/eval-comparison.js.map +1 -0
  129. package/dist/routes/eval-runs.d.ts.map +1 -1
  130. package/dist/routes/eval-runs.js +8 -0
  131. package/dist/routes/eval-runs.js.map +1 -1
  132. package/dist/routes/flows.d.ts +13 -1
  133. package/dist/routes/flows.d.ts.map +1 -1
  134. package/dist/routes/flows.js +44 -3
  135. package/dist/routes/flows.js.map +1 -1
  136. package/dist/routes/hierarchy-errors.d.ts +35 -0
  137. package/dist/routes/hierarchy-errors.d.ts.map +1 -0
  138. package/dist/routes/hierarchy-errors.js +39 -0
  139. package/dist/routes/hierarchy-errors.js.map +1 -0
  140. package/dist/routes/identity.d.ts +7 -0
  141. package/dist/routes/identity.d.ts.map +1 -1
  142. package/dist/routes/identity.js +3 -2
  143. package/dist/routes/identity.js.map +1 -1
  144. package/dist/routes/judged-suites.d.ts +20 -0
  145. package/dist/routes/judged-suites.d.ts.map +1 -0
  146. package/dist/routes/judged-suites.js +272 -0
  147. package/dist/routes/judged-suites.js.map +1 -0
  148. package/dist/routes/judgment-context.d.ts +22 -0
  149. package/dist/routes/judgment-context.d.ts.map +1 -0
  150. package/dist/routes/judgment-context.js +88 -0
  151. package/dist/routes/judgment-context.js.map +1 -0
  152. package/dist/routes/judgment-flow-context.d.ts +32 -0
  153. package/dist/routes/judgment-flow-context.d.ts.map +1 -0
  154. package/dist/routes/judgment-flow-context.js +195 -0
  155. package/dist/routes/judgment-flow-context.js.map +1 -0
  156. package/dist/routes/judgments.d.ts +41 -0
  157. package/dist/routes/judgments.d.ts.map +1 -0
  158. package/dist/routes/judgments.js +566 -0
  159. package/dist/routes/judgments.js.map +1 -0
  160. package/dist/routes/orgs.d.ts +5 -2
  161. package/dist/routes/orgs.d.ts.map +1 -1
  162. package/dist/routes/orgs.js +38 -22
  163. package/dist/routes/orgs.js.map +1 -1
  164. package/dist/routes/policies.d.ts.map +1 -1
  165. package/dist/routes/policies.js +12 -1
  166. package/dist/routes/policies.js.map +1 -1
  167. package/dist/routes/projects.d.ts +10 -2
  168. package/dist/routes/projects.d.ts.map +1 -1
  169. package/dist/routes/projects.js +87 -79
  170. package/dist/routes/projects.js.map +1 -1
  171. package/dist/routes/provenance.d.ts.map +1 -1
  172. package/dist/routes/provenance.js +32 -1
  173. package/dist/routes/provenance.js.map +1 -1
  174. package/dist/routes/providers.d.ts.map +1 -1
  175. package/dist/routes/providers.js +6 -1
  176. package/dist/routes/providers.js.map +1 -1
  177. package/dist/routes/runs.d.ts +0 -7
  178. package/dist/routes/runs.d.ts.map +1 -1
  179. package/dist/routes/runs.js +65 -20
  180. package/dist/routes/runs.js.map +1 -1
  181. package/dist/routes/scope-params.d.ts +16 -1
  182. package/dist/routes/scope-params.d.ts.map +1 -1
  183. package/dist/routes/scope-params.js +28 -0
  184. package/dist/routes/scope-params.js.map +1 -1
  185. package/dist/routes/teams.d.ts +6 -2
  186. package/dist/routes/teams.d.ts.map +1 -1
  187. package/dist/routes/teams.js +77 -73
  188. package/dist/routes/teams.js.map +1 -1
  189. package/dist/types.d.ts +4 -3
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/webhook-endpoint-binding.d.ts +11 -0
  192. package/dist/webhook-endpoint-binding.d.ts.map +1 -1
  193. package/dist/webhook-endpoint-binding.js.map +1 -1
  194. package/openapi.json +13316 -9299
  195. package/package.json +21 -21
  196. package/src/agent-binding.ts +19 -4
  197. package/src/agent-pins.ts +147 -0
  198. package/src/app.ts +76 -3
  199. package/src/block-binding.ts +137 -0
  200. package/src/block-pins.ts +148 -0
  201. package/src/cost-binding.ts +116 -5
  202. package/src/deploy-versions.ts +157 -0
  203. package/src/deployment-binding.ts +27 -3
  204. package/src/derive-agent-version.ts +206 -0
  205. package/src/errors.ts +17 -0
  206. package/src/eval-case-binding.ts +71 -0
  207. package/src/eval-run-binding.ts +33 -0
  208. package/src/eval-run-dispatcher.ts +57 -16
  209. package/src/eval-suite-binding.ts +2 -0
  210. package/src/flow-binding.ts +11 -4
  211. package/src/flow-pins.ts +113 -0
  212. package/src/hitl-binding.ts +20 -4
  213. package/src/index.ts +88 -2
  214. package/src/judged-dispatcher.ts +507 -0
  215. package/src/judged-items.ts +263 -0
  216. package/src/judgment-binding.ts +349 -0
  217. package/src/middleware/auth.ts +3 -2
  218. package/src/middleware/idempotency.ts +7 -1
  219. package/src/openapi/generate.ts +7 -1
  220. package/src/openapi/operations.ts +615 -24
  221. package/src/openapi/schemas.ts +1157 -22
  222. package/src/provenance-binding.ts +42 -1
  223. package/src/provider-binding.ts +12 -7
  224. package/src/reviewer-binding.ts +10 -2
  225. package/src/reviewer-role.ts +35 -0
  226. package/src/routes/agents.ts +243 -19
  227. package/src/routes/approvals.ts +70 -19
  228. package/src/routes/blocks.ts +362 -0
  229. package/src/routes/conversations.ts +57 -1
  230. package/src/routes/cost.ts +159 -21
  231. package/src/routes/deployments.ts +266 -56
  232. package/src/routes/eval-comparison.ts +101 -0
  233. package/src/routes/eval-runs.ts +11 -0
  234. package/src/routes/flows.ts +63 -5
  235. package/src/routes/hierarchy-errors.ts +51 -0
  236. package/src/routes/identity.ts +10 -2
  237. package/src/routes/judged-suites.ts +363 -0
  238. package/src/routes/judgment-context.ts +128 -0
  239. package/src/routes/judgment-flow-context.ts +245 -0
  240. package/src/routes/judgments.ts +743 -0
  241. package/src/routes/orgs.ts +44 -27
  242. package/src/routes/policies.ts +19 -0
  243. package/src/routes/projects.ts +106 -95
  244. package/src/routes/provenance.ts +48 -1
  245. package/src/routes/providers.ts +5 -0
  246. package/src/routes/runs.ts +79 -22
  247. package/src/routes/scope-params.ts +35 -1
  248. package/src/routes/teams.ts +96 -90
  249. package/src/types.ts +4 -3
  250. package/src/webhook-endpoint-binding.ts +11 -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,7 @@ 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 { projectDefaultAlreadyExistsError, slugConflictError } from './hierarchy-errors.js';
22
23
  import { clampLimit } from './pagination.js';
23
24
 
24
25
  /**
@@ -30,12 +31,20 @@ import { clampLimit } from './pagination.js';
30
31
  * `?nameContains=`.
31
32
  * - `POST /` — create; body `{ name, slug,
32
33
  * orgId?, description?, isDefault? }`;
33
- * 201 `{ id }`.
34
+ * 201 `{ id }`; 409 `slug-conflict`
35
+ * when its org (or, without an org,
36
+ * the tenant's projects without one)
37
+ * has a project with that slug, or
38
+ * `project-default-already-exists`
39
+ * for a second Default.
34
40
  * - `GET /default` — returns the tenant's Default
35
41
  * project or 404 `project-not-found`.
36
42
  * MUST be mounted BEFORE `/:projectId`.
37
43
  * - `GET /:projectId` — get; 200 or 404 `project-not-found`.
38
- * - `PATCH /:projectId` — partial update; 204 or 404.
44
+ * - `PATCH /:projectId` — partial update; 204, 404
45
+ * `project-not-found` or 409
46
+ * `slug-conflict` (a new slug, or a
47
+ * move to an org that has it).
39
48
  * - `DELETE /:projectId` — 204 idempotent.
40
49
  * - `GET /:projectId/memberships` — list; cursor-paginated.
41
50
  * - `POST /:projectId/memberships` — add member; body `{ userId, role }`;
@@ -235,23 +244,31 @@ export function projectsRouter(
235
244
  : ('00000000-0000-0000-0000-000000000000' as UserId);
236
245
  const result = await tenantHierarchy.createProject({ tenantId, creatorUserId, spec });
237
246
  if (result.kind === 'err') {
238
- const code =
247
+ const error =
239
248
  result.error.code === 'slug-conflict'
240
- ? 'slug-conflict'
249
+ ? slugConflictError('project', spec.slug)
241
250
  : 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));
251
+ ? projectDefaultAlreadyExistsError()
252
+ : { code: 'internal-server-error', message: result.error.message };
253
+ c.status(statusFor(error.code) as never);
254
+ return c.json(toWireError(error, requestId));
246
255
  }
247
256
  c.status(201);
248
257
  return c.json({ id: result.value.projectId as unknown as string });
249
258
  }
250
259
  // No authorizer configured — fall back to the raw binding create;
251
260
  // no FGA tuples written.
252
- const id = await binding.create(tenantId, spec);
261
+ const outcome = await binding.create(tenantId, spec);
262
+ if (outcome.kind !== 'ok') {
263
+ const error =
264
+ outcome.kind === 'slug-conflict'
265
+ ? slugConflictError('project', outcome.slug)
266
+ : projectDefaultAlreadyExistsError();
267
+ c.status(statusFor(error.code) as never);
268
+ return c.json(toWireError(error, requestId));
269
+ }
253
270
  c.status(201);
254
- return c.json({ id: id as unknown as string });
271
+ return c.json({ id: outcome.projectId as unknown as string });
255
272
  });
256
273
 
257
274
  // ---------- GET /:projectId/memberships ----------
@@ -346,42 +363,41 @@ export function projectsRouter(
346
363
  ),
347
364
  );
348
365
  }
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
- );
366
+ // Racy — the project can be deleted between the preliminary get and the add.
367
+ const projectGone = () => {
368
+ c.status(statusFor('project-not-found') as never);
369
+ return c.json(
370
+ toWireError(
371
+ {
372
+ code: 'project-not-found',
373
+ message: `No project with id "${projectId as unknown as string}"`,
374
+ projectId: projectId as unknown as string,
375
+ },
376
+ requestId,
377
+ ),
378
+ );
379
+ };
380
+ // With an authorizer, add through the tenant-hierarchy binding — it
381
+ // writes the FGA `<role>@project` tuple atomically so the granted
382
+ // user's permissions land in FGA.
383
+ if (authorizer !== undefined) {
384
+ const res = await tenantHierarchy.addProjectMember({
385
+ tenantId,
386
+ projectId,
387
+ userId: b.userId as unknown as UserId,
388
+ role: b.role as ProjectRole,
389
+ });
390
+ if (res.kind === 'err') {
391
+ if (res.error.code === 'project-not-found') return projectGone();
392
+ throw new Error(res.error.message, { cause: res.error });
383
393
  }
384
- throw cause;
394
+ } else {
395
+ const outcome = await membershipBinding.add(tenantId, {
396
+ projectId,
397
+ userId: b.userId as unknown as UserId,
398
+ role: b.role,
399
+ });
400
+ if (outcome.kind === 'project-not-found') return projectGone();
385
401
  }
386
402
  c.status(201);
387
403
  return c.json({
@@ -436,38 +452,33 @@ export function projectsRouter(
436
452
  ),
437
453
  );
438
454
  }
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;
455
+ const outcome = await membershipBinding.updateRole(tenantId, projectId, userId, b.role);
456
+ if (outcome.kind === 'project-membership-not-found') {
457
+ c.status(statusFor('project-membership-not-found') as never);
458
+ return c.json(
459
+ toWireError(
460
+ {
461
+ code: 'project-membership-not-found',
462
+ message: `No membership for project "${projectId as unknown as string}" + user "${userId as unknown as string}"`,
463
+ projectId: projectId as unknown as string,
464
+ userId: userId as unknown as string,
465
+ },
466
+ requestId,
467
+ ),
468
+ );
469
+ }
470
+ if (outcome.kind === 'project-not-found') {
471
+ c.status(statusFor('project-not-found') as never);
472
+ return c.json(
473
+ toWireError(
474
+ {
475
+ code: 'project-not-found',
476
+ message: `No project with id "${projectId as unknown as string}"`,
477
+ projectId: projectId as unknown as string,
478
+ },
479
+ requestId,
480
+ ),
481
+ );
471
482
  }
472
483
  c.status(204);
473
484
  return c.body(null);
@@ -571,24 +582,24 @@ export function projectsRouter(
571
582
  );
572
583
  }
573
584
  }
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;
585
+ const outcome = await binding.update(tenantId, projectId, shaped);
586
+ if (outcome.kind === 'project-not-found') {
587
+ c.status(statusFor('project-not-found') as never);
588
+ return c.json(
589
+ toWireError(
590
+ {
591
+ code: 'project-not-found',
592
+ message: `No project with id "${projectId as unknown as string}"`,
593
+ projectId: projectId as unknown as string,
594
+ },
595
+ requestId,
596
+ ),
597
+ );
598
+ }
599
+ if (outcome.kind === 'slug-conflict') {
600
+ const error = slugConflictError('project', outcome.slug);
601
+ c.status(statusFor(error.code) as never);
602
+ return c.json(toWireError(error, requestId));
592
603
  }
593
604
  c.status(204);
594
605
  return c.body(null);
@@ -12,12 +12,15 @@ import type { ConversationId, RunId, SigningKeyId, TenantId, Timestamp } from '@
12
12
  import { statusFor, toWireError } from '../errors.js';
13
13
 
14
14
  import type {
15
+ CallUsageByCallId,
15
16
  ProvenanceBinding,
17
+ ProvenanceBindingError,
16
18
  ProvenanceListCursor,
17
19
  ProvenanceRecordSummary,
18
20
  } from '../provenance-binding.js';
19
21
  import type { AppEnv } from '../types.js';
20
22
  import { clampLimit, decodeCursor, encodeCursor } from './pagination.js';
23
+ import { parseListScope } from './scope-params.js';
21
24
 
22
25
  /**
23
26
  * Provenance resource routes.
@@ -48,7 +51,8 @@ export interface ProvenanceRouterOptions {
48
51
  }
49
52
 
50
53
  /** Bundle schema version — bump when the wire shape of `bundle.body` changes. */
51
- const BUNDLE_SCHEMA_VERSION = '1.0.0';
54
+ /** 1.1.0 adds `callUsage`: the model calls' usage from the cost ledger, when the run has any. */
55
+ const BUNDLE_SCHEMA_VERSION = '1.1.0';
52
56
 
53
57
  export function provenanceRouter(
54
58
  binding: ProvenanceBinding,
@@ -76,6 +80,14 @@ export function provenanceRouter(
76
80
  cursorFilter = { createdAt: decoded.createdAt, id: decoded.id };
77
81
  }
78
82
 
83
+ const scopeParsed = parseListScope(c.req.query(), { tenantId });
84
+ if (scopeParsed.kind === 'err') {
85
+ c.status(statusFor('scope-invalid') as never);
86
+ return c.json(
87
+ toWireError({ code: 'scope-invalid', message: scopeParsed.message }, requestId),
88
+ );
89
+ }
90
+
79
91
  const runIdFilter = c.req.query('runId');
80
92
  const agentIdFilter = c.req.query('agentId');
81
93
  const createdAfterRaw = c.req.query('createdAfter');
@@ -98,6 +110,7 @@ export function provenanceRouter(
98
110
  const result = await binding.listRecords({
99
111
  tenantId,
100
112
  limit,
113
+ ...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
101
114
  ...(runIdFilter !== undefined && runIdFilter.length > 0 && { runId: runIdFilter as RunId }),
102
115
  ...(agentIdFilter !== undefined && agentIdFilter.length > 0 && { agentId: agentIdFilter }),
103
116
  ...(createdAfter !== undefined && { createdAfter }),
@@ -148,6 +161,13 @@ export function provenanceRouter(
148
161
  );
149
162
  }
150
163
  const provenance = result.value;
164
+ const callUsage = await readCallUsage(binding, tenantId, runId);
165
+ if (callUsage.kind === 'err') {
166
+ c.status(statusFor(callUsage.error.code) as never);
167
+ return c.json(
168
+ toWireError({ code: callUsage.error.code, message: callUsage.error.message }, requestId),
169
+ );
170
+ }
151
171
  return c.json({
152
172
  id: provenance.id as unknown as string,
153
173
  runId: provenance.runId as unknown as string,
@@ -165,6 +185,7 @@ export function provenanceRouter(
165
185
  edges: provenance.edges,
166
186
  },
167
187
  ...(provenance.signature !== undefined && { signature: provenance.signature }),
188
+ ...(callUsage.value !== undefined && { callUsage: callUsage.value }),
168
189
  });
169
190
  });
170
191
 
@@ -232,6 +253,14 @@ export function provenanceRouter(
232
253
  );
233
254
  }
234
255
  const provenance = loaded.value;
256
+ // The calls' usage as it stands now: the signature covers it.
257
+ const callUsage = await readCallUsage(binding, tenantId, runId);
258
+ if (callUsage.kind === 'err') {
259
+ c.status(statusFor(callUsage.error.code) as never);
260
+ return c.json(
261
+ toWireError({ code: callUsage.error.code, message: callUsage.error.message }, requestId),
262
+ );
263
+ }
235
264
 
236
265
  // 2. Optionally hydrate conversation messages. Agent turns use the
237
266
  // conversation id as the run id; for flow-only runs no
@@ -306,6 +335,7 @@ export function provenanceRouter(
306
335
  edges: provenance.edges,
307
336
  },
308
337
  ...(messages !== undefined && { messages }),
338
+ ...(callUsage.value !== undefined && { callUsage: callUsage.value }),
309
339
  };
310
340
  const canonicalBundleBytes = new TextEncoder().encode(canonicalize(bundleBody));
311
341
 
@@ -355,6 +385,7 @@ interface RecordMetadata {
355
385
  readonly createdAt: string;
356
386
  readonly flowRef?: { readonly id: string; readonly version: string };
357
387
  readonly signed: boolean;
388
+ readonly projectId?: string;
358
389
  }
359
390
 
360
391
  function serializeRecordMetadata(row: ProvenanceRecordSummary): RecordMetadata {
@@ -371,6 +402,7 @@ function serializeRecordMetadata(row: ProvenanceRecordSummary): RecordMetadata {
371
402
  },
372
403
  }),
373
404
  signed: row.signed,
405
+ ...(row.projectId !== undefined && { projectId: row.projectId as unknown as string }),
374
406
  };
375
407
  }
376
408
 
@@ -404,3 +436,18 @@ function parseExportBody(body: Record<string, unknown>): ParseResult<ValidatedEx
404
436
  },
405
437
  };
406
438
  }
439
+
440
+ /** The run's call usage, when the binding has a cost ledger and the run made calls. */
441
+ async function readCallUsage(
442
+ binding: ProvenanceBinding,
443
+ tenantId: TenantId,
444
+ runId: RunId,
445
+ ): Promise<
446
+ | { readonly kind: 'ok'; readonly value: CallUsageByCallId | undefined }
447
+ | { readonly kind: 'err'; readonly error: ProvenanceBindingError }
448
+ > {
449
+ if (binding.getCallUsage === undefined) return { kind: 'ok', value: undefined };
450
+ const read = await binding.getCallUsage(tenantId, runId);
451
+ if (read.kind === 'err') return read;
452
+ return { kind: 'ok', value: Object.keys(read.value).length > 0 ? read.value : undefined };
453
+ }
@@ -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
  }