@kici-dev/orchestrator 0.4.0 → 0.5.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 (67) hide show
  1. package/dist/__test-helpers__/mock-db.d.ts +4 -0
  2. package/dist/agent/agent-version.d.ts +34 -0
  3. package/dist/agent/dispatcher.d.ts +70 -0
  4. package/dist/app.d.ts +23 -1
  5. package/dist/cache/global-eval-round-cache.d.ts +88 -0
  6. package/dist/cache/index.d.ts +3 -0
  7. package/dist/cache/pending-global-evals.d.ts +42 -0
  8. package/dist/cache/pending-inits.d.ts +10 -0
  9. package/dist/cli/commands/cluster-settings.d.ts +41 -3
  10. package/dist/cli/commands/runs.d.ts +1 -0
  11. package/dist/cli.js +1205 -620
  12. package/dist/cluster/cluster-settings-reader.d.ts +53 -1
  13. package/dist/config.d.ts +27 -0
  14. package/dist/content-requirements-cache.d.ts +55 -0
  15. package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
  16. package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
  17. package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
  18. package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
  19. package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
  20. package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
  21. package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
  22. package/dist/db/types.d.ts +60 -2
  23. package/dist/metrics/agent-metrics-aggregator.d.ts +2 -2
  24. package/dist/metrics/prometheus.d.ts +59 -0
  25. package/dist/orchestrator-core.d.ts +12 -1
  26. package/dist/pipeline/content-filter.d.ts +71 -0
  27. package/dist/pipeline/dispatch-matched-workflow.d.ts +247 -8
  28. package/dist/pipeline/global-eval-round.d.ts +293 -0
  29. package/dist/pipeline/job-contexts.d.ts +16 -17
  30. package/dist/pipeline/process-webhook.d.ts +7 -0
  31. package/dist/pipeline/processor.d.ts +56 -2
  32. package/dist/pipeline/route-or-dispatch-jobs.d.ts +6 -0
  33. package/dist/pipeline/test-pipeline.d.ts +12 -0
  34. package/dist/pipeline/webhook-payload-store.d.ts +20 -0
  35. package/dist/provenance/backfill-run.d.ts +10 -1
  36. package/dist/provider-registry.d.ts +38 -3
  37. package/dist/providers/github/check-status-poster.d.ts +22 -3
  38. package/dist/providers/github/commit-message.d.ts +20 -0
  39. package/dist/providers/github/file-contents.d.ts +40 -0
  40. package/dist/providers/github/index.d.ts +2 -0
  41. package/dist/providers/universal-git/config.d.ts +2 -0
  42. package/dist/providers/universal-git/normalizer.d.ts +10 -0
  43. package/dist/queue/cleanup.d.ts +7 -1
  44. package/dist/queue/job-queue.d.ts +69 -6
  45. package/dist/queue/terminalize-unroutable.d.ts +13 -0
  46. package/dist/registration/registration-run-match.d.ts +47 -0
  47. package/dist/reporting/check-run-reporter.d.ts +52 -1
  48. package/dist/reporting/execution-tracker.d.ts +117 -7
  49. package/dist/reporting/log-chunk-sink.d.ts +8 -5
  50. package/dist/routes/admin-org-settings.d.ts +5 -0
  51. package/dist/routes/admin.d.ts +6 -0
  52. package/dist/scaler/manager.d.ts +10 -8
  53. package/dist/security/global-workflow-policy.d.ts +52 -12
  54. package/dist/server.js +27553 -23779
  55. package/dist/standalone.js +5840 -2207
  56. package/dist/webhook/ingest-accept.d.ts +70 -0
  57. package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
  58. package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
  59. package/dist/ws/agent-handler.d.ts +3 -0
  60. package/dist/ws/dashboard-global-workflows-handler.d.ts +30 -9
  61. package/dist/ws/execution-status-frame.d.ts +32 -0
  62. package/dist/ws/platform-client.d.ts +14 -0
  63. package/dist/ws/test-relay-handlers.d.ts +35 -10
  64. package/installer-image-digests.json +3 -3
  65. package/package.json +4 -4
  66. package/sbom.spdx.json +57 -52
  67. package/dist/pipeline/inline-eval.d.ts +0 -44
@@ -87,6 +87,15 @@ interface SetPendingOptions {
87
87
  repo: string;
88
88
  sha: string;
89
89
  workflowName: string;
90
+ /**
91
+ * The repository that DEFINES the workflow, when that is not the repository
92
+ * the run acted on — an organization-wide workflow dispatched against
93
+ * another repository. Qualifies the check-run name so the run cannot share a
94
+ * check run with a same-named workflow of the acted-on repository; see
95
+ * `workflowLabel`. Passing the acted-on repository here is a no-op, so a
96
+ * caller cannot change a per-repository run's name by accident.
97
+ */
98
+ workflowRepoIdentifier?: string;
90
99
  jobNames: string[];
91
100
  installationId?: number;
92
101
  /** Routing key for per-app credential lookup (e.g., "github:12345"). */
@@ -105,6 +114,8 @@ interface SetBuildPendingOptions {
105
114
  repo: string;
106
115
  sha: string;
107
116
  workflowName: string;
117
+ /** See {@link SetPendingOptions.workflowRepoIdentifier}. */
118
+ workflowRepoIdentifier?: string;
108
119
  installationId?: number;
109
120
  /** Routing key for per-app credential lookup (e.g., "github:12345"). */
110
121
  routingKey?: string;
@@ -122,6 +133,8 @@ interface SetBuildCompleteOptions {
122
133
  repo: string;
123
134
  sha: string;
124
135
  workflowName: string;
136
+ /** See {@link SetPendingOptions.workflowRepoIdentifier}. */
137
+ workflowRepoIdentifier?: string;
125
138
  status: TerminalJobStatus;
126
139
  installationId?: number;
127
140
  /** Routing key for per-app credential lookup (e.g., "github:12345"). */
@@ -141,6 +154,8 @@ interface UpdateJobStatusOptions {
141
154
  repo: string;
142
155
  sha: string;
143
156
  workflowName: string;
157
+ /** See {@link SetPendingOptions.workflowRepoIdentifier}. */
158
+ workflowRepoIdentifier?: string;
144
159
  jobName: string;
145
160
  state: TerminalJobStatus;
146
161
  installationId?: number;
@@ -167,6 +182,8 @@ interface UpdateWorkflowStatusOptions {
167
182
  repo: string;
168
183
  sha: string;
169
184
  workflowName: string;
185
+ /** See {@link SetPendingOptions.workflowRepoIdentifier}. */
186
+ workflowRepoIdentifier?: string;
170
187
  overallStatus: TerminalJobStatus;
171
188
  installationId?: number;
172
189
  /** Routing key for per-app credential lookup (e.g., "github:12345"). */
@@ -186,6 +203,8 @@ interface UpdateStepProgressOptions {
186
203
  repo: string;
187
204
  sha: string;
188
205
  workflowName: string;
206
+ /** See {@link SetPendingOptions.workflowRepoIdentifier}. */
207
+ workflowRepoIdentifier?: string;
189
208
  jobName: string;
190
209
  stepIndex: number;
191
210
  stepName: string;
@@ -321,6 +340,8 @@ export declare class CheckRunReporter {
321
340
  repo: string;
322
341
  sha: string;
323
342
  workflowName: string;
343
+ /** See {@link SetPendingOptions.workflowRepoIdentifier}. */
344
+ workflowRepoIdentifier?: string;
324
345
  jobNames: string[];
325
346
  }): void;
326
347
  /**
@@ -337,7 +358,8 @@ export declare class CheckRunReporter {
337
358
  * Separate from execution check runs so users see build progress independently.
338
359
  * Fire-and-forget: errors are logged but don't block the pipeline.
339
360
  *
340
- * Check run name format: kici/{workflowName}/setup
361
+ * Check run name format: kici/{workflowName}/setup — prefixed with the
362
+ * defining repository for a cross-repository global run, see `workflowLabel`.
341
363
  */
342
364
  setBuildPending(opts: SetBuildPendingOptions): void;
343
365
  /**
@@ -390,6 +412,13 @@ export declare class CheckRunReporter {
390
412
  * Read-through helper: look up a check-run ID. Checks L1 first, falls
391
413
  * through to the store on miss, caches the result on hit. Returns
392
414
  * undefined when neither layer has the ID — the caller logs + skips.
415
+ *
416
+ * The same row also rehydrates the `terminalSent` latch. Both L1 entries are
417
+ * dropped together by `cleanupRun`, and only one of them used to come back:
418
+ * the id reloaded from here while the latch did not, which is exactly the
419
+ * pair that lets a late step-progress update resolve a check run and PATCH
420
+ * `status: in_progress` over its completion. Reading `terminal_sent_at` off
421
+ * the row this query already selects costs nothing and closes that gap.
393
422
  */
394
423
  private resolveCheckRunId;
395
424
  /**
@@ -425,6 +454,28 @@ export declare class CheckRunReporter {
425
454
  */
426
455
  private appendTraceIds;
427
456
  private checkRunKey;
457
+ /**
458
+ * The workflow label every check-run name and title is built from.
459
+ *
460
+ * A check run's identity is `(owner, repo, sha, check name)` — on the
461
+ * provider, in `check_run_tracking`'s primary key, and in this class's L1
462
+ * keys. There is no run id anywhere in it. Two per-repository runs cannot
463
+ * collide on that identity, because one lock file cannot define a workflow
464
+ * name twice. An organization-wide workflow can: it is defined in ANOTHER
465
+ * repository, so its name is free to equal a workflow name of the repository
466
+ * it was dispatched against, and on the same commit the two runs then resolve
467
+ * to one check run. The global run's conclusion would complete the acted-on
468
+ * repository's check — the signal branch protection reads — and point its
469
+ * `details_url` at the wrong run, while `cleanupRun` would evict the other
470
+ * run's check-run id and terminal latch on prune.
471
+ *
472
+ * Qualifying the label with the defining repository keeps them apart. The
473
+ * "differs from the acted-on repository" narrowing lives here rather than at
474
+ * the call sites, so a per-repository run's name — which is customer-visible
475
+ * and may sit in a branch-protection required-check list — cannot move
476
+ * because a caller passed the field where it did not apply.
477
+ */
478
+ private workflowLabel;
428
479
  /**
429
480
  * Build the `details_url` for a check run pointing at the dashboard's
430
481
  * public-alias resolver (`/r/orgs/<oal_xxx>/runs/<runId>`). Returns
@@ -28,6 +28,14 @@ export interface ExecutionContext {
28
28
  installationId?: number;
29
29
  requestId?: string;
30
30
  routingKey?: string;
31
+ /**
32
+ * The repository that DEFINES the workflow, when that is not
33
+ * `repoIdentifier` — a global workflow authored in one repository and
34
+ * dispatched against another. Set only when the two differ, so "present"
35
+ * marks a cross-repository global run; forwarded to the Platform on
36
+ * `execution.status`.
37
+ */
38
+ workflowRepoIdentifier?: string;
31
39
  /** Git branch or tag (e.g. "main", "feature/foo"). */
32
40
  ref?: string;
33
41
  /** Trigger event type (e.g. "push", "pr:open"). */
@@ -302,7 +310,14 @@ export declare class ExecutionTracker {
302
310
  /** Agent provenance label when triggered through an agent credential. */
303
311
  triggeredByAgentLabel?: string | null,
304
312
  /** Pull-request number for PR-triggered runs; null/omitted for non-PR runs. */
305
- prNumber?: number | null): Promise<void>;
313
+ prNumber?: number | null,
314
+ /**
315
+ * The repository that DEFINES the workflow, when that is not
316
+ * `repoIdentifier` — an organization-wide workflow authored in one
317
+ * repository and dispatched against another. Omitted/null for every
318
+ * per-repository run, where the two are the same repository.
319
+ */
320
+ workflowRepoIdentifier?: string | null): Promise<void>;
306
321
  /**
307
322
  * Upsert one execution_jobs row per dispatched job (idempotent on
308
323
  * (run_id, job_id) to tolerate a race with an early `onJobStatus`). The
@@ -515,6 +530,58 @@ export declare class ExecutionTracker {
515
530
  initFailure: InitFailure;
516
531
  triggerEvent?: string;
517
532
  commitMessage?: string;
533
+ /**
534
+ * The repository that DEFINES the workflow this run was about to execute.
535
+ * REQUIRED: every run has one, and a caller that does not state it must not
536
+ * compile. Left optional, a global dispatch path added later would record a
537
+ * null marker — and a null marker does not mean "unknown", it means "the
538
+ * workflow lives in this run's own repository", which the whole
539
+ * defining-repository predicate then reads as fact.
540
+ *
541
+ * Narrowed to the differing case here, exactly as `onExecutionStarted`
542
+ * does, so passing `repoIdentifier` (the per-repository case) is correct
543
+ * and records nothing.
544
+ */
545
+ workflowRepoIdentifier: string;
546
+ }): Promise<void>;
547
+ /**
548
+ * Write one `failed` execution_runs row for a global eval round that never
549
+ * produced verdicts.
550
+ *
551
+ * The round decides which organization-wide workflows apply to an event, so a
552
+ * round that fails suppresses every workflow it was deciding on. Without a row
553
+ * that outcome is invisible: no run was ever created for those workflows, so
554
+ * there is nothing on the dashboard to explain why they did not appear.
555
+ *
556
+ * **One row for the whole round, not one per workflow.** The round exists to
557
+ * collapse N candidate workflows into a single pre-run job; fanning its
558
+ * failure back out into N rows would undo exactly that. The reason names every
559
+ * affected workflow instead.
560
+ *
561
+ * Its `runId` is the round job's own — the last attempt's — so the row, the
562
+ * `dispatch_queue` row, and the attempt's logs all carry one id. No conflict
563
+ * guard is needed beyond `doNothing`: the id was minted for this round and
564
+ * belongs to no other run.
565
+ */
566
+ recordGlobalEvalRoundFailureRun(args: {
567
+ runId: string;
568
+ workflowName: string;
569
+ provider: string;
570
+ repoIdentifier: string;
571
+ ref: string;
572
+ sha: string;
573
+ deliveryId: string | null;
574
+ providerContext: Record<string, unknown>;
575
+ routingKey: string;
576
+ failureReason: string;
577
+ triggerEvent?: string;
578
+ /**
579
+ * The repository whose global workflows the round was deciding. A round is
580
+ * definitionally the cross-repository case, so this is normally a different
581
+ * repository from `repoIdentifier` — recorded, like everywhere else, only
582
+ * when the two actually differ.
583
+ */
584
+ workflowRepoIdentifier: string;
518
585
  }): Promise<void>;
519
586
  /**
520
587
  * Record a run paused at the workflow install gate (a `registries:` /
@@ -549,6 +616,13 @@ export declare class ExecutionTracker {
549
616
  * security hold fail-closed unreachable by the comment path.
550
617
  */
551
618
  prNumber?: number | null;
619
+ /**
620
+ * The repository that DEFINES the held workflow. REQUIRED for the same
621
+ * reason as on {@link ExecutionTracker.recordInitFailureRun}: a hold is a
622
+ * live, resumable run, so a null marker here would misattribute it for the
623
+ * whole time it sits in the queue and for the run it resumes into.
624
+ */
625
+ workflowRepoIdentifier: string;
552
626
  }): Promise<void>;
553
627
  /**
554
628
  * Flip a `held` run back to `pending` so the resumed dispatch can proceed
@@ -593,11 +667,13 @@ export declare class ExecutionTracker {
593
667
  * registered. Returns true when a token was taken, false for an unknown or
594
668
  * already-completed run (nothing to hold).
595
669
  *
596
- * Three registration windows need this. A run whose source-pack `__build__`
670
+ * Four registration windows need this. A run whose source-pack `__build__`
597
671
  * job is dispatched first is registered with that job ALONE, and its real
598
- * jobs are only dispatched once the build finishes. A deferred init job and a
599
- * deferred dynamic entry each register their jobs from a fire-and-forget task
600
- * that outlives the dispatch call. In every case, without a token the already
672
+ * jobs are only dispatched once the build finishes; a run without a build is
673
+ * registered with NO jobs before its dispatch loop, so its jobs land once
674
+ * every dispatch has returned. A deferred init job and a deferred dynamic
675
+ * entry each register their jobs from a fire-and-forget task that outlives
676
+ * the dispatch call. In every case, without a token the already
601
677
  * -registered jobs reaching a terminal state satisfies {@link isRunComplete},
602
678
  * so the run is finalized early: a terminal run status is written, the
603
679
  * provider check is posted, and the status is forwarded to the Platform — all
@@ -612,9 +688,13 @@ export declare class ExecutionTracker {
612
688
  * and an off-by-one job count for the length of the window. A counter holds
613
689
  * the completion check open without being visible to any of them, and it
614
690
  * writes no `execution_jobs` row, so it never surfaces as a phantom job.
615
- * A token whose holder never settles keeps the run `running` until the stale
691
+ * A token whose holder never settles keeps the run open until the stale
616
692
  * detector reaps it — the same backstop that covers a job that never
617
- * reports.
693
+ * reports. That backstop only reaches a run that has at least one job: every
694
+ * sub-scan of the stale detector starts from `execution_jobs` or
695
+ * `dispatch_queue`. So a holder that registers NO jobs must terminalize the
696
+ * run itself rather than rely on the sweep — which is what
697
+ * `dispatchMatchedWorkflow` does when its dispatch loop throws.
618
698
  *
619
699
  * Each token must be paired with exactly one {@link releasePendingJobsHold}.
620
700
  */
@@ -645,6 +725,35 @@ export declare class ExecutionTracker {
645
725
  * Used by LogWriter to construct log paths.
646
726
  */
647
727
  getJobName(runId: string, jobId: string): string | undefined;
728
+ /**
729
+ * Resolve the job name that names a step-log storage path, with a durable
730
+ * fallback. The step-log path is `executions/{runId}/job-{jobName}/…` and it
731
+ * is written from two sites — the chunk writer (`log-chunk-sink`) and the
732
+ * `execution_steps.log_path` upsert in `onStepStatus`. A purely in-memory
733
+ * lookup (`getJobName`) returns `undefined` during the brief window between
734
+ * dispatch and `addJobsToRun` populating `run.jobs`, so an early chunk would
735
+ * land under `job-{jobId}` while the reader later keys on `job-{realName}` —
736
+ * silently losing those lines. Both write sites go through this resolver so
737
+ * they cannot disagree: in-memory first, then the authoritative
738
+ * `dispatch_queue.job_name` (the same source `recoverJobFromDispatchQueue`
739
+ * trusts), and only `jobId` when the job is genuinely unknown.
740
+ */
741
+ resolveJobName(runId: string, jobId: string): Promise<string>;
742
+ /**
743
+ * Seed the in-memory job name for a job the coordinator did not itself
744
+ * dispatch — a worker-rerouted job. Such a job is tracked in the cluster
745
+ * coordinator's own map, never enters this tracker's `run.jobs` via
746
+ * `addJobsToRun`, and has no coordinator-owned `dispatch_queue` row (the
747
+ * worker owns it under a fresh id), so `resolveJobName` would fall back to
748
+ * the bare `jobId` for its early log chunks while the reader keys on the real
749
+ * name — the same split-brain that loses lines. Seeding the name here, at
750
+ * reroute time, makes `resolveJobName`'s fast path correct from the first
751
+ * relayed chunk, independent of when the worker's `dispatch_queue` row becomes
752
+ * visible. In-memory only: the `execution_jobs` row is still created lazily by
753
+ * the worker's first status update. Guarded against clobbering an existing
754
+ * entry so a later terminal status is never overwritten.
755
+ */
756
+ registerJobName(runId: string, jobId: string, jobName: string): void;
648
757
  /**
649
758
  * Find the runId of the in-memory run that owns a given dispatched jobId.
650
759
  *
@@ -697,6 +806,7 @@ export declare class ExecutionTracker {
697
806
  status: ExecutionRunStatus;
698
807
  routingKey?: string;
699
808
  repoIdentifier?: string;
809
+ workflowRepoIdentifier?: string;
700
810
  sha?: string;
701
811
  ref?: string;
702
812
  triggerEvent?: string;
@@ -7,9 +7,12 @@
7
7
  * behave identically — a worker-dispatched job's logs are persisted, counted
8
8
  * and forwarded exactly like a locally-dispatched one's.
9
9
  *
10
- * The storage key is derived from `executionTracker.getJobName`, the same call
11
- * `ExecutionTracker.onStepStatus` uses to fill `execution_steps.log_path`, so
12
- * the reader and the writer cannot disagree about the naming rule.
10
+ * The storage key is derived from `executionTracker.resolveJobName`, the same
11
+ * call `ExecutionTracker.onStepStatus` uses to fill `execution_steps.log_path`,
12
+ * so the reader and the writer cannot disagree about the naming rule — the
13
+ * resolver falls back to `dispatch_queue.job_name` during the dispatch window
14
+ * before in-memory job state is populated, which is what stops an early chunk
15
+ * from being persisted under an unreadable `job-{jobId}` path.
13
16
  */
14
17
  import type { LogStream } from '@kici-dev/engine';
15
18
  import type { LogWriter } from './log-writer.js';
@@ -32,9 +35,9 @@ export interface LogChunkSinkDeps {
32
35
  stepLogBuffer?: StepLogBuffer;
33
36
  /** Durable step-log persistence. Absent when the orchestrator has no database. */
34
37
  logWriter?: LogWriter;
35
- /** Resolves the job name that names the storage path. */
38
+ /** Resolves the job name that names the storage path (durable fallback). */
36
39
  executionTracker?: {
37
- getJobName(runId: string, jobId: string): string | undefined;
40
+ resolveJobName(runId: string, jobId: string): Promise<string>;
38
41
  };
39
42
  /**
40
43
  * Forward to the Platform for browser fan-out. Absent in independent mode.
@@ -18,6 +18,11 @@ import type { AccessLogWriter } from '../audit/access-log.js';
18
18
  interface OrgSettingsRouteDeps {
19
19
  db: Kysely<Database>;
20
20
  rbac: RbacEnforcer;
21
+ /**
22
+ * Applies when `cluster_settings.global_workflows_enabled` is NULL —
23
+ * `config.globalWorkflowsEnabled`.
24
+ */
25
+ globalWorkflowsEnabledDefault: boolean;
21
26
  /**
22
27
  * Optional — when wired, each `dashboard_write_policy` flip emits one
23
28
  * `access_log` row (`org_settings.dashboard_write_policy.update`)
@@ -40,6 +40,12 @@ export interface AdminRouteDeps {
40
40
  rbac: RbacEnforcer;
41
41
  secretStore: PgSecretStore;
42
42
  auditLogger: AuditLogger;
43
+ /**
44
+ * Fleet-wide default for the global-workflows master switch, applied when the
45
+ * cluster column is NULL. Optional so WS-only / test admins can omit it;
46
+ * omitting it means the secure default (disabled).
47
+ */
48
+ globalWorkflowsEnabledDefault?: boolean;
43
49
  /**
44
50
  * Optional -- the coordinator drain controller. When provided, the
45
51
  * `POST`/`GET /api/v1/admin/orchestrator/drain` routes are mounted (backing
@@ -422,16 +422,18 @@ export declare class ScalerManager {
422
422
  */
423
423
  onAgentDisconnected(agentId: string): void;
424
424
  /**
425
- * Look up the scaler backend name (e.g. `container`, `firecracker`,
425
+ * Look up the scaler backend TYPE (`container`, `firecracker`,
426
426
  * `bare-metal`) for a registered agent. Returns null if the agent is
427
- * not scaler-managed (i.e. a static / stateful agent).
427
+ * not scaler-managed (a static / stateful agent).
428
428
  *
429
- * Used by AgentMetricsAggregator to inject a `scaler` label on each
430
- * `kici_agent_*` series so dashboards can split per-scaler instead of
431
- * per-agent_id (which is too high-cardinality and doesn't tell the
432
- * operator which pool is loaded).
433
- */
434
- getBackendName(agentId: string): string | null;
429
+ * Used by AgentMetricsAggregator to stamp the `scaler` label on each
430
+ * kici_agent_* series. The label MUST be the backend type, not the
431
+ * operator-chosen scaler name: the Platform catalog filter constrains
432
+ * it to AGENT_SCALER_VALUES (the four types), so a free-form name is
433
+ * dropped as bad_label_value. `managedAgentIndex` stores the scaler
434
+ * name, so resolve it through `backends` to the type.
435
+ */
436
+ getBackendType(agentId: string): string | null;
435
437
  /**
436
438
  * Called from agent-handler.ts when an agent sends config.ack.
437
439
  * For Firecracker agents, clears MMDS data (belt-and-suspenders with in-VM iptables).
@@ -1,14 +1,25 @@
1
1
  import type { Kysely } from 'kysely';
2
2
  import type { Database } from '../db/types.js';
3
+ import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
3
4
  interface GlobalWorkflowPermission {
4
5
  allowed: boolean;
5
6
  reason?: string;
6
7
  }
7
8
  /**
8
- * Org-level permission enforcement for global workflows.
9
+ * Permission enforcement for global workflows.
9
10
  *
10
- * Three independent axes (each stored as a jsonb array of
11
- * `{routingKey?, pattern}` entries on `org_settings`):
11
+ * A fleet-wide master switch gates everything first:
12
+ * `cluster_settings.global_workflows_enabled`, held by the orchestrator
13
+ * operator through `kici-admin cluster-settings` (NULL ⇒ the configured
14
+ * `KICI_GLOBAL_WORKFLOWS_ENABLED` default, off by default). When the switch is
15
+ * off — or the cluster row is unreadable, which fails closed — no repo may
16
+ * register or dispatch a global workflow, whatever the per-org lists say.
17
+ *
18
+ * With the switch on, three independent per-org axes apply (each stored as a
19
+ * jsonb array of `{routingKey?, pattern}` entries on `org_settings`). A missing
20
+ * `org_settings` row means "no per-org restrictions" for the repo and source
21
+ * axes, not a denial; elevated access still requires an explicit list, so a
22
+ * missing row grants none.
12
23
  *
13
24
  * - Workflow-repo allow-list (`global_workflow_allowed_repos`): which repos
14
25
  * may author global workflows. null/empty = any repo. Checked at
@@ -32,15 +43,32 @@ interface GlobalWorkflowPermission {
32
43
  */
33
44
  export declare class GlobalWorkflowPolicy {
34
45
  private readonly db;
35
- constructor(db: Kysely<Database>);
46
+ private readonly clusterSettings;
47
+ /** Applies when the cluster column is NULL — `config.globalWorkflowsEnabled`. */
48
+ private readonly defaultEnabled;
49
+ constructor(db: Kysely<Database>, clusterSettings: ClusterSettingsReader,
50
+ /** Applies when the cluster column is NULL — `config.globalWorkflowsEnabled`. */
51
+ defaultEnabled: boolean);
52
+ /**
53
+ * The fleet-wide master gate, consulted before any per-org list.
54
+ *
55
+ * Returns `undefined` when the gate passes, or the denial to return
56
+ * verbatim when it does not — so all three axes share one decision and one
57
+ * pair of reasons.
58
+ *
59
+ * Fails closed on `{ ok: false }`. That case is a database fault, not an
60
+ * operator choice, and answering it from `defaultEnabled` would open the
61
+ * feature during an outage the moment that default is ever flipped.
62
+ */
63
+ private clusterGate;
36
64
  /**
37
65
  * Check whether a workflow-authoring repository is allowed to register or
38
66
  * dispatch global workflows. The workflow's own routing key is matched
39
67
  * against each entry's optional `routingKey` qualifier.
40
68
  *
41
69
  * Decision flow:
42
- * 1. No org_settings row or globally disabled → not allowed (opt-in).
43
- * 2. allowed list null/empty → any repo allowed.
70
+ * 1. Cluster master switch off or unreadable → not allowed (fails closed).
71
+ * 2. allowed list null/empty (or no org_settings row) → any repo allowed.
44
72
  * 3. Entry matches when its `routingKey` is absent OR equals
45
73
  * `workflowRoutingKey`, AND its pattern matches the workflow repo.
46
74
  * 4. Otherwise → not allowed.
@@ -52,18 +80,30 @@ export declare class GlobalWorkflowPolicy {
52
80
  * (so a source-qualified deny only applies to that specific source).
53
81
  *
54
82
  * Decision flow:
55
- * 1. No org_settings row or globally disabled → not allowed (opt-in).
56
- * 2. denied list null/empty → allowed.
83
+ * 1. Cluster master switch off or unreadable → not allowed (fails closed).
84
+ * 2. denied list null/empty (or no org_settings row) → allowed.
57
85
  * 3. Any deny entry whose routing key (if set) equals the event's routing
58
86
  * key AND whose pattern matches the source repo → not allowed.
59
87
  */
60
88
  isSourceRepoAllowed(eventRoutingKey: string, sourceRepoIdentifier: string, customerId: string): Promise<GlobalWorkflowPermission>;
61
89
  /**
62
- * Check whether a workflow-authoring repository has elevated access (i.e.,
63
- * may read source-repo secrets during execution). The workflow's routing
64
- * key is matched against each entry's optional `routingKey` qualifier.
90
+ * Check whether a workflow-authoring repository is on the elevated-access
91
+ * list. The workflow's routing key is matched against each entry's optional
92
+ * `routingKey` qualifier. Returns false if no org_settings row or the
93
+ * elevated list is null/empty.
94
+ *
95
+ * @deprecated Not enforced, and not enforceable in this shape. It was meant
96
+ * to gate a global workflow's job reading the *source* repository's secrets,
97
+ * but the organization-wide dispatch path resolves no secrets at all — it
98
+ * binds no secret contexts, and writes no secret material into a job config
99
+ * (asserted by `pipeline/process-webhook-globals-secrets.test.ts`). So there
100
+ * is no injection for a grant to widen, and this method has no caller.
65
101
  *
66
- * Returns false if no org_settings row or the elevated list is null/empty.
102
+ * Granting it would not be a matter of calling this from the dispatch path:
103
+ * secrets are stored `(org_id, scope, key)` with no repository dimension, so
104
+ * "the source repository's secrets" is not a set the orchestrator can name
105
+ * today. The list, its admin route, its CLI mutators and its wire field are
106
+ * deprecated pending removal at v1.0.0 (`docs/user/deprecations.md`).
67
107
  */
68
108
  isElevatedAccessAllowed(workflowRoutingKey: string, repoIdentifier: string, customerId: string): Promise<boolean>;
69
109
  private getSettings;