@kici-dev/orchestrator 0.1.15 → 0.1.16

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 (54) hide show
  1. package/dist/app.d.ts +25 -0
  2. package/dist/approvals/apply-decision.d.ts +60 -0
  3. package/dist/approvals/approval-resolver.d.ts +66 -0
  4. package/dist/approvals/step-approval-bridge.d.ts +72 -0
  5. package/dist/approvals/team-membership-lookup.d.ts +13 -0
  6. package/dist/cache/user-cache.d.ts +1 -1
  7. package/dist/cli/api-client.d.ts +24 -0
  8. package/dist/cli/commands/firecracker/index.d.ts +11 -0
  9. package/dist/cli/commands/firecracker/provision.d.ts +13 -0
  10. package/dist/cli/commands/firecracker/teardown.d.ts +3 -0
  11. package/dist/cli/commands/firecracker/verify.d.ts +3 -0
  12. package/dist/cli/commands/scaler.d.ts +18 -0
  13. package/dist/cli/kici-admin.d.ts +10 -1
  14. package/dist/cli/service/image-digests.d.ts +21 -0
  15. package/dist/cli.js +3692 -411
  16. package/dist/cluster/peer-client.d.ts +23 -1
  17. package/dist/cluster/peer-handler.d.ts +9 -1
  18. package/dist/db/migrations/033_org_settings_approval.d.ts +19 -0
  19. package/dist/db/migrations/034_held_runs_generalize.d.ts +24 -0
  20. package/dist/db/types.d.ts +52 -1
  21. package/dist/diagnostics/bundle-writer.d.ts +1 -17
  22. package/dist/diagnostics/checks/firecracker-network.d.ts +13 -0
  23. package/dist/diagnostics/checks/index.d.ts +2 -1
  24. package/dist/diagnostics/fleet-collector.d.ts +52 -0
  25. package/dist/diagnostics/fleet-constants.d.ts +8 -0
  26. package/dist/diagnostics/fleet-selection.d.ts +15 -0
  27. package/dist/diagnostics/fleet-topology.d.ts +47 -0
  28. package/dist/diagnostics/fleet-wiring.d.ts +60 -0
  29. package/dist/environments/held-runs.d.ts +69 -1
  30. package/dist/firecracker/host-network.d.ts +83 -0
  31. package/dist/firecracker/persist.d.ts +14 -0
  32. package/dist/index.js +24 -6
  33. package/dist/orchestrator-core.d.ts +9 -1
  34. package/dist/pipeline/processor.d.ts +22 -0
  35. package/dist/routes/fleet.d.ts +20 -0
  36. package/dist/scaler/config.d.ts +4 -2
  37. package/dist/scaler/disk-guard.d.ts +27 -0
  38. package/dist/scaler/firecracker-backend.d.ts +18 -0
  39. package/dist/scaler/manager.d.ts +6 -0
  40. package/dist/scaler/nftables.d.ts +4 -0
  41. package/dist/scaler/reap-orphans.d.ts +27 -0
  42. package/dist/scaler/types.d.ts +2 -0
  43. package/dist/server.js +3205 -1214
  44. package/dist/stale-detector/stale-run-detector.d.ts +18 -0
  45. package/dist/standalone.js +2585 -1062
  46. package/dist/storage/filesystem.d.ts +2 -1
  47. package/dist/storage/s3.d.ts +2 -1
  48. package/dist/storage/types.d.ts +7 -0
  49. package/dist/ws/agent-handler.d.ts +32 -0
  50. package/dist/ws/dashboard-env-handler.d.ts +35 -0
  51. package/dist/ws/fleet-agent-collector.d.ts +23 -0
  52. package/installer-image-digests.json +7 -0
  53. package/package.json +6 -5
  54. package/sbom.spdx.json +62 -57
@@ -0,0 +1,14 @@
1
+ import { type FirecrackerBridgeConfig } from './host-network.js';
2
+ export declare const BOOT_SCRIPT_DIR = "/usr/local/lib/kici-firecracker";
3
+ export declare const SYSTEMD_DIR = "/etc/systemd/system";
4
+ export declare function persistUnitName(bridgeName: string): string;
5
+ export declare function renderPersistUnit(bridgeName: string): string;
6
+ /**
7
+ * Install the boot script + unit and `systemctl enable` it (enable only — the
8
+ * deploy/CLI provisions the bridge live in the same run, so the unit fires on
9
+ * the NEXT boot rather than re-churning nft now).
10
+ */
11
+ export declare function installPersist(cfg: FirecrackerBridgeConfig, opts?: {
12
+ requireSudo?: boolean;
13
+ }): Promise<void>;
14
+ //# sourceMappingURL=persist.d.ts.map
package/dist/index.js CHANGED
@@ -233,6 +233,9 @@ var S3CacheStorage = class {
233
233
  items.sort((a, b) => b.created - a.created);
234
234
  return items.map((i) => i.key);
235
235
  }
236
+ async getMetadata(key) {
237
+ return this.readMeta(key);
238
+ }
236
239
  async copy(srcKey, destKey) {
237
240
  const srcObj = this.objectKey(srcKey);
238
241
  const destObj = this.objectKey(destKey);
@@ -445,6 +448,9 @@ var FilesystemCacheStorage = class {
445
448
  entries.sort((a, b) => b.mtime - a.mtime);
446
449
  return entries.map((e) => e.key);
447
450
  }
451
+ async getMetadata(key) {
452
+ return this.readMeta(key);
453
+ }
448
454
  async copy(srcKey, destKey) {
449
455
  const srcPath = this.resolvePath(srcKey);
450
456
  const destPath = this.resolvePath(destKey);
@@ -741,9 +747,10 @@ var DepCache = class {
741
747
  * delete the temp), so a crashed save never leaves a corrupt final entry.
742
748
  *
743
749
  * Eviction: per-org byte quota plus the TTL the backing CacheStorage already
744
- * enforces lazily on access. On a save that pushes the org over quota, oldest
745
- * entries (by createdAt) are evicted until under quota; each eviction is
746
- * logged. The companion `.hash` / `.size` sidecar objects carry the integrity
750
+ * enforces lazily on access. On a save that pushes the org over quota,
751
+ * least-recently-used entries (oldest `lastAccessedAt`, read from the storage
752
+ * backend's metadata) are evicted until under quota; each eviction is logged.
753
+ * The companion `.hash` / `.size` sidecar objects carry the integrity
747
754
  * hash and size accounting outside the tarball's own (presigned, metadata-less)
748
755
  * upload.
749
756
  */
@@ -923,7 +930,7 @@ var UserCache = class {
923
930
  async readHash(key) {
924
931
  return (await this.storage.get(`${key}.hash`))?.toString("utf-8") || void 0;
925
932
  }
926
- /** Evict oldest entries for the org until total tarball size <= the per-org quota. */
933
+ /** Evict least-recently-used entries for the org until total tarball size <= the per-org quota. */
927
934
  async enforceQuota(ref) {
928
935
  const { quotaBytes } = await this.resolveLimits(ref.org);
929
936
  const orgPrefix = this.orgPrefix(ref);
@@ -940,8 +947,19 @@ var UserCache = class {
940
947
  });
941
948
  }
942
949
  if (total <= quotaBytes) return;
943
- for (let i = sized.length - 1; i >= 0 && total > quotaBytes; i--) {
944
- const { key, size } = sized[i];
950
+ const candidates = [];
951
+ for (const e of sized) {
952
+ const meta = await this.storage.getMetadata(e.key);
953
+ const lastAccessed = meta ? new Date(meta.lastAccessedAt).getTime() : 0;
954
+ candidates.push({
955
+ key: e.key,
956
+ size: e.size,
957
+ lastAccessed
958
+ });
959
+ }
960
+ candidates.sort((a, b) => a.lastAccessed - b.lastAccessed);
961
+ for (const { key, size } of candidates) {
962
+ if (total <= quotaBytes) break;
945
963
  await this.storage.delete(key);
946
964
  await this.storage.delete(`${key}.hash`);
947
965
  await this.storage.delete(`${key}.size`);
@@ -22,7 +22,7 @@ import { DedupCache } from './webhook/dedup.js';
22
22
  import { ObserverRegistry } from './ws/observer-registry.js';
23
23
  import { AgentMetricsAggregator } from './metrics/agent-metrics-aggregator.js';
24
24
  import { SourceLocationStore } from './app.js';
25
- import { type PeerHeartbeat } from '@kici-dev/engine';
25
+ import { type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
26
26
  import { ScalerManager } from './scaler/index.js';
27
27
  import type { ScalerConfig } from './scaler/index.js';
28
28
  import type { CacheStorage } from './storage/index.js';
@@ -146,6 +146,14 @@ export interface OrchestratorSubsystems {
146
146
  * store. the metrics enforcement plan.
147
147
  */
148
148
  agentMetricsAggregator: AgentMetricsAggregator;
149
+ /**
150
+ * Peer-side fleet collect responder. On an inbound peer.logs.collect.request,
151
+ * assembles this node's subtree and streams it back. Wired into every outgoing
152
+ * PeerClient (the incoming peer-handler is wired directly in setupCluster).
153
+ * Armed after the fleet runtime is built, so calls before that resolve to a
154
+ * no-op.
155
+ */
156
+ fleetCollectResponder: (msg: PeerLogsCollectRequest, send: (out: PeerToPeerMessage) => boolean) => Promise<void>;
149
157
  }
150
158
  /**
151
159
  * Mode-specific hooks injected by each entry point.
@@ -30,9 +30,11 @@ import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
30
30
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
31
31
  import type { AgentRegistry } from '../agent/registry.js';
32
32
  import type { RunCoordinator } from '../cluster/coordinator.js';
33
+ import type { TeamMembershipLookup } from '../approvals/team-membership-lookup.js';
33
34
  import type { LogStorage } from '../reporting/log-storage.js';
34
35
  import type { SecretResolver } from '../secrets/secret-resolver.js';
35
36
  import type { ContributorCache } from '../security/contributor-cache.js';
37
+ import type { AccessLogWriter } from '../audit/access-log.js';
36
38
  import type { LockFile as FullLockFile, LockWorkflow, SimulatedEvent, WebhookNormalizer } from '@kici-dev/engine';
37
39
  import { LockFileParseError } from '@kici-dev/engine';
38
40
  import type { EventRouter } from '../events/event-router.js';
@@ -213,6 +215,17 @@ export declare function extractCommitMessage(event: string, payload: unknown): s
213
215
  * Build a human-readable summary for a security hold check.
214
216
  */
215
217
  export declare function buildSecurityHoldSummary(reason: string, tier: string, contributorUsername?: string): string;
218
+ /**
219
+ * Build the pending check-run description for a job/workflow approval hold,
220
+ * naming the clauses an approver must satisfy. `{team:X}` renders as
221
+ * "team X", `{user:Y}` as "user Y". An empty clause list (any eligible
222
+ * reviewer satisfies the hold) falls back to a generic message.
223
+ */
224
+ export declare function summarizeApprovalClauses(clauses: ReadonlyArray<{
225
+ team: string;
226
+ } | {
227
+ user: string;
228
+ }>): string;
216
229
  /**
217
230
  * Dependencies for the processing pipeline.
218
231
  * All injected for testability. Fully provider-agnostic.
@@ -283,6 +296,12 @@ export interface ProcessingDeps {
283
296
  identityLinks?: IdentityLink[];
284
297
  /** ci_trust permission levels per user ID from Platform push. Optional -- defaults to empty. */
285
298
  orgMemberPermissions?: Map<string, PermissionLevel>;
299
+ /**
300
+ * Team-membership lookup pushed from the Platform (team name → member set).
301
+ * Consumed by the approval resolver to satisfy `{team}` approval clauses.
302
+ * Optional -- defaults to "no teams".
303
+ */
304
+ teamMembershipLookup?: TeamMembershipLookup;
286
305
  /** Global workflow policy for org-level permission enforcement. Optional -- if not set, global workflows are unrestricted. */
287
306
  globalWorkflowPolicy?: GlobalWorkflowPolicy;
288
307
  /** Inbound webhook delivery log writer. Optional -- if not set, deliveries are not persisted to event_log. */
@@ -296,6 +315,9 @@ export interface ProcessingDeps {
296
315
  * created in server.ts and threaded through both the Platform-relay WS
297
316
  * path and the generic webhook HTTP path. */
298
317
  contributorCache?: ContributorCache;
318
+ /** Access-log writer for the orchestrator audit stream. Optional -- if not
319
+ * set, hold-creation audit rows (`held_run.request`) are skipped. */
320
+ accessLogWriter?: AccessLogWriter;
299
321
  }
300
322
  /**
301
323
  * Check if any trigger in the lock file workflows uses path filters.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Fleet log-collection admin routes.
3
+ *
4
+ * GET /admin/fleet-topology — enumerate the cluster (no fan-out) for
5
+ * `debug-bundle --fleet --list` / `--pick`.
6
+ * POST /admin/fleet-bundle — drive the recursive fan-out and stream the
7
+ * assembled nested ZIP back as an octet-stream.
8
+ *
9
+ * Both are protected by the same Bearer admin-token auth as the other admin
10
+ * routes (token-manager validation).
11
+ */
12
+ import { Hono } from 'hono';
13
+ import type { TokenManager } from '../secrets/token-manager.js';
14
+ import type { FleetRoutesDeps } from '../app.js';
15
+ export interface FleetAdminRouteDeps {
16
+ fleet: FleetRoutesDeps;
17
+ tokenManager: TokenManager;
18
+ }
19
+ export declare function createFleetRoutes(deps: FleetAdminRouteDeps): Hono;
20
+ //# sourceMappingURL=fleet.d.ts.map
@@ -89,6 +89,7 @@ export declare const firecrackerNetworkSchema: z.ZodObject<{
89
89
  bridgeName: z.ZodDefault<z.ZodString>;
90
90
  gateway: z.ZodDefault<z.ZodString>;
91
91
  netmask: z.ZodDefault<z.ZodString>;
92
+ table: z.ZodDefault<z.ZodString>;
92
93
  }, z.core.$strip>;
93
94
  /**
94
95
  * Zod schema for the complete scaler configuration file.
@@ -241,10 +242,10 @@ export declare const scalerFileSchema: z.ZodObject<{
241
242
  }, z.core.$strip>>;
242
243
  mandatoryLabels: z.ZodDefault<z.ZodArray<z.ZodString>>;
243
244
  roles: z.ZodPipe<z.ZodOptional<z.ZodArray<z.ZodEnum<{
245
+ all: "all";
244
246
  builder: "builder";
245
247
  "init-runner": "init-runner";
246
- all: "all";
247
- }>>>, z.ZodTransform<string[] | undefined, ("builder" | "init-runner" | "all")[] | undefined>>;
248
+ }>>>, z.ZodTransform<string[] | undefined, ("all" | "builder" | "init-runner")[] | undefined>>;
248
249
  resourceCap: z.ZodOptional<z.ZodPipe<z.ZodObject<{
249
250
  maxCpu: z.ZodOptional<z.ZodNumber>;
250
251
  maxMemory: z.ZodOptional<z.ZodString>;
@@ -272,6 +273,7 @@ export declare const scalerFileSchema: z.ZodObject<{
272
273
  bridgeName: z.ZodDefault<z.ZodString>;
273
274
  gateway: z.ZodDefault<z.ZodString>;
274
275
  netmask: z.ZodDefault<z.ZodString>;
276
+ table: z.ZodDefault<z.ZodString>;
275
277
  }, z.core.$strip>>;
276
278
  }, z.core.$strip>;
277
279
  /**
@@ -0,0 +1,27 @@
1
+ import { type ReapCounts } from './reap-orphans.js';
2
+ import type { ScalerConfig } from './index.js';
3
+ /** Default minimum free space before startup is allowed to proceed: 1 GiB. */
4
+ export declare const DEFAULT_DISK_GUARD_THRESHOLD_BYTES: number;
5
+ export interface DiskGuardResult {
6
+ reaped: boolean;
7
+ recovered: boolean;
8
+ freeBytesAfter: number;
9
+ }
10
+ interface DiskGuardOpts {
11
+ scalerConfig: ScalerConfig;
12
+ thresholdBytes?: number;
13
+ /** Injectable for tests. */
14
+ statfsFn?: (path: string) => Promise<{
15
+ bavail: bigint;
16
+ bsize: number;
17
+ }>;
18
+ reapFn?: (cfg: ScalerConfig) => Promise<ReapCounts>;
19
+ }
20
+ /**
21
+ * Run the disk guard. Returns reaped/recovered status; the caller decides
22
+ * whether to proceed or exit with backoff. No-ops (recovered=true) when there
23
+ * is no FC scaler or free space is already above threshold.
24
+ */
25
+ export declare function runDiskGuard(opts: DiskGuardOpts): Promise<DiskGuardResult>;
26
+ export {};
27
+ //# sourceMappingURL=disk-guard.d.ts.map
@@ -61,10 +61,14 @@ export interface FirecrackerScalerBackendOptions {
61
61
  memSizeMib?: number;
62
62
  /** Bridge interface name for TAP attachment */
63
63
  bridgeName: string;
64
+ /** Network CIDR (e.g. '10.0.0.0/24'); supplies the prefix for the bridge gateway CIDR. */
65
+ cidr?: string;
64
66
  /** Gateway IP for guest networking */
65
67
  gateway: string;
66
68
  /** Netmask for guest networking */
67
69
  netmask: string;
70
+ /** nft table name for host-network diagnostics. @default 'kici' */
71
+ table?: string;
68
72
  /** Token store for creating ephemeral agent auth tokens. Optional -- when undefined, no token is injected. */
69
73
  tokenStore?: AgentTokenStore;
70
74
  /** TTL for ephemeral agent tokens in ms. Default: 1 hour. */
@@ -99,8 +103,10 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
99
103
  private readonly vcpuCount;
100
104
  private readonly memSizeMib;
101
105
  private readonly bridgeName;
106
+ private readonly cidr;
102
107
  private readonly gateway;
103
108
  private readonly netmask;
109
+ private readonly table;
104
110
  private readonly tokenStore?;
105
111
  private readonly tokenTtlMs;
106
112
  private readonly roles;
@@ -120,6 +126,18 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
120
126
  getActiveCount(): number;
121
127
  spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits): Promise<ManagedAgent>;
122
128
  getScalerContext(agentId: string): Record<string, unknown> | undefined;
129
+ /**
130
+ * Bridge config for host-network diagnostics (read-only snapshot).
131
+ * `bridgeCidr` is the gateway IP with the network prefix (e.g. '10.0.0.1/24'),
132
+ * which is exactly what `provisionBridge`/`verifyBridge` consume. The prefix
133
+ * comes from the configured network `cidr`; if absent, it is derived from the
134
+ * dotted `netmask`.
135
+ */
136
+ getBridgeConfig(): {
137
+ bridgeName: string;
138
+ bridgeCidr: string;
139
+ table: string;
140
+ };
123
141
  destroy(managedId: string): Promise<void>;
124
142
  shutdownAll(): Promise<void>;
125
143
  /**
@@ -336,6 +336,12 @@ export declare class ScalerManager {
336
336
  * Returns null if the agent is not scaler-managed (standalone).
337
337
  */
338
338
  getBackendForAgent(agentId: string): string | null;
339
+ /**
340
+ * Get a backend instance by scaler name. Returns undefined for an unknown
341
+ * name. Used by diagnostics to reach backend-specific accessors (e.g. the
342
+ * Firecracker backend's `getBridgeConfig()`).
343
+ */
344
+ getBackend(name: string): ScalerBackend | undefined;
339
345
  /**
340
346
  * Get scaler-specific configuration metadata for a managed agent.
341
347
  * Returns undefined for non-scaler-managed (static) agents.
@@ -17,6 +17,10 @@ import type { NetworkPolicy } from './types.js';
17
17
  * - 'saddr': Match on source IP address (container backends)
18
18
  */
19
19
  export type NftMatchMode = 'iifname' | 'saddr';
20
+ /** RFC1918 private address ranges. */
21
+ export declare const RFC1918_RANGES: string[];
22
+ /** Cloud metadata service range (AWS/GCP/Azure link-local). */
23
+ export declare const METADATA_RANGE = "169.254.0.0/16";
20
24
  /**
21
25
  * Options for nft command execution.
22
26
  */
@@ -0,0 +1,27 @@
1
+ import type { ScalerConfig } from './index.js';
2
+ /** Per-scaler orphan counts, keyed by scaler name. */
3
+ export type ReapCounts = Record<string, number>;
4
+ /**
5
+ * Reap orphan Firecracker resources (dead-VM chroots + TAPs) for every
6
+ * `firecracker` scaler in the config. Safe to run alongside a live
7
+ * orchestrator: the reap is liveness-driven and only touches dead VMs.
8
+ */
9
+ export declare function reapFirecrackerOrphans(scalerConfig: ScalerConfig): Promise<ReapCounts>;
10
+ /**
11
+ * Reap orphan container resources for every `container` scaler. Unlike the FC
12
+ * reap, container cleanup is unconditional (it removes every kici-managed
13
+ * container), so callers MUST ensure the orchestrator is NOT running before
14
+ * invoking this — otherwise live agents are killed. The CLI command enforces
15
+ * this via the /health gate.
16
+ */
17
+ export declare function reapContainerOrphans(scalerConfig: ScalerConfig): Promise<ReapCounts>;
18
+ /**
19
+ * Reap orphans for all supported backends. Firecracker is always reaped
20
+ * (liveness-driven, safe). Containers are reaped only when includeContainers
21
+ * is set (caller has confirmed the orchestrator is down).
22
+ */
23
+ export declare function reapAllOrphans(opts: {
24
+ scalerConfig: ScalerConfig;
25
+ includeContainers: boolean;
26
+ }): Promise<ReapCounts>;
27
+ //# sourceMappingURL=reap-orphans.d.ts.map
@@ -248,6 +248,8 @@ export interface FirecrackerNetworkConfig {
248
248
  gateway?: string;
249
249
  /** Subnet mask for guest networking @default '255.255.255.0' */
250
250
  netmask?: string;
251
+ /** nft table name for this coordinator's host bridge @default 'kici' */
252
+ table?: string;
251
253
  }
252
254
  /**
253
255
  * Parsed and validated scaler configuration from YAML.