@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.
Files changed (101) hide show
  1. package/dist/__test-helpers__/test-db.d.ts +14 -1
  2. package/dist/agent/dispatcher.d.ts +137 -41
  3. package/dist/agent/registry.d.ts +79 -2
  4. package/dist/app.d.ts +2 -0
  5. package/dist/cancel/cancel-held-run.d.ts +68 -0
  6. package/dist/cancel/cancel-route-answer.d.ts +21 -0
  7. package/dist/cancel/cancel-run.d.ts +32 -0
  8. package/dist/cli/api-client.d.ts +23 -1
  9. package/dist/cli/commands/attestations.d.ts +10 -0
  10. package/dist/cli/commands/cold-store-impl.d.ts +3 -3
  11. package/dist/cli/commands/cold-store.d.ts +6 -3
  12. package/dist/cli/commands/context.d.ts +6 -2
  13. package/dist/cli/commands/dashboard-encryption-key.d.ts +18 -0
  14. package/dist/cli/commands/event-dlq.d.ts +2 -2
  15. package/dist/cli/commands/registration.d.ts +8 -4
  16. package/dist/cli/commands/runs.d.ts +7 -0
  17. package/dist/cli/commands/secret.d.ts +1 -0
  18. package/dist/cli/commands/shared/confirm.d.ts +19 -0
  19. package/dist/cli/commands/shared/unbound-context-warning.d.ts +43 -0
  20. package/dist/cli/commands/signing-key.d.ts +22 -1
  21. package/dist/cli.js +1522 -871
  22. package/dist/cluster/cluster-settings-reader.d.ts +1 -1
  23. package/dist/cluster/coordinator.d.ts +2 -0
  24. package/dist/cluster/live-coordinator-peer.d.ts +13 -0
  25. package/dist/config.d.ts +9 -0
  26. package/dist/contexts/context-store.d.ts +3 -2
  27. package/dist/contexts/protection/satisfiability.d.ts +6 -3
  28. package/dist/contexts/variable-store.d.ts +2 -0
  29. package/dist/db/migrations/145_execution_runs_workflow_provenance.d.ts +4 -0
  30. package/dist/db/migrations/146_pending_job_contexts_context_resolution.d.ts +4 -0
  31. package/dist/db/migrations/147_sealed_job_secrets.d.ts +19 -0
  32. package/dist/db/migrations/148_cluster_settings_sealed_secrets_retry_backoff_ms.d.ts +21 -0
  33. package/dist/db/migrations/149_workflow_registrations_dep_cache_key.d.ts +4 -0
  34. package/dist/db/migrations/150_execution_runs_status_epoch.d.ts +18 -0
  35. package/dist/db/migrations/151_workflow_registrations_dep_cache_key_sha.d.ts +4 -0
  36. package/dist/db/repos/signing-keys-repo.d.ts +48 -3
  37. package/dist/db/types.d.ts +81 -1
  38. package/dist/git/clone-token-auth.d.ts +7 -0
  39. package/dist/git/dispatch-git-auth.d.ts +63 -0
  40. package/dist/index.js +26 -0
  41. package/dist/lockfile-validate.d.ts +4 -2
  42. package/dist/metrics/prometheus.d.ts +1 -1
  43. package/dist/oidc/id-token-claims.d.ts +10 -0
  44. package/dist/oidc/oidc-mint-registration.d.ts +2 -0
  45. package/dist/oidc/orchestrator-mint.d.ts +25 -4
  46. package/dist/oidc/reconcile-signing-key.d.ts +31 -6
  47. package/dist/oidc/resolve-signer.d.ts +12 -6
  48. package/dist/pipeline/decorating-secret-resolver.d.ts +5 -3
  49. package/dist/pipeline/dispatch-matched-workflow.d.ts +105 -13
  50. package/dist/pipeline/dispatch-matched-workflow.test-helpers.d.ts +121 -0
  51. package/dist/pipeline/global-dispatch-identity.d.ts +76 -0
  52. package/dist/pipeline/global-dispatch.d.ts +91 -0
  53. package/dist/pipeline/global-eval-round.d.ts +56 -4
  54. package/dist/pipeline/global-round-hold.d.ts +41 -0
  55. package/dist/pipeline/held-context-data.d.ts +143 -0
  56. package/dist/pipeline/held-round-registrations.d.ts +40 -0
  57. package/dist/pipeline/install-gate.d.ts +93 -0
  58. package/dist/pipeline/install-secrets-resolver.d.ts +28 -13
  59. package/dist/pipeline/job-contexts.d.ts +21 -6
  60. package/dist/pipeline/pending-workflow-context.d.ts +15 -3
  61. package/dist/pipeline/process-webhook.d.ts +38 -1
  62. package/dist/pipeline/processor.d.ts +20 -0
  63. package/dist/pipeline/rerun-global.d.ts +28 -0
  64. package/dist/pipeline/rerun-inputs.d.ts +88 -0
  65. package/dist/pipeline/rerun.d.ts +25 -2
  66. package/dist/pipeline/resume-workflow.d.ts +36 -2
  67. package/dist/pipeline/unbound-job-context.d.ts +34 -0
  68. package/dist/pipeline/workflow-lock-at-sha.d.ts +67 -0
  69. package/dist/provenance/backfill-run.d.ts +5 -0
  70. package/dist/providers/github/check-status-poster.d.ts +4 -16
  71. package/dist/queue/job-queue.d.ts +127 -10
  72. package/dist/queue/terminalize-unroutable.d.ts +37 -4
  73. package/dist/registration/registration-index.d.ts +13 -1
  74. package/dist/registration/registration-store.d.ts +10 -1
  75. package/dist/reporting/execution-tracker.d.ts +149 -8
  76. package/dist/reporting/log-chunk-sink.d.ts +1 -1
  77. package/dist/reporting/log-writer.d.ts +14 -0
  78. package/dist/reporting/step-log-path.d.ts +8 -0
  79. package/dist/reporting/step-log-reader.d.ts +21 -3
  80. package/dist/routes/admin-contexts.d.ts +6 -0
  81. package/dist/routes/admin-registrations.d.ts +2 -2
  82. package/dist/routes/admin-signing-keys.d.ts +33 -0
  83. package/dist/scaler/agent-fit.d.ts +159 -0
  84. package/dist/scaler/container-routing.d.ts +16 -16
  85. package/dist/scaler/manager.d.ts +23 -17
  86. package/dist/secrets/audit-logger.d.ts +1 -1
  87. package/dist/secrets/context-secret-resolver.d.ts +1 -1
  88. package/dist/secrets/index.d.ts +1 -1
  89. package/dist/secrets/job-secret-gate.d.ts +25 -3
  90. package/dist/secrets/job-secret-seal.d.ts +36 -0
  91. package/dist/secrets/master-key-rotation.d.ts +7 -3
  92. package/dist/secrets/secret-resolver.d.ts +42 -23
  93. package/dist/server.js +27611 -23219
  94. package/dist/standalone.js +58327 -53119
  95. package/dist/worker/in-memory-job-queue.d.ts +12 -3
  96. package/dist/worker/reroute-dispatch.d.ts +35 -0
  97. package/dist/ws/agent-handler.d.ts +23 -10
  98. package/dist/ws/git-credential-relay.d.ts +16 -3
  99. package/installer-image-digests.json +3 -3
  100. package/package.json +4 -4
  101. 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
- * Capability label for hosts that can run containers.
2
+ * Label for hosts that can run containers.
3
3
  *
4
- * An operator may put this on a pool's label sets and on a container job's
5
- * `runsOn` to steer container work at hosts with a runtime. KiCI does NOT add
6
- * it automatically, and does not gate routing on it.
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
- * **Why it is not automatic.** A container job needs a runtime on the host that
9
- * ends up running it, and the orchestrator does not know that: an agent runs on
10
- * its own machine, and whether that machine has docker or podman is the agent's
11
- * fact, not the orchestrator's. Probing the orchestrator's own filesystem
12
- * answers a different question — it was tried, and it stranded container jobs
13
- * that had been running fine, because the probe and the job ran in different
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
- * Doing this properly means the AGENT reporting the capability at registration,
17
- * alongside the `kici:os:*` / `kici:arch:*` facts it already self-reports. Until
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
@@ -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, i.e. whether
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
- * Whether a pre-spawned (warm) agent can serve this job.
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 and running the pool's agent image.
511
- * Both are applied when the agent starts and cannot be changed afterwards —
512
- * nothing in the agent reads a job's `resources`, and an already-running
513
- * container cannot become a different image. So a job that needs something
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 or
517
- * static agent is not this predicate's business. {@link isPrespawnedAgent}
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
- canPrespawnedAgentServe(agentId: string, job: {
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 contexts, bindings, and registered
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
  */
@@ -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 ContextStoreLike, type BindingStoreLike, type SecretStoreLike, } from './secret-resolver.js';
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 bindings is not the same as resolving with no
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 { TrustTier } from '@kici-dev/engine';
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 four wrapped stores `rotate-key` did not
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, and stored secret outputs.
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 four sweeps, each in its own transaction so a bug in one cannot roll
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 with a context name, the resolver:
6
- * 1. Looks up the context by name
7
- * 2. Gets bindings for that context
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
- * Minimal context store interface (subset needed by resolver).
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 ContextStoreLike {
25
- getByName(orgId: string, name: string): Promise<{
26
- id: string;
27
- name: string;
28
- orgId: string;
29
- } | null>;
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
- resolveForJob(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, string>>;
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
- resolveForJobWithMeta(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
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 contextName - Context name to resolve secrets for
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
- resolveForJob(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, string>>;
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 before reaching this method. The
126
- * `Internal` suffix is the enforcement: this method's doc comment already
127
- * warned that it bypassed context bindings, and three call sites took it
128
- * anyway — a warning is not a boundary, but a name shows up in a grep of
129
- * callers.
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 `resolveForJob`'s
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
- resolveForJobWithMeta(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
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.