@kindgi/api 0.1.4-rc.0 → 0.1.4-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-binding.d.ts +20 -2
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts.map +1 -1
- package/dist/agent-pins.js +4 -1
- package/dist/agent-pins.js.map +1 -1
- package/dist/app.d.ts +8 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +30 -5
- package/dist/app.js.map +1 -1
- package/dist/deploy-versions.d.ts +10 -5
- package/dist/deploy-versions.d.ts.map +1 -1
- package/dist/deploy-versions.js +4 -10
- 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 +26 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +10 -0
- package/dist/eval-case-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.d.ts +21 -3
- 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/flow-pins.d.ts +20 -7
- package/dist/flow-pins.d.ts.map +1 -1
- package/dist/flow-pins.js +36 -11
- package/dist/flow-pins.js.map +1 -1
- package/dist/gate-policy-binding.d.ts +164 -0
- package/dist/gate-policy-binding.d.ts.map +1 -0
- package/dist/gate-policy-binding.js +12 -0
- package/dist/gate-policy-binding.js.map +1 -0
- package/dist/gate.d.ts +56 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +359 -0
- package/dist/gate.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 +17 -2
- package/dist/handler-binding.d.ts.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +20 -1
- package/dist/judged-dispatcher.d.ts.map +1 -1
- package/dist/judged-dispatcher.js +57 -7
- package/dist/judged-dispatcher.js.map +1 -1
- package/dist/judgment-binding.d.ts +38 -0
- package/dist/judgment-binding.d.ts.map +1 -1
- package/dist/judgment-binding.js +19 -0
- package/dist/judgment-binding.js.map +1 -1
- package/dist/live-version-binding.d.ts +206 -0
- package/dist/live-version-binding.d.ts.map +1 -0
- package/dist/live-version-binding.js +4 -0
- package/dist/live-version-binding.js.map +1 -0
- package/dist/middleware/authorize.d.ts.map +1 -1
- package/dist/middleware/authorize.js +13 -2
- package/dist/middleware/authorize.js.map +1 -1
- package/dist/middleware/project-ref.d.ts +25 -0
- package/dist/middleware/project-ref.d.ts.map +1 -0
- package/dist/middleware/project-ref.js +72 -0
- package/dist/middleware/project-ref.js.map +1 -0
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +4 -0
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts +6 -0
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +524 -27
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +43 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +1050 -7
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/publish-refused.d.ts +18 -0
- package/dist/publish-refused.d.ts.map +1 -0
- package/dist/publish-refused.js +22 -0
- package/dist/publish-refused.js.map +1 -0
- 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/retention-binding.d.ts +29 -0
- package/dist/retention-binding.d.ts.map +1 -1
- package/dist/routes/agent-releases.d.ts +40 -0
- package/dist/routes/agent-releases.d.ts.map +1 -0
- package/dist/routes/agent-releases.js +462 -0
- package/dist/routes/agent-releases.js.map +1 -0
- package/dist/routes/agents.d.ts +7 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +38 -4
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +27 -9
- package/dist/routes/approvals.js.map +1 -1
- package/dist/routes/audit.d.ts.map +1 -1
- package/dist/routes/audit.js +7 -0
- package/dist/routes/audit.js.map +1 -1
- package/dist/routes/auth.js +1 -1
- package/dist/routes/auth.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/conversations.d.ts +0 -7
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +19 -3
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/deployments.d.ts +6 -0
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +49 -5
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/env.d.ts.map +1 -1
- package/dist/routes/env.js +1 -0
- package/dist/routes/env.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +4 -2
- package/dist/routes/eval-comparison.d.ts.map +1 -1
- package/dist/routes/eval-comparison.js +59 -10
- 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 +14 -7
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +14 -8
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/gate-policies.d.ts +19 -0
- package/dist/routes/gate-policies.d.ts.map +1 -0
- package/dist/routes/gate-policies.js +191 -0
- package/dist/routes/gate-policies.js.map +1 -0
- package/dist/routes/gate-policy-spec.d.ts +17 -0
- package/dist/routes/gate-policy-spec.d.ts.map +1 -0
- package/dist/routes/gate-policy-spec.js +192 -0
- package/dist/routes/gate-policy-spec.js.map +1 -0
- package/dist/routes/gate-policy-wire.d.ts +3 -0
- package/dist/routes/gate-policy-wire.d.ts.map +1 -0
- package/dist/routes/gate-policy-wire.js +18 -0
- package/dist/routes/gate-policy-wire.js.map +1 -0
- 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 +21 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -1
- package/dist/routes/hierarchy-errors.js +21 -0
- package/dist/routes/hierarchy-errors.js.map +1 -1
- package/dist/routes/judged-suites.js +7 -0
- package/dist/routes/judged-suites.js.map +1 -1
- package/dist/routes/judgments.d.ts +4 -1
- package/dist/routes/judgments.d.ts.map +1 -1
- package/dist/routes/judgments.js +92 -19
- package/dist/routes/judgments.js.map +1 -1
- package/dist/routes/live-scope-wire.d.ts +32 -0
- package/dist/routes/live-scope-wire.d.ts.map +1 -0
- package/dist/routes/live-scope-wire.js +71 -0
- package/dist/routes/live-scope-wire.js.map +1 -0
- package/dist/routes/policies.d.ts +6 -3
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +55 -3
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +59 -9
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/retention.d.ts.map +1 -1
- package/dist/routes/retention.js +18 -2
- package/dist/routes/retention.js.map +1 -1
- package/dist/routes/reviewers.d.ts +8 -1
- package/dist/routes/reviewers.d.ts.map +1 -1
- package/dist/routes/reviewers.js +20 -3
- package/dist/routes/reviewers.js.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +23 -13
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/secrets.d.ts.map +1 -1
- package/dist/routes/secrets.js +2 -0
- package/dist/routes/secrets.js.map +1 -1
- package/dist/routes/segments.d.ts +17 -0
- package/dist/routes/segments.d.ts.map +1 -0
- package/dist/routes/segments.js +68 -0
- package/dist/routes/segments.js.map +1 -0
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +53 -8
- 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/routes/uuid-param.d.ts +11 -0
- package/dist/routes/uuid-param.d.ts.map +1 -0
- package/dist/routes/uuid-param.js +19 -0
- package/dist/routes/uuid-param.js.map +1 -0
- package/dist/tool-binding.d.ts +8 -0
- package/dist/tool-binding.d.ts.map +1 -1
- package/openapi.json +6484 -2402
- package/package.json +21 -21
- package/src/agent-binding.ts +21 -2
- package/src/agent-pins.ts +3 -1
- package/src/app.ts +47 -4
- package/src/deploy-versions.ts +12 -15
- package/src/derive-agent-version.ts +12 -1
- package/src/errors.ts +26 -0
- package/src/eval-case-binding.ts +7 -0
- package/src/eval-run-binding.ts +31 -3
- package/src/eval-run-dispatcher.ts +3 -0
- package/src/flow-binding.ts +9 -0
- package/src/flow-pins.ts +49 -9
- package/src/gate-policy-binding.ts +171 -0
- package/src/gate.ts +467 -0
- package/src/guardrail-binding.ts +9 -0
- package/src/handler-binding.ts +17 -2
- package/src/index.ts +47 -1
- package/src/judged-dispatcher.ts +100 -9
- package/src/judgment-binding.ts +62 -0
- package/src/live-version-binding.ts +237 -0
- package/src/middleware/authorize.ts +18 -2
- package/src/middleware/project-ref.ts +88 -0
- package/src/openapi/generate.ts +5 -0
- package/src/openapi/operations.ts +664 -27
- package/src/openapi/schemas.ts +1175 -33
- package/src/publish-refused.ts +31 -0
- package/src/registry-read-only.ts +43 -0
- package/src/retention-binding.ts +30 -0
- package/src/routes/agent-releases.ts +581 -0
- package/src/routes/agents.ts +53 -3
- package/src/routes/approvals.ts +36 -8
- package/src/routes/audit.ts +10 -0
- package/src/routes/auth.ts +1 -1
- package/src/routes/blocks.ts +39 -14
- package/src/routes/conversations.ts +31 -3
- package/src/routes/deployments.ts +75 -5
- package/src/routes/env.ts +1 -0
- package/src/routes/eval-comparison.ts +59 -11
- package/src/routes/eval-runs.ts +46 -3
- package/src/routes/eval-versions.ts +110 -0
- package/src/routes/flows.ts +34 -11
- package/src/routes/gate-policies.ts +226 -0
- package/src/routes/gate-policy-spec.ts +229 -0
- package/src/routes/gate-policy-wire.ts +20 -0
- package/src/routes/guardrails.ts +7 -0
- package/src/routes/hierarchy-errors.ts +23 -0
- package/src/routes/judged-suites.ts +5 -0
- package/src/routes/judgments.ts +107 -23
- package/src/routes/live-scope-wire.ts +90 -0
- package/src/routes/policies.ts +61 -3
- package/src/routes/projects.ts +72 -9
- package/src/routes/retention.ts +21 -2
- package/src/routes/reviewers.ts +23 -4
- package/src/routes/runs.ts +34 -20
- package/src/routes/secrets.ts +2 -0
- package/src/routes/segments.ts +75 -0
- package/src/routes/teams.ts +66 -8
- package/src/routes/tools.ts +7 -0
- package/src/routes/uuid-param.ts +28 -0
- package/src/tool-binding.ts +9 -0
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 {
|
|
@@ -49,3 +58,17 @@ export function projectDefaultAlreadyExistsError() {
|
|
|
49
58
|
message: 'The tenant already has a Default project',
|
|
50
59
|
} as const;
|
|
51
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `501 authz-membership-unsupported`: authorization is enforced, but the
|
|
64
|
+
* tenant-hierarchy binding can't change a membership together with its
|
|
65
|
+
* authorization tuple (it has no `method`). Nothing is changed: removing
|
|
66
|
+
* the row alone would leave the user's permission in place.
|
|
67
|
+
*/
|
|
68
|
+
export function membershipNotKeptInStepError(method: string) {
|
|
69
|
+
return {
|
|
70
|
+
code: 'authz-membership-unsupported',
|
|
71
|
+
message: `This runtime can't keep permissions in step with a membership change (its tenant-hierarchy binding has no ${method}), so nothing was changed.`,
|
|
72
|
+
method,
|
|
73
|
+
} as const;
|
|
74
|
+
}
|
|
@@ -252,12 +252,16 @@ async function summarize(
|
|
|
252
252
|
let no = 0;
|
|
253
253
|
let yesWeight = 0;
|
|
254
254
|
let totalWeight = 0;
|
|
255
|
+
// What judges a class was restricted to asserted, as it was when each judgment was recorded.
|
|
256
|
+
const restricted = { yesWeight: 0, totalWeight: 0 };
|
|
255
257
|
for (const j of judgments) {
|
|
256
258
|
const w = await weightOf(j.judgeClassId);
|
|
257
259
|
totalWeight += w;
|
|
260
|
+
if (j.restricted === true) restricted.totalWeight += w;
|
|
258
261
|
if (j.verdict === 'yes') {
|
|
259
262
|
yes += 1;
|
|
260
263
|
yesWeight += w;
|
|
264
|
+
if (j.restricted === true) restricted.yesWeight += w;
|
|
261
265
|
} else {
|
|
262
266
|
no += 1;
|
|
263
267
|
}
|
|
@@ -271,6 +275,7 @@ async function summarize(
|
|
|
271
275
|
no,
|
|
272
276
|
yesWeight,
|
|
273
277
|
totalWeight,
|
|
278
|
+
restricted,
|
|
274
279
|
reasons: judgments.flatMap((j) =>
|
|
275
280
|
j.reason !== undefined ? [{ verdict: j.verdict, reason: j.reason }] : [],
|
|
276
281
|
),
|
package/src/routes/judgments.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { type Context, Hono } from 'hono';
|
|
5
5
|
|
|
6
6
|
import type { ConversationBinding } from '@kindgi/agents';
|
|
7
|
-
import { ref } from '@kindgi/authz';
|
|
7
|
+
import { REVIEWER_ROLE_RANK, type ReviewerRole, ref } from '@kindgi/authz';
|
|
8
8
|
import type { RunBinding } from '@kindgi/runtime';
|
|
9
9
|
import type { Cursor, ProjectId, RunId, TenantId } from '@kindgi/types';
|
|
10
10
|
|
|
@@ -12,6 +12,7 @@ import { statusFor, toWireError } from '../errors.js';
|
|
|
12
12
|
import type { FlowRegistryBinding } from '../flow-binding.js';
|
|
13
13
|
import {
|
|
14
14
|
type JudgeClass,
|
|
15
|
+
type JudgeClassAssertableBy,
|
|
15
16
|
type JudgeClassScope,
|
|
16
17
|
type JudgedItem,
|
|
17
18
|
type JudgedRunContext,
|
|
@@ -23,8 +24,11 @@ import {
|
|
|
23
24
|
VERDICTS,
|
|
24
25
|
type Verdict,
|
|
25
26
|
judgeClassApplies,
|
|
27
|
+
whyNotAssertable,
|
|
26
28
|
} from '../judgment-binding.js';
|
|
27
29
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
30
|
+
import type { ReviewerBinding } from '../reviewer-binding.js';
|
|
31
|
+
import { callerReviewerRole } from '../reviewer-role.js';
|
|
28
32
|
import type { AppEnv } from '../types.js';
|
|
29
33
|
import { captureTurnContext } from './judgment-context.js';
|
|
30
34
|
import { captureFlowContext } from './judgment-flow-context.js';
|
|
@@ -52,6 +56,8 @@ export function judgmentsRouter(
|
|
|
52
56
|
authorizer?: Authorizer,
|
|
53
57
|
conversations?: ConversationBinding,
|
|
54
58
|
flows?: FlowRegistryBinding,
|
|
59
|
+
/** Whose reviewer role a restricted judge class checks (T200). */
|
|
60
|
+
reviewers?: ReviewerBinding,
|
|
55
61
|
): Hono<AppEnv> {
|
|
56
62
|
const r = new Hono<AppEnv>();
|
|
57
63
|
|
|
@@ -72,9 +78,12 @@ export function judgmentsRouter(
|
|
|
72
78
|
return fail('permission-denied', 'Judging needs a user or a service token.');
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
const prepared = await prepareJudgment(c, body, runBinding, binding, authorizer
|
|
81
|
+
const prepared = await prepareJudgment(c, body, runBinding, binding, authorizer, {
|
|
82
|
+
asserted,
|
|
83
|
+
reviewers,
|
|
84
|
+
});
|
|
76
85
|
if (prepared.kind === 'err') return fail(prepared.code, prepared.message);
|
|
77
|
-
const { run, subject, projectId, itemValue, conversationId } = prepared;
|
|
86
|
+
const { run, subject, projectId, itemValue, conversationId, restricted } = prepared;
|
|
78
87
|
const context = (await isFirstJudgment(binding, tenantId, body.runId))
|
|
79
88
|
? await captureContext({
|
|
80
89
|
tenantId,
|
|
@@ -103,6 +112,7 @@ export function judgmentsRouter(
|
|
|
103
112
|
verdict: body.verdict,
|
|
104
113
|
...(body.reason !== undefined && { reason: body.reason }),
|
|
105
114
|
...(body.judgeClassId !== undefined && { judgeClassId: body.judgeClassId }),
|
|
115
|
+
...(restricted === true && { restricted }),
|
|
106
116
|
assertedBy: asserted,
|
|
107
117
|
...(body.participantId !== undefined && { participantId: body.participantId }),
|
|
108
118
|
});
|
|
@@ -239,7 +249,7 @@ export function judgeClassesRouter(
|
|
|
239
249
|
};
|
|
240
250
|
const parsed = parseClassBody(await c.req.json().catch(() => null));
|
|
241
251
|
if (parsed.kind === 'err') return fail('bad-input', parsed.message);
|
|
242
|
-
const { scope, name, weight, description } = parsed.body;
|
|
252
|
+
const { scope, name, weight, description, assertableBy } = parsed.body;
|
|
243
253
|
if (
|
|
244
254
|
authorizer !== undefined &&
|
|
245
255
|
!(await authorizer.can(c, 'admin', scopeRef(tenantId, scope)))
|
|
@@ -252,6 +262,7 @@ export function judgeClassesRouter(
|
|
|
252
262
|
name,
|
|
253
263
|
weight,
|
|
254
264
|
...(description !== undefined && { description }),
|
|
265
|
+
...(assertableBy !== undefined && { assertableBy }),
|
|
255
266
|
});
|
|
256
267
|
if (outcome.kind === 'name-taken') {
|
|
257
268
|
return fail('judge-class-name-taken', `A judge class named "${name}" already exists here.`);
|
|
@@ -332,19 +343,25 @@ export function judgeClassesRouter(
|
|
|
332
343
|
const raw = (await c.req.json().catch(() => null)) as Record<string, unknown> | null;
|
|
333
344
|
const weight = raw?.weight;
|
|
334
345
|
const description = raw?.description;
|
|
346
|
+
const assertableBy =
|
|
347
|
+
raw?.assertableBy === undefined ? undefined : parseAssertableBy(raw.assertableBy);
|
|
335
348
|
if (
|
|
336
349
|
raw === null ||
|
|
337
350
|
typeof raw !== 'object' ||
|
|
338
|
-
(weight === undefined && description === undefined) ||
|
|
351
|
+
(weight === undefined && description === undefined && assertableBy === undefined) ||
|
|
339
352
|
(weight !== undefined && !isWeight(weight)) ||
|
|
340
|
-
(description !== undefined && typeof description !== 'string')
|
|
353
|
+
(description !== undefined && typeof description !== 'string') ||
|
|
354
|
+
typeof assertableBy === 'string'
|
|
341
355
|
) {
|
|
342
356
|
c.status(statusFor('bad-input') as never);
|
|
343
357
|
return c.json(
|
|
344
358
|
toWireError(
|
|
345
359
|
{
|
|
346
360
|
code: 'bad-input',
|
|
347
|
-
message:
|
|
361
|
+
message:
|
|
362
|
+
typeof assertableBy === 'string'
|
|
363
|
+
? assertableBy
|
|
364
|
+
: 'Send weight (a number ≥ 0), description (a string) and/or assertableBy (who may assert the class, or null for anyone).',
|
|
348
365
|
},
|
|
349
366
|
requestId,
|
|
350
367
|
),
|
|
@@ -358,6 +375,7 @@ export function judgeClassesRouter(
|
|
|
358
375
|
judgeClassId: found.id,
|
|
359
376
|
...(weight !== undefined && { weight: weight as number }),
|
|
360
377
|
...(description !== undefined && { description: description as string }),
|
|
378
|
+
...(assertableBy !== undefined && { assertableBy }),
|
|
361
379
|
});
|
|
362
380
|
return updated === null ? classNotFound(c, found.id) : c.json(serializeJudgeClass(updated));
|
|
363
381
|
});
|
|
@@ -382,6 +400,8 @@ type Prepared =
|
|
|
382
400
|
readonly projectId: ProjectId;
|
|
383
401
|
readonly itemValue?: unknown;
|
|
384
402
|
readonly conversationId?: string;
|
|
403
|
+
/** The class was restricted and the caller met it. */
|
|
404
|
+
readonly restricted?: true;
|
|
385
405
|
}
|
|
386
406
|
| { readonly kind: 'err'; readonly code: string; readonly message: string };
|
|
387
407
|
|
|
@@ -396,6 +416,7 @@ async function prepareJudgment(
|
|
|
396
416
|
runBinding: RunBinding,
|
|
397
417
|
binding: JudgmentRegistryBinding,
|
|
398
418
|
authorizer: Authorizer | undefined,
|
|
419
|
+
who: { readonly asserted: JudgmentAssertedBy; readonly reviewers: ReviewerBinding | undefined },
|
|
399
420
|
): Promise<Prepared> {
|
|
400
421
|
const err = (code: string, message: string): Prepared => ({ kind: 'err', code, message });
|
|
401
422
|
const tenantId = c.get('tenantId') as TenantId;
|
|
@@ -417,20 +438,37 @@ async function prepareJudgment(
|
|
|
417
438
|
}
|
|
418
439
|
const subject = subjectOf(run);
|
|
419
440
|
const projectId = run.projectId as unknown as ProjectId;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
let restricted = false;
|
|
442
|
+
if (body.judgeClassId !== undefined) {
|
|
443
|
+
const judgeClass = await applicableClass(binding, tenantId, body.judgeClassId, {
|
|
444
|
+
projectId,
|
|
445
|
+
subject,
|
|
446
|
+
});
|
|
447
|
+
if (judgeClass === null) {
|
|
448
|
+
return err(
|
|
449
|
+
'judge-class-not-applicable',
|
|
450
|
+
`Judge class "${body.judgeClassId}" doesn't exist or doesn't apply to this run's project or agent.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (judgeClass.assertableBy !== undefined) {
|
|
454
|
+
const reviewerRole = await callerReviewerRole(c, who.reviewers);
|
|
455
|
+
const why = whyNotAssertable(judgeClass.assertableBy, {
|
|
456
|
+
...who.asserted,
|
|
457
|
+
...(reviewerRole !== undefined && { reviewerRole }),
|
|
458
|
+
});
|
|
459
|
+
if (why !== undefined) {
|
|
460
|
+
return err('judge-class-not-allowed', `You can't judge as "${judgeClass.name}": ${why}.`);
|
|
461
|
+
}
|
|
462
|
+
restricted = true;
|
|
463
|
+
}
|
|
428
464
|
}
|
|
429
465
|
const copy = { input: run.input, output: run.output };
|
|
430
|
-
const
|
|
431
|
-
run.agent !== undefined
|
|
466
|
+
const extra = {
|
|
467
|
+
...(run.agent !== undefined && { conversationId: run.agent.conversationId as string }),
|
|
468
|
+
...(restricted && { restricted: true as const }),
|
|
469
|
+
};
|
|
432
470
|
if (body.item.pointer === undefined)
|
|
433
|
-
return { kind: 'ok', run: copy, subject, projectId, ...
|
|
471
|
+
return { kind: 'ok', run: copy, subject, projectId, ...extra };
|
|
434
472
|
const found = resolvePointer(run.output, body.item.pointer);
|
|
435
473
|
if (!found.found) {
|
|
436
474
|
return err(
|
|
@@ -438,7 +476,7 @@ async function prepareJudgment(
|
|
|
438
476
|
`Nothing at "${body.item.pointer}" in run "${body.runId}"'s output.`,
|
|
439
477
|
);
|
|
440
478
|
}
|
|
441
|
-
return { kind: 'ok', run: copy, subject, projectId, itemValue: found.value, ...
|
|
479
|
+
return { kind: 'ok', run: copy, subject, projectId, itemValue: found.value, ...extra };
|
|
442
480
|
}
|
|
443
481
|
|
|
444
482
|
/** Whether the run has no live judgment yet (its copy is stored with the first). */
|
|
@@ -484,15 +522,15 @@ async function isFirstJudgment(
|
|
|
484
522
|
return page.data.length === 0;
|
|
485
523
|
}
|
|
486
524
|
|
|
487
|
-
/**
|
|
488
|
-
async function
|
|
525
|
+
/** The live class, when it exists and its scope covers the run; else `null`. */
|
|
526
|
+
async function applicableClass(
|
|
489
527
|
binding: JudgmentRegistryBinding,
|
|
490
528
|
tenantId: TenantId,
|
|
491
529
|
judgeClassId: string,
|
|
492
530
|
run: { readonly projectId: ProjectId; readonly subject: JudgedSubject },
|
|
493
|
-
): Promise<
|
|
531
|
+
): Promise<JudgeClass | null> {
|
|
494
532
|
const judgeClass = await binding.getClass({ tenantId, judgeClassId });
|
|
495
|
-
return judgeClass !== null && judgeClassApplies(judgeClass.scope, run);
|
|
533
|
+
return judgeClass !== null && judgeClassApplies(judgeClass.scope, run) ? judgeClass : null;
|
|
496
534
|
}
|
|
497
535
|
|
|
498
536
|
// ---------- request parsing ----------
|
|
@@ -573,6 +611,47 @@ interface ClassBody {
|
|
|
573
611
|
readonly name: string;
|
|
574
612
|
readonly weight: number;
|
|
575
613
|
readonly description?: string;
|
|
614
|
+
readonly assertableBy?: JudgeClassAssertableBy;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const ASSERTABLE_BY_USAGE =
|
|
618
|
+
'assertableBy says who may assert the class: { minReviewerRole?: "standard" | "senior" | "admin", principalKinds?: ["user" | "service", …], principalIds?: [id, …] }, with at least one of them.';
|
|
619
|
+
|
|
620
|
+
/** `assertableBy`: a restriction, `null` (on an update: lift it), or what's wrong. */
|
|
621
|
+
function parseAssertableBy(raw: unknown): JudgeClassAssertableBy | null | string {
|
|
622
|
+
if (raw === null) return null;
|
|
623
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) return ASSERTABLE_BY_USAGE;
|
|
624
|
+
const b = raw as Record<string, unknown>;
|
|
625
|
+
const keys = Object.keys(b);
|
|
626
|
+
const known = ['minReviewerRole', 'principalKinds', 'principalIds'];
|
|
627
|
+
if (keys.length === 0 || keys.some((k) => !known.includes(k))) return ASSERTABLE_BY_USAGE;
|
|
628
|
+
const role = b.minReviewerRole;
|
|
629
|
+
if (role !== undefined && !(typeof role === 'string' && role in REVIEWER_ROLE_RANK)) {
|
|
630
|
+
return ASSERTABLE_BY_USAGE;
|
|
631
|
+
}
|
|
632
|
+
const kinds = b.principalKinds;
|
|
633
|
+
if (
|
|
634
|
+
kinds !== undefined &&
|
|
635
|
+
!(
|
|
636
|
+
Array.isArray(kinds) &&
|
|
637
|
+
kinds.length > 0 &&
|
|
638
|
+
kinds.every((k) => k === 'user' || k === 'service')
|
|
639
|
+
)
|
|
640
|
+
) {
|
|
641
|
+
return ASSERTABLE_BY_USAGE;
|
|
642
|
+
}
|
|
643
|
+
const ids = b.principalIds;
|
|
644
|
+
if (
|
|
645
|
+
ids !== undefined &&
|
|
646
|
+
!(Array.isArray(ids) && ids.length > 0 && ids.length <= 100 && ids.every(nonEmpty))
|
|
647
|
+
) {
|
|
648
|
+
return ASSERTABLE_BY_USAGE;
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
...(role !== undefined && { minReviewerRole: role as ReviewerRole }),
|
|
652
|
+
...(kinds !== undefined && { principalKinds: kinds as ('user' | 'service')[] }),
|
|
653
|
+
...(ids !== undefined && { principalIds: ids as string[] }),
|
|
654
|
+
};
|
|
576
655
|
}
|
|
577
656
|
|
|
578
657
|
function parseClassBody(raw: unknown): Parsed<ClassBody> {
|
|
@@ -587,6 +666,8 @@ function parseClassBody(raw: unknown): Parsed<ClassBody> {
|
|
|
587
666
|
}
|
|
588
667
|
const scope = parseClassScope(b.scope);
|
|
589
668
|
if (typeof scope === 'string') return err(scope);
|
|
669
|
+
const assertableBy = b.assertableBy === undefined ? undefined : parseAssertableBy(b.assertableBy);
|
|
670
|
+
if (typeof assertableBy === 'string') return err(assertableBy);
|
|
590
671
|
return {
|
|
591
672
|
kind: 'ok',
|
|
592
673
|
body: {
|
|
@@ -594,6 +675,7 @@ function parseClassBody(raw: unknown): Parsed<ClassBody> {
|
|
|
594
675
|
name: b.name,
|
|
595
676
|
weight: b.weight,
|
|
596
677
|
...(typeof b.description === 'string' && { description: b.description }),
|
|
678
|
+
...(assertableBy !== undefined && assertableBy !== null && { assertableBy }),
|
|
597
679
|
},
|
|
598
680
|
};
|
|
599
681
|
}
|
|
@@ -712,6 +794,7 @@ function serializeJudgment(j: Judgment): Record<string, unknown> {
|
|
|
712
794
|
verdict: j.verdict,
|
|
713
795
|
...(j.reason !== undefined && { reason: j.reason }),
|
|
714
796
|
...(j.judgeClassId !== undefined && { judgeClassId: j.judgeClassId }),
|
|
797
|
+
...(j.restricted === true && { restricted: true }),
|
|
715
798
|
assertedBy: j.assertedBy,
|
|
716
799
|
...(j.participantId !== undefined && { participantId: j.participantId }),
|
|
717
800
|
createdAt: j.createdAt,
|
|
@@ -736,6 +819,7 @@ function serializeJudgeClass(k: JudgeClass): Record<string, unknown> {
|
|
|
736
819
|
name: k.name,
|
|
737
820
|
weight: k.weight,
|
|
738
821
|
...(k.description !== undefined && { description: k.description }),
|
|
822
|
+
...(k.assertableBy !== undefined && { assertableBy: k.assertableBy }),
|
|
739
823
|
createdAt: k.createdAt,
|
|
740
824
|
updatedAt: k.updatedAt,
|
|
741
825
|
...(k.unregisteredAt !== undefined && { unregisteredAt: k.unregisteredAt }),
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { LiveScope, OrgId, ProjectId } from '@kindgi/types';
|
|
5
|
+
|
|
6
|
+
import { parseSegmentsBody } from './segments.js';
|
|
7
|
+
|
|
8
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
10
|
+
/** A live scope on the wire: `{kind}` plus the id or path it needs. */
|
|
11
|
+
export type WireLiveScope =
|
|
12
|
+
| { readonly kind: 'tenant' }
|
|
13
|
+
| { readonly kind: 'org'; readonly orgId: string }
|
|
14
|
+
| { readonly kind: 'project'; readonly projectId: string }
|
|
15
|
+
| {
|
|
16
|
+
readonly kind: 'segment';
|
|
17
|
+
readonly projectId: string;
|
|
18
|
+
readonly path: readonly { readonly key: string; readonly value: string }[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function liveScopeToWire(scope: LiveScope): WireLiveScope {
|
|
22
|
+
switch (scope.kind) {
|
|
23
|
+
case 'tenant':
|
|
24
|
+
return { kind: 'tenant' };
|
|
25
|
+
case 'org':
|
|
26
|
+
return { kind: 'org', orgId: scope.orgId as unknown as string };
|
|
27
|
+
case 'project':
|
|
28
|
+
return { kind: 'project', projectId: scope.projectId as unknown as string };
|
|
29
|
+
case 'segment':
|
|
30
|
+
return {
|
|
31
|
+
kind: 'segment',
|
|
32
|
+
projectId: scope.projectId as unknown as string,
|
|
33
|
+
path: scope.path.map(({ key, value }) => ({ key, value })),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A live scope from a request body (`scope`): `{kind: 'tenant'}`,
|
|
40
|
+
* `{kind: 'org', orgId}`, `{kind: 'project', projectId}` or
|
|
41
|
+
* `{kind: 'segment', projectId, path: [{key, value}, …]}`.
|
|
42
|
+
*/
|
|
43
|
+
export function parseLiveScopeBody(
|
|
44
|
+
raw: unknown,
|
|
45
|
+
):
|
|
46
|
+
| { readonly kind: 'ok'; readonly scope: LiveScope }
|
|
47
|
+
| { readonly kind: 'err'; readonly message: string } {
|
|
48
|
+
const b = (raw ?? {}) as Record<string, unknown>;
|
|
49
|
+
const id = (field: 'orgId' | 'projectId'): string | undefined => {
|
|
50
|
+
const v = b[field];
|
|
51
|
+
return typeof v === 'string' && UUID_RE.test(v) ? v : undefined;
|
|
52
|
+
};
|
|
53
|
+
switch (b.kind) {
|
|
54
|
+
case 'tenant':
|
|
55
|
+
return { kind: 'ok', scope: { kind: 'tenant' } };
|
|
56
|
+
case 'org': {
|
|
57
|
+
const orgId = id('orgId');
|
|
58
|
+
if (orgId === undefined)
|
|
59
|
+
return { kind: 'err', message: '`scope.orgId` must be an org id (a UUID)' };
|
|
60
|
+
return { kind: 'ok', scope: { kind: 'org', orgId: orgId as OrgId } };
|
|
61
|
+
}
|
|
62
|
+
case 'project': {
|
|
63
|
+
const projectId = id('projectId');
|
|
64
|
+
if (projectId === undefined) {
|
|
65
|
+
return { kind: 'err', message: '`scope.projectId` must be a project id (a UUID)' };
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'ok', scope: { kind: 'project', projectId: projectId as ProjectId } };
|
|
68
|
+
}
|
|
69
|
+
case 'segment': {
|
|
70
|
+
const projectId = id('projectId');
|
|
71
|
+
if (projectId === undefined) {
|
|
72
|
+
return { kind: 'err', message: '`scope.projectId` must be a project id (a UUID)' };
|
|
73
|
+
}
|
|
74
|
+
const path = parseSegmentsBody(b.path, 'scope.path');
|
|
75
|
+
if (path.kind === 'err') return path;
|
|
76
|
+
if (path.segments === undefined) {
|
|
77
|
+
return { kind: 'err', message: '`scope.path` needs at least one segment' };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
kind: 'ok',
|
|
81
|
+
scope: { kind: 'segment', projectId: projectId as ProjectId, path: path.segments },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
return {
|
|
86
|
+
kind: 'err',
|
|
87
|
+
message: '`scope.kind` must be one of tenant, org, project, segment',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/routes/policies.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
type Policy,
|
|
12
12
|
type PolicyKind,
|
|
13
13
|
type PolicyRegistryBinding,
|
|
14
|
+
type PolicyScopeChanged,
|
|
15
|
+
type PolicyScopeTaken,
|
|
14
16
|
isAppliedPolicyKind,
|
|
15
17
|
validatePolicySpec,
|
|
16
18
|
} from '@kindgi/policy-contract';
|
|
@@ -27,9 +29,12 @@ import { clampLimit } from './pagination.js';
|
|
|
27
29
|
* that kind. The route validates the top-level shape (id, tenantId,
|
|
28
30
|
* semver version, kind ∈ closed enum, spec is an object), and `spec`
|
|
29
31
|
* against its kind's contract where `@kindgi/policy-contract` has one
|
|
30
|
-
* (`tool-errors`, `hitl`) — a policy that couldn't be
|
|
31
|
-
* here, not found out on a turn
|
|
32
|
-
* consumer's to validate.
|
|
32
|
+
* (`tool-errors`, `hitl`, `retention`) — a policy that couldn't be
|
|
33
|
+
* applied is refused here, not found out on a turn or a sweep. Other
|
|
34
|
+
* kinds' specs are their runtime consumer's to validate. A kind that
|
|
35
|
+
* holds a scope (`policyScope`: a retention policy, its domain) gets one
|
|
36
|
+
* policy per scope; the binding enforces it (`409 policy-scope-taken`,
|
|
37
|
+
* `409 policy-scope-changed`).
|
|
33
38
|
*
|
|
34
39
|
* Enforcement is out of scope. This is the *registry* surface only.
|
|
35
40
|
* Runtime consumers (e.g. `@kindgi/capabilities` `route` for
|
|
@@ -255,6 +260,10 @@ export function policiesRouter(binding: PolicyRegistryBinding): Hono<AppEnv> {
|
|
|
255
260
|
),
|
|
256
261
|
);
|
|
257
262
|
}
|
|
263
|
+
if (outcome.kind === 'scope-taken' || outcome.kind === 'scope-changed') {
|
|
264
|
+
c.status(statusFor(`policy-${outcome.kind}`) as never);
|
|
265
|
+
return c.json(toWireError(scopeError(outcome, 'publish'), requestId));
|
|
266
|
+
}
|
|
258
267
|
c.status(201);
|
|
259
268
|
return c.json({ policyId: outcome.policyId, version: outcome.version });
|
|
260
269
|
});
|
|
@@ -305,6 +314,10 @@ export function policiesRouter(binding: PolicyRegistryBinding): Hono<AppEnv> {
|
|
|
305
314
|
),
|
|
306
315
|
);
|
|
307
316
|
}
|
|
317
|
+
if (outcome.kind === 'scope-taken' || outcome.kind === 'scope-changed') {
|
|
318
|
+
c.status(statusFor(`policy-${outcome.kind}`) as never);
|
|
319
|
+
return c.json(toWireError(scopeError(outcome, 'reinstate'), requestId));
|
|
320
|
+
}
|
|
308
321
|
return c.json({
|
|
309
322
|
policyId: outcome.policyId,
|
|
310
323
|
version: outcome.version,
|
|
@@ -315,6 +328,51 @@ export function policiesRouter(binding: PolicyRegistryBinding): Hono<AppEnv> {
|
|
|
315
328
|
return r;
|
|
316
329
|
}
|
|
317
330
|
|
|
331
|
+
/**
|
|
332
|
+
* The 409 for a policy whose scope (`policyScope`) another policy holds,
|
|
333
|
+
* or that a version would move: for `retention`, a second policy for a
|
|
334
|
+
* domain, which a tenant can't have.
|
|
335
|
+
*/
|
|
336
|
+
function scopeError(
|
|
337
|
+
outcome: PolicyScopeTaken | PolicyScopeChanged,
|
|
338
|
+
action: 'publish' | 'reinstate',
|
|
339
|
+
): { code: string; message: string } & Record<string, unknown> {
|
|
340
|
+
const { policyId, version, policyKind, scope } = outcome;
|
|
341
|
+
const retention = policyKind === 'retention';
|
|
342
|
+
if (outcome.kind === 'scope-taken') {
|
|
343
|
+
const { heldBy } = outcome;
|
|
344
|
+
const message = !retention
|
|
345
|
+
? `Policy "${heldBy}" already holds "${scope}" for ${policyKind} policies.`
|
|
346
|
+
: action === 'publish'
|
|
347
|
+
? `Retention domain "${scope}" is already covered by policy "${heldBy}": a tenant has one retention policy per domain, and one for "*". Publish a new version of "${heldBy}" instead, or unregister it first.`
|
|
348
|
+
: `Retention domain "${scope}" is now covered by policy "${heldBy}", so reinstating "${policyId}" ${version} would make two: a tenant has one retention policy per domain, and one for "*". Unregister "${heldBy}" first.`;
|
|
349
|
+
return {
|
|
350
|
+
code: 'policy-scope-taken',
|
|
351
|
+
message,
|
|
352
|
+
policyId,
|
|
353
|
+
version,
|
|
354
|
+
policyKind,
|
|
355
|
+
scope,
|
|
356
|
+
heldBy,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const { previousScope } = outcome;
|
|
360
|
+
const message = !retention
|
|
361
|
+
? `Policy "${policyId}" holds "${previousScope}"; version ${version} can't move it to "${scope}".`
|
|
362
|
+
: action === 'publish'
|
|
363
|
+
? `Policy "${policyId}" covers retention domain "${previousScope}", and a new version can't move it to "${scope}". Publish a policy for "${scope}" under a new id.`
|
|
364
|
+
: `Version ${version} of policy "${policyId}" covers retention domain "${scope}", but its other versions cover "${previousScope}": a policy keeps one domain. Publish it under a new id instead.`;
|
|
365
|
+
return {
|
|
366
|
+
code: 'policy-scope-changed',
|
|
367
|
+
message,
|
|
368
|
+
policyId,
|
|
369
|
+
version,
|
|
370
|
+
policyKind,
|
|
371
|
+
scope,
|
|
372
|
+
previousScope,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
318
376
|
function serializePolicy(p: Policy): Record<string, unknown> {
|
|
319
377
|
return {
|
|
320
378
|
id: p.id,
|