@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
@@ -10,6 +10,7 @@ import type {
10
10
  ProjectBinding,
11
11
  ProjectMembership,
12
12
  ProjectMembershipBinding,
13
+ ProjectMembershipUpdateRoleOutcome,
13
14
  ProjectPatch,
14
15
  ProjectRole,
15
16
  ProjectSpec,
@@ -19,7 +20,12 @@ import type { Cursor, OrgId, ProjectId, TenantId, UserId } from '@kindgi/types';
19
20
  import { statusFor, toWireError } from '../errors.js';
20
21
  import type { Authorizer } from '../middleware/authorize.js';
21
22
  import type { AppEnv } from '../types.js';
22
- import { projectDefaultAlreadyExistsError, slugConflictError } from './hierarchy-errors.js';
23
+ import {
24
+ membershipNotKeptInStepError,
25
+ orgNotFoundError,
26
+ projectDefaultAlreadyExistsError,
27
+ slugConflictError,
28
+ } from './hierarchy-errors.js';
23
29
  import { clampLimit } from './pagination.js';
24
30
 
25
31
  /**
@@ -74,6 +80,39 @@ export function projectsRouter(
74
80
  ): Hono<AppEnv> {
75
81
  const r = new Hono<AppEnv>();
76
82
 
83
+ /**
84
+ * Change a member's role. With an authorizer, the row and its FGA tuple
85
+ * change together through the tenant-hierarchy binding, or the change
86
+ * is refused; without one, the membership binding alone.
87
+ */
88
+ async function updateMemberRole(
89
+ tenantId: TenantId,
90
+ projectId: ProjectId,
91
+ userId: UserId,
92
+ role: ProjectRole,
93
+ ): Promise<
94
+ | { readonly kind: 'done'; readonly outcome: ProjectMembershipUpdateRoleOutcome }
95
+ | { readonly kind: 'refused'; readonly error: ReturnType<typeof membershipNotKeptInStepError> }
96
+ > {
97
+ if (authorizer === undefined) {
98
+ return {
99
+ kind: 'done',
100
+ outcome: await membershipBinding.updateRole(tenantId, projectId, userId, role),
101
+ };
102
+ }
103
+ if (tenantHierarchy.updateProjectMemberRole === undefined) {
104
+ return { kind: 'refused', error: membershipNotKeptInStepError('updateProjectMemberRole') };
105
+ }
106
+ const res = await tenantHierarchy.updateProjectMemberRole({
107
+ tenantId,
108
+ projectId,
109
+ userId,
110
+ role,
111
+ });
112
+ if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
113
+ return { kind: 'done', outcome: res.value };
114
+ }
115
+
77
116
  // ---------- GET /default (literal segment; must precede /:projectId) ----------
78
117
  r.get('/default', async (c) => {
79
118
  const requestId = c.get('requestId');
@@ -247,9 +286,11 @@ export function projectsRouter(
247
286
  const error =
248
287
  result.error.code === 'slug-conflict'
249
288
  ? slugConflictError('project', spec.slug)
250
- : result.error.code === 'default-conflict'
251
- ? projectDefaultAlreadyExistsError()
252
- : { code: 'internal-server-error', message: result.error.message };
289
+ : result.error.code === 'org-not-found'
290
+ ? orgNotFoundError(result.error.orgId as unknown as string)
291
+ : result.error.code === 'default-conflict'
292
+ ? projectDefaultAlreadyExistsError()
293
+ : { code: 'internal-server-error', message: result.error.message };
253
294
  c.status(statusFor(error.code) as never);
254
295
  return c.json(toWireError(error, requestId));
255
296
  }
@@ -263,7 +304,9 @@ export function projectsRouter(
263
304
  const error =
264
305
  outcome.kind === 'slug-conflict'
265
306
  ? slugConflictError('project', outcome.slug)
266
- : projectDefaultAlreadyExistsError();
307
+ : outcome.kind === 'org-not-found'
308
+ ? orgNotFoundError(outcome.orgId as unknown as string)
309
+ : projectDefaultAlreadyExistsError();
267
310
  c.status(statusFor(error.code) as never);
268
311
  return c.json(toWireError(error, requestId));
269
312
  }
@@ -412,7 +455,19 @@ export function projectsRouter(
412
455
  const tenantId = c.get('tenantId') as TenantId;
413
456
  const projectId = c.req.param('projectId') as ProjectId;
414
457
  const userId = c.req.param('userId') as UserId;
415
- await membershipBinding.remove(tenantId, projectId, userId);
458
+ // With an authorizer, the row and its FGA tuple go together, or not
459
+ // at all: removing the row alone would leave the permission in place.
460
+ if (authorizer !== undefined) {
461
+ if (tenantHierarchy.removeProjectMember === undefined) {
462
+ const refusal = membershipNotKeptInStepError('removeProjectMember');
463
+ c.status(statusFor(refusal.code) as never);
464
+ return c.json(toWireError(refusal, c.get('requestId')));
465
+ }
466
+ const res = await tenantHierarchy.removeProjectMember({ tenantId, projectId, userId });
467
+ if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
468
+ } else {
469
+ await membershipBinding.remove(tenantId, projectId, userId);
470
+ }
416
471
  c.status(204);
417
472
  return c.body(null);
418
473
  });
@@ -452,7 +507,12 @@ export function projectsRouter(
452
507
  ),
453
508
  );
454
509
  }
455
- const outcome = await membershipBinding.updateRole(tenantId, projectId, userId, b.role);
510
+ const updated = await updateMemberRole(tenantId, projectId, userId, b.role);
511
+ if (updated.kind === 'refused') {
512
+ c.status(statusFor(updated.error.code) as never);
513
+ return c.json(toWireError(updated.error, requestId));
514
+ }
515
+ const { outcome } = updated;
456
516
  if (outcome.kind === 'project-membership-not-found') {
457
517
  c.status(statusFor('project-membership-not-found') as never);
458
518
  return c.json(
@@ -596,8 +656,11 @@ export function projectsRouter(
596
656
  ),
597
657
  );
598
658
  }
599
- if (outcome.kind === 'slug-conflict') {
600
- const error = slugConflictError('project', outcome.slug);
659
+ if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
660
+ const error =
661
+ outcome.kind === 'slug-conflict'
662
+ ? slugConflictError('project', outcome.slug)
663
+ : orgNotFoundError(outcome.orgId as unknown as string);
601
664
  c.status(statusFor(error.code) as never);
602
665
  return c.json(toWireError(error, requestId));
603
666
  }
@@ -62,15 +62,21 @@ export function retentionRouter(binding: RetentionBinding, authorizer?: Authoriz
62
62
  const limit = clampLimit(url.searchParams.get('limit') ?? undefined);
63
63
  const pastGraceRaw = url.searchParams.get('pastGraceOnly');
64
64
  const pastGraceOnly = pastGraceRaw === 'true';
65
+ const cursor = url.searchParams.get('cursor') ?? '';
65
66
 
66
- const page = await binding.scheduled({
67
+ const { hasMore, nextCursor, ...page } = await binding.scheduled({
67
68
  tenantId,
68
69
  limit,
69
70
  pastGraceOnly,
70
71
  ...(domain !== undefined && { domain }),
72
+ ...(cursor !== '' && { cursor }),
71
73
  });
72
74
 
73
- return c.json(page);
75
+ return c.json({
76
+ ...page,
77
+ hasMore: hasMore ?? someDomainFilled(page.data, limit),
78
+ ...(nextCursor !== undefined && { nextCursor }),
79
+ });
74
80
  });
75
81
 
76
82
  r.post('/sweep', async (c) => {
@@ -153,6 +159,19 @@ export function retentionRouter(binding: RetentionBinding, authorizer?: Authoriz
153
159
  return r;
154
160
  }
155
161
 
162
+ /**
163
+ * A runtime that doesn't report `hasMore`: there may be more when some
164
+ * domain's rows fill `limit` (each domain is read up to `limit`).
165
+ */
166
+ function someDomainFilled(
167
+ data: readonly { readonly domain: RetentionDomain }[],
168
+ limit: number,
169
+ ): boolean {
170
+ const perDomain = new Map<RetentionDomain, number>();
171
+ for (const row of data) perDomain.set(row.domain, (perDomain.get(row.domain) ?? 0) + 1);
172
+ return [...perDomain.values()].some((n) => n >= limit);
173
+ }
174
+
156
175
  function parseDomain(raw: string | null | undefined): RetentionDomain | 'invalid' | undefined {
157
176
  if (raw === null || raw === undefined || raw === '') return undefined;
158
177
  if ((RETENTION_DOMAINS as readonly string[]).includes(raw)) return raw as RetentionDomain;
@@ -3,10 +3,11 @@
3
3
 
4
4
  import { Hono } from 'hono';
5
5
 
6
- import type { ReviewerRole } from '@kindgi/authz';
6
+ import { type ReviewerRole, ref } from '@kindgi/authz';
7
7
  import type { Cursor, ReviewerId, TenantId, UserId } from '@kindgi/types';
8
8
 
9
9
  import { statusFor, toWireError } from '../errors.js';
10
+ import type { Authorizer } from '../middleware/authorize.js';
10
11
  import type { ReviewerRecord, ReviewerRegistryBinding } from '../reviewer-binding.js';
11
12
  import type { AppEnv } from '../types.js';
12
13
  import { clampLimit } from './pagination.js';
@@ -20,8 +21,9 @@ import { clampLimit } from './pagination.js';
20
21
  * inside `approvalsRouter`: registering a reviewer is an admin surface
21
22
  * action, not a reviewer-only one, so requiring a `reviewerRole` on the
22
23
  * caller would be circular ("only reviewers can appoint reviewers").
23
- * The caller still needs a valid bearer token — the `/v1/*` auth chain
24
- * covers that upstream.
24
+ * With authorization enforced, registering or unregistering needs
25
+ * `admin` on the tenant; reading the roster needs only a valid bearer
26
+ * token (the `/v1/*` auth chain upstream).
25
27
  *
26
28
  * Mounted BEFORE `approvalsRouter` on the parent `/v1` router so path
27
29
  * matching resolves `/v1/approvals/reviewers/*` here rather than being
@@ -29,9 +31,26 @@ import { clampLimit } from './pagination.js';
29
31
  */
30
32
  const ROLE_VALUES: ReadonlySet<ReviewerRole> = new Set(['standard', 'senior', 'admin']);
31
33
 
32
- export function reviewersRouter(binding: ReviewerRegistryBinding): Hono<AppEnv> {
34
+ export function reviewersRouter(
35
+ binding: ReviewerRegistryBinding,
36
+ /**
37
+ * When set, registering or unregistering a reviewer needs `admin` on
38
+ * the tenant: a reviewer (an `admin` one above all) decides approvals.
39
+ * Reading the list doesn't.
40
+ */
41
+ authorizer?: Authorizer,
42
+ ): Hono<AppEnv> {
33
43
  const r = new Hono<AppEnv>();
34
44
 
45
+ if (authorizer !== undefined) {
46
+ r.use('*', async (c, next) => {
47
+ if (c.req.method === 'GET') return next();
48
+ const tenantId = c.get('tenantId') as TenantId;
49
+ const mw = authorizer.authorize('admin', () => ref('tenant', tenantId as unknown as string));
50
+ return mw(c, next);
51
+ });
52
+ }
53
+
35
54
  // ---------- GET / (list, cursor-paginated) ----------
36
55
  r.get('/', async (c) => {
37
56
  const requestId = c.get('requestId');
@@ -2,11 +2,18 @@
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
4
  import { Hono } from 'hono';
5
- import type { MiddlewareHandler } from 'hono';
6
5
 
7
6
  import type { AgentId } from '@kindgi/agents';
8
7
  import type { KernelRunRecord, ListRunsInput, RunBinding } from '@kindgi/runtime';
9
- import type { FlowId, ListScope, ProjectId, RunId, Semver, TenantId } from '@kindgi/types';
8
+ import type {
9
+ FlowId,
10
+ ListScope,
11
+ ProjectId,
12
+ RunId,
13
+ ScopeSegment,
14
+ Semver,
15
+ TenantId,
16
+ } from '@kindgi/types';
10
17
 
11
18
  import { ref } from '@kindgi/authz';
12
19
 
@@ -20,9 +27,11 @@ import type {
20
27
  import type { Authorizer } from '../middleware/authorize.js';
21
28
  import type { MintPublicRunTokenResult } from '../public-run-token.js';
22
29
  import type { AppEnv } from '../types.js';
30
+ import { liveScopeToWire } from './live-scope-wire.js';
23
31
  import type { DecodedCursor } from './pagination.js';
24
32
  import { clampLimit, decodeCursor } from './pagination.js';
25
33
  import { parseListScope } from './scope-params.js';
34
+ import { parseSegmentsBody } from './segments.js';
26
35
  import {
27
36
  formatSseFrame,
28
37
  isTerminalWireKind,
@@ -30,6 +39,7 @@ import {
30
39
  projectJournalEntry,
31
40
  toRunProgressEvent,
32
41
  } from './sse.js';
42
+ import { UUID_RE, refuseMalformedUuidParam } from './uuid-param.js';
33
43
 
34
44
  const KERNEL_RUN_CHANNEL_PREFIX = 'kernel:run:';
35
45
 
@@ -61,23 +71,8 @@ const MAX_RUN_ANCESTRY = 16;
61
71
  * `POST /:runId/cancel`, `POST /:runId/resume`, `GET /:runId/stream`
62
72
  * (SSE), `GET /:runId/journal`.
63
73
  */
64
- /** A run id: a UUID. */
65
- const RUN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
66
-
67
- /**
68
- * A `:runId` that isn't a run id is a 400, before it reaches a query (where
69
- * Postgres's uuid cast would fail it as a 500).
70
- */
71
- const refuseMalformedRunId: MiddlewareHandler<AppEnv> = async (c, next) => {
72
- if (RUN_ID_RE.test(c.req.param('runId') ?? '')) return next();
73
- c.status(statusFor('bad-input') as never);
74
- return c.json(
75
- toWireError(
76
- { code: 'bad-input', message: '`runId` must be a run id (a UUID)' },
77
- c.get('requestId'),
78
- ),
79
- );
80
- };
74
+ /** A `:runId` that isn't a run id is a 400 (`uuid-param.ts`). */
75
+ const refuseMalformedRunId = refuseMalformedUuidParam('runId', 'a run id');
81
76
 
82
77
  export function runsRouter(
83
78
  binding: RunHandlerBinding,
@@ -634,6 +629,7 @@ function invokeFromBody(
634
629
  tenantId,
635
630
  ...(body.projectId !== undefined && { projectId: body.projectId }),
636
631
  input: body.input,
632
+ ...(body.segments !== undefined && { segments: body.segments }),
637
633
  ...(body.dryRun !== undefined && { dryRun: body.dryRun }),
638
634
  ...(body.wait !== undefined && { wait: body.wait }),
639
635
  };
@@ -681,10 +677,19 @@ function serializeRun(
681
677
  id: row.agent.id,
682
678
  version: row.agent.version,
683
679
  conversationId: row.agent.conversationId as unknown as string,
680
+ ...(row.agent.via !== undefined && { via: row.agent.via }),
681
+ ...(row.agent.liveScope !== undefined && {
682
+ liveScope: liveScopeToWire(row.agent.liveScope),
683
+ }),
684
684
  },
685
685
  }),
686
686
  ...(row.replayOf != null && { replayOf: row.replayOf as unknown as string }),
687
687
  ...(row.evalRunId != null && { evalRunId: row.evalRunId }),
688
+ ...(row.versions != null && { versions: row.versions }),
689
+ ...(row.segments !== undefined &&
690
+ row.segments.length > 0 && {
691
+ segments: row.segments.map(({ key, value }) => ({ key, value })),
692
+ }),
688
693
  };
689
694
  }
690
695
 
@@ -776,7 +781,7 @@ function parseRunListFilter(
776
781
  return { kind: 'err', message: '`topLevel` must be `true` or `false`' };
777
782
  }
778
783
  const topLevelOnly = topLevel === 'true';
779
- if (parentRunId !== undefined && !RUN_ID_RE.test(parentRunId)) {
784
+ if (parentRunId !== undefined && !UUID_RE.test(parentRunId)) {
780
785
  return { kind: 'err', message: '`parentRunId` must be a run id (a UUID)' };
781
786
  }
782
787
  if (parentRunId !== undefined && topLevelOnly) {
@@ -848,6 +853,7 @@ type ParsedStartRunBody =
848
853
  readonly agentId: AgentId;
849
854
  readonly agentVersion?: Semver;
850
855
  readonly projectId?: ProjectId;
856
+ readonly segments?: readonly ScopeSegment[];
851
857
  readonly input: unknown;
852
858
  readonly dryRun?: boolean;
853
859
  readonly wait?: boolean;
@@ -857,6 +863,7 @@ type ParsedStartRunBody =
857
863
  readonly flowId: FlowId;
858
864
  readonly flowVersion?: Semver;
859
865
  readonly projectId?: ProjectId;
866
+ readonly segments?: readonly ScopeSegment[];
860
867
  readonly input: unknown;
861
868
  readonly dryRun?: boolean;
862
869
  readonly wait?: boolean;
@@ -892,6 +899,11 @@ function parseStartRunBody(
892
899
  const options = parseStartOptions(b.options);
893
900
  if (options.kind === 'err') return options;
894
901
  const { dryRun, wait } = options.value;
902
+ const parsedSegments = parseSegmentsBody(b.segments);
903
+ if (parsedSegments.kind === 'err') {
904
+ return { kind: 'err', error: { code: 'bad-input', message: parsedSegments.message } };
905
+ }
906
+ const { segments } = parsedSegments;
895
907
  if (hasAgent) {
896
908
  const agentVersion = b.agentVersion;
897
909
  if (agentVersion !== undefined && typeof agentVersion !== 'string') {
@@ -914,6 +926,7 @@ function parseStartRunBody(
914
926
  agentId: b.agent as AgentId,
915
927
  ...(agentVersion !== undefined && { agentVersion: agentVersion as Semver }),
916
928
  ...(agentProjectIdRaw !== undefined && { projectId: agentProjectIdRaw as ProjectId }),
929
+ ...(segments !== undefined && { segments }),
917
930
  input: b.input,
918
931
  ...(dryRun !== undefined && { dryRun }),
919
932
  ...(wait !== undefined && { wait }),
@@ -941,6 +954,7 @@ function parseStartRunBody(
941
954
  flowId: b.flow as FlowId,
942
955
  ...(flowVersion !== undefined && { flowVersion: flowVersion as Semver }),
943
956
  ...(projectId !== undefined && { projectId: projectId as ProjectId }),
957
+ ...(segments !== undefined && { segments }),
944
958
  input: b.input,
945
959
  ...(dryRun !== undefined && { dryRun }),
946
960
  ...(wait !== undefined && { wait }),
@@ -160,6 +160,7 @@ export function secretsRouter(options: SecretsRouterOptions): Hono<AppEnv> {
160
160
  });
161
161
  return c.json({
162
162
  data: page.data.map(serializeSecretRecord),
163
+ hasMore: page.nextCursor !== undefined,
163
164
  ...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
164
165
  });
165
166
  });
@@ -232,6 +233,7 @@ export function secretsRouter(options: SecretsRouterOptions): Hono<AppEnv> {
232
233
  });
233
234
  return c.json({
234
235
  data: page.data.map(serializeVersion),
236
+ hasMore: page.nextCursor !== undefined,
235
237
  ...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
236
238
  });
237
239
  });
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ScopeSegment } from '@kindgi/types';
5
+
6
+ /** A segment key: lowercase, like an identifier (`company`, `contact-role`). */
7
+ const SEGMENT_KEY_RE = /^[a-z][a-z0-9_-]{0,63}$/;
8
+ const MAX_SEGMENTS = 8;
9
+ const MAX_VALUE_LENGTH = 256;
10
+
11
+ type Parsed =
12
+ | { readonly kind: 'ok'; readonly segments?: readonly ScopeSegment[] }
13
+ | { readonly kind: 'err'; readonly message: string };
14
+
15
+ function checked(segments: readonly ScopeSegment[], field: string): Parsed {
16
+ if (segments.length === 0) return { kind: 'ok' };
17
+ if (segments.length > MAX_SEGMENTS) {
18
+ return { kind: 'err', message: `\`${field}\` takes at most ${MAX_SEGMENTS} segments` };
19
+ }
20
+ const seen = new Set<string>();
21
+ for (const { key, value } of segments) {
22
+ if (!SEGMENT_KEY_RE.test(key)) {
23
+ return {
24
+ kind: 'err',
25
+ message: `\`${field}\`: a segment key is lowercase letters, digits, - and _ (starting with a letter); got "${key}"`,
26
+ };
27
+ }
28
+ if (seen.has(key))
29
+ return { kind: 'err', message: `\`${field}\`: the key "${key}" appears twice` };
30
+ seen.add(key);
31
+ if (value.length === 0 || value.length > MAX_VALUE_LENGTH) {
32
+ return {
33
+ kind: 'err',
34
+ message: `\`${field}\`: a segment value is 1–${MAX_VALUE_LENGTH} characters (key "${key}")`,
35
+ };
36
+ }
37
+ }
38
+ return { kind: 'ok', segments };
39
+ }
40
+
41
+ /**
42
+ * A run's or a pin's segment path from a JSON body: an ordered list of
43
+ * `{key, value}`, coarse to fine (e.g. `company` then `role`).
44
+ */
45
+ export function parseSegmentsBody(raw: unknown, field = 'segments'): Parsed {
46
+ if (raw === undefined) return { kind: 'ok' };
47
+ if (!Array.isArray(raw)) {
48
+ return {
49
+ kind: 'err',
50
+ message: `\`${field}\` must be an array of {key, value}, coarse to fine`,
51
+ };
52
+ }
53
+ const segments: ScopeSegment[] = [];
54
+ for (const item of raw) {
55
+ const { key, value } = (item ?? {}) as { key?: unknown; value?: unknown };
56
+ if (typeof key !== 'string' || typeof value !== 'string') {
57
+ return { kind: 'err', message: `\`${field}\`: each segment is {key: string, value: string}` };
58
+ }
59
+ segments.push({ key, value });
60
+ }
61
+ return checked(segments, field);
62
+ }
63
+
64
+ /** The same from a query: repeated `segment=key:value`, in order. */
65
+ export function parseSegmentsQuery(values: readonly string[]): Parsed {
66
+ const segments: ScopeSegment[] = [];
67
+ for (const raw of values) {
68
+ const colon = raw.indexOf(':');
69
+ if (colon <= 0) {
70
+ return { kind: 'err', message: `\`segment\` is key:value; got "${raw}"` };
71
+ }
72
+ segments.push({ key: raw.slice(0, colon), value: raw.slice(colon + 1) });
73
+ }
74
+ return checked(segments, 'segment');
75
+ }
@@ -10,6 +10,7 @@ import type {
10
10
  TeamBinding,
11
11
  TeamMembership,
12
12
  TeamMembershipBinding,
13
+ TeamMembershipUpdateRoleOutcome,
13
14
  TeamPatch,
14
15
  TeamRole,
15
16
  TeamSpec,
@@ -19,7 +20,11 @@ import type { Cursor, OrgId, TeamId, TenantId, UserId } from '@kindgi/types';
19
20
  import { statusFor, toWireError } from '../errors.js';
20
21
  import type { Authorizer } from '../middleware/authorize.js';
21
22
  import type { AppEnv } from '../types.js';
22
- import { slugConflictError } from './hierarchy-errors.js';
23
+ import {
24
+ membershipNotKeptInStepError,
25
+ orgNotFoundError,
26
+ slugConflictError,
27
+ } from './hierarchy-errors.js';
23
28
  import { clampLimit } from './pagination.js';
24
29
 
25
30
  /**
@@ -57,6 +62,34 @@ export function teamsRouter(
57
62
  ): Hono<AppEnv> {
58
63
  const r = new Hono<AppEnv>();
59
64
 
65
+ /**
66
+ * Change a member's role. With an authorizer, the row and its FGA tuple
67
+ * change together through the tenant-hierarchy binding, or the change
68
+ * is refused; without one, the membership binding alone.
69
+ */
70
+ async function updateMemberRole(
71
+ tenantId: TenantId,
72
+ teamId: TeamId,
73
+ userId: UserId,
74
+ role: TeamRole,
75
+ ): Promise<
76
+ | { readonly kind: 'done'; readonly outcome: TeamMembershipUpdateRoleOutcome }
77
+ | { readonly kind: 'refused'; readonly error: ReturnType<typeof membershipNotKeptInStepError> }
78
+ > {
79
+ if (authorizer === undefined) {
80
+ return {
81
+ kind: 'done',
82
+ outcome: await membershipBinding.updateRole(tenantId, teamId, userId, role),
83
+ };
84
+ }
85
+ if (tenantHierarchy.updateTeamMemberRole === undefined) {
86
+ return { kind: 'refused', error: membershipNotKeptInStepError('updateTeamMemberRole') };
87
+ }
88
+ const res = await tenantHierarchy.updateTeamMemberRole({ tenantId, teamId, userId, role });
89
+ if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
90
+ return { kind: 'done', outcome: res.value };
91
+ }
92
+
60
93
  // ---------- Authorization middleware ----------
61
94
  // Full PEP surface:
62
95
  // POST / → admin on tenant
@@ -201,7 +234,9 @@ export function teamsRouter(
201
234
  const error =
202
235
  result.error.code === 'slug-conflict'
203
236
  ? slugConflictError('team', spec.slug)
204
- : { code: 'internal-server-error', message: result.error.message };
237
+ : result.error.code === 'org-not-found'
238
+ ? orgNotFoundError(result.error.orgId as unknown as string)
239
+ : { code: 'internal-server-error', message: result.error.message };
205
240
  c.status(statusFor(error.code) as never);
206
241
  return c.json(toWireError(error, requestId));
207
242
  }
@@ -209,8 +244,11 @@ export function teamsRouter(
209
244
  return c.json({ id: result.value.teamId as unknown as string });
210
245
  }
211
246
  const outcome = await binding.create(tenantId, spec);
212
- if (outcome.kind === 'slug-conflict') {
213
- const error = slugConflictError('team', outcome.slug);
247
+ if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
248
+ const error =
249
+ outcome.kind === 'slug-conflict'
250
+ ? slugConflictError('team', outcome.slug)
251
+ : orgNotFoundError(outcome.orgId as unknown as string);
214
252
  c.status(statusFor(error.code) as never);
215
253
  return c.json(toWireError(error, requestId));
216
254
  }
@@ -358,7 +396,19 @@ export function teamsRouter(
358
396
  const tenantId = c.get('tenantId') as TenantId;
359
397
  const teamId = c.req.param('teamId') as TeamId;
360
398
  const userId = c.req.param('userId') as UserId;
361
- await membershipBinding.remove(tenantId, teamId, userId);
399
+ // With an authorizer, the row and its FGA tuple go together, or not
400
+ // at all: removing the row alone would leave the permission in place.
401
+ if (authorizer !== undefined) {
402
+ if (tenantHierarchy.removeTeamMember === undefined) {
403
+ const refusal = membershipNotKeptInStepError('removeTeamMember');
404
+ c.status(statusFor(refusal.code) as never);
405
+ return c.json(toWireError(refusal, c.get('requestId')));
406
+ }
407
+ const res = await tenantHierarchy.removeTeamMember({ tenantId, teamId, userId });
408
+ if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
409
+ } else {
410
+ await membershipBinding.remove(tenantId, teamId, userId);
411
+ }
362
412
  c.status(204);
363
413
  return c.body(null);
364
414
  });
@@ -398,7 +448,12 @@ export function teamsRouter(
398
448
  ),
399
449
  );
400
450
  }
401
- const outcome = await membershipBinding.updateRole(tenantId, teamId, userId, b.role);
451
+ const updated = await updateMemberRole(tenantId, teamId, userId, b.role);
452
+ if (updated.kind === 'refused') {
453
+ c.status(statusFor(updated.error.code) as never);
454
+ return c.json(toWireError(updated.error, requestId));
455
+ }
456
+ const { outcome } = updated;
402
457
  if (outcome.kind === 'team-membership-not-found') {
403
458
  c.status(statusFor('team-membership-not-found') as never);
404
459
  return c.json(
@@ -542,8 +597,11 @@ export function teamsRouter(
542
597
  ),
543
598
  );
544
599
  }
545
- if (outcome.kind === 'slug-conflict') {
546
- const error = slugConflictError('team', outcome.slug);
600
+ if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
601
+ const error =
602
+ outcome.kind === 'slug-conflict'
603
+ ? slugConflictError('team', outcome.slug)
604
+ : orgNotFoundError(outcome.orgId as unknown as string);
547
605
  c.status(statusFor(error.code) as never);
548
606
  return c.json(toWireError(error, requestId));
549
607
  }
@@ -9,6 +9,7 @@ import type { Cursor, ProjectId, TenantId, ToolId, UserId } from '@kindgi/types'
9
9
 
10
10
  import { statusFor, toWireError } from '../errors.js';
11
11
  import type { Authorizer } from '../middleware/authorize.js';
12
+ import { refuseWritesWhenReadOnly } from '../registry-read-only.js';
12
13
  import type { ToolRegistryBinding } from '../tool-binding.js';
13
14
  import type { AppEnv } from '../types.js';
14
15
  import { clampLimit } from './pagination.js';
@@ -47,6 +48,12 @@ export function toolsRouter(
47
48
  onWrite?: ToolWriteHook,
48
49
  ): Hono<AppEnv> {
49
50
  const r = new Hono<AppEnv>();
51
+ // A read-only registry (under `kindgi dev`, the pack's files) refuses
52
+ // every write before anything else runs.
53
+ r.use(
54
+ '*',
55
+ refuseWritesWhenReadOnly(() => binding.readOnly),
56
+ );
50
57
 
51
58
  // Authorization (PEP) — mirrors agents. Cascade via `tool#parent@project`
52
59
  // written in-tx by publish; direct check via `ref('tool', businessId)`.
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { MiddlewareHandler } from 'hono';
5
+
6
+ import { statusFor, toWireError } from '../errors.js';
7
+ import type { AppEnv } from '../types.js';
8
+
9
+ /** A UUID, the shape of a run's and a conversation's id. */
10
+ export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+
12
+ /**
13
+ * A path parameter that must be a UUID: one that isn't is a 400 naming
14
+ * it, before it reaches a query (where Postgres's uuid cast would fail it
15
+ * as a 500). `what` names the id, e.g. `a run id`.
16
+ */
17
+ export function refuseMalformedUuidParam(param: string, what: string): MiddlewareHandler<AppEnv> {
18
+ return async (c, next) => {
19
+ if (UUID_RE.test(c.req.param(param) ?? '')) return next();
20
+ c.status(statusFor('bad-input') as never);
21
+ return c.json(
22
+ toWireError(
23
+ { code: 'bad-input', message: `\`${param}\` must be ${what} (a UUID)` },
24
+ c.get('requestId'),
25
+ ),
26
+ );
27
+ };
28
+ }
@@ -6,6 +6,8 @@ import type { Scope } from '@kindgi/platform';
6
6
  import type { ToolManifest } from '@kindgi/tools';
7
7
  import type { Cursor, ProjectId, Semver, TenantId, ToolId } from '@kindgi/types';
8
8
 
9
+ import type { RegistryReadOnly } from './registry-read-only.js';
10
+
9
11
  /**
10
12
  * Caller-plugged surface for the tool catalog. Versioned CRUD mirroring
11
13
  * `AgentRegistryBinding` — the API package does NOT own registry
@@ -30,6 +32,13 @@ import type { Cursor, ProjectId, Semver, TenantId, ToolId } from '@kindgi/types'
30
32
  * not-found via `headExists`).
31
33
  */
32
34
  export interface ToolRegistryBinding {
35
+ /**
36
+ * Set when this registry takes no writes (under `kindgi dev`, the
37
+ * pack's files are the source of its tools): every write is refused
38
+ * with `409 registry-read-only` and this reason, before the binding is
39
+ * called. See `RegistryReadOnly`.
40
+ */
41
+ readonly readOnly?: RegistryReadOnly;
33
42
  /**
34
43
  * Cursor-paginated list of tools (latest active version per id).
35
44
  * Optional `nameFilter` is a prefix match on the tool id — tools use