@kindgi/api 0.1.3 → 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 +26 -4
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts +48 -0
- package/dist/agent-pins.d.ts.map +1 -0
- package/dist/agent-pins.js +102 -0
- package/dist/agent-pins.js.map +1 -0
- package/dist/app.d.ts +22 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +27 -3
- package/dist/app.js.map +1 -1
- package/dist/block-binding.d.ts +132 -0
- package/dist/block-binding.d.ts.map +1 -0
- package/dist/block-binding.js +4 -0
- package/dist/block-binding.js.map +1 -0
- package/dist/block-pins.d.ts +22 -0
- package/dist/block-pins.d.ts.map +1 -0
- package/dist/block-pins.js +112 -0
- package/dist/block-pins.js.map +1 -0
- package/dist/cost-binding.d.ts +20 -1
- package/dist/cost-binding.d.ts.map +1 -1
- package/dist/cost-binding.js +4 -0
- package/dist/cost-binding.js.map +1 -1
- package/dist/deploy-versions.d.ts +60 -0
- package/dist/deploy-versions.d.ts.map +1 -0
- package/dist/deploy-versions.js +91 -0
- package/dist/deploy-versions.js.map +1 -0
- package/dist/deployment-binding.d.ts +24 -3
- package/dist/deployment-binding.d.ts.map +1 -1
- package/dist/derive-agent-version.d.ts +77 -0
- package/dist/derive-agent-version.d.ts.map +1 -0
- package/dist/derive-agent-version.js +149 -0
- package/dist/derive-agent-version.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +65 -0
- package/dist/eval-case-binding.d.ts.map +1 -0
- package/dist/eval-case-binding.js +4 -0
- package/dist/eval-case-binding.js.map +1 -0
- package/dist/eval-run-binding.d.ts +37 -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 +41 -3
- package/dist/eval-run-dispatcher.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.js +21 -15
- package/dist/eval-run-dispatcher.js.map +1 -1
- package/dist/eval-suite-binding.d.ts +1 -1
- package/dist/eval-suite-binding.d.ts.map +1 -1
- package/dist/eval-suite-binding.js +2 -0
- package/dist/eval-suite-binding.js.map +1 -1
- package/dist/flow-binding.d.ts +18 -4
- package/dist/flow-binding.d.ts.map +1 -1
- package/dist/flow-pins.d.ts +36 -0
- package/dist/flow-pins.d.ts.map +1 -0
- package/dist/flow-pins.js +81 -0
- package/dist/flow-pins.js.map +1 -0
- 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 +18 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +138 -0
- package/dist/judged-dispatcher.d.ts.map +1 -0
- package/dist/judged-dispatcher.js +308 -0
- package/dist/judged-dispatcher.js.map +1 -0
- package/dist/judged-items.d.ts +86 -0
- package/dist/judged-items.d.ts.map +1 -0
- package/dist/judged-items.js +184 -0
- package/dist/judged-items.js.map +1 -0
- package/dist/judgment-binding.d.ts +316 -0
- package/dist/judgment-binding.d.ts.map +1 -0
- package/dist/judgment-binding.js +19 -0
- package/dist/judgment-binding.js.map +1 -0
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +4 -1
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +482 -13
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +46 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +1350 -175
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/provider-binding.d.ts +12 -7
- package/dist/provider-binding.d.ts.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 +9 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +181 -11
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/blocks.d.ts +19 -0
- package/dist/routes/blocks.d.ts.map +1 -0
- package/dist/routes/blocks.js +306 -0
- package/dist/routes/blocks.js.map +1 -0
- package/dist/routes/cost.d.ts.map +1 -1
- package/dist/routes/cost.js +47 -2
- package/dist/routes/cost.js.map +1 -1
- package/dist/routes/deployments.d.ts +3 -0
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +224 -55
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +15 -0
- package/dist/routes/eval-comparison.d.ts.map +1 -0
- package/dist/routes/eval-comparison.js +123 -0
- package/dist/routes/eval-comparison.js.map +1 -0
- 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 +42 -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 +13 -1
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +48 -3
- 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 +45 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -0
- package/dist/routes/hierarchy-errors.js +47 -0
- package/dist/routes/hierarchy-errors.js.map +1 -0
- package/dist/routes/judged-suites.d.ts +20 -0
- package/dist/routes/judged-suites.d.ts.map +1 -0
- package/dist/routes/judged-suites.js +272 -0
- package/dist/routes/judged-suites.js.map +1 -0
- package/dist/routes/judgment-context.d.ts +22 -0
- package/dist/routes/judgment-context.d.ts.map +1 -0
- package/dist/routes/judgment-context.js +88 -0
- package/dist/routes/judgment-context.js.map +1 -0
- package/dist/routes/judgment-flow-context.d.ts +32 -0
- package/dist/routes/judgment-flow-context.d.ts.map +1 -0
- package/dist/routes/judgment-flow-context.js +195 -0
- package/dist/routes/judgment-flow-context.js.map +1 -0
- package/dist/routes/judgments.d.ts +41 -0
- package/dist/routes/judgments.d.ts.map +1 -0
- package/dist/routes/judgments.js +566 -0
- package/dist/routes/judgments.js.map +1 -0
- package/dist/routes/orgs.d.ts +5 -2
- package/dist/routes/orgs.d.ts.map +1 -1
- package/dist/routes/orgs.js +38 -22
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +12 -1
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/projects.d.ts +10 -2
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +94 -80
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +6 -1
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/runs.js +25 -3
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/teams.d.ts +6 -2
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +83 -73
- 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 +14253 -10099
- package/package.json +21 -21
- package/src/agent-binding.ts +28 -4
- package/src/agent-pins.ts +147 -0
- package/src/app.ts +81 -3
- package/src/block-binding.ts +137 -0
- package/src/block-pins.ts +148 -0
- package/src/cost-binding.ts +21 -1
- package/src/deploy-versions.ts +157 -0
- package/src/deployment-binding.ts +27 -3
- package/src/derive-agent-version.ts +217 -0
- package/src/errors.ts +18 -0
- package/src/eval-case-binding.ts +71 -0
- package/src/eval-run-binding.ts +40 -0
- package/src/eval-run-dispatcher.ts +60 -16
- package/src/eval-suite-binding.ts +2 -0
- package/src/flow-binding.ts +20 -4
- package/src/flow-pins.ts +113 -0
- package/src/guardrail-binding.ts +9 -0
- package/src/handler-binding.ts +3 -0
- package/src/index.ts +86 -2
- package/src/judged-dispatcher.ts +530 -0
- package/src/judged-items.ts +263 -0
- package/src/judgment-binding.ts +349 -0
- package/src/openapi/generate.ts +7 -1
- package/src/openapi/operations.ts +579 -13
- package/src/openapi/schemas.ts +1408 -128
- package/src/provider-binding.ts +12 -7
- package/src/registry-read-only.ts +43 -0
- package/src/routes/agents.ts +252 -19
- package/src/routes/blocks.ts +387 -0
- package/src/routes/cost.ts +55 -1
- package/src/routes/deployments.ts +291 -56
- package/src/routes/eval-comparison.ts +135 -0
- package/src/routes/eval-runs.ts +57 -3
- package/src/routes/eval-versions.ts +110 -0
- package/src/routes/flows.ts +70 -5
- package/src/routes/guardrails.ts +7 -0
- package/src/routes/hierarchy-errors.ts +60 -0
- package/src/routes/judged-suites.ts +363 -0
- package/src/routes/judgment-context.ts +128 -0
- package/src/routes/judgment-flow-context.ts +245 -0
- package/src/routes/judgments.ts +743 -0
- package/src/routes/orgs.ts +44 -27
- package/src/routes/policies.ts +19 -0
- package/src/routes/projects.ts +118 -96
- package/src/routes/providers.ts +5 -0
- package/src/routes/runs.ts +29 -3
- package/src/routes/teams.ts +104 -90
- package/src/routes/tools.ts +7 -0
- package/src/tool-binding.ts +9 -0
package/src/routes/eval-runs.ts
CHANGED
|
@@ -10,13 +10,17 @@ 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,
|
|
16
17
|
type EvalRunStatus,
|
|
17
18
|
type FlowRef,
|
|
18
19
|
} from '../eval-run-binding.js';
|
|
20
|
+
import { VERSIONS_NEED_A_FLOW } from '../judged-dispatcher.js';
|
|
19
21
|
import type { AppEnv } from '../types.js';
|
|
22
|
+
import { parseComparison } from './eval-comparison.js';
|
|
23
|
+
import { type FlowVersionsCheck, checkFlowVersions } from './eval-versions.js';
|
|
20
24
|
import { clampLimit } from './pagination.js';
|
|
21
25
|
import { parseScopeParams } from './scope-params.js';
|
|
22
26
|
import { formatSseFrame } from './sse.js';
|
|
@@ -45,16 +49,49 @@ export interface EvalRunsRouters {
|
|
|
45
49
|
readonly readback: Hono<AppEnv>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
|
|
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 {
|
|
49
61
|
return {
|
|
50
|
-
start: startRouter(binding),
|
|
62
|
+
start: startRouter(binding, versionsCheck),
|
|
51
63
|
readback: readbackRouter(binding),
|
|
52
64
|
};
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
// ---------- POST /v1/eval-suites/:suiteId/runs ----------
|
|
56
68
|
|
|
57
|
-
|
|
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> {
|
|
58
95
|
const r = new Hono<AppEnv>();
|
|
59
96
|
|
|
60
97
|
r.post('/:suiteId/runs', async (c) => {
|
|
@@ -82,6 +119,11 @@ function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
|
|
|
82
119
|
c.status(statusFor(parsed.error.code) as never);
|
|
83
120
|
return c.json(toWireError(parsed.error, requestId));
|
|
84
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
|
+
}
|
|
85
127
|
|
|
86
128
|
const outcome = await binding.start({
|
|
87
129
|
tenantId,
|
|
@@ -93,6 +135,7 @@ function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
|
|
|
93
135
|
...(parsed.value.correlationId !== undefined && {
|
|
94
136
|
correlationId: parsed.value.correlationId,
|
|
95
137
|
}),
|
|
138
|
+
...(parsed.value.comparison !== undefined && { comparison: parsed.value.comparison }),
|
|
96
139
|
});
|
|
97
140
|
if (outcome.kind === 'suite-not-found') {
|
|
98
141
|
c.status(statusFor('eval-suite-not-found') as never);
|
|
@@ -454,6 +497,7 @@ function serializeEvalRun(run: EvalRun): Record<string, unknown> {
|
|
|
454
497
|
...(run.result !== undefined && { result: run.result }),
|
|
455
498
|
...(run.error !== undefined && { error: run.error }),
|
|
456
499
|
...(run.correlationId !== undefined && { correlationId: run.correlationId }),
|
|
500
|
+
...(run.comparison !== undefined && { comparison: run.comparison }),
|
|
457
501
|
};
|
|
458
502
|
}
|
|
459
503
|
|
|
@@ -486,6 +530,7 @@ interface ParsedStartBody {
|
|
|
486
530
|
readonly flowRef?: FlowRef;
|
|
487
531
|
readonly dryRun?: boolean;
|
|
488
532
|
readonly correlationId?: string;
|
|
533
|
+
readonly comparison?: EvalComparison;
|
|
489
534
|
}
|
|
490
535
|
|
|
491
536
|
function parseStartBody(
|
|
@@ -618,6 +663,14 @@ function parseStartBody(
|
|
|
618
663
|
};
|
|
619
664
|
}
|
|
620
665
|
|
|
666
|
+
const comparison = parseComparison(b);
|
|
667
|
+
if (comparison.kind === 'err') {
|
|
668
|
+
return { kind: 'err', error: { code: 'bad-input', message: comparison.message } };
|
|
669
|
+
}
|
|
670
|
+
if (comparison.value?.versions !== undefined && agentRef !== undefined) {
|
|
671
|
+
return { kind: 'err', error: { code: 'bad-input', message: VERSIONS_NEED_A_FLOW } };
|
|
672
|
+
}
|
|
673
|
+
|
|
621
674
|
return {
|
|
622
675
|
kind: 'ok',
|
|
623
676
|
value: {
|
|
@@ -626,6 +679,7 @@ function parseStartBody(
|
|
|
626
679
|
...(flowRef !== undefined && { flowRef }),
|
|
627
680
|
...(dryRun !== undefined && { dryRun }),
|
|
628
681
|
...(correlationId !== undefined && { correlationId: correlationId as string }),
|
|
682
|
+
...(comparison.value !== undefined && { comparison: comparison.value }),
|
|
629
683
|
},
|
|
630
684
|
};
|
|
631
685
|
}
|
|
@@ -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
|
@@ -4,12 +4,17 @@
|
|
|
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 { refuseWritesWhenReadOnly } from '../registry-read-only.js';
|
|
17
|
+
import type { ToolRegistryBinding } from '../tool-binding.js';
|
|
13
18
|
import type { AppEnv } from '../types.js';
|
|
14
19
|
import { clampLimit } from './pagination.js';
|
|
15
20
|
import { parseScopeParams } from './scope-params.js';
|
|
@@ -30,9 +35,26 @@ import { parseScopeParams } from './scope-params.js';
|
|
|
30
35
|
* Run initiation stays at `POST /v1/runs` (one primitive,
|
|
31
36
|
* discriminated subject). No
|
|
32
37
|
* `/v1/flows/:flowId/run` route is added here.
|
|
38
|
+
*
|
|
39
|
+
* With `pinning` (the tool and agent registries), a published version is
|
|
40
|
+
* pinned: each tool it runs, and each agent it runs at no named version,
|
|
41
|
+
* resolves once, at publish, to the latest version, which every run of
|
|
42
|
+
* that version uses (`pins`, see `resolveFlowPins`); one with no
|
|
43
|
+
* published version refuses the publish. Without it, versions carry no
|
|
44
|
+
* pins and bind the latest versions per run.
|
|
33
45
|
*/
|
|
34
|
-
export function flowsRouter(
|
|
46
|
+
export function flowsRouter(
|
|
47
|
+
binding: FlowRegistryBinding,
|
|
48
|
+
authorizer?: Authorizer,
|
|
49
|
+
pinning?: { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding },
|
|
50
|
+
): Hono<AppEnv> {
|
|
35
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
|
+
);
|
|
36
58
|
|
|
37
59
|
// Authorization — mirrors agents 1:1. Check the resource directly;
|
|
38
60
|
// permissions cascade from the parent project because publish writes
|
|
@@ -275,13 +297,23 @@ export function flowsRouter(binding: FlowRegistryBinding, authorizer?: Authorize
|
|
|
275
297
|
);
|
|
276
298
|
}
|
|
277
299
|
|
|
300
|
+
// Pin the version: every tool and unversioned agent resolves now,
|
|
301
|
+
// once, to the version its runs use. One with no published version
|
|
302
|
+
// refuses the publish rather than store a partly pinned version.
|
|
303
|
+
const pinned = await pinFlow(pinning, tenantId, loaded.value);
|
|
304
|
+
if (pinned.kind === 'unpinnable') {
|
|
305
|
+
c.status(statusFor('validation-failed') as never);
|
|
306
|
+
return c.json(toWireError(unpinnableFlow(loaded.value, pinned.issues), requestId));
|
|
307
|
+
}
|
|
308
|
+
const flow = pinned.flow;
|
|
309
|
+
|
|
278
310
|
const principal = c.get('principal') as Principal | undefined;
|
|
279
311
|
const creatorUserId =
|
|
280
312
|
principal?.actor.kind === 'user' ? (principal.actor.id as UserId) : undefined;
|
|
281
313
|
const outcome = await binding.publish({
|
|
282
314
|
tenantId,
|
|
283
315
|
projectId,
|
|
284
|
-
flow
|
|
316
|
+
flow,
|
|
285
317
|
enqueueTuples: (flowId) =>
|
|
286
318
|
tuplesForCreate({ kind: 'flow', id: flowId as FlowId, tenantId, projectId }, creatorUserId),
|
|
287
319
|
});
|
|
@@ -383,7 +415,36 @@ export function flowsRouter(binding: FlowRegistryBinding, authorizer?: Authorize
|
|
|
383
415
|
return r;
|
|
384
416
|
}
|
|
385
417
|
|
|
386
|
-
|
|
418
|
+
/** The flow with its pins, or the references with no published version; as it is without `pinning`. */
|
|
419
|
+
async function pinFlow(
|
|
420
|
+
pinning:
|
|
421
|
+
| { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding }
|
|
422
|
+
| undefined,
|
|
423
|
+
tenantId: TenantId,
|
|
424
|
+
flow: Flow,
|
|
425
|
+
): Promise<
|
|
426
|
+
| { readonly kind: 'ok'; readonly flow: Flow }
|
|
427
|
+
| { readonly kind: 'unpinnable'; readonly issues: readonly UnpinnableRef[] }
|
|
428
|
+
> {
|
|
429
|
+
if (pinning === undefined) return { kind: 'ok', flow };
|
|
430
|
+
const resolved = await resolveFlowPins(pinning.tools, pinning.agents, tenantId, flow);
|
|
431
|
+
if (resolved.kind === 'unpinnable') return resolved;
|
|
432
|
+
return {
|
|
433
|
+
kind: 'ok',
|
|
434
|
+
flow: { ...flow, pins: resolved.pins, pinsDigest: flowPinsDigest(resolved.pins) },
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The `validation-failed` error naming each tool or agent a flow runs that isn't published. */
|
|
439
|
+
function unpinnableFlow(flow: Flow, issues: readonly UnpinnableRef[]) {
|
|
440
|
+
return {
|
|
441
|
+
code: 'validation-failed' as const,
|
|
442
|
+
message: `Flow "${flow.id as unknown as string}" runs tools or agents that aren't published (${issues.length} issue${issues.length === 1 ? '' : 's'})`,
|
|
443
|
+
issues: issues as unknown as Record<string, unknown>[],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function serializeGraph(g: FlowVersionRecord): Record<string, unknown> {
|
|
387
448
|
return {
|
|
388
449
|
id: g.id as unknown as string,
|
|
389
450
|
version: g.version,
|
|
@@ -393,6 +454,10 @@ function serializeGraph(g: Flow): Record<string, unknown> {
|
|
|
393
454
|
edges: g.edges,
|
|
394
455
|
...(g.maxParallelism !== undefined && { maxParallelism: g.maxParallelism }),
|
|
395
456
|
...(g.metadata !== undefined && { metadata: g.metadata }),
|
|
457
|
+
...(g.pins !== undefined && { pins: g.pins }),
|
|
458
|
+
...(g.pinsDigest !== undefined && { pinsDigest: g.pinsDigest }),
|
|
459
|
+
...(g.derivedFrom !== undefined && { derivedFrom: g.derivedFrom }),
|
|
460
|
+
...(g.unregisteredAt !== undefined && { unregisteredAt: g.unregisteredAt }),
|
|
396
461
|
};
|
|
397
462
|
}
|
|
398
463
|
|
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)`.
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
/**
|
|
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
|
+
|
|
54
|
+
/** `409 project-default-already-exists`: the tenant has a Default project. */
|
|
55
|
+
export function projectDefaultAlreadyExistsError() {
|
|
56
|
+
return {
|
|
57
|
+
code: 'project-default-already-exists',
|
|
58
|
+
message: 'The tenant already has a Default project',
|
|
59
|
+
} as const;
|
|
60
|
+
}
|