@kindgi/api 0.1.4-rc.1 → 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 +12 -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 +23 -4
- package/dist/app.js.map +1 -1
- package/dist/deploy-versions.d.ts +8 -5
- package/dist/deploy-versions.d.ts.map +1 -1
- package/dist/deploy-versions.js +3 -9
- package/dist/deploy-versions.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +25 -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 +14 -3
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.js.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/handler-binding.d.ts +14 -2
- package/dist/handler-binding.d.ts.map +1 -1
- package/dist/index.d.ts +10 -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 +16 -1
- package/dist/judged-dispatcher.d.ts.map +1 -1
- package/dist/judged-dispatcher.js +45 -6
- 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 +504 -22
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +37 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +709 -5
- 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/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 +31 -3
- 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/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 +33 -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 +2 -1
- package/dist/routes/eval-comparison.d.ts.map +1 -1
- package/dist/routes/eval-comparison.js +22 -9
- package/dist/routes/eval-comparison.js.map +1 -1
- package/dist/routes/flows.d.ts +14 -7
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +10 -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/hierarchy-errors.d.ts +11 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -1
- package/dist/routes/hierarchy-errors.js +13 -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 +47 -3
- 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 +22 -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 +42 -3
- package/dist/routes/teams.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/openapi.json +6995 -3625
- package/package.json +21 -21
- package/src/agent-binding.ts +12 -2
- package/src/agent-pins.ts +3 -1
- package/src/app.ts +37 -3
- package/src/deploy-versions.ts +11 -14
- package/src/errors.ts +25 -0
- package/src/eval-case-binding.ts +7 -0
- package/src/eval-run-binding.ts +24 -3
- package/src/flow-pins.ts +49 -9
- package/src/gate-policy-binding.ts +171 -0
- package/src/gate.ts +467 -0
- package/src/handler-binding.ts +14 -2
- package/src/index.ts +46 -1
- package/src/judged-dispatcher.ts +74 -6
- 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 +609 -22
- package/src/openapi/schemas.ts +788 -5
- package/src/publish-refused.ts +31 -0
- package/src/retention-binding.ts +30 -0
- package/src/routes/agent-releases.ts +581 -0
- package/src/routes/agents.ts +43 -2
- package/src/routes/approvals.ts +36 -8
- package/src/routes/audit.ts +10 -0
- package/src/routes/auth.ts +1 -1
- package/src/routes/conversations.ts +31 -3
- package/src/routes/deployments.ts +50 -5
- package/src/routes/env.ts +1 -0
- package/src/routes/eval-comparison.ts +24 -10
- package/src/routes/flows.ts +27 -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/hierarchy-errors.ts +14 -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 +54 -2
- package/src/routes/retention.ts +21 -2
- package/src/routes/reviewers.ts +23 -4
- package/src/routes/runs.ts +33 -20
- package/src/routes/secrets.ts +2 -0
- package/src/routes/segments.ts +75 -0
- package/src/routes/teams.ts +53 -3
- package/src/routes/uuid-param.ts +28 -0
package/src/index.ts
CHANGED
|
@@ -331,7 +331,12 @@ export type {
|
|
|
331
331
|
PolicyVersionPage,
|
|
332
332
|
PolicyVersionRow,
|
|
333
333
|
} from '@kindgi/policy-contract';
|
|
334
|
-
export {
|
|
334
|
+
export {
|
|
335
|
+
JUDGE_CLASS_SCOPE_KINDS,
|
|
336
|
+
VERDICTS,
|
|
337
|
+
judgeClassApplies,
|
|
338
|
+
whyNotAssertable,
|
|
339
|
+
} from './judgment-binding.js';
|
|
335
340
|
export type {
|
|
336
341
|
EvalCaseListInput,
|
|
337
342
|
EvalCasePage,
|
|
@@ -344,6 +349,8 @@ export { MAX_JUDGED_CASES } from './routes/judged-suites.js';
|
|
|
344
349
|
export { MAX_JUDGED_HISTORY } from './routes/judgment-context.js';
|
|
345
350
|
export type {
|
|
346
351
|
JudgeClass,
|
|
352
|
+
JudgeClassAssertableBy,
|
|
353
|
+
JudgeClassAsserter,
|
|
347
354
|
JudgeClassCreateInput,
|
|
348
355
|
JudgeClassCreateOutcome,
|
|
349
356
|
JudgeClassGetInput,
|
|
@@ -386,6 +393,42 @@ export type {
|
|
|
386
393
|
BlockReinstateOutcome,
|
|
387
394
|
BlockVersionInput,
|
|
388
395
|
} from './block-binding.js';
|
|
396
|
+
export type {
|
|
397
|
+
AgentReleaseBindings,
|
|
398
|
+
ListPromotionsInput,
|
|
399
|
+
LivePin,
|
|
400
|
+
LiveResolution,
|
|
401
|
+
LiveResolveInput,
|
|
402
|
+
LiveVersionBinding,
|
|
403
|
+
PromoteInput,
|
|
404
|
+
Promotion,
|
|
405
|
+
PromotionAction,
|
|
406
|
+
PromotionActor,
|
|
407
|
+
PromotionBinding,
|
|
408
|
+
PromotionError,
|
|
409
|
+
PromotionErrorCode,
|
|
410
|
+
PromotionRequestInput,
|
|
411
|
+
PromotionStatus,
|
|
412
|
+
RollbackInput,
|
|
413
|
+
UnpinInput,
|
|
414
|
+
} from './live-version-binding.js';
|
|
415
|
+
export type {
|
|
416
|
+
GateMetricName,
|
|
417
|
+
GateMetricSpec,
|
|
418
|
+
GatePolicy,
|
|
419
|
+
GatePolicyBinding,
|
|
420
|
+
GatePolicyError,
|
|
421
|
+
GatePolicyErrorCode,
|
|
422
|
+
GatePolicyListInput,
|
|
423
|
+
GatePolicyPublishInput,
|
|
424
|
+
GatePolicyRef,
|
|
425
|
+
GatePolicySpec,
|
|
426
|
+
GatePolicyVersionInput,
|
|
427
|
+
} from './gate-policy-binding.js';
|
|
428
|
+
export { GATE_METRICS } from './gate-policy-binding.js';
|
|
429
|
+
export type { GateApproval, GateCheck, GateInput, GateResult } from './gate.js';
|
|
430
|
+
export { evaluateGate, gateApproval } from './gate.js';
|
|
431
|
+
export type { AgentReleaseGateDeps } from './routes/agent-releases.js';
|
|
389
432
|
export { EVAL_KINDS } from './eval-suite-binding.js';
|
|
390
433
|
export type {
|
|
391
434
|
EvalKind,
|
|
@@ -407,6 +450,7 @@ export { EVAL_RUN_STATUSES } from './eval-run-binding.js';
|
|
|
407
450
|
export type {
|
|
408
451
|
AgentRef,
|
|
409
452
|
EvalBaseline,
|
|
453
|
+
EvalClassWeights,
|
|
410
454
|
EvalComparison,
|
|
411
455
|
EvalReads,
|
|
412
456
|
EvalRun,
|
|
@@ -685,6 +729,7 @@ export type { JsonSchema } from './openapi/schemas.js';
|
|
|
685
729
|
|
|
686
730
|
export type {
|
|
687
731
|
RetentionBinding,
|
|
732
|
+
RetentionPolicyConflict,
|
|
688
733
|
RetentionScheduledInput,
|
|
689
734
|
RetentionScheduledItem,
|
|
690
735
|
RetentionScheduledPage,
|
package/src/judged-dispatcher.ts
CHANGED
|
@@ -19,8 +19,9 @@ import type { ReplayTurnReport } from '@kindgi/agents';
|
|
|
19
19
|
import type { FlowVersionOverrides } from '@kindgi/flow';
|
|
20
20
|
import type { RunId } from '@kindgi/types';
|
|
21
21
|
|
|
22
|
+
import type { AgentRegistryBinding } from './agent-binding.js';
|
|
22
23
|
import type { EvalCaseStoreBinding, JudgedEvalCase } from './eval-case-binding.js';
|
|
23
|
-
import type { AgentRef, EvalComparison, FlowRef } from './eval-run-binding.js';
|
|
24
|
+
import type { AgentRef, EvalClassWeights, EvalComparison, FlowRef } from './eval-run-binding.js';
|
|
24
25
|
import type {
|
|
25
26
|
DispatchContext,
|
|
26
27
|
DispatchResult,
|
|
@@ -81,7 +82,17 @@ export type RecordedVersion =
|
|
|
81
82
|
|
|
82
83
|
/** What ran on the cases: an agent version, or a flow version. */
|
|
83
84
|
export type ComparisonCandidate =
|
|
84
|
-
| {
|
|
85
|
+
| {
|
|
86
|
+
readonly kind: 'agent';
|
|
87
|
+
readonly agentId: string;
|
|
88
|
+
readonly version: string;
|
|
89
|
+
/**
|
|
90
|
+
* The version's `pinsDigest`: what it ran, as a promotion gate checks.
|
|
91
|
+
* Absent for a version published before pins, and from a dispatcher
|
|
92
|
+
* without an agent registry.
|
|
93
|
+
*/
|
|
94
|
+
readonly pinsDigest?: string;
|
|
95
|
+
}
|
|
85
96
|
| {
|
|
86
97
|
readonly kind: 'flow';
|
|
87
98
|
readonly flowId: string;
|
|
@@ -113,6 +124,12 @@ export interface JudgedComparisonSummary {
|
|
|
113
124
|
*/
|
|
114
125
|
readonly stopped: number;
|
|
115
126
|
readonly reads: EvalComparison['reads'];
|
|
127
|
+
/**
|
|
128
|
+
* Which judgments counted: `restricted-only` weighs a judgment not
|
|
129
|
+
* recorded under a restricted class 0 (a gate can require it). Absent
|
|
130
|
+
* from a summary recorded before T200: `as-recorded`.
|
|
131
|
+
*/
|
|
132
|
+
readonly classWeights?: EvalClassWeights;
|
|
116
133
|
/** The models that answered the candidate's replays, and how many replays each. */
|
|
117
134
|
readonly sampling: {
|
|
118
135
|
readonly models: readonly {
|
|
@@ -152,6 +169,8 @@ export interface JudgedCaseResult {
|
|
|
152
169
|
|
|
153
170
|
export interface JudgedDispatcherOptions {
|
|
154
171
|
readonly cases: EvalCaseStoreBinding;
|
|
172
|
+
/** Where the candidate's `pinsDigest` is read, for the summary. */
|
|
173
|
+
readonly agents?: Pick<AgentRegistryBinding, 'getVersion'>;
|
|
155
174
|
}
|
|
156
175
|
|
|
157
176
|
/** Cases read per page. */
|
|
@@ -193,7 +212,9 @@ export function createJudgedDispatcher(options: JudgedDispatcherOptions): EvalRu
|
|
|
193
212
|
validate: validateComparison,
|
|
194
213
|
async dispatch(ctx): Promise<DispatchResult> {
|
|
195
214
|
const comparison = ctx.comparison ?? DEFAULT_COMPARISON;
|
|
196
|
-
const
|
|
215
|
+
const stored = await allCases(options.cases, ctx);
|
|
216
|
+
const all =
|
|
217
|
+
comparison.classWeights === 'restricted-only' ? stored.map(restrictedOnly) : stored;
|
|
197
218
|
if (ctx.dryRun) {
|
|
198
219
|
return { result: { dryRun: true, cases: all.length, comparison } };
|
|
199
220
|
}
|
|
@@ -205,7 +226,8 @@ export function createJudgedDispatcher(options: JudgedDispatcherOptions): EvalRu
|
|
|
205
226
|
results.push(result);
|
|
206
227
|
ctx.onProgress(result as unknown as Readonly<Record<string, unknown>>);
|
|
207
228
|
}
|
|
208
|
-
const
|
|
229
|
+
const pinsDigest = await candidatePins(options.agents, ctx);
|
|
230
|
+
const summary = summarize(ctx, comparison, all, results, [...models.values()], pinsDigest);
|
|
209
231
|
return {
|
|
210
232
|
result: { summary, perCase: results },
|
|
211
233
|
...(ctx.abortSignal.aborted && { error: 'cancelled' }),
|
|
@@ -451,6 +473,7 @@ function summarize(
|
|
|
451
473
|
cases: readonly JudgedEvalCase[],
|
|
452
474
|
results: readonly JudgedCaseResult[],
|
|
453
475
|
models: readonly { providerId: string; model: string; runs: number }[],
|
|
476
|
+
pinsDigest: string | undefined,
|
|
454
477
|
): JudgedComparisonSummary {
|
|
455
478
|
const errors = results.filter((r) => r.error !== undefined).length;
|
|
456
479
|
const stopped = results.filter((r) => r.stopped !== undefined).length;
|
|
@@ -482,7 +505,7 @@ function summarize(
|
|
|
482
505
|
: 'failed',
|
|
483
506
|
completedAt: new Date().toISOString(),
|
|
484
507
|
suite: { id: ctx.suite.id, version: ctx.suite.version },
|
|
485
|
-
candidate: candidateOf(ctx.target, ctx.comparison?.versions),
|
|
508
|
+
candidate: candidateOf(ctx.target, ctx.comparison?.versions, pinsDigest),
|
|
486
509
|
baseline: {
|
|
487
510
|
kind: 'recorded',
|
|
488
511
|
versions: [...versions.values()].map(
|
|
@@ -499,6 +522,7 @@ function summarize(
|
|
|
499
522
|
errors,
|
|
500
523
|
stopped,
|
|
501
524
|
reads: comparison.reads,
|
|
525
|
+
classWeights: comparison.classWeights ?? 'as-recorded',
|
|
502
526
|
sampling: { models },
|
|
503
527
|
repetitions: comparison.repetitions,
|
|
504
528
|
metrics: {
|
|
@@ -515,12 +539,56 @@ function summarize(
|
|
|
515
539
|
};
|
|
516
540
|
}
|
|
517
541
|
|
|
542
|
+
/**
|
|
543
|
+
* A case as a `restricted-only` comparison counts it: each item at its
|
|
544
|
+
* restricted judgments' weights; an item with none is left out, so it
|
|
545
|
+
* counts as unjudged.
|
|
546
|
+
*/
|
|
547
|
+
function restrictedOnly(c: JudgedEvalCase): JudgedEvalCase {
|
|
548
|
+
return {
|
|
549
|
+
...c,
|
|
550
|
+
items: c.items.flatMap((item) =>
|
|
551
|
+
item.restricted !== undefined && item.restricted.totalWeight > 0
|
|
552
|
+
? [
|
|
553
|
+
{
|
|
554
|
+
...item,
|
|
555
|
+
yesWeight: item.restricted.yesWeight,
|
|
556
|
+
totalWeight: item.restricted.totalWeight,
|
|
557
|
+
},
|
|
558
|
+
]
|
|
559
|
+
: [],
|
|
560
|
+
),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** The agent candidate's `pinsDigest`, from the registry. */
|
|
565
|
+
async function candidatePins(
|
|
566
|
+
agents: JudgedDispatcherOptions['agents'],
|
|
567
|
+
ctx: DispatchContext,
|
|
568
|
+
): Promise<string | undefined> {
|
|
569
|
+
if (agents === undefined || !('agentId' in ctx.target) || ctx.target.version === undefined) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
const found = await agents.getVersion({
|
|
573
|
+
tenantId: ctx.tenantId,
|
|
574
|
+
agentId: ctx.target.agentId,
|
|
575
|
+
version: ctx.target.version,
|
|
576
|
+
});
|
|
577
|
+
return found?.pinsDigest;
|
|
578
|
+
}
|
|
579
|
+
|
|
518
580
|
function candidateOf(
|
|
519
581
|
target: AgentRef | FlowRef,
|
|
520
582
|
versions: FlowVersionOverrides | undefined,
|
|
583
|
+
pinsDigest?: string,
|
|
521
584
|
): ComparisonCandidate {
|
|
522
585
|
return 'agentId' in target
|
|
523
|
-
? {
|
|
586
|
+
? {
|
|
587
|
+
kind: 'agent',
|
|
588
|
+
agentId: target.agentId as unknown as string,
|
|
589
|
+
version: target.version ?? '',
|
|
590
|
+
...(pinsDigest !== undefined && { pinsDigest }),
|
|
591
|
+
}
|
|
524
592
|
: {
|
|
525
593
|
kind: 'flow',
|
|
526
594
|
flowId: target.flowId as unknown as string,
|
package/src/judgment-binding.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
|
+
import { REVIEWER_ROLE_RANK, type ReviewerRole } from '@kindgi/authz';
|
|
4
5
|
import type { Cursor, ListScope, ProjectId, TenantId } from '@kindgi/types';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -77,6 +78,54 @@ export type JudgeClassScope =
|
|
|
77
78
|
export const JUDGE_CLASS_SCOPE_KINDS = ['tenant', 'project', 'agent'] as const;
|
|
78
79
|
export type JudgeClassScopeKind = (typeof JUDGE_CLASS_SCOPE_KINDS)[number];
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Who may assert a judge class (T200): every part that's set must hold.
|
|
83
|
+
* Absent: anyone who may judge the run may assert the class, as before.
|
|
84
|
+
* A class with one is *restricted*; a judgment recorded while it is
|
|
85
|
+
* carries `restricted`, and a comparison weighted `restricted-only`
|
|
86
|
+
* counts only those.
|
|
87
|
+
*/
|
|
88
|
+
export interface JudgeClassAssertableBy {
|
|
89
|
+
/** The caller's reviewer role is at least this (its token's, or the roster's). */
|
|
90
|
+
readonly minReviewerRole?: ReviewerRole;
|
|
91
|
+
/** Users, service tokens, or both. */
|
|
92
|
+
readonly principalKinds?: readonly ('user' | 'service')[];
|
|
93
|
+
/** Only these principals: user ids, or service token ids. */
|
|
94
|
+
readonly principalIds?: readonly string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Who asserts a judgment, as `JudgeClassAssertableBy` checks it. */
|
|
98
|
+
export interface JudgeClassAsserter {
|
|
99
|
+
readonly kind: 'user' | 'service';
|
|
100
|
+
readonly id: string;
|
|
101
|
+
/** The caller's reviewer role; absent for a caller who isn't a reviewer. */
|
|
102
|
+
readonly reviewerRole?: ReviewerRole;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Why `asserter` may not assert a class with `assertableBy`, or `undefined` when they may. */
|
|
106
|
+
export function whyNotAssertable(
|
|
107
|
+
assertableBy: JudgeClassAssertableBy,
|
|
108
|
+
asserter: JudgeClassAsserter,
|
|
109
|
+
): string | undefined {
|
|
110
|
+
if (
|
|
111
|
+
assertableBy.principalKinds !== undefined &&
|
|
112
|
+
!assertableBy.principalKinds.includes(asserter.kind)
|
|
113
|
+
) {
|
|
114
|
+
return `only ${assertableBy.principalKinds.map((k) => (k === 'user' ? 'users' : 'service tokens')).join(' and ')} may assert it`;
|
|
115
|
+
}
|
|
116
|
+
if (assertableBy.principalIds !== undefined && !assertableBy.principalIds.includes(asserter.id)) {
|
|
117
|
+
return "it's restricted to named people or tokens, and you aren't one of them";
|
|
118
|
+
}
|
|
119
|
+
if (assertableBy.minReviewerRole !== undefined) {
|
|
120
|
+
const need = assertableBy.minReviewerRole;
|
|
121
|
+
const role = asserter.reviewerRole;
|
|
122
|
+
if (role === undefined || REVIEWER_ROLE_RANK[role] < REVIEWER_ROLE_RANK[need]) {
|
|
123
|
+
return `it needs a ${need} reviewer or above${role === undefined ? ", and you aren't a reviewer" : `; you're a ${role} reviewer`}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
80
129
|
export interface JudgeClass {
|
|
81
130
|
readonly id: string;
|
|
82
131
|
readonly tenantId: TenantId;
|
|
@@ -86,6 +135,8 @@ export interface JudgeClass {
|
|
|
86
135
|
/** How much a judgment of this class counts (≥ 0); an unclassified judgment counts 1. */
|
|
87
136
|
readonly weight: number;
|
|
88
137
|
readonly description?: string;
|
|
138
|
+
/** Who may assert it; absent: anyone who may judge the run (T200). */
|
|
139
|
+
readonly assertableBy?: JudgeClassAssertableBy;
|
|
89
140
|
readonly createdAt: string;
|
|
90
141
|
readonly updatedAt: string;
|
|
91
142
|
/** Set when the class was retired. */
|
|
@@ -98,6 +149,7 @@ export interface JudgeClassCreateInput {
|
|
|
98
149
|
readonly name: string;
|
|
99
150
|
readonly weight: number;
|
|
100
151
|
readonly description?: string;
|
|
152
|
+
readonly assertableBy?: JudgeClassAssertableBy;
|
|
101
153
|
}
|
|
102
154
|
|
|
103
155
|
export type JudgeClassCreateOutcome =
|
|
@@ -128,6 +180,8 @@ export interface JudgeClassUpdateInput {
|
|
|
128
180
|
readonly judgeClassId: string;
|
|
129
181
|
readonly weight?: number;
|
|
130
182
|
readonly description?: string;
|
|
183
|
+
/** Who may assert it; `null` lifts the restriction. Absent: unchanged. */
|
|
184
|
+
readonly assertableBy?: JudgeClassAssertableBy | null;
|
|
131
185
|
}
|
|
132
186
|
|
|
133
187
|
/** Whether a class's scope covers a run in `projectId` whose subject is `subject`. */
|
|
@@ -189,6 +243,12 @@ export interface Judgment {
|
|
|
189
243
|
readonly reason?: string;
|
|
190
244
|
/** The judge's class; absent for an unclassified judgment (weight 1). */
|
|
191
245
|
readonly judgeClassId?: string;
|
|
246
|
+
/**
|
|
247
|
+
* Set when the class was restricted (`assertableBy`) when the judgment
|
|
248
|
+
* was recorded, so the judge was checked against it. A restriction added
|
|
249
|
+
* or lifted later doesn't change it (T200).
|
|
250
|
+
*/
|
|
251
|
+
readonly restricted?: true;
|
|
192
252
|
readonly assertedBy: JudgmentAssertedBy;
|
|
193
253
|
/** The app's opaque id for its end user who judged, when an app judged on their behalf. */
|
|
194
254
|
readonly participantId?: string;
|
|
@@ -292,6 +352,8 @@ export interface JudgmentRecordInput {
|
|
|
292
352
|
readonly verdict: Verdict;
|
|
293
353
|
readonly reason?: string;
|
|
294
354
|
readonly judgeClassId?: string;
|
|
355
|
+
/** The class was restricted, and the judge met it (see `Judgment.restricted`). */
|
|
356
|
+
readonly restricted?: true;
|
|
295
357
|
readonly assertedBy: JudgmentAssertedBy;
|
|
296
358
|
readonly participantId?: string;
|
|
297
359
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Cursor,
|
|
6
|
+
LiveScope,
|
|
7
|
+
OrgId,
|
|
8
|
+
ProjectId,
|
|
9
|
+
Result,
|
|
10
|
+
ScopeSegment,
|
|
11
|
+
Semver,
|
|
12
|
+
TenantId,
|
|
13
|
+
Timestamp,
|
|
14
|
+
} from '@kindgi/types';
|
|
15
|
+
|
|
16
|
+
import type { GatePolicyBinding, GatePolicyRef } from './gate-policy-binding.js';
|
|
17
|
+
import type { GateApproval, GateCheck } from './gate.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Live versions of agents: which version serves a scope, and the
|
|
21
|
+
* promotions that put it there (evals step 4). A run that names no
|
|
22
|
+
* version takes the most specific live version that covers it (a
|
|
23
|
+
* segment path, then its project, its org, the tenant), else the latest
|
|
24
|
+
* registered one, as before anything is pinned.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** What a run's version is resolved for: its project (and org) and its segment path. */
|
|
28
|
+
export interface LiveResolveInput {
|
|
29
|
+
readonly tenantId: TenantId;
|
|
30
|
+
readonly agentId: string;
|
|
31
|
+
/** Absent → only the org's pin (with `orgId`) and the tenant-wide pin apply. */
|
|
32
|
+
readonly projectId?: ProjectId;
|
|
33
|
+
/**
|
|
34
|
+
* The project's org, when it has one (the resolver may look it up from
|
|
35
|
+
* `projectId` itself). Without `projectId`: an org scope's own
|
|
36
|
+
* coordinates, as the promotion gate resolves what serves an org.
|
|
37
|
+
*/
|
|
38
|
+
readonly orgId?: OrgId;
|
|
39
|
+
/** Coarse to fine (needs `projectId`); a segment pin covers every path that starts with its own. */
|
|
40
|
+
readonly segments?: readonly ScopeSegment[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LiveResolution {
|
|
44
|
+
readonly version: Semver;
|
|
45
|
+
/** The pin that matched. */
|
|
46
|
+
readonly scope: LiveScope;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LivePin {
|
|
50
|
+
readonly agentId: string;
|
|
51
|
+
readonly scope: LiveScope;
|
|
52
|
+
readonly version: Semver;
|
|
53
|
+
/** The promotion that set it. */
|
|
54
|
+
readonly promotionId: string;
|
|
55
|
+
readonly setAt: Timestamp;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LiveVersionBinding {
|
|
59
|
+
/** The most specific live version covering the input; `null` when none is pinned on the way up. */
|
|
60
|
+
resolve(input: LiveResolveInput): Promise<LiveResolution | null>;
|
|
61
|
+
/** Every pin of an agent. */
|
|
62
|
+
list(input: { readonly tenantId: TenantId; readonly agentId: string }): Promise<
|
|
63
|
+
readonly LivePin[]
|
|
64
|
+
>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Who asked: the request's principal (a user, or a service token). */
|
|
68
|
+
export interface PromotionActor {
|
|
69
|
+
readonly kind: 'user' | 'service';
|
|
70
|
+
readonly id: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type PromotionAction = 'promote' | 'rollback' | 'unpin';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Where a promotion request stands. Only `promote` rows have one; rollback
|
|
77
|
+
* and unpin are immediate. A request's status changes once, from
|
|
78
|
+
* `pending-approval` to its final state.
|
|
79
|
+
*/
|
|
80
|
+
export type PromotionStatus =
|
|
81
|
+
/** The version is live for the scope. */
|
|
82
|
+
| 'promoted'
|
|
83
|
+
/** The gate passed; a reviewer's approval is open. The live version is unchanged. */
|
|
84
|
+
| 'pending-approval'
|
|
85
|
+
/** The gate failed (`checks` say why). The live version is unchanged. */
|
|
86
|
+
| 'refused'
|
|
87
|
+
/** Approved, but the scope's live version or policy changed meanwhile: check again. */
|
|
88
|
+
| 'superseded'
|
|
89
|
+
/** The reviewer rejected it. */
|
|
90
|
+
| 'rejected'
|
|
91
|
+
/** The approval expired undecided. */
|
|
92
|
+
| 'expired';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One change of a scope's live version, kept for good (the audit trail):
|
|
96
|
+
* what was live before, what is after, who asked and why.
|
|
97
|
+
*/
|
|
98
|
+
export interface Promotion {
|
|
99
|
+
readonly id: string;
|
|
100
|
+
readonly agentId: string;
|
|
101
|
+
readonly scope: LiveScope;
|
|
102
|
+
readonly action: PromotionAction;
|
|
103
|
+
/** The scope's own pin before; `null` when it had none. */
|
|
104
|
+
readonly fromVersion: Semver | null;
|
|
105
|
+
/** The scope's own pin after; `null` after an unpin. */
|
|
106
|
+
readonly toVersion: Semver | null;
|
|
107
|
+
readonly requestedBy: PromotionActor;
|
|
108
|
+
readonly reason?: string;
|
|
109
|
+
/** The comparison the change was judged on, when there was one. */
|
|
110
|
+
readonly evalRunId?: string;
|
|
111
|
+
readonly createdAt: Timestamp;
|
|
112
|
+
/** A `promote` row's state; absent on a promotion made before gates (`promoted`). */
|
|
113
|
+
readonly status?: PromotionStatus;
|
|
114
|
+
/** The gate policy that applied; `null` when none did. Absent before gates. */
|
|
115
|
+
readonly policy?: GatePolicyRef | null;
|
|
116
|
+
/** The gate's checks, as they ran. */
|
|
117
|
+
readonly checks?: readonly GateCheck[];
|
|
118
|
+
/** The approval a `pending-approval` promotion waits on (kept once decided). */
|
|
119
|
+
readonly approvalId?: string;
|
|
120
|
+
/** When a `pending-approval` promotion reached its final state. */
|
|
121
|
+
readonly resolvedAt?: Timestamp;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type PromotionErrorCode =
|
|
125
|
+
/** The version isn't registered for this agent, or was unregistered. */
|
|
126
|
+
| 'agent-version-not-found'
|
|
127
|
+
/** The scope names an org or project the tenant doesn't have. */
|
|
128
|
+
| 'scope-invalid'
|
|
129
|
+
/** Rollback: the scope has no earlier live version to go back to. */
|
|
130
|
+
| 'nothing-to-roll-back'
|
|
131
|
+
/** Unpin or rollback: the scope has no pin of its own. */
|
|
132
|
+
| 'not-pinned'
|
|
133
|
+
/** The scope's live version changed while the gate ran: check again. */
|
|
134
|
+
| 'promotion-superseded'
|
|
135
|
+
/**
|
|
136
|
+
* Unpin or rollback: it would leave a scope a gate policy applies to
|
|
137
|
+
* resolving to the latest version, where publishing goes live ungated.
|
|
138
|
+
*/
|
|
139
|
+
| 'gate-policy-needs-pin'
|
|
140
|
+
| 'persistence-error';
|
|
141
|
+
|
|
142
|
+
export interface PromotionError {
|
|
143
|
+
readonly code: PromotionErrorCode;
|
|
144
|
+
readonly message: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface PromoteInput {
|
|
148
|
+
readonly tenantId: TenantId;
|
|
149
|
+
readonly agentId: string;
|
|
150
|
+
readonly version: Semver;
|
|
151
|
+
readonly scope: LiveScope;
|
|
152
|
+
readonly requestedBy: PromotionActor;
|
|
153
|
+
readonly reason?: string;
|
|
154
|
+
readonly evalRunId?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A promotion with the gate's verdict, recorded as one request: refused,
|
|
159
|
+
* waiting for approval, or promoted. The route runs the gate; the
|
|
160
|
+
* binding records the outcome atomically.
|
|
161
|
+
*/
|
|
162
|
+
export interface PromotionRequestInput extends PromoteInput {
|
|
163
|
+
readonly gate: {
|
|
164
|
+
readonly policy: GatePolicyRef | null;
|
|
165
|
+
readonly checks: readonly GateCheck[];
|
|
166
|
+
readonly passed: boolean;
|
|
167
|
+
/** A passing promotion waits for this approval instead of going live. */
|
|
168
|
+
readonly approval?: GateApproval;
|
|
169
|
+
/**
|
|
170
|
+
* What served the scope when the gate ran. A binding refuses with
|
|
171
|
+
* `promotion-superseded` when it no longer does, and an approved
|
|
172
|
+
* promotion whose scope moved on becomes `superseded`.
|
|
173
|
+
*/
|
|
174
|
+
readonly servingVersion: Semver;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface RollbackInput {
|
|
179
|
+
readonly tenantId: TenantId;
|
|
180
|
+
readonly agentId: string;
|
|
181
|
+
readonly scope: LiveScope;
|
|
182
|
+
/** Back to this earlier version; absent → the scope's previous live version. */
|
|
183
|
+
readonly toVersion?: Semver;
|
|
184
|
+
readonly requestedBy: PromotionActor;
|
|
185
|
+
readonly reason?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface UnpinInput {
|
|
189
|
+
readonly tenantId: TenantId;
|
|
190
|
+
readonly agentId: string;
|
|
191
|
+
readonly scope: LiveScope;
|
|
192
|
+
readonly requestedBy: PromotionActor;
|
|
193
|
+
readonly reason?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface ListPromotionsInput {
|
|
197
|
+
readonly tenantId: TenantId;
|
|
198
|
+
readonly agentId: string;
|
|
199
|
+
/** Only this scope's history. */
|
|
200
|
+
readonly scope?: LiveScope;
|
|
201
|
+
readonly limit: number;
|
|
202
|
+
readonly cursor?: Cursor;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface PromotionBinding {
|
|
206
|
+
/** Make `version` live for `scope`. The version must be registered and active. */
|
|
207
|
+
promote(input: PromoteInput): Promise<Result<Promotion, PromotionError>>;
|
|
208
|
+
/**
|
|
209
|
+
* Record a gated promotion request (evals step 4b): `refused` when the
|
|
210
|
+
* gate failed, `pending-approval` (opening a HITL approval, subject
|
|
211
|
+
* `agent-promotion`) when it passed and the policy wants an approval,
|
|
212
|
+
* else `promoted`. Optional: without it, the route refuses a promotion
|
|
213
|
+
* a gate policy applies to (`501`), rather than promoting ungated.
|
|
214
|
+
*/
|
|
215
|
+
request?(input: PromotionRequestInput): Promise<Result<Promotion, PromotionError>>;
|
|
216
|
+
/** Back to the scope's previous live version, or a named earlier one. */
|
|
217
|
+
rollback(input: RollbackInput): Promise<Result<Promotion, PromotionError>>;
|
|
218
|
+
/**
|
|
219
|
+
* Remove the scope's own pin: it falls back to the next scope up. With
|
|
220
|
+
* gate policies, `gate-policy-needs-pin` when that would leave a gated
|
|
221
|
+
* scope resolving to the latest version.
|
|
222
|
+
*/
|
|
223
|
+
unpin(input: UnpinInput): Promise<Result<Promotion, PromotionError>>;
|
|
224
|
+
list(input: ListPromotionsInput): Promise<{
|
|
225
|
+
readonly data: readonly Promotion[];
|
|
226
|
+
readonly nextCursor?: Cursor;
|
|
227
|
+
}>;
|
|
228
|
+
get(tenantId: TenantId, promotionId: string): Promise<Promotion | null>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The bindings behind an agent's live versions and promotions (`createApp({ agentReleases })`). */
|
|
232
|
+
export interface AgentReleaseBindings {
|
|
233
|
+
readonly live: LiveVersionBinding;
|
|
234
|
+
readonly promotions: PromotionBinding;
|
|
235
|
+
/** Gate policies (evals step 4b). Absent: no gates, every promotion goes through as before. */
|
|
236
|
+
readonly gatePolicies?: GatePolicyBinding;
|
|
237
|
+
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
denyPayload,
|
|
29
29
|
} from '@kindgi/authz';
|
|
30
30
|
|
|
31
|
+
import { toWireError } from '../errors.js';
|
|
31
32
|
import type { AppEnv } from '../types.js';
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -97,9 +98,24 @@ export function createAuthorizer(binding: AuthzCheckBinding): Authorizer {
|
|
|
97
98
|
const resource = await getResource(c);
|
|
98
99
|
const decision = await checkInternal(c, action, resource);
|
|
99
100
|
if (!decision.allowed) {
|
|
100
|
-
|
|
101
|
+
// In the error envelope every route answers in, so clients type it
|
|
102
|
+
// (`permission-denied` is an auth error, forbidden); what was
|
|
103
|
+
// denied, and why, is in `details`.
|
|
104
|
+
const deny = denyPayload(action, resource.type, resource.id, decision.reason);
|
|
105
|
+
const requestId = c.get('requestId');
|
|
101
106
|
c.status(403);
|
|
102
|
-
return c.json(
|
|
107
|
+
return c.json(
|
|
108
|
+
toWireError(
|
|
109
|
+
{
|
|
110
|
+
code: deny.code,
|
|
111
|
+
message: `Permission denied: ${deny.reason}`,
|
|
112
|
+
action: deny.action,
|
|
113
|
+
resource: deny.resource,
|
|
114
|
+
reason: deny.reason,
|
|
115
|
+
},
|
|
116
|
+
typeof requestId === 'string' ? requestId : '',
|
|
117
|
+
),
|
|
118
|
+
);
|
|
103
119
|
}
|
|
104
120
|
return next();
|
|
105
121
|
};
|