@kici-dev/engine 0.6.0 → 0.7.0
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/audit/access-log-policy.js +1 -0
- package/dist/audit/retention-policy.js +2 -0
- package/dist/billing/subscription-status.d.ts +38 -0
- package/dist/billing/subscription-status.js +63 -0
- package/dist/context/host-match.d.ts +25 -2
- package/dist/context/host-match.js +29 -7
- package/dist/index.d.ts +6 -1
- package/dist/index.js +10 -5
- package/dist/labels/compile.d.ts +3 -1
- package/dist/labels/compile.js +10 -5
- package/dist/labels-canonical.d.ts +36 -0
- package/dist/labels-canonical.js +21 -0
- package/dist/labels-match.d.ts +38 -7
- package/dist/labels-match.js +45 -9
- package/dist/metrics/catalog-policy.js +6 -1
- package/dist/metrics/metric-catalog.generated.d.ts +97 -2
- package/dist/metrics/metric-catalog.generated.js +116 -2
- package/dist/protocol/dashboard-write-operations.d.ts +17 -0
- package/dist/protocol/dashboard-write-operations.js +20 -3
- package/dist/protocol/messages/access-log.d.ts +5 -0
- package/dist/protocol/messages/access-log.js +1 -0
- package/dist/protocol/messages/config-paths.d.ts +20 -0
- package/dist/protocol/messages/config-paths.js +27 -0
- package/dist/protocol/messages/dashboard.d.ts +137 -0
- package/dist/protocol/messages/dashboard.js +80 -2
- package/dist/protocol/messages/event-log.d.ts +6 -0
- package/dist/protocol/messages/event-log.js +6 -0
- package/dist/protocol/messages/orchestrator-agent.d.ts +32 -0
- package/dist/protocol/messages/orchestrator-agent.js +52 -5
- package/dist/protocol/messages/peer.d.ts +3 -0
- package/dist/protocol/messages/peer.js +6 -1
- package/dist/protocol/messages/platform-orchestrator.d.ts +36 -0
- package/dist/protocol/messages/source-registration.d.ts +5 -0
- package/dist/protocol/messages/source-registration.js +8 -0
- package/dist/provenance/id-token-event-claims.d.ts +142 -0
- package/dist/provenance/id-token-event-claims.js +113 -0
- package/dist/provenance/statement-hash.d.ts +14 -5
- package/dist/provenance/statement-hash.js +14 -5
- package/dist/provenance/verify.d.ts +21 -0
- package/dist/provenance/verify.js +26 -16
- package/dist/regex-flags.d.ts +32 -0
- package/dist/regex-flags.js +43 -0
- package/dist/trigger/compiled-matchers.d.ts +9 -2
- package/dist/trigger/compiled-matchers.js +14 -5
- package/dist/trigger/decision-trace.d.ts +1 -1
- package/dist/trigger/decision-trace.js +1 -1
- package/dist/trigger/text-match.d.ts +8 -3
- package/dist/trigger/text-match.js +11 -5
- package/dist/trigger/trigger-event-type.d.ts +16 -0
- package/dist/trigger/trigger-event-type.js +25 -1
- package/dist/trigger/types.d.ts +55 -4
- package/dist/trigger/types.js +5 -1
- package/dist/ws/close-codes.d.ts +8 -0
- package/dist/ws/close-codes.js +9 -1
- package/package.json +1 -1
- package/sbom.spdx.json +5 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "../../rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import { provenanceContextSchema } from "../../provenance/id-token-event-claims.js";
|
|
2
3
|
import { ExecutionJobStatus, ExecutionStepStatus, StepConcurrencyKind } from "./execution-status.js";
|
|
3
4
|
import { dsseEnvelopeSchema } from "../../provenance/dsse.js";
|
|
4
5
|
import { approvalTimeoutSecondsSchema, approverClauseSchema } from "../../approval/types.js";
|
|
@@ -103,8 +104,19 @@ const jobDispatchSchema = z.object({
|
|
|
103
104
|
concurrencyWaitTimeoutMs: z.coerce.number().optional(),
|
|
104
105
|
/** URL or file:// path to a pre-packed `.kici/` source tarball. If present, agent extracts it into workDir instead of cloning the repo. */
|
|
105
106
|
sourceTarUrl: z.string().optional(),
|
|
106
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* @deprecated Use `sourceTarDigest`. Despite its name this carries the
|
|
109
|
+
* workflow `contentHash`, not a hash of the tarball bytes, so an agent
|
|
110
|
+
* could not verify a restored tarball against it. Kept on the wire for
|
|
111
|
+
* older agents; removed at v1.0.0.
|
|
112
|
+
*/
|
|
107
113
|
sourceTarHash: z.string().optional(),
|
|
114
|
+
/**
|
|
115
|
+
* SHA-256 of the source tarball's own bytes, for integrity verification
|
|
116
|
+
* before extraction. The sibling of `depsHash`, which has always carried
|
|
117
|
+
* the dependency tarball's real digest.
|
|
118
|
+
*/
|
|
119
|
+
sourceTarDigest: z.string().optional(),
|
|
108
120
|
/** URL or file:// path to pre-built dependency tarball. If present, agent extracts to .kici/node_modules/ instead of running install. */
|
|
109
121
|
depsUrl: z.string().optional(),
|
|
110
122
|
/** SHA-256 hash of the dependency tarball for integrity verification. */
|
|
@@ -113,6 +125,22 @@ const jobDispatchSchema = z.object({
|
|
|
113
125
|
requestId: z.string().optional(),
|
|
114
126
|
/** Base64-encoded X25519 public key for the workflow run (for encrypting secret outputs). */
|
|
115
127
|
runPublicKey: z.string().optional(),
|
|
128
|
+
/**
|
|
129
|
+
* The orchestrator's own view of the build, for a provenance statement the
|
|
130
|
+
* agent has to freeze before its identity token exists (the deferred path).
|
|
131
|
+
*
|
|
132
|
+
* Every field is what `buildIdTokenClaims` derives from the run row, so a
|
|
133
|
+
* frozen statement built from this is field-for-field what a live mint
|
|
134
|
+
* would have produced — and the server can therefore cross-check it. The
|
|
135
|
+
* agent's local guess is NOT: the job's checkout `ref` is a pull request's
|
|
136
|
+
* HEAD branch where the claim is the BASE branch, and `workflowRef` here is
|
|
137
|
+
* the `<name>@<sha>` claim rather than a global workflow's clone ref.
|
|
138
|
+
*
|
|
139
|
+
* Additive and optional: an older orchestrator omits it and the agent falls
|
|
140
|
+
* back to its local guess. That fallback statement fails the capture
|
|
141
|
+
* cross-check, so the defer is dropped rather than stored unchecked.
|
|
142
|
+
*/
|
|
143
|
+
provenanceContext: provenanceContextSchema.optional(),
|
|
116
144
|
/** Plain outputs from upstream jobs (keyed by job name, then by step name). Populated for downstream jobs with `needs` dependencies. */
|
|
117
145
|
upstreamJobOutputs: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
|
118
146
|
/** Terminal status of each upstream job (keyed by job name; per-child for fan-out). Powers `ctx.needs.<job>.status`. */
|
|
@@ -545,7 +573,18 @@ const cacheUploadRequestSchema = z.object({
|
|
|
545
573
|
* and hashed it by the time it asks. Optional so an older agent that omits it
|
|
546
574
|
* still gets a usable (lockfile-keyed) URL during a mixed-version rollout.
|
|
547
575
|
*/
|
|
548
|
-
depsHash: z.string().optional()
|
|
576
|
+
depsHash: z.string().optional(),
|
|
577
|
+
/**
|
|
578
|
+
* SHA-256 of the source tarball about to be uploaded. Source uploads only.
|
|
579
|
+
*
|
|
580
|
+
* The source tarball is stored under its own content hash, so the
|
|
581
|
+
* orchestrator needs it to sign the upload URL — the agent has already packed
|
|
582
|
+
* and hashed it by the time it asks. Optional so an older agent that omits it
|
|
583
|
+
* still gets a usable URL during a mixed-version rollout.
|
|
584
|
+
*/
|
|
585
|
+
sourceTarDigest: z.string().optional(),
|
|
586
|
+
/** In-repo `workspace:` sibling closure digest; part of the dep pointer key. */
|
|
587
|
+
siblingsDigest: z.string().optional()
|
|
549
588
|
});
|
|
550
589
|
/** Orchestrator -> Agent: return the pre-signed upload URL. */
|
|
551
590
|
const cacheUploadResponseSchema = z.object({
|
|
@@ -564,7 +603,11 @@ const cacheUploadCompleteSchema = z.object({
|
|
|
564
603
|
platform: z.string(),
|
|
565
604
|
arch: z.string(),
|
|
566
605
|
/** SHA-256 hash of the dependency tarball for integrity verification. Only present for deps uploads. */
|
|
567
|
-
depsHash: z.string().optional()
|
|
606
|
+
depsHash: z.string().optional(),
|
|
607
|
+
/** SHA-256 of the source tarball's own bytes. Only present for source uploads. */
|
|
608
|
+
sourceTarDigest: z.string().optional(),
|
|
609
|
+
/** In-repo `workspace:` sibling closure digest; part of the dep pointer key. */
|
|
610
|
+
siblingsDigest: z.string().optional()
|
|
568
611
|
});
|
|
569
612
|
/** Agent -> Orchestrator: request a user-cache restore (presigned download). */
|
|
570
613
|
const cacheUserRestoreRequestSchema = z.object({
|
|
@@ -659,8 +702,12 @@ const provenanceUploadDeferSchema = z.object({
|
|
|
659
702
|
subjectName: z.string(),
|
|
660
703
|
/** Primary subject digest (lowercase hex). */
|
|
661
704
|
subjectDigest: z.string(),
|
|
662
|
-
/**
|
|
663
|
-
audience
|
|
705
|
+
/**
|
|
706
|
+
* Requested token audience for the later mint. Bounded to match the LIVE
|
|
707
|
+
* mint's `oidcTokenRequestParamsSchema` — the two are the same
|
|
708
|
+
* agent-supplied value and had no reason to differ.
|
|
709
|
+
*/
|
|
710
|
+
audience: z.string().min(1).max(255),
|
|
664
711
|
/** Bundle media type. */
|
|
665
712
|
mediaType: z.string(),
|
|
666
713
|
/** SHA-256 of the frozen DSSE statement payload — the later-mint binding. */
|
|
@@ -173,6 +173,7 @@ export declare const jobRerouteSchema: z.ZodObject<{
|
|
|
173
173
|
providerContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
174
174
|
sourceTarUrl: z.ZodOptional<z.ZodString>;
|
|
175
175
|
sourceTarHash: z.ZodOptional<z.ZodString>;
|
|
176
|
+
sourceTarDigest: z.ZodOptional<z.ZodString>;
|
|
176
177
|
depsUrl: z.ZodOptional<z.ZodString>;
|
|
177
178
|
depsHash: z.ZodOptional<z.ZodString>;
|
|
178
179
|
cloneToken: z.ZodOptional<z.ZodString>;
|
|
@@ -598,6 +599,7 @@ export declare const peerToPeerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
598
599
|
providerContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
599
600
|
sourceTarUrl: z.ZodOptional<z.ZodString>;
|
|
600
601
|
sourceTarHash: z.ZodOptional<z.ZodString>;
|
|
602
|
+
sourceTarDigest: z.ZodOptional<z.ZodString>;
|
|
601
603
|
depsUrl: z.ZodOptional<z.ZodString>;
|
|
602
604
|
depsHash: z.ZodOptional<z.ZodString>;
|
|
603
605
|
cloneToken: z.ZodOptional<z.ZodString>;
|
|
@@ -914,6 +916,7 @@ export declare const peerFromPeerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodOb
|
|
|
914
916
|
providerContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
915
917
|
sourceTarUrl: z.ZodOptional<z.ZodString>;
|
|
916
918
|
sourceTarHash: z.ZodOptional<z.ZodString>;
|
|
919
|
+
sourceTarDigest: z.ZodOptional<z.ZodString>;
|
|
917
920
|
depsUrl: z.ZodOptional<z.ZodString>;
|
|
918
921
|
depsHash: z.ZodOptional<z.ZodString>;
|
|
919
922
|
cloneToken: z.ZodOptional<z.ZodString>;
|
|
@@ -210,8 +210,13 @@ const jobRerouteSchema = z.object({
|
|
|
210
210
|
providerContext: z.record(z.string(), z.unknown()).optional(),
|
|
211
211
|
/** Pre-signed source tarball download URL (cache hit). */
|
|
212
212
|
sourceTarUrl: z.string().optional(),
|
|
213
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* @deprecated Use `sourceTarDigest` — this carries the workflow
|
|
215
|
+
* `contentHash`, not a hash of the tarball bytes.
|
|
216
|
+
*/
|
|
214
217
|
sourceTarHash: z.string().optional(),
|
|
218
|
+
/** SHA-256 of the source tarball's own bytes, for integrity verification. */
|
|
219
|
+
sourceTarDigest: z.string().optional(),
|
|
215
220
|
/** Pre-signed dependency tarball download URL (cache hit). */
|
|
216
221
|
depsUrl: z.string().optional(),
|
|
217
222
|
/** Dependency tarball hash for cache keying. */
|
|
@@ -1110,6 +1110,35 @@ export declare const platformToOrchestratorMessageSchema: z.ZodDiscriminatedUnio
|
|
|
1110
1110
|
}, z.core.$strip>], "type">;
|
|
1111
1111
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
1112
1112
|
cursor: z.ZodOptional<z.ZodString>;
|
|
1113
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1114
|
+
type: z.ZodLiteral<"dashboard.admin-tokens.list">;
|
|
1115
|
+
requestId: z.ZodString;
|
|
1116
|
+
actor: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1117
|
+
type: z.ZodLiteral<"user">;
|
|
1118
|
+
sub: z.ZodString;
|
|
1119
|
+
agent: z.ZodOptional<z.ZodObject<{
|
|
1120
|
+
patId: z.ZodString;
|
|
1121
|
+
label: z.ZodString;
|
|
1122
|
+
}, z.core.$strip>>;
|
|
1123
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1124
|
+
type: z.ZodLiteral<"api_key">;
|
|
1125
|
+
keyId: z.ZodString;
|
|
1126
|
+
ownerSub: z.ZodString;
|
|
1127
|
+
agent: z.ZodOptional<z.ZodObject<{
|
|
1128
|
+
label: z.ZodString;
|
|
1129
|
+
}, z.core.$strip>>;
|
|
1130
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1131
|
+
type: z.ZodLiteral<"service_account">;
|
|
1132
|
+
id: z.ZodString;
|
|
1133
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1134
|
+
type: z.ZodLiteral<"platform_operator">;
|
|
1135
|
+
sub: z.ZodString;
|
|
1136
|
+
reason: z.ZodString;
|
|
1137
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
1138
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1139
|
+
type: z.ZodLiteral<"system">;
|
|
1140
|
+
component: z.ZodString;
|
|
1141
|
+
}, z.core.$strip>], "type">;
|
|
1113
1142
|
}, z.core.$strip>, z.ZodObject<{
|
|
1114
1143
|
type: z.ZodLiteral<"run.rerun.request">;
|
|
1115
1144
|
requestId: z.ZodString;
|
|
@@ -2616,6 +2645,7 @@ export declare const platformToOrchestratorMessageSchema: z.ZodDiscriminatedUnio
|
|
|
2616
2645
|
lockfile_missing: "lockfile_missing";
|
|
2617
2646
|
processed: "processed";
|
|
2618
2647
|
received: "received";
|
|
2648
|
+
shed: "shed";
|
|
2619
2649
|
}>>;
|
|
2620
2650
|
fromTimestamp: z.ZodOptional<z.ZodString>;
|
|
2621
2651
|
toTimestamp: z.ZodOptional<z.ZodString>;
|
|
@@ -2952,6 +2982,7 @@ export declare const platformToOrchestratorMessageSchema: z.ZodDiscriminatedUnio
|
|
|
2952
2982
|
actorId: z.ZodOptional<z.ZodString>;
|
|
2953
2983
|
action: z.ZodOptional<z.ZodEnum<{
|
|
2954
2984
|
"access_log.list.read": "access_log.list.read";
|
|
2985
|
+
"admin_tokens.list.read": "admin_tokens.list.read";
|
|
2955
2986
|
archive_chunk: "archive_chunk";
|
|
2956
2987
|
"artifacts.read": "artifacts.read";
|
|
2957
2988
|
"attestation.retry": "attestation.retry";
|
|
@@ -3628,6 +3659,11 @@ export declare const orchestratorToPlatformMessageSchema: z.ZodDiscriminatedUnio
|
|
|
3628
3659
|
adminInvocation: z.ZodOptional<z.ZodString>;
|
|
3629
3660
|
adminPath: z.ZodOptional<z.ZodString>;
|
|
3630
3661
|
}, z.core.$strip>>;
|
|
3662
|
+
configPaths: z.ZodOptional<z.ZodObject<{
|
|
3663
|
+
envFile: z.ZodOptional<z.ZodString>;
|
|
3664
|
+
scalerConfig: z.ZodOptional<z.ZodString>;
|
|
3665
|
+
composeFile: z.ZodOptional<z.ZodString>;
|
|
3666
|
+
}, z.core.$strip>>;
|
|
3631
3667
|
s3LogAccess: z.ZodOptional<z.ZodBoolean>;
|
|
3632
3668
|
queueTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
3633
3669
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -126,6 +126,11 @@ export declare const sourceRegistrationSchema: z.ZodObject<{
|
|
|
126
126
|
adminInvocation: z.ZodOptional<z.ZodString>;
|
|
127
127
|
adminPath: z.ZodOptional<z.ZodString>;
|
|
128
128
|
}, z.core.$strip>>;
|
|
129
|
+
configPaths: z.ZodOptional<z.ZodObject<{
|
|
130
|
+
envFile: z.ZodOptional<z.ZodString>;
|
|
131
|
+
scalerConfig: z.ZodOptional<z.ZodString>;
|
|
132
|
+
composeFile: z.ZodOptional<z.ZodString>;
|
|
133
|
+
}, z.core.$strip>>;
|
|
129
134
|
s3LogAccess: z.ZodOptional<z.ZodBoolean>;
|
|
130
135
|
queueTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
131
136
|
}, z.core.$strip>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "../../rolldown-runtime-ClRpJifh.js";
|
|
2
2
|
import { DeploymentIdentitySchema } from "./deployment-identity.js";
|
|
3
|
+
import { ConfigPathsSchema } from "./config-paths.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region src/protocol/messages/source-registration.ts
|
|
5
6
|
/**
|
|
@@ -150,6 +151,13 @@ const sourceRegistrationSchema = z.object({
|
|
|
150
151
|
* publish it, in which case the dashboard treats the shape as `unknown`.
|
|
151
152
|
*/
|
|
152
153
|
deployment: DeploymentIdentitySchema.optional(),
|
|
154
|
+
/**
|
|
155
|
+
* Where this orchestrator's own config files live on its host, so the
|
|
156
|
+
* dashboard can point an operator straight at them. Optional: an
|
|
157
|
+
* orchestrator that predates the field omits it, and each member is omitted
|
|
158
|
+
* independently when that path is not knowable.
|
|
159
|
+
*/
|
|
160
|
+
configPaths: ConfigPathsSchema.optional(),
|
|
153
161
|
/** Whether this orchestrator has S3 log storage configured. Used for multi-orch pool validation. */
|
|
154
162
|
s3LogAccess: z.boolean().optional(),
|
|
155
163
|
/** Queue timeout in ms. Platform uses this (with margin) for safety-net GC of stale queued jobs. */
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The event-context half of an OIDC ID token's claims, shared by every minter.
|
|
3
|
+
*
|
|
4
|
+
* A cloud trust policy pins `sub` and, ideally, the claims below. Those values
|
|
5
|
+
* are what tells a fork pull request from a trusted push, so the two minters —
|
|
6
|
+
* the customer's orchestrator and the hosted Platform — must derive them
|
|
7
|
+
* identically. Any drift between them is a hole: a bundle minted by one would
|
|
8
|
+
* satisfy a policy written against the other. So the derivation lives here,
|
|
9
|
+
* once, and each minter spreads the result into its own claim object.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
/** The run columns the event claims read. Every one may be absent. */
|
|
13
|
+
export interface EventClaimSource {
|
|
14
|
+
repo_identifier: string | null;
|
|
15
|
+
ref: string | null;
|
|
16
|
+
workflow_name: string | null;
|
|
17
|
+
/** Normalized event that started the run (`push`, `pull_request:opened`, …). */
|
|
18
|
+
trigger_event?: string | null;
|
|
19
|
+
/**
|
|
20
|
+
* The event the SUBJECT is derived from, when that is not `trigger_event`.
|
|
21
|
+
*
|
|
22
|
+
* A re-run records `trigger_event: 'rerun'`, which carries no pull-request
|
|
23
|
+
* dimension, so a re-run of a pull request would present the branch-shaped
|
|
24
|
+
* subject a push to the same base branch presents. This column carries the
|
|
25
|
+
* original run's event forward for that one purpose. NULL means "use
|
|
26
|
+
* `trigger_event`", which is what every row written before the column
|
|
27
|
+
* existed says — so a legacy row keeps the subject it already mints.
|
|
28
|
+
*
|
|
29
|
+
* Read ONLY by `buildIdTokenSubject`. It is deliberately absent from
|
|
30
|
+
* `buildEventClaims`: `event_name` reports what actually started THIS run,
|
|
31
|
+
* and a re-run is a re-run.
|
|
32
|
+
*/
|
|
33
|
+
subject_trigger_event?: string | null;
|
|
34
|
+
/** Pull-request HEAD branch. */
|
|
35
|
+
head_ref?: string | null;
|
|
36
|
+
/** `owner/repo` of the pull-request HEAD. */
|
|
37
|
+
head_repository?: string | null;
|
|
38
|
+
/** True for a fork pull request. NULL means the run did not resolve one. */
|
|
39
|
+
is_fork?: boolean | null;
|
|
40
|
+
/** Resolved trust tier for the triggering actor. */
|
|
41
|
+
trust_tier?: string | null;
|
|
42
|
+
/** Provider login of the triggering actor. */
|
|
43
|
+
trigger_actor_username?: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The event-context claims. Every value is a STRING and every one is ALWAYS
|
|
47
|
+
* PRESENT.
|
|
48
|
+
*
|
|
49
|
+
* Strings because an AWS IAM `StringEquals` condition on an OIDC claim is
|
|
50
|
+
* string-typed and a three-valued `is_fork` has to be expressible. Always
|
|
51
|
+
* present because an ABSENT claim makes a `StringEquals` condition fail — which
|
|
52
|
+
* silently removes the constraint for a policy author who wrote it expecting
|
|
53
|
+
* enforcement. An explicit `''` / `'unresolved'` makes that policy fail closed
|
|
54
|
+
* instead, and makes the unknown state visible in a decoded token.
|
|
55
|
+
*/
|
|
56
|
+
export interface EventClaims {
|
|
57
|
+
/** Normalized event type, or `'unknown'`. */
|
|
58
|
+
event_name: string;
|
|
59
|
+
/** The BASE branch — the same value as `ref`, named the way GitHub names it. */
|
|
60
|
+
base_ref: string;
|
|
61
|
+
/** The pull-request HEAD branch, or `''`. */
|
|
62
|
+
head_ref: string;
|
|
63
|
+
/** `owner/repo` of the pull-request HEAD, or `''`. */
|
|
64
|
+
head_repository: string;
|
|
65
|
+
/** `'true'` / `'false'`, or `'unresolved'` when the run resolved no answer. */
|
|
66
|
+
is_fork: string;
|
|
67
|
+
/** The run's trust tier, or `'unresolved'`. */
|
|
68
|
+
trust_tier: string;
|
|
69
|
+
/** Provider login of the triggering actor, or `''`. */
|
|
70
|
+
actor: string;
|
|
71
|
+
}
|
|
72
|
+
/** The sentinel for a value the run did not resolve. Never a plausible default. */
|
|
73
|
+
export declare const UNRESOLVED_CLAIM = "unresolved";
|
|
74
|
+
/**
|
|
75
|
+
* Build the event-context claims from a run row.
|
|
76
|
+
*
|
|
77
|
+
* A NULL never becomes a plausible default. `head_repository` does not fall
|
|
78
|
+
* back to `repository`, and `is_fork` does not fall back to `'false'`: both
|
|
79
|
+
* would fail OPEN on a lost write, which is the failure these claims exist to
|
|
80
|
+
* remove.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildEventClaims(run: EventClaimSource): EventClaims;
|
|
83
|
+
/**
|
|
84
|
+
* Build the token subject.
|
|
85
|
+
*
|
|
86
|
+
* Two shapes, mirroring GitHub Actions' own claim vocabulary — which is what
|
|
87
|
+
* every customer's existing trust policy is written against:
|
|
88
|
+
*
|
|
89
|
+
* - pull-request family: `repo:<owner/repo>:pull_request`, with NO ref
|
|
90
|
+
* segment. GitHub omits the ref there for exactly this reason.
|
|
91
|
+
* - everything else: `repo:<owner/repo>:ref:<ref>:workflow:<name>`, unchanged.
|
|
92
|
+
*
|
|
93
|
+
* A pull request's `ref` is its BASE branch, so the branch-shaped subject made
|
|
94
|
+
* a fork PR against `main` byte-identical to a trusted push to `main`. An
|
|
95
|
+
* external contributor whose PR ran the same workflow minted the exact string a
|
|
96
|
+
* policy pinned, and assumed the customer's cloud role. The two shapes cannot
|
|
97
|
+
* collide: no branch name produces the literal segment `pull_request` in the
|
|
98
|
+
* position a branch subject puts `ref`.
|
|
99
|
+
*
|
|
100
|
+
* The event tested is `subject_trigger_event ?? trigger_event`. A re-run writes
|
|
101
|
+
* `trigger_event: 'rerun'` and carries the original run's event in
|
|
102
|
+
* `subject_trigger_event`, so re-running a pull request keeps the
|
|
103
|
+
* pull-request subject instead of decaying to the colliding branch shape.
|
|
104
|
+
* `trigger_event` itself is left alone because two other readers depend on it:
|
|
105
|
+
* the dashboard's trigger-type filter and the git credential relay's
|
|
106
|
+
* `triggerTypeFilters`, both of which must keep seeing `'rerun'`.
|
|
107
|
+
*
|
|
108
|
+
* `legacyPullRequestSubject` restores the old, colliding form for one release —
|
|
109
|
+
* see `KICI_OIDC_LEGACY_PR_SUB`. It is deprecated on arrival.
|
|
110
|
+
*
|
|
111
|
+
* @deprecated `legacyPullRequestSubject` is removed at v1.0.0.
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildIdTokenSubject(run: Pick<EventClaimSource, 'repo_identifier' | 'ref' | 'workflow_name' | 'trigger_event' | 'subject_trigger_event'>, opts?: {
|
|
114
|
+
legacyPullRequestSubject?: boolean;
|
|
115
|
+
}): string;
|
|
116
|
+
/**
|
|
117
|
+
* The orchestrator's own view of a build, sent with the job so the agent can
|
|
118
|
+
* freeze a provenance statement that matches what a live mint would say.
|
|
119
|
+
*
|
|
120
|
+
* Every field mirrors a claim `buildIdTokenClaims` derives, so a statement
|
|
121
|
+
* built from this passes `crossCheckBuildContext` against the token that is
|
|
122
|
+
* later minted for the same (run, job) — which is what lets the orchestrator
|
|
123
|
+
* refuse a deferred statement it has not checked against its own run row.
|
|
124
|
+
*
|
|
125
|
+
* `.passthrough()` so an older agent tolerates a field a newer orchestrator
|
|
126
|
+
* adds.
|
|
127
|
+
*/
|
|
128
|
+
export declare const provenanceContextSchema: z.ZodObject<{
|
|
129
|
+
repository: z.ZodNullable<z.ZodString>;
|
|
130
|
+
ref: z.ZodNullable<z.ZodString>;
|
|
131
|
+
sha: z.ZodNullable<z.ZodString>;
|
|
132
|
+
workflowRef: z.ZodNullable<z.ZodString>;
|
|
133
|
+
runId: z.ZodString;
|
|
134
|
+
jobId: z.ZodString;
|
|
135
|
+
orgId: z.ZodString;
|
|
136
|
+
sourceOrigin: z.ZodString;
|
|
137
|
+
provider: z.ZodNullable<z.ZodString>;
|
|
138
|
+
issuer: z.ZodString;
|
|
139
|
+
orchestratorId: z.ZodString;
|
|
140
|
+
}, z.core.$loose>;
|
|
141
|
+
export type ProvenanceContext = z.infer<typeof provenanceContextSchema>;
|
|
142
|
+
//# sourceMappingURL=id-token-event-claims.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import { isPullRequestFamilyTriggerEvent } from "../trigger/trigger-event-type.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/provenance/id-token-event-claims.ts
|
|
5
|
+
/**
|
|
6
|
+
* The event-context half of an OIDC ID token's claims, shared by every minter.
|
|
7
|
+
*
|
|
8
|
+
* A cloud trust policy pins `sub` and, ideally, the claims below. Those values
|
|
9
|
+
* are what tells a fork pull request from a trusted push, so the two minters —
|
|
10
|
+
* the customer's orchestrator and the hosted Platform — must derive them
|
|
11
|
+
* identically. Any drift between them is a hole: a bundle minted by one would
|
|
12
|
+
* satisfy a policy written against the other. So the derivation lives here,
|
|
13
|
+
* once, and each minter spreads the result into its own claim object.
|
|
14
|
+
*/
|
|
15
|
+
/** The sentinel for a value the run did not resolve. Never a plausible default. */
|
|
16
|
+
const UNRESOLVED_CLAIM = "unresolved";
|
|
17
|
+
/**
|
|
18
|
+
* Build the event-context claims from a run row.
|
|
19
|
+
*
|
|
20
|
+
* A NULL never becomes a plausible default. `head_repository` does not fall
|
|
21
|
+
* back to `repository`, and `is_fork` does not fall back to `'false'`: both
|
|
22
|
+
* would fail OPEN on a lost write, which is the failure these claims exist to
|
|
23
|
+
* remove.
|
|
24
|
+
*/
|
|
25
|
+
function buildEventClaims(run) {
|
|
26
|
+
return {
|
|
27
|
+
event_name: run.trigger_event ?? "unknown",
|
|
28
|
+
base_ref: run.ref ?? "",
|
|
29
|
+
head_ref: run.head_ref ?? "",
|
|
30
|
+
head_repository: run.head_repository ?? "",
|
|
31
|
+
is_fork: run.is_fork == null ? UNRESOLVED_CLAIM : String(run.is_fork),
|
|
32
|
+
trust_tier: run.trust_tier ?? "unresolved",
|
|
33
|
+
actor: run.trigger_actor_username ?? ""
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the token subject.
|
|
38
|
+
*
|
|
39
|
+
* Two shapes, mirroring GitHub Actions' own claim vocabulary — which is what
|
|
40
|
+
* every customer's existing trust policy is written against:
|
|
41
|
+
*
|
|
42
|
+
* - pull-request family: `repo:<owner/repo>:pull_request`, with NO ref
|
|
43
|
+
* segment. GitHub omits the ref there for exactly this reason.
|
|
44
|
+
* - everything else: `repo:<owner/repo>:ref:<ref>:workflow:<name>`, unchanged.
|
|
45
|
+
*
|
|
46
|
+
* A pull request's `ref` is its BASE branch, so the branch-shaped subject made
|
|
47
|
+
* a fork PR against `main` byte-identical to a trusted push to `main`. An
|
|
48
|
+
* external contributor whose PR ran the same workflow minted the exact string a
|
|
49
|
+
* policy pinned, and assumed the customer's cloud role. The two shapes cannot
|
|
50
|
+
* collide: no branch name produces the literal segment `pull_request` in the
|
|
51
|
+
* position a branch subject puts `ref`.
|
|
52
|
+
*
|
|
53
|
+
* The event tested is `subject_trigger_event ?? trigger_event`. A re-run writes
|
|
54
|
+
* `trigger_event: 'rerun'` and carries the original run's event in
|
|
55
|
+
* `subject_trigger_event`, so re-running a pull request keeps the
|
|
56
|
+
* pull-request subject instead of decaying to the colliding branch shape.
|
|
57
|
+
* `trigger_event` itself is left alone because two other readers depend on it:
|
|
58
|
+
* the dashboard's trigger-type filter and the git credential relay's
|
|
59
|
+
* `triggerTypeFilters`, both of which must keep seeing `'rerun'`.
|
|
60
|
+
*
|
|
61
|
+
* `legacyPullRequestSubject` restores the old, colliding form for one release —
|
|
62
|
+
* see `KICI_OIDC_LEGACY_PR_SUB`. It is deprecated on arrival.
|
|
63
|
+
*
|
|
64
|
+
* @deprecated `legacyPullRequestSubject` is removed at v1.0.0.
|
|
65
|
+
*/
|
|
66
|
+
function buildIdTokenSubject(run, opts) {
|
|
67
|
+
const repository = run.repo_identifier ?? "unknown";
|
|
68
|
+
const subjectEvent = run.subject_trigger_event ?? run.trigger_event;
|
|
69
|
+
if (!opts?.legacyPullRequestSubject && isPullRequestFamilyTriggerEvent(subjectEvent)) return `repo:${repository}:pull_request`;
|
|
70
|
+
return `repo:${repository}:ref:${run.ref ?? "unknown"}:workflow:${run.workflow_name ?? "unknown"}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The orchestrator's own view of a build, sent with the job so the agent can
|
|
74
|
+
* freeze a provenance statement that matches what a live mint would say.
|
|
75
|
+
*
|
|
76
|
+
* Every field mirrors a claim `buildIdTokenClaims` derives, so a statement
|
|
77
|
+
* built from this passes `crossCheckBuildContext` against the token that is
|
|
78
|
+
* later minted for the same (run, job) — which is what lets the orchestrator
|
|
79
|
+
* refuse a deferred statement it has not checked against its own run row.
|
|
80
|
+
*
|
|
81
|
+
* `.passthrough()` so an older agent tolerates a field a newer orchestrator
|
|
82
|
+
* adds.
|
|
83
|
+
*/
|
|
84
|
+
const provenanceContextSchema = z.object({
|
|
85
|
+
/** `owner/repo`, the token's `repository` claim. */
|
|
86
|
+
repository: z.string().nullable(),
|
|
87
|
+
/** The branch the run PRESENTS — a pull request's BASE branch. */
|
|
88
|
+
ref: z.string().nullable(),
|
|
89
|
+
/** The run's commit SHA, the token's `sha` claim. */
|
|
90
|
+
sha: z.string().nullable(),
|
|
91
|
+
/**
|
|
92
|
+
* The token's `workflow_ref` claim (`<workflow_name>@<sha>`) — NOT the git
|
|
93
|
+
* ref used to clone a global workflow's repository. The statement's
|
|
94
|
+
* `workflow.path` is compared against this.
|
|
95
|
+
*/
|
|
96
|
+
workflowRef: z.string().nullable(),
|
|
97
|
+
runId: z.string(),
|
|
98
|
+
jobId: z.string(),
|
|
99
|
+
/** The customer's public org id, resolved server-side from the routing key. */
|
|
100
|
+
orgId: z.string(),
|
|
101
|
+
/** `triggered` or `run-remote`, derived from the run's local-working-tree flag. */
|
|
102
|
+
sourceOrigin: z.string(),
|
|
103
|
+
/** Informational source provider (github / gitlab / …). */
|
|
104
|
+
provider: z.string().nullable(),
|
|
105
|
+
/** The orchestrator's provenance issuer, for the statement's `builder.id`. */
|
|
106
|
+
issuer: z.string(),
|
|
107
|
+
/** This orchestrator's instance id, also for `builder.id`. */
|
|
108
|
+
orchestratorId: z.string()
|
|
109
|
+
}).passthrough();
|
|
110
|
+
//#endregion
|
|
111
|
+
export { UNRESOLVED_CLAIM, buildEventClaims, buildIdTokenSubject, provenanceContextSchema };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=id-token-event-claims.js.map
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lowercase-hex SHA-256 of the DSSE statement payload bytes.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Lowercase-hex SHA-256 of the DSSE statement payload bytes.
|
|
3
|
+
*
|
|
4
|
+
* One of the two bindings a deferred OIDC mint commits to: the later token
|
|
5
|
+
* carries this hash as a claim, so the identity cannot be re-bound to a
|
|
6
|
+
* different frozen statement at retry time.
|
|
7
|
+
*
|
|
8
|
+
* It is a binding, not a substitute for the build-context cross-check. The
|
|
9
|
+
* verifier requires BOTH for a non-live origin — the hash proves the statement
|
|
10
|
+
* has not been swapped, and the cross-check proves the statement agrees with
|
|
11
|
+
* the run the token names. A hash-only rule verified a bundle whose statement
|
|
12
|
+
* claimed a release SHA the build never touched.
|
|
13
|
+
*
|
|
14
|
+
* Browser-safe: `crypto.subtle` only, so the verifier (dashboard + CLI) can
|
|
15
|
+
* recompute it.
|
|
7
16
|
*/
|
|
8
17
|
export declare function computeStatementHash(payload: Uint8Array): Promise<string>;
|
|
9
18
|
//# sourceMappingURL=statement-hash.d.ts.map
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
2
|
//#region src/provenance/statement-hash.ts
|
|
3
3
|
/**
|
|
4
|
-
* Lowercase-hex SHA-256 of the DSSE statement payload bytes.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Lowercase-hex SHA-256 of the DSSE statement payload bytes.
|
|
5
|
+
*
|
|
6
|
+
* One of the two bindings a deferred OIDC mint commits to: the later token
|
|
7
|
+
* carries this hash as a claim, so the identity cannot be re-bound to a
|
|
8
|
+
* different frozen statement at retry time.
|
|
9
|
+
*
|
|
10
|
+
* It is a binding, not a substitute for the build-context cross-check. The
|
|
11
|
+
* verifier requires BOTH for a non-live origin — the hash proves the statement
|
|
12
|
+
* has not been swapped, and the cross-check proves the statement agrees with
|
|
13
|
+
* the run the token names. A hash-only rule verified a bundle whose statement
|
|
14
|
+
* claimed a release SHA the build never touched.
|
|
15
|
+
*
|
|
16
|
+
* Browser-safe: `crypto.subtle` only, so the verifier (dashboard + CLI) can
|
|
17
|
+
* recompute it.
|
|
9
18
|
*/
|
|
10
19
|
async function computeStatementHash(payload) {
|
|
11
20
|
const digest = await crypto.subtle.digest("SHA-256", payload);
|
|
@@ -75,4 +75,25 @@ export interface VerifyKiciBundleOptions {
|
|
|
75
75
|
* verification is `verified:false` with populated `failures`).
|
|
76
76
|
*/
|
|
77
77
|
export declare function verifyKiciBundle(opts: VerifyKiciBundleOptions): Promise<VerifyResult>;
|
|
78
|
+
/**
|
|
79
|
+
* The statement's build context must equal the (server-truth) JWT claims.
|
|
80
|
+
*
|
|
81
|
+
* Only the **claims** side is normalized (`?? ''`): the producer coerces a
|
|
82
|
+
* null/absent `repository` / `ref` / `workflow_ref` claim into the
|
|
83
|
+
* schema-required empty string and omits `commit` entirely when `sha` is
|
|
84
|
+
* falsy, so a null/absent claim is compared against that coerced `''` /
|
|
85
|
+
* omitted-commit. The **statement** side stays strict — a non-empty
|
|
86
|
+
* attacker-controlled statement field never matches a null claim
|
|
87
|
+
* (`'evil/repo' === (null ?? '')` → `'evil/repo' === ''` → false), and an
|
|
88
|
+
* empty statement field never matches a non-null claim — so this relaxation
|
|
89
|
+
* is forgery-safe in exactly one direction: empty/absent statement ⇔
|
|
90
|
+
* null/absent claim, and nothing else. `runId` / `jobId` are always minted,
|
|
91
|
+
* so they stay strict.
|
|
92
|
+
*/
|
|
93
|
+
export declare function crossCheckBuildContext(statement: KiciProvenanceStatement, claims: Record<string, unknown>): boolean;
|
|
94
|
+
/** At least one subject must carry a digest matching the supplied artifact digest. */
|
|
95
|
+
export declare function checkSubjectDigest(statement: KiciProvenanceStatement, expected: {
|
|
96
|
+
alg: string;
|
|
97
|
+
hex: string;
|
|
98
|
+
}): boolean;
|
|
78
99
|
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -106,18 +106,17 @@ async function verifyKiciBundle(opts) {
|
|
|
106
106
|
checks.dsse = "fail";
|
|
107
107
|
failures.push("dsse_signature_invalid");
|
|
108
108
|
}
|
|
109
|
-
const attestationOrigin = resolveAttestationOrigin(claims
|
|
109
|
+
const attestationOrigin = resolveAttestationOrigin(claims);
|
|
110
110
|
if (statement && claims && statementBytes) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
} else {
|
|
111
|
+
const contextOk = crossCheckBuildContext(statement, claims);
|
|
112
|
+
if (!contextOk) failures.push("build_context_mismatch");
|
|
113
|
+
let hashOk = true;
|
|
114
|
+
if (attestationOrigin !== AttestationOrigin.enum.live) {
|
|
116
115
|
const actual = await computeStatementHash(statementBytes);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!ok) failures.push("statement_hash_mismatch");
|
|
116
|
+
hashOk = typeof claims.statement_hash === "string" && claims.statement_hash === actual;
|
|
117
|
+
if (!hashOk) failures.push("statement_hash_mismatch");
|
|
120
118
|
}
|
|
119
|
+
checks.buildContext = contextOk && hashOk ? "pass" : "fail";
|
|
121
120
|
} else {
|
|
122
121
|
checks.buildContext = "fail";
|
|
123
122
|
failures.push("build_context_uncheckable");
|
|
@@ -137,13 +136,24 @@ async function verifyKiciBundle(opts) {
|
|
|
137
136
|
attestationOrigin
|
|
138
137
|
};
|
|
139
138
|
}
|
|
140
|
-
/**
|
|
141
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Derive the mint-timing origin from the SIGNED token claim.
|
|
141
|
+
*
|
|
142
|
+
* There is deliberately no fallback to the frozen statement's
|
|
143
|
+
* `internalParameters.attestationOrigin`. That field is unsigned and
|
|
144
|
+
* producer-written (the agent sets it itself), so honouring it let a producer
|
|
145
|
+
* choose which verification branch ran by writing one field — a per-bundle
|
|
146
|
+
* opt-out of the check.
|
|
147
|
+
*
|
|
148
|
+
* A token carrying no claim is treated as `live`, which is now the STRICTER
|
|
149
|
+
* reading: `live` requires the field-by-field cross-check and nothing else can
|
|
150
|
+
* satisfy it. Both minters have emitted the claim unconditionally since the
|
|
151
|
+
* deferral feature shipped, so a token without one predates it and is live in
|
|
152
|
+
* fact as well as by default.
|
|
153
|
+
*/
|
|
154
|
+
function resolveAttestationOrigin(claims) {
|
|
142
155
|
const fromClaim = AttestationOrigin.safeParse(claims?.attestation_origin);
|
|
143
|
-
|
|
144
|
-
const internal = statement?.predicate.buildDefinition.internalParameters;
|
|
145
|
-
const fromStatement = AttestationOrigin.safeParse(internal?.attestationOrigin);
|
|
146
|
-
return fromStatement.success ? fromStatement.data : AttestationOrigin.enum.live;
|
|
156
|
+
return fromClaim.success ? fromClaim.data : AttestationOrigin.enum.live;
|
|
147
157
|
}
|
|
148
158
|
/**
|
|
149
159
|
* The statement's build context must equal the (server-truth) JWT claims.
|
|
@@ -171,6 +181,6 @@ function checkSubjectDigest(statement, expected) {
|
|
|
171
181
|
return statement.subject.some((s) => s.digest[expected.alg] === expected.hex);
|
|
172
182
|
}
|
|
173
183
|
//#endregion
|
|
174
|
-
export { verifyKiciBundle };
|
|
184
|
+
export { checkSubjectDigest, crossCheckBuildContext, verifyKiciBundle };
|
|
175
185
|
|
|
176
186
|
//# sourceMappingURL=verify.js.map
|