@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
@@ -10,7 +10,7 @@
10
10
  * Authentication uses ECDH key exchange followed by join token (first connect)
11
11
  * or HMAC credential proof (reconnection).
12
12
  */
13
- import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
13
+ import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
14
14
  import type { PeerRegistry } from './peer-registry.js';
15
15
  type PeerConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'authenticating' | 'connected';
16
16
  export interface PeerClientOptions {
@@ -78,7 +78,20 @@ export interface PeerClientOptions {
78
78
  * Platform-mediated discovery dedupes against the same client.
79
79
  */
80
80
  onAuthenticated?: (targetInstanceId: string) => void;
81
+ /**
82
+ * Callback when a peer.logs.collect.request is received from the peer. Builds
83
+ * this node's subtree bundle and streams it back via the supplied `send`
84
+ * (peer.logs.collect.chunk frames, or a peer.logs.collect.error on failure).
85
+ * If undefined, incoming collect requests are ignored.
86
+ */
87
+ onLogsCollectRequest?: PeerLogsCollectResponder;
81
88
  }
89
+ /**
90
+ * Builds a node's subtree bundle in response to a peer.logs.collect.request and
91
+ * streams it back through `send`. Shared by PeerClient (outgoing-dialed peers)
92
+ * and the peer handler (incoming-dialed peers).
93
+ */
94
+ export type PeerLogsCollectResponder = (msg: PeerLogsCollectRequest, send: (out: PeerToPeerMessage) => boolean) => Promise<void>;
82
95
  export declare class PeerClient {
83
96
  private ws;
84
97
  private _state;
@@ -91,6 +104,8 @@ export declare class PeerClient {
91
104
  private readonly ackWaiters;
92
105
  private readonly cacheWaiters;
93
106
  private readonly configReloadWaiters;
107
+ /** Correlates peer.logs.collect.request with the peer's chunked subtree response. */
108
+ private readonly logsCollectWaiters;
94
109
  private readonly url;
95
110
  private readonly joinToken?;
96
111
  private readonly credentialFile;
@@ -112,6 +127,7 @@ export declare class PeerClient {
112
127
  private readonly onAgentTokenRevoke?;
113
128
  private readonly onPeerConfigReload?;
114
129
  private readonly onAuthenticated?;
130
+ private readonly onLogsCollectRequest?;
115
131
  constructor(options: PeerClientOptions);
116
132
  /** Current connection state. */
117
133
  get state(): PeerConnectionState;
@@ -150,6 +166,12 @@ export declare class PeerClient {
150
166
  * if the peer doesn't reply within the timeout.
151
167
  */
152
168
  sendConfigReloadAndWait(msg: PeerConfigReload, timeoutMs?: number): Promise<PeerConfigReloadResponse | null>;
169
+ /**
170
+ * Send a peer.logs.collect.request to the connected peer and await its
171
+ * reassembled subtree-bundle ZIP. Rejects on timeout, an error frame, or
172
+ * peer disconnect.
173
+ */
174
+ sendLogsCollectAndWait(msg: PeerLogsCollectRequest, timeoutMs: number): Promise<Buffer>;
153
175
  /**
154
176
  * Send a cache upload request to the coordinator and wait for a response
155
177
  * with a pre-signed URL.
@@ -6,7 +6,7 @@
6
6
  * proof (reconnection), registers the peer in PeerRegistry, and routes
7
7
  * messages bidirectionally. Sends periodic heartbeats to the connecting peer.
8
8
  */
9
- import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
9
+ import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
10
10
  import type { PeerRegistry } from './peer-registry.js';
11
11
  import type { PeerCredentialStore } from './peer-credentials.js';
12
12
  import { type JoinTokenManager } from './join-token.js';
@@ -78,6 +78,13 @@ export interface PeerHandlerDeps {
78
78
  restartRequired?: string[];
79
79
  fieldsChanged?: string[];
80
80
  }>;
81
+ /**
82
+ * Callback when a peer.logs.collect.request arrives from an incoming-dialed
83
+ * peer. Builds this node's subtree bundle and streams it back through `send`
84
+ * (peer.logs.collect.chunk frames, or a peer.logs.collect.error on failure).
85
+ * If undefined, incoming collect requests are ignored.
86
+ */
87
+ onLogsCollectRequest?: (msg: PeerLogsCollectRequest, send: (out: PeerToPeerMessage) => boolean) => Promise<void>;
81
88
  }
82
89
  /**
83
90
  * Create a handler function for incoming peer WebSocket connections.
@@ -90,6 +97,7 @@ export declare function createPeerHandler(deps: PeerHandlerDeps): {
90
97
  sendToPeer: (targetInstanceId: string, msg: PeerToPeerMessage) => boolean;
91
98
  sendAndWaitAck: (targetInstanceId: string, msg: JobReroute, timeoutMs?: number) => Promise<boolean>;
92
99
  sendConfigReloadAndWait: (targetInstanceId: string, msg: PeerConfigReload, timeoutMs?: number) => Promise<PeerConfigReloadResponse | null>;
100
+ sendLogsCollectAndWait: (targetInstanceId: string, msg: PeerLogsCollectRequest, timeoutMs: number) => Promise<Buffer>;
93
101
  getConnectionCount: () => number;
94
102
  broadcastHeartbeat: (inventory: Omit<PeerHeartbeat, "type">) => void;
95
103
  broadcastAgentTokenRevoke: (msg: PeerAgentTokenRevoke) => void;
@@ -0,0 +1,19 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add the two approval-policy columns to `org_settings`:
4
+ *
5
+ * - `approval_expiry_seconds INTEGER NOT NULL DEFAULT 86400` — how long a held
6
+ * approval element waits before it expires (and its run/job/step is
7
+ * rejected). One day by default. An SDK `requireApproval` `timeout` overrides
8
+ * this per element; otherwise this per-org value applies.
9
+ * - `allow_self_approval BOOLEAN NOT NULL DEFAULT true` — whether the user who
10
+ * triggered a run may also approve its held elements. Operators turn it off
11
+ * to enforce four-eyes review.
12
+ *
13
+ * Both are cluster-configurable per org via `kici-admin org-settings approval`
14
+ * and the orchestrator admin route. Idempotent: a re-run on a DB that already
15
+ * has the columns is a no-op (each column is guarded independently).
16
+ */
17
+ export declare function up(db: Kysely<unknown>): Promise<void>;
18
+ export declare function down(db: Kysely<unknown>): Promise<void>;
19
+ //# sourceMappingURL=033_org_settings_approval.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Generalize `held_runs` from an environment-only hold into the unified
4
+ * "held element" model that backs per-element approvals, and add the
5
+ * `held_run_approvals` table that records each approver's decision.
6
+ *
7
+ * New `held_runs` columns (all idempotent, column-exists guarded):
8
+ * - `hold_scope text NOT NULL DEFAULT 'job'` — 'workflow' | 'job' | 'step'
9
+ * (engine `HoldScope`). Existing rows held a single job, so they default to
10
+ * 'job'.
11
+ * - `step_index integer` — nullable; set only for step-scoped holds.
12
+ * - `trigger_source text NOT NULL DEFAULT 'environment'` — 'environment' |
13
+ * 'explicit' (engine `TriggerSource`). Existing holds came from environment
14
+ * protection, so they default to 'environment'.
15
+ * - `approval_requirement jsonb` — the normalized `ApprovalRequirement`
16
+ * (clauses + expiresAt + reason) the hold must satisfy. Nullable for legacy
17
+ * rows that predate the approval model.
18
+ *
19
+ * New `held_run_approvals` table: one row per approver decision, FK to
20
+ * `held_runs.id` (uuid) with ON DELETE CASCADE.
21
+ */
22
+ export declare function up(db: Kysely<unknown>): Promise<void>;
23
+ export declare function down(db: Kysely<unknown>): Promise<void>;
24
+ //# sourceMappingURL=034_held_runs_generalize.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';
2
- import type { InitFailure } from '@kici-dev/engine';
2
+ import type { ApprovalRequirement, ApproverClause, InitFailure } from '@kici-dev/engine';
3
3
  /**
4
4
  * PostgreSQL-only database types.
5
5
  * Column names use snake_case matching the actual database column names.
@@ -19,6 +19,7 @@ export interface Database {
19
19
  environment_variables: EnvironmentVariablesTable;
20
20
  environment_source_overrides: EnvironmentSourceOverridesTable;
21
21
  held_runs: HeldRunsTable;
22
+ held_run_approvals: HeldRunApprovalsTable;
22
23
  admin_tokens: AdminTokenTable;
23
24
  agent_tokens: AgentTokenTable;
24
25
  config_versions: ConfigVersionTable;
@@ -630,10 +631,49 @@ export interface HeldRunsTable {
630
631
  expires_at: Date;
631
632
  /** When this hold was resolved */
632
633
  resolved_at: Date | null;
634
+ /**
635
+ * Hold granularity: 'workflow' | 'job' | 'step' (engine `HoldScope`).
636
+ * Existing environment holds are job-scoped, hence the 'job' default.
637
+ */
638
+ hold_scope: Generated<string>;
639
+ /** Step index within the job for step-scoped holds; null otherwise. */
640
+ step_index: number | null;
641
+ /**
642
+ * What created the hold: 'environment' (mandatory env policy) | 'explicit'
643
+ * (SDK `requireApproval`). Engine `TriggerSource`.
644
+ */
645
+ trigger_source: Generated<string>;
646
+ /**
647
+ * Normalized `ApprovalRequirement` (clauses + expiresAt + reason) the hold
648
+ * must satisfy. Null for legacy rows that predate the approval model.
649
+ */
650
+ approval_requirement: ColumnType<ApprovalRequirement | null, ApprovalRequirement | string | null | undefined, ApprovalRequirement | string | null>;
633
651
  }
634
652
  export type HeldRun = Selectable<HeldRunsTable>;
635
653
  export type NewHeldRun = Insertable<HeldRunsTable>;
636
654
  export type HeldRunUpdate = Updateable<HeldRunsTable>;
655
+ /**
656
+ * One approver's recorded decision on a held element. Multiple rows accumulate
657
+ * until the hold's `ApprovalRequirement` clauses are all satisfied (approve) or
658
+ * any single reject lands.
659
+ */
660
+ export interface HeldRunApprovalsTable {
661
+ /** UUID primary key */
662
+ id: Generated<string>;
663
+ /** FK to held_runs.id (ON DELETE CASCADE) */
664
+ held_run_id: string;
665
+ /** The approver's user id (Keycloak sub) */
666
+ approver_user_id: string;
667
+ /** 'approve' | 'reject' (engine `ApprovalDecision`) */
668
+ decision: string;
669
+ /** Which requirement clauses this decision satisfied (for attribution). */
670
+ clauses_satisfied: ColumnType<ApproverClause[] | null, ApproverClause[] | string | null | undefined, ApproverClause[] | string | null>;
671
+ /** When the decision was recorded */
672
+ created_at: Generated<Date>;
673
+ }
674
+ export type HeldRunApproval = Selectable<HeldRunApprovalsTable>;
675
+ export type NewHeldRunApproval = Insertable<HeldRunApprovalsTable>;
676
+ export type HeldRunApprovalUpdate = Updateable<HeldRunApprovalsTable>;
637
677
  /**
638
678
  * Secret audit log table
639
679
  * Immutable log of secret access and denial events.
@@ -1192,6 +1232,17 @@ export interface OrgSettingsTable {
1192
1232
  * BIGINT — pg returns a string on select; accept a number on insert/update.
1193
1233
  */
1194
1234
  dispatch_ack_timeout_ms: ColumnType<string | null, number | null | undefined, number | null>;
1235
+ /**
1236
+ * Per-org expiry (seconds) for a held approval element before it is rejected
1237
+ * and its run/job/step fails. NOT NULL, default 86400 (one day). An SDK
1238
+ * `requireApproval` `timeout` overrides this per element.
1239
+ */
1240
+ approval_expiry_seconds: ColumnType<number, number | undefined, number>;
1241
+ /**
1242
+ * Whether the user who triggered a run may also approve its held elements.
1243
+ * NOT NULL, default true. Operators turn it off to enforce four-eyes review.
1244
+ */
1245
+ allow_self_approval: ColumnType<boolean, boolean | undefined, boolean>;
1195
1246
  /** When this setting was created */
1196
1247
  created_at: Generated<Date>;
1197
1248
  /** When this setting was last updated */
@@ -6,8 +6,8 @@
6
6
  * This is the primary support tool -- operators run debug-bundle and share
7
7
  * the ZIP for troubleshooting.
8
8
  */
9
- import archiver from 'archiver';
10
9
  import type { DiagnosticDeps } from './types.js';
10
+ export { redactConfig, addLogsToArchive } from '@kici-dev/shared';
11
11
  export interface BundleOptions {
12
12
  /** Where to write the ZIP. */
13
13
  outputPath: string;
@@ -26,11 +26,6 @@ export interface BundleOptions {
26
26
  /** URL for recent runs endpoint. */
27
27
  recentRunsUrl?: string;
28
28
  }
29
- /**
30
- * Redact config values using allowlist approach.
31
- * Only known-safe fields are preserved; everything else becomes "****".
32
- */
33
- export declare function redactConfig(obj: unknown, parentKey?: string): unknown;
34
29
  /**
35
30
  * Create a debug bundle ZIP file.
36
31
  *
@@ -43,15 +38,4 @@ export declare function redactConfig(obj: unknown, parentKey?: string): unknown;
43
38
  * - logs/summary.json: log statistics
44
39
  */
45
40
  export declare function createDebugBundle(options: BundleOptions): Promise<string>;
46
- /**
47
- * Add log files from logDir to the archive, respecting MAX_LOG_BYTES cap
48
- * and the logWindow time filter. Matches any `*.log` file in the directory,
49
- * so the per-instance filename pattern produced by
50
- * `buildLogFilename()` is picked up without additional configuration.
51
- *
52
- * Exported for reuse by the `kici-admin debug-bundle` CLI command, which
53
- * runs outside the orchestrator process but still needs to include the
54
- * same log files in its locally-assembled bundle.
55
- */
56
- export declare function addLogsToArchive(archive: archiver.Archiver, logDir: string, logWindowHours: number): Promise<void>;
57
41
  //# sourceMappingURL=bundle-writer.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Firecracker host-bridge diagnostic. Emits one row per configured Firecracker
3
+ * scaler backend: the bridge interface + its gateway addr + its nft table must
4
+ * all be present, else the scaler cannot spawn microVMs.
5
+ */
6
+ import type { DiagnosticDeps, DiagnosticResult } from '../types.js';
7
+ import { type BridgeHealth, type FirecrackerBridgeConfig } from '../../firecracker/host-network.js';
8
+ interface CheckOpts {
9
+ verify?: (cfg: FirecrackerBridgeConfig) => Promise<BridgeHealth>;
10
+ }
11
+ export declare function checkFirecrackerNetwork(deps: DiagnosticDeps, opts?: CheckOpts): Promise<DiagnosticResult[]>;
12
+ export {};
13
+ //# sourceMappingURL=firecracker-network.d.ts.map
@@ -12,7 +12,8 @@ import { checkDiskSpace } from './disk.js';
12
12
  import { checkConfigValidity } from './config.js';
13
13
  import { checkCertificateExpiry } from './certs.js';
14
14
  import { checkScalerProvisioning } from './scaler.js';
15
+ import { checkFirecrackerNetwork } from './firecracker-network.js';
15
16
  /** All diagnostic checks in display order. */
16
17
  export declare const defaultChecks: DiagnosticCheck[];
17
- export { checkDbConnectivity, checkWsToPlatform, checkAgentConnectivity, checkDiskSpace, checkConfigValidity, checkCertificateExpiry, checkScalerProvisioning, };
18
+ export { checkDbConnectivity, checkWsToPlatform, checkAgentConnectivity, checkDiskSpace, checkConfigValidity, checkCertificateExpiry, checkScalerProvisioning, checkFirecrackerNetwork, };
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Recursive fleet subtree assembler.
3
+ *
4
+ * Every orchestrator runs this to produce a self-similar subtree ZIP:
5
+ * local/ + nested agents/<id>.zip + workers/<id>.zip + peers/<id>.zip, plus
6
+ * fleet-manifest.json recording per-node status. Dead branches are recorded,
7
+ * never fatal (Promise.allSettled at each level). The loop guard
8
+ * (includeCoordinatorMesh) is true only on the root call; every downstream peer
9
+ * request sets it false so the coordinator mesh never echoes.
10
+ */
11
+ import { z } from 'zod';
12
+ /** Per-node collection outcome recorded in the fleet manifest. */
13
+ export declare const FleetNodeStatus: z.ZodEnum<{
14
+ error: "error";
15
+ ok: "ok";
16
+ timeout: "timeout";
17
+ unreachable: "unreachable";
18
+ }>;
19
+ export type FleetNodeStatus = z.infer<typeof FleetNodeStatus>;
20
+ export interface FleetCollectorDeps {
21
+ /** This orchestrator's instanceId (the manifest owner). */
22
+ instanceId: string;
23
+ /** Build this node's own debug bundle as a Buffer (createDebugBundle output). */
24
+ buildLocalBundle: () => Promise<Buffer>;
25
+ /** List this node's directly-connected agents. */
26
+ listAgents: () => {
27
+ agentId: string;
28
+ }[];
29
+ /** Request a connected agent's mini-bundle over the agent WS channel. */
30
+ requestAgentBundle: (agentId: string) => Promise<Buffer>;
31
+ /** List this node's downstream peers (coordinator-mesh peers + its workers). */
32
+ listPeers: () => {
33
+ instanceId: string;
34
+ role: 'coordinator' | 'worker';
35
+ kind: 'peer' | 'worker';
36
+ }[];
37
+ /** Request a peer's subtree bundle over the peer WS channel (loop-guarded false). */
38
+ requestPeerSubtree: (instanceId: string, includeCoordinatorMesh: boolean) => Promise<Buffer>;
39
+ }
40
+ export interface FleetCollectOptions {
41
+ logWindowHours: number;
42
+ selection: {
43
+ all: boolean;
44
+ agentIds: string[];
45
+ workerInstanceIds: string[];
46
+ };
47
+ /** Loop guard. True only on the root call; downstream requests force false. */
48
+ includeCoordinatorMesh: boolean;
49
+ timeoutMs: number;
50
+ }
51
+ export declare function collectFleetSubtree(opts: FleetCollectOptions, deps: FleetCollectorDeps): Promise<Buffer>;
52
+ //# sourceMappingURL=fleet-collector.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared constants for fleet log collection.
3
+ */
4
+ /** Default per-node deadline for a fleet bundle request (overridable via --fleet-timeout). */
5
+ export declare const FLEET_NODE_TIMEOUT_MS = 60000;
6
+ /** Per-node cap on raw log bytes an agent includes in its mini-bundle (50 MiB). */
7
+ export declare const FLEET_MAX_LOG_BYTES: number;
8
+ //# sourceMappingURL=fleet-constants.d.ts.map
@@ -0,0 +1,15 @@
1
+ import type { FleetSelection } from '@kici-dev/engine';
2
+ import type { FleetTopology } from './fleet-topology.js';
3
+ /** A per-orchestrator selection, keyed by orchestrator instanceId. Absent = prune. */
4
+ export type ResolvedSelectionMap = Map<string, FleetSelection>;
5
+ /**
6
+ * Resolve `--pick` selectors into a per-orchestrator FleetSelection map.
7
+ *
8
+ * No selectors -> every orchestrator gets `{ all: true }`. With selectors, an
9
+ * orchestrator is included only if at least one of its agents/workers (or the
10
+ * orchestrator itself) matches; its FleetSelection then carries exactly the
11
+ * matched agent ids and worker instanceIds. Orchestrators with no matches are
12
+ * omitted from the map (their branch is pruned).
13
+ */
14
+ export declare function resolveSelection(topology: FleetTopology, selectors: string[]): ResolvedSelectionMap;
15
+ //# sourceMappingURL=fleet-selection.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Fleet topology enumeration.
3
+ *
4
+ * Builds the cluster tree for `debug-bundle --fleet --list` / `--pick` from the
5
+ * local agent registry plus the heartbeat-cached peer inventory — no fan-out.
6
+ * Each peer's cached agent list is already maintained by ~30s heartbeats, so
7
+ * enumeration is a cheap in-memory read.
8
+ */
9
+ /** A node in the enumerated fleet topology. */
10
+ export interface FleetTopologyNode {
11
+ kind: 'orchestrator' | 'agent';
12
+ id: string;
13
+ /** Cluster role for orchestrator nodes (coordinator | worker); undefined for agents. */
14
+ role?: 'coordinator' | 'worker';
15
+ hostname?: string;
16
+ labels: Record<string, string>;
17
+ /** Parent orchestrator instanceId; null for the collector (root) node. */
18
+ parentId: string | null;
19
+ }
20
+ export interface FleetTopology {
21
+ nodes: FleetTopologyNode[];
22
+ }
23
+ export interface FleetTopologyDeps {
24
+ /** This collector's own instanceId (the root orchestrator). */
25
+ instanceId: string;
26
+ /** This collector's role. */
27
+ role: 'coordinator' | 'worker';
28
+ /** This collector's hostname, if known. */
29
+ hostname?: string;
30
+ /** This node's directly-connected agents. */
31
+ listLocalAgents: () => {
32
+ agentId: string;
33
+ labels: string[];
34
+ }[];
35
+ /** The heartbeat-cached peer inventory (each peer + its cached agents). */
36
+ listPeers: () => {
37
+ instanceId: string;
38
+ role: 'coordinator' | 'worker';
39
+ hostname?: string;
40
+ agents: {
41
+ agentId: string;
42
+ labels: string[];
43
+ }[];
44
+ }[];
45
+ }
46
+ export declare function buildFleetTopology(deps: FleetTopologyDeps): FleetTopology;
47
+ //# sourceMappingURL=fleet-topology.d.ts.map
@@ -0,0 +1,60 @@
1
+ import type { PeerLogsCollectRequest, PeerToPeerMessage, FleetSelection } from '@kici-dev/engine';
2
+ import type { AgentRegistry } from '../agent/registry.js';
3
+ import type { PeerRegistry } from '../cluster/peer-registry.js';
4
+ import type { PeerClient } from '../cluster/peer-client.js';
5
+ import type { DiagnosticDeps } from './types.js';
6
+ import { type FleetCollectorDeps } from './fleet-collector.js';
7
+ import { type FleetTopology, type FleetTopologyDeps } from './fleet-topology.js';
8
+ /** A peer-handler-like object exposing the dual-direction collect send. */
9
+ export interface FleetPeerHandlerLike {
10
+ sendLogsCollectAndWait: (targetInstanceId: string, msg: PeerLogsCollectRequest, timeoutMs: number) => Promise<Buffer>;
11
+ }
12
+ /** A collector that issues a fleet.logs.request to an agent and awaits its bundle. */
13
+ export interface FleetAgentRequester {
14
+ request: (requestId: string, agentId: string, send: () => void) => Promise<Buffer>;
15
+ }
16
+ export interface FleetRuntime {
17
+ instanceId: string;
18
+ role: 'coordinator' | 'worker';
19
+ /** Loop window for log files (hours). */
20
+ logWindowHours: number;
21
+ /** Per-node deadline (ms). */
22
+ timeoutMs: number;
23
+ /** Local agent log directory (KICI_LOG_DIR), if configured. */
24
+ logDir?: string;
25
+ agentRegistry: AgentRegistry;
26
+ peerRegistry: PeerRegistry;
27
+ fleetAgentCollector: FleetAgentRequester;
28
+ /** Outgoing peer clients, keyed by instanceId. */
29
+ peerClients: Map<string, PeerClient>;
30
+ /** Incoming peer-handler with the dual-direction collect send. */
31
+ peerHandler: FleetPeerHandlerLike;
32
+ /** Deps for the local createDebugBundle. */
33
+ diagnosticDeps: DiagnosticDeps;
34
+ /** Raw orchestrator config (redacted by createDebugBundle). */
35
+ config: Record<string, unknown>;
36
+ /** Cluster health endpoint for the local bundle's cluster/health.json. */
37
+ clusterHealthUrl?: string;
38
+ }
39
+ /**
40
+ * Build the FleetCollectorDeps for a given selection of this node's downstream
41
+ * agents/workers. The collector passes per-branch selection in when it knows
42
+ * which downstream subset each peer should gather.
43
+ */
44
+ export declare function buildFleetCollectorDeps(runtime: FleetRuntime, perBranchSelection: (instanceId: string) => FleetSelection): FleetCollectorDeps;
45
+ /** Build the FleetTopologyDeps for `--list` / `--pick` enumeration. */
46
+ export declare function buildFleetTopologyDeps(runtime: FleetRuntime): FleetTopologyDeps;
47
+ /** Enumerate this node's fleet topology (no fan-out). */
48
+ export declare function getFleetTopology(runtime: FleetRuntime): FleetTopology;
49
+ /**
50
+ * Collect this node's full subtree (root call: includeCoordinatorMesh=true) with
51
+ * an optional per-orchestrator selection map. Absent map entry => collect all.
52
+ */
53
+ export declare function collectFleet(runtime: FleetRuntime, selectionByOrch: Map<string, FleetSelection> | null): Promise<Buffer>;
54
+ /**
55
+ * Peer-side responder: on an inbound peer.logs.collect.request, assemble this
56
+ * node's subtree (with the request's loop guard + selection) and stream it back
57
+ * as peer.logs.collect.chunk frames, or a peer.logs.collect.error on failure.
58
+ */
59
+ export declare function makeFleetCollectResponder(runtime: FleetRuntime): (msg: PeerLogsCollectRequest, send: (out: PeerToPeerMessage) => boolean) => Promise<void>;
60
+ //# sourceMappingURL=fleet-wiring.d.ts.map
@@ -4,7 +4,8 @@
4
4
  * Manages the lifecycle: pending -> approved/rejected/expired.
5
5
  */
6
6
  import { type Kysely } from 'kysely';
7
- import type { Database, HeldRun } from '../db/types.js';
7
+ import { type ApprovalRequirement, type ApproverClause, ApprovalDecision, HoldScope, TriggerSource } from '@kici-dev/engine';
8
+ import type { Database, HeldRun, HeldRunApproval } from '../db/types.js';
8
9
  /** Status values for held runs (held_runs table). */
9
10
  export declare enum HeldRunStatus {
10
11
  Pending = "pending",
@@ -24,6 +25,47 @@ export interface CreateHeldRunData {
24
25
  /** Queue type: 'environment' (default) or 'security'. */
25
26
  queueType?: 'environment' | 'security';
26
27
  }
28
+ /**
29
+ * Data required to create a generalized approval hold. Unlike the legacy
30
+ * environment-only `create()`, this carries the hold scope, trigger source,
31
+ * optional step index, and the normalized approval requirement.
32
+ */
33
+ export interface CreateHoldData {
34
+ runId: string;
35
+ jobId: string;
36
+ /** Granularity of the held element. */
37
+ scope: HoldScope;
38
+ /** Step index within the job for step-scoped holds; omit otherwise. */
39
+ stepIndex?: number;
40
+ /** What triggered the hold (environment policy vs SDK requireApproval). */
41
+ triggerSource: TriggerSource;
42
+ /** The normalized requirement the hold must satisfy. */
43
+ requirement: ApprovalRequirement;
44
+ /** Environment id, when the hold originates from an environment policy. */
45
+ environmentId?: string | null;
46
+ /** Queue type: 'environment' (default) or 'security'. */
47
+ queueType?: 'environment' | 'security';
48
+ }
49
+ /** A single decision to record against a hold. */
50
+ export interface RecordDecisionData {
51
+ approverSub: string;
52
+ decision: ApprovalDecision;
53
+ /** Which requirement clauses this decision satisfied (for attribution). */
54
+ clausesSatisfied?: ApproverClause[];
55
+ }
56
+ /**
57
+ * The outcome of `release()` — describes how the held element must be resumed.
58
+ * The store only writes the terminal DB state; the caller performs the actual
59
+ * re-dispatch (job/workflow) or agent notification (step) using this signal.
60
+ */
61
+ export interface ReleaseSignal {
62
+ holdId: string;
63
+ runId: string;
64
+ jobId: string;
65
+ scope: HoldScope;
66
+ /** Set only for step-scoped holds. */
67
+ stepIndex: number | null;
68
+ }
27
69
  /** Options for listing held runs. */
28
70
  export interface ListHeldRunsOptions {
29
71
  status?: string;
@@ -35,6 +77,26 @@ export declare class HeldRunStore {
35
77
  constructor(db: Kysely<Database>);
36
78
  /** Create a new held run with pending status. */
37
79
  create(orgId: string, data: CreateHeldRunData): Promise<HeldRun>;
80
+ /**
81
+ * Create a generalized approval hold (workflow/job/step scope, explicit or
82
+ * environment trigger) carrying a normalized `ApprovalRequirement`. Returns
83
+ * the created row.
84
+ */
85
+ createHold(orgId: string, data: CreateHoldData): Promise<HeldRun>;
86
+ /** Record one approve/reject decision against a hold. */
87
+ recordDecision(heldRunId: string, data: RecordDecisionData): Promise<HeldRunApproval>;
88
+ /** List the recorded decisions for a hold, oldest first. */
89
+ listDecisions(heldRunId: string): Promise<HeldRunApproval[]>;
90
+ /** Get a single held run by id (org-scoped). Returns null if absent. */
91
+ getById(orgId: string, heldRunId: string): Promise<HeldRun | null>;
92
+ /**
93
+ * Release a hold whose approval requirement is satisfied. Flips the row to
94
+ * 'approved' and returns a `ReleaseSignal` describing how the caller must
95
+ * resume the element (re-dispatch for job/workflow, agent notification for
96
+ * step). Throws if the hold is not found or not pending. Approver attribution
97
+ * lives in `held_run_approvals`, not on the row.
98
+ */
99
+ release(orgId: string, heldRunId: string): Promise<ReleaseSignal>;
38
100
  /** Approve a pending held run. Throws if not found or not pending. */
39
101
  approve(orgId: string, heldRunId: string, approvedBy: string): Promise<HeldRun>;
40
102
  /** Reject a pending held run. Throws if not found or not pending. */
@@ -53,6 +115,12 @@ export declare class HeldRunStore {
53
115
  approveByQueueType(orgId: string, heldRunId: string, approvedBy: string, queueType: 'environment' | 'security'): Promise<HeldRun>;
54
116
  /** Get a held run by run ID and job ID. Returns null if not found. */
55
117
  getByRunAndJob(orgId: string, runId: string, jobId: string): Promise<HeldRun | null>;
118
+ /**
119
+ * List pending holds past their `expires_at`. Called by the stale detector
120
+ * BEFORE `expireOverdue()` so it can route each overdue hold by scope (step
121
+ * holds notify the waiting agent; job/workflow holds fail the run).
122
+ */
123
+ listOverdue(): Promise<HeldRun[]>;
56
124
  /**
57
125
  * Expire overdue pending runs. Called by the stale detector.
58
126
  * Sets status to 'expired' and resolved_at to now() for all
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Firecracker host-network provisioning.
3
+ *
4
+ * Creates the per-coordinator bridge (kici-brN), assigns its gateway IP,
5
+ * marks kici-* interfaces unmanaged by NetworkManager, enables IP forwarding,
6
+ * and builds a disjoint, source-scoped nftables table for NAT + egress
7
+ * isolation. One pure command-builder drives live provisioning, the rendered
8
+ * boot script, and (read-only) verification.
9
+ *
10
+ * This is HOST setup, distinct from the runtime per-VM isolation in
11
+ * scaler/nftables.ts (added at spawn / removed at destroy). The two share the
12
+ * nft table name but have separate lifecycles; this module never touches the
13
+ * per-VM rules.
14
+ */
15
+ /** A single subprocess invocation (no shell). */
16
+ export interface CommandSpec {
17
+ bin: string;
18
+ args: string[];
19
+ /** Optional stdin payload (used for `nft -f -`). */
20
+ stdin?: string;
21
+ }
22
+ /** Host-bridge configuration for one Firecracker coordinator. */
23
+ export interface FirecrackerBridgeConfig {
24
+ /** Bridge interface name, e.g. 'kici-br0'. */
25
+ bridgeName: string;
26
+ /** Gateway IP + prefix, e.g. '10.0.0.1/24'. */
27
+ bridgeCidr: string;
28
+ /** nft table name, e.g. 'kici' or 'kici_b'. */
29
+ table: string;
30
+ /** NAT egress interface; auto-detected from the default route when omitted. */
31
+ hostIface?: string;
32
+ }
33
+ /**
34
+ * Derive the network address (CIDR) from a gateway CIDR by masking host bits.
35
+ * '10.0.0.1/24' -> '10.0.0.0/24'.
36
+ */
37
+ export declare function cidrToNetwork(cidr: string): string;
38
+ /**
39
+ * Build the ordered command list that provisions one Firecracker host bridge.
40
+ * Pure — performs no I/O. `provisionBridge` executes these; `renderBootScript`
41
+ * serializes them.
42
+ *
43
+ * The nft `delete table`/`add table` here only ever touches `cfg.table`, so a
44
+ * coord-B provision never wipes coord A's table (and vice versa). Every
45
+ * forward/postrouting/MSS rule is source-scoped to the bridge subnet so two
46
+ * tables on the shared hooks do not cross-drop each other's traffic.
47
+ */
48
+ export declare function buildBridgeCommands(cfg: FirecrackerBridgeConfig): CommandSpec[];
49
+ export declare const NM_CONF_PATH = "/etc/NetworkManager/conf.d/90-kici-unmanaged.conf";
50
+ export declare const NM_CONF_CONTENT: string;
51
+ export type CommandRunner = (spec: CommandSpec) => Promise<{
52
+ stdout: string;
53
+ }>;
54
+ /** Writes the host-scoped NetworkManager conf. Injectable for tests. */
55
+ export type FileWriter = (path: string, content: string) => Promise<void>;
56
+ export interface ExecOptions {
57
+ /** Inject a runner for tests. */
58
+ runner?: CommandRunner;
59
+ /** Inject the NM-conf file writer for tests. */
60
+ writeNmConf?: FileWriter;
61
+ /** Wrap privileged bins with `sudo -n` (non-root orchestrator hosts). */
62
+ requireSudo?: boolean;
63
+ }
64
+ /** Resolve the default-route egress interface. */
65
+ export declare function resolveHostIface(opts?: ExecOptions): Promise<string>;
66
+ export interface BridgeHealth {
67
+ bridgeName: string;
68
+ bridgeExists: boolean;
69
+ bridgeUp: boolean;
70
+ addrPresent: boolean;
71
+ tablePresent: boolean;
72
+ healthy: boolean;
73
+ detail: string;
74
+ }
75
+ /** Provision (or heal) one Firecracker host bridge. Throws on any failure. */
76
+ export declare function provisionBridge(cfg: FirecrackerBridgeConfig, opts?: ExecOptions): Promise<void>;
77
+ /** Read-only health probe for one bridge. Never throws on a missing resource. */
78
+ export declare function verifyBridge(cfg: FirecrackerBridgeConfig, opts?: ExecOptions): Promise<BridgeHealth>;
79
+ /** Remove the bridge + its nft table. Leaves the host-scoped NM conf in place. */
80
+ export declare function teardownBridge(cfg: FirecrackerBridgeConfig, opts?: ExecOptions): Promise<void>;
81
+ /** Serialize the provisioning command list into a dependency-free boot script. */
82
+ export declare function renderBootScript(cfg: FirecrackerBridgeConfig): string;
83
+ //# sourceMappingURL=host-network.d.ts.map