@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
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Event scaler backend — workflow-driven autoscaling.
3
+ *
4
+ * Unlike the local backends (container / bare-metal / firecracker), this backend
5
+ * performs NO local compute. Its `spawn()` emits a `kici.scaler.scale-up` event
6
+ * and its `destroy()` emits a `kici.scaler.scale-down` event; a customer-authored
7
+ * provisioning / teardown workflow (subscribed via `kiciEvent()`) consumes those
8
+ * events to boot and tear down an ephemeral cloud instance. The provisioned
9
+ * instance's agent registers back with the scaler-chosen `agentId` and the
10
+ * existing bound-job dispatch runs the pending job — reusing the whole scaling
11
+ * engine (demand detection, caps, reservations, spawn timeout, teardown).
12
+ */
13
+ import type { EffectiveLimits, LabelSetConfig, ManagedAgent, ScalerBackend, ScalerDestroyContext, ScalerEntry, ScalerEventCallback, SpawnContext, ValidationResult } from './types.js';
14
+ import type { ClaimStore } from './claim-store.js';
15
+ import type { ScalerScaleUpPayload, ScalerScaleDownPayload } from './scaler-events.js';
16
+ /**
17
+ * Subset of `EventEmitter` the backend needs. Injected so the backend is unit
18
+ * testable without a real event router.
19
+ */
20
+ export interface ScalerEventEmitterLike {
21
+ emitScalerScaleUp(payload: ScalerScaleUpPayload, targets: string[]): Promise<string>;
22
+ emitScalerScaleDown(payload: ScalerScaleDownPayload, targets: string[]): Promise<string>;
23
+ }
24
+ export interface EventScalerBackendOptions {
25
+ /** The parsed scaler entry (`type: 'event'`). */
26
+ entry: ScalerEntry;
27
+ /** Emits the reserved scale-up / scale-down events. */
28
+ emitter: ScalerEventEmitterLike;
29
+ /** Mints ephemeral credentials when a claim code is redeemed — normally by
30
+ * the provisioned agent self-bootstrapping, or by a provisioning workflow. */
31
+ claimStore: ClaimStore;
32
+ /** Correlation-id generator for scale-up / scale-down requests. */
33
+ requestId: () => string;
34
+ }
35
+ export declare class EventScalerBackend implements ScalerBackend {
36
+ readonly type: "event";
37
+ readonly spawnsOnLocalHost = false;
38
+ readonly logsSource = "event";
39
+ labelSets: LabelSetConfig[];
40
+ maxAgents: number;
41
+ /** Exposed so the composition root can wire `onClaimCredentials` to it. */
42
+ readonly claimStore: ClaimStore;
43
+ private entry;
44
+ private readonly emitter;
45
+ private readonly requestId;
46
+ private readonly agents;
47
+ constructor(opts: EventScalerBackendOptions);
48
+ /** The entry this backend is serving; the reload rollback restores it. */
49
+ get currentEntry(): ScalerEntry;
50
+ getActiveCount(): number;
51
+ /**
52
+ * Register a pending claim, emit a scale-up event to the provisioning targets,
53
+ * and track the agent as provisioning. Performs no local compute.
54
+ */
55
+ spawn(labelSet: string[], agentId: string, orchestratorUrl: string, _onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, signal?: AbortSignal): Promise<ManagedAgent>;
56
+ /**
57
+ * Drop the tracked agent, emit a scale-down event (carrying the teardown
58
+ * reason), then invalidate the pending claim. Idempotent: a repeat destroy of
59
+ * an already-torn-down agent is a no-op (no duplicate scale-down).
60
+ *
61
+ * The tracking entry is dropped before the first `await` so a concurrent
62
+ * destroy of the same agent returns at the guard above and cannot emit a
63
+ * second scale-down.
64
+ *
65
+ * The claim invalidation is last and fails open. It is a DB delete, and the
66
+ * scale-down is the only thing that tears down the customer's cloud instance:
67
+ * running the delete first lets a DB error suppress the teardown for good,
68
+ * because the agent is already out of `this.agents` and a retried destroy
69
+ * returns at the guard. A claim left behind instead is inert — the row is
70
+ * single-use and TTL-bounded, so it can mint at most one token for an agent
71
+ * id that is being torn down.
72
+ */
73
+ destroy(managedId: string, context?: ScalerDestroyContext): Promise<void>;
74
+ /**
75
+ * Drop the tracked agent without emitting anything.
76
+ *
77
+ * The counterpart to `adopt()` on the other side of a cross-instance
78
+ * registration: the spawning coordinator is never told that a peer adopted
79
+ * its provision, so its entry would otherwise live until the process exits —
80
+ * inflating `getActiveCount()` against the global cap forever, and handing
81
+ * `shutdownAll()` a peer's live agent to tear down. Forgetting says "this
82
+ * coordinator no longer tracks the provision", which is exactly true and says
83
+ * nothing about whether the customer's instance is still running; the adopter
84
+ * emits the teardown, and the leader-gated reaper is the backstop.
85
+ */
86
+ forget(agentId: string): void;
87
+ /**
88
+ * Take over bookkeeping for an agent another instance spawned. Seeds the same
89
+ * entry `spawn()` would have, so `destroy()` emits its teardown instead of
90
+ * no-opping on an unknown id.
91
+ */
92
+ adopt(agentId: string, labelSet: string[]): void;
93
+ /** Mark a provisioned agent active once its instance registers over WS. */
94
+ markActive(agentId: string): void;
95
+ /** Delegate a credential claim to the backend's claim store. */
96
+ claim(code: string): Promise<import("./claim-store.js").ClaimedCredentials>;
97
+ /**
98
+ * Emits nothing. An event provision is a customer cloud instance whose
99
+ * lifetime spans the cluster, not this process: the coordinator holding the
100
+ * agent emits its teardown from the WS-close path, and the leader-gated
101
+ * reaper is the backstop for one that reaches nobody. This map also carries
102
+ * provisions a peer has already adopted — the spawning coordinator is never
103
+ * told — so emitting here would make a routine restart tear down a peer's
104
+ * running agents.
105
+ *
106
+ * The map is deliberately left intact rather than cleared: a WS close landing
107
+ * during the remaining shutdown steps then still emits its own teardown.
108
+ */
109
+ shutdownAll(): Promise<void>;
110
+ /**
111
+ * Apply the new config. The backend reads `roles`, `mandatoryLabels`,
112
+ * `agentTokenTtlSeconds` and `provisioningTargets` off its entry at every
113
+ * spawn, so the entry is replaced here — otherwise a reload that retargets
114
+ * the provisioning workflow, or changes the scaler's roles, would keep
115
+ * emitting scale-up events to the old workflow refs and minting agent labels
116
+ * from the old roles, while the manager's own routing gate used the new ones.
117
+ */
118
+ reload(labelSets: LabelSetConfig[], opts?: {
119
+ maxAgents?: number;
120
+ entry?: ScalerEntry;
121
+ }): ValidationResult;
122
+ private provisioningTargets;
123
+ }
124
+ //# sourceMappingURL=event-backend.d.ts.map
@@ -0,0 +1,350 @@
1
+ import { ScaleDownReason } from './scaler-events.js';
2
+ import type { ReapCandidate } from './scaler-state-store.js';
3
+ /**
4
+ * How long a provision outcome is kept once its spawn row is gone.
5
+ *
6
+ * Deliberately a constant rather than a `cluster_settings` knob: unlike the
7
+ * claim retention beside it, there is no operational decision here. The only
8
+ * reader is the stale-spawn prune, and the interval the row must survive is
9
+ * derived from windows the reaper already resolves — so a knob would be surface
10
+ * with nothing behind it. The purge floors this against twice the spawn
11
+ * deadline, so raising `KICI_SCALER_SPAWN_TIMEOUT_MS` for a slow cloud widens
12
+ * the retention with it.
13
+ */
14
+ export declare const PROVISION_OUTCOME_RETENTION_MS = 86400000;
15
+ /**
16
+ * The live values one sweep runs against. Read together at the top of each tick
17
+ * so a sweep is internally consistent even if an operator changes a knob
18
+ * halfway through.
19
+ */
20
+ export interface ReaperWindows {
21
+ /** How often the leader sweeps. A change reschedules the timer after this tick. */
22
+ intervalMs: number;
23
+ /** How long an unadopted provision may stay unregistered before teardown. */
24
+ spawnTimeoutMs: number;
25
+ /** How long an adopted provision may stay unseen before teardown. */
26
+ strandedTimeoutMs: number;
27
+ /**
28
+ * How long a peer disconnection, or an agent's absence, must persist before
29
+ * it counts as real rather than as a reconnect in progress. Shared with the
30
+ * rerouted-job guard (`reroute_flap_grace_ms`) because it answers the same
31
+ * question for the same reason.
32
+ */
33
+ flapGraceMs: number;
34
+ /** How long before a candidate whose teardown did not clear it is retried. */
35
+ reattemptIntervalMs: number;
36
+ /** How long an expired provisioning claim is kept before it is purged. */
37
+ claimRetentionMs: number;
38
+ }
39
+ /**
40
+ * The flap grace the reaper actually runs on.
41
+ *
42
+ * `reroute_flap_grace_ms` is shared with the rerouted-job guard, and the two
43
+ * consumers do not carry the same risk: the reroute guard force-fails a run it
44
+ * could have waited for, while this one deletes a customer's running instance.
45
+ * An operator lowering the knob for reroute reasons must not silently collapse
46
+ * that protection to two consecutive sweeps.
47
+ *
48
+ * The floor is two peer-stale-timeout windows — the very derivation the knob's
49
+ * own default (`DEFAULT_REROUTE_FLAP_GRACE_MS`) comes from, applied as a floor
50
+ * rather than a default. It has to cover the worst honest gap between an agent
51
+ * being alive and being visible here: an agent reconnect backing off to its cap
52
+ * (60s), plus the peer heartbeat interval (30s) during which a freshly
53
+ * reconnected peer advertises no agents at all. Two stale windows (120s at the
54
+ * defaults) covers that 90s; one (60s) would not.
55
+ *
56
+ * A raised knob is honoured as-is: the floor only ever raises.
57
+ */
58
+ export declare function reaperFlapGraceMs(flapGraceMs: number, peerStaleTimeoutMs: number): number;
59
+ /** The slice of the peer registry the cluster-view guard reads. */
60
+ export interface ReaperPeerView {
61
+ /** Every coordinator peer this instance knows, connected or not. */
62
+ getCoordinatorPeers(): readonly unknown[];
63
+ /** How many of those are connected right now. */
64
+ getConnectedCoordinatorPeerCount(): number;
65
+ }
66
+ /**
67
+ * The reaper's `canReap` predicate, over static config plus the live peer
68
+ * registry.
69
+ *
70
+ * Extracted and exported so the wiring itself is testable. The bug this closes
71
+ * lived in exactly this expression — `clusterViewSufficient` was unit-tested
72
+ * throughout, while the call site that fed it the wrong count was not, so the
73
+ * guard read permissive on a Platform-mode HA pair through two review rounds.
74
+ * A component whose false positive deletes a customer's running instances
75
+ * cannot have an untested load-bearing line.
76
+ *
77
+ * The peer count is the max of the two sources because neither alone covers
78
+ * both modes: `KICI_CLUSTER_PEERS` is set only in independent mode, while a
79
+ * Platform-mode cluster learns its peers from the matchmaker and leaves that
80
+ * array empty. `getCoordinatorPeers()` filters on role, not on connectivity, so
81
+ * a peer lost to a partition stays counted and the guard stays armed.
82
+ */
83
+ export declare function canReapForCluster(configuredPeerCount: number, peers: ReaperPeerView): boolean;
84
+ /**
85
+ * Whether an adopting coordinator should still be treated as live.
86
+ *
87
+ * Mirrors `shouldDeferReroutedJob`, and for the same reason: `PeerRegistry`
88
+ * flips `connected` the instant a peer WS closes, with no grace of its own, so
89
+ * sampling it alone cannot tell a link blip between two healthy processes from
90
+ * a dead coordinator — while the agent is still attached to that peer running a
91
+ * customer job. A peer we have never heard of is genuinely gone.
92
+ */
93
+ export declare function adopterIsLive(peer: {
94
+ connected: boolean;
95
+ lastHeartbeatAt: number;
96
+ } | undefined, nowMs: number, flapGraceMs: number): boolean;
97
+ /**
98
+ * Whether this coordinator sees enough of its cluster to tear down provisions
99
+ * on the cluster's behalf.
100
+ *
101
+ * Raft self-elects a coordinator that has zero connected coordinator peers, so
102
+ * both halves of a partitioned pair become leader on their own side. Each then
103
+ * reads every agent the other side holds as registered nowhere, and roughly one
104
+ * flap grace later both emit teardowns for the other's live instances —
105
+ * symmetric destruction of a running fleet. Standing down costs a delayed
106
+ * backstop; not standing down costs the customer their fleet.
107
+ *
108
+ * `knownCoordinatorPeerCount` must count peers this coordinator has *ever*
109
+ * handshaken with, not the ones named in static config. `KICI_CLUSTER_PEERS` is
110
+ * set only in multi-orchestrator independent mode: the documented HA-pair recipe
111
+ * runs Platform mode and joins by token, so peers arrive through the Platform
112
+ * matchmaker and the static array stays empty. Keying on it left the guard inert
113
+ * on precisely the topology it exists to protect. A known peer survives a
114
+ * partition because `evictStalePeers` only marks it disconnected, so the guard
115
+ * stays armed for as long as the partition lasts.
116
+ *
117
+ * A coordinator that has never met a peer is a single-coordinator deployment,
118
+ * not a partitioned one, so it is never blocked.
119
+ */
120
+ export declare function clusterViewSufficient(knownCoordinatorPeerCount: number, connectedCoordinatorPeerCount: number): boolean;
121
+ export interface EventProvisionReaperOptions {
122
+ /** Event rows that are either adopted, or past the spawn deadline unadopted. */
123
+ listCandidates: (spawnCutoff: Date) => Promise<ReapCandidate[]>;
124
+ /**
125
+ * Whether an instance id is still a live cluster member (self counts as live).
126
+ * `flapGraceMs` is passed in so the implementation can treat a peer that
127
+ * disconnected moments ago as live — `peer.connected` alone flips on WS close
128
+ * with no grace at all.
129
+ */
130
+ isPeerLive: (instanceId: string, flapGraceMs: number) => boolean;
131
+ /** Whether an agent id is registered anywhere in the cluster we can see. */
132
+ isAgentRegistered: (agentId: string) => boolean;
133
+ /**
134
+ * Whether this coordinator can see enough of the cluster to act on its
135
+ * behalf. False on a node that *knows* coordinator peers but is connected to
136
+ * none of them, which is exactly the shape a network partition produces.
137
+ *
138
+ * "Knows" spans both modes: peers named in static config, plus peers this
139
+ * instance has handshaken with. Keying on static config alone is what left
140
+ * this guard inert on a Platform-mode HA pair, which configures none.
141
+ */
142
+ canReap: () => boolean;
143
+ /** Emit the teardown and drop the row. */
144
+ emitScaleDown: (candidate: ReapCandidate, reason: ScaleDownReason) => Promise<void>;
145
+ /** Delete pending claims that expired before the cutoff; returns rows deleted. */
146
+ purgeExpiredClaims: (cutoff: Date) => Promise<number>;
147
+ /**
148
+ * Delete provision outcomes older than the cutoff whose spawn row is gone;
149
+ * returns rows deleted. Runs on the same leader-gated sweep as the claim
150
+ * purge, which is the orchestrator's one precedent for a timer-driven purge
151
+ * of scaler bookkeeping.
152
+ */
153
+ purgeProvisionOutcomes: (cutoff: Date) => Promise<number>;
154
+ /**
155
+ * Read the live windows for one sweep. Backed by `cluster_settings`, so an
156
+ * operator retunes the reaper on a running cluster; each read falls back to
157
+ * the orchestrator's configured default when the column is NULL.
158
+ */
159
+ resolveWindows: () => Promise<ReaperWindows>;
160
+ /**
161
+ * Interval used for the very first schedule, before any knob has been read.
162
+ * `onBecomeLeader` is synchronous (it is a Raft callback), so the timer starts
163
+ * at the configured default and the first tick reconciles it to the live value.
164
+ */
165
+ bootIntervalMs: number;
166
+ }
167
+ /**
168
+ * Leader-gated teardown backstop for event-scaler provisions.
169
+ *
170
+ * An event scaler emits a scale-up and then has no further local handle on the
171
+ * instance a customer workflow booted. `spawn()` returns as soon as the event
172
+ * is emitted, so the spawn-call deadline says nothing about whether the VM ever
173
+ * appeared, and nothing else tears the provision down: an agent that never
174
+ * registers — a cloud API 500, a cancelled run, a denied quota — leaves a
175
+ * permanent row holding a `maxAgents` slot and no `kici.scaler.scale-down` is
176
+ * ever emitted. Two more shapes strand a provision on a cluster: the spawning
177
+ * coordinator dies before the agent registers, and the adopting coordinator
178
+ * dies mid-job.
179
+ *
180
+ * The same pass purges pending provisioning claims past their expiry. A claim
181
+ * row outlives its agent whenever a registration unwinds after the claim was
182
+ * written, or whenever the coordinator that would have invalidated it crashed
183
+ * first, and nothing else deletes one on a timer.
184
+ *
185
+ * Every teardown deletes a customer's cloud instance, so the whole design is
186
+ * biased towards leaving a doubtful row alone:
187
+ *
188
+ * - **Nothing is ever condemned on one sample.** "The agent is registered
189
+ * nowhere" is an observation that flips false for a live agent on routine
190
+ * events — a local WS reconnect, or the ~30s after any peer reconnect during
191
+ * which that peer's advertised agent list is empty. So the reaper times how
192
+ * long the observation has *persisted* (leader-local, never a database
193
+ * column a re-adopt could refresh) and requires it to hold for at least the
194
+ * flap grace before any arm can fire.
195
+ * - **A peer disconnection gets the same grace.** `peer.connected` flips on WS
196
+ * close with no grace of its own, so the adopter-liveness question is asked
197
+ * the way `shouldDeferReroutedJob` asks it: connected, or last heard from
198
+ * inside the flap window.
199
+ * - **A node that cannot see its cluster does not act for it.** A coordinator
200
+ * isolated by a partition self-elects after ~60s with zero connected peers;
201
+ * without a guard its first sweep would read every adopter as dead and
202
+ * condemn every adopted provision in the cluster, including the ones the
203
+ * majority side is still running. The guard counts *known* peers — static
204
+ * config plus everyone handshaken with — never static config alone.
205
+ *
206
+ * Modelled on `PendingScaleSweeper` / `HostRosterReaper`: one timer, started and
207
+ * stopped by the Raft leadership callbacks, so several coordinators never each
208
+ * tear the same provision down. Unlike those two, every window it runs on is a
209
+ * live `cluster_settings` knob re-read per sweep — including the interval, which
210
+ * reschedules the timer at the end of the sweep that observed the change.
211
+ */
212
+ export declare class EventProvisionReaper {
213
+ private readonly opts;
214
+ private timer;
215
+ private isLeader;
216
+ /** The interval the live timer was created with, so a change is detectable. */
217
+ private scheduledIntervalMs;
218
+ /** Whether the previous sweep was blocked, so the log fires on the edge only. */
219
+ private reapBlocked;
220
+ /**
221
+ * agentId → when this reaper last emitted a teardown for it. A candidate that
222
+ * comes back means the emit did not clear the row — most often because
223
+ * neither the row nor the leader's config names a provisioning target, which
224
+ * `ScalerManager.emitScaleDownForSpec` deliberately answers by keeping the
225
+ * row and logging. Without this throttle that row would be re-emitted and
226
+ * re-logged at error level on every single tick, forever.
227
+ */
228
+ private readonly lastAttemptAt;
229
+ /**
230
+ * agentId → when this reaper first observed the agent registered nowhere.
231
+ *
232
+ * This is the debounce that makes "unseen" mean sustained absence rather than
233
+ * one unlucky sample. It is leader-local observation, deliberately not a
234
+ * database column: `adopted_at` is refreshed by a coordinator re-adopting its
235
+ * own agent, so a restart loop on the adopter would keep resetting the clock
236
+ * on its own stranded provision.
237
+ */
238
+ private readonly strandedSince;
239
+ /**
240
+ * How many entries of `strandedSince` the last sweep found absent for at
241
+ * least the flap grace.
242
+ *
243
+ * The gauge publishes this rather than `strandedSince.size`, because the two
244
+ * answer different questions. `size` counts every agent currently observed
245
+ * absent, and an agent transits that set on every ordinary reconnect — a 30s
246
+ * peer heartbeat against a 60s sweep, and `PeerRegistry` emptying a peer's
247
+ * agent list on any disconnect, mean one flap enrols every peer-adopted agent
248
+ * at once. So `size` is routinely non-zero on a healthy cluster, while
249
+ * `verdict()` refuses to act on anything below the flap grace. Publishing the
250
+ * past-grace count makes the gauge measure the same thing the reaper does.
251
+ */
252
+ private unseenPastGrace;
253
+ /**
254
+ * agentIds already reported as having an unknown owner, so the warning fires
255
+ * once per row rather than once per sweep. Pruned with the other per-agent
256
+ * maps when the row leaves the candidate set.
257
+ */
258
+ private readonly unknownOwnerLogged;
259
+ constructor(opts: EventProvisionReaperOptions);
260
+ onBecomeLeader(): void;
261
+ onLoseLeadership(): void;
262
+ stop(): void;
263
+ /**
264
+ * The interval the timer currently runs at, or null when no timer is armed.
265
+ * Public so a caller can observe that a live knob change actually took.
266
+ */
267
+ currentIntervalMs(): number | null;
268
+ /** One sweep pass. Public for tests. Never throws (logs and keeps ticking). */
269
+ tick(): Promise<void>;
270
+ /** Tear down every candidate the verdict condemns. */
271
+ private reapProvisions;
272
+ /** The candidate half of one sweep, once the blocked check has passed. */
273
+ private sweepCandidates;
274
+ /** How many absence clocks have run for at least the flap grace. */
275
+ private countUnseenPastGrace;
276
+ /** Emit a teardown for every candidate the verdict condemns. */
277
+ private condemnCandidates;
278
+ /**
279
+ * Publish what this sweep observed.
280
+ *
281
+ * The unseen count is the only outward sign of the one failure this design
282
+ * cannot fix on its own: the absence clock is leader-local, so a cluster with
283
+ * enough leader churn — or a peer link flapping faster than the stranded
284
+ * window — restarts it before it can ever expire, and a genuinely stranded
285
+ * provision bills forever with no log line to show for it.
286
+ */
287
+ private publishGauges;
288
+ /** Delete pending claims past their expiry plus the retention grace. */
289
+ private purgeClaims;
290
+ /**
291
+ * Delete provision outcomes nothing can ask about any more.
292
+ *
293
+ * The retention is floored against twice the spawn deadline because that
294
+ * deadline also floors the stale-spawn prune window: recovery rehydrates a
295
+ * spawning entry from the spawn row, and that entry asks about its provision
296
+ * one prune later. The store's own `NOT EXISTS` predicate is what makes the
297
+ * purge safe while the spawn row is still there; this floor covers the window
298
+ * after it goes.
299
+ *
300
+ * `spawnTimeoutMs` needs no finiteness guard the way `reconcileInterval`'s
301
+ * period does: `config.scalerSpawnTimeoutMs` is parsed as an integer of at
302
+ * least 1000, and this is the one window `resolveWindows` takes straight from
303
+ * config rather than from a `cluster_settings` column.
304
+ */
305
+ private purgeOutcomes;
306
+ /**
307
+ * Adopt a changed sweep interval by rebuilding the timer.
308
+ *
309
+ * Re-read every sweep rather than only at a leadership transition: a knob an
310
+ * operator can set but that is silently ignored until the next election is
311
+ * worse than no knob at all. A value that is not a usable interval is ignored
312
+ * — a zero or negative `setInterval` period would spin the sweep as fast as
313
+ * the event loop allows.
314
+ */
315
+ private reconcileInterval;
316
+ /** Replace the timer with one running at `intervalMs`. */
317
+ private schedule;
318
+ /**
319
+ * Drop per-agent tracking for rows that are gone, so a long-lived leader's
320
+ * maps cannot grow without bound.
321
+ */
322
+ private pruneTracking;
323
+ /**
324
+ * `null` means leave it alone.
325
+ *
326
+ * A registered agent is always spared, and seeing it registered resets its
327
+ * stranded clock: that is the one signal saying the provision is genuinely
328
+ * alive, and it outranks every other test.
329
+ *
330
+ * Otherwise the agent is unseen, and every arm below needs that observation
331
+ * to have held for at least the flap grace — one sample proves nothing, since
332
+ * a live agent reads unseen during its own WS reconnect and during the window
333
+ * after a peer reconnect in which that peer advertises no agents yet.
334
+ *
335
+ * An adopted row means the agent reached some coordinator at least once, so
336
+ * its disappearance is a lost heartbeat rather than a spawn that never
337
+ * landed — both adopted shapes emit the same reason, and which coordinator
338
+ * the agent happened to reach stays invisible to the customer's teardown
339
+ * workflow. They differ only in how much sustained absence they need: a dead
340
+ * adopter is strong corroboration that the provision is orphaned (its agent's
341
+ * WS died with it), so the flap grace alone suffices; a live adopter is no
342
+ * evidence either way, so that arm waits out the full stranded window.
343
+ *
344
+ * A row with neither an owner nor an adopter predates migration 119 —
345
+ * `owner_instance_id IS NULL` reads as "unknown owner", never as "not mine",
346
+ * so it is left for a human rather than reaped during a rolling upgrade.
347
+ */
348
+ private verdict;
349
+ }
350
+ //# sourceMappingURL=event-provision-reaper.d.ts.map
@@ -11,7 +11,7 @@
11
11
  export interface ScalerFailureRecord {
12
12
  /** Scaler instance name (the configured scaler `name`); the diagnose row key. */
13
13
  backendName: string;
14
- /** Backend type: 'container' | 'bare-metal' | 'firecracker' | 'unknown'. */
14
+ /** Backend type: 'container' | 'bare-metal' | 'firecracker' | 'event' | 'unknown'. */
15
15
  backendType: string;
16
16
  /** True when the failed spawn was bound to a queued job (a run was affected). */
17
17
  bound: boolean;
@@ -20,7 +20,7 @@ import { type ToolRequirement } from '@kici-dev/shared';
20
20
  import type { IpAllocator, IpAllocationResult } from './ip-allocator.js';
21
21
  import type { AgentTokenStore } from '../agent/token-store.js';
22
22
  import { type FirecrackerBridgeConfig, type BridgeHealth, type ExecOptions } from '../firecracker/host-network.js';
23
- import type { ScalerBackend, ScalerEntry, ManagedAgent, LabelSetConfig, ScalerEventCallback, ValidationResult, EffectiveLimits, SpawnContext } from './types.js';
23
+ import type { ScalerBackend, ScalerDestroyContext, ScalerEntry, ManagedAgent, LabelSetConfig, ScalerEventCallback, ValidationResult, EffectiveLimits, SpawnContext } from './types.js';
24
24
  /**
25
25
  * Infrastructure interfaces that share the `kici-` prefix but must NEVER be
26
26
  * deleted by the orphan sweep. Bridges (kici-br0/kici-br1) carry all VM
@@ -104,7 +104,7 @@ export interface FirecrackerScalerBackendOptions {
104
104
  export declare class FirecrackerScalerBackend implements ScalerBackend {
105
105
  readonly type: "firecracker";
106
106
  readonly spawnsOnLocalHost = true;
107
- readonly maxAgents: number;
107
+ maxAgents: number;
108
108
  readonly logsSource = "firecracker-serial";
109
109
  /** AbortControllers for file tailing per managed VM (keyed by agent ID) */
110
110
  private readonly tailAbortControllers;
@@ -145,7 +145,7 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
145
145
  static getRequiredTools(entry: ScalerEntry): ToolRequirement[];
146
146
  get labelSets(): LabelSetConfig[];
147
147
  getActiveCount(): number;
148
- spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, _spawnContext?: SpawnContext, _signal?: AbortSignal): Promise<ManagedAgent>;
148
+ spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, _signal?: AbortSignal): Promise<ManagedAgent>;
149
149
  getScalerContext(agentId: string): Record<string, unknown> | undefined;
150
150
  /**
151
151
  * Bridge config for host-network diagnostics (read-only snapshot).
@@ -165,19 +165,46 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
165
165
  * ScalerManager.ensureHostsReady catches per-backend and degrades this scaler.
166
166
  */
167
167
  ensureHostReady(): Promise<void>;
168
- destroy(managedId: string): Promise<void>;
168
+ destroy(managedId: string, _context?: ScalerDestroyContext): Promise<void>;
169
+ /**
170
+ * Force-reclaim a VM this backend no longer tracks in memory.
171
+ *
172
+ * `destroy()` opens with `if (!managed) return`, so after an orchestrator
173
+ * restart it silently no-ops while the VM keeps running, holding its RAM and
174
+ * its IP. `cleanupOrphans()` cannot recover that VM either: every one of its
175
+ * three passes skips an id whose jailer process is alive, deliberately, since
176
+ * a live process is the only thing that distinguishes a healthy VM from a
177
+ * leaked one when you are sweeping blind.
178
+ *
179
+ * This is the same reclaim addressed to ONE id whose orphan status the caller
180
+ * already established, so the liveness check is exactly what must not apply.
181
+ * See `ScalerBackend.reapUnowned` for the two properties the caller relies on;
182
+ * the host-local half is enforced here by refusing to act unless the jailer
183
+ * chroot for `managedId` exists on THIS host. That directory is the only
184
+ * host-local artifact a VM leaves: an `ip_allocations` row is not one, because
185
+ * an HA pair is "two identical orchestrators sharing the same PostgreSQL,
186
+ * scalers, and routing key" (`docs/operator/orchestrator/clustering.md`), so a
187
+ * peer's live VM has a row naming this very scaler. The row is read only to
188
+ * finish a reclaim the chroot already authorized.
189
+ */
190
+ reapUnowned(managedId: string): Promise<boolean>;
169
191
  shutdownAll(): Promise<void>;
170
192
  /**
171
193
  * Clear MMDS data for an agent after receiving config.ack via WS.
172
194
  * Belt-and-suspenders: even though MMDS only contains orchestrator URL,
173
195
  * clearing it reduces the attack surface to zero post-startup.
174
196
  *
175
- * Note: Wiring from agent-handler is done in Plan 04.
197
+ * Called by `ScalerManager.onConfigAck`, which reaches it structurally
198
+ * (`'clearAgentMmds' in backend`) rather than through `ScalerBackend` — no
199
+ * other backend has an MMDS to clear.
176
200
  *
177
201
  * @param agentId - The agent ID whose VM MMDS should be cleared
178
202
  */
179
203
  clearAgentMmds(agentId: string): Promise<void>;
180
- reload(labelSets: LabelSetConfig[]): ValidationResult;
204
+ reload(labelSets: LabelSetConfig[], opts?: {
205
+ maxAgents?: number;
206
+ entry?: ScalerEntry;
207
+ }): ValidationResult;
181
208
  /**
182
209
  * Clean up orphaned VMs.
183
210
  *
@@ -14,9 +14,19 @@ export { BareMetalScalerBackend } from './bare-metal-backend.js';
14
14
  export type { BareMetalScalerBackendOptions } from './bare-metal-backend.js';
15
15
  export { FirecrackerScalerBackend } from './firecracker-backend.js';
16
16
  export type { FirecrackerScalerBackendOptions, FirecrackerManagedAgent, } from './firecracker-backend.js';
17
+ export { createScalerBackend, requiredToolsFor } from './backend-factory.js';
18
+ export type { BackendFactoryContext, IpAllocatorParams } from './backend-factory.js';
19
+ export { EventScalerBackend } from './event-backend.js';
20
+ export type { EventScalerBackendOptions, ScalerEventEmitterLike } from './event-backend.js';
21
+ export { ClaimStore, DEFAULT_CLAIM_TTL_SECONDS } from './claim-store.js';
22
+ export type { ClaimSpec, ClaimedCredentials, ClaimStoreOptions, CreateEphemeralToken, } from './claim-store.js';
23
+ export { SCALER_EVENT_NAMES, ScaleDownReason, ScalerScaleUpPayload, ScalerScaleDownPayload, KICI_EVENT_NAME_PREFIX, } from './scaler-events.js';
24
+ export type { ScalerDestroyContext } from './types.js';
17
25
  export { FirecrackerApi, FirecrackerApiError } from './firecracker-api.js';
18
26
  export { ScalerManager } from './manager.js';
19
- export type { ScalerStatus } from './manager.js';
27
+ export type { ScalerStatus, ScalerManagerDeps } from './manager.js';
28
+ export { EventProvisionReaper } from './event-provision-reaper.js';
29
+ export type { EventProvisionReaperOptions, ReaperWindows } from './event-provision-reaper.js';
20
30
  export { WarmPoolManager } from './warm-pool.js';
21
31
  export type { WarmPoolCallbacks } from './warm-pool.js';
22
32
  export { DbIpAllocator, InMemoryIpAllocator, parseCidr, ipToNumber, numberToIp, generateMac, generateTapName, } from './ip-allocator.js';
@@ -52,10 +52,10 @@ export declare function detectLabelSetOverlaps(scalers: Array<{
52
52
  * ['macos'] matches ['macos', 'darwin', 'bare-metal']. This is consistent
53
53
  * with peer routing (findPeersWithCapacity) which uses the same semantics.
54
54
  *
55
- * In addition, every label declared in a scaler's `mandatoryLabels` must be
55
+ * In addition, every label in the gate for the matched label set must be
56
56
  * present in the job's `runsOn`. This is the Kubernetes-taint-style opt-in
57
- * gate: a scaler with `mandatoryLabels: ['gpu']` only accepts jobs that
58
- * explicitly include `gpu` in `runsOn`.
57
+ * gate: a label set gated on `['gpu']` only accepts jobs that explicitly
58
+ * include `gpu` in `runsOn`.
59
59
  *
60
60
  * When multiple backends match, the one with the smallest label set wins
61
61
  * (most specific match).
@@ -66,6 +66,7 @@ export declare function findBackendForLabels(labels: string[], scalers: Array<{
66
66
  labels: string[];
67
67
  }>;
68
68
  mandatoryLabels?: string[];
69
+ labelSetMandatoryLabels?: string[][];
69
70
  }>, excludeLabels?: string[]): {
70
71
  scalerName: string;
71
72
  labelSetIndex: number;