@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
@@ -16,7 +16,7 @@
16
16
  * This module owns the partition, the grouping, and the dispatch-and-await. The
17
17
  * caller decides what to do with the verdicts.
18
18
  */
19
- import type { GlobalEvalCandidateResult, LockWorkflow, SimulatedEvent, WorkflowDecision } from '@kici-dev/engine';
19
+ import type { AgentCapabilities, GlobalEvalCandidateResult, LockJobOrFactory, LockWorkflow, SimulatedEvent, WorkflowDecision } from '@kici-dev/engine';
20
20
  import { type GlobalEvalRoundCache } from '../cache/global-eval-round-cache.js';
21
21
  import type { PendingGlobalEvalTracker } from '../cache/pending-global-evals.js';
22
22
  import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
@@ -69,14 +69,29 @@ export interface GlobalEvalCandidate {
69
69
  * blank id still separates two workflows.
70
70
  */
71
71
  export declare function candidateKey(candidate: GlobalEvalCandidate): string;
72
+ /**
73
+ * True for a `DynamicJobFn` entry the pre-run round runs: a generator with no
74
+ * upstream `needs`.
75
+ *
76
+ * A result-aware generator (`resultAware`, or any declared `needs`) reads its
77
+ * upstreams' outputs, which do not exist before the run starts. It runs on the
78
+ * run's deferred path instead, once those upstreams complete.
79
+ */
80
+ export declare function isRoundGenerator(job: LockJobOrFactory): boolean;
81
+ /** True for a `DynamicJobFn` entry that runs on the deferred path, never in the round. */
82
+ export declare function isDeferredGenerator(job: LockJobOrFactory): boolean;
83
+ /** True when the workflow carries at least one generator the round must NOT run. */
84
+ export declare function hasDeferredGenerator(lockEntry: LockWorkflow): boolean;
72
85
  /**
73
86
  * Split matched global candidates into those that can dispatch straight away and
74
87
  * those that must go through an eval round first.
75
88
  *
76
89
  * A candidate needs the round when it declares a `filter` (only the agent may
77
- * run the predicate) or carries a `DynamicJobFn` (only the agent may run the
78
- * generator). Everything else is fully described by the lock file, so routing it
79
- * through a round would add a job dispatch and an agent round trip for nothing.
90
+ * run the predicate) or carries a needs-free `DynamicJobFn` (only the agent may
91
+ * run the generator, and it depends on nothing the run produces). Everything
92
+ * else — static jobs, and result-aware generators the run evaluates later — is
93
+ * handed to the dispatch pipeline as declared, so routing it through a round
94
+ * would add a job dispatch and an agent round trip for nothing.
80
95
  */
81
96
  export declare function partitionCandidates(candidates: readonly GlobalEvalCandidate[]): {
82
97
  immediate: GlobalEvalCandidate[];
@@ -119,6 +134,18 @@ export interface GlobalEvalAgentRegistry {
119
134
  arch: string;
120
135
  version?: string | null;
121
136
  }>;
137
+ /**
138
+ * Every registered agent, busy or idle. Optional so a test double can omit
139
+ * it; without it the up-front result-aware refusal never fires.
140
+ */
141
+ getAllEntries?(): Iterable<{
142
+ labels: ReadonlySet<string>;
143
+ platform: string;
144
+ arch: string;
145
+ version: string | null;
146
+ /** What the agent advertised on `agent.register`; `null` = supports nothing optional. */
147
+ capabilities: AgentCapabilities | null;
148
+ }>;
122
149
  }
123
150
  /**
124
151
  * First agent release whose job runner understands a `globalEvalRound`
@@ -162,6 +189,12 @@ export interface GlobalEvalRoundArgs {
162
189
  ref: string;
163
190
  dispatchBundle: ProviderBundle;
164
191
  dispatchCredentials: Record<string, unknown>;
192
+ /**
193
+ * The source `dispatchBundle` and `dispatchCredentials` belong to, when a
194
+ * cross-provider lock-file fallback made it a different source from
195
+ * `info.routingKey`. Absent means the inbound source.
196
+ */
197
+ dispatchRoutingKey?: string;
165
198
  /** Cluster defaults for the round budgets and the wait ceiling (`config.ts`). */
166
199
  config: {
167
200
  globalEvalRoundTimeoutMs: number;
@@ -192,6 +225,25 @@ export interface GlobalEvalRoundArgs {
192
225
  * timeout message that says nothing about agent versions.
193
226
  */
194
227
  export declare function unsupportedFleetReason(agentRegistry?: GlobalEvalAgentRegistry): string | null;
228
+ /**
229
+ * Why a candidate with a result-aware generator is refused before any round.
230
+ *
231
+ * An agent without the capability runs every generator in the round, a
232
+ * result-aware one included; that generator sees no upstream outputs and
233
+ * returns the wrong jobs.
234
+ */
235
+ export declare const RESULT_AWARE_UNSUPPORTED_REASON: string;
236
+ /**
237
+ * Why no agent can run a round holding a result-aware generator, or `null`
238
+ * when one might.
239
+ *
240
+ * The same bar as {@link unsupportedFleetReason}: refused only on proof. That
241
+ * means init-runners ARE registered, every one reports a readable version, and
242
+ * none advertises the capability. An empty fleet, a busy capable agent, or an
243
+ * agent whose version cannot be read is not refused — the round queues on the
244
+ * agent-feature label, and a scaler or a registering agent can still serve it.
245
+ */
246
+ export declare function resultAwareUnsupportedReason(agentRegistry?: GlobalEvalAgentRegistry): string | null;
195
247
  /**
196
248
  * Narrow one wire verdict into a value the dispatch path can trust.
197
249
  *
@@ -0,0 +1,41 @@
1
+ import type { SimulatedEvent } from '@kici-dev/engine';
2
+ import type { WebhookInfo } from '../webhook/handler.js';
3
+ import type { ProviderBundle } from '../provider-registry.js';
4
+ import type { TrustResolution } from '../security/trust-resolver.js';
5
+ import type { TrustPolicyOutcome } from '../security/trust-policy-gate.js';
6
+ import { type GlobalEvalCandidate } from './global-eval-round.js';
7
+ import { type ProcessingDeps } from './processor.js';
8
+ /** Inputs of one pass's held evaluation rounds. */
9
+ export interface HoldGlobalEvalRoundsArgs {
10
+ info: WebhookInfo;
11
+ deps: ProcessingDeps;
12
+ event: SimulatedEvent;
13
+ /** The candidates that needed the round. */
14
+ candidates: readonly GlobalEvalCandidate[];
15
+ /** The source repository the event came from. */
16
+ repoIdentifier: string;
17
+ /** The event's commit SHA. */
18
+ ref: string;
19
+ resolvedOrgId: string;
20
+ /** Credentials the round would have run with, stored on the held row for its release. */
21
+ dispatchCredentials: Record<string, unknown>;
22
+ /** The source `dispatchCredentials` belongs to, when it is not the inbound one. */
23
+ dispatchRoutingKey?: string;
24
+ /** The inbound event's bundle and credentials: the security check lands on its repository. */
25
+ bundle?: ProviderBundle;
26
+ credentials?: Record<string, unknown>;
27
+ trustResolution: TrustResolution | undefined;
28
+ decision: Extract<TrustPolicyOutcome, {
29
+ action: 'hold';
30
+ }>;
31
+ }
32
+ /**
33
+ * Hold the evaluation round of every workflow repository among `candidates`.
34
+ * Returns the id of each held run, in workflow-repository order.
35
+ *
36
+ * One run per workflow repository, not per registered SHA: the release
37
+ * re-evaluates a whole workflow repository, so two rows for one repository would
38
+ * dispatch its workflows twice.
39
+ */
40
+ export declare function holdGlobalEvalRounds(args: HoldGlobalEvalRoundsArgs): Promise<string[]>;
41
+ //# sourceMappingURL=global-round-hold.d.ts.map
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Resolves a job's context data (variables, scoped secrets, container registry
3
+ * credentials) at dispatch, and again when a job stored as a pending dispatch
4
+ * context is dispatched later. A stored job keeps only what resolution needs
5
+ * ({@link DeferredContextResolution}), never a value, and both moments merge
6
+ * the result into the job config through {@link contextDataConfigFields}.
7
+ */
8
+ import { z } from 'zod';
9
+ import { type Context as EngineContext, type HostFacts, type LockJob, type TrustTier } from '@kici-dev/engine';
10
+ import type { ContextStore } from '../contexts/context-store.js';
11
+ import type { VariableStore } from '../contexts/variable-store.js';
12
+ import type { JobDispatchContext } from '../contexts/protection/pipeline.js';
13
+ import type { QueuedJobInput } from '../queue/job-queue.js';
14
+ import type { SecretResolverApi } from '../secrets/secret-resolver.js';
15
+ type LockContainer = NonNullable<LockJob['container']>;
16
+ /** The context data a job config carries. */
17
+ export interface ContextJobData {
18
+ contextVars?: Record<string, string>;
19
+ jobSecrets?: Record<string, string>;
20
+ jobNamespacedSecrets?: Record<string, Record<string, string>>;
21
+ containerRegistryAuth?: {
22
+ username: string;
23
+ password: string;
24
+ serveraddress: string;
25
+ };
26
+ }
27
+ /** The stores context-data resolution reads. */
28
+ export interface ContextDataDeps {
29
+ contextStore?: ContextStore;
30
+ variableStore?: VariableStore;
31
+ secretResolver?: SecretResolverApi;
32
+ }
33
+ /** What a job's container registry credentials resolve against. */
34
+ export interface RegistryAuthInputs {
35
+ container: LockContainer;
36
+ /** The dispatch facts the named context's protection rules evaluate. */
37
+ dispatchCtx: JobDispatchContext;
38
+ trustTier?: TrustTier;
39
+ /** Contexts already admitted to this job, whose rules are not re-run. */
40
+ admittedContextIds?: ReadonlySet<string>;
41
+ /**
42
+ * `omit`: a refusal (a context that does not admit the run, a missing
43
+ * secret, a malformed reference) leaves the job without registry
44
+ * credentials, as dispatch does. Only a store or resolver failure throws.
45
+ * `throw` (default): every failure throws, for a caller that catches.
46
+ */
47
+ onRefusal?: 'throw' | 'omit';
48
+ }
49
+ /**
50
+ * Stored with a job's pending dispatch context: everything resolution at its
51
+ * later dispatch needs, and no secret value.
52
+ */
53
+ export declare const DeferredContextResolutionSchema: z.ZodObject<{
54
+ contexts: z.ZodArray<z.ZodObject<{
55
+ name: z.ZodString;
56
+ id: z.ZodString;
57
+ }, z.core.$strip>>;
58
+ orgId: z.ZodString;
59
+ resolvesSecrets: z.ZodOptional<z.ZodBoolean>;
60
+ routingKey: z.ZodOptional<z.ZodString>;
61
+ hostCtx: z.ZodOptional<z.ZodObject<{
62
+ agentId: z.ZodString;
63
+ host: z.ZodString;
64
+ labels: z.ZodArray<z.ZodString>;
65
+ }, z.core.$strip>>;
66
+ registryAuth: z.ZodOptional<z.ZodObject<{
67
+ container: z.ZodCustom<LockContainer, LockContainer>;
68
+ dispatchCtx: z.ZodObject<{
69
+ branch: z.ZodString;
70
+ triggerType: z.ZodString;
71
+ repository: z.ZodString;
72
+ runId: z.ZodString;
73
+ jobId: z.ZodString;
74
+ internallyTriggered: z.ZodOptional<z.ZodBoolean>;
75
+ }, z.core.$strip>;
76
+ trustTier: z.ZodOptional<z.ZodEnum<{
77
+ trusted: "trusted";
78
+ unknown: "unknown";
79
+ }>>;
80
+ }, z.core.$strip>>;
81
+ }, z.core.$strip>;
82
+ export type DeferredContextResolution = z.infer<typeof DeferredContextResolutionSchema>;
83
+ /** Identity fields for log lines. */
84
+ interface LogFields {
85
+ runId: string;
86
+ workflow: string;
87
+ job: string;
88
+ }
89
+ /**
90
+ * Resolve the merged variables and secrets of `entries`, then the container
91
+ * registry credentials, writing each onto `into` as soon as it resolves. A
92
+ * context that has no scope binding is named in a warning log line.
93
+ *
94
+ * Writing progressively keeps what already resolved when a later step throws,
95
+ * which is what the dispatch path's catch relies on.
96
+ */
97
+ export declare function resolveContextJobData(args: {
98
+ deps: ContextDataDeps;
99
+ orgId: string;
100
+ entries: ReadonlyArray<{
101
+ name: string;
102
+ env: EngineContext;
103
+ }>;
104
+ hostCtx?: HostFacts;
105
+ routingKey?: string;
106
+ registryAuth?: RegistryAuthInputs;
107
+ log: LogFields;
108
+ into: ContextJobData;
109
+ }): Promise<void>;
110
+ /**
111
+ * The job-config fields a job's context data maps to.
112
+ *
113
+ * `runWideFlatSecrets` are layered over the context secrets so a run-wide value
114
+ * wins a key collision, and so they reach a job with no context at all.
115
+ */
116
+ export declare function contextDataConfigFields(data: ContextJobData | undefined, runWideFlatSecrets: Record<string, string> | undefined): Record<string, unknown>;
117
+ /**
118
+ * A job's dispatch input with its context data removed, for storing as a
119
+ * pending dispatch context. `runWideFlatSecrets` stay: they belong to the run,
120
+ * not to a context, and resolution at release layers them back on top.
121
+ */
122
+ export declare function withoutContextData(jobInput: QueuedJobInput, runWideFlatSecrets: Record<string, string> | undefined): QueuedJobInput;
123
+ /**
124
+ * Resolve a released job's context data and return its dispatch input with
125
+ * that data merged into `jobConfig`.
126
+ *
127
+ * The job's own contexts are not re-gated: it passed their rules, or its hold
128
+ * was released. Throws when the record is malformed, the stores are
129
+ * unavailable, a bound context is gone or replaced, or a lookup fails, so the
130
+ * caller fails the job rather than dispatching it without the secrets it was
131
+ * bound to. A registry-auth refusal leaves the job without registry
132
+ * credentials, as dispatch does.
133
+ */
134
+ export declare function resolveReleasedJobInput(args: {
135
+ jobInput: QueuedJobInput;
136
+ jobName: string;
137
+ resolution: unknown;
138
+ deps: ContextDataDeps | undefined;
139
+ }): Promise<QueuedJobInput>;
140
+ /** Log a release-time resolution failure; the caller fails the job. */
141
+ export declare function logReleaseResolutionFailure(jobInput: QueuedJobInput, err: unknown): string;
142
+ export {};
143
+ //# sourceMappingURL=held-context-data.d.ts.map
@@ -0,0 +1,40 @@
1
+ import type { RegisteredWorkflow } from '../registration/registration-index.js';
2
+ import { type ProcessingDeps } from './processor.js';
3
+ /** The held round's run row fields the release reads. */
4
+ export interface HeldRoundRow {
5
+ customer_id: string;
6
+ workflow_sha: string | null;
7
+ workflow_branch: string | null;
8
+ trigger_decision: string | null;
9
+ }
10
+ /**
11
+ * The workflows the hold recorded the round as covering, or `undefined` for a
12
+ * row held before the record existed.
13
+ */
14
+ export declare function heldRoundWorkflowsOf(triggerDecision: string | null): string[] | undefined;
15
+ /**
16
+ * The workflow repository's organization-wide registrations for the held
17
+ * event, as they were at the held commit.
18
+ *
19
+ * Throws, naming the workflow repository and the workflows, when the release
20
+ * cannot run what the approver approved: the repository registers nothing any
21
+ * more, its lock file at the held commit cannot be read, a workflow the round
22
+ * covered has no live, enabled registration that still subscribes to the event,
23
+ * or the lock file at the held commit does not define it as an
24
+ * organization-wide workflow for the event. The release then fails the held run
25
+ * with that reason and dispatches nothing — never a partial round.
26
+ *
27
+ * Every live registration of a covered workflow is returned, one per source
28
+ * that registers it, as the pass the round was held from evaluates them.
29
+ *
30
+ * The covered set is the one the hold recorded. A row held before that record
31
+ * existed falls back to the held commit's round-shaped workflows, which cannot
32
+ * see a workflow whose registration vanished at an unchanged commit.
33
+ */
34
+ export declare function registrationsAtHeldCommit(args: {
35
+ row: HeldRoundRow;
36
+ deps: Pick<ProcessingDeps, 'registrationIndex' | 'providerRegistry'>;
37
+ workflowRepo: string;
38
+ eventName: string;
39
+ }): Promise<RegisteredWorkflow[]>;
40
+ //# sourceMappingURL=held-round-registrations.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The workflow install gate: the protection rules of every context a
3
+ * workflow's `registries:` / `installEnv:` references name.
4
+ *
5
+ * Every context is evaluated before the gate decides, with the combination rule
6
+ * the job-level multi-context gate uses: a reject from any context rejects,
7
+ * whatever the others say. When none rejects and at least one holds, the gate
8
+ * raises ONE hold whose requirement aggregates every context's rules, most
9
+ * restrictive wins (`aggregateProtectionParams`): the reviewers of every held
10
+ * context must approve, the longest wait timer applies. That hold records which
11
+ * contexts held — its approval covers them — and which were admitted.
12
+ *
13
+ * The release gates again every context the approval did not cover, and
14
+ * refuses when any recorded context now matches a different row than the one
15
+ * the hold was raised against.
16
+ */
17
+ import { z } from 'zod';
18
+ import type { ProtectionGateResult } from '@kici-dev/engine';
19
+ import type { TrustResolution } from '../security/trust-resolver.js';
20
+ import type { Context as ContextRow } from '../db/types.js';
21
+ import { type JobDispatchContext } from '../contexts/protection/pipeline.js';
22
+ import { InstallSecretsDecisionReason } from '../metrics/prometheus.js';
23
+ /** A context the install gate matched: the name the workflow used, and the row it matched. */
24
+ export declare const InstallGateContextSchema: z.ZodObject<{
25
+ name: z.ZodString;
26
+ id: z.ZodString;
27
+ }, z.core.$strip>;
28
+ export type InstallGateContext = z.infer<typeof InstallGateContextSchema>;
29
+ /**
30
+ * What an install-gate hold records about its contexts. Stored with the held
31
+ * dispatch, so the release knows which contexts the approval covers.
32
+ */
33
+ export declare const InstallGateRecordSchema: z.ZodObject<{
34
+ held: z.ZodArray<z.ZodObject<{
35
+ name: z.ZodString;
36
+ id: z.ZodString;
37
+ }, z.core.$strip>>;
38
+ admitted: z.ZodArray<z.ZodObject<{
39
+ name: z.ZodString;
40
+ id: z.ZodString;
41
+ }, z.core.$strip>>;
42
+ }, z.core.$strip>;
43
+ export type InstallGateRecord = z.infer<typeof InstallGateRecordSchema>;
44
+ /** The reject reasons the install gate reports, as its decision metric labels them. */
45
+ export type InstallGateRejectReason = typeof InstallSecretsDecisionReason.EnvNotFound | typeof InstallSecretsDecisionReason.ProtectionRuleBlock;
46
+ /** A gate verdict that holds the install. */
47
+ export type InstallGateHoldResult = ProtectionGateResult & {
48
+ action: 'hold' | 'wait' | 'queue';
49
+ };
50
+ export type InstallGateOutcome = {
51
+ kind: 'pass';
52
+ } | {
53
+ kind: 'reject';
54
+ reasonKind: InstallGateRejectReason;
55
+ reason: string;
56
+ } | {
57
+ kind: 'hold';
58
+ /** The first held context in declaration order; it names the hold. */
59
+ primary: InstallGateContext;
60
+ result: InstallGateHoldResult;
61
+ record: InstallGateRecord;
62
+ };
63
+ interface GateInputs {
64
+ /** Every referenced context name, in declaration order, with the row it matched. */
65
+ matched: ReadonlyMap<string, ContextRow | null>;
66
+ trustResolution: TrustResolution | undefined;
67
+ protectionContext: JobDispatchContext;
68
+ }
69
+ /**
70
+ * Evaluate every referenced context's rules and decide the install gate.
71
+ *
72
+ * Reject wins over hold, as on the job-level gate. A hold aggregates the rules
73
+ * of every context, so its requirement is at least as strict as each held
74
+ * context's own, and its record lists which contexts held and which passed.
75
+ */
76
+ export declare function gateInstall(inputs: GateInputs): Promise<InstallGateOutcome>;
77
+ /**
78
+ * Decide the install gate for a dispatch resumed from a released hold.
79
+ *
80
+ * The contexts the hold recorded as held are covered by its approval and are
81
+ * not gated again: their reviewer and wait-timer rules are stateless and would
82
+ * hold the approved run once more. Every other context is gated again, and any
83
+ * verdict but pass rejects — a release never raises a second hold. A context
84
+ * the hold recorded must still match the row it matched then.
85
+ *
86
+ * `releasedHold` comes from stored JSON, so it is parsed here: an absent or
87
+ * malformed record covers no context, and every context is gated again.
88
+ */
89
+ export declare function gateReleasedInstall(inputs: GateInputs & {
90
+ releasedHold: unknown;
91
+ }): Promise<InstallGateOutcome>;
92
+ export {};
93
+ //# sourceMappingURL=install-gate.d.ts.map
@@ -7,16 +7,20 @@
7
7
  * `<context>:<secret-name>` syntax. The resolver:
8
8
  *
9
9
  * 1. Parses every qualified ref and groups by context name.
10
- * 2. Fires the per-context protection-rule pipeline once per unique env
11
- * (branch / trust / concurrency / reviewer / wait-timer). A `reject`
12
- * result fails the whole workflow dispatch with a clear reason; a
13
- * `hold` / `wait` / `queue` result returns a structured `hold` decision
14
- * so the caller can pause the workflow dispatch as a workflow-scoped held
15
- * run and resume it when the gate clears. On the resume path the caller
16
- * sets `skipProtectionGate` so the gate (already satisfied) is bypassed
17
- * and secrets are resolved directly.
18
- * 3. Resolves secrets per context via `secretResolver.resolveForJob`
19
- * (which writes its own audit log lines).
10
+ * 2. Runs the install gate (`install-gate.ts`): every referenced context's
11
+ * protection rules (branch / trust / concurrency / reviewer / wait-timer)
12
+ * are evaluated before it decides. Any `reject` fails the whole workflow
13
+ * dispatch with a clear reason; otherwise any hold returns ONE structured
14
+ * `hold` decision, aggregated across the contexts, so the caller can
15
+ * pause the workflow dispatch as a workflow-scoped held run and resume it
16
+ * when the gate clears. The hold records which contexts held and which
17
+ * passed. On the resume path the caller sets `skipProtectionGate` and
18
+ * hands back that record: the contexts that held are covered by the
19
+ * approval, every other context is gated again, and a context that now
20
+ * matches a different row is refused.
21
+ * 3. Resolves secrets per context via `secretResolver.resolveForContext`
22
+ * against the row step 2 gated (which writes its own audit log lines),
23
+ * so a context whose name is a glob pattern delivers its own bindings.
20
24
  * 4. Validates each registry URL scheme: HTTPS always allowed; `http://`
21
25
  * allowed only for loopback / `*.local` hosts OR when the org operator
22
26
  * has flipped `org_settings.allow_http_npm_registries=true`.
@@ -33,7 +37,8 @@ import type { ApproverClause, LockRegistry } from '@kici-dev/engine';
33
37
  import type { TrustResolution } from '../security/trust-resolver.js';
34
38
  import type { SecretResolverApi } from '../secrets/secret-resolver.js';
35
39
  import type { ContextStore } from '../contexts/context-store.js';
36
- import { type JobDispatchContext } from '../contexts/protection/pipeline.js';
40
+ import type { JobDispatchContext } from '../contexts/protection/pipeline.js';
41
+ import { type InstallGateRecord } from './install-gate.js';
37
42
  /** Registry spec carried on the dispatch message (token already resolved). */
38
43
  export interface NpmRegistrySpec {
39
44
  url: string;
@@ -51,10 +56,18 @@ export interface ResolveInstallSecretsArgs {
51
56
  secretResolver: SecretResolverApi | undefined;
52
57
  protectionContext: JobDispatchContext;
53
58
  /**
54
- * Resume path: skip the protection-rule gate (already satisfied) and resolve
55
- * secrets directly. The untrusted-contributor strip still runs first.
59
+ * Resume path: the dispatch continues past a released install-gate hold. The
60
+ * gate runs only for the contexts `releasedHold` does not cover. The
61
+ * untrusted-contributor strip still runs first.
56
62
  */
57
63
  skipProtectionGate?: boolean;
64
+ /**
65
+ * Resume path: what the released hold recorded (`hold` decision's
66
+ * `record`). Its held contexts are covered by the approval; every recorded
67
+ * context must still match the same row. Read as stored JSON: absent or
68
+ * malformed, it covers no context.
69
+ */
70
+ releasedHold?: InstallGateRecord;
58
71
  }
59
72
  /** Normalized requirement carried on a `hold` decision. */
60
73
  export interface InstallHoldRequirement {
@@ -83,6 +96,8 @@ export type ResolveInstallSecretsResult = {
83
96
  holdType: string;
84
97
  queueType: 'context' | 'security';
85
98
  requirement: InstallHoldRequirement;
99
+ /** Which contexts held (the approval covers them) and which passed. */
100
+ record: InstallGateRecord;
86
101
  };
87
102
  /** Parse `<context>:<secret-name>`. Returns null on malformed input. */
88
103
  export declare function parseQualifiedSecretRef(ref: string): {
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Multi-context resolution helpers for the dispatch path.
3
3
  *
4
- * A job binds an ordered list of contexts (`LockJob.contexts`). This
4
+ * A job binds an ordered list of contexts: the workflow-level names
5
+ * (`LockWorkflow.contexts`) first, then its own (`LockJob.contexts`). This
5
6
  * module resolves that list into concrete context names (static values
6
7
  * verbatim; every dynamic element is resolved by the agent's init job) and
7
8
  * folds the per-context secrets/variables last-wins. It keeps the heavy fold logic
8
9
  * out of `dispatchMatchedWorkflow`, which must stay under the function-length cap.
9
10
  */
10
- import { type Context, type HostFacts, type LockJob } from '@kici-dev/engine';
11
+ import { type Context, type HostFacts, type LockJob, type LockWorkflow } from '@kici-dev/engine';
11
12
  import type { SecretResolverApi } from '../secrets/secret-resolver.js';
12
13
  import type { VariableStore } from '../contexts/variable-store.js';
13
14
  /**
@@ -23,12 +24,25 @@ export interface ResolvedJobContexts {
23
24
  /** True when a dynamic element must be resolved by an agent init job. */
24
25
  needsInit: boolean;
25
26
  }
27
+ /** One bound-context element, as a lock job carries it. */
28
+ export type ContextRef = NonNullable<LockJob['contexts']>[number];
26
29
  /**
27
- * Resolve the ordered bound-context names from a lock job. Static elements
30
+ * The ordered context elements a job binds: the workflow-level names first,
31
+ * then the job's own. Placing the workflow names first means a job-level
32
+ * context wins a key collision, since later contexts override earlier ones.
33
+ *
34
+ * A workflow-level name the job also binds statically is dropped from the
35
+ * workflow part, so each context is gated and resolved once, at the job's own
36
+ * position. Every gating site reads its names through this function, so a
37
+ * workflow-level context passes the same gates a job-level one does.
38
+ */
39
+ export declare function effectiveContextRefs(workflow: Pick<LockWorkflow, 'contexts'>, job: Pick<LockJob, 'contexts'>): ContextRef[];
40
+ /**
41
+ * Resolve the ordered bound-context names from a lock job and its workflow. Static elements
28
42
  * use their value verbatim; a dynamic element is resolved by the agent's init
29
43
  * job and flags `needsInit`.
30
44
  */
31
- export declare function resolveJobContextNames(lockJob: LockJob): ResolvedJobContexts;
45
+ export declare function resolveJobContextNames(workflow: Pick<LockWorkflow, 'contexts'>, lockJob: Pick<LockJob, 'contexts'>): ResolvedJobContexts;
32
46
  /**
33
47
  * Build the ordered bound-context display list for persistence at dispatch.
34
48
  * Unlike {@link resolveJobContextNames}, this never drops an unresolved
@@ -38,7 +52,7 @@ export declare function resolveJobContextNames(lockJob: LockJob): ResolvedJobCon
38
52
  * The deferred-init flow-back overwrites the placeholder once the agent
39
53
  * resolves the name. Returns an empty array when the job binds no context.
40
54
  */
41
- export declare function buildJobContextDisplayNames(lockJob: LockJob): string[];
55
+ export declare function buildJobContextDisplayNames(workflow: Pick<LockWorkflow, 'contexts'>, lockJob: Pick<LockJob, 'contexts'>): string[];
42
56
  /** Merged secrets/variables across an ordered list of resolved contexts. */
43
57
  export interface MultiEnvMergedData {
44
58
  contextVars?: Record<string, string>;
@@ -52,7 +66,8 @@ export interface MultiEnvMergedData {
52
66
  * then folded in array order so a later context overrides an earlier key.
53
67
  * Secrets are also returned namespaced per context so qualified
54
68
  * `<env>:<secret>` references still resolve. `entries` carries the matched
55
- * `Context` for each name (in order); variables resolve by context id.
69
+ * `Context` for each name (in order); variables and secrets both resolve by the
70
+ * matched context's id, so a glob context contributes what its own row holds.
56
71
  */
57
72
  export declare function resolveMultiEnvMergedData(args: {
58
73
  deps: {
@@ -15,19 +15,31 @@
15
15
  import { type Kysely } from 'kysely';
16
16
  import type { Database } from '../db/types.js';
17
17
  import type { WorkflowDispatchContext } from './dispatch-matched-workflow.js';
18
+ import { type SerializableGlobalDispatchIdentity } from './global-dispatch-identity.js';
18
19
  /**
19
20
  * The serializable subset of a `WorkflowDispatchContext` — everything except
20
21
  * the live `deps` and `bundle`, which are rebuilt on resume. Every field here
21
22
  * is JSON-safe (the event, payload, and lock file already ride the WS protocol
22
- * as JSON).
23
+ * as JSON). A global run's identity is stored without its workflow bundle and
24
+ * credentials, which the resume re-derives.
23
25
  */
24
- export type SerializableWorkflowDispatchInputs = Omit<WorkflowDispatchContext, 'deps' | 'bundle'>;
26
+ export type SerializableWorkflowDispatchInputs = Omit<WorkflowDispatchContext, 'deps' | 'bundle' | 'global'> & {
27
+ global?: SerializableGlobalDispatchIdentity;
28
+ };
29
+ /** Stored inputs as a read returns them. */
30
+ export type LoadedWorkflowDispatchInputs = SerializableWorkflowDispatchInputs & {
31
+ /**
32
+ * Set when the row's sealed secret fields could not be decrypted; the
33
+ * inputs then carry none of them, and the resume abandons the run.
34
+ */
35
+ secretsUnavailable?: string;
36
+ };
25
37
  /** Extract the serializable inputs from a live dispatch context. */
26
38
  export declare function toSerializableInputs(ctx: WorkflowDispatchContext): SerializableWorkflowDispatchInputs;
27
39
  /** Persist the pending workflow context to the in-memory Map and the DB. */
28
40
  export declare function storePendingWorkflowContext(db: Kysely<Database> | undefined, inputs: SerializableWorkflowDispatchInputs): Promise<void>;
29
41
  /** Load the pending workflow context by run id (memory first, then DB). */
30
- export declare function loadPendingWorkflowContext(db: Kysely<Database> | undefined, runId: string): Promise<SerializableWorkflowDispatchInputs | null>;
42
+ export declare function loadPendingWorkflowContext(db: Kysely<Database> | undefined, runId: string): Promise<LoadedWorkflowDispatchInputs | null>;
31
43
  /** Delete the pending workflow context from the in-memory Map and the DB. */
32
44
  export declare function deletePendingWorkflowContext(db: Kysely<Database> | undefined, runId: string): Promise<void>;
33
45
  /**
@@ -21,6 +21,7 @@ import type { LockFile as FullLockFile, SimulatedEvent } from '@kici-dev/engine'
21
21
  import type { OrchestratorMode } from '@kici-dev/engine';
22
22
  import type { WebhookInfo } from '../webhook/handler.js';
23
23
  import type { ProviderBundle } from '../provider-registry.js';
24
+ import type { RegisteredWorkflow } from '../registration/registration-index.js';
24
25
  import { type TrustResolution } from '../security/trust-resolver.js';
25
26
  import { type ForkSwitchContext, type TrustPolicyOutcome } from '../security/trust-policy-gate.js';
26
27
  import { type WorkflowModification } from '../security/workflow-diff.js';
@@ -200,9 +201,16 @@ export declare function dispatchGlobalWorkflowsForOtherRepos(args: {
200
201
  ref: string;
201
202
  dispatchBundle: ProviderBundle;
202
203
  dispatchCredentials: Record<string, unknown>;
203
- /** The inbound event's own bundle, used to post the globals-skipped check. */
204
+ /** The inbound event's own bundle + credentials, used to post the failed-round check. */
204
205
  bundle: ProviderBundle;
205
206
  credentials: Record<string, unknown>;
207
+ /** The event's trust resolution, recorded on each global run. */
208
+ trustResolution: TrustResolution | undefined;
209
+ /**
210
+ * The event's trust-policy verdict. It applies to each global run exactly as
211
+ * it applies to the per-repository ones: a `hold` parks the run in the
212
+ * security queue next to the pull request's own runs.
213
+ */
206
214
  securityDecision: TrustPolicyOutcome;
207
215
  /**
208
216
  * The source `dispatchCredentials` belongs to, when a cross-provider lock-file
@@ -216,6 +224,35 @@ export declare function dispatchGlobalWorkflowsForOtherRepos(args: {
216
224
  * the re-run of a failed evaluation round, which re-decides that round alone.
217
225
  */
218
226
  onlyWorkflowRepo?: string;
227
+ /**
228
+ * Dispatch only the candidates that need the evaluation round. Set by the
229
+ * re-evaluation of a failed or held round: the workflow repository's other
230
+ * candidates reached their outcome on the original delivery.
231
+ */
232
+ onlyRoundCandidates?: boolean;
233
+ /**
234
+ * Set with `onlyRoundCandidates` when the round being re-evaluated is a held
235
+ * round whose security hold was just approved. That hold also held every
236
+ * result-aware-only candidate of the repository, so the release dispatches
237
+ * them. A failed round's re-run leaves it unset: the delivery already
238
+ * dispatched those candidates.
239
+ */
240
+ releasedHold?: boolean;
241
+ /**
242
+ * Asked once the round has decided, before anything it admitted dispatches.
243
+ * Resolving false withdraws those verdicts: nothing the round admitted
244
+ * dispatches, and its workflow repositories are not reported as decided. Set
245
+ * by the release of a held round, whose run may have been cancelled while
246
+ * the round ran.
247
+ */
248
+ admitRoundVerdicts?: () => Promise<boolean>;
249
+ /**
250
+ * The global registrations to evaluate, in place of the ones the index holds
251
+ * for this event. Set by the release of a held round, which evaluates the
252
+ * workflow repository as it was at the commit the hold recorded: the round,
253
+ * its partition, and the dispatch after it all read these entries.
254
+ */
255
+ registrations?: readonly RegisteredWorkflow[];
219
256
  }): Promise<GlobalDispatchOutcome>;
220
257
  /**
221
258
  * Process a webhook through the complete pipeline, recording a `failed` event-log