@kici-dev/orchestrator 0.4.0 → 0.6.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 (183) hide show
  1. package/dist/__test-helpers__/fake-scaler-state-store.d.ts +24 -0
  2. package/dist/__test-helpers__/mock-db-query.d.ts +130 -0
  3. package/dist/__test-helpers__/mock-db.d.ts +27 -0
  4. package/dist/agent/agent-version.d.ts +34 -0
  5. package/dist/agent/dispatcher.d.ts +151 -13
  6. package/dist/agent/host-roster.d.ts +37 -5
  7. package/dist/agent/registry.d.ts +2 -0
  8. package/dist/app.d.ts +56 -7
  9. package/dist/approvals/apply-decision.d.ts +67 -19
  10. package/dist/approvals/held-run-release-wiring.d.ts +45 -0
  11. package/dist/approvals/independent-wiring.d.ts +55 -0
  12. package/dist/approvals/resume-router.d.ts +9 -0
  13. package/dist/approvals/triggerer-subject.d.ts +37 -0
  14. package/dist/cache/dep-cache.d.ts +54 -10
  15. package/dist/cache/global-eval-round-cache.d.ts +95 -0
  16. package/dist/cache/index.d.ts +3 -0
  17. package/dist/cache/pending-global-evals.d.ts +42 -0
  18. package/dist/cache/pending-inits.d.ts +10 -0
  19. package/dist/cli/api-client.d.ts +18 -0
  20. package/dist/cli/commands/cluster-settings.d.ts +52 -3
  21. package/dist/cli/commands/diagnose.d.ts +6 -0
  22. package/dist/cli/commands/held-run.d.ts +58 -0
  23. package/dist/cli/commands/runs.d.ts +1 -0
  24. package/dist/cli/commands/trust-policy.d.ts +117 -12
  25. package/dist/cli/kici-admin.d.ts +10 -0
  26. package/dist/cli.js +2770 -938
  27. package/dist/cluster/cluster-identity.d.ts +3 -3
  28. package/dist/cluster/cluster-settings-reader.d.ts +59 -1
  29. package/dist/cluster/coordinator.d.ts +44 -0
  30. package/dist/cluster/index.d.ts +4 -0
  31. package/dist/cluster/peer-handler.d.ts +19 -0
  32. package/dist/cluster/peer-registry.d.ts +17 -0
  33. package/dist/cluster/plan-headroom-store.d.ts +26 -0
  34. package/dist/cluster/worker-eviction.d.ts +69 -0
  35. package/dist/cold-store/load-event-log-range.d.ts +10 -3
  36. package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
  37. package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
  38. package/dist/config.d.ts +48 -21
  39. package/dist/content-requirements-cache.d.ts +55 -0
  40. package/dist/contexts/held-runs.d.ts +123 -16
  41. package/dist/contexts/protection/aggregate.d.ts +3 -2
  42. package/dist/contexts/protection/branch-gate.d.ts +15 -0
  43. package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
  44. package/dist/contexts/protection/pipeline.d.ts +18 -0
  45. package/dist/contexts/release-queued-holds.d.ts +35 -0
  46. package/dist/dashboard/attestation-filters.d.ts +8 -0
  47. package/dist/dashboard/handler.d.ts +6 -24
  48. package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
  49. package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
  50. package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
  51. package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
  52. package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
  53. package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
  54. package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
  55. package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
  56. package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
  57. package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
  58. package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
  59. package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
  60. package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
  61. package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
  62. package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
  63. package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
  64. package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
  65. package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
  66. package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
  67. package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
  68. package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
  69. package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
  70. package/dist/db/types.d.ts +428 -12
  71. package/dist/entry-helpers.d.ts +28 -0
  72. package/dist/events/event-emitter.d.ts +22 -0
  73. package/dist/events/event-router.d.ts +87 -1
  74. package/dist/events/types.d.ts +2 -23
  75. package/dist/git/credential-broker.d.ts +94 -0
  76. package/dist/git/inherited-secret.d.ts +24 -0
  77. package/dist/git/installation-token.d.ts +38 -0
  78. package/dist/git/job-context.d.ts +13 -0
  79. package/dist/index.js +324 -84
  80. package/dist/metrics/agent-metrics-aggregator.d.ts +4 -4
  81. package/dist/metrics/prometheus.d.ts +170 -3
  82. package/dist/oidc/oidc-mint-registration.d.ts +7 -3
  83. package/dist/orchestrator-core.d.ts +91 -24
  84. package/dist/pipeline/content-filter.d.ts +71 -0
  85. package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
  86. package/dist/pipeline/dispatch-matched-workflow.d.ts +521 -11
  87. package/dist/pipeline/global-eval-round.d.ts +307 -0
  88. package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
  89. package/dist/pipeline/invoke-gate.d.ts +164 -0
  90. package/dist/pipeline/job-contexts.d.ts +16 -17
  91. package/dist/pipeline/needs-scheduler.d.ts +13 -0
  92. package/dist/pipeline/process-webhook.d.ts +126 -31
  93. package/dist/pipeline/processor.d.ts +282 -23
  94. package/dist/pipeline/rerun.d.ts +21 -0
  95. package/dist/pipeline/resume-workflow.d.ts +40 -7
  96. package/dist/pipeline/route-or-dispatch-jobs.d.ts +15 -0
  97. package/dist/pipeline/security-hold-check.d.ts +217 -0
  98. package/dist/pipeline/test-pipeline.d.ts +12 -0
  99. package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
  100. package/dist/pipeline/webhook-payload-store.d.ts +20 -0
  101. package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
  102. package/dist/policy/dashboard-write-policy.d.ts +14 -0
  103. package/dist/provenance/backfill-run.d.ts +10 -1
  104. package/dist/provider-registry.d.ts +47 -4
  105. package/dist/providers/github/check-status-poster.d.ts +33 -3
  106. package/dist/providers/github/commit-message.d.ts +20 -0
  107. package/dist/providers/github/file-contents.d.ts +40 -0
  108. package/dist/providers/github/index.d.ts +2 -2
  109. package/dist/providers/github/normalizer.d.ts +3 -2
  110. package/dist/providers/local/normalizer.d.ts +3 -5
  111. package/dist/providers/universal-git/config.d.ts +2 -0
  112. package/dist/providers/universal-git/index.d.ts +11 -2
  113. package/dist/providers/universal-git/normalizer.d.ts +10 -0
  114. package/dist/queue/cleanup.d.ts +7 -1
  115. package/dist/queue/job-queue.d.ts +112 -7
  116. package/dist/queue/terminalize-unroutable.d.ts +13 -0
  117. package/dist/registration/extractor.d.ts +3 -3
  118. package/dist/registration/registration-index.d.ts +7 -0
  119. package/dist/registration/registration-run-match.d.ts +47 -0
  120. package/dist/registration/registration-store.d.ts +22 -0
  121. package/dist/reporting/check-run-reporter.d.ts +146 -2
  122. package/dist/reporting/check-run-summary.d.ts +25 -1
  123. package/dist/reporting/execution-tracker.d.ts +324 -9
  124. package/dist/reporting/log-chunk-sink.d.ts +8 -5
  125. package/dist/reporting/run-aggregator.d.ts +4 -14
  126. package/dist/reporting/step-display-order.d.ts +43 -0
  127. package/dist/routes/admin-held-runs.d.ts +142 -0
  128. package/dist/routes/admin-org-settings.d.ts +5 -0
  129. package/dist/routes/admin-trust-policy.d.ts +34 -6
  130. package/dist/routes/admin.d.ts +15 -0
  131. package/dist/scaler/backend-factory.d.ts +55 -0
  132. package/dist/scaler/bare-metal-backend.d.ts +43 -5
  133. package/dist/scaler/claim-store.d.ts +119 -0
  134. package/dist/scaler/config.d.ts +4 -0
  135. package/dist/scaler/container-backend.d.ts +18 -4
  136. package/dist/scaler/container-routing.d.ts +23 -0
  137. package/dist/scaler/container-spawn.d.ts +28 -0
  138. package/dist/scaler/event-backend.d.ts +124 -0
  139. package/dist/scaler/event-provision-reaper.d.ts +350 -0
  140. package/dist/scaler/failure-tracker.d.ts +1 -1
  141. package/dist/scaler/firecracker-backend.d.ts +33 -6
  142. package/dist/scaler/index.d.ts +11 -1
  143. package/dist/scaler/label-matcher.d.ts +4 -3
  144. package/dist/scaler/manager.d.ts +905 -76
  145. package/dist/scaler/resolve-container-auth.d.ts +45 -0
  146. package/dist/scaler/scaler-events.d.ts +11 -0
  147. package/dist/scaler/scaler-state-store.d.ts +294 -10
  148. package/dist/scaler/types.d.ts +131 -13
  149. package/dist/scaler/warm-pool.d.ts +132 -29
  150. package/dist/security/comment-handler.d.ts +50 -11
  151. package/dist/security/global-workflow-policy.d.ts +52 -12
  152. package/dist/security/identity-link.d.ts +60 -0
  153. package/dist/security/lock-source.d.ts +11 -13
  154. package/dist/security/reduced-privilege-note.d.ts +59 -0
  155. package/dist/security/trust-directory-store.d.ts +233 -0
  156. package/dist/security/trust-policy-gate.d.ts +94 -59
  157. package/dist/security/trust-policy-store.d.ts +3 -0
  158. package/dist/security/trust-resolver.d.ts +14 -98
  159. package/dist/server.d.ts +11 -1
  160. package/dist/server.js +56329 -42234
  161. package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
  162. package/dist/stale-detector/stale-run-detector.d.ts +61 -3
  163. package/dist/standalone.js +37619 -23570
  164. package/dist/webhook/ingest-accept.d.ts +70 -0
  165. package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
  166. package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
  167. package/dist/worker/in-memory-job-queue.d.ts +1 -1
  168. package/dist/ws/agent-handler.d.ts +31 -4
  169. package/dist/ws/dashboard-context-handler.d.ts +28 -4
  170. package/dist/ws/dashboard-global-workflows-handler.d.ts +36 -9
  171. package/dist/ws/execution-status-frame.d.ts +32 -0
  172. package/dist/ws/failure-messages.d.ts +2 -0
  173. package/dist/ws/git-credential-relay.d.ts +39 -0
  174. package/dist/ws/oidc-token-relay.d.ts +6 -11
  175. package/dist/ws/platform-client.d.ts +68 -9
  176. package/dist/ws/test-relay-handlers.d.ts +35 -10
  177. package/installer-image-digests.json +3 -3
  178. package/package.json +24 -19
  179. package/sbom.spdx.json +868 -821
  180. package/dist/approvals/team-membership-lookup.d.ts +0 -13
  181. package/dist/pipeline/inline-eval.d.ts +0 -44
  182. package/dist/providers/github/contributor-resolver.d.ts +0 -30
  183. package/dist/security/contributor-cache.d.ts +0 -83
@@ -1,22 +1,15 @@
1
- /**
2
- * Shared approve/reject applier — the single code path behind both the
3
- * dashboard WS handler and the `kici` CLI HTTP route, so the authorization +
4
- * resume story is identical regardless of the surface.
5
- *
6
- * Flow for an approve:
7
- * 1. Load the hold + its recorded decisions.
8
- * 2. Build the team-membership lookup and run `canApprove` (eligibility +
9
- * self-approval gate). Ineligible actors are rejected with a clear reason.
10
- * 3. Record the decision in `held_run_approvals`.
11
- * 4. Re-evaluate. If all clauses are satisfied, `release()` the hold and
12
- * resume the element (re-dispatch for job/workflow; the agent bridge for
13
- * step scope — wired by the caller via `onStepRelease`).
14
- *
15
- * A reject records the decision and `reject()`s the hold (failing the element).
16
- */
17
1
  import { ApprovalDecision } from '@kici-dev/engine';
2
+ import type { HeldRun } from '../db/types.js';
3
+ import { HoldOutcome } from '../pipeline/security-hold-check.js';
18
4
  import type { HeldRunStore, ReleaseSignal } from '../contexts/held-runs.js';
19
5
  import { type TeamMembershipLookup } from './approval-resolver.js';
6
+ /** Whether a decision's post-commit consequence ran to completion. */
7
+ export interface DecisionConsequenceResult {
8
+ /** True when the whole consequence chain ran; false when a step of it threw. */
9
+ ok: boolean;
10
+ /** The failure message, when `ok` is false. */
11
+ error?: string;
12
+ }
20
13
  /** Outcome of applying a decision. */
21
14
  export interface ApplyDecisionResult {
22
15
  /** Whether the decision was accepted (recorded). */
@@ -29,6 +22,29 @@ export interface ApplyDecisionResult {
29
22
  reason?: string;
30
23
  /** The release signal, when the element was released (for the caller to resume). */
31
24
  release?: ReleaseSignal;
25
+ /**
26
+ * The decision's post-commit consequence — already running, deliberately not
27
+ * awaited by the applier. Present on `released` and `rejected`, absent on
28
+ * every outcome that changed nothing (`pending`, `ineligible`, `not-found`).
29
+ *
30
+ * It NEVER rejects: a throw anywhere in the chain is logged and surfaces as
31
+ * `{ ok: false, error }`, so a caller may drop it on the floor without
32
+ * risking an unhandled rejection. A caller that answers a waiting client
33
+ * answers without awaiting this, and attaches its audit record to it instead
34
+ * — the record then carries the consequence's real outcome rather than a
35
+ * guess. A caller that needs the consequence finished before it continues
36
+ * (a test, a sequential sweep) awaits it.
37
+ *
38
+ * The consequence is where an approve was spending nine-plus seconds behind a
39
+ * ten-second relay budget, answering 504 on an approval that had already
40
+ * landed. It is not a stronger answer for having been awaited: the resume it
41
+ * runs is the same dispatch the original webhook delivery ran without any
42
+ * client waiting on it, `dispatchMatchedWorkflow` returning says only that
43
+ * jobs were routed rather than that any of them ran, and `resumeWorkflow`
44
+ * already reports its own failures on the run row and on the provider's check
45
+ * runs rather than through this return value.
46
+ */
47
+ consequence?: Promise<DecisionConsequenceResult>;
32
48
  }
33
49
  /** Dependencies injected into `applyDecision` (testable in isolation). */
34
50
  export interface ApplyDecisionDeps {
@@ -55,10 +71,42 @@ export interface ApplyDecisionDeps {
55
71
  */
56
72
  onStepReject?: (heldRunId: string, reason?: string) => Promise<void> | void;
57
73
  /**
58
- * Called when a workflow-scoped hold is rejected (install gate rejected)
59
- * cancel the run and drop the pending workflow context. Carries the runId.
74
+ * Called when a workflow-scoped hold is rejected cancel the run and drop
75
+ * the pending workflow context. Carries the whole hold row and the rejecter's
76
+ * own reason, which becomes the run's cancellation reason: the callers serve
77
+ * the install gate, the org trust policy's PR-wide hold and the SDK's
78
+ * workflow-level `requireApproval` alike, so a reason invented at the wiring
79
+ * site would misattribute one of them.
80
+ *
81
+ * The row is what tells those apart — only some of them posted a pending
82
+ * `KiCI Security` check, and only those have one to complete. Resolves to
83
+ * whether a terminal security check was WRITTEN, so this applier suppresses
84
+ * its own post only when one actually landed.
85
+ */
86
+ onWorkflowReject?: (hold: HeldRun, reason?: string) => Promise<boolean>;
87
+ /**
88
+ * Terminalize the `KiCI Security` check a just-ended hold posted, for every
89
+ * hold this applier ends that `onWorkflowReject` did not already report.
90
+ *
91
+ * Without it a job-scoped security hold answered from the dashboard strands
92
+ * its check forever: the row leaves `pending`, so the stale detector's
93
+ * approval-window sweep — which only ever sees pending rows — can no longer
94
+ * reach it. Optional so an orchestrator with no provider wiring degrades to
95
+ * flip-and-resume rather than failing the decision.
96
+ *
97
+ * `actorSub` is the deciding actor's opaque subject id, which is what this
98
+ * applier has and NOT something a contributor can read. Resolving it to a
99
+ * display name — or dropping the attribution when it resolves to none — is
100
+ * the wiring site's job, because only that site holds the org's identity
101
+ * directory. Omitted entirely where the summary must match one a delegate
102
+ * built without it; see the reject arm below.
60
103
  */
61
- onWorkflowReject?: (runId: string) => Promise<void>;
104
+ settleSecurityCheck?: (args: {
105
+ hold: HeldRun;
106
+ outcome: HoldOutcome;
107
+ actorSub?: string | undefined;
108
+ reason?: string | undefined;
109
+ }) => Promise<boolean>;
62
110
  }
63
111
  /** Apply a single approve/reject decision to a hold. */
64
112
  export declare function applyDecision(deps: ApplyDecisionDeps, args: {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The release wiring the local (`kici-admin`) held-run decision surface runs on.
3
+ *
4
+ * `createApp` composes this for every mode. It lives here rather than inline in
5
+ * that composition root because the defect it guards against is a MISSING
6
+ * field, not a wrong algorithm: a release path that flips the `held_runs` row
7
+ * without dispatching looks like it works and does nothing. A factory can be
8
+ * constructed by a test and its fields asserted; an object literal buried in
9
+ * `createApp` cannot.
10
+ *
11
+ * It returns `undefined` when there is no job-release callback, so an app
12
+ * assembled without one mounts no decision route at all rather than one whose
13
+ * approves are silent no-ops.
14
+ *
15
+ * Every callback reads the LIVE processing-deps bag at call time rather than
16
+ * capturing one: the provider registry is swapped whenever sources reload, and
17
+ * a captured bag would resolve a check poster from a registry that no longer
18
+ * serves the routing key.
19
+ */
20
+ import type { ReleaseSignal } from '../contexts/held-runs.js';
21
+ import type { ProcessingDeps } from '../pipeline/processor.js';
22
+ import type { HeldRunReleaseWiring } from '../routes/admin-held-runs.js';
23
+ /** The subset of `createApp`'s deps this factory reads. */
24
+ export interface HeldRunReleaseInputs {
25
+ /**
26
+ * Re-dispatch a released job-scoped hold. Supplied by both mode hooks through
27
+ * `appDepsExtras`; its absence is what makes the whole surface absent.
28
+ */
29
+ onJobRelease?: ((signal: ReleaseSignal) => Promise<void>) | undefined;
30
+ /** Replay the stored dispatch context of a released workflow-scoped hold. */
31
+ onWorkflowRelease?: ((signal: ReleaseSignal) => Promise<void>) | undefined;
32
+ /** The live processing-deps bag, assembled per call. */
33
+ buildProcessingDeps: () => ProcessingDeps;
34
+ }
35
+ /**
36
+ * The reason a workflow-scoped hold rejected through this surface records on the
37
+ * run it cancels, when the operator supplied none.
38
+ *
39
+ * Scope-neutral on purpose: the install gate, the org trust policy's PR-wide
40
+ * hold and an SDK workflow-level `requireApproval` all arrive here, so a
41
+ * reason naming one of them would mislabel the other two.
42
+ */
43
+ export declare const DEFAULT_LOCAL_REJECT_REASON = "Workflow hold rejected by the orchestrator operator";
44
+ export declare function buildHeldRunRelease(inputs: HeldRunReleaseInputs): HeldRunReleaseWiring | undefined;
45
+ //# sourceMappingURL=held-run-release-wiring.d.ts.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The approval subsystem an INDEPENDENT orchestrator runs on.
3
+ *
4
+ * `server.ts` composes this inline for the three Platform-attached modes. An
5
+ * independent orchestrator has no Platform and therefore none of that hook, so
6
+ * every gate that raises a hold was inert there: a fork PR the org policy said
7
+ * to HOLD was dropped instead of held, an SDK `requireApproval` job ran
8
+ * UNGATED (the dispatch site logged an error and dispatched anyway), and the
9
+ * stale detector had no store through which to expire an overdue hold,
10
+ * terminalize its `KiCI Security` check, or drop the pending dispatch context
11
+ * it would have replayed.
12
+ *
13
+ * It lives here rather than inside `standalone.ts` because that module is an
14
+ * entry point: it runs `guardStartup` at import time, so nothing can construct
15
+ * its wiring in a test. A factory can be constructed, and the fields it
16
+ * produces can be asserted — which is the whole point, since every defect this
17
+ * closes was a missing field rather than a wrong algorithm.
18
+ *
19
+ * What it deliberately does NOT produce is a `stepApprovalBridge`. A
20
+ * step-scoped hold is opened by the agent WS `onStepApproval` seam and answered
21
+ * by the dashboard applier, which is Platform-relayed and has no
22
+ * independent-mode equivalent — so wiring the bridge would let an agent open a
23
+ * hold nothing could resolve short of expiry.
24
+ */
25
+ import type { Kysely } from 'kysely';
26
+ import type { Database } from '../db/types.js';
27
+ import { HeldRunStore, type ReleaseSignal } from '../contexts/held-runs.js';
28
+ import { type ProcessingDeps, type ReadyDispatchContextRow } from '../pipeline/processor.js';
29
+ import type { Dispatcher } from '../agent/dispatcher.js';
30
+ import type { ExecutionTracker } from '../reporting/execution-tracker.js';
31
+ import type { RunCoordinator } from '../cluster/coordinator.js';
32
+ import type { InvokeGateDeps } from '../pipeline/invoke-gate.js';
33
+ /** The subsystem fields the composition reads. A subset of `OrchestratorSubsystems`. */
34
+ export interface IndependentApprovalSubsystems {
35
+ db: Kysely<Database>;
36
+ dispatcher: Dispatcher;
37
+ executionTracker?: ExecutionTracker | undefined;
38
+ coordinator?: RunCoordinator | undefined;
39
+ invokeGateDeps?: InvokeGateDeps | undefined;
40
+ /**
41
+ * The live direct-ingress deps bag. Read lazily inside the release callbacks:
42
+ * it is populated by `createApp`, which runs after the mode hook, and reading
43
+ * it at wiring time throws.
44
+ */
45
+ buildProcessingDeps: () => ProcessingDeps;
46
+ }
47
+ /** What the independent mode hook merges into `appDepsExtras`. */
48
+ export interface IndependentApprovalExtras {
49
+ heldRunStore: HeldRunStore;
50
+ onWorkflowRelease: (signal: ReleaseSignal) => Promise<void>;
51
+ onJobRelease: (signal: ReleaseSignal) => Promise<void>;
52
+ matchContext: (orgId: string, name: string) => Promise<ReadyDispatchContextRow | null>;
53
+ }
54
+ export declare function createIndependentApprovalExtras(sub: IndependentApprovalSubsystems): IndependentApprovalExtras;
55
+ //# sourceMappingURL=independent-wiring.d.ts.map
@@ -0,0 +1,9 @@
1
+ import type { ReleaseSignal } from '../contexts/held-runs.js';
2
+ /** The three resume paths a released hold can take. */
3
+ export interface ResumeHandlers {
4
+ onStepRelease?: (signal: ReleaseSignal) => Promise<void>;
5
+ onWorkflowRelease?: (signal: ReleaseSignal) => Promise<void>;
6
+ onJobRelease: (signal: ReleaseSignal) => Promise<void>;
7
+ }
8
+ export declare function routeRelease(signal: ReleaseSignal, handlers: ResumeHandlers): Promise<void>;
9
+ //# sourceMappingURL=resume-router.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The `service:` namespace both approving surfaces use for a non-human
3
+ * principal: `kici-admin`'s token identity (its only subject), and the
4
+ * dashboard's rendering of a `service_account` actor.
5
+ */
6
+ export declare const SERVICE_SUBJECT_PREFIX = "service:";
7
+ /** Render an admin-token user id in the shared `service:` namespace. */
8
+ export declare function adminActorSub(tokenUserId: string): string;
9
+ /**
10
+ * Resolve the subject of whoever triggered a run, in the approver vocabulary.
11
+ *
12
+ * | `triggered_by` | Returns | Why |
13
+ * |---|---|---|
14
+ * | `user:<sub>` | `<sub>` | what a `{user}` clause and both surfaces' own subject use |
15
+ * | `platform_operator:<sub>` | `<sub>` | same Keycloak subject as a `user` |
16
+ * | `service_account:<id>` | `service:<id>` | the namespace {@link adminActorSub} and the dashboard both produce |
17
+ * | `system:<component>` | `system:<component>` | the dashboard renders a system actor with its prefix intact |
18
+ * | `api_key:<keyId>` | `<keyId>` | see the residual below |
19
+ * | anything else | unchanged | no other actor type reaches these surfaces |
20
+ *
21
+ * The agent suffix is stripped first. `stringifyActor` renders a `user` or
22
+ * `api_key` who acted through an agent as `user:<sub> via agent:<label>`, and a
23
+ * reader splitting on the first colon gets `<sub> via agent:<label>` — which
24
+ * matches no live subject, so the gate was inert for every agent-mediated
25
+ * trigger. That is the `user` case, i.e. the common one.
26
+ *
27
+ * **Residual: `api_key`.** A key's persisted identifier is its `keyId`, while
28
+ * both surfaces render a live `api_key` actor as its owner's Keycloak subject
29
+ * (`ownerSub`). Those are different identifiers for the same principal and no
30
+ * string transform bridges them — closing it needs a keyId → ownerSub lookup,
31
+ * which is a store this pure function does not have. So an owner approving a
32
+ * hold on a run their own key triggered is still admitted. Recorded rather than
33
+ * papered over: returning the bare keyId is at least honest about what was
34
+ * stored.
35
+ */
36
+ export declare function triggererSubjectFor(triggeredBy: string | null | undefined): string | undefined;
37
+ //# sourceMappingURL=triggerer-subject.d.ts.map
@@ -1,14 +1,40 @@
1
1
  /**
2
2
  * Dependency-specific cache layer wrapping CacheStorage.
3
3
  *
4
- * Stores and retrieves dependency tarballs keyed by lockfileHash + platform + arch.
5
- * Shared CacheStorage backend with SourceCache (same S3 bucket).
6
- * Refreshes TTL on reads (touch-on-read).
4
+ * Stores dependency tarballs under their own content hash, with a small pointer
5
+ * resolving lockfileHash + platform + arch to that hash. Shared CacheStorage
6
+ * backend with SourceCache (same S3 bucket). Refreshes TTL on reads
7
+ * (touch-on-read).
7
8
  *
8
- * Cache key format: deps/{platform}-{arch}/{lockfileHash}.tar.gz
9
+ * Keys: `deps/{platform}-{arch}/{depsHash}.tar.gz` (immutable) and
10
+ * `deps/{platform}-{arch}/{lockfileHash}.hash` (the pointer).
9
11
  */
10
12
  import type { CacheStorage } from '../storage/types.js';
11
13
  import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
14
+ /**
15
+ * Cache key for a dependency tarball, addressed by the tarball's OWN content
16
+ * hash: `deps/{platform}-{arch}/{depsHash}.tar.gz`.
17
+ *
18
+ * Content-addressing is what makes a mismatched pair unrepresentable. When the
19
+ * tarball lived at a lockfile-derived name, it and its companion `.hash` were
20
+ * two independently-written objects under keys that any two builds sharing a
21
+ * lockfile + platform + arch also share — so concurrent builders could leave the
22
+ * `.tar.gz` from one and the `.hash` from the other, and the reader failed
23
+ * verification durably. Naming the object by its own hash means every pair a
24
+ * reader can observe is self-consistent, and the bytes at a given key can never
25
+ * change after a URL is signed for it.
26
+ */
27
+ export declare function depTarballKey(depsHash: string, platform: string, arch: string): string;
28
+ /**
29
+ * Cache key for the pointer that resolves a lockfile to the content hash of the
30
+ * tarball built from it: `deps/{platform}-{arch}/{lockfileHash}.hash`.
31
+ *
32
+ * This is the one mutable object in the scheme. A concurrent write replaces a
33
+ * pointer wholesale — there is no window in which it is half-written — so the
34
+ * worst a racing pair of builders can do is leave whichever pointer landed last,
35
+ * and both tarballs remain valid and fetchable.
36
+ */
37
+ export declare function depPointerKey(lockfileHash: string, platform: string, arch: string): string;
12
38
  export declare class DepCache {
13
39
  private readonly storage;
14
40
  private readonly maxTarballBytes;
@@ -30,7 +56,17 @@ export declare class DepCache {
30
56
  * `cache_ttl_days` change takes effect on the next lookup.
31
57
  */
32
58
  private resolveTtlMsOverride;
33
- /** Check if a dep tarball exists in cache. */
59
+ /**
60
+ * Resolve a lockfile to the content hash of the tarball built from it, or
61
+ * null when no pointer exists. The single place the indirection is read.
62
+ */
63
+ private resolvePointer;
64
+ /**
65
+ * Check if a dep tarball exists in cache.
66
+ *
67
+ * Both halves must be present: a pointer whose tarball has aged out is a miss,
68
+ * not a hit, or the caller skips a rebuild and dispatches a URL that 404s.
69
+ */
34
70
  has(lockfileHash: string, platform: string, arch: string): Promise<boolean>;
35
71
  /**
36
72
  * Get a pre-signed download URL for the dep tarball (for agent delivery).
@@ -38,9 +74,10 @@ export declare class DepCache {
38
74
  */
39
75
  getUrl(lockfileHash: string, platform: string, arch: string): Promise<string | null>;
40
76
  /**
41
- * Get a pre-signed download URL and the tarball content hash (if available).
42
- * Returns null on cache miss. Hash may be undefined for old entries stored
43
- * before integrity tracking was added.
77
+ * Get a pre-signed download URL and the tarball content hash.
78
+ *
79
+ * Returns null on cache miss — including an entry with no pointer, which is
80
+ * unverifiable and so is deliberately not served.
44
81
  */
45
82
  getUrlAndHash(lockfileHash: string, platform: string, arch: string): Promise<{
46
83
  url: string;
@@ -49,7 +86,14 @@ export declare class DepCache {
49
86
  /**
50
87
  * Get a pre-signed upload URL for direct agent-to-S3 upload.
51
88
  */
52
- getUploadUrl(lockfileHash: string, platform: string, arch: string): Promise<string>;
89
+ getUploadUrl(depsHash: string, platform: string, arch: string): Promise<string>;
90
+ /**
91
+ * Publish the pointer that makes an uploaded tarball discoverable by lockfile.
92
+ *
93
+ * Called only after the agent confirms its upload completed. Publishing before
94
+ * the bytes land would let a reader follow the pointer to a missing object.
95
+ */
96
+ publishPointer(lockfileHash: string, platform: string, arch: string, depsHash: string): Promise<void>;
53
97
  /**
54
98
  * Store a dep tarball in cache.
55
99
  * Throws if tarball exceeds max size (per user decision).
@@ -60,7 +104,7 @@ export declare class DepCache {
60
104
  * Used by build agents to compute the hash for depsHash protocol field.
61
105
  */
62
106
  static computeHash(data: Buffer): string;
63
- /** Remove a dep tarball from cache. */
107
+ /** Remove a dep tarball and its pointer from cache. */
64
108
  remove(lockfileHash: string, platform: string, arch: string): Promise<boolean>;
65
109
  }
66
110
  //# sourceMappingURL=dep-cache.d.ts.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * LRU for completed Tier-2 global eval round results.
3
+ *
4
+ * **This cache only helps webhook redelivery and same-input re-deliveries.** Its
5
+ * key pins the source commit and a digest of the whole round input, and every
6
+ * real push carries a new source SHA — so a developer pushing twice gets two
7
+ * rounds, by design. Read it as a duplicate-delivery guard, never as a
8
+ * steady-state optimization: sizing it up buys nothing, and a hit rate near zero
9
+ * on a healthy cluster is the expected shape, not a defect.
10
+ *
11
+ * There is deliberately no TTL. A round verdict is a pure function of its
12
+ * inputs, and {@link globalEvalRoundCacheKey} covers all of them, so an entry
13
+ * cannot go stale within its own key — the same content-addressable argument the
14
+ * lock-file and content caches make for their SHA-keyed entries, without their
15
+ * branch-name escape hatch.
16
+ */
17
+ import type { GlobalEvalRoundResult } from '@kici-dev/engine';
18
+ /**
19
+ * Key one round over **every input its verdict depends on**.
20
+ *
21
+ * The three SHAs are not enough, and the gap is a wrong-answer bug rather than a
22
+ * missed-hit one. `createFilterContext` and `buildGeneratorContext` both receive
23
+ * the whole event, so a filter can branch on `event.type`, `targetBranch`,
24
+ * `changedFiles`, or anything in the raw payload. A push to `main` at commit X
25
+ * and a pull-request synchronize whose head is commit X share all three SHAs and
26
+ * genuinely deserve different verdicts: their events differ, and their
27
+ * changed-file sets are computed over different ranges. Two branches pointing at
28
+ * one commit collide the same way. `workflowRoutingKey` is in the key for the
29
+ * reason `groupCandidates` keeps it in the group key — it selects the provider
30
+ * bundle that mints the clone credentials.
31
+ *
32
+ * `sourceRepoIdentifier` is its own component, placed next to `sourceSha` so the
33
+ * source repo and its commit stay adjacent. Every event already carries its
34
+ * source repo, so the event digest covers it too — but keying it explicitly
35
+ * makes that coverage structural rather than incidental: an event shape that
36
+ * stopped carrying the source repo could otherwise collide two repos' rounds.
37
+ *
38
+ * The candidate list and the event are folded in as a SHA-256 digest of their
39
+ * JSON, so the key stays a bounded string no matter how large a payload is. Both
40
+ * are rebuilt by the same code from the same delivery, so a genuine redelivery
41
+ * reproduces the digest; anything else is a miss, which costs a round rather
42
+ * than a wrong verdict.
43
+ *
44
+ * Returns `null` when the inputs cannot be serialized. A round that cannot be
45
+ * keyed is simply not cached — never keyed on a partial input, which is how a
46
+ * cache starts answering questions it was not asked.
47
+ */
48
+ export declare function globalEvalRoundCacheKey(args: {
49
+ workflowRepoIdentifier: string;
50
+ workflowSha: string;
51
+ workflowRoutingKey: string;
52
+ sourceRepoIdentifier: string;
53
+ sourceSha: string;
54
+ /** The exact per-candidate payload the round job carries. */
55
+ candidates: unknown;
56
+ /** The exact event the round job carries. */
57
+ event: unknown;
58
+ }): string | null;
59
+ /**
60
+ * A round that left ANY candidate undecided is not a result worth replaying.
61
+ *
62
+ * An agent-side budget breach reports `success` with the affected candidates
63
+ * marked indeterminate — the whole round on a round-budget breach, one workflow
64
+ * on a candidate-budget breach. Caching either turns one slow round into a
65
+ * permanently replayed failure for the whole key: a webhook redelivery, which
66
+ * is exactly when an operator is retrying, would be served the stored failure
67
+ * instead of running again, and the round's own retry sits behind this cache
68
+ * read so a stored breach short-circuits that too.
69
+ *
70
+ * Requiring EVERY candidate to be decided costs almost no hit rate — an
71
+ * indeterminate verdict is the exceptional path — and buys a guarantee that is
72
+ * easy to state: nothing a redelivery replays was ever undecided.
73
+ *
74
+ * An empty candidate list is not cacheable either. For a non-empty group it
75
+ * means every candidate is about to be recorded as "no verdict", which is the
76
+ * same failure by another route.
77
+ */
78
+ export declare function isCacheableRoundResult(result: GlobalEvalRoundResult): boolean;
79
+ export declare class GlobalEvalRoundCache {
80
+ private readonly cache;
81
+ private hits;
82
+ private misses;
83
+ constructor(options: {
84
+ max: number;
85
+ });
86
+ get(key: string): GlobalEvalRoundResult | undefined;
87
+ set(key: string, value: GlobalEvalRoundResult): void;
88
+ /** Hit/miss counters for metrics and debugging. */
89
+ stats(): {
90
+ hits: number;
91
+ misses: number;
92
+ size: number;
93
+ };
94
+ }
95
+ //# sourceMappingURL=global-eval-round-cache.d.ts.map
@@ -15,4 +15,7 @@ export { PendingBuildTracker } from './pending-builds.js';
15
15
  export { PendingInitTracker } from './pending-inits.js';
16
16
  export type { InitResult } from './pending-inits.js';
17
17
  export { PendingDynamicTracker } from './pending-dynamics.js';
18
+ export { PendingGlobalEvalTracker } from './pending-global-evals.js';
19
+ export { GlobalEvalRoundCache, globalEvalRoundCacheKey, isCacheableRoundResult, } from './global-eval-round-cache.js';
20
+ export { parseGlobalEvalResult } from './pending-global-evals.js';
18
21
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pending tracker for the Tier-2 global eval round.
3
+ *
4
+ * The orchestrator dispatches one round job per (event × workflow repo) and
5
+ * waits for the agent to return a verdict per candidate workflow. The
6
+ * underlying tracker logic lives in `PendingTracker<GlobalEvalRoundResult>`;
7
+ * this subclass wires the round-specific logger prefix and disconnect error.
8
+ *
9
+ * Unlike the dynamic-eval and init trackers, this round runs BEFORE any run row
10
+ * exists: its whole purpose is to decide which global workflows produce a run at
11
+ * all, so creating one up-front would defeat it.
12
+ */
13
+ import { type GlobalEvalRoundResult } from '@kici-dev/engine';
14
+ import { PendingTracker } from './pending-tracker.js';
15
+ /**
16
+ * Validate the round result carried on a `job.status` message before it reaches
17
+ * the tracker.
18
+ *
19
+ * **This is the one place the round's wire payload is validated.** The status
20
+ * message's `data` field is `z.record(z.string(), z.unknown())`, so nothing
21
+ * upstream checks `globalEvalResult` against its schema — a cast at the message
22
+ * handler would be a type assertion over an arbitrary agent-supplied value, and
23
+ * any agent that owns the round job can send one. Parsing here keeps the
24
+ * "typed but unvalidated" boundary at a single named function instead of spread
25
+ * across every consumer.
26
+ *
27
+ * The parsed value is widened back to {@link GlobalEvalRoundResult}: the schema
28
+ * is deliberately loose about `jobs` (the lock-job shape has no Zod mirror), and
29
+ * the dispatch path re-checks every generated job before it builds anything from
30
+ * one.
31
+ */
32
+ export declare function parseGlobalEvalResult(raw: unknown): {
33
+ ok: true;
34
+ value: GlobalEvalRoundResult;
35
+ } | {
36
+ ok: false;
37
+ error: string;
38
+ };
39
+ export declare class PendingGlobalEvalTracker extends PendingTracker<GlobalEvalRoundResult> {
40
+ constructor();
41
+ }
42
+ //# sourceMappingURL=pending-global-evals.d.ts.map
@@ -18,6 +18,16 @@ export interface InitResult {
18
18
  * function. The dispatch path re-materializes these into N execution jobs.
19
19
  */
20
20
  matrixValues?: Array<Record<string, string | undefined>>;
21
+ /**
22
+ * Verdict of the workflow-level `filter`, reported only when the init job was
23
+ * asked to evaluate one. `false` means the workflow does not apply to this
24
+ * event and its job must not be dispatched.
25
+ *
26
+ * Optional on purpose: an agent that predates the filter never sends it, so
27
+ * absence means "no verdict was reported" and dispatch proceeds — only an
28
+ * explicit `false` suppresses.
29
+ */
30
+ filterPassed?: boolean;
21
31
  }
22
32
  export declare class PendingInitTracker extends PendingTracker<InitResult> {
23
33
  constructor();
@@ -84,6 +84,24 @@ export interface GenericSourceResponse {
84
84
  * print an empty one.
85
85
  */
86
86
  export declare function firstCauseMessage(err: unknown): string;
87
+ /**
88
+ * True when a rejected `fetch` failed at the transport layer, as opposed to
89
+ * failing while the request was still being built.
90
+ *
91
+ * `fetch` uses a `TypeError` for both, so they are easy to conflate — but only
92
+ * a transport failure means the orchestrator was actually dialled. Measured
93
+ * against Node 24's undici:
94
+ *
95
+ * closed port → TypeError('fetch failed') cause: Error(ECONNREFUSED)
96
+ * invalid header → TypeError('Headers.append: … invalid header …') cause: undefined
97
+ * invalid URL → TypeError('Failed to parse URL from …') cause: Error(ERR_INVALID_URL)
98
+ *
99
+ * The discriminator is therefore the message, NOT the presence of a `cause`:
100
+ * the invalid-URL case carries one. undici uses exactly `fetch failed` for a
101
+ * transport failure and a descriptive message for everything it rejects before
102
+ * opening a connection.
103
+ */
104
+ export declare function isTransportFailure(err: unknown): boolean;
87
105
  export declare function fetchAdminApi(url: string, init: RequestInit, baseUrl: string): Promise<Response>;
88
106
  export declare class AdminApiClient {
89
107
  private readonly baseUrl;
@@ -25,14 +25,63 @@ interface StringKnobSpec {
25
25
  expects: string;
26
26
  validate: (value: string) => boolean;
27
27
  }
28
+ /**
29
+ * One boolean knob. A third spec type for the same reason {@link StringKnobSpec}
30
+ * is separate from {@link KnobSpec}: the value parser, the PATCH body type, and
31
+ * the `show` formatter all differ. A boolean also has no bounds to validate —
32
+ * only two accepted spellings.
33
+ */
34
+ interface BooleanKnobSpec {
35
+ field: string;
36
+ flag: string;
37
+ label: string;
38
+ }
28
39
  /** camelCase → value (null clears the override). */
29
- type PatchBody = Record<string, number | string | null>;
30
- /** Parse an integer CLI flag with a minimum, exiting with an error on failure. */
31
- export declare function parseKnobValue(flag: string, value: string, min: number): number;
40
+ type PatchBody = Record<string, number | string | boolean | null>;
41
+ /**
42
+ * Parse an integer CLI flag against its knob's bounds, exiting on failure.
43
+ *
44
+ * `max` is optional because most knobs have no meaningful ceiling; the ones
45
+ * that do carry a hard boot-safety bound (see {@link KnobSpec.max}).
46
+ */
47
+ export declare function parseKnobValue(flag: string, value: string, min: number, max?: number): number;
32
48
  /** Validate a text CLI flag against its knob's shape, exiting on failure. */
33
49
  export declare function parseStringKnobValue(knob: StringKnobSpec, value: string): string;
50
+ /**
51
+ * Parse a boolean CLI flag, exiting on failure.
52
+ *
53
+ * Accepts only the exact strings `true` and `false`. A permissive parser (any
54
+ * non-empty string is true) would read `--global-workflows-enabled no` as an
55
+ * enable, which for this knob turns a typo into a security posture change.
56
+ */
57
+ export declare function parseBooleanKnobValue(knob: BooleanKnobSpec, value: string): boolean;
34
58
  /** Assemble the PATCH body from provided `set` flags (exits if none given). */
35
59
  export declare function buildClusterPatch(opts: Record<string, string | undefined>): PatchBody;
60
+ /**
61
+ * Warn when only one of the two ordered global-eval timeouts is being set.
62
+ *
63
+ * The orchestrator's wait ceiling must exceed the agent's round budget — the
64
+ * agent's budget starts only once the round job is RUNNING, so a lower ceiling
65
+ * fires on every round that merely waited for a free agent, failing every round
66
+ * silently. The server rejects the pair outright when BOTH effective values are
67
+ * stored, but it cannot compare against a column left NULL: NULL means "the
68
+ * orchestrator's configured default applies", and the route does not know that
69
+ * number. Setting one alone is exactly that blind spot, so warn here.
70
+ *
71
+ * Returns the lines rather than printing them so the check is unit-testable.
72
+ */
73
+ export declare function unpairedEvalTimeoutWarnings(patch: PatchBody): string[];
74
+ /**
75
+ * Warn for every deprecated knob the patch sets.
76
+ *
77
+ * The set still goes through — the column and the route field are a released
78
+ * operator surface and keep accepting writes — so the warning says what the
79
+ * stored value now does, which is nothing.
80
+ *
81
+ * Returns the lines rather than printing them so the check is unit-testable,
82
+ * matching {@link unpairedEvalTimeoutWarnings}.
83
+ */
84
+ export declare function deprecatedKnobWarnings(patch: PatchBody): string[];
36
85
  /** Build the reset PATCH body: all knobs → null, or just the flagged ones. */
37
86
  export declare function buildClusterReset(opts: Record<string, boolean | undefined>): PatchBody;
38
87
  /**
@@ -11,5 +11,11 @@
11
11
  */
12
12
  import type { Command } from 'commander';
13
13
  import type { AdminApiClient } from '../api-client.js';
14
+ /**
15
+ * Width of the table's Message column. A check message is cut at this many
16
+ * characters, so anything a check wants an operator to see in the default
17
+ * (non-`--json`) output has to fit inside it.
18
+ */
19
+ export declare const DIAGNOSE_TABLE_MESSAGE_WIDTH = 42;
14
20
  export declare function registerDiagnoseCommand(program: Command, getClient: () => AdminApiClient): void;
15
21
  //# sourceMappingURL=diagnose.d.ts.map