@kici-dev/orchestrator 0.6.1 → 0.8.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 (187) hide show
  1. package/dist/agent/dispatcher.d.ts +10 -1
  2. package/dist/agent/host-roster.d.ts +8 -3
  3. package/dist/agent/registry.d.ts +60 -5
  4. package/dist/app.d.ts +10 -5
  5. package/dist/cache/dep-cache.d.ts +14 -6
  6. package/dist/cache/global-eval-round-cache.d.ts +1 -1
  7. package/dist/cache/index.d.ts +1 -1
  8. package/dist/cache/source-cache.d.ts +92 -15
  9. package/dist/cancel/cancel-run.d.ts +20 -2
  10. package/dist/cancel/sweep-stuck-cancelling.d.ts +30 -0
  11. package/dist/cli/api-client.d.ts +10 -0
  12. package/dist/cli/commands/agent-service/logs.d.ts +1 -1
  13. package/dist/cli/commands/agent-service/restart.d.ts +1 -1
  14. package/dist/cli/commands/agent-service/start.d.ts +1 -1
  15. package/dist/cli/commands/agent-service/status.d.ts +1 -1
  16. package/dist/cli/commands/agent-service/stop.d.ts +1 -1
  17. package/dist/cli/commands/agent-service/uninstall.d.ts +1 -1
  18. package/dist/cli/commands/agent.d.ts +25 -0
  19. package/dist/cli/commands/config.d.ts +16 -0
  20. package/dist/cli/commands/db-backup.d.ts +46 -0
  21. package/dist/cli/commands/held-run.d.ts +9 -0
  22. package/dist/cli/commands/orchestrator-service/drain.d.ts +2 -19
  23. package/dist/cli/commands/orchestrator-service/index.d.ts +9 -1
  24. package/dist/cli/commands/orchestrator-service/install-env.d.ts +43 -2
  25. package/dist/cli/commands/orchestrator-service/logs.d.ts +1 -1
  26. package/dist/cli/commands/orchestrator-service/restart.d.ts +1 -1
  27. package/dist/cli/commands/orchestrator-service/start.d.ts +1 -1
  28. package/dist/cli/commands/orchestrator-service/status.d.ts +17 -1
  29. package/dist/cli/commands/orchestrator-service/stop.d.ts +1 -1
  30. package/dist/cli/commands/orchestrator-service/uninstall.d.ts +1 -1
  31. package/dist/cli/commands/orchestrator-service/upgrade.d.ts +2 -1
  32. package/dist/cli/commands/rotate.d.ts +1 -1
  33. package/dist/cli/commands/shared/env-file-mode.d.ts +16 -0
  34. package/dist/cli/commands/shared/upgrade-hooks.d.ts +32 -0
  35. package/dist/cli/commands/shared/upgrade-safety.d.ts +81 -0
  36. package/dist/cli/commands/shared/versioned-upgrade.d.ts +67 -12
  37. package/dist/cli/commands/source.d.ts +1 -0
  38. package/dist/cli/commands/token.d.ts +4 -0
  39. package/dist/cli/commands/trust-policy.d.ts +57 -1
  40. package/dist/cli/join.d.ts +1 -1
  41. package/dist/cli/service/backup-timer.d.ts +108 -0
  42. package/dist/cli/service/compose-path.d.ts +11 -0
  43. package/dist/cli/service/compose.d.ts +22 -0
  44. package/dist/cli/service/deploy-env.d.ts +10 -11
  45. package/dist/cli/service/env-assignment.d.ts +24 -0
  46. package/dist/cli/service/index.d.ts +2 -2
  47. package/dist/cli/service/instance/resolve.d.ts +71 -7
  48. package/dist/cli/service/instance/types.d.ts +15 -0
  49. package/dist/cli/service/launchd.d.ts +7 -0
  50. package/dist/cli/service/systemd.d.ts +1 -0
  51. package/dist/cli/service/types.d.ts +36 -0
  52. package/dist/cli/service/windows.d.ts +16 -0
  53. package/dist/cli/wizard/orchestrator-wizard.d.ts +12 -3
  54. package/dist/cli/wizard/prompts.d.ts +7 -0
  55. package/dist/cli.js +12544 -9299
  56. package/dist/cluster/cluster-settings-reader.d.ts +2 -2
  57. package/dist/cluster/coordinator.d.ts +11 -0
  58. package/dist/cluster/instance-heartbeat.d.ts +96 -0
  59. package/dist/cluster/join-client.d.ts +55 -5
  60. package/dist/cluster/join-handler.d.ts +1 -0
  61. package/dist/cluster/orphan-recovery.d.ts +16 -0
  62. package/dist/cluster/peer-registry.d.ts +10 -0
  63. package/dist/concurrency/dispatch-next-queued.d.ts +1 -13
  64. package/dist/concurrency/index.d.ts +1 -2
  65. package/dist/concurrency/queue-manager.d.ts +63 -18
  66. package/dist/config/env-overlay.d.ts +3 -2
  67. package/dist/config.d.ts +24 -0
  68. package/dist/contexts/release-queued-holds.d.ts +23 -1
  69. package/dist/dashboard/attestation-filters.d.ts +2 -0
  70. package/dist/dashboard/handler.d.ts +22 -2
  71. package/dist/db/deferred-indexes.d.ts +72 -0
  72. package/dist/db/migration-test-harness.d.ts +22 -0
  73. package/dist/db/migrations/131_execution_jobs_git_credentials.d.ts +30 -0
  74. package/dist/db/migrations/132_execution_runs_trigger_event.d.ts +23 -0
  75. package/dist/db/migrations/133_dispatch_queue_source_tar_digest.d.ts +23 -0
  76. package/dist/db/migrations/134_dispatch_queue_ownership.d.ts +4 -0
  77. package/dist/db/migrations/135_cluster_settings_retention_days.d.ts +4 -0
  78. package/dist/db/migrations/136_master_key_wrapped_tables_key_version.d.ts +4 -0
  79. package/dist/db/migrations/137_execution_runs_pr_head_context.d.ts +4 -0
  80. package/dist/db/migrations/138_execution_runs_subject_trigger_event.d.ts +4 -0
  81. package/dist/db/migrations/139_admin_token_subject.d.ts +4 -0
  82. package/dist/db/migrations/140_dispatch_queue_canonical_labels.d.ts +54 -0
  83. package/dist/db/migrations/141_event_log_shed.d.ts +15 -0
  84. package/dist/db/migrator.d.ts +47 -0
  85. package/dist/db/notify-listener.d.ts +60 -0
  86. package/dist/db/synthetic-job-ids.d.ts +24 -0
  87. package/dist/db/types.d.ts +169 -4
  88. package/dist/deployment/deployment-identity.d.ts +31 -1
  89. package/dist/diagnostics/checks/index.d.ts +2 -1
  90. package/dist/diagnostics/checks/retention.d.ts +24 -0
  91. package/dist/diagnostics/types.d.ts +7 -0
  92. package/dist/events/event-router.d.ts +1 -1
  93. package/dist/fault-injection-types.d.ts +53 -0
  94. package/dist/firecracker/host-network.d.ts +63 -8
  95. package/dist/git/credential-broker.d.ts +35 -2
  96. package/dist/git/inherited-secret.d.ts +7 -2
  97. package/dist/git/job-context.d.ts +7 -4
  98. package/dist/helpers/secure-write.d.ts +35 -0
  99. package/dist/index.js +196 -67
  100. package/dist/metrics/prometheus.d.ts +116 -1
  101. package/dist/oidc/db-signer.d.ts +27 -5
  102. package/dist/oidc/id-token-claims.d.ts +33 -2
  103. package/dist/oidc/oidc-mint-registration.d.ts +6 -0
  104. package/dist/oidc/orchestrator-mint.d.ts +8 -0
  105. package/dist/oidc/reconcile-signing-key.d.ts +37 -0
  106. package/dist/orchestrator-core.d.ts +9 -3
  107. package/dist/pipeline/decorating-secret-resolver.d.ts +1 -1
  108. package/dist/pipeline/dispatch-matched-workflow.d.ts +65 -14
  109. package/dist/pipeline/manual-schedule.d.ts +1 -0
  110. package/dist/pipeline/process-webhook.d.ts +20 -2
  111. package/dist/pipeline/route-or-dispatch-jobs.d.ts +7 -0
  112. package/dist/pipeline/run-event-context.d.ts +39 -0
  113. package/dist/policy/dashboard-write-policy-listener.d.ts +1 -1
  114. package/dist/policy/dashboard-write-policy.d.ts +47 -1
  115. package/dist/provenance/dispatch-context.d.ts +46 -0
  116. package/dist/provenance/verify-deferred-capture.d.ts +39 -0
  117. package/dist/providers/github/auth.d.ts +9 -0
  118. package/dist/providers/github/deliveries.d.ts +118 -0
  119. package/dist/providers/local/lock-file-fetcher.d.ts +8 -0
  120. package/dist/providers/universal-git/config.d.ts +1 -1
  121. package/dist/queue/cleanup.d.ts +13 -0
  122. package/dist/queue/job-queue.d.ts +162 -14
  123. package/dist/queue/retention.d.ts +105 -0
  124. package/dist/reporting/execution-tracker.d.ts +87 -36
  125. package/dist/routes/admin-db.d.ts +5 -0
  126. package/dist/routes/admin-events.d.ts +13 -0
  127. package/dist/routes/admin-held-runs.d.ts +11 -1
  128. package/dist/routes/admin-org-settings.d.ts +9 -0
  129. package/dist/routes/admin-runs.d.ts +7 -4
  130. package/dist/routes/admin-sources.d.ts +15 -0
  131. package/dist/routes/admin-trust-policy.d.ts +11 -0
  132. package/dist/routes/admin.d.ts +25 -0
  133. package/dist/scaler/backend-factory.d.ts +16 -0
  134. package/dist/scaler/bare-metal-backend.d.ts +22 -4
  135. package/dist/scaler/claim-store.d.ts +0 -2
  136. package/dist/scaler/config.d.ts +6 -3
  137. package/dist/scaler/container-backend.d.ts +102 -3
  138. package/dist/scaler/container-hostconfig.d.ts +60 -0
  139. package/dist/scaler/event-backend.d.ts +7 -6
  140. package/dist/scaler/file-tail.d.ts +25 -2
  141. package/dist/scaler/firecracker-api.d.ts +9 -1
  142. package/dist/scaler/firecracker-backend.d.ts +135 -2
  143. package/dist/scaler/host-access.d.ts +59 -0
  144. package/dist/scaler/index.d.ts +1 -0
  145. package/dist/scaler/manager.d.ts +51 -2
  146. package/dist/scaler/scaler-state-store.d.ts +13 -0
  147. package/dist/scaler/types.d.ts +15 -7
  148. package/dist/scaler/warm-pool.d.ts +6 -4
  149. package/dist/secrets/config.d.ts +48 -0
  150. package/dist/secrets/dashboard-encryption-key.d.ts +19 -0
  151. package/dist/secrets/ephemeral-keys.d.ts +9 -3
  152. package/dist/secrets/index.d.ts +3 -1
  153. package/dist/secrets/job-secret-gate.d.ts +76 -0
  154. package/dist/secrets/master-key-rotation.d.ts +65 -0
  155. package/dist/secrets/secret-output-crypto.d.ts +17 -0
  156. package/dist/secrets/secret-resolver.d.ts +23 -6
  157. package/dist/secrets/source-credentials.d.ts +1 -1
  158. package/dist/secrets/token-manager.d.ts +4 -1
  159. package/dist/security/identity-link.d.ts +1 -1
  160. package/dist/security/reduced-privilege-note.d.ts +5 -0
  161. package/dist/security/trust-directory-store.d.ts +1 -1
  162. package/dist/security/trust-policy-gate.d.ts +70 -6
  163. package/dist/security/trust-tier.d.ts +31 -0
  164. package/dist/server.d.ts +1 -1
  165. package/dist/server.js +10793 -5323
  166. package/dist/sources/source-manager.d.ts +1 -1
  167. package/dist/stale-detector/stale-dispatch-candidates.d.ts +43 -0
  168. package/dist/standalone.js +9385 -4160
  169. package/dist/storage/loopback-guard.d.ts +53 -6
  170. package/dist/webhook/event-log.d.ts +14 -1
  171. package/dist/webhook/generic-sources-listener.d.ts +1 -1
  172. package/dist/webhook/ingest-admission.d.ts +24 -0
  173. package/dist/webhook/ingest-overflow-replayer.d.ts +78 -14
  174. package/dist/webhook/ingest-overflow-types.d.ts +13 -1
  175. package/dist/webhook/register-source-bundle.d.ts +23 -0
  176. package/dist/webhook/relay-reinject.d.ts +11 -12
  177. package/dist/webhook/shed-breadcrumb.d.ts +15 -0
  178. package/dist/worker/in-memory-job-queue.d.ts +9 -0
  179. package/dist/ws/fleet-agent-collector.d.ts +7 -0
  180. package/dist/ws/git-credential-relay.d.ts +41 -12
  181. package/dist/ws/platform-client.d.ts +28 -5
  182. package/dist/ws/test-relay-handlers.d.ts +2 -0
  183. package/installer-image-digests.json +3 -3
  184. package/package.json +4 -4
  185. package/sbom.spdx.json +53 -53
  186. package/dist/concurrency/group-tracker.d.ts +0 -51
  187. package/dist/scaler/nftables.d.ts +0 -111
@@ -6,9 +6,14 @@
6
6
  * Handles full container lifecycle: pull, create, start, stop, remove.
7
7
  * Supports container socket sharing, resource limits, and orphan cleanup.
8
8
  */
9
+ import Docker from 'dockerode';
9
10
  import { type ToolRequirement } from '@kici-dev/shared';
10
11
  import type { AgentTokenStore } from '../agent/token-store.js';
11
12
  import type { ScalerBackend, ScalerDestroyContext, ManagedAgent, LabelSetConfig, LogCapture, ResourceRequest, EffectiveLimits, SpawnContext, ScalerEventCallback, ValidationResult, ScalerEntry } from './types.js';
13
+ /** Name of the isolated bridge network for agent containers. */
14
+ export declare const ISOLATED_NETWORK_NAME = "kici-agent-net";
15
+ /** Gateway IP for the isolated agent network (host-side). */
16
+ export declare const ISOLATED_NETWORK_GATEWAY = "172.30.0.1";
12
17
  /**
13
18
  * Result of runtime detection.
14
19
  */
@@ -40,6 +45,12 @@ export interface ContainerScalerBackendOptions {
40
45
  extraHosts?: string[];
41
46
  /** Disable nftables-based network isolation (default: true). Set to false when nft is unavailable (e.g. rootless containers without NET_ADMIN). */
42
47
  networkIsolation?: boolean;
48
+ /**
49
+ * Extra `hostAccess` entries the orchestrator itself directed agents at —
50
+ * today its object-storage endpoint. Folded into the agent default so the
51
+ * narrowing does not cut off a host-local object store.
52
+ */
53
+ hostServices?: string[];
43
54
  /** Token store for creating ephemeral agent auth tokens. Optional -- when undefined, no token is injected. */
44
55
  tokenStore?: AgentTokenStore;
45
56
  /** TTL for ephemeral agent tokens in ms. Default: 1 hour. */
@@ -52,7 +63,27 @@ export interface ContainerScalerBackendOptions {
52
63
  tokenTtlProvider?: () => Promise<number>;
53
64
  /** Agent roles for this scaler. undefined = all, [] = execution only. */
54
65
  roles?: string[];
66
+ /**
67
+ * Whether an agent id is currently registered with this orchestrator.
68
+ *
69
+ * Used by `cleanupOrphans` to distinguish a running container whose agent is
70
+ * doing work from one whose agent never registered (or has gone). Defaults
71
+ * to "assume registered" when unwired, so an un-plumbed caller never reaps a
72
+ * live agent.
73
+ */
74
+ isRegistered?: (agentId: string) => boolean;
55
75
  }
76
+ /**
77
+ * Create or find the isolated agent bridge network, and return its id.
78
+ *
79
+ * Module-level rather than a method because the bare-metal backend's container
80
+ * mode attaches its agent containers to the same network: both agent-container
81
+ * spawn paths must land on it, or one of them runs on the runtime's default
82
+ * bridge with full LAN, RFC1918 and cloud-metadata reach.
83
+ *
84
+ * Idempotent, including against a concurrent creator (409).
85
+ */
86
+ export declare function ensureIsolatedNetwork(docker: Docker): Promise<string>;
56
87
  export declare class ContainerScalerBackend implements ScalerBackend {
57
88
  readonly type: "container";
58
89
  readonly spawnsOnLocalHost: boolean;
@@ -69,6 +100,8 @@ export declare class ContainerScalerBackend implements ScalerBackend {
69
100
  private readonly extraHosts?;
70
101
  /** Whether nftables-based network isolation is enabled */
71
102
  private readonly networkIsolation;
103
+ /** Host services the orchestrator directed agents at, as `hostAccess` entries. */
104
+ private readonly hostServices?;
72
105
  /** Token store for creating ephemeral agent auth tokens */
73
106
  private readonly tokenStore?;
74
107
  /** TTL for ephemeral agent tokens in ms */
@@ -77,6 +110,8 @@ export declare class ContainerScalerBackend implements ScalerBackend {
77
110
  private readonly tokenTtlProvider?;
78
111
  /** Agent roles for this scaler. undefined = all, [] = execution only. */
79
112
  private readonly roles;
113
+ /** Registration probe for the orphan sweep; fail-safe default assumes registered. */
114
+ private readonly isRegistered;
80
115
  /** ID of the isolated bridge network (set after creation/discovery) */
81
116
  private isolatedNetworkId;
82
117
  /** Host bridge interface name for the isolated network (used by nftables) */
@@ -132,6 +167,15 @@ export declare class ContainerScalerBackend implements ScalerBackend {
132
167
  * `spawnContext` rather than letting the agent claim them.
133
168
  */
134
169
  private mintAgentIdentity;
170
+ /**
171
+ * Write one agent container's nftables rules, on both hooks.
172
+ *
173
+ * `forward` governs what the container reaches THROUGH the host; `input`
174
+ * governs what it reaches ON the host. A packet to one of the host's own
175
+ * addresses is delivered on input and never traverses forward, so the two
176
+ * are not interchangeable and both are written here.
177
+ */
178
+ private applyIsolationRules;
135
179
  spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, signal?: AbortSignal): Promise<ManagedAgent>;
136
180
  getScalerContext(agentId: string): Record<string, unknown> | undefined;
137
181
  destroy(managedId: string, _context?: ScalerDestroyContext): Promise<void>;
@@ -145,10 +189,65 @@ export declare class ContainerScalerBackend implements ScalerBackend {
145
189
  entry?: ScalerEntry;
146
190
  }): ValidationResult;
147
191
  /**
148
- * Clean up orphaned kici-managed containers on startup.
149
- * Finds containers with the `kici-managed=true` label and removes them.
150
- * Returns the count of cleaned containers.
192
+ * Clean up this scaler's orphaned containers and stale isolation rules.
193
+ *
194
+ * Every filter below exists because the sweep used to list every
195
+ * `kici-managed=true` container on the host and force-remove it, running at
196
+ * orchestrator boot, at worker-peer start, on a live config reload that adds
197
+ * a scaler, and from `kici-admin scaler reap-orphans`. Two orchestrators
198
+ * sharing one docker host therefore killed each other's running agents on
199
+ * every boot, and adding a second container scaler removed the first one's
200
+ * agents mid-job — every in-flight job failing as "agent disconnected" on a
201
+ * healthy system.
202
+ *
203
+ * A container is removed only when all of these hold:
204
+ * - it carries this scaler's own `kici-scaler-name`, so a backend can only
205
+ * ever reap what it stamped;
206
+ * - its `kici-agent-id` is not one this backend is currently tracking
207
+ * (spawning, running, or destroying);
208
+ * - it is not running, or it is running with an agent id that is not
209
+ * registered — a running, registered agent is doing work.
210
+ *
211
+ * Returns the count of cleaned containers plus reaped rules.
151
212
  */
152
213
  cleanupOrphans(): Promise<number>;
214
+ /**
215
+ * Remove containers this backend spawned for one agent id.
216
+ *
217
+ * Implements the optional `ScalerBackend.reapUnowned` hook. It is what
218
+ * reclaims a container whose registration the orchestrator refused — the
219
+ * case `cleanupOrphans` used to cover by removing every `kici-managed`
220
+ * container on the host, which is exactly the breadth that killed other
221
+ * orchestrators' live agents.
222
+ *
223
+ * @returns true when a container was found and removed
224
+ */
225
+ reapUnowned(managedId: string): Promise<boolean>;
226
+ /**
227
+ * Delete isolation rules for container IPs on this scaler's isolated network
228
+ * that no tracked container holds.
229
+ *
230
+ * Rules are removed only on the teardown paths this process drives, so a
231
+ * crash or a `kill -9` strands them. The network's address pool is recycled,
232
+ * so the next container to take that IP inherits the dead job's allowlist.
233
+ *
234
+ * Scoped to addresses no live container holds: `kici-agent-net` is one fixed
235
+ * network shared by every container-spawning backend on the host, so this
236
+ * backend's own tracking map is not enough to tell an abandoned address from
237
+ * a neighbour's live one.
238
+ */
239
+ private reapUnownedIsolationRules;
240
+ /**
241
+ * Every address held on the isolated agent network by a kici-managed
242
+ * container, across every scaler and every orchestrator on this runtime.
243
+ *
244
+ * Deliberately NOT filtered by `kici-scaler-name`: the point is to see the
245
+ * neighbours, and a neighbour is by definition stamped with a name this
246
+ * backend does not know.
247
+ *
248
+ * @returns the addresses, or `null` when the runtime could not be listed — in
249
+ * which case the caller reaps nothing rather than reaping blind.
250
+ */
251
+ private isolatedNetworkAddressesInUse;
153
252
  }
154
253
  //# sourceMappingURL=container-backend.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The `HostConfig` every KiCI agent container is created with.
3
+ *
4
+ * Two backends spawn an agent container — `ContainerScalerBackend` and the
5
+ * bare-metal backend's container mode — and they had diverged into two
6
+ * postures for one threat: the first applied resource limits, the second
7
+ * applied none and dropped the limits the operator configured on the floor.
8
+ * One builder is what keeps them from diverging again the next time either is
9
+ * touched.
10
+ */
11
+ import type Docker from 'dockerode';
12
+ import type { EffectiveLimits } from './types.js';
13
+ /**
14
+ * Maximum processes an agent container may create.
15
+ *
16
+ * High enough that no realistic build reaches it, low enough to bound a fork
17
+ * bomb. Not operator-configurable: a `limits.pids` key is a new surface with
18
+ * its own docs and validation, and nothing yet says the constant is wrong.
19
+ */
20
+ export declare const AGENT_CONTAINER_PIDS_LIMIT = 4096;
21
+ /**
22
+ * Why this container is NOT capability-dropped, though the job containers the
23
+ * agent starts are.
24
+ *
25
+ * Both spawn paths set `KICI_EXECUTION_MODE=bare-metal`, and bare-metal
26
+ * container mode also sets `KICI_JOB_IMAGE_AGENT=1` — so unless the job
27
+ * declares its own image, the workflow's steps run as host processes INSIDE
28
+ * this container. It is a build execution environment, not a supervisor.
29
+ * `CapDrop: ['ALL']` here would therefore land on customer build steps:
30
+ * `apt-get` and `dpkg` need CHOWN, DAC_OVERRIDE, FOWNER, SETUID and SETGID,
31
+ * and `no-new-privileges` breaks `sudo` and every setuid binary. There is no
32
+ * opt-out, so the break would be silent and total for `type: container`
33
+ * scalers.
34
+ *
35
+ * The hardening the operator docs promise already exists, one boundary in:
36
+ * the agent applies `CapDrop: ['ALL']`, `no-new-privileges` and a tighter
37
+ * `PidsLimit` to each nested JOB container
38
+ * (`packages/agent/src/execution/sandbox/container-hardening.ts`). That is the
39
+ * boundary that confines customer code; this one hosts it.
40
+ *
41
+ * `PidsLimit` is the exception and stays: a fork-bomb ceiling costs a build
42
+ * nothing at 4096.
43
+ */
44
+ export interface AgentContainerHostConfigInput {
45
+ /** Resolved cpu / memory ceiling for this spawn, when the operator set one. */
46
+ limits?: EffectiveLimits;
47
+ /** Volume and path bind mounts. */
48
+ binds?: string[];
49
+ /** Extra `host:ip` entries (e.g. a private registry alias). */
50
+ extraHosts?: string[];
51
+ }
52
+ /**
53
+ * Build the agent container's `HostConfig`.
54
+ *
55
+ * Carries the operator's resource limits, a fork-bomb ceiling, and nothing
56
+ * that would restrict the build steps this container runs — see the note on
57
+ * {@link AGENT_CONTAINER_PIDS_LIMIT}.
58
+ */
59
+ export declare function buildAgentContainerHostConfig(input: AgentContainerHostConfigInput): Docker.HostConfig;
60
+ //# sourceMappingURL=container-hostconfig.d.ts.map
@@ -108,12 +108,13 @@ export declare class EventScalerBackend implements ScalerBackend {
108
108
  */
109
109
  shutdownAll(): Promise<void>;
110
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.
111
+ * Apply the new config. The backend reads `roles`, `agentTokenTtlSeconds`
112
+ * and `provisioningTargets` off its entry at every spawn (and
113
+ * `mandatoryLabels` only when a spawn reaches it without a context), so the
114
+ * entry is replaced here otherwise a reload that retargets the
115
+ * provisioning workflow, or changes the scaler's roles, would keep emitting
116
+ * scale-up events to the old workflow refs and minting agent labels from the
117
+ * old roles, while the manager's own routing gate used the new ones.
117
118
  */
118
119
  reload(labelSets: LabelSetConfig[], opts?: {
119
120
  maxAgents?: number;
@@ -2,12 +2,35 @@
2
2
  * File tailing utility for append-only log files.
3
3
  *
4
4
  * Provides an async generator that yields complete lines as they are appended
5
- * to a file, using `fs.watch()` for change notifications and partial line
5
+ * to a file, using `fs.watchFile()` for change notifications and partial line
6
6
  * buffering for split writes.
7
7
  *
8
8
  * Used by the Firecracker backend to tail serial console and VMM log files
9
- * from the jailer chroot directory.
9
+ * from the jailer chroot directory. The guest writes to `/dev/console`
10
+ * unprivileged, so everything this module buffers is tenant-controlled: every
11
+ * buffer here is bounded, and an overflow is reported in-band rather than
12
+ * growing the orchestrator's heap.
10
13
  */
14
+ /**
15
+ * Largest logical line kept intact. A guest that writes without ever emitting a
16
+ * newline (`yes | tr -d '\n' > /dev/console`) would otherwise grow the partial
17
+ * buffer at disk-write speed until the Node heap limit takes the orchestrator —
18
+ * and every in-flight job on it — down.
19
+ */
20
+ export declare const MAX_LINE_BYTES: number;
21
+ /**
22
+ * Largest slice read from the file per poll. A burst is drained across several
23
+ * ticks instead of materialising the whole delta in one string.
24
+ */
25
+ export declare const MAX_READ_BYTES: number;
26
+ /**
27
+ * Largest number of complete lines held for the consumer. `forwardLine` →
28
+ * Winston → Loki is orders of magnitude slower than a guest can write to the
29
+ * serial console, so the queue is the second place unbounded growth appears.
30
+ * Matches the agent-side log buffer's cap, which uses gap markers for the same
31
+ * situation.
32
+ */
33
+ export declare const MAX_QUEUED_LINES = 10000;
11
34
  /**
12
35
  * Tail an append-only file, yielding complete lines as they are written.
13
36
  *
@@ -35,7 +35,15 @@ export declare class FirecrackerApiError extends Error {
35
35
  */
36
36
  export declare class FirecrackerApi {
37
37
  private readonly socketPath;
38
- constructor(socketPath: string);
38
+ /**
39
+ * Aborts every in-flight and subsequent request on this client.
40
+ *
41
+ * Set by the spawn path so a spawn deadline actually stops the VM
42
+ * provisioning calls, rather than leaving them running against a VM the
43
+ * manager has already given up on.
44
+ */
45
+ private signal;
46
+ constructor(socketPath: string, signal?: AbortSignal);
39
47
  /**
40
48
  * Send an HTTP request to the Firecracker API.
41
49
  *
@@ -34,6 +34,14 @@ export interface FirecrackerManagedAgent extends ManagedAgent {
34
34
  ip: string;
35
35
  /** TAP device name for cleanup */
36
36
  tapDevice: string;
37
+ /**
38
+ * The `hostAccess` policy resolved for this VM at provision time.
39
+ *
40
+ * Carried on the record because a bridge self-heal re-applies isolation for
41
+ * every live VM, and that path has no `orchestratorUrl` to re-derive the
42
+ * default from.
43
+ */
44
+ hostAccess?: string[];
37
45
  }
38
46
  export interface FirecrackerScalerBackendOptions {
39
47
  /** Human-readable name for this scaler */
@@ -44,6 +52,12 @@ export interface FirecrackerScalerBackendOptions {
44
52
  maxAgents: number;
45
53
  /** Shared IP allocator instance */
46
54
  ipAllocator: IpAllocator;
55
+ /**
56
+ * Extra `hostAccess` entries the orchestrator itself directed agents at —
57
+ * today its object-storage endpoint. Folded into the agent default so the
58
+ * narrowing does not cut off a host-local object store.
59
+ */
60
+ hostServices?: string[];
47
61
  /** Path to firecracker binary */
48
62
  firecrackerPath: string;
49
63
  /** Path to jailer binary */
@@ -108,6 +122,15 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
108
122
  readonly logsSource = "firecracker-serial";
109
123
  /** AbortControllers for file tailing per managed VM (keyed by agent ID) */
110
124
  private readonly tailAbortControllers;
125
+ /**
126
+ * Per-spawn `scaler.failed` emitters, keyed by agent id.
127
+ *
128
+ * The jailer's `exit` handler and the spawn-deadline abort both fire outside
129
+ * `spawn`'s own scope, and the manager releases a spawn reservation only on a
130
+ * failure event. Holding the emitter here is what lets those two paths report
131
+ * the failure instead of leaving the reservation held until restart.
132
+ */
133
+ private readonly spawnFailureHandlers;
111
134
  private _labelSets;
112
135
  private readonly name;
113
136
  private readonly ipAllocator;
@@ -122,6 +145,8 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
122
145
  private readonly bridgeName;
123
146
  private readonly cidr;
124
147
  private readonly gateway;
148
+ /** Host services the orchestrator directed agents at, as `hostAccess` entries. */
149
+ private readonly hostServices?;
125
150
  private readonly netmask;
126
151
  private readonly table;
127
152
  private readonly tokenStore?;
@@ -145,7 +170,16 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
145
170
  static getRequiredTools(entry: ScalerEntry): ToolRequirement[];
146
171
  get labelSets(): LabelSetConfig[];
147
172
  getActiveCount(): number;
148
- spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, _signal?: AbortSignal): Promise<ManagedAgent>;
173
+ spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, signal?: AbortSignal): Promise<ManagedAgent>;
174
+ /**
175
+ * Tear down a spawn that will never produce a usable VM, from a path that is
176
+ * not `spawn`'s own `catch` — a deadline abort, or the jailer exiting during
177
+ * boot.
178
+ *
179
+ * Idempotent: whichever of the two fires first wins and the other is a no-op,
180
+ * because `cleanupFailedSpawn` removes the tracking entry.
181
+ */
182
+ private abandonSpawn;
149
183
  getScalerContext(agentId: string): Record<string, unknown> | undefined;
150
184
  /**
151
185
  * Bridge config for host-network diagnostics (read-only snapshot).
@@ -164,7 +198,41 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
164
198
  * No-op unless autoProvisionHost is set. Throws on a real provision failure;
165
199
  * ScalerManager.ensureHostsReady catches per-backend and degrades this scaler.
166
200
  */
201
+ /**
202
+ * nft options for this backend's own rules.
203
+ *
204
+ * `table` is the operator-configured one, not the literal `kici`: with two
205
+ * coordinators on one host, coordinator B's baseline goes to `kici_b` while
206
+ * its per-VM rules used to go to `kici` — coordinator A's table. A's next
207
+ * re-provision then swept B's live VMs' isolation along with its own, and B's
208
+ * rules were evaluated against A's subnet-scoped baseline.
209
+ */
210
+ private nftOpts;
167
211
  ensureHostReady(): Promise<void>;
212
+ /**
213
+ * Fail loudly on a kernel that will not honour bridge port isolation.
214
+ *
215
+ * Without the flag every concurrent tenant's VM can reach every other one at
216
+ * L2, and no nft rule sees that traffic — so the isolation the operator docs
217
+ * promise silently does not exist. The same fail-closed posture
218
+ * `validateNftablesAvailability` takes for a missing `nft`: better to refuse
219
+ * to run VMs than to run them unisolated.
220
+ *
221
+ * The probe is an end-to-end one — create a TAP, enslave it isolated, delete
222
+ * it — because the flag can be rejected by the kernel, by iproute2, or by the
223
+ * bridge itself, and only the real command exercises all three.
224
+ */
225
+ private assertBridgePortIsolationSupported;
226
+ /**
227
+ * Re-apply per-VM isolation rules for every VM this backend still tracks.
228
+ *
229
+ * Provisioning no longer deletes the table, but it does sweep the `forward`
230
+ * chain of everything no live VM owns, and an old-shaped host carries its
231
+ * baseline there too. Re-applying is what guarantees a self-heal cannot leave
232
+ * a running VM fail-open. Each identifier is removed before it is re-added,
233
+ * so a self-heal that fires twice does not duplicate the rules.
234
+ */
235
+ private reapplyIsolationForTrackedVms;
168
236
  destroy(managedId: string, _context?: ScalerDestroyContext): Promise<void>;
169
237
  /**
170
238
  * Force-reclaim a VM this backend no longer tracks in memory.
@@ -222,6 +290,18 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
222
290
  * Returns the count of cleaned orphans.
223
291
  */
224
292
  cleanupOrphans(): Promise<number>;
293
+ /**
294
+ * Delete isolation rules for every identifier no live VM owns.
295
+ *
296
+ * The live set is the VM ids the passes above established, mapped to the IPs
297
+ * this scaler allocated for them. An identifier the DB does not attribute to
298
+ * a live VM of this scaler is left alone: a second scaler on the same host
299
+ * writes to the same chain, and reaping its rules would cut its running jobs
300
+ * off the network — the mistake this whole sweep exists to stop making.
301
+ *
302
+ * @returns the number of rules deleted
303
+ */
304
+ private reapUnownedIsolationRules;
225
305
  /**
226
306
  * Default interval for periodic orphan sweeps (15 minutes).
227
307
  *
@@ -309,7 +389,60 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
309
389
  */
310
390
  getChrootDir(agentId: string): string;
311
391
  /**
312
- * Check if a VM process is still alive by reading its PID file and sending signal 0.
392
+ * Probe the process a VM's PID file names.
393
+ *
394
+ * Checks, in order:
395
+ * 1. the file parses to a plausible PID;
396
+ * 2. `/proc/<pid>/stat` exists at all — the file is world-readable
397
+ * regardless of the jailer's `--uid` drop, so an unreadable one means no
398
+ * such process;
399
+ * 3. field 22 (`starttime`) is not later than the PID file's mtime — a
400
+ * recycled number belongs to a process that started after the file
401
+ * naming it was written;
402
+ * 4. field 2 (`comm`) is `firecracker` (11 characters, so it is not
403
+ * truncated by the 15-character `comm` limit) and `/proc/<pid>/root` —
404
+ * where the jailer pivot_roots — is this VM's chroot when the link is
405
+ * readable at all. Those carry `identityConfirmed`, not liveness.
406
+ */
407
+ private probeVmPid;
408
+ /**
409
+ * Read a VM's PID file and return the PID only when the process it names is
410
+ * provably still *this* VM's firecracker process.
411
+ *
412
+ * This is the guard for acting ON the process — `destroy` and `reapUnowned`
413
+ * SIGKILL through it — so it fails closed: anything short of a confirmed
414
+ * identity yields `undefined` and nothing is signalled.
415
+ *
416
+ * @returns the PID when it is provably this VM's, otherwise `undefined`
417
+ */
418
+ private readVmPid;
419
+ /**
420
+ * Whether a chroot's PID file names a process that is running right now.
421
+ *
422
+ * This is the guard for the orphan reaper, which asks the opposite question
423
+ * from {@link readVmPid} and therefore fails closed in the opposite
424
+ * direction. `readVmPid` refuses to signal a PID it cannot claim; the reaper
425
+ * must refuse to DELETE THE ROOTFS under one. A process whose `comm` or
426
+ * `/proc/<pid>/root` does not match is still a process, and deleting the
427
+ * files it is running out of corrupts whatever it is — while sparing it costs
428
+ * only the disk one chroot occupies.
429
+ *
430
+ * The leak the identity checks exist to prevent is unaffected: a recycled
431
+ * number is detected by start time, which this guard honours, so a chroot
432
+ * whose VM is genuinely over is still reclaimed.
433
+ */
434
+ private isChrootPidRunning;
435
+ /**
436
+ * Whether *this* VM's own firecracker process is still up.
437
+ *
438
+ * Liveness is `readVmPid` plus signal 0, so it answers the identity-bearing
439
+ * question — "is the VM I started still running?" — that
440
+ * {@link waitForProcessExit} needs. Without the identity checks a recycled
441
+ * PID answers signal 0 successfully and the VM is spared forever.
442
+ *
443
+ * The orphan reaper asks a different question and uses
444
+ * {@link isChrootPidRunning} instead: a PID it cannot claim is still a live
445
+ * process, and its rootfs must not be deleted underneath it.
313
446
  */
314
447
  private isVmProcessAlive;
315
448
  /**
@@ -0,0 +1,59 @@
1
+ /**
2
+ * What a scaled agent may reach on the orchestrator's own host.
3
+ *
4
+ * An agent container or Firecracker VM sends its packets to one of the host's
5
+ * addresses — a bridge gateway, the host's LAN address — and those are
6
+ * delivered on the netfilter input hook. The `forward` rules the backends
7
+ * already write never see them, so until the `input` chain existed a sandbox
8
+ * reached every port on the host. These helpers resolve the narrow set it
9
+ * actually needs, expressed in the same `hostAccess` vocabulary an operator
10
+ * writes in `scalers.yaml`.
11
+ */
12
+ import type { NetworkPolicy } from '@kici-dev/shared/net';
13
+ import type { AppConfig } from '../config.js';
14
+ /**
15
+ * Host-access entries for the agent-facing storage endpoint, when it is a
16
+ * service on this host rather than a remote one.
17
+ *
18
+ * A scaled agent connects to storage directly — that is why
19
+ * `assertAgentReachableStorage` refuses to start on a loopback endpoint — so an
20
+ * orchestrator-port-only default would cut off every deployment running its own
21
+ * object store beside the orchestrator.
22
+ *
23
+ * Two shapes are recognised, and nothing else:
24
+ *
25
+ * - **An IP literal.** The entry is scoped to that exact address and port, the
26
+ * narrowest rule that can work.
27
+ * - **A name with an explicit port.** No address rule can be built for a name
28
+ * that may move, so the entry names the port on any host address. It is
29
+ * still one port rather than the whole host.
30
+ *
31
+ * A name with no port is a public endpoint (`https://…`), which the agent
32
+ * reaches through the host over the `forward` hook and which no input rule
33
+ * governs. Emitting `*:443` for it would open a host port for nothing.
34
+ */
35
+ export declare function storageHostAccessEntries(config: AppConfig): string[];
36
+ /**
37
+ * Resolve the `hostAccess` policy for one agent sandbox.
38
+ *
39
+ * An explicit `hostAccess` on the label set wins outright: host reachability is
40
+ * operator-configurable policy, and an operator who names it has said exactly
41
+ * what this class of agent may reach.
42
+ *
43
+ * The default is the set an agent cannot work without:
44
+ *
45
+ * - **DNS on the bridge gateway.** Rootful podman resolves there, so a sandbox
46
+ * with no such rule cannot resolve the orchestrator's own hostname. Docker
47
+ * resolves inside the container's netns and is unaffected either way.
48
+ * - **The orchestrator's port**, on any host address, since the address the
49
+ * agent was told to dial is not knowable from here.
50
+ * - **Whatever host services the orchestrator itself directed the agent at**,
51
+ * which today means its object storage.
52
+ */
53
+ export declare function resolveAgentHostAccess(input: {
54
+ policy: NetworkPolicy | undefined;
55
+ orchestratorUrl: string;
56
+ gateway: string;
57
+ hostServices?: string[];
58
+ }): string[];
59
+ //# sourceMappingURL=host-access.d.ts.map
@@ -16,6 +16,7 @@ export { FirecrackerScalerBackend } from './firecracker-backend.js';
16
16
  export type { FirecrackerScalerBackendOptions, FirecrackerManagedAgent, } from './firecracker-backend.js';
17
17
  export { createScalerBackend, requiredToolsFor } from './backend-factory.js';
18
18
  export type { BackendFactoryContext, IpAllocatorParams } from './backend-factory.js';
19
+ export { resolveAgentHostAccess, storageHostAccessEntries } from './host-access.js';
19
20
  export { EventScalerBackend } from './event-backend.js';
20
21
  export type { EventScalerBackendOptions, ScalerEventEmitterLike } from './event-backend.js';
21
22
  export { ClaimStore, DEFAULT_CLAIM_TTL_SECONDS } from './claim-store.js';
@@ -198,6 +198,12 @@ export interface ScalerManagerDeps {
198
198
  activeJobs: number;
199
199
  registeredAt: number;
200
200
  }>;
201
+ /** Whether one agent id is currently registered on this host. */
202
+ isRegistered(agentId: string): boolean;
203
+ /** Condemn an agent before tearing it down; returns its active-job count. */
204
+ markDraining(agentId: string): number | undefined;
205
+ /** Undo `markDraining` when the teardown did not happen. */
206
+ clearDraining(agentId: string): void;
201
207
  };
202
208
  /**
203
209
  * Id of the orchestrator instance this manager runs on. Rows this
@@ -778,6 +784,33 @@ export declare class ScalerManager {
778
784
  * which is always `releaseAll(agentId)`.
779
785
  */
780
786
  private reserveForSpawn;
787
+ /**
788
+ * How many reservations one scaler currently holds.
789
+ *
790
+ * Derived from `this.reservations` rather than kept as a counter beside it:
791
+ * the map IS the admitted set, so a derived count cannot drift out of step
792
+ * with it — including on the boot/leader-switch rehydrate path, which
793
+ * repopulates the map directly.
794
+ */
795
+ /**
796
+ * Record a refusal taken on one of the two agent-count caps.
797
+ *
798
+ * The refusal itself is returned to the dispatcher as `at-capacity`, which
799
+ * leaves the job queued and logs nothing — correct while agents are genuinely
800
+ * busy, and indistinguishable from a scaler that has stopped spawning
801
+ * altogether. So the counter the metric's own help text promises
802
+ * (`maxAgents`, `globalMaxAgents`) is incremented here, beside the resource
803
+ * caps that already increment it.
804
+ *
805
+ * The warning is narrower than the counter on purpose. Ordinary saturation
806
+ * refuses with live agents holding the cap, and warning on that would emit a
807
+ * line per queued job for as long as the cluster is busy. A refusal where the
808
+ * reservation ledger outruns every backend's active count is the other shape:
809
+ * capacity billed to agents that are not running, which does not resolve on
810
+ * its own.
811
+ */
812
+ private reportCountCapRefusal;
813
+ private reservedCountForScaler;
781
814
  /**
782
815
  * Run a critical section under the reservation lock. Serializes concurrent
783
816
  * `requestScale()` calls so the check+reserve sequence is atomic.
@@ -1248,10 +1281,25 @@ export declare class ScalerManager {
1248
1281
  */
1249
1282
  getScalerContextForAgent(agentId: string): Record<string, unknown> | undefined;
1250
1283
  /**
1251
- * Start the warm pool idle check interval, the machine-pool ledger reaper,
1252
- * and the retirement sweep that tears down drained retiring backends.
1284
+ * Start the machine-pool ledger reaper and the retirement sweep that tears
1285
+ * down drained retiring backends, and publish the warm-pool gauges so each
1286
+ * pool's target is visible before its first fill.
1287
+ *
1288
+ * The warm pools themselves are NOT started here — see
1289
+ * {@link startWarmPools}, which the host calls once every dependency a fill
1290
+ * can reach is wired.
1253
1291
  */
1254
1292
  start(): void;
1293
+ /**
1294
+ * Run the warm pools' first deficit pass and arm their periodic tick.
1295
+ *
1296
+ * Separate from {@link start} because the first pass spawns immediately,
1297
+ * and on an event backend a spawn emits through the event emitter — which
1298
+ * the coordinator assigns after the scaler is built. The coordinator calls
1299
+ * this once the emitter exists; the worker, which hosts no event backend,
1300
+ * calls it right after `start()`.
1301
+ */
1302
+ startWarmPools(): void;
1255
1303
  /**
1256
1304
  * Provision/heal every backend's host prerequisites before spawning starts.
1257
1305
  * Awaits each backend's optional ensureHostReady, catching per-backend so one
@@ -1352,6 +1400,7 @@ export declare class ScalerManager {
1352
1400
  * coord boot or Raft leader switch. Reconstructs:
1353
1401
  *
1354
1402
  * - `spawningAgents` (with `boundJobId` preserved for eager-dispatch on register)
1403
+ * — event-backend rows only; a local-backend row is dropped, see below
1355
1404
  * - `agentJobCorrelation` (so scaler-lifecycle events route correctly)
1356
1405
  * - `reservations` + `perScalerUsage` (so the cap-check critical
1357
1406
  * section reflects the cluster-wide truth, not the local empty
@@ -374,8 +374,21 @@ export declare class ScalerStateStore {
374
374
  */
375
375
  export interface ScalerStateRecovery {
376
376
  spawningAgentsRehydrated: number;
377
+ /**
378
+ * Spawn rows released during recovery because their backend is local
379
+ * (container / firecracker / bare-metal): such a spawn cannot outlive the
380
+ * process that started it, so nothing could ever register against it, and
381
+ * rehydrating it would hold its reservation until the stale prune.
382
+ */
383
+ spawningDropped: number;
377
384
  agentJobsRehydrated: number;
378
385
  reservationsRehydrated: number;
386
+ /**
387
+ * Reservation rows discarded during recovery because no rehydrated spawn
388
+ * backs them. Counted separately from `reservationsRehydrated` so a
389
+ * deployment carrying orphaned rows says so on every boot until it is clean.
390
+ */
391
+ reservationsDropped: number;
379
392
  bufferedEventsLost: number;
380
393
  }
381
394
  /**