@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
@@ -0,0 +1,101 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { AgentId } from '@kindgi/agents';
5
+ import type { ProjectId, Semver } from '@kindgi/types';
6
+
7
+ import type { EvalBaseline, EvalComparison } from '../eval-run-binding.js';
8
+ import { DEFAULT_COMPARISON } from '../judged-dispatcher.js';
9
+
10
+ const MAX_REPETITIONS = 10;
11
+ const MAX_K = 100;
12
+
13
+ function obj(value: unknown): Record<string, unknown> | undefined {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
15
+ ? (value as Record<string, unknown>)
16
+ : undefined;
17
+ }
18
+
19
+ function parseLive(raw: unknown): EvalBaseline | string {
20
+ const live = obj(raw);
21
+ if (live === undefined) return '`baseline.live` must be an object';
22
+ const segments = live.segments === undefined ? undefined : obj(live.segments);
23
+ const stringsOnly =
24
+ segments !== undefined && Object.values(segments).every((v) => typeof v === 'string');
25
+ if (live.segments !== undefined && !stringsOnly) {
26
+ return '`baseline.live.segments` must be an object of strings';
27
+ }
28
+ if (live.projectId !== undefined && typeof live.projectId !== 'string') {
29
+ return '`baseline.live.projectId` must be a string';
30
+ }
31
+ return {
32
+ live: {
33
+ ...(typeof live.projectId === 'string' && { projectId: live.projectId as ProjectId }),
34
+ ...(segments !== undefined && { segments: segments as Record<string, string> }),
35
+ },
36
+ };
37
+ }
38
+
39
+ function parseBaseline(raw: unknown): EvalBaseline | string {
40
+ if (raw === 'recorded') return 'recorded';
41
+ const o = obj(raw);
42
+ if (o === undefined) {
43
+ return "`baseline` must be 'recorded', { agentId, version } or { live: { projectId?, segments? } }";
44
+ }
45
+ if (o.live !== undefined) return parseLive(o.live);
46
+ if (typeof o.agentId !== 'string' || o.agentId === '' || typeof o.version !== 'string') {
47
+ return '`baseline` as a version needs `agentId` and `version`';
48
+ }
49
+ return { agentId: o.agentId as AgentId, version: o.version as Semver };
50
+ }
51
+
52
+ function integerIn(raw: unknown, name: string, max: number): number | string {
53
+ return typeof raw === 'number' && Number.isInteger(raw) && raw >= 1 && raw <= max
54
+ ? raw
55
+ : `\`${name}\` must be an integer from 1 to ${max}`;
56
+ }
57
+
58
+ /**
59
+ * A comparison eval run's settings from the start body: `baseline`,
60
+ * `reads`, `repetitions` and `k`. `undefined` when none is given (the
61
+ * defaults apply); an error message for a bad one.
62
+ */
63
+ export function parseComparison(
64
+ b: Readonly<Record<string, unknown>>,
65
+ ):
66
+ | { readonly kind: 'ok'; readonly value?: EvalComparison }
67
+ | { readonly kind: 'err'; readonly message: string } {
68
+ const { baseline, reads, repetitions, k } = b;
69
+ if (
70
+ baseline === undefined &&
71
+ reads === undefined &&
72
+ repetitions === undefined &&
73
+ k === undefined
74
+ ) {
75
+ return { kind: 'ok' };
76
+ }
77
+ const parsedBaseline =
78
+ baseline === undefined ? DEFAULT_COMPARISON.baseline : parseBaseline(baseline);
79
+ if (typeof parsedBaseline === 'string' && parsedBaseline !== 'recorded') {
80
+ return { kind: 'err', message: parsedBaseline };
81
+ }
82
+ if (reads !== undefined && reads !== 'recorded' && reads !== 'live') {
83
+ return { kind: 'err', message: "`reads` must be 'recorded' or 'live'" };
84
+ }
85
+ const reps =
86
+ repetitions === undefined
87
+ ? DEFAULT_COMPARISON.repetitions
88
+ : integerIn(repetitions, 'repetitions', MAX_REPETITIONS);
89
+ if (typeof reps === 'string') return { kind: 'err', message: reps };
90
+ const topK = k === undefined ? DEFAULT_COMPARISON.k : integerIn(k, 'k', MAX_K);
91
+ if (typeof topK === 'string') return { kind: 'err', message: topK };
92
+ return {
93
+ kind: 'ok',
94
+ value: {
95
+ baseline: parsedBaseline,
96
+ reads: reads ?? DEFAULT_COMPARISON.reads,
97
+ repetitions: reps,
98
+ k: topK,
99
+ },
100
+ };
101
+ }
@@ -10,6 +10,7 @@ import { statusFor, toWireError } from '../errors.js';
10
10
  import {
11
11
  type AgentRef,
12
12
  EVAL_RUN_STATUSES,
13
+ type EvalComparison,
13
14
  type EvalRun,
14
15
  type EvalRunBinding,
15
16
  type EvalRunFilter,
@@ -17,6 +18,7 @@ import {
17
18
  type FlowRef,
18
19
  } from '../eval-run-binding.js';
19
20
  import type { AppEnv } from '../types.js';
21
+ import { parseComparison } from './eval-comparison.js';
20
22
  import { clampLimit } from './pagination.js';
21
23
  import { parseScopeParams } from './scope-params.js';
22
24
  import { formatSseFrame } from './sse.js';
@@ -93,6 +95,7 @@ function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
93
95
  ...(parsed.value.correlationId !== undefined && {
94
96
  correlationId: parsed.value.correlationId,
95
97
  }),
98
+ ...(parsed.value.comparison !== undefined && { comparison: parsed.value.comparison }),
96
99
  });
97
100
  if (outcome.kind === 'suite-not-found') {
98
101
  c.status(statusFor('eval-suite-not-found') as never);
@@ -454,6 +457,7 @@ function serializeEvalRun(run: EvalRun): Record<string, unknown> {
454
457
  ...(run.result !== undefined && { result: run.result }),
455
458
  ...(run.error !== undefined && { error: run.error }),
456
459
  ...(run.correlationId !== undefined && { correlationId: run.correlationId }),
460
+ ...(run.comparison !== undefined && { comparison: run.comparison }),
457
461
  };
458
462
  }
459
463
 
@@ -486,6 +490,7 @@ interface ParsedStartBody {
486
490
  readonly flowRef?: FlowRef;
487
491
  readonly dryRun?: boolean;
488
492
  readonly correlationId?: string;
493
+ readonly comparison?: EvalComparison;
489
494
  }
490
495
 
491
496
  function parseStartBody(
@@ -618,6 +623,11 @@ function parseStartBody(
618
623
  };
619
624
  }
620
625
 
626
+ const comparison = parseComparison(b);
627
+ if (comparison.kind === 'err') {
628
+ return { kind: 'err', error: { code: 'bad-input', message: comparison.message } };
629
+ }
630
+
621
631
  return {
622
632
  kind: 'ok',
623
633
  value: {
@@ -626,6 +636,7 @@ function parseStartBody(
626
636
  ...(flowRef !== undefined && { flowRef }),
627
637
  ...(dryRun !== undefined && { dryRun }),
628
638
  ...(correlationId !== undefined && { correlationId: correlationId as string }),
639
+ ...(comparison.value !== undefined && { comparison: comparison.value }),
629
640
  },
630
641
  };
631
642
  }
@@ -4,12 +4,16 @@
4
4
  import { Hono } from 'hono';
5
5
 
6
6
  import { type Principal, ref, tuplesForCreate } from '@kindgi/authz';
7
- import { type Flow, type FlowError, loadFlow } from '@kindgi/flow';
7
+ import { type Flow, type FlowError, flowPinsDigest, loadFlow } from '@kindgi/flow';
8
8
  import type { Cursor, FlowId, ProjectId, TenantId, UserId } from '@kindgi/types';
9
9
 
10
+ import type { AgentRegistryBinding } from '../agent-binding.js';
11
+ import type { UnpinnableRef } from '../agent-pins.js';
10
12
  import { statusFor, toWireError } from '../errors.js';
11
- import type { FlowRegistryBinding } from '../flow-binding.js';
13
+ import type { FlowRegistryBinding, FlowVersionRecord } from '../flow-binding.js';
14
+ import { resolveFlowPins } from '../flow-pins.js';
12
15
  import type { Authorizer } from '../middleware/authorize.js';
16
+ import type { ToolRegistryBinding } from '../tool-binding.js';
13
17
  import type { AppEnv } from '../types.js';
14
18
  import { clampLimit } from './pagination.js';
15
19
  import { parseScopeParams } from './scope-params.js';
@@ -30,8 +34,19 @@ import { parseScopeParams } from './scope-params.js';
30
34
  * Run initiation stays at `POST /v1/runs` (one primitive,
31
35
  * discriminated subject). No
32
36
  * `/v1/flows/:flowId/run` route is added here.
37
+ *
38
+ * With `pinning` (the tool and agent registries), a published version is
39
+ * pinned: each tool it runs, and each agent it runs at no named version,
40
+ * resolves once, at publish, to the latest version, which every run of
41
+ * that version uses (`pins`, see `resolveFlowPins`); one with no
42
+ * published version refuses the publish. Without it, versions carry no
43
+ * pins and bind the latest versions per run.
33
44
  */
34
- export function flowsRouter(binding: FlowRegistryBinding, authorizer?: Authorizer): Hono<AppEnv> {
45
+ export function flowsRouter(
46
+ binding: FlowRegistryBinding,
47
+ authorizer?: Authorizer,
48
+ pinning?: { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding },
49
+ ): Hono<AppEnv> {
35
50
  const r = new Hono<AppEnv>();
36
51
 
37
52
  // Authorization — mirrors agents 1:1. Check the resource directly;
@@ -275,13 +290,23 @@ export function flowsRouter(binding: FlowRegistryBinding, authorizer?: Authorize
275
290
  );
276
291
  }
277
292
 
293
+ // Pin the version: every tool and unversioned agent resolves now,
294
+ // once, to the version its runs use. One with no published version
295
+ // refuses the publish rather than store a partly pinned version.
296
+ const pinned = await pinFlow(pinning, tenantId, loaded.value);
297
+ if (pinned.kind === 'unpinnable') {
298
+ c.status(statusFor('validation-failed') as never);
299
+ return c.json(toWireError(unpinnableFlow(loaded.value, pinned.issues), requestId));
300
+ }
301
+ const flow = pinned.flow;
302
+
278
303
  const principal = c.get('principal') as Principal | undefined;
279
304
  const creatorUserId =
280
305
  principal?.actor.kind === 'user' ? (principal.actor.id as UserId) : undefined;
281
306
  const outcome = await binding.publish({
282
307
  tenantId,
283
308
  projectId,
284
- flow: loaded.value,
309
+ flow,
285
310
  enqueueTuples: (flowId) =>
286
311
  tuplesForCreate({ kind: 'flow', id: flowId as FlowId, tenantId, projectId }, creatorUserId),
287
312
  });
@@ -383,7 +408,36 @@ export function flowsRouter(binding: FlowRegistryBinding, authorizer?: Authorize
383
408
  return r;
384
409
  }
385
410
 
386
- function serializeGraph(g: Flow): Record<string, unknown> {
411
+ /** The flow with its pins, or the references with no published version; as it is without `pinning`. */
412
+ async function pinFlow(
413
+ pinning:
414
+ | { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding }
415
+ | undefined,
416
+ tenantId: TenantId,
417
+ flow: Flow,
418
+ ): Promise<
419
+ | { readonly kind: 'ok'; readonly flow: Flow }
420
+ | { readonly kind: 'unpinnable'; readonly issues: readonly UnpinnableRef[] }
421
+ > {
422
+ if (pinning === undefined) return { kind: 'ok', flow };
423
+ const resolved = await resolveFlowPins(pinning.tools, pinning.agents, tenantId, flow);
424
+ if (resolved.kind === 'unpinnable') return resolved;
425
+ return {
426
+ kind: 'ok',
427
+ flow: { ...flow, pins: resolved.pins, pinsDigest: flowPinsDigest(resolved.pins) },
428
+ };
429
+ }
430
+
431
+ /** The `validation-failed` error naming each tool or agent a flow runs that isn't published. */
432
+ function unpinnableFlow(flow: Flow, issues: readonly UnpinnableRef[]) {
433
+ return {
434
+ code: 'validation-failed' as const,
435
+ message: `Flow "${flow.id as unknown as string}" runs tools or agents that aren't published (${issues.length} issue${issues.length === 1 ? '' : 's'})`,
436
+ issues: issues as unknown as Record<string, unknown>[],
437
+ };
438
+ }
439
+
440
+ function serializeGraph(g: FlowVersionRecord): Record<string, unknown> {
387
441
  return {
388
442
  id: g.id as unknown as string,
389
443
  version: g.version,
@@ -393,6 +447,10 @@ function serializeGraph(g: Flow): Record<string, unknown> {
393
447
  edges: g.edges,
394
448
  ...(g.maxParallelism !== undefined && { maxParallelism: g.maxParallelism }),
395
449
  ...(g.metadata !== undefined && { metadata: g.metadata }),
450
+ ...(g.pins !== undefined && { pins: g.pins }),
451
+ ...(g.pinsDigest !== undefined && { pinsDigest: g.pinsDigest }),
452
+ ...(g.derivedFrom !== undefined && { derivedFrom: g.derivedFrom }),
453
+ ...(g.unregisteredAt !== undefined && { unregisteredAt: g.unregisteredAt }),
396
454
  };
397
455
  }
398
456
 
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * Wire errors the tenant-hierarchy routes (orgs, teams, projects) share,
6
+ * so a conflict reads the same whichever route or write path (with or
7
+ * without an authorizer) produced it.
8
+ */
9
+
10
+ /** A hierarchy resource, as its wire errors name it. */
11
+ export type HierarchyResource = 'org' | 'team' | 'project';
12
+
13
+ /**
14
+ * `409 slug-conflict`: another `resource` has `slug` where it must be
15
+ * unique. An org's and a team's slug are unique in the tenant; a
16
+ * project's in its org, and a project without an org's among the
17
+ * projects without one.
18
+ */
19
+ export function slugConflictError(resource: HierarchyResource, slug: string) {
20
+ return {
21
+ code: 'slug-conflict',
22
+ message:
23
+ resource === 'project'
24
+ ? `Another project in its org has the slug "${slug}" (for a project without an org: another project without one)`
25
+ : `Another ${resource} in the tenant has the slug "${slug}"`,
26
+ resource,
27
+ slug,
28
+ } as const;
29
+ }
30
+
31
+ /**
32
+ * `409 slug-conflict` on deleting an org: its projects would leave it
33
+ * with `slugs` that projects without an org already have.
34
+ */
35
+ export function orgDeleteSlugConflictError(slugs: readonly string[]) {
36
+ const named = slugs.map((s) => `"${s}"`).join(', ');
37
+ return {
38
+ code: 'slug-conflict',
39
+ message: `Deleting the org would leave its projects ${named} without an org, where other projects already have those slugs. Rename or move them first.`,
40
+ resource: 'project',
41
+ slugs,
42
+ } as const;
43
+ }
44
+
45
+ /** `409 project-default-already-exists`: the tenant has a Default project. */
46
+ export function projectDefaultAlreadyExistsError() {
47
+ return {
48
+ code: 'project-default-already-exists',
49
+ message: 'The tenant already has a Default project',
50
+ } as const;
51
+ }
@@ -11,6 +11,8 @@ import type {
11
11
  SessionSummary,
12
12
  UserRecord,
13
13
  } from '../identity-directory-binding.js';
14
+ import type { ReviewerBinding } from '../reviewer-binding.js';
15
+ import { callerReviewerRole } from '../reviewer-role.js';
14
16
  import type { SessionStoreBinding } from '../session-store-binding.js';
15
17
  import type { AppEnv } from '../types.js';
16
18
  import { clampLimit } from './pagination.js';
@@ -47,10 +49,16 @@ export interface IdentityRouterOptions {
47
49
  * deployments that don't wire an OAuth surface.
48
50
  */
49
51
  readonly sessionStore?: SessionStoreBinding;
52
+ /**
53
+ * Optional. When present, `whoami` reports the reviewer role of a
54
+ * caller whose token carries none from the roster, as the approvals
55
+ * routes resolve it.
56
+ */
57
+ readonly reviewerBinding?: ReviewerBinding;
50
58
  }
51
59
 
52
60
  export function identityRouter(options: IdentityRouterOptions = {}): Hono<AppEnv> {
53
- const { directory, sessionStore } = options;
61
+ const { directory, sessionStore, reviewerBinding } = options;
54
62
  const r = new Hono<AppEnv>();
55
63
 
56
64
  // ---------- GET / whoami ----------
@@ -62,7 +70,7 @@ export function identityRouter(options: IdentityRouterOptions = {}): Hono<AppEnv
62
70
  const userId = c.get('userId') as UserId | undefined;
63
71
  const providerId = c.get('providerId') as string | undefined;
64
72
  const scopes = (c.get('scopes') as readonly string[] | undefined) ?? [];
65
- const reviewerRole = c.get('reviewerRole') as string | undefined;
73
+ const reviewerRole = await callerReviewerRole(c, reviewerBinding);
66
74
 
67
75
  const body: Record<string, unknown> = { tenantId };
68
76
  if (sessionId !== undefined) body.sessionId = sessionId;