@kindgi/api 0.1.4-rc.1 → 0.1.4-rc.3
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/hitl-binding.d.ts +5 -0
- package/dist/hitl-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 +512 -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 +40 -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 +7008 -3624
- 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/hitl-binding.ts +5 -0
- 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 +619 -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 +58 -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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AgentRegistryBinding } from './agent-binding.js';
|
|
2
|
+
import type { FlowRegistryBinding } from './flow-binding.js';
|
|
3
|
+
import type { GuardrailRegistryBinding } from './guardrail-binding.js';
|
|
4
|
+
import type { ToolRegistryBinding } from './tool-binding.js';
|
|
5
|
+
/**
|
|
6
|
+
* A deploy's primitive came back from its registry with an outcome other
|
|
7
|
+
* than `ok` or `already-registered` (e.g. `project-not-found`): the deploy
|
|
8
|
+
* stops, what it published is rolled back, and the route answers with
|
|
9
|
+
* that outcome's own status and code (T205). Before, such an outcome was
|
|
10
|
+
* skipped, and the deployment was recorded without that primitive.
|
|
11
|
+
*/
|
|
12
|
+
export declare class PublishRefused extends Error {
|
|
13
|
+
readonly primitive: 'tool' | 'guardrail' | 'agent' | 'flow';
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly code: Exclude<Awaited<ReturnType<ToolRegistryBinding['publish']>>['kind'] | Awaited<ReturnType<GuardrailRegistryBinding['register']>>['kind'] | Awaited<ReturnType<AgentRegistryBinding['publish']>>['kind'] | Awaited<ReturnType<FlowRegistryBinding['publish']>>['kind'], 'ok' | 'already-registered'>;
|
|
16
|
+
constructor(primitive: 'tool' | 'guardrail' | 'agent' | 'flow', id: string, code: Exclude<Awaited<ReturnType<ToolRegistryBinding['publish']>>['kind'] | Awaited<ReturnType<GuardrailRegistryBinding['register']>>['kind'] | Awaited<ReturnType<AgentRegistryBinding['publish']>>['kind'] | Awaited<ReturnType<FlowRegistryBinding['publish']>>['kind'], 'ok' | 'already-registered'>);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=publish-refused.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-refused.d.ts","sourceRoot":"","sources":["../src/publish-refused.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAErC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM;IACnB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAClB,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAC3D,OAAO,CAAC,UAAU,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GACjE,OAAO,CAAC,UAAU,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAC5D,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC7D,IAAI,GAAG,oBAAoB,CAC5B;gBARQ,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,EAClD,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,OAAO,CAClB,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAC3D,OAAO,CAAC,UAAU,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GACjE,OAAO,CAAC,UAAU,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAC5D,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC7D,IAAI,GAAG,oBAAoB,CAC5B;CAKJ"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* A deploy's primitive came back from its registry with an outcome other
|
|
5
|
+
* than `ok` or `already-registered` (e.g. `project-not-found`): the deploy
|
|
6
|
+
* stops, what it published is rolled back, and the route answers with
|
|
7
|
+
* that outcome's own status and code (T205). Before, such an outcome was
|
|
8
|
+
* skipped, and the deployment was recorded without that primitive.
|
|
9
|
+
*/
|
|
10
|
+
export class PublishRefused extends Error {
|
|
11
|
+
primitive;
|
|
12
|
+
id;
|
|
13
|
+
code;
|
|
14
|
+
constructor(primitive, id, code) {
|
|
15
|
+
super(`The ${primitive} ${id} wasn't published: ${code}`);
|
|
16
|
+
this.primitive = primitive;
|
|
17
|
+
this.id = id;
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.name = 'PublishRefused';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=publish-refused.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-refused.js","sourceRoot":"","sources":["../src/publish-refused.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAOjC;;;;;;GAMG;AACH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAE5B;IACA;IACA;IAHX,YACW,SAAkD,EAClD,EAAU,EACV,IAMR;QAED,KAAK,CAAC,OAAO,SAAS,IAAI,EAAE,sBAAsB,IAAI,EAAE,CAAC,CAAC;QAVjD,cAAS,GAAT,SAAS,CAAyC;QAClD,OAAE,GAAF,EAAE,CAAQ;QACV,SAAI,GAAJ,IAAI,CAMZ;QAGD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -29,9 +29,12 @@ export interface RetentionBinding {
|
|
|
29
29
|
export interface RetentionScheduledInput {
|
|
30
30
|
readonly tenantId: TenantId;
|
|
31
31
|
readonly domain?: RetentionDomain;
|
|
32
|
+
/** Rows per domain at most: each domain is read up to `limit`. */
|
|
32
33
|
readonly limit: number;
|
|
33
34
|
readonly pastGraceOnly?: boolean;
|
|
34
35
|
readonly now?: Date;
|
|
36
|
+
/** Where a previous page stopped (its `nextCursor`, the runtime's own encoding). */
|
|
37
|
+
readonly cursor?: string;
|
|
35
38
|
}
|
|
36
39
|
export interface RetentionScheduledItem {
|
|
37
40
|
readonly domain: RetentionDomain;
|
|
@@ -49,6 +52,30 @@ export interface RetentionScheduledPage {
|
|
|
49
52
|
readonly domainsMissingAdapter: readonly RetentionDomain[];
|
|
50
53
|
/** Domains that have an adapter but no matching policy — tombstones sit forever. */
|
|
51
54
|
readonly unpolicedDomains: readonly RetentionDomain[];
|
|
55
|
+
/** Domains more than one retention policy covers (see `RetentionPolicyConflict`). */
|
|
56
|
+
readonly conflicts?: readonly RetentionPolicyConflict[];
|
|
57
|
+
/**
|
|
58
|
+
* More rows are scheduled than this page holds: some domain stopped at
|
|
59
|
+
* `limit`. A runtime that doesn't say leaves it out, and the route then
|
|
60
|
+
* reports `true` when some domain's rows fill `limit` (there may be more).
|
|
61
|
+
*/
|
|
62
|
+
readonly hasMore?: boolean;
|
|
63
|
+
/** Pass as `cursor` to continue where this page stopped. Absent: nothing more, or no way to continue. */
|
|
64
|
+
readonly nextCursor?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A domain more than one retention policy covers. Publishing refuses a
|
|
68
|
+
* second policy for a domain (`409 policy-scope-taken`), so only policies
|
|
69
|
+
* stored before that rule can do this. The one whose latest version is
|
|
70
|
+
* highest applies, and on equal versions the lower policy id; unregister
|
|
71
|
+
* the others.
|
|
72
|
+
*/
|
|
73
|
+
export interface RetentionPolicyConflict {
|
|
74
|
+
readonly domain: RetentionDomain;
|
|
75
|
+
/** Every policy id that covers the domain, sorted. */
|
|
76
|
+
readonly policyIds: readonly string[];
|
|
77
|
+
/** The one of them that applies. */
|
|
78
|
+
readonly appliedPolicyId: string;
|
|
52
79
|
}
|
|
53
80
|
export interface RetentionSweepInput {
|
|
54
81
|
readonly tenantId: TenantId;
|
|
@@ -65,5 +92,7 @@ export interface RetentionSweepResult {
|
|
|
65
92
|
readonly missingAdapter?: true;
|
|
66
93
|
}[];
|
|
67
94
|
readonly totalPurged: number;
|
|
95
|
+
/** Domains more than one retention policy covers (see `RetentionPolicyConflict`). */
|
|
96
|
+
readonly conflicts?: readonly RetentionPolicyConflict[];
|
|
68
97
|
}
|
|
69
98
|
//# sourceMappingURL=retention-binding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention-binding.d.ts","sourceRoot":"","sources":["../src/retention-binding.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,SAAS,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAE3E;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"retention-binding.d.ts","sourceRoot":"","sources":["../src/retention-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { LiveScope } from '@kindgi/types';
|
|
3
|
+
import type { ProjectBinding } from '@kindgi/platform';
|
|
4
|
+
import type { AgentRegistryBinding } from '../agent-binding.js';
|
|
5
|
+
import type { EvalRunBinding } from '../eval-run-binding.js';
|
|
6
|
+
import type { AgentReleaseBindings, Promotion } from '../live-version-binding.js';
|
|
7
|
+
import type { AppEnv } from '../types.js';
|
|
8
|
+
/** The routes that change what's live: authorized as `promote` on the agent, not `admin`. */
|
|
9
|
+
export declare function isPromotionWrite(method: string, path: string): boolean;
|
|
10
|
+
/** `POST …/promotions/check` changes nothing: `read` on the agent. */
|
|
11
|
+
export declare function isPromotionCheck(method: string, path: string): boolean;
|
|
12
|
+
/** What the gate reads besides the releases: comparisons, and a project's org. */
|
|
13
|
+
export interface AgentReleaseGateDeps {
|
|
14
|
+
readonly evalRuns?: EvalRunBinding;
|
|
15
|
+
readonly projects?: ProjectBinding;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Live versions of an agent per scope, and the promotions that set them
|
|
19
|
+
* (evals step 4):
|
|
20
|
+
* GET /:agentId/live the version a run would use for a project and segment path
|
|
21
|
+
* GET /:agentId/live-versions every pin
|
|
22
|
+
* POST /:agentId/promotions make a version live for a scope, through its gate
|
|
23
|
+
* (201 promoted, 202 waiting for approval, 422 gate-failed)
|
|
24
|
+
* POST /:agentId/promotions/check what the gate would say, writing nothing
|
|
25
|
+
* GET /:agentId/gate-policy the gate policy that applies to a scope
|
|
26
|
+
* GET /:agentId/promotions[/:id] the history
|
|
27
|
+
* POST /:agentId/live/rollback back to the scope's previous live version
|
|
28
|
+
* POST /:agentId/live/unpin remove the scope's pin (it falls back to the scope above)
|
|
29
|
+
*/
|
|
30
|
+
export declare function mountAgentReleaseRoutes(r: Hono<AppEnv>, registry: AgentRegistryBinding, releases: AgentReleaseBindings, deps?: AgentReleaseGateDeps): void;
|
|
31
|
+
/** `?scopeKind=tenant|org|project|segment&scopeId=…&segment=key:value` for the history filter. */
|
|
32
|
+
export declare function scopeFromQuery(query: (name: string) => string | undefined, segmentValues: readonly string[]): {
|
|
33
|
+
kind: 'ok';
|
|
34
|
+
scope?: LiveScope;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'err';
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function serializePromotion(p: Promotion): Record<string, unknown>;
|
|
40
|
+
//# sourceMappingURL=agent-releases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-releases.d.ts","sourceRoot":"","sources":["../../src/routes/agent-releases.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAU,SAAS,EAA6C,MAAM,eAAe,CAAC;AAElG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEhE,OAAO,KAAK,EAAW,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAItE,OAAO,KAAK,EACV,oBAAoB,EAEpB,SAAS,EAEV,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQ1C,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,EACf,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,oBAAoB,EAC9B,IAAI,GAAE,oBAAyB,GAC9B,IAAI,CAkRN;AAgMD,kGAAkG;AAClG,wBAAgB,cAAc,CAC5B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAC3C,aAAa,EAAE,SAAS,MAAM,EAAE,GAC/B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA0BtE;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBxE"}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
import { statusFor, toWireError } from '../errors.js';
|
|
4
|
+
import { evaluateGate } from '../gate.js';
|
|
5
|
+
import { serializeGatePolicy } from './gate-policy-wire.js';
|
|
6
|
+
import { liveScopeToWire, parseLiveScopeBody } from './live-scope-wire.js';
|
|
7
|
+
import { clampLimit } from './pagination.js';
|
|
8
|
+
import { parseSegmentsQuery } from './segments.js';
|
|
9
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
/** The routes that change what's live: authorized as `promote` on the agent, not `admin`. */
|
|
11
|
+
export function isPromotionWrite(method, path) {
|
|
12
|
+
return method === 'POST' && /\/(promotions|live\/rollback|live\/unpin)$/.test(path);
|
|
13
|
+
}
|
|
14
|
+
/** `POST …/promotions/check` changes nothing: `read` on the agent. */
|
|
15
|
+
export function isPromotionCheck(method, path) {
|
|
16
|
+
return method === 'POST' && path.endsWith('/promotions/check');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Live versions of an agent per scope, and the promotions that set them
|
|
20
|
+
* (evals step 4):
|
|
21
|
+
* GET /:agentId/live the version a run would use for a project and segment path
|
|
22
|
+
* GET /:agentId/live-versions every pin
|
|
23
|
+
* POST /:agentId/promotions make a version live for a scope, through its gate
|
|
24
|
+
* (201 promoted, 202 waiting for approval, 422 gate-failed)
|
|
25
|
+
* POST /:agentId/promotions/check what the gate would say, writing nothing
|
|
26
|
+
* GET /:agentId/gate-policy the gate policy that applies to a scope
|
|
27
|
+
* GET /:agentId/promotions[/:id] the history
|
|
28
|
+
* POST /:agentId/live/rollback back to the scope's previous live version
|
|
29
|
+
* POST /:agentId/live/unpin remove the scope's pin (it falls back to the scope above)
|
|
30
|
+
*/
|
|
31
|
+
export function mountAgentReleaseRoutes(r, registry, releases, deps = {}) {
|
|
32
|
+
r.get('/:agentId/live', async (c) => {
|
|
33
|
+
const requestId = c.get('requestId');
|
|
34
|
+
const tenantId = c.get('tenantId');
|
|
35
|
+
const agentId = c.req.param('agentId');
|
|
36
|
+
const projectId = c.req.query('projectId');
|
|
37
|
+
if (projectId !== undefined && !UUID_RE.test(projectId)) {
|
|
38
|
+
c.status(statusFor('bad-input'));
|
|
39
|
+
return c.json(toWireError({ code: 'bad-input', message: '`projectId` must be a project id (a UUID)' }, requestId));
|
|
40
|
+
}
|
|
41
|
+
const segments = parseSegmentsQuery(c.req.queries('segment') ?? []);
|
|
42
|
+
if (segments.kind === 'err') {
|
|
43
|
+
c.status(statusFor('bad-input'));
|
|
44
|
+
return c.json(toWireError({ code: 'bad-input', message: segments.message }, requestId));
|
|
45
|
+
}
|
|
46
|
+
if (segments.segments !== undefined && projectId === undefined) {
|
|
47
|
+
c.status(statusFor('bad-input'));
|
|
48
|
+
return c.json(toWireError({ code: 'bad-input', message: 'A `segment` path needs a `projectId`' }, requestId));
|
|
49
|
+
}
|
|
50
|
+
const resolved = await releases.live.resolve({
|
|
51
|
+
tenantId,
|
|
52
|
+
agentId,
|
|
53
|
+
...(projectId !== undefined && { projectId: projectId }),
|
|
54
|
+
...(segments.segments !== undefined && { segments: segments.segments }),
|
|
55
|
+
});
|
|
56
|
+
if (resolved !== null) {
|
|
57
|
+
return c.json({
|
|
58
|
+
agentId,
|
|
59
|
+
version: resolved.version,
|
|
60
|
+
via: 'live',
|
|
61
|
+
liveScope: liveScopeToWire(resolved.scope),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Nothing pinned on the way up: what a run gets is the latest registered version.
|
|
65
|
+
const latest = await registry.get({ tenantId, agentId: agentId });
|
|
66
|
+
if (latest === null) {
|
|
67
|
+
c.status(statusFor('agent-not-found'));
|
|
68
|
+
return c.json(toWireError({ code: 'agent-not-found', message: `No agent "${agentId}" is registered` }, requestId));
|
|
69
|
+
}
|
|
70
|
+
return c.json({ agentId, version: latest.version, via: 'latest' });
|
|
71
|
+
});
|
|
72
|
+
r.get('/:agentId/live-versions', async (c) => {
|
|
73
|
+
const tenantId = c.get('tenantId');
|
|
74
|
+
const pins = await releases.live.list({ tenantId, agentId: c.req.param('agentId') });
|
|
75
|
+
return c.json({
|
|
76
|
+
data: pins.map((p) => ({
|
|
77
|
+
agentId: p.agentId,
|
|
78
|
+
scope: liveScopeToWire(p.scope),
|
|
79
|
+
version: p.version,
|
|
80
|
+
promotionId: p.promotionId,
|
|
81
|
+
setAt: p.setAt,
|
|
82
|
+
})),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
r.post('/:agentId/promotions', async (c) => {
|
|
86
|
+
const requestId = c.get('requestId');
|
|
87
|
+
const parsed = await readPromotionBody(c);
|
|
88
|
+
if (parsed.kind === 'err')
|
|
89
|
+
return badInput(c, requestId, parsed.message);
|
|
90
|
+
const tenantId = c.get('tenantId');
|
|
91
|
+
const agentId = c.req.param('agentId');
|
|
92
|
+
const input = {
|
|
93
|
+
tenantId,
|
|
94
|
+
agentId,
|
|
95
|
+
version: parsed.value.version,
|
|
96
|
+
scope: parsed.value.scope,
|
|
97
|
+
requestedBy: actorOf(c),
|
|
98
|
+
...(parsed.value.reason !== undefined && { reason: parsed.value.reason }),
|
|
99
|
+
...(parsed.value.evalRunId !== undefined && { evalRunId: parsed.value.evalRunId }),
|
|
100
|
+
};
|
|
101
|
+
const policy = await policyFor(releases, tenantId, agentId, parsed.value.scope);
|
|
102
|
+
if (releases.promotions.request === undefined) {
|
|
103
|
+
if (policy !== null) {
|
|
104
|
+
c.status(statusFor('promotion-gate-unsupported'));
|
|
105
|
+
return c.json(toWireError({
|
|
106
|
+
code: 'promotion-gate-unsupported',
|
|
107
|
+
message: `Gate policy ${policy.id} ${policy.version} applies to this promotion, and this deployment can't record a gated promotion yet.`,
|
|
108
|
+
}, requestId));
|
|
109
|
+
}
|
|
110
|
+
// No gate, and a binding from before gates: promote as before.
|
|
111
|
+
const outcome = await releases.promotions.promote(input);
|
|
112
|
+
if (outcome.kind === 'err')
|
|
113
|
+
return failed(c, requestId, outcome.error);
|
|
114
|
+
c.status(201);
|
|
115
|
+
return c.json(serializePromotion(outcome.value));
|
|
116
|
+
}
|
|
117
|
+
const gate = await runGate(registry, releases, deps, { ...parsed.value, tenantId, agentId }, policy);
|
|
118
|
+
if (gate.kind === 'err')
|
|
119
|
+
return failed(c, requestId, gate.error);
|
|
120
|
+
const outcome = await releases.promotions.request({
|
|
121
|
+
...input,
|
|
122
|
+
gate: {
|
|
123
|
+
policy: policy === null ? null : { id: policy.id, version: policy.version },
|
|
124
|
+
checks: gate.result.checks,
|
|
125
|
+
passed: gate.result.passed,
|
|
126
|
+
...(gate.result.approval !== undefined && { approval: gate.result.approval }),
|
|
127
|
+
servingVersion: gate.servingVersion,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
if (outcome.kind === 'err')
|
|
131
|
+
return failed(c, requestId, outcome.error);
|
|
132
|
+
const promotion = outcome.value;
|
|
133
|
+
if (promotion.status === 'refused') {
|
|
134
|
+
const checks = promotion.checks ?? gate.result.checks;
|
|
135
|
+
c.status(statusFor('gate-failed'));
|
|
136
|
+
return c.json(toWireError({
|
|
137
|
+
code: 'gate-failed',
|
|
138
|
+
message: `The gate refused ${agentId} ${parsed.value.version}: ${checks
|
|
139
|
+
.filter((check) => !check.passed)
|
|
140
|
+
.map((check) => check.message)
|
|
141
|
+
.join(' ')}`,
|
|
142
|
+
promotionId: promotion.id,
|
|
143
|
+
policy: promotion.policy ?? null,
|
|
144
|
+
checks,
|
|
145
|
+
}, requestId));
|
|
146
|
+
}
|
|
147
|
+
c.status(promotion.status === 'pending-approval' ? 202 : 201);
|
|
148
|
+
return c.json(serializePromotion(promotion));
|
|
149
|
+
});
|
|
150
|
+
r.post('/:agentId/promotions/check', async (c) => {
|
|
151
|
+
const requestId = c.get('requestId');
|
|
152
|
+
const parsed = await readPromotionBody(c);
|
|
153
|
+
if (parsed.kind === 'err')
|
|
154
|
+
return badInput(c, requestId, parsed.message);
|
|
155
|
+
const tenantId = c.get('tenantId');
|
|
156
|
+
const agentId = c.req.param('agentId');
|
|
157
|
+
const policy = await policyFor(releases, tenantId, agentId, parsed.value.scope);
|
|
158
|
+
const gate = await runGate(registry, releases, deps, { ...parsed.value, tenantId, agentId }, policy);
|
|
159
|
+
if (gate.kind === 'err')
|
|
160
|
+
return failed(c, requestId, gate.error);
|
|
161
|
+
const { result } = gate;
|
|
162
|
+
return c.json({
|
|
163
|
+
outcome: !result.passed
|
|
164
|
+
? 'gate-failed'
|
|
165
|
+
: result.approval !== undefined
|
|
166
|
+
? 'needs-approval'
|
|
167
|
+
: 'would-promote',
|
|
168
|
+
policy: policy === null ? null : { id: policy.id, version: policy.version },
|
|
169
|
+
checks: result.checks,
|
|
170
|
+
...(result.approval !== undefined && { approval: result.approval }),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
r.get('/:agentId/gate-policy', async (c) => {
|
|
174
|
+
const requestId = c.get('requestId');
|
|
175
|
+
const scope = scopeFromQuery((n) => c.req.query(n), c.req.queries('segment') ?? []);
|
|
176
|
+
if (scope.kind === 'err')
|
|
177
|
+
return badInput(c, requestId, scope.message);
|
|
178
|
+
if (scope.scope === undefined) {
|
|
179
|
+
return badInput(c, requestId, '`scopeKind` is required: the scope a promotion would be for');
|
|
180
|
+
}
|
|
181
|
+
const policy = await policyFor(releases, c.get('tenantId'), c.req.param('agentId'), scope.scope);
|
|
182
|
+
return c.json({ policy: policy === null ? null : serializeGatePolicy(policy) });
|
|
183
|
+
});
|
|
184
|
+
r.get('/:agentId/promotions', async (c) => {
|
|
185
|
+
const requestId = c.get('requestId');
|
|
186
|
+
const scope = scopeFromQuery((n) => c.req.query(n), c.req.queries('segment') ?? []);
|
|
187
|
+
if (scope.kind === 'err')
|
|
188
|
+
return badInput(c, requestId, scope.message);
|
|
189
|
+
const cursor = c.req.query('cursor');
|
|
190
|
+
const page = await releases.promotions.list({
|
|
191
|
+
tenantId: c.get('tenantId'),
|
|
192
|
+
agentId: c.req.param('agentId'),
|
|
193
|
+
limit: clampLimit(c.req.query('limit')),
|
|
194
|
+
...(scope.scope !== undefined && { scope: scope.scope }),
|
|
195
|
+
...(cursor !== undefined && cursor.length > 0 && { cursor: cursor }),
|
|
196
|
+
});
|
|
197
|
+
return c.json({
|
|
198
|
+
data: page.data.map(serializePromotion),
|
|
199
|
+
hasMore: page.nextCursor !== undefined,
|
|
200
|
+
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor }),
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
r.get('/:agentId/promotions/:promotionId', async (c) => {
|
|
204
|
+
const requestId = c.get('requestId');
|
|
205
|
+
const found = await releases.promotions.get(c.get('tenantId'), c.req.param('promotionId'));
|
|
206
|
+
if (found === null || found.agentId !== c.req.param('agentId')) {
|
|
207
|
+
c.status(statusFor('promotion-not-found'));
|
|
208
|
+
return c.json(toWireError({ code: 'promotion-not-found', message: 'No such promotion for this agent' }, requestId));
|
|
209
|
+
}
|
|
210
|
+
return c.json(serializePromotion(found));
|
|
211
|
+
});
|
|
212
|
+
r.post('/:agentId/live/rollback', async (c) => {
|
|
213
|
+
const requestId = c.get('requestId');
|
|
214
|
+
const body = await readBody(c);
|
|
215
|
+
if (body === undefined)
|
|
216
|
+
return badJson(c, requestId);
|
|
217
|
+
const scope = parseLiveScopeBody(body.scope);
|
|
218
|
+
if (scope.kind === 'err')
|
|
219
|
+
return badInput(c, requestId, scope.message);
|
|
220
|
+
if (body.toVersion !== undefined &&
|
|
221
|
+
(typeof body.toVersion !== 'string' || body.toVersion === '')) {
|
|
222
|
+
return badInput(c, requestId, '`toVersion` must be a version when supplied');
|
|
223
|
+
}
|
|
224
|
+
const text = optionalText(body, ['reason']);
|
|
225
|
+
if (text.kind === 'err')
|
|
226
|
+
return badInput(c, requestId, text.message);
|
|
227
|
+
const outcome = await releases.promotions.rollback({
|
|
228
|
+
tenantId: c.get('tenantId'),
|
|
229
|
+
agentId: c.req.param('agentId'),
|
|
230
|
+
scope: scope.scope,
|
|
231
|
+
requestedBy: actorOf(c),
|
|
232
|
+
...(typeof body.toVersion === 'string' && { toVersion: body.toVersion }),
|
|
233
|
+
...text.value,
|
|
234
|
+
});
|
|
235
|
+
if (outcome.kind === 'err')
|
|
236
|
+
return failed(c, requestId, outcome.error);
|
|
237
|
+
return c.json(serializePromotion(outcome.value));
|
|
238
|
+
});
|
|
239
|
+
r.post('/:agentId/live/unpin', async (c) => {
|
|
240
|
+
const requestId = c.get('requestId');
|
|
241
|
+
const body = await readBody(c);
|
|
242
|
+
if (body === undefined)
|
|
243
|
+
return badJson(c, requestId);
|
|
244
|
+
const scope = parseLiveScopeBody(body.scope);
|
|
245
|
+
if (scope.kind === 'err')
|
|
246
|
+
return badInput(c, requestId, scope.message);
|
|
247
|
+
const text = optionalText(body, ['reason']);
|
|
248
|
+
if (text.kind === 'err')
|
|
249
|
+
return badInput(c, requestId, text.message);
|
|
250
|
+
const outcome = await releases.promotions.unpin({
|
|
251
|
+
tenantId: c.get('tenantId'),
|
|
252
|
+
agentId: c.req.param('agentId'),
|
|
253
|
+
scope: scope.scope,
|
|
254
|
+
requestedBy: actorOf(c),
|
|
255
|
+
...text.value,
|
|
256
|
+
});
|
|
257
|
+
if (outcome.kind === 'err')
|
|
258
|
+
return failed(c, requestId, outcome.error);
|
|
259
|
+
return c.json(serializePromotion(outcome.value));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function readBody(c) {
|
|
263
|
+
try {
|
|
264
|
+
const body = await c.req.json();
|
|
265
|
+
return body !== null && typeof body === 'object' && !Array.isArray(body)
|
|
266
|
+
? body
|
|
267
|
+
: undefined;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function badJson(c, requestId) {
|
|
274
|
+
return badInput(c, requestId, 'Request body must be a JSON object');
|
|
275
|
+
}
|
|
276
|
+
function badInput(c, requestId, message) {
|
|
277
|
+
c.status(statusFor('bad-input'));
|
|
278
|
+
return c.json(toWireError({ code: 'bad-input', message }, requestId));
|
|
279
|
+
}
|
|
280
|
+
function failed(c, requestId, error) {
|
|
281
|
+
c.status(statusFor(error.code));
|
|
282
|
+
return c.json(toWireError(error, requestId));
|
|
283
|
+
}
|
|
284
|
+
/** Optional short strings (`reason`, `evalRunId`): absent, or a non-empty string. */
|
|
285
|
+
function optionalText(body, fields) {
|
|
286
|
+
const value = {};
|
|
287
|
+
for (const field of fields) {
|
|
288
|
+
const v = body[field];
|
|
289
|
+
if (v === undefined)
|
|
290
|
+
continue;
|
|
291
|
+
if (typeof v !== 'string' || v.trim() === '' || v.length > 2000) {
|
|
292
|
+
return { kind: 'err', message: `\`${field}\` must be a non-empty string when supplied` };
|
|
293
|
+
}
|
|
294
|
+
value[field] = v;
|
|
295
|
+
}
|
|
296
|
+
return { kind: 'ok', value };
|
|
297
|
+
}
|
|
298
|
+
/** The promotion body: `{version, scope, evalRunId?, reason?}`. */
|
|
299
|
+
async function readPromotionBody(c) {
|
|
300
|
+
const body = await readBody(c);
|
|
301
|
+
if (body === undefined)
|
|
302
|
+
return { kind: 'err', message: 'Request body must be a JSON object' };
|
|
303
|
+
const scope = parseLiveScopeBody(body.scope);
|
|
304
|
+
if (scope.kind === 'err')
|
|
305
|
+
return scope;
|
|
306
|
+
if (typeof body.version !== 'string' || body.version.length === 0) {
|
|
307
|
+
return { kind: 'err', message: '`version` is required: the agent version to make live' };
|
|
308
|
+
}
|
|
309
|
+
const text = optionalText(body, ['reason', 'evalRunId']);
|
|
310
|
+
if (text.kind === 'err')
|
|
311
|
+
return text;
|
|
312
|
+
return {
|
|
313
|
+
kind: 'ok',
|
|
314
|
+
value: { version: body.version, scope: scope.scope, ...text.value },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** The gate policy for a promotion of `agentId` for `scope`; `null` when none applies. */
|
|
318
|
+
async function policyFor(releases, tenantId, agentId, scope) {
|
|
319
|
+
return releases.gatePolicies === undefined
|
|
320
|
+
? null
|
|
321
|
+
: releases.gatePolicies.resolve({ tenantId, agentId, scope });
|
|
322
|
+
}
|
|
323
|
+
/** The scope's coordinates, as a run in it would resolve its version. */
|
|
324
|
+
function coordinatesOf(scope) {
|
|
325
|
+
switch (scope.kind) {
|
|
326
|
+
case 'tenant':
|
|
327
|
+
return {};
|
|
328
|
+
case 'org':
|
|
329
|
+
return { orgId: scope.orgId };
|
|
330
|
+
case 'project':
|
|
331
|
+
return { projectId: scope.projectId };
|
|
332
|
+
case 'segment':
|
|
333
|
+
return { projectId: scope.projectId, segments: scope.path };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** A finished comparison's summary, or `null` for any other eval run. */
|
|
337
|
+
function summaryOf(run) {
|
|
338
|
+
const summary = run.result?.summary;
|
|
339
|
+
return run.kind === 'judged' && summary !== null && typeof summary === 'object'
|
|
340
|
+
? summary
|
|
341
|
+
: null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The gate for a promotion request: what serves the scope now, the
|
|
345
|
+
* comparison it names, and the policy's checks against them. With no
|
|
346
|
+
* policy there's nothing to check.
|
|
347
|
+
*/
|
|
348
|
+
async function runGate(registry, releases, deps, req, policy) {
|
|
349
|
+
const { tenantId, agentId } = req;
|
|
350
|
+
const err = (code, message) => ({
|
|
351
|
+
kind: 'err',
|
|
352
|
+
error: { code, message },
|
|
353
|
+
});
|
|
354
|
+
const promoted = await registry.getVersion({
|
|
355
|
+
tenantId,
|
|
356
|
+
agentId: agentId,
|
|
357
|
+
version: req.version,
|
|
358
|
+
});
|
|
359
|
+
if (promoted === null || promoted.unregisteredAt !== undefined) {
|
|
360
|
+
return err('agent-version-not-found', `${agentId} has no active version ${req.version}`);
|
|
361
|
+
}
|
|
362
|
+
const live = await releases.live.resolve({ tenantId, agentId, ...coordinatesOf(req.scope) });
|
|
363
|
+
let servingVersion = live?.version;
|
|
364
|
+
if (servingVersion === undefined) {
|
|
365
|
+
const latest = await registry.get({ tenantId, agentId: agentId });
|
|
366
|
+
if (latest === null)
|
|
367
|
+
return err('agent-not-found', `No agent "${agentId}" is registered`);
|
|
368
|
+
servingVersion = latest.version;
|
|
369
|
+
}
|
|
370
|
+
if (policy === null)
|
|
371
|
+
return { kind: 'ok', result: { checks: [], passed: true }, servingVersion };
|
|
372
|
+
let summary = null;
|
|
373
|
+
if (req.evalRunId !== undefined) {
|
|
374
|
+
const run = deps.evalRuns === undefined
|
|
375
|
+
? null
|
|
376
|
+
: await deps.evalRuns.get({ tenantId, runId: req.evalRunId });
|
|
377
|
+
if (run === null)
|
|
378
|
+
return err('eval-run-not-found', `No eval run ${req.evalRunId}`);
|
|
379
|
+
summary = summaryOf(run);
|
|
380
|
+
if (summary === null) {
|
|
381
|
+
return err('bad-input', `Eval run ${req.evalRunId} isn't a finished comparison: it has no comparison summary`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
let summaryProjectOrgId;
|
|
385
|
+
const judgedIn = summary?.scope.projectId;
|
|
386
|
+
if (req.scope.kind === 'org' && judgedIn !== undefined && deps.projects !== undefined) {
|
|
387
|
+
const project = await deps.projects.get(tenantId, judgedIn);
|
|
388
|
+
summaryProjectOrgId = project?.orgId;
|
|
389
|
+
}
|
|
390
|
+
const result = evaluateGate({
|
|
391
|
+
spec: policy.spec,
|
|
392
|
+
promotion: {
|
|
393
|
+
agentId,
|
|
394
|
+
version: req.version,
|
|
395
|
+
pinsDigest: promoted.pinsDigest ?? null,
|
|
396
|
+
scope: req.scope,
|
|
397
|
+
},
|
|
398
|
+
summary,
|
|
399
|
+
servingVersion,
|
|
400
|
+
...(summaryProjectOrgId !== undefined && { summaryProjectOrgId }),
|
|
401
|
+
now: new Date(),
|
|
402
|
+
});
|
|
403
|
+
return { kind: 'ok', result, servingVersion };
|
|
404
|
+
}
|
|
405
|
+
/** Who asked, from the request's principal. */
|
|
406
|
+
function actorOf(c) {
|
|
407
|
+
const actor = c.get('principal')?.actor;
|
|
408
|
+
if (actor === undefined)
|
|
409
|
+
return { kind: 'service', id: 'unknown' };
|
|
410
|
+
return { kind: actor.kind === 'user' ? 'user' : 'service', id: actor.id };
|
|
411
|
+
}
|
|
412
|
+
/** `?scopeKind=tenant|org|project|segment&scopeId=…&segment=key:value` for the history filter. */
|
|
413
|
+
export function scopeFromQuery(query, segmentValues) {
|
|
414
|
+
const kind = query('scopeKind');
|
|
415
|
+
const id = query('scopeId');
|
|
416
|
+
if (kind === undefined || kind === '') {
|
|
417
|
+
return id === undefined
|
|
418
|
+
? { kind: 'ok' }
|
|
419
|
+
: { kind: 'err', message: '`scopeId` needs `scopeKind`' };
|
|
420
|
+
}
|
|
421
|
+
if (kind === 'tenant')
|
|
422
|
+
return { kind: 'ok', scope: { kind: 'tenant' } };
|
|
423
|
+
if (id === undefined || !UUID_RE.test(id)) {
|
|
424
|
+
return { kind: 'err', message: '`scopeId` must be an org or project id (a UUID)' };
|
|
425
|
+
}
|
|
426
|
+
if (kind === 'org')
|
|
427
|
+
return { kind: 'ok', scope: { kind: 'org', orgId: id } };
|
|
428
|
+
if (kind === 'project')
|
|
429
|
+
return { kind: 'ok', scope: { kind: 'project', projectId: id } };
|
|
430
|
+
if (kind === 'segment') {
|
|
431
|
+
const path = parseSegmentsQuery(segmentValues);
|
|
432
|
+
if (path.kind === 'err')
|
|
433
|
+
return path;
|
|
434
|
+
if (path.segments === undefined)
|
|
435
|
+
return { kind: 'err', message: 'A segment scope needs `segment`' };
|
|
436
|
+
return {
|
|
437
|
+
kind: 'ok',
|
|
438
|
+
scope: { kind: 'segment', projectId: id, path: path.segments },
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return { kind: 'err', message: '`scopeKind` must be one of tenant, org, project, segment' };
|
|
442
|
+
}
|
|
443
|
+
export function serializePromotion(p) {
|
|
444
|
+
return {
|
|
445
|
+
id: p.id,
|
|
446
|
+
agentId: p.agentId,
|
|
447
|
+
scope: liveScopeToWire(p.scope),
|
|
448
|
+
action: p.action,
|
|
449
|
+
fromVersion: p.fromVersion,
|
|
450
|
+
toVersion: p.toVersion,
|
|
451
|
+
requestedBy: p.requestedBy,
|
|
452
|
+
...(p.reason !== undefined && { reason: p.reason }),
|
|
453
|
+
...(p.evalRunId !== undefined && { evalRunId: p.evalRunId }),
|
|
454
|
+
createdAt: p.createdAt,
|
|
455
|
+
...(p.status !== undefined && { status: p.status }),
|
|
456
|
+
...(p.policy !== undefined && { policy: p.policy }),
|
|
457
|
+
...(p.checks !== undefined && { checks: p.checks }),
|
|
458
|
+
...(p.approvalId !== undefined && { approvalId: p.approvalId }),
|
|
459
|
+
...(p.resolvedAt !== undefined && { resolvedAt: p.resolvedAt }),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
//# sourceMappingURL=agent-releases.js.map
|