@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.
- package/dist/agent-binding.d.ts +8 -0
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +7 -1
- package/dist/app.js.map +1 -1
- package/dist/deploy-versions.d.ts +2 -0
- package/dist/deploy-versions.d.ts.map +1 -1
- package/dist/deploy-versions.js +1 -1
- package/dist/deploy-versions.js.map +1 -1
- package/dist/derive-agent-version.d.ts +9 -1
- package/dist/derive-agent-version.d.ts.map +1 -1
- package/dist/derive-agent-version.js +11 -1
- package/dist/derive-agent-version.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-run-binding.d.ts +7 -0
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.js.map +1 -1
- package/dist/eval-run-dispatcher.d.ts +3 -0
- package/dist/eval-run-dispatcher.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.js.map +1 -1
- package/dist/flow-binding.d.ts +8 -0
- package/dist/flow-binding.d.ts.map +1 -1
- package/dist/guardrail-binding.d.ts +8 -0
- package/dist/guardrail-binding.d.ts.map +1 -1
- package/dist/handler-binding.d.ts +3 -0
- package/dist/handler-binding.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +4 -0
- package/dist/judged-dispatcher.d.ts.map +1 -1
- package/dist/judged-dispatcher.js +14 -3
- package/dist/judged-dispatcher.js.map +1 -1
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +21 -6
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +6 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +341 -2
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/registry-read-only.d.ts +32 -0
- package/dist/registry-read-only.d.ts.map +1 -0
- package/dist/registry-read-only.js +22 -0
- package/dist/registry-read-only.js.map +1 -0
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +7 -1
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/blocks.d.ts.map +1 -1
- package/dist/routes/blocks.js +37 -12
- package/dist/routes/blocks.js.map +1 -1
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +16 -0
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +3 -2
- package/dist/routes/eval-comparison.d.ts.map +1 -1
- package/dist/routes/eval-comparison.js +39 -3
- package/dist/routes/eval-comparison.js.map +1 -1
- package/dist/routes/eval-runs.d.ts +7 -1
- package/dist/routes/eval-runs.d.ts.map +1 -1
- package/dist/routes/eval-runs.js +34 -3
- package/dist/routes/eval-runs.js.map +1 -1
- package/dist/routes/eval-versions.d.ts +25 -0
- package/dist/routes/eval-versions.d.ts.map +1 -0
- package/dist/routes/eval-versions.js +66 -0
- package/dist/routes/eval-versions.js.map +1 -0
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +4 -0
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/guardrails.d.ts.map +1 -1
- package/dist/routes/guardrails.js +4 -0
- package/dist/routes/guardrails.js.map +1 -1
- package/dist/routes/hierarchy-errors.d.ts +10 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -1
- package/dist/routes/hierarchy-errors.js +8 -0
- package/dist/routes/hierarchy-errors.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +13 -7
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/runs.js +1 -0
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +12 -6
- package/dist/routes/teams.js.map +1 -1
- package/dist/routes/tools.d.ts.map +1 -1
- package/dist/routes/tools.js +4 -0
- package/dist/routes/tools.js.map +1 -1
- package/dist/tool-binding.d.ts +8 -0
- package/dist/tool-binding.d.ts.map +1 -1
- package/openapi.json +729 -17
- package/package.json +21 -21
- package/src/agent-binding.ts +9 -0
- package/src/app.ts +10 -1
- package/src/deploy-versions.ts +1 -1
- package/src/derive-agent-version.ts +12 -1
- package/src/errors.ts +1 -0
- package/src/eval-run-binding.ts +7 -0
- package/src/eval-run-dispatcher.ts +3 -0
- package/src/flow-binding.ts +9 -0
- package/src/guardrail-binding.ts +9 -0
- package/src/handler-binding.ts +3 -0
- package/src/index.ts +1 -0
- package/src/judged-dispatcher.ts +27 -4
- package/src/openapi/operations.ts +56 -6
- package/src/openapi/schemas.ts +361 -2
- package/src/registry-read-only.ts +43 -0
- package/src/routes/agents.ts +10 -1
- package/src/routes/blocks.ts +39 -14
- package/src/routes/deployments.ts +25 -0
- package/src/routes/eval-comparison.ts +37 -3
- package/src/routes/eval-runs.ts +46 -3
- package/src/routes/eval-versions.ts +110 -0
- package/src/routes/flows.ts +7 -0
- package/src/routes/guardrails.ts +7 -0
- package/src/routes/hierarchy-errors.ts +9 -0
- package/src/routes/projects.ts +18 -7
- package/src/routes/runs.ts +1 -0
- package/src/routes/teams.ts +14 -6
- package/src/routes/tools.ts +7 -0
- 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
|
|
61
|
-
* defaults apply); an error message
|
|
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
|
}
|
package/src/routes/eval-runs.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/routes/flows.ts
CHANGED
|
@@ -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
|
package/src/routes/guardrails.ts
CHANGED
|
@@ -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 {
|
package/src/routes/projects.ts
CHANGED
|
@@ -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 {
|
|
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 === '
|
|
251
|
-
?
|
|
252
|
-
:
|
|
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
|
-
:
|
|
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 =
|
|
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
|
}
|
package/src/routes/runs.ts
CHANGED
|
@@ -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
|
|
package/src/routes/teams.ts
CHANGED
|
@@ -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
|
-
:
|
|
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 =
|
|
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 =
|
|
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
|
}
|
package/src/routes/tools.ts
CHANGED
|
@@ -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)`.
|
package/src/tool-binding.ts
CHANGED
|
@@ -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
|