@kici-dev/orchestrator 0.1.16 → 0.1.18

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 (83) hide show
  1. package/dist/agent/dispatcher.d.ts +27 -1
  2. package/dist/agent/host-roster-reaper.d.ts +42 -0
  3. package/dist/agent/host-roster.d.ts +110 -0
  4. package/dist/agent/registry.d.ts +65 -2
  5. package/dist/app.d.ts +3 -0
  6. package/dist/approvals/apply-decision.d.ts +11 -1
  7. package/dist/cache/pending-inits.d.ts +5 -0
  8. package/dist/cli/api-client.d.ts +11 -0
  9. package/dist/cli/commands/host.d.ts +13 -0
  10. package/dist/cli/commands/local-hook.d.ts +15 -0
  11. package/dist/cli/commands/local-trigger.d.ts +24 -0
  12. package/dist/cli/commands/remote-source.d.ts +17 -0
  13. package/dist/cli/commands/shared/versioned-upgrade.d.ts +37 -0
  14. package/dist/cli/kici-admin.d.ts +13 -0
  15. package/dist/cli/service/compose.d.ts +2 -1
  16. package/dist/cli/service/index.d.ts +2 -2
  17. package/dist/cli/service/instance/manifest.d.ts +12 -0
  18. package/dist/cli/service/instance/resolve.d.ts +12 -2
  19. package/dist/cli/service/launchd.d.ts +4 -1
  20. package/dist/cli/service/platform-detect.d.ts +5 -0
  21. package/dist/cli/service/systemd.d.ts +2 -1
  22. package/dist/cli/service/types.d.ts +37 -0
  23. package/dist/cli/service/windows.d.ts +2 -1
  24. package/dist/cli.js +1340 -198
  25. package/dist/cluster/coordinator.d.ts +5 -1
  26. package/dist/config/schema.d.ts +4 -0
  27. package/dist/config/types.d.ts +9 -0
  28. package/dist/config.d.ts +6 -4
  29. package/dist/dashboard/handler.d.ts +26 -2
  30. package/dist/dashboard/needs-edges.d.ts +13 -0
  31. package/dist/db/migrations/035_pending_workflow_contexts.d.ts +11 -0
  32. package/dist/db/migrations/036_attestations.d.ts +15 -0
  33. package/dist/db/migrations/037_generic_sources_provider_type_local.d.ts +19 -0
  34. package/dist/db/migrations/038_remote_sources.d.ts +14 -0
  35. package/dist/db/migrations/039_host_roster.d.ts +19 -0
  36. package/dist/db/migrations/040_runsonall_pin.d.ts +4 -0
  37. package/dist/db/migrations/041_wave_gated.d.ts +4 -0
  38. package/dist/db/migrations/042_dispatch_queue_patterns.d.ts +4 -0
  39. package/dist/db/types.d.ts +144 -10
  40. package/dist/diagnostics/fleet-collector.d.ts +1 -1
  41. package/dist/entry-helpers.d.ts +7 -29
  42. package/dist/environments/held-runs.d.ts +33 -0
  43. package/dist/index.js +1 -0
  44. package/dist/lockfile-redos-guard.d.ts +19 -0
  45. package/dist/metrics/prometheus.d.ts +17 -2
  46. package/dist/metrics/scheduled-jobs.d.ts +2 -2
  47. package/dist/orchestrator-core.d.ts +94 -1
  48. package/dist/pipeline/dispatch-matched-workflow.d.ts +60 -2
  49. package/dist/pipeline/install-secrets-resolver.d.ts +32 -5
  50. package/dist/pipeline/needs-scheduler.d.ts +12 -10
  51. package/dist/pipeline/pending-workflow-context.d.ts +44 -0
  52. package/dist/pipeline/processor.d.ts +31 -4
  53. package/dist/pipeline/remote-source-store.d.ts +21 -0
  54. package/dist/pipeline/resume-workflow.d.ts +26 -0
  55. package/dist/pipeline/wave-scheduler.d.ts +60 -0
  56. package/dist/providers/local/index.d.ts +33 -0
  57. package/dist/providers/local/local-source-config.d.ts +17 -0
  58. package/dist/providers/{internal → local}/lock-file-fetcher.d.ts +6 -6
  59. package/dist/providers/{internal → local}/normalizer.d.ts +28 -28
  60. package/dist/providers/{internal → local}/repo-url-builder.d.ts +6 -6
  61. package/dist/queue/job-queue.d.ts +61 -2
  62. package/dist/reporting/execution-tracker.d.ts +61 -0
  63. package/dist/routes/admin-events.d.ts +3 -4
  64. package/dist/routes/uploads.d.ts +36 -26
  65. package/dist/server.js +53755 -50888
  66. package/dist/sources/build-platform-sources.d.ts +4 -2
  67. package/dist/stale-detector/stale-run-detector.d.ts +14 -1
  68. package/dist/standalone.js +17234 -15725
  69. package/dist/webhook/generic-sources-listener.d.ts +4 -0
  70. package/dist/webhook/generic-sources.d.ts +37 -11
  71. package/dist/webhook/register-source-bundle.d.ts +14 -4
  72. package/dist/ws/agent-handler.d.ts +17 -0
  73. package/dist/ws/dashboard-dispatch-guard.d.ts +21 -0
  74. package/dist/ws/dashboard-env-handler.d.ts +13 -1
  75. package/dist/ws/oidc-token-relay.d.ts +59 -0
  76. package/dist/ws/platform-client.d.ts +30 -1
  77. package/dist/ws/test-relay-handlers.d.ts +112 -0
  78. package/installer-image-digests.json +3 -3
  79. package/package.json +22 -22
  80. package/sbom.spdx.json +3228 -6633
  81. package/dist/providers/internal/index.d.ts +0 -32
  82. package/dist/routes/test-trigger.d.ts +0 -41
  83. package/dist/ws/observer-handler.d.ts +0 -42
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Lock-load ReDoS revalidation.
3
+ *
4
+ * Re-validates every regex matcher in a fetched lock file before it is cached
5
+ * or dispatched. Defense-in-depth against a hand-edited or non-compiled lock
6
+ * that smuggled a ReDoS-prone pattern past the compile-time gate: the compiler
7
+ * runs the same `assertMatchersSafe` check when it emits the lock, but the
8
+ * orchestrator does not trust that the lock it fetched was produced by our
9
+ * compiler.
10
+ */
11
+ import type { LockFile } from '@kici-dev/engine';
12
+ /**
13
+ * Walk every static job's `runsOn` / `excludeLabels` / `runsOnAll` matchers and
14
+ * throw if any regex matcher is ReDoS-prone. Dynamic job generators carry no
15
+ * static routing matchers (they materialize jobs at eval time, which re-runs the
16
+ * compile-time gate), so only static jobs are checked.
17
+ */
18
+ export declare function assertLockFileRegexesSafe(lockFile: LockFile): void;
19
+ //# sourceMappingURL=lockfile-redos-guard.d.ts.map
@@ -2,6 +2,8 @@
2
2
  export declare function setAgentsActive(value: number): void;
3
3
  /** Set the current config version number. */
4
4
  export declare function setConfigVersion(value: number): void;
5
+ /** Set the current number of declared (static) roster hosts that are unreachable. */
6
+ export declare function setDeclaredHostsUnreachable(value: number): void;
5
7
  /** Set the current number of stale runs detected. */
6
8
  export declare function setStaleRunsCurrent(value: number): void;
7
9
  interface ScalerUsageRow {
@@ -264,6 +266,8 @@ export declare const InstallSecretsDecisionReason: {
264
266
  readonly ProtectionRuleBlock: "protection_rule_block";
265
267
  readonly MissingToken: "missing_token";
266
268
  readonly MissingInstallEnv: "missing_install_env";
269
+ /** A protection gate held the workflow install (hold / wait / queue). */
270
+ readonly Held: "held";
267
271
  };
268
272
  export type InstallSecretsDecisionReason = (typeof InstallSecretsDecisionReason)[keyof typeof InstallSecretsDecisionReason];
269
273
  /**
@@ -280,8 +284,8 @@ export type InstallSecretsChannel = (typeof InstallSecretsChannel)[keyof typeof
280
284
  * Total install-secrets resolution decisions.
281
285
  *
282
286
  * Labels:
283
- * - decision: pass | reject
284
- * - reason: ok (only with `pass`), or one of the reject reasons enumerated in `InstallSecretsDecisionReason`
287
+ * - decision: pass | reject | hold
288
+ * - reason: ok (only with `pass`), held (only with `hold`), or one of the reject reasons enumerated in `InstallSecretsDecisionReason`
285
289
  *
286
290
  * Pass series are emitted exactly once per resolver call that does any work
287
291
  * (workflows declaring neither `registries:` nor `installEnv:` are NOT
@@ -320,5 +324,16 @@ export declare const installSecretsContributorStrippedTotal: import("@openteleme
320
324
  * - environment: the environment name referenced in the qualified `<environment>:<secret>` ref (per-org count is typically <10)
321
325
  */
322
326
  export declare const installSecretsTokenResolutionDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
327
+ /**
328
+ * Register every queued orchestrator observable gauge on the real meter.
329
+ *
330
+ * Must run AFTER `initTelemetry()` has wired the global MeterProvider —
331
+ * `createApp()` calls it once during bootstrap. Registering the gauges at
332
+ * module-eval time instead would bind them to the no-op provider (the
333
+ * bundler hoists some module init above the entry's `initTelemetry()` call),
334
+ * leaving every `kici_orch_*` gauge absent from the /metrics scrape and the
335
+ * Platform push. Idempotent: repeat calls are no-ops.
336
+ */
337
+ export declare function registerOrchestratorMetrics(): void;
323
338
  export {};
324
339
  //# sourceMappingURL=prometheus.d.ts.map
@@ -4,13 +4,13 @@
4
4
  * - job: scheduled-job name (one of `OrchestratorScheduledJobName`)
5
5
  * - result: success | failure
6
6
  */
7
- export declare const jobRunsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
7
+ export declare const jobRunsTotal: Pick<import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>, "add">;
8
8
  /**
9
9
  * Histogram of per-tick duration, seconds.
10
10
  * Labels:
11
11
  * - job: scheduled-job name (one of `OrchestratorScheduledJobName`)
12
12
  */
13
- export declare const jobDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
13
+ export declare const jobDurationSeconds: Pick<import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>, "record">;
14
14
  /**
15
15
  * Unix timestamp (seconds) of the most recent successful tick.
16
16
  * Labels:
@@ -13,6 +13,7 @@ import { type ColdStore } from '@kici-dev/shared';
13
13
  import type { AppConfig } from './config.js';
14
14
  import { ConfigReloader } from './config/reload.js';
15
15
  import { AgentRegistry } from './agent/registry.js';
16
+ import { HostRosterStore } from './agent/host-roster.js';
16
17
  import { JobQueue } from './queue/job-queue.js';
17
18
  import { EventLogWriter } from './webhook/event-log.js';
18
19
  import { AccessLogWriter } from './audit/access-log.js';
@@ -22,7 +23,7 @@ import { DedupCache } from './webhook/dedup.js';
22
23
  import { ObserverRegistry } from './ws/observer-registry.js';
23
24
  import { AgentMetricsAggregator } from './metrics/agent-metrics-aggregator.js';
24
25
  import { SourceLocationStore } from './app.js';
25
- import { type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
26
+ import { type LabelMatcher, type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
26
27
  import { ScalerManager } from './scaler/index.js';
27
28
  import type { ScalerConfig } from './scaler/index.js';
28
29
  import type { CacheStorage } from './storage/index.js';
@@ -63,6 +64,7 @@ export interface OrchestratorSubsystems {
63
64
  pool: pg.Pool;
64
65
  providerRegistry: ProviderRegistry;
65
66
  agentRegistry: AgentRegistry;
67
+ hostRosterStore: HostRosterStore;
66
68
  dispatcher: Dispatcher;
67
69
  queue: JobQueue;
68
70
  scalerManager: ScalerManager | null;
@@ -210,6 +212,97 @@ export interface OrchestratorHooks {
210
212
  */
211
213
  startupLogMessage: (port: number) => string;
212
214
  }
215
+ /**
216
+ * Normalize a lock job's `needs` array (strings, NeedsEntry objects, or
217
+ * NeedsGroupEntry objects) to the set of upstream BASE job names. Group entries
218
+ * are skipped here — group fan-in is resolved separately by the scheduler.
219
+ */
220
+ export declare function upstreamBaseNamesFromNeeds(needs: unknown): string[];
221
+ /**
222
+ * Partition a lock job's `runsOn` / `excludeLabels` matchers into exact label
223
+ * strings and regex patterns for internal-event (cron / `ctx.emit`) dispatch.
224
+ * Lock jobs carry `runsOn` as `LabelMatcher[]`; the coordinator routing and the
225
+ * direct dispatcher both need exact labels for the indexed/SQL fast path and
226
+ * regex patterns as a separate JS post-filter — never the raw matcher objects.
227
+ */
228
+ export declare function internalJobRunsOnSelectors(job: {
229
+ runsOn?: readonly LabelMatcher[];
230
+ excludeLabels?: readonly LabelMatcher[];
231
+ }): {
232
+ runsOnLabels: string[];
233
+ runsOnPatterns: LabelMatcher[];
234
+ excludeLabels: string[];
235
+ excludePatterns: LabelMatcher[];
236
+ };
237
+ /**
238
+ * Parse an `execution_jobs.outputs` cell (string JSON or object) to a plain
239
+ * object, or null when empty / unparseable.
240
+ */
241
+ export declare function parseOutputsCell(outputs: unknown): Record<string, unknown> | null;
242
+ /**
243
+ * Build the downstream `upstreamJobOutputs` map keyed by BASE name. A base name
244
+ * that fanned into matrix children (rows with `matrix_values`) gets the
245
+ * `{ byMatrix, merged }` envelope; a single non-fanned row keeps the flat shape.
246
+ * Returns undefined when no upstream produced outputs.
247
+ */
248
+ export declare function buildUpstreamOutputsByBase(baseNames: string[], rows: Array<{
249
+ job_name: string;
250
+ outputs: unknown;
251
+ matrix_values: unknown;
252
+ variant_kind?: string | null;
253
+ variant_label?: string | null;
254
+ status?: string | null;
255
+ }>): Record<string, Record<string, unknown>> | undefined;
256
+ /**
257
+ * Fold a `runsOnAll` upstream's host children into the `byHost` envelope
258
+ * `{ byHost: { '<host>': outputs }, summary: { succeededHosts, failedHosts, outputs } }`.
259
+ * Unlike the matrix envelope, `summary.outputs[key]` is an array view across hosts
260
+ * (host order), never a last-write-wins scalar; `succeededHosts`/`failedHosts`
261
+ * record each host's terminal outcome.
262
+ */
263
+ export declare function buildHostOutputsEnvelope(children: Array<{
264
+ host: string;
265
+ status: string | null;
266
+ parsed: Record<string, unknown>;
267
+ }>): {
268
+ byHost: Record<string, Record<string, unknown>>;
269
+ summary: {
270
+ succeededHosts: string[];
271
+ failedHosts: string[];
272
+ outputs: Record<string, unknown[]>;
273
+ };
274
+ };
275
+ /**
276
+ * Group an upstream's child rows into the matrix outputs envelope
277
+ * `{ byMatrix: { '<suffix>': outputs }, merged: <last-write-wins> }`. The suffix
278
+ * is the text inside the `(...)` of each expanded child name; children are
279
+ * merged in name order (deterministic, matching dispatch order).
280
+ */
281
+ export declare function buildMatrixOutputsEnvelope(baseName: string, children: Array<{
282
+ job_name: string;
283
+ parsed: Record<string, unknown>;
284
+ }>): {
285
+ byMatrix: Record<string, Record<string, unknown>>;
286
+ merged: Record<string, unknown>;
287
+ };
288
+ /**
289
+ * Merge plain `outputs` and decrypted `secret outputs` from upstream jobs
290
+ * into the dispatch envelope's `secrets` + `upstreamJobOutputs` fields.
291
+ *
292
+ * For a fanned (matrix) upstream, the downstream sees a keyed envelope
293
+ * `{ byMatrix: { '<suffix>': outputs }, merged: <last-write-wins> }` under the
294
+ * BASE name; a non-fanned upstream keeps the flat outputs shape.
295
+ *
296
+ * The needs-aware scheduler guarantees upstreams are
297
+ * terminal before the downstream is dispatched, so the lookups below
298
+ * always find final values. Errors are swallowed (warn-logged) so a flaky
299
+ * upstream-outputs read doesn't block dispatch — the agent will still get
300
+ * its declared secrets, just without the merged upstream additions.
301
+ */
302
+ export declare function mergeUpstreamOutputs(db: Kysely<Database>, runId: string, jobName: string, needs: unknown, dispatchSecrets: Record<string, string> | undefined, secretKey: string): Promise<{
303
+ mergedSecrets: Record<string, string> | undefined;
304
+ upstreamJobOutputs: Record<string, Record<string, unknown>> | undefined;
305
+ }>;
213
306
  export declare function bootstrapOrchestrator(config: AppConfig, hooks: OrchestratorHooks, options?: {
214
307
  otelSdk?: {
215
308
  shutdown(): Promise<void>;
@@ -14,7 +14,7 @@
14
14
  * results through the pipeline.
15
15
  */
16
16
  import { CacheRefScope } from '@kici-dev/engine';
17
- import type { LockWorkflow, SimulatedEvent, WorkflowDecision } from '@kici-dev/engine';
17
+ import type { LabelMatcher, LockWorkflow, SimulatedEvent, WorkflowDecision, MaterializedJob } from '@kici-dev/engine';
18
18
  import type { WebhookInfo } from '../webhook/handler.js';
19
19
  import type { ProviderBundle } from '../provider-registry.js';
20
20
  import type { TrustResolution } from '../security/trust-resolver.js';
@@ -91,7 +91,64 @@ export interface WorkflowDispatchContext {
91
91
  export interface DispatchMatchedWorkflowResult {
92
92
  /** Number of jobs successfully dispatched (non-rejected). */
93
93
  dispatchedJobCount: number;
94
+ /** True when the workflow install gate paused the dispatch (held run). */
95
+ held?: boolean;
94
96
  }
97
+ /** Options controlling a (re-)dispatch of a matched workflow. */
98
+ export interface DispatchMatchedWorkflowOptions {
99
+ /**
100
+ * Resume path: skip the workflow install protection gate (already satisfied)
101
+ * so secrets resolve directly and the dispatch flows into job dispatch.
102
+ */
103
+ skipInstallProtectionGate?: boolean;
104
+ /** The released held-run id being resumed (for logging / correlation). */
105
+ reuseHeldRunId?: string;
106
+ /**
107
+ * The run id whose `held` execution_runs row should be reused (flipped to
108
+ * pending) instead of inserting a fresh row.
109
+ */
110
+ reuseRunId?: string;
111
+ }
112
+ /** Exact labels + regex patterns partitioned from a lock job's selectors. */
113
+ interface JobRoutingSelectors {
114
+ runsOnLabels: string[];
115
+ runsOnPatterns: LabelMatcher[];
116
+ excludeLabels: string[];
117
+ excludePatterns: LabelMatcher[];
118
+ }
119
+ /**
120
+ * Partition a lock job's runsOn / excludeLabels matchers into exact labels (SQL
121
+ * `@>` prefilter + registry index) and regex patterns (JS post-filter). A
122
+ * `runsOnAll` host-fanout job has no `runsOn`; its pinned children carry no
123
+ * routing (the pin targets the resolved agent directly).
124
+ */
125
+ export declare function runsOnSelectorsForLockJob(lockJob: {
126
+ runsOn?: readonly LabelMatcher[];
127
+ excludeLabels?: readonly LabelMatcher[];
128
+ }): JobRoutingSelectors;
129
+ /** Per-child rolling-wave plan: which children are held + the base's wave policy. */
130
+ export interface WavePlan {
131
+ /** `expandedName`s held behind the wave gate (beyond the maxParallel window). */
132
+ held: Set<string>;
133
+ /** `expandedName` → the base's `{maxParallel, failFast}`, stamped on every child of a bounded wave. */
134
+ policy: Map<string, {
135
+ maxParallel: number;
136
+ failFast: boolean;
137
+ }>;
138
+ }
139
+ /**
140
+ * Compute the rolling-wave plan for a materialized job set.
141
+ *
142
+ * For each base job declaring `maxParallel` whose fan-out produced more than one
143
+ * child, children are ordered deterministically by `variant_label` (the matrix
144
+ * suffix / hostname, via `expandedName`) and every child at index `>=
145
+ * maxParallel` is held (`wave_gated=true`). The first `maxParallel` dispatch
146
+ * immediately; held children release one-per-terminal via the wave-scheduler.
147
+ * Every child of a bounded-wave base — held or not — gets a `policy` entry so
148
+ * the wave-scheduler can read the width/failFast at terminal time. A non-fan-out
149
+ * job (single child) or one without `maxParallel` contributes nothing.
150
+ */
151
+ export declare function computeWavePlan(materializedJobs: readonly MaterializedJob[]): WavePlan;
95
152
  /**
96
153
  * Dispatch a single matched workflow.
97
154
  *
@@ -107,5 +164,6 @@ export interface DispatchMatchedWorkflowResult {
107
164
  * I. deferred init dispatch (fire-and-forget per job)
108
165
  * J. deferred dynamic dispatch (fire-and-forget per dynamic entry)
109
166
  */
110
- export declare function dispatchMatchedWorkflow(ctx: WorkflowDispatchContext): Promise<DispatchMatchedWorkflowResult>;
167
+ export declare function dispatchMatchedWorkflow(ctx: WorkflowDispatchContext, opts?: DispatchMatchedWorkflowOptions): Promise<DispatchMatchedWorkflowResult>;
168
+ export {};
111
169
  //# sourceMappingURL=dispatch-matched-workflow.d.ts.map
@@ -8,10 +8,13 @@
8
8
  *
9
9
  * 1. Parses every qualified ref and groups by environment name.
10
10
  * 2. Fires the per-environment protection-rule pipeline once per unique env
11
- * (branch / trust / concurrency / reviewer / wait-timer). Any non-pass
12
- * result rejects the whole workflow dispatch v1 does not yet support
13
- * workflow-scoped held-runs, so a `hold` from the reviewer-gate is
14
- * surfaced as a reject with a clear reason.
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.
15
18
  * 3. Resolves secrets per environment via `secretResolver.resolveForJob`
16
19
  * (which writes its own audit log lines).
17
20
  * 4. Validates each registry URL scheme: HTTPS always allowed; `http://`
@@ -26,7 +29,7 @@
26
29
  * Pure helper: no imports of the dispatch giant. Tested in isolation in
27
30
  * `install-secrets-resolver.test.ts`.
28
31
  */
29
- import type { LockRegistry } from '@kici-dev/engine';
32
+ import type { ApproverClause, LockRegistry } from '@kici-dev/engine';
30
33
  import type { TrustResolution } from '../security/trust-resolver.js';
31
34
  import type { SecretResolver } from '../secrets/secret-resolver.js';
32
35
  import type { EnvironmentStore } from '../environments/environment-store.js';
@@ -47,6 +50,18 @@ export interface ResolveInstallSecretsArgs {
47
50
  environmentStore: EnvironmentStore | undefined;
48
51
  secretResolver: SecretResolver | undefined;
49
52
  protectionContext: JobDispatchContext;
53
+ /**
54
+ * Resume path: skip the protection-rule gate (already satisfied) and resolve
55
+ * secrets directly. The untrusted-contributor strip still runs first.
56
+ */
57
+ skipProtectionGate?: boolean;
58
+ }
59
+ /** Normalized requirement carried on a `hold` decision. */
60
+ export interface InstallHoldRequirement {
61
+ clauses: ApproverClause[];
62
+ /** ISO timestamp after which the hold expires. */
63
+ expiresAt: string;
64
+ reason: string;
50
65
  }
51
66
  export type ResolveInstallSecretsResult = {
52
67
  decision: 'pass';
@@ -56,6 +71,18 @@ export type ResolveInstallSecretsResult = {
56
71
  } | {
57
72
  decision: 'reject';
58
73
  reason: string;
74
+ } | {
75
+ decision: 'hold';
76
+ /** The gate action that paused the dispatch. */
77
+ action: 'hold' | 'wait' | 'queue';
78
+ /** The environment whose install gate held. */
79
+ envName: string;
80
+ /** Resolved environment id (for the held row). */
81
+ environmentId: string;
82
+ /** Discriminates the release trigger: 'reviewer' | 'wait_timer' | 'concurrency' | 'security'. */
83
+ holdType: string;
84
+ queueType: 'environment' | 'security';
85
+ requirement: InstallHoldRequirement;
59
86
  };
60
87
  /** Parse `<environment>:<secret-name>`. Returns null on malformed input. */
61
88
  export declare function parseQualifiedSecretRef(ref: string): {
@@ -19,25 +19,28 @@
19
19
  */
20
20
  import type { Kysely } from 'kysely';
21
21
  import type { Database } from '../db/types.js';
22
- import type { NeedsEntry, NeedsGroupEntry } from '@kici-dev/engine';
22
+ import type { MaterializedJob } from '@kici-dev/engine';
23
23
  /** Result of evaluating downstream jobs after an upstream completes. */
24
24
  export interface SchedulerResult {
25
25
  jobName: string;
26
26
  action: 'dispatch' | 'skip';
27
27
  reason?: string;
28
28
  }
29
- /** Minimal lock job shape needed by insertEdgesForRun. */
30
- interface LockJobLike {
31
- readonly name: string;
32
- readonly needs: readonly (string | NeedsEntry | NeedsGroupEntry)[];
33
- readonly dependsOnGroups?: readonly string[];
34
- }
35
29
  /**
36
30
  * Insert dependency edges at run start for static-to-static needs.
31
+ *
32
+ * Operates on materialized jobs (matrix-expanded children) and an expansion map
33
+ * (base job name -> the expanded child names). An upstream reference to a base
34
+ * name `test` that fanned into `test (a)` / `test (b)` produces one edge per
35
+ * child, so a downstream `needs: ['test']` waits for ALL children to terminate.
36
+ * Each materialized child likewise inherits the full upstream edge set under its
37
+ * own expanded name.
38
+ *
37
39
  * NeedsGroupEntry items are skipped (resolved later via resolveGroupEdges).
38
- * Root jobs (no needs, no dependsOnGroups) are marked needs_satisfied=true.
40
+ * Root jobs (no needs, no dependsOnGroups) are marked needs_satisfied=true,
41
+ * keyed by expanded name.
39
42
  */
40
- export declare function insertEdgesForRun(db: Kysely<Database>, runId: string, lockJobs: readonly LockJobLike[]): Promise<void>;
43
+ export declare function insertEdgesForRun(db: Kysely<Database>, runId: string, jobs: readonly MaterializedJob[], expansionMap: ReadonlyMap<string, readonly string[]>): Promise<void>;
41
44
  /**
42
45
  * Resolve dynamic group edges after eval completion.
43
46
  *
@@ -81,5 +84,4 @@ export declare function checkSchedulerInvariant(db: Kysely<Database>, runId: str
81
84
  * due to failure propagation. Only follows edges where if_failed='skip'.
82
85
  */
83
86
  export declare function getFailurePropagationTargets(db: Kysely<Database>, runId: string, failedJobName: string): Promise<string[]>;
84
- export {};
85
87
  //# sourceMappingURL=needs-scheduler.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pending workflow dispatch context store — the workflow-level analogue of the
3
+ * `pending_job_contexts` store. Persists the serializable inputs of a
4
+ * `WorkflowDispatchContext` so a workflow whose install gate held can be
5
+ * resumed (reviewer approve, wait-timer expiry, concurrency slot free) by
6
+ * rebuilding the dispatch context and re-running `dispatchMatchedWorkflow`.
7
+ *
8
+ * Only the serializable inputs are stored — the live `deps` (ProcessingDeps)
9
+ * and `bundle` (ProviderBundle) are NOT persisted; they are rebuilt from the
10
+ * live orchestrator on resume.
11
+ *
12
+ * Writes to both an in-memory Map (fast read on the same process) and the DB
13
+ * (crash recovery + cross-orchestrator read), mirroring the pending-job store.
14
+ */
15
+ import type { Kysely } from 'kysely';
16
+ import type { Database } from '../db/types.js';
17
+ import type { WorkflowDispatchContext } from './dispatch-matched-workflow.js';
18
+ /**
19
+ * The serializable subset of a `WorkflowDispatchContext` — everything except
20
+ * the live `deps` and `bundle`, which are rebuilt on resume. Every field here
21
+ * is JSON-safe (the event, payload, and lock file already ride the WS protocol
22
+ * as JSON).
23
+ */
24
+ export type SerializableWorkflowDispatchInputs = Omit<WorkflowDispatchContext, 'deps' | 'bundle'>;
25
+ /** Extract the serializable inputs from a live dispatch context. */
26
+ export declare function toSerializableInputs(ctx: WorkflowDispatchContext): SerializableWorkflowDispatchInputs;
27
+ /** Persist the pending workflow context to the in-memory Map and the DB. */
28
+ export declare function storePendingWorkflowContext(db: Kysely<Database> | undefined, inputs: SerializableWorkflowDispatchInputs): Promise<void>;
29
+ /** 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>;
31
+ /** Delete the pending workflow context from the in-memory Map and the DB. */
32
+ export declare function deletePendingWorkflowContext(db: Kysely<Database> | undefined, runId: string): Promise<void>;
33
+ /**
34
+ * Restore the in-memory Map from the DB on startup, skipping rows whose run has
35
+ * already reached a terminal state. Mirrors `restorePendingJobContexts`.
36
+ * Returns the number of restored contexts.
37
+ */
38
+ export declare function restorePendingWorkflowContexts(db: Kysely<Database>): Promise<number>;
39
+ /**
40
+ * Clear all entries from the in-memory pending workflow contexts Map.
41
+ * @internal Exported for testing only.
42
+ */
43
+ export declare function clearPendingWorkflowContextsMap(): void;
44
+ //# sourceMappingURL=pending-workflow-context.d.ts.map
@@ -29,6 +29,7 @@ import type { PendingDynamicTracker } from '../cache/pending-dynamics.js';
29
29
  import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
30
30
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
31
31
  import type { AgentRegistry } from '../agent/registry.js';
32
+ import type { HostRosterStore } from '../agent/host-roster.js';
32
33
  import type { RunCoordinator } from '../cluster/coordinator.js';
33
34
  import type { TeamMembershipLookup } from '../approvals/team-membership-lookup.js';
34
35
  import type { LogStorage } from '../reporting/log-storage.js';
@@ -62,6 +63,21 @@ interface PendingJobContext {
62
63
  jobInput: QueuedJobInput;
63
64
  runsOnLabels: string[];
64
65
  }
66
+ /**
67
+ * Register an eval gate and return a promise that resolves when the scheduler
68
+ * opens it (the eval job's upstream needs are all satisfied).
69
+ */
70
+ export declare function trackEvalGate(runId: string, evalJobName: string): Promise<void>;
71
+ /**
72
+ * Open a registered eval gate, unblocking the deferred dispatch task. Returns
73
+ * true if a gate was registered for this eval job (so the scheduler knows it
74
+ * handled the ready signal itself and must not run the normal dispatch path).
75
+ */
76
+ export declare function openEvalGate(runId: string, evalJobName: string): boolean;
77
+ /** True when a job name is a result-aware dynamic eval job awaiting its gate. */
78
+ export declare function isEvalGatePending(runId: string, evalJobName: string): boolean;
79
+ /** Clear all eval gates for a run (called on run completion / cleanup). */
80
+ export declare function clearEvalGatesForRun(runId: string): void;
65
81
  /**
66
82
  * Store a pending dispatch context for a job that will be dispatched later
67
83
  * by the needs scheduler. The key is `${runId}:${jobName}`.
@@ -112,7 +128,7 @@ export declare function isRootJob(lockJob: LockJob): boolean;
112
128
  * Why this exists
113
129
  * ----------------
114
130
  * The webhook pipeline binds `lockFileFetcher` to the inbound webhook's
115
- * provider bundle. When an internal-sourced webhook (e.g., the staging
131
+ * provider bundle. When a local-sourced webhook (e.g., the staging
116
132
  * stg-ha-smoke failover-dispatch test) arrives for a repo whose lock file
117
133
  * is only accessible via a different provider (e.g., github), the inbound
118
134
  * fetcher returns null and trigger matching silently drops the webhook.
@@ -143,7 +159,7 @@ export declare function isRootJob(lockJob: LockJob): boolean;
143
159
  * -----------
144
160
  * Each fallback fetcher is invoked with the REGISTRATION'S
145
161
  * `providerContext`, NOT the inbound normalizer's credentials. This is
146
- * load-bearing: the InternalWebhookNormalizer returns `{}` as
162
+ * load-bearing: the LocalWebhookNormalizer returns `{}` as
147
163
  * credentials, which would never satisfy a github fetcher that requires
148
164
  * `installationId`. The registration carries the correct credentials
149
165
  * because it was created via the owning provider's source.
@@ -178,8 +194,10 @@ export declare function resolveLockFileWithFallback(args: {
178
194
  * Resolve the customer/org ID for a routing key.
179
195
  *
180
196
  * Checks the `sources` table first (GitHub App sources), then
181
- * `generic_webhook_sources` (generic webhook sources). Falls back to
182
- * '__default__' if neither table has the routing key.
197
+ * `generic_webhook_sources` (generic webhook sources), then `remote_sources`
198
+ * (the auto-provisioned anchor for Platform-relayed `kici run remote`, routing
199
+ * key `remote:<orgId>`). Falls back to '__default__' if none of the three
200
+ * tables has the routing key.
183
201
  */
184
202
  export declare function resolveOrgId(db: Kysely<Database>, routingKey: string): Promise<string>;
185
203
  /**
@@ -318,6 +336,15 @@ export interface ProcessingDeps {
318
336
  /** Access-log writer for the orchestrator audit stream. Optional -- if not
319
337
  * set, hold-creation audit rows (`held_run.request`) are skipped. */
320
338
  accessLogWriter?: AccessLogWriter;
339
+ /** Host roster store for runsOnAll fan-out resolution. Optional -- if not set,
340
+ * runsOnAll jobs cannot be resolved and fail at materialize. */
341
+ hostRosterStore?: HostRosterStore;
342
+ /** This orchestrator instance id (for the cross-cluster host-fanout pin reroute). */
343
+ instanceId?: string;
344
+ /** Static-host grace before a disconnected static host reads unreachable (ms). */
345
+ rosterGraceMs?: number;
346
+ /** Cap on runsOnAll per-host children (default 1024). */
347
+ maxFanoutHosts?: number;
321
348
  }
322
349
  /**
323
350
  * Check if any trigger in the lock file workflows uses path filters.
@@ -0,0 +1,21 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { Database, RemoteSourceRow } from '../db/types.js';
3
+ /**
4
+ * Deterministic routing key for an org's Platform-relayed remote runs. A
5
+ * Platform-relayed `test.trigger` carries this key; `resolveOrgId` maps it back
6
+ * to the canonical org id via the `remote_sources` row.
7
+ */
8
+ export declare function remoteRoutingKeyFor(orgId: string): string;
9
+ /**
10
+ * Idempotently upsert the `remote_sources` anchor for an org. Called on every
11
+ * Platform (re)connect once the orchestrator learns its canonical org id from
12
+ * `auth.success`. The unique `(customer_id)` constraint makes this a safe
13
+ * self-heal — re-running updates `cluster_id` if the cluster identity changed.
14
+ */
15
+ export declare function provisionRemoteSource(db: Kysely<Database>, params: {
16
+ orgId: string;
17
+ clusterId: string | null;
18
+ }): Promise<void>;
19
+ /** Read the auto-provisioned remote-source row for an org, if it exists. */
20
+ export declare function getRemoteSource(db: Kysely<Database>, orgId: string): Promise<RemoteSourceRow | undefined>;
21
+ //# sourceMappingURL=remote-source-store.d.ts.map
@@ -0,0 +1,26 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { Database } from '../db/types.js';
3
+ import type { ProcessingDeps } from './processor.js';
4
+ import type { ReleaseSignal } from '../environments/held-runs.js';
5
+ import { type WorkflowDispatchContext } from './dispatch-matched-workflow.js';
6
+ import { type SerializableWorkflowDispatchInputs } from './pending-workflow-context.js';
7
+ /**
8
+ * Rebuild a live `WorkflowDispatchContext` from the persisted serializable
9
+ * inputs by re-attaching the orchestrator's live `deps` and reconstructing the
10
+ * provider `bundle` from the live registry (keyed by the stored routing key).
11
+ * Returns null when the provider bundle can no longer be resolved.
12
+ */
13
+ export declare function rebuildWorkflowDispatchContext(inputs: SerializableWorkflowDispatchInputs, deps: ProcessingDeps): WorkflowDispatchContext | null;
14
+ /**
15
+ * Resume a released workflow install-gate hold. Loads the pending context,
16
+ * rebuilds the dispatch context, and re-dispatches with the gate skipped. On a
17
+ * lost pending context (or unresolvable provider bundle) the run is failed
18
+ * loudly rather than silently dropped.
19
+ */
20
+ export declare function resumeWorkflow(signal: ReleaseSignal, deps: ProcessingDeps, db: Kysely<Database> | undefined): Promise<void>;
21
+ /**
22
+ * Cancel a rejected workflow install-gate hold: mark the run cancelled and drop
23
+ * the pending context.
24
+ */
25
+ export declare function rejectWorkflow(runId: string, deps: ProcessingDeps, db: Kysely<Database> | undefined, reason: string): Promise<void>;
26
+ //# sourceMappingURL=resume-workflow.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Rolling-wave scheduler for bounded fan-out (`maxParallel` / `failFast`).
3
+ *
4
+ * Sibling of the needs-scheduler (`needs-scheduler.ts`): both fire on a job
5
+ * reaching terminal state and decide what to do with held downstream/sibling
6
+ * jobs. The needs-scheduler releases jobs whose `needs` edges are now satisfied;
7
+ * the wave-scheduler releases the next `wave_gated` sibling of a fan-out base
8
+ * whenever an in-flight slot frees up (or, under `failFast`, skips the held
9
+ * remainder on the first child failure).
10
+ *
11
+ * Pure DB — no in-memory state. Every decision is a fresh query against
12
+ * `execution_jobs` keyed by `(run_id, base_job_name)`, so the scheduler needs
13
+ * zero recovery code on orchestrator restart. The caller performs the DB write
14
+ * (clear `wave_gated` / mark skipped) and the dispatch (`onJobReady`), keeping
15
+ * this module a pure decision function.
16
+ */
17
+ import type { Kysely } from 'kysely';
18
+ import type { Database } from '../db/types.js';
19
+ /** Inputs identifying a completed fan-out child. The wave policy is read from the base group. */
20
+ export interface WaveEvaluation {
21
+ runId: string;
22
+ /** The base (logical) job name shared by every fan-out child. */
23
+ baseJobName: string;
24
+ /** Terminal status of the child that just completed. */
25
+ completedStatus: string;
26
+ }
27
+ /** The wave-scheduler's decision for one completed child. */
28
+ export type WaveResult = {
29
+ action: 'release';
30
+ jobName: string;
31
+ baseJobName: string;
32
+ maxParallel: number;
33
+ failFast: boolean;
34
+ } | {
35
+ action: 'skip-remaining';
36
+ jobNames: string[];
37
+ } | {
38
+ action: 'noop';
39
+ };
40
+ /**
41
+ * Decide what happens after a fan-out child of `baseJobName` reaches terminal.
42
+ *
43
+ * The wave policy (`maxParallel` / `failFast`) is read from the base group's
44
+ * own rows — every child of a bounded wave carries the same stamped
45
+ * `wave_max_parallel` / `wave_fail_fast`, so the just-completed child's slot
46
+ * being re-inserted without the policy on release does not break the chain
47
+ * (the still-held siblings carry it). If no sibling carries a policy, this is
48
+ * not a bounded wave → `noop`.
49
+ *
50
+ * - `failFast` + a child failure → `skip-remaining` every still-held sibling.
51
+ * - in-flight count `< maxParallel` AND a held sibling exists → `release` the
52
+ * next held sibling (lowest `variant_label`).
53
+ * - otherwise → `noop`.
54
+ *
55
+ * "In-flight" = a non-terminal, non-`wave_gated` child (it has been dispatched
56
+ * and not yet completed). The just-completed child is terminal, so it does not
57
+ * count against the window — its slot is the one we are filling.
58
+ */
59
+ export declare function evaluateWave(db: Kysely<Database>, evaluation: WaveEvaluation): Promise<WaveResult>;
60
+ //# sourceMappingURL=wave-scheduler.d.ts.map