@kici-dev/orchestrator 0.9.1 → 0.11.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/__test-helpers__/test-db.d.ts +14 -1
- package/dist/agent/dispatcher.d.ts +137 -41
- package/dist/agent/registry.d.ts +79 -2
- package/dist/app.d.ts +2 -0
- package/dist/cancel/cancel-held-run.d.ts +68 -0
- package/dist/cancel/cancel-route-answer.d.ts +21 -0
- package/dist/cancel/cancel-run.d.ts +32 -0
- package/dist/cli/api-client.d.ts +23 -1
- package/dist/cli/commands/attestations.d.ts +10 -0
- package/dist/cli/commands/cold-store-impl.d.ts +3 -3
- package/dist/cli/commands/cold-store.d.ts +6 -3
- package/dist/cli/commands/context.d.ts +6 -2
- package/dist/cli/commands/dashboard-encryption-key.d.ts +18 -0
- package/dist/cli/commands/event-dlq.d.ts +2 -2
- package/dist/cli/commands/registration.d.ts +8 -4
- package/dist/cli/commands/runs.d.ts +7 -0
- package/dist/cli/commands/secret.d.ts +1 -0
- package/dist/cli/commands/shared/confirm.d.ts +19 -0
- package/dist/cli/commands/shared/unbound-context-warning.d.ts +43 -0
- package/dist/cli/commands/signing-key.d.ts +22 -1
- package/dist/cli.js +1522 -871
- package/dist/cluster/cluster-settings-reader.d.ts +1 -1
- package/dist/cluster/coordinator.d.ts +2 -0
- package/dist/cluster/live-coordinator-peer.d.ts +13 -0
- package/dist/config.d.ts +9 -0
- package/dist/contexts/context-store.d.ts +3 -2
- package/dist/contexts/protection/satisfiability.d.ts +6 -3
- package/dist/contexts/variable-store.d.ts +2 -0
- package/dist/db/migrations/145_execution_runs_workflow_provenance.d.ts +4 -0
- package/dist/db/migrations/146_pending_job_contexts_context_resolution.d.ts +4 -0
- package/dist/db/migrations/147_sealed_job_secrets.d.ts +19 -0
- package/dist/db/migrations/148_cluster_settings_sealed_secrets_retry_backoff_ms.d.ts +21 -0
- package/dist/db/migrations/149_workflow_registrations_dep_cache_key.d.ts +4 -0
- package/dist/db/migrations/150_execution_runs_status_epoch.d.ts +18 -0
- package/dist/db/migrations/151_workflow_registrations_dep_cache_key_sha.d.ts +4 -0
- package/dist/db/repos/signing-keys-repo.d.ts +48 -3
- package/dist/db/types.d.ts +81 -1
- package/dist/git/clone-token-auth.d.ts +7 -0
- package/dist/git/dispatch-git-auth.d.ts +63 -0
- package/dist/index.js +26 -0
- package/dist/lockfile-validate.d.ts +4 -2
- package/dist/metrics/prometheus.d.ts +1 -1
- package/dist/oidc/id-token-claims.d.ts +10 -0
- package/dist/oidc/oidc-mint-registration.d.ts +2 -0
- package/dist/oidc/orchestrator-mint.d.ts +25 -4
- package/dist/oidc/reconcile-signing-key.d.ts +31 -6
- package/dist/oidc/resolve-signer.d.ts +12 -6
- package/dist/pipeline/decorating-secret-resolver.d.ts +5 -3
- package/dist/pipeline/dispatch-matched-workflow.d.ts +105 -13
- package/dist/pipeline/dispatch-matched-workflow.test-helpers.d.ts +121 -0
- package/dist/pipeline/global-dispatch-identity.d.ts +76 -0
- package/dist/pipeline/global-dispatch.d.ts +91 -0
- package/dist/pipeline/global-eval-round.d.ts +56 -4
- package/dist/pipeline/global-round-hold.d.ts +41 -0
- package/dist/pipeline/held-context-data.d.ts +143 -0
- package/dist/pipeline/held-round-registrations.d.ts +40 -0
- package/dist/pipeline/install-gate.d.ts +93 -0
- package/dist/pipeline/install-secrets-resolver.d.ts +28 -13
- package/dist/pipeline/job-contexts.d.ts +21 -6
- package/dist/pipeline/pending-workflow-context.d.ts +15 -3
- package/dist/pipeline/process-webhook.d.ts +38 -1
- package/dist/pipeline/processor.d.ts +20 -0
- package/dist/pipeline/rerun-global.d.ts +28 -0
- package/dist/pipeline/rerun-inputs.d.ts +88 -0
- package/dist/pipeline/rerun.d.ts +25 -2
- package/dist/pipeline/resume-workflow.d.ts +36 -2
- package/dist/pipeline/unbound-job-context.d.ts +34 -0
- package/dist/pipeline/workflow-lock-at-sha.d.ts +67 -0
- package/dist/provenance/backfill-run.d.ts +5 -0
- package/dist/providers/github/check-status-poster.d.ts +4 -16
- package/dist/queue/job-queue.d.ts +127 -10
- package/dist/queue/terminalize-unroutable.d.ts +37 -4
- package/dist/registration/registration-index.d.ts +13 -1
- package/dist/registration/registration-store.d.ts +10 -1
- package/dist/reporting/execution-tracker.d.ts +149 -8
- package/dist/reporting/log-chunk-sink.d.ts +1 -1
- package/dist/reporting/log-writer.d.ts +14 -0
- package/dist/reporting/step-log-path.d.ts +8 -0
- package/dist/reporting/step-log-reader.d.ts +21 -3
- package/dist/routes/admin-contexts.d.ts +6 -0
- package/dist/routes/admin-registrations.d.ts +2 -2
- package/dist/routes/admin-signing-keys.d.ts +33 -0
- package/dist/scaler/agent-fit.d.ts +159 -0
- package/dist/scaler/container-routing.d.ts +16 -16
- package/dist/scaler/manager.d.ts +23 -17
- package/dist/secrets/audit-logger.d.ts +1 -1
- package/dist/secrets/context-secret-resolver.d.ts +1 -1
- package/dist/secrets/index.d.ts +1 -1
- package/dist/secrets/job-secret-gate.d.ts +25 -3
- package/dist/secrets/job-secret-seal.d.ts +36 -0
- package/dist/secrets/master-key-rotation.d.ts +7 -3
- package/dist/secrets/secret-resolver.d.ts +42 -23
- package/dist/server.js +27611 -23219
- package/dist/standalone.js +58327 -53119
- package/dist/worker/in-memory-job-queue.d.ts +12 -3
- package/dist/worker/reroute-dispatch.d.ts +35 -0
- package/dist/ws/agent-handler.d.ts +23 -10
- package/dist/ws/git-credential-relay.d.ts +16 -3
- package/installer-image-digests.json +3 -3
- package/package.json +4 -4
- package/sbom.spdx.json +53 -53
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which agents may run a job, beyond what its labels say — above all, which
|
|
3
|
+
* agents may run a job that declares `container:`.
|
|
4
|
+
*
|
|
5
|
+
* A container job runs in one of two places. Either the scaler starts an agent
|
|
6
|
+
* inside the job's image (the agent then runs the steps directly), or an
|
|
7
|
+
* ordinary agent nests the job's container through a container runtime on its
|
|
8
|
+
* host. So an agent not started for the job can run it only when the agent can
|
|
9
|
+
* start containers, and an agent started inside one job's image can run nothing
|
|
10
|
+
* else: any other job would run in an image it never declared.
|
|
11
|
+
*
|
|
12
|
+
* {@link canAgentRunJob} is the one predicate; every dispatch path that hands a
|
|
13
|
+
* queued job to an agent asks it.
|
|
14
|
+
*/
|
|
15
|
+
import { ScalerBackendType } from '@kici-dev/engine';
|
|
16
|
+
import type { ResourceRequest } from '@kici-dev/engine';
|
|
17
|
+
import type { LabelSetConfig } from './types.js';
|
|
18
|
+
/** What a job asks of the container runtime on the agent that runs it. */
|
|
19
|
+
export declare enum JobContainerNeed {
|
|
20
|
+
/** The job declares no `container:`, so any agent can run it. */
|
|
21
|
+
None = "none",
|
|
22
|
+
/** The job names an image: the agent pulls it and runs the job inside it. */
|
|
23
|
+
Image = "image",
|
|
24
|
+
/** The job names a Dockerfile: the agent builds the image, then runs it. */
|
|
25
|
+
Dockerfile = "dockerfile"
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify a job's `container:` declaration.
|
|
29
|
+
*
|
|
30
|
+
* Mirrors the agent, which runs a job in container mode whenever its
|
|
31
|
+
* `container` field is set at all. So any non-empty declaration counts, not
|
|
32
|
+
* only a well-formed `image`: routing a job to an agent that would then nest
|
|
33
|
+
* a container it cannot start is the failure this classification prevents.
|
|
34
|
+
*/
|
|
35
|
+
export declare function jobContainerNeed(container: unknown): JobContainerNeed;
|
|
36
|
+
/** The self-reported labels that prove an agent's host can start a container. */
|
|
37
|
+
export declare const CONTAINER_RUNTIME_LABELS: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* Whether an agent's registered labels prove it can start a container.
|
|
40
|
+
*
|
|
41
|
+
* The agent reports `kici:runtime:docker` / `kici:runtime:podman` when it finds
|
|
42
|
+
* that runtime's socket at startup. `kici:runtime:container-build` does not
|
|
43
|
+
* count: it says a build CLI is on PATH, and a host can have the CLI and no
|
|
44
|
+
* daemon to run the result on.
|
|
45
|
+
*/
|
|
46
|
+
export declare function hasContainerRuntime(labels: Iterable<string>): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a spawn this backend makes for a job that names its own image
|
|
49
|
+
* starts the agent INSIDE that image.
|
|
50
|
+
*
|
|
51
|
+
* - The container backend always does: a spawn carrying the job's image runs
|
|
52
|
+
* that image with the KiCI runtime injected.
|
|
53
|
+
* - The bare-metal backend does only for a label set that declares an `image`
|
|
54
|
+
* and no `binaryPath`. Such a set has no local binary to start, so it can
|
|
55
|
+
* only mean job-image mode; every other bare-metal set starts a local agent
|
|
56
|
+
* that nests the job's container. The backend decides its launch with this
|
|
57
|
+
* same function, so the two cannot disagree.
|
|
58
|
+
* - Every other backend starts an ordinary agent.
|
|
59
|
+
*/
|
|
60
|
+
export declare function spawnsInJobImage(backendType: ScalerBackendType, labelSet: Pick<LabelSetConfig, 'image' | 'binaryPath'>): boolean;
|
|
61
|
+
/** The facts about one job that decide which scaler agents may run it. */
|
|
62
|
+
export interface AgentFitJob {
|
|
63
|
+
/**
|
|
64
|
+
* The queued job's id. An agent the scaler started for a job recognizes
|
|
65
|
+
* that job by it. Absent for a job not yet in the queue, which no agent can
|
|
66
|
+
* have been started for.
|
|
67
|
+
*/
|
|
68
|
+
jobId?: string;
|
|
69
|
+
/** The cpu / memory shape the job declares, if any. */
|
|
70
|
+
resources?: ResourceRequest;
|
|
71
|
+
/** What the job's `container:` asks of the agent's host. */
|
|
72
|
+
container: JobContainerNeed;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The first agent release whose `kici:runtime:docker` / `kici:runtime:podman`
|
|
76
|
+
* labels describe the runtime its job containers actually start on. From it
|
|
77
|
+
* on, an agent with neither label has told the orchestrator it cannot start a
|
|
78
|
+
* container.
|
|
79
|
+
*
|
|
80
|
+
* Earlier releases (0.6.0 to 0.9.x) report runtime labels too, but read
|
|
81
|
+
* `DOCKER_HOST` only as a socket file path, while their container client
|
|
82
|
+
* honours any `DOCKER_HOST`. An agent pointed at `tcp://…` — a Docker-in-Docker
|
|
83
|
+
* sidecar, say — reports no Docker label and runs container jobs fine, so its
|
|
84
|
+
* missing label proves nothing.
|
|
85
|
+
*/
|
|
86
|
+
export declare const MIN_RUNTIME_FACTS_AGENT_VERSION = "0.10.0";
|
|
87
|
+
/**
|
|
88
|
+
* Whether an agent's runtime labels are proof, so that a missing
|
|
89
|
+
* `kici:runtime:docker` / `kici:runtime:podman` label means no runtime: its
|
|
90
|
+
* version is at least {@link MIN_RUNTIME_FACTS_AGENT_VERSION}, or it reports
|
|
91
|
+
* `kici:runtime:job-image`, which only such a release sends.
|
|
92
|
+
*
|
|
93
|
+
* Any other runtime label is not proof: `kici:runtime:container-build` says a
|
|
94
|
+
* build CLI is on PATH, and an older agent reports it beside a remote
|
|
95
|
+
* `DOCKER_HOST` it does not report.
|
|
96
|
+
*/
|
|
97
|
+
export declare function reportsRuntimeFacts(agent: {
|
|
98
|
+
labels: Iterable<string>;
|
|
99
|
+
version: string | null | undefined;
|
|
100
|
+
}): boolean;
|
|
101
|
+
/** What the scaler knows about an agent it started. */
|
|
102
|
+
export interface ScalerAgentView {
|
|
103
|
+
/** The job the scaler started the agent for, and whether it runs in that job's image. */
|
|
104
|
+
binding?: {
|
|
105
|
+
jobId: string;
|
|
106
|
+
jobImage: boolean;
|
|
107
|
+
};
|
|
108
|
+
/** The scaler pre-spawned the agent for its warm pool. */
|
|
109
|
+
prespawned: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Whether the agent's fixed shape leaves a job's declared shape unchanged.
|
|
112
|
+
* Always true for an agent the scaler did not pre-spawn.
|
|
113
|
+
*/
|
|
114
|
+
shapeFits(resources: ResourceRequest | undefined): boolean;
|
|
115
|
+
}
|
|
116
|
+
/** The facts about one registered agent that {@link canAgentRunJob} reads. */
|
|
117
|
+
export interface FitAgent {
|
|
118
|
+
/** The labels the agent registered with, its self-reported facts included. */
|
|
119
|
+
labels: ReadonlySet<string>;
|
|
120
|
+
/** The agent's self-reported version, or null when it reported none. */
|
|
121
|
+
version: string | null;
|
|
122
|
+
/** The registry records the agent as scaler-started. */
|
|
123
|
+
scalerManaged: boolean;
|
|
124
|
+
/** Present when this coordinator's scaler started the agent. */
|
|
125
|
+
scaler?: ScalerAgentView | undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The facts `canAgentRunJob` judges a registered agent on: what it reported at
|
|
129
|
+
* registration, and what the scaler that started it (if one did) recorded.
|
|
130
|
+
*/
|
|
131
|
+
export declare function fitAgentFor(agent: {
|
|
132
|
+
agentId: string;
|
|
133
|
+
labels: ReadonlySet<string>;
|
|
134
|
+
version: string | null;
|
|
135
|
+
scalerManaged: boolean;
|
|
136
|
+
}, scalerAgentView?: (agentId: string) => ScalerAgentView | undefined): FitAgent;
|
|
137
|
+
/**
|
|
138
|
+
* Whether this agent may run this job. Every dispatch path that hands a
|
|
139
|
+
* queued job to an agent asks this one predicate; labels have already matched.
|
|
140
|
+
*
|
|
141
|
+
* In order:
|
|
142
|
+
*
|
|
143
|
+
* - **The agent started for this job** always runs it: the scaler chose that
|
|
144
|
+
* spawn for this job, image and shape included.
|
|
145
|
+
* - **An agent inside another job's image** runs nothing else. The scaler's
|
|
146
|
+
* record says so, or the agent itself does (`kici:runtime:job-image`).
|
|
147
|
+
* - **A job that declares `container:`** needs an agent that reports
|
|
148
|
+
* `kici:runtime:docker` or `kici:runtime:podman`, whenever the missing label
|
|
149
|
+
* is proof ({@link runtimeLabelIsProof}).
|
|
150
|
+
* - **A pre-spawned (warm) agent** must also match any shape the job declares.
|
|
151
|
+
*/
|
|
152
|
+
export declare function canAgentRunJob(agent: FitAgent, job: AgentFitJob): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Whether {@link canAgentRunJob} can ever answer false for this agent. The
|
|
155
|
+
* queue drain carries the predicate into its claim only when it can: for an
|
|
156
|
+
* agent that runs any job, the drain keeps its single-statement fast path.
|
|
157
|
+
*/
|
|
158
|
+
export declare function agentMayRefuse(agent: FitAgent): boolean;
|
|
159
|
+
//# sourceMappingURL=agent-fit.d.ts.map
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Label for hosts that can run containers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
4
|
+
* The agent reports it itself at registration when it finds a Docker socket,
|
|
5
|
+
* alongside `kici:runtime:podman` for a Podman one and the `kici:os:*` /
|
|
6
|
+
* `kici:arch:*` facts: whether a machine has a container runtime is that
|
|
7
|
+
* machine's fact, not the orchestrator's. Probing the orchestrator's own
|
|
8
|
+
* filesystem answers a different question — it was tried, and it stranded
|
|
9
|
+
* container jobs that had been running fine, because the probe and the job ran
|
|
10
|
+
* in different places.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* places.
|
|
12
|
+
* Routing gates on the reported label (`canAgentRunJob`): a `container:` job
|
|
13
|
+
* reaches an agent only when that agent was started for it, or reports
|
|
14
|
+
* `kici:runtime:docker` or `kici:runtime:podman`. For an operator's own agent
|
|
15
|
+
* the gate applies from the release that reports runtime labels at all; an
|
|
16
|
+
* older agent reports none, so its silence proves nothing and a job it cannot
|
|
17
|
+
* run fails on it with a message that names the missing runtime.
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* then a mis-routed container job fails at the image preflight or a backend's
|
|
19
|
-
* fail-fast, both of which name what is missing — a clear late error, rather
|
|
20
|
-
* than a job that silently matches nothing and never runs.
|
|
19
|
+
* An operator may still put the label on a pool's label sets and on a container
|
|
20
|
+
* job's `runsOn` to steer container work at hosts with a runtime.
|
|
21
21
|
*/
|
|
22
22
|
export declare const KICI_RUNTIME_DOCKER_LABEL = "kici:runtime:docker";
|
|
23
23
|
//# sourceMappingURL=container-routing.d.ts.map
|
package/dist/scaler/manager.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ResourceRequest, ScalerCapacitySummary } from '@kici-dev/engine';
|
|
10
10
|
import { ScaleDownReason } from './scaler-events.js';
|
|
11
|
+
import { type ScalerAgentView } from './agent-fit.js';
|
|
11
12
|
import type { ScalerEventEmitterLike } from './event-backend.js';
|
|
12
13
|
import type { ClaimStore, ClaimedCredentials } from './claim-store.js';
|
|
13
14
|
import type { BackendFailureSummary } from './failure-tracker.js';
|
|
@@ -374,6 +375,14 @@ export declare class ScalerManager {
|
|
|
374
375
|
* dispatch has not yet arrived.
|
|
375
376
|
*/
|
|
376
377
|
private readonly warmAgents;
|
|
378
|
+
/**
|
|
379
|
+
* Registered agents this instance started for a specific queued job, keyed
|
|
380
|
+
* by agent id. The spawning entry that carries the binding is deleted at
|
|
381
|
+
* registration, so this is where {@link agentView} reads it: the agent always
|
|
382
|
+
* runs its own job, and an agent started inside a job's image runs nothing
|
|
383
|
+
* else.
|
|
384
|
+
*/
|
|
385
|
+
private readonly boundAgents;
|
|
377
386
|
/**
|
|
378
387
|
* Warm agents whose destroy is issued but has not settled yet.
|
|
379
388
|
*
|
|
@@ -497,25 +506,25 @@ export declare class ScalerManager {
|
|
|
497
506
|
*/
|
|
498
507
|
private warmPoolLabelSetFillable;
|
|
499
508
|
/**
|
|
500
|
-
* Whether this scaler pre-spawned (warm-filled) the agent
|
|
501
|
-
* {@link canPrespawnedAgentServe} can ever answer false for it. A caller that
|
|
502
|
-
* picks one agent at a time uses this to decide whether the suitability check
|
|
503
|
-
* is worth carrying at all.
|
|
509
|
+
* Whether this scaler pre-spawned (warm-filled) the agent.
|
|
504
510
|
*/
|
|
505
511
|
isPrespawnedAgent(agentId: string): boolean;
|
|
506
512
|
/**
|
|
507
|
-
*
|
|
513
|
+
* What this scaler knows about an agent it started, for `canAgentRunJob`,
|
|
514
|
+
* or undefined for an agent it did not start.
|
|
515
|
+
*/
|
|
516
|
+
agentView(agentId: string): ScalerAgentView | undefined;
|
|
517
|
+
/**
|
|
518
|
+
* Whether a pre-spawned (warm) agent's shape fits the shape a job declares.
|
|
508
519
|
*
|
|
509
520
|
* A warm agent is generic by construction: it was started before the job
|
|
510
|
-
* existed, at the pool's declared shape
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
* else must get its own agent instead of silently running with the pool's.
|
|
521
|
+
* existed, at the pool's declared shape. The shape is applied when the agent
|
|
522
|
+
* starts and cannot be changed afterwards — nothing in the agent reads a
|
|
523
|
+
* job's `resources`. So a job that needs another shape must get its own
|
|
524
|
+
* agent instead of silently running with the pool's.
|
|
515
525
|
*
|
|
516
|
-
* Returns true for any agent this scaler did not pre-spawn: a job-bound
|
|
517
|
-
*
|
|
518
|
-
* answers that half on its own.
|
|
526
|
+
* Returns true for any agent this scaler did not pre-spawn: a job-bound agent
|
|
527
|
+
* was started at its job's shape.
|
|
519
528
|
*
|
|
520
529
|
* Only the fields the job actually declares are compared. `resolveEffective`
|
|
521
530
|
* layers a job's declaration over the label set and the scaler defaults field
|
|
@@ -536,10 +545,7 @@ export declare class ScalerManager {
|
|
|
536
545
|
* them apart, so a job matched on `requests` alone can still land on an agent
|
|
537
546
|
* capped somewhere else entirely.
|
|
538
547
|
*/
|
|
539
|
-
|
|
540
|
-
resources?: ResourceRequest;
|
|
541
|
-
hasOwnContainerImage: boolean;
|
|
542
|
-
}): boolean;
|
|
548
|
+
private prespawnedShapeFits;
|
|
543
549
|
/**
|
|
544
550
|
* Whether one side (`requests` or `limits`) of a job's declared shape leaves
|
|
545
551
|
* the agent's resolved side unchanged.
|
|
@@ -32,7 +32,7 @@ export declare class AuditLogger {
|
|
|
32
32
|
* engine's `shouldRecordSecretResolve` helper) since they're written
|
|
33
33
|
* once per JOB-step secret resolution and dwarf every other action's
|
|
34
34
|
* volume. Denied resolves and every other action (`setSecret`,
|
|
35
|
-
* `deleteSecret`, `rotateKey`, `secret-outputs.reveal`) bypass the
|
|
35
|
+
* `deleteSecret`, the scope writes, `rotateKey`, `secret-outputs.reveal`) bypass the
|
|
36
36
|
* sampler — they are transactional with the mutation and must always
|
|
37
37
|
* land.
|
|
38
38
|
*
|
|
@@ -22,7 +22,7 @@ export interface ContextSecretResolverDeps {
|
|
|
22
22
|
logger: Logger;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Build a `SecretResolver` over the org's
|
|
25
|
+
* Build a `SecretResolver` over the org's context bindings and registered
|
|
26
26
|
* secret backends. The `pg` backend decrypts through the master key; external
|
|
27
27
|
* backends (Vault) return plaintext, so their decrypt is identity.
|
|
28
28
|
*/
|
package/dist/secrets/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { secretOutputAad, sealSecretOutput, unsealSecretOutput } from './secret-
|
|
|
11
11
|
export { rotateMasterKeyWrappedTables, selfHealStrandedDashboardKey, selfHealStrandedSigningKey, strandedKeyError, type MasterKeyRotationResult, type SweepResult, } from './master-key-rotation.js';
|
|
12
12
|
export { encrypt, decrypt, deriveKey, generateMasterKey, type EncryptedValue, } from '@kici-dev/shared';
|
|
13
13
|
export { VaultSecretStore, type VaultConfig } from './vault-secret-store.js';
|
|
14
|
-
export { SecretResolver, type SecretResolverDeps, type ResolvedSecretMeta, type
|
|
14
|
+
export { SecretResolver, type SecretResolverDeps, type ResolvedSecretMeta, type MatchedContextRef, type BindingStoreLike, type SecretStoreLike, } from './secret-resolver.js';
|
|
15
15
|
export { BackendSyncManager } from './backend-sync.js';
|
|
16
16
|
export { RbacEnforcer, PermissionDeniedError, type Role, type Permission } from './rbac.js';
|
|
17
17
|
export { TokenManager } from './token-manager.js';
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* `docs/user/container-jobs.md` do, so a bound-only rule would refuse every
|
|
9
9
|
* documented use.
|
|
10
10
|
*
|
|
11
|
-
* Resolving outside the
|
|
12
|
-
* authorization. Three independent checks stand between a job and the value,
|
|
11
|
+
* Resolving outside the job's `contexts:` list is not the same as resolving with
|
|
12
|
+
* no authorization. Three independent checks stand between a job and the value,
|
|
13
13
|
* and this module is where all three run:
|
|
14
14
|
*
|
|
15
15
|
* 1. the reserved namespace is refused — a job may never name the
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
* the branch restriction, `minimumTrust`, concurrency, reviewers and wait
|
|
21
21
|
* timers the operator configured on `prod` apply to a `prod:` reference.
|
|
22
22
|
*
|
|
23
|
+
* The value is then read through the secret-scope bindings of the context row
|
|
24
|
+
* those rules ran against — for a name a glob context matches, the glob row —
|
|
25
|
+
* the way a job's bound context secrets resolve. One deprecated fallback remains
|
|
26
|
+
* (removal planned for v1.0.0): when the row matched by the exact name is not a
|
|
27
|
+
* glob context and no scope bound to it carries the key, the scope named after
|
|
28
|
+
* the context is read instead, and a deprecation warning is logged. A
|
|
29
|
+
* glob-matched row never reads a same-named scope.
|
|
30
|
+
*
|
|
23
31
|
* The lock declaration is the fourth check and lives at the callers: the relay
|
|
24
32
|
* pins the wire ref to `execution_jobs.git_credentials`, and container-registry
|
|
25
33
|
* refs come from the lock by construction.
|
|
@@ -27,7 +35,7 @@
|
|
|
27
35
|
* Callers use this instead of `SecretResolverApi.resolveNamedInternal`, which
|
|
28
36
|
* is the system-scoped direct lookup and applies none of the above.
|
|
29
37
|
*/
|
|
30
|
-
import type
|
|
38
|
+
import { type TrustTier } from '@kici-dev/engine';
|
|
31
39
|
import type { ContextStore } from '../contexts/context-store.js';
|
|
32
40
|
import { type JobDispatchContext } from '../contexts/protection/pipeline.js';
|
|
33
41
|
import type { SecretResolverApi } from './secret-resolver.js';
|
|
@@ -37,6 +45,15 @@ import type { SecretResolverApi } from './secret-resolver.js';
|
|
|
37
45
|
* its `customer_id`, so a job can never legitimately present this value.
|
|
38
46
|
*/
|
|
39
47
|
export declare const RESERVED_ORG_ID = "__system__";
|
|
48
|
+
/**
|
|
49
|
+
* A refusal this module decides — a reserved namespace, an untrusted tier, a
|
|
50
|
+
* context that does not exist or does not admit the run, a secret that is not
|
|
51
|
+
* there. Distinct from a store or resolver failure, which propagates as-is, so
|
|
52
|
+
* a caller can tell "this job may not have it" from "we could not look".
|
|
53
|
+
*/
|
|
54
|
+
export declare class JobSecretRefusedError extends Error {
|
|
55
|
+
constructor(message: string);
|
|
56
|
+
}
|
|
40
57
|
/**
|
|
41
58
|
* Refuse a reference into the orchestrator's own reserved namespace.
|
|
42
59
|
*
|
|
@@ -64,6 +81,11 @@ export interface ResolveJobQualifiedSecretArgs {
|
|
|
64
81
|
/** The dispatch facts the protection rules evaluate against, from server truth. */
|
|
65
82
|
dispatchCtx: JobDispatchContext;
|
|
66
83
|
trustTier: TrustTier | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Ids of contexts whose protection rules already admitted this job. Their
|
|
86
|
+
* rules are not re-run; the namespace and trust-tier refusals still apply.
|
|
87
|
+
*/
|
|
88
|
+
admittedContextIds?: ReadonlySet<string>;
|
|
67
89
|
}
|
|
68
90
|
/**
|
|
69
91
|
* Resolve one `<context>:<key>` reference on behalf of a running job.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ResolvedMasterKeys } from './config.js';
|
|
2
|
+
/** The job-config fields that carry secret material. */
|
|
3
|
+
export declare const SEALED_JOB_CONFIG_KEYS: readonly ['secrets', 'namespacedSecrets', 'installEnvSecrets', 'containerRegistryAuth', 'npmRegistries', 'orchestratorPrivateKey'];
|
|
4
|
+
/** The key version every seal is written at. The AAD, not the version, binds a value. */
|
|
5
|
+
export declare const JOB_SECRETS_KEY_VERSION = 1;
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a sealed value cannot be opened: it was sealed with a master key
|
|
8
|
+
* this orchestrator does not hold. During a rolling key rotation that is a
|
|
9
|
+
* coordinator not yet restarted with the new key; the message names the fix.
|
|
10
|
+
*/
|
|
11
|
+
export declare class JobSecretsUnsealError extends Error {
|
|
12
|
+
constructor(runId: string, cause: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Register the master keys the stores seal with. `null` keeps the fields in
|
|
16
|
+
* plaintext and logs the one warning that names the setting.
|
|
17
|
+
*/
|
|
18
|
+
export declare function configureJobSecretSealing(keys: ResolvedMasterKeys | null): void;
|
|
19
|
+
/** The AAD a stored job's sealed secrets are bound to. */
|
|
20
|
+
export declare function jobSecretsAad(runId: string): string;
|
|
21
|
+
/** Seal `value` for `runId`; `null` when no master key is configured. */
|
|
22
|
+
export declare function sealJobSecretValue(runId: string, value: unknown): string | null;
|
|
23
|
+
/** Open a value {@link sealJobSecretValue} sealed, with the old key as fallback. */
|
|
24
|
+
export declare function unsealJobSecretValue(runId: string, sealed: string): unknown;
|
|
25
|
+
/**
|
|
26
|
+
* Split a job config into what is stored in the plain column and the sealed
|
|
27
|
+
* secret fields. With nothing to seal, or no master key, the config is
|
|
28
|
+
* returned as is and `sealed` is null.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sealJobConfig(runId: string, jobConfig: Record<string, unknown>): {
|
|
31
|
+
jobConfig: Record<string, unknown>;
|
|
32
|
+
sealed: string | null;
|
|
33
|
+
};
|
|
34
|
+
/** The job config with its sealed secret fields merged back. A null `sealed` returns it as is. */
|
|
35
|
+
export declare function unsealJobConfig(runId: string, jobConfig: Record<string, unknown>, sealed: string | null | undefined): Record<string, unknown>;
|
|
36
|
+
//# sourceMappingURL=job-secret-seal.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Master-key rotation sweeps for the
|
|
2
|
+
* Master-key rotation sweeps for the wrapped stores `rotate-key` did not
|
|
3
3
|
* cover: the provenance signing key, the dashboard-encryption key, run
|
|
4
|
-
* ephemeral keys,
|
|
4
|
+
* ephemeral keys, stored secret outputs, and the sealed secrets of stored jobs.
|
|
5
5
|
*
|
|
6
6
|
* Each sweep runs in its own transaction with the same skip-and-count
|
|
7
7
|
* discipline as `BackendRegistry.rotateKey` — a row neither key opens is
|
|
@@ -26,10 +26,14 @@ export interface MasterKeyRotationResult {
|
|
|
26
26
|
dashboardKeys: SweepResult;
|
|
27
27
|
ephemeralKeys: SweepResult;
|
|
28
28
|
secretOutputs: SweepResult;
|
|
29
|
+
/** `sealed_secrets` of `dispatch_queue`, `pending_job_contexts` and `pending_workflow_contexts`. */
|
|
30
|
+
jobSecrets: SweepResult;
|
|
29
31
|
}
|
|
30
32
|
type Warn = (message: string, meta: Record<string, unknown>) => void;
|
|
33
|
+
/** Rows of `dispatch_queue` one rotation transaction re-seals. */
|
|
34
|
+
export declare const JOB_SECRETS_SWEEP_BATCH = 500;
|
|
31
35
|
/**
|
|
32
|
-
* Run all
|
|
36
|
+
* Run all the sweeps, each in its own transaction so a bug in one cannot roll
|
|
33
37
|
* back a good rotation of another. Order is least-to-most volume, so the
|
|
34
38
|
* singleton key tables — the two whose loss brings the orchestrator down — move
|
|
35
39
|
* first.
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Secret resolver for dispatch-time secret resolution.
|
|
3
3
|
*
|
|
4
4
|
* Uses context bindings + scope resolver to match secrets from multiple backends.
|
|
5
|
-
* When a job dispatches
|
|
6
|
-
* 1.
|
|
7
|
-
*
|
|
5
|
+
* When a job dispatches through a context, the resolver:
|
|
6
|
+
* 1. Takes the context row the dispatch path already matched (a fixed context
|
|
7
|
+
* by exact name, or a glob context whose pattern matched the declared name)
|
|
8
|
+
* 2. Gets bindings for that row
|
|
8
9
|
* 3. Queries ALL registered backend stores for secrets
|
|
9
10
|
* 4. Prefixes each secret's scope with the backend name (e.g., pg:aws/prod)
|
|
10
11
|
* 5. Uses resolveSecretsWithProvenance to match bindings against prefixed secrets
|
|
@@ -19,14 +20,23 @@ import { type ContextBinding, type HostFacts, type ScopedSecret } from '@kici-de
|
|
|
19
20
|
import type { Logger } from '@kici-dev/shared';
|
|
20
21
|
import type { AuditLogger } from './audit-logger.js';
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* The context a job's secrets resolve through.
|
|
24
|
+
*
|
|
25
|
+
* `id` is the row the dispatch path matched — which, for a glob context, is not
|
|
26
|
+
* the row whose name equals the declared one — so the resolver reads that row's
|
|
27
|
+
* bindings and never matches a name again: a second match could land on a
|
|
28
|
+
* different row than the one the caller's protection rules and held-run
|
|
29
|
+
* checks ran against. `name` is the name the job declared; it labels the audit
|
|
30
|
+
* entry and keys per-context namespacing.
|
|
23
31
|
*/
|
|
24
|
-
export interface
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
export interface MatchedContextRef {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
/** The run and job a resolution is recorded against in the secret audit log. */
|
|
37
|
+
export interface SecretResolutionAttribution {
|
|
38
|
+
runId?: string;
|
|
39
|
+
jobId?: string;
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
32
42
|
* Minimal binding store interface (subset needed by resolver).
|
|
@@ -49,7 +59,6 @@ export interface SecretStoreLike {
|
|
|
49
59
|
* Dependencies for the SecretResolver.
|
|
50
60
|
*/
|
|
51
61
|
export interface SecretResolverDeps {
|
|
52
|
-
contextStore: ContextStoreLike;
|
|
53
62
|
bindingStore: BindingStoreLike;
|
|
54
63
|
/** Map of backend name to store. Replaces single secretStore. */
|
|
55
64
|
backendStores: Map<string, SecretStoreLike>;
|
|
@@ -69,7 +78,7 @@ export interface ResolvedSecretMeta {
|
|
|
69
78
|
* can flow through `ProcessingDeps.secretResolver`.
|
|
70
79
|
*/
|
|
71
80
|
export interface SecretResolverApi {
|
|
72
|
-
|
|
81
|
+
resolveForContext(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts, attribution?: SecretResolutionAttribution): Promise<Record<string, string>>;
|
|
73
82
|
/**
|
|
74
83
|
* System-scoped direct lookup — no context binding, no protection rule, no
|
|
75
84
|
* trust tier. A job-originated reference goes through
|
|
@@ -80,7 +89,12 @@ export interface SecretResolverApi {
|
|
|
80
89
|
runId?: string;
|
|
81
90
|
jobId?: string;
|
|
82
91
|
}): Promise<string | null>;
|
|
83
|
-
|
|
92
|
+
resolveForContextWithMeta(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
|
|
93
|
+
/**
|
|
94
|
+
* How many scope bindings the context row `contextId` has. A context with
|
|
95
|
+
* none resolves no secret; the dispatch path reads this to say so in its log.
|
|
96
|
+
*/
|
|
97
|
+
countContextBindings?(contextId: string): Promise<number>;
|
|
84
98
|
}
|
|
85
99
|
/**
|
|
86
100
|
* Resolves secrets for a job by matching context bindings against scoped secrets
|
|
@@ -92,7 +106,6 @@ export interface SecretResolverApi {
|
|
|
92
106
|
* stripping the backend prefix.
|
|
93
107
|
*/
|
|
94
108
|
export declare class SecretResolver implements SecretResolverApi {
|
|
95
|
-
private readonly contextStore;
|
|
96
109
|
private readonly bindingStore;
|
|
97
110
|
private readonly backendStores;
|
|
98
111
|
private readonly auditLogger;
|
|
@@ -102,14 +115,16 @@ export declare class SecretResolver implements SecretResolverApi {
|
|
|
102
115
|
* Resolve secrets for a job dispatch.
|
|
103
116
|
*
|
|
104
117
|
* @param orgId - Organization ID
|
|
105
|
-
* @param
|
|
118
|
+
* @param context - The matched context row and the name the job declared
|
|
106
119
|
* @param hostCtx - Optional fan-out child identity for per-host resolution.
|
|
107
120
|
* When supplied, each binding is gated by its `host_pattern` and its
|
|
108
121
|
* `scope_pattern` is templated per-child; when omitted, only fleet-wide
|
|
109
122
|
* (`'**'`) non-templated bindings contribute.
|
|
123
|
+
* @param attribution - The run and job the audit entry names, when the
|
|
124
|
+
* caller resolves on behalf of one.
|
|
110
125
|
* @returns Flat map of decrypted secret key-value pairs
|
|
111
126
|
*/
|
|
112
|
-
|
|
127
|
+
resolveForContext(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts, attribution?: SecretResolutionAttribution): Promise<Record<string, string>>;
|
|
113
128
|
/**
|
|
114
129
|
* Resolve a single named secret by (orgId, scope, key), optionally scoped to
|
|
115
130
|
* a specific backend.
|
|
@@ -122,18 +137,20 @@ export declare class SecretResolver implements SecretResolverApi {
|
|
|
122
137
|
*
|
|
123
138
|
* A JOB-ORIGINATED reference goes through `resolveJobQualifiedSecret`
|
|
124
139
|
* (`secrets/job-secret-gate.ts`) instead, which runs the named context's
|
|
125
|
-
* protection rules and the trust-tier strip
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
140
|
+
* protection rules and the trust-tier strip, then reads the value through
|
|
141
|
+
* that context's bindings with `resolveForContext`. It calls this method in
|
|
142
|
+
* one deprecated case only, after its own checks pass: a non-glob context
|
|
143
|
+
* matched by its exact name, whose bound scopes do not carry the key, reads
|
|
144
|
+
* the scope named after the context (removal planned for v1.0.0). The `Internal` suffix marks the
|
|
145
|
+
* boundary: a warning in a doc comment is not one, but a name shows up in a
|
|
146
|
+
* grep of callers.
|
|
130
147
|
*
|
|
131
148
|
* When `store` is omitted, backends are tried in Map iteration order (the
|
|
132
149
|
* order they were registered) and the first hit wins. An explicit `store`
|
|
133
150
|
* restricts the lookup to that one backend and returns null on miss.
|
|
134
151
|
*
|
|
135
152
|
* Audit-log: writes one `resolve_named` entry on success. Throws when the
|
|
136
|
-
* named store is requested but doesn't exist, mirroring `
|
|
153
|
+
* named store is requested but doesn't exist, mirroring `resolveForContext`'s
|
|
137
154
|
* fail-fast policy — the caller asked for a specific backend and
|
|
138
155
|
* it's gone.
|
|
139
156
|
*/
|
|
@@ -142,12 +159,14 @@ export declare class SecretResolver implements SecretResolverApi {
|
|
|
142
159
|
runId?: string;
|
|
143
160
|
jobId?: string;
|
|
144
161
|
}): Promise<string | null>;
|
|
162
|
+
/** How many scope bindings the context row `contextId` has. */
|
|
163
|
+
countContextBindings(contextId: string): Promise<number>;
|
|
145
164
|
/**
|
|
146
165
|
* Resolve secrets with metadata (per, for secrets.getMeta).
|
|
147
166
|
*
|
|
148
167
|
* Returns the secret value along with which backend and scope provided it.
|
|
149
168
|
*/
|
|
150
|
-
|
|
169
|
+
resolveForContextWithMeta(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
|
|
151
170
|
/**
|
|
152
171
|
* Collect all secrets from all backend stores, prefixing scopes.
|
|
153
172
|
* Per /: unreachable backends are tracked but not fatal here.
|