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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/agent-binding.d.ts +8 -0
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +7 -1
  5. package/dist/app.js.map +1 -1
  6. package/dist/deploy-versions.d.ts +2 -0
  7. package/dist/deploy-versions.d.ts.map +1 -1
  8. package/dist/deploy-versions.js +1 -1
  9. package/dist/deploy-versions.js.map +1 -1
  10. package/dist/derive-agent-version.d.ts +9 -1
  11. package/dist/derive-agent-version.d.ts.map +1 -1
  12. package/dist/derive-agent-version.js +11 -1
  13. package/dist/derive-agent-version.js.map +1 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +1 -0
  16. package/dist/errors.js.map +1 -1
  17. package/dist/eval-run-binding.d.ts +7 -0
  18. package/dist/eval-run-binding.d.ts.map +1 -1
  19. package/dist/eval-run-binding.js.map +1 -1
  20. package/dist/eval-run-dispatcher.d.ts +3 -0
  21. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  22. package/dist/eval-run-dispatcher.js.map +1 -1
  23. package/dist/flow-binding.d.ts +8 -0
  24. package/dist/flow-binding.d.ts.map +1 -1
  25. package/dist/guardrail-binding.d.ts +8 -0
  26. package/dist/guardrail-binding.d.ts.map +1 -1
  27. package/dist/handler-binding.d.ts +3 -0
  28. package/dist/handler-binding.d.ts.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/judged-dispatcher.d.ts +4 -0
  33. package/dist/judged-dispatcher.d.ts.map +1 -1
  34. package/dist/judged-dispatcher.js +14 -3
  35. package/dist/judged-dispatcher.js.map +1 -1
  36. package/dist/openapi/operations.d.ts.map +1 -1
  37. package/dist/openapi/operations.js +21 -6
  38. package/dist/openapi/operations.js.map +1 -1
  39. package/dist/openapi/schemas.d.ts +6 -0
  40. package/dist/openapi/schemas.d.ts.map +1 -1
  41. package/dist/openapi/schemas.js +341 -2
  42. package/dist/openapi/schemas.js.map +1 -1
  43. package/dist/registry-read-only.d.ts +32 -0
  44. package/dist/registry-read-only.d.ts.map +1 -0
  45. package/dist/registry-read-only.js +22 -0
  46. package/dist/registry-read-only.js.map +1 -0
  47. package/dist/routes/agents.d.ts.map +1 -1
  48. package/dist/routes/agents.js +7 -1
  49. package/dist/routes/agents.js.map +1 -1
  50. package/dist/routes/blocks.d.ts.map +1 -1
  51. package/dist/routes/blocks.js +37 -12
  52. package/dist/routes/blocks.js.map +1 -1
  53. package/dist/routes/deployments.d.ts.map +1 -1
  54. package/dist/routes/deployments.js +16 -0
  55. package/dist/routes/deployments.js.map +1 -1
  56. package/dist/routes/eval-comparison.d.ts +3 -2
  57. package/dist/routes/eval-comparison.d.ts.map +1 -1
  58. package/dist/routes/eval-comparison.js +39 -3
  59. package/dist/routes/eval-comparison.js.map +1 -1
  60. package/dist/routes/eval-runs.d.ts +7 -1
  61. package/dist/routes/eval-runs.d.ts.map +1 -1
  62. package/dist/routes/eval-runs.js +34 -3
  63. package/dist/routes/eval-runs.js.map +1 -1
  64. package/dist/routes/eval-versions.d.ts +25 -0
  65. package/dist/routes/eval-versions.d.ts.map +1 -0
  66. package/dist/routes/eval-versions.js +66 -0
  67. package/dist/routes/eval-versions.js.map +1 -0
  68. package/dist/routes/flows.d.ts.map +1 -1
  69. package/dist/routes/flows.js +4 -0
  70. package/dist/routes/flows.js.map +1 -1
  71. package/dist/routes/guardrails.d.ts.map +1 -1
  72. package/dist/routes/guardrails.js +4 -0
  73. package/dist/routes/guardrails.js.map +1 -1
  74. package/dist/routes/hierarchy-errors.d.ts +10 -0
  75. package/dist/routes/hierarchy-errors.d.ts.map +1 -1
  76. package/dist/routes/hierarchy-errors.js +8 -0
  77. package/dist/routes/hierarchy-errors.js.map +1 -1
  78. package/dist/routes/projects.d.ts.map +1 -1
  79. package/dist/routes/projects.js +13 -7
  80. package/dist/routes/projects.js.map +1 -1
  81. package/dist/routes/runs.js +1 -0
  82. package/dist/routes/runs.js.map +1 -1
  83. package/dist/routes/teams.d.ts.map +1 -1
  84. package/dist/routes/teams.js +12 -6
  85. package/dist/routes/teams.js.map +1 -1
  86. package/dist/routes/tools.d.ts.map +1 -1
  87. package/dist/routes/tools.js +4 -0
  88. package/dist/routes/tools.js.map +1 -1
  89. package/dist/tool-binding.d.ts +8 -0
  90. package/dist/tool-binding.d.ts.map +1 -1
  91. package/openapi.json +729 -17
  92. package/package.json +21 -21
  93. package/src/agent-binding.ts +9 -0
  94. package/src/app.ts +10 -1
  95. package/src/deploy-versions.ts +1 -1
  96. package/src/derive-agent-version.ts +12 -1
  97. package/src/errors.ts +1 -0
  98. package/src/eval-run-binding.ts +7 -0
  99. package/src/eval-run-dispatcher.ts +3 -0
  100. package/src/flow-binding.ts +9 -0
  101. package/src/guardrail-binding.ts +9 -0
  102. package/src/handler-binding.ts +3 -0
  103. package/src/index.ts +1 -0
  104. package/src/judged-dispatcher.ts +27 -4
  105. package/src/openapi/operations.ts +56 -6
  106. package/src/openapi/schemas.ts +361 -2
  107. package/src/registry-read-only.ts +43 -0
  108. package/src/routes/agents.ts +10 -1
  109. package/src/routes/blocks.ts +39 -14
  110. package/src/routes/deployments.ts +25 -0
  111. package/src/routes/eval-comparison.ts +37 -3
  112. package/src/routes/eval-runs.ts +46 -3
  113. package/src/routes/eval-versions.ts +110 -0
  114. package/src/routes/flows.ts +7 -0
  115. package/src/routes/guardrails.ts +7 -0
  116. package/src/routes/hierarchy-errors.ts +9 -0
  117. package/src/routes/projects.ts +18 -7
  118. package/src/routes/runs.ts +1 -0
  119. package/src/routes/teams.ts +14 -6
  120. package/src/routes/tools.ts +7 -0
  121. package/src/tool-binding.ts +9 -0
@@ -2,6 +2,7 @@
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
4
  import type { AgentId } from '@kindgi/agents';
5
+ import type { FlowVersionOverrides } from '@kindgi/flow';
5
6
  import type { ProjectId, Semver } from '@kindgi/types';
6
7
 
7
8
  import type { EvalBaseline, EvalComparison } from '../eval-run-binding.js';
@@ -49,6 +50,34 @@ function parseBaseline(raw: unknown): EvalBaseline | string {
49
50
  return { agentId: o.agentId as AgentId, version: o.version as Semver };
50
51
  }
51
52
 
53
+ /** `{ id: version }` with non-empty strings; an error message otherwise. */
54
+ function versionMap(raw: unknown, name: string): Readonly<Record<string, string>> | string {
55
+ const o = obj(raw);
56
+ const valid =
57
+ o !== undefined &&
58
+ Object.entries(o).every(([id, v]) => id !== '' && typeof v === 'string' && v !== '');
59
+ return valid
60
+ ? (o as Record<string, string>)
61
+ : `\`${name}\` must be an object of { id: version } with non-empty strings`;
62
+ }
63
+
64
+ /** `versions: { agents?, tools? }`; `undefined` when it names nothing. */
65
+ function parseVersions(raw: unknown): FlowVersionOverrides | undefined | string {
66
+ const o = obj(raw);
67
+ if (o === undefined) return '`versions` must be an object: { agents?, tools? }';
68
+ const extra = Object.keys(o).filter((key) => key !== 'agents' && key !== 'tools');
69
+ if (extra.length > 0) return `\`versions\` takes \`agents\` and \`tools\`, not \`${extra[0]}\``;
70
+ const agents = o.agents === undefined ? {} : versionMap(o.agents, 'versions.agents');
71
+ if (typeof agents === 'string') return agents;
72
+ const tools = o.tools === undefined ? {} : versionMap(o.tools, 'versions.tools');
73
+ if (typeof tools === 'string') return tools;
74
+ if (Object.keys(agents).length === 0 && Object.keys(tools).length === 0) return undefined;
75
+ return {
76
+ ...(Object.keys(agents).length > 0 && { agents }),
77
+ ...(Object.keys(tools).length > 0 && { tools }),
78
+ };
79
+ }
80
+
52
81
  function integerIn(raw: unknown, name: string, max: number): number | string {
53
82
  return typeof raw === 'number' && Number.isInteger(raw) && raw >= 1 && raw <= max
54
83
  ? raw
@@ -57,8 +86,9 @@ function integerIn(raw: unknown, name: string, max: number): number | string {
57
86
 
58
87
  /**
59
88
  * A comparison eval run's settings from the start body: `baseline`,
60
- * `reads`, `repetitions` and `k`. `undefined` when none is given (the
61
- * defaults apply); an error message for a bad one.
89
+ * `reads`, `repetitions`, `k` and a flow candidate's `versions`.
90
+ * `undefined` when none is given (the defaults apply); an error message
91
+ * for a bad one.
62
92
  */
63
93
  export function parseComparison(
64
94
  b: Readonly<Record<string, unknown>>,
@@ -66,11 +96,14 @@ export function parseComparison(
66
96
  | { readonly kind: 'ok'; readonly value?: EvalComparison }
67
97
  | { readonly kind: 'err'; readonly message: string } {
68
98
  const { baseline, reads, repetitions, k } = b;
99
+ const versions = b.versions === undefined ? undefined : parseVersions(b.versions);
100
+ if (typeof versions === 'string') return { kind: 'err', message: versions };
69
101
  if (
70
102
  baseline === undefined &&
71
103
  reads === undefined &&
72
104
  repetitions === undefined &&
73
- k === undefined
105
+ k === undefined &&
106
+ versions === undefined
74
107
  ) {
75
108
  return { kind: 'ok' };
76
109
  }
@@ -96,6 +129,7 @@ export function parseComparison(
96
129
  reads: reads ?? DEFAULT_COMPARISON.reads,
97
130
  repetitions: reps,
98
131
  k: topK,
132
+ ...(versions !== undefined && { versions }),
99
133
  },
100
134
  };
101
135
  }
@@ -17,8 +17,10 @@ import {
17
17
  type EvalRunStatus,
18
18
  type FlowRef,
19
19
  } from '../eval-run-binding.js';
20
+ import { VERSIONS_NEED_A_FLOW } from '../judged-dispatcher.js';
20
21
  import type { AppEnv } from '../types.js';
21
22
  import { parseComparison } from './eval-comparison.js';
23
+ import { type FlowVersionsCheck, checkFlowVersions } from './eval-versions.js';
22
24
  import { clampLimit } from './pagination.js';
23
25
  import { parseScopeParams } from './scope-params.js';
24
26
  import { formatSseFrame } from './sse.js';
@@ -47,16 +49,49 @@ export interface EvalRunsRouters {
47
49
  readonly readback: Hono<AppEnv>;
48
50
  }
49
51
 
50
- export function evalRunsRouters(binding: EvalRunBinding): EvalRunsRouters {
52
+ /**
53
+ * `versionsCheck`: where a flow candidate's `versions` are checked
54
+ * against the flow version at start (without it, they're applied as
55
+ * given).
56
+ */
57
+ export function evalRunsRouters(
58
+ binding: EvalRunBinding,
59
+ versionsCheck?: FlowVersionsCheck,
60
+ ): EvalRunsRouters {
51
61
  return {
52
- start: startRouter(binding),
62
+ start: startRouter(binding, versionsCheck),
53
63
  readback: readbackRouter(binding),
54
64
  };
55
65
  }
56
66
 
57
67
  // ---------- POST /v1/eval-suites/:suiteId/runs ----------
58
68
 
59
- function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
69
+ /** The `validation-failed` error for a flow candidate's `versions` that don't fit the flow; `undefined` when they do. */
70
+ async function versionsRefusal(
71
+ check: FlowVersionsCheck | undefined,
72
+ tenantId: TenantId,
73
+ start: ParsedStartBody,
74
+ ) {
75
+ const versions = start.comparison?.versions;
76
+ const flowRef = start.flowRef;
77
+ if (check === undefined || versions === undefined || flowRef?.version === undefined) {
78
+ return undefined;
79
+ }
80
+ const issues = await checkFlowVersions(
81
+ check,
82
+ tenantId,
83
+ { flowId: flowRef.flowId as unknown as string, version: flowRef.version },
84
+ versions,
85
+ );
86
+ if (issues.length === 0) return undefined;
87
+ return {
88
+ code: 'validation-failed' as const,
89
+ message: `The versions don't fit flow ${flowRef.flowId as unknown as string} ${flowRef.version} (${issues.length} issue${issues.length === 1 ? '' : 's'})`,
90
+ issues: issues as unknown as Record<string, unknown>[],
91
+ };
92
+ }
93
+
94
+ function startRouter(binding: EvalRunBinding, versionsCheck?: FlowVersionsCheck): Hono<AppEnv> {
60
95
  const r = new Hono<AppEnv>();
61
96
 
62
97
  r.post('/:suiteId/runs', async (c) => {
@@ -84,6 +119,11 @@ function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
84
119
  c.status(statusFor(parsed.error.code) as never);
85
120
  return c.json(toWireError(parsed.error, requestId));
86
121
  }
122
+ const refusal = await versionsRefusal(versionsCheck, tenantId, parsed.value);
123
+ if (refusal !== undefined) {
124
+ c.status(statusFor(refusal.code) as never);
125
+ return c.json(toWireError(refusal, requestId));
126
+ }
87
127
 
88
128
  const outcome = await binding.start({
89
129
  tenantId,
@@ -627,6 +667,9 @@ function parseStartBody(
627
667
  if (comparison.kind === 'err') {
628
668
  return { kind: 'err', error: { code: 'bad-input', message: comparison.message } };
629
669
  }
670
+ if (comparison.value?.versions !== undefined && agentRef !== undefined) {
671
+ return { kind: 'err', error: { code: 'bad-input', message: VERSIONS_NEED_A_FLOW } };
672
+ }
630
673
 
631
674
  return {
632
675
  kind: 'ok',
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * A flow candidate's `versions`, checked against the flow version when
6
+ * the comparison starts: each id must be an agent or tool the flow uses,
7
+ * at a published version. Checked once, here, before any run exists: the
8
+ * runs apply them as given, on resume too.
9
+ */
10
+
11
+ import type { AgentId } from '@kindgi/agents';
12
+ import { type FlowVersionOverrides, overridableRefs } from '@kindgi/flow';
13
+ import type { FlowId, Semver, TenantId, ToolId } from '@kindgi/types';
14
+
15
+ import type { AgentRegistryBinding } from '../agent-binding.js';
16
+ import type { FlowRegistryBinding } from '../flow-binding.js';
17
+ import type { ToolRegistryBinding } from '../tool-binding.js';
18
+
19
+ /** Where the check reads the flow version and the agents' and tools' versions. */
20
+ export interface FlowVersionsCheck {
21
+ readonly flows: FlowRegistryBinding;
22
+ readonly agents?: AgentRegistryBinding;
23
+ readonly tools?: ToolRegistryBinding;
24
+ }
25
+
26
+ export interface VersionsIssue {
27
+ readonly path: string;
28
+ readonly message: string;
29
+ }
30
+
31
+ /** A JSON Pointer token: `~` and `/` escaped. */
32
+ const token = (id: string): string => id.replaceAll('~', '~0').replaceAll('/', '~1');
33
+
34
+ async function agentIssues(
35
+ check: FlowVersionsCheck,
36
+ tenantId: TenantId,
37
+ label: string,
38
+ used: ReadonlySet<string>,
39
+ overrides: Readonly<Record<string, string>>,
40
+ ): Promise<VersionsIssue[]> {
41
+ const issues: VersionsIssue[] = [];
42
+ for (const [id, version] of Object.entries(overrides)) {
43
+ const path = `/versions/agents/${token(id)}`;
44
+ if (!used.has(id)) {
45
+ issues.push({ path, message: `flow ${label} doesn't use agent ${id}` });
46
+ continue;
47
+ }
48
+ const found = await check.agents?.getVersion({
49
+ tenantId,
50
+ agentId: id as AgentId,
51
+ version: version as Semver,
52
+ });
53
+ if (found === null) issues.push({ path, message: `agent ${id} has no version ${version}` });
54
+ else if (found?.unregisteredAt !== undefined) {
55
+ issues.push({ path, message: `agent ${id} ${version} is unregistered` });
56
+ }
57
+ }
58
+ return issues;
59
+ }
60
+
61
+ async function toolIssues(
62
+ check: FlowVersionsCheck,
63
+ tenantId: TenantId,
64
+ label: string,
65
+ used: ReadonlySet<string>,
66
+ overrides: Readonly<Record<string, string>>,
67
+ ): Promise<VersionsIssue[]> {
68
+ const issues: VersionsIssue[] = [];
69
+ for (const [id, version] of Object.entries(overrides)) {
70
+ const path = `/versions/tools/${token(id)}`;
71
+ if (!used.has(id)) {
72
+ issues.push({ path, message: `flow ${label} doesn't use tool ${id}` });
73
+ continue;
74
+ }
75
+ const found = await check.tools?.getVersion({
76
+ tenantId,
77
+ toolId: id as ToolId,
78
+ version: version as Semver,
79
+ });
80
+ if (found === null) issues.push({ path, message: `tool ${id} has no version ${version}` });
81
+ }
82
+ return issues;
83
+ }
84
+
85
+ /**
86
+ * What's wrong with a flow candidate's `versions`, one issue per id; none
87
+ * when they fit. Without an agent or tool registry, those versions'
88
+ * existence isn't checked (the runs refuse an unpublished one).
89
+ */
90
+ export async function checkFlowVersions(
91
+ check: FlowVersionsCheck,
92
+ tenantId: TenantId,
93
+ flowRef: { readonly flowId: string; readonly version: string },
94
+ versions: FlowVersionOverrides,
95
+ ): Promise<readonly VersionsIssue[]> {
96
+ const label = `${flowRef.flowId} ${flowRef.version}`;
97
+ const flow = await check.flows.getVersion({
98
+ tenantId,
99
+ flowId: flowRef.flowId as FlowId,
100
+ version: flowRef.version,
101
+ });
102
+ if (flow === null) {
103
+ return [{ path: '/flowRef/version', message: `flow ${label} isn't registered` }];
104
+ }
105
+ const refs = overridableRefs(flow);
106
+ return [
107
+ ...(await agentIssues(check, tenantId, label, new Set(refs.agents), versions.agents ?? {})),
108
+ ...(await toolIssues(check, tenantId, label, new Set(refs.tools), versions.tools ?? {})),
109
+ ];
110
+ }
@@ -13,6 +13,7 @@ import { statusFor, toWireError } from '../errors.js';
13
13
  import type { FlowRegistryBinding, FlowVersionRecord } from '../flow-binding.js';
14
14
  import { resolveFlowPins } from '../flow-pins.js';
15
15
  import type { Authorizer } from '../middleware/authorize.js';
16
+ import { refuseWritesWhenReadOnly } from '../registry-read-only.js';
16
17
  import type { ToolRegistryBinding } from '../tool-binding.js';
17
18
  import type { AppEnv } from '../types.js';
18
19
  import { clampLimit } from './pagination.js';
@@ -48,6 +49,12 @@ export function flowsRouter(
48
49
  pinning?: { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding },
49
50
  ): Hono<AppEnv> {
50
51
  const r = new Hono<AppEnv>();
52
+ // A read-only registry (under `kindgi dev`, the pack's files) refuses
53
+ // every write before anything else runs.
54
+ r.use(
55
+ '*',
56
+ refuseWritesWhenReadOnly(() => binding.readOnly),
57
+ );
51
58
 
52
59
  // Authorization — mirrors agents 1:1. Check the resource directly;
53
60
  // permissions cascade from the parent project because publish writes
@@ -10,6 +10,7 @@ import type { Cursor, GuardrailId, ProjectId, TenantId, UserId } from '@kindgi/t
10
10
  import { statusFor, toWireError } from '../errors.js';
11
11
  import type { GuardrailRegistryBinding } from '../guardrail-binding.js';
12
12
  import type { Authorizer } from '../middleware/authorize.js';
13
+ import { refuseWritesWhenReadOnly } from '../registry-read-only.js';
13
14
  import type { AppEnv } from '../types.js';
14
15
  import { clampLimit } from './pagination.js';
15
16
  import { parseScopeParams } from './scope-params.js';
@@ -47,6 +48,12 @@ export function guardrailsRouter(
47
48
  onWrite?: GuardrailWriteHook,
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 `guardrail#parent@project`
52
59
  // written in-tx by register; direct check via `ref('guardrail', id)`.
@@ -42,6 +42,15 @@ export function orgDeleteSlugConflictError(slugs: readonly string[]) {
42
42
  } as const;
43
43
  }
44
44
 
45
+ /**
46
+ * `404 org-not-found`: the org a project or team would be in isn't the
47
+ * tenant's (it never existed, or it was deleted). The code and message
48
+ * `GET /v1/orgs/{id}` answers (T220).
49
+ */
50
+ export function orgNotFoundError(orgId: string) {
51
+ return { code: 'org-not-found', message: `No org with id "${orgId}"`, orgId } as const;
52
+ }
53
+
45
54
  /** `409 project-default-already-exists`: the tenant has a Default project. */
46
55
  export function projectDefaultAlreadyExistsError() {
47
56
  return {
@@ -19,7 +19,11 @@ import type { Cursor, OrgId, ProjectId, TenantId, UserId } from '@kindgi/types';
19
19
  import { statusFor, toWireError } from '../errors.js';
20
20
  import type { Authorizer } from '../middleware/authorize.js';
21
21
  import type { AppEnv } from '../types.js';
22
- import { projectDefaultAlreadyExistsError, slugConflictError } from './hierarchy-errors.js';
22
+ import {
23
+ orgNotFoundError,
24
+ projectDefaultAlreadyExistsError,
25
+ slugConflictError,
26
+ } from './hierarchy-errors.js';
23
27
  import { clampLimit } from './pagination.js';
24
28
 
25
29
  /**
@@ -247,9 +251,11 @@ export function projectsRouter(
247
251
  const error =
248
252
  result.error.code === 'slug-conflict'
249
253
  ? slugConflictError('project', spec.slug)
250
- : result.error.code === 'default-conflict'
251
- ? projectDefaultAlreadyExistsError()
252
- : { code: 'internal-server-error', message: result.error.message };
254
+ : result.error.code === 'org-not-found'
255
+ ? orgNotFoundError(result.error.orgId as unknown as string)
256
+ : result.error.code === 'default-conflict'
257
+ ? projectDefaultAlreadyExistsError()
258
+ : { code: 'internal-server-error', message: result.error.message };
253
259
  c.status(statusFor(error.code) as never);
254
260
  return c.json(toWireError(error, requestId));
255
261
  }
@@ -263,7 +269,9 @@ export function projectsRouter(
263
269
  const error =
264
270
  outcome.kind === 'slug-conflict'
265
271
  ? slugConflictError('project', outcome.slug)
266
- : projectDefaultAlreadyExistsError();
272
+ : outcome.kind === 'org-not-found'
273
+ ? orgNotFoundError(outcome.orgId as unknown as string)
274
+ : projectDefaultAlreadyExistsError();
267
275
  c.status(statusFor(error.code) as never);
268
276
  return c.json(toWireError(error, requestId));
269
277
  }
@@ -596,8 +604,11 @@ export function projectsRouter(
596
604
  ),
597
605
  );
598
606
  }
599
- if (outcome.kind === 'slug-conflict') {
600
- const error = slugConflictError('project', outcome.slug);
607
+ if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
608
+ const error =
609
+ outcome.kind === 'slug-conflict'
610
+ ? slugConflictError('project', outcome.slug)
611
+ : orgNotFoundError(outcome.orgId as unknown as string);
601
612
  c.status(statusFor(error.code) as never);
602
613
  return c.json(toWireError(error, requestId));
603
614
  }
@@ -685,6 +685,7 @@ function serializeRun(
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 }),
688
689
  };
689
690
  }
690
691
 
@@ -19,7 +19,7 @@ import type { Cursor, OrgId, TeamId, 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 { slugConflictError } from './hierarchy-errors.js';
22
+ import { orgNotFoundError, slugConflictError } from './hierarchy-errors.js';
23
23
  import { clampLimit } from './pagination.js';
24
24
 
25
25
  /**
@@ -201,7 +201,9 @@ export function teamsRouter(
201
201
  const error =
202
202
  result.error.code === 'slug-conflict'
203
203
  ? slugConflictError('team', spec.slug)
204
- : { code: 'internal-server-error', message: result.error.message };
204
+ : result.error.code === 'org-not-found'
205
+ ? orgNotFoundError(result.error.orgId as unknown as string)
206
+ : { code: 'internal-server-error', message: result.error.message };
205
207
  c.status(statusFor(error.code) as never);
206
208
  return c.json(toWireError(error, requestId));
207
209
  }
@@ -209,8 +211,11 @@ export function teamsRouter(
209
211
  return c.json({ id: result.value.teamId as unknown as string });
210
212
  }
211
213
  const outcome = await binding.create(tenantId, spec);
212
- if (outcome.kind === 'slug-conflict') {
213
- const error = slugConflictError('team', outcome.slug);
214
+ if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
215
+ const error =
216
+ outcome.kind === 'slug-conflict'
217
+ ? slugConflictError('team', outcome.slug)
218
+ : orgNotFoundError(outcome.orgId as unknown as string);
214
219
  c.status(statusFor(error.code) as never);
215
220
  return c.json(toWireError(error, requestId));
216
221
  }
@@ -542,8 +547,11 @@ export function teamsRouter(
542
547
  ),
543
548
  );
544
549
  }
545
- if (outcome.kind === 'slug-conflict') {
546
- const error = slugConflictError('team', outcome.slug);
550
+ if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
551
+ const error =
552
+ outcome.kind === 'slug-conflict'
553
+ ? slugConflictError('team', outcome.slug)
554
+ : orgNotFoundError(outcome.orgId as unknown as string);
547
555
  c.status(statusFor(error.code) as never);
548
556
  return c.json(toWireError(error, requestId));
549
557
  }
@@ -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)`.
@@ -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