@kici-dev/orchestrator 0.1.15 → 0.1.17

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 (94) hide show
  1. package/dist/agent/dispatcher.d.ts +18 -1
  2. package/dist/app.d.ts +25 -0
  3. package/dist/approvals/apply-decision.d.ts +70 -0
  4. package/dist/approvals/approval-resolver.d.ts +66 -0
  5. package/dist/approvals/step-approval-bridge.d.ts +72 -0
  6. package/dist/approvals/team-membership-lookup.d.ts +13 -0
  7. package/dist/cache/pending-inits.d.ts +5 -0
  8. package/dist/cache/user-cache.d.ts +1 -1
  9. package/dist/cli/api-client.d.ts +35 -0
  10. package/dist/cli/commands/firecracker/index.d.ts +11 -0
  11. package/dist/cli/commands/firecracker/provision.d.ts +13 -0
  12. package/dist/cli/commands/firecracker/teardown.d.ts +3 -0
  13. package/dist/cli/commands/firecracker/verify.d.ts +3 -0
  14. package/dist/cli/commands/local-hook.d.ts +15 -0
  15. package/dist/cli/commands/local-trigger.d.ts +24 -0
  16. package/dist/cli/commands/remote-source.d.ts +17 -0
  17. package/dist/cli/commands/scaler.d.ts +18 -0
  18. package/dist/cli/commands/shared/versioned-upgrade.d.ts +14 -0
  19. package/dist/cli/kici-admin.d.ts +23 -1
  20. package/dist/cli/service/image-digests.d.ts +21 -0
  21. package/dist/cli.js +4320 -490
  22. package/dist/cluster/peer-client.d.ts +23 -1
  23. package/dist/cluster/peer-handler.d.ts +9 -1
  24. package/dist/config.d.ts +0 -4
  25. package/dist/dashboard/handler.d.ts +26 -2
  26. package/dist/dashboard/needs-edges.d.ts +13 -0
  27. package/dist/db/migrations/033_org_settings_approval.d.ts +19 -0
  28. package/dist/db/migrations/034_held_runs_generalize.d.ts +24 -0
  29. package/dist/db/migrations/035_pending_workflow_contexts.d.ts +11 -0
  30. package/dist/db/migrations/036_attestations.d.ts +15 -0
  31. package/dist/db/migrations/037_generic_sources_provider_type_local.d.ts +19 -0
  32. package/dist/db/migrations/038_remote_sources.d.ts +14 -0
  33. package/dist/db/types.d.ts +135 -11
  34. package/dist/diagnostics/bundle-writer.d.ts +1 -17
  35. package/dist/diagnostics/checks/firecracker-network.d.ts +13 -0
  36. package/dist/diagnostics/checks/index.d.ts +2 -1
  37. package/dist/diagnostics/fleet-collector.d.ts +52 -0
  38. package/dist/diagnostics/fleet-constants.d.ts +8 -0
  39. package/dist/diagnostics/fleet-selection.d.ts +15 -0
  40. package/dist/diagnostics/fleet-topology.d.ts +47 -0
  41. package/dist/diagnostics/fleet-wiring.d.ts +60 -0
  42. package/dist/entry-helpers.d.ts +7 -29
  43. package/dist/environments/held-runs.d.ts +93 -1
  44. package/dist/firecracker/host-network.d.ts +83 -0
  45. package/dist/firecracker/persist.d.ts +14 -0
  46. package/dist/index.js +25 -6
  47. package/dist/metrics/prometheus.d.ts +4 -2
  48. package/dist/orchestrator-core.d.ts +57 -1
  49. package/dist/pipeline/dispatch-matched-workflow.d.ts +18 -1
  50. package/dist/pipeline/install-secrets-resolver.d.ts +32 -5
  51. package/dist/pipeline/needs-scheduler.d.ts +12 -10
  52. package/dist/pipeline/pending-workflow-context.d.ts +44 -0
  53. package/dist/pipeline/processor.d.ts +28 -4
  54. package/dist/pipeline/remote-source-store.d.ts +21 -0
  55. package/dist/pipeline/resume-workflow.d.ts +26 -0
  56. package/dist/providers/local/index.d.ts +33 -0
  57. package/dist/providers/local/local-source-config.d.ts +17 -0
  58. package/dist/providers/{internal → local}/lock-file-fetcher.d.ts +6 -6
  59. package/dist/providers/{internal → local}/normalizer.d.ts +28 -28
  60. package/dist/providers/{internal → local}/repo-url-builder.d.ts +6 -6
  61. package/dist/reporting/execution-tracker.d.ts +33 -0
  62. package/dist/routes/admin-events.d.ts +3 -4
  63. package/dist/routes/fleet.d.ts +20 -0
  64. package/dist/routes/uploads.d.ts +36 -26
  65. package/dist/scaler/config.d.ts +4 -2
  66. package/dist/scaler/disk-guard.d.ts +27 -0
  67. package/dist/scaler/firecracker-backend.d.ts +18 -0
  68. package/dist/scaler/manager.d.ts +6 -0
  69. package/dist/scaler/nftables.d.ts +4 -0
  70. package/dist/scaler/reap-orphans.d.ts +27 -0
  71. package/dist/scaler/types.d.ts +2 -0
  72. package/dist/server.js +26929 -23511
  73. package/dist/sources/build-platform-sources.d.ts +4 -2
  74. package/dist/stale-detector/stale-run-detector.d.ts +32 -1
  75. package/dist/standalone.js +16941 -15329
  76. package/dist/storage/filesystem.d.ts +2 -1
  77. package/dist/storage/s3.d.ts +2 -1
  78. package/dist/storage/types.d.ts +7 -0
  79. package/dist/webhook/generic-sources-listener.d.ts +4 -0
  80. package/dist/webhook/generic-sources.d.ts +37 -11
  81. package/dist/webhook/register-source-bundle.d.ts +14 -4
  82. package/dist/ws/agent-handler.d.ts +49 -0
  83. package/dist/ws/dashboard-dispatch-guard.d.ts +21 -0
  84. package/dist/ws/dashboard-env-handler.d.ts +47 -0
  85. package/dist/ws/fleet-agent-collector.d.ts +23 -0
  86. package/dist/ws/oidc-token-relay.d.ts +59 -0
  87. package/dist/ws/platform-client.d.ts +30 -1
  88. package/dist/ws/test-relay-handlers.d.ts +112 -0
  89. package/installer-image-digests.json +7 -0
  90. package/package.json +6 -5
  91. package/sbom.spdx.json +112 -57
  92. package/dist/providers/internal/index.d.ts +0 -32
  93. package/dist/routes/test-trigger.d.ts +0 -41
  94. package/dist/ws/observer-handler.d.ts +0 -42
@@ -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
@@ -5,14 +5,13 @@
5
5
  * Both entry points import these helpers instead of maintaining separate copies.
6
6
  */
7
7
  import { type SourceProvider, SourceSubtype } from '@kici-dev/engine';
8
- import { ProviderRegistry } from './provider-registry.js';
9
8
  /**
10
9
  * A provider source for Platform registration.
11
10
  *
12
11
  * `provider` is the coarse-grained Platform-routing family (`github` /
13
12
  * `gitlab` / `bitbucket` / `generic`). `subtype` is the fine-grained
14
13
  * source kind that survives all the way to the dashboard so universal-git,
15
- * generic_webhook, and internal sources stay distinguishable. `name` is
14
+ * generic_webhook, and local sources stay distinguishable. `name` is
16
15
  * the human-readable label shown in the runs view + sources tab.
17
16
  */
18
17
  export interface ProviderSource {
@@ -50,24 +49,6 @@ export declare function diffProviderSources(oldSources: ProviderSource[], newSou
50
49
  added: ProviderSource[];
51
50
  removed: ProviderSource[];
52
51
  };
53
- /**
54
- * Register the internal provider bundle for E2E testing when the
55
- * `internalProviderRepoPath` config field (KICI_INTERNAL_PROVIDER_REPO_PATH)
56
- * is set.
57
- *
58
- * When the field is present, this overwrites the previously-registered
59
- * `generic:default` bundle with one that has a lockFileFetcher and
60
- * repoUrlBuilder (needed for the webhook pipeline to match triggers and
61
- * dispatch jobs).
62
- *
63
- * When the field is not set, does nothing (leaves `generic:default` as-is).
64
- *
65
- * @returns true if the internal provider was registered, false otherwise
66
- */
67
- export declare function registerInternalProviderIfConfigured(registry: ProviderRegistry, config: {
68
- internalProviderRepoPath?: string;
69
- internalProviderCloneUrl?: string;
70
- }): boolean;
71
52
  /**
72
53
  * Decide whether the local orchestrator can serve a `generic_webhook_sources`
73
54
  * row with the given `provider_type`. Used at boot (and any future reload) to
@@ -79,15 +60,12 @@ export declare function registerInternalProviderIfConfigured(registry: ProviderR
79
60
  * - `'generic'` / `'universal-git'` → always servable (the bundles register
80
61
  * without filesystem dependencies; per-row failures during universal-git
81
62
  * bundle registration are already isolated in orchestrator-core.ts).
82
- * - `'internal'` → only servable when `internalProviderRepoPath` is set, the
83
- * path exists on disk, and is a directory. This mirrors the fail-fast
84
- * guard in `registerInternalProviderIfConfigured`. The check is binary
85
- * per peer: a single `KICI_INTERNAL_PROVIDER_REPO_PATH` backs every
86
- * internal source on that peer (per-source `repoIdentifier`s nest under
87
- * the same base dir).
63
+ * - `'local'` → only servable when the ROW's own `git_config.repoBasePath`
64
+ * exists as a directory on THIS peer. The check is per-source and per-peer:
65
+ * a local repo present on one HA peer may be absent on another, so only the
66
+ * peer that hosts the repo advertises the routing key. Mirrors the statSync
67
+ * gate in `registerProviderBundleForSource`.
88
68
  * - Any unknown provider_type returns false to fail closed.
89
69
  */
90
- export declare function canServeGenericProviderType(providerType: string, config: {
91
- internalProviderRepoPath?: string;
92
- }): boolean;
70
+ export declare function canServeGenericProviderType(providerType: string, gitConfig?: string | Record<string, unknown> | null): boolean;
93
71
  //# sourceMappingURL=entry-helpers.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,53 @@ 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
+ * Held-run `hold_type` discriminator. Defaults to `'approval'` (reviewer
50
+ * holds). The workflow install gate sets `'wait_timer'` / `'concurrency'` so
51
+ * the automated release sweeps can find their rows.
52
+ */
53
+ holdType?: string;
54
+ }
55
+ /** A single decision to record against a hold. */
56
+ export interface RecordDecisionData {
57
+ approverSub: string;
58
+ decision: ApprovalDecision;
59
+ /** Which requirement clauses this decision satisfied (for attribution). */
60
+ clausesSatisfied?: ApproverClause[];
61
+ }
62
+ /**
63
+ * The outcome of `release()` — describes how the held element must be resumed.
64
+ * The store only writes the terminal DB state; the caller performs the actual
65
+ * re-dispatch (job/workflow) or agent notification (step) using this signal.
66
+ */
67
+ export interface ReleaseSignal {
68
+ holdId: string;
69
+ runId: string;
70
+ jobId: string;
71
+ scope: HoldScope;
72
+ /** Set only for step-scoped holds. */
73
+ stepIndex: number | null;
74
+ }
27
75
  /** Options for listing held runs. */
28
76
  export interface ListHeldRunsOptions {
29
77
  status?: string;
@@ -35,6 +83,26 @@ export declare class HeldRunStore {
35
83
  constructor(db: Kysely<Database>);
36
84
  /** Create a new held run with pending status. */
37
85
  create(orgId: string, data: CreateHeldRunData): Promise<HeldRun>;
86
+ /**
87
+ * Create a generalized approval hold (workflow/job/step scope, explicit or
88
+ * environment trigger) carrying a normalized `ApprovalRequirement`. Returns
89
+ * the created row.
90
+ */
91
+ createHold(orgId: string, data: CreateHoldData): Promise<HeldRun>;
92
+ /** Record one approve/reject decision against a hold. */
93
+ recordDecision(heldRunId: string, data: RecordDecisionData): Promise<HeldRunApproval>;
94
+ /** List the recorded decisions for a hold, oldest first. */
95
+ listDecisions(heldRunId: string): Promise<HeldRunApproval[]>;
96
+ /** Get a single held run by id (org-scoped). Returns null if absent. */
97
+ getById(orgId: string, heldRunId: string): Promise<HeldRun | null>;
98
+ /**
99
+ * Release a hold whose approval requirement is satisfied. Flips the row to
100
+ * 'approved' and returns a `ReleaseSignal` describing how the caller must
101
+ * resume the element (re-dispatch for job/workflow, agent notification for
102
+ * step). Throws if the hold is not found or not pending. Approver attribution
103
+ * lives in `held_run_approvals`, not on the row.
104
+ */
105
+ release(orgId: string, heldRunId: string): Promise<ReleaseSignal>;
38
106
  /** Approve a pending held run. Throws if not found or not pending. */
39
107
  approve(orgId: string, heldRunId: string, approvedBy: string): Promise<HeldRun>;
40
108
  /** Reject a pending held run. Throws if not found or not pending. */
@@ -53,6 +121,30 @@ export declare class HeldRunStore {
53
121
  approveByQueueType(orgId: string, heldRunId: string, approvedBy: string, queueType: 'environment' | 'security'): Promise<HeldRun>;
54
122
  /** Get a held run by run ID and job ID. Returns null if not found. */
55
123
  getByRunAndJob(orgId: string, runId: string, jobId: string): Promise<HeldRun | null>;
124
+ /**
125
+ * List pending holds past their `expires_at`. Called by the stale detector
126
+ * BEFORE `expireOverdue()` so it can route each overdue hold by scope (step
127
+ * holds notify the waiting agent; job/workflow holds fail the run).
128
+ */
129
+ listOverdue(): Promise<HeldRun[]>;
130
+ /**
131
+ * Release overdue workflow `wait_timer` holds. The install-gate wait action
132
+ * pauses the workflow as a held run; on timer expiry it must RESUME (not
133
+ * fail like a reviewer-hold expiry). Flips each overdue pending
134
+ * `hold_type='wait_timer'`, `hold_scope='workflow'` row to `released` and
135
+ * returns a `ReleaseSignal` per row so the caller can resume the workflow.
136
+ * Runs BEFORE `expireOverdue()` so these rows leave the pending pool before
137
+ * the expire-and-fail sweep sees them.
138
+ */
139
+ releaseDueWaitHolds(): Promise<ReleaseSignal[]>;
140
+ /**
141
+ * Release the oldest pending workflow `concurrency` hold for a group. Called
142
+ * when a concurrency slot frees on run completion. Flips the oldest matching
143
+ * pending row to `released` and returns its `ReleaseSignal`, or null when no
144
+ * concurrency hold is queued for the group. `group` matches the held row's
145
+ * `environment_id` (workflow-level install concurrency keys on the env id).
146
+ */
147
+ releaseConcurrencyHold(orgId: string, group: string): Promise<ReleaseSignal | null>;
56
148
  /**
57
149
  * Expire overdue pending runs. Called by the stale detector.
58
150
  * 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
@@ -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`);
@@ -973,6 +991,7 @@ const DEFAULT_EXPIRY_DAYS = 90;
973
991
  * Used by the coordinator to manage credentials for all cluster members.
974
992
  */
975
993
  var PeerCredentialStore = class {
994
+ db;
976
995
  constructor(db) {
977
996
  this.db = db;
978
997
  }
@@ -264,6 +264,8 @@ export declare const InstallSecretsDecisionReason: {
264
264
  readonly ProtectionRuleBlock: "protection_rule_block";
265
265
  readonly MissingToken: "missing_token";
266
266
  readonly MissingInstallEnv: "missing_install_env";
267
+ /** A protection gate held the workflow install (hold / wait / queue). */
268
+ readonly Held: "held";
267
269
  };
268
270
  export type InstallSecretsDecisionReason = (typeof InstallSecretsDecisionReason)[keyof typeof InstallSecretsDecisionReason];
269
271
  /**
@@ -280,8 +282,8 @@ export type InstallSecretsChannel = (typeof InstallSecretsChannel)[keyof typeof
280
282
  * Total install-secrets resolution decisions.
281
283
  *
282
284
  * Labels:
283
- * - decision: pass | reject
284
- * - reason: ok (only with `pass`), or one of the reject reasons enumerated in `InstallSecretsDecisionReason`
285
+ * - decision: pass | reject | hold
286
+ * - reason: ok (only with `pass`), held (only with `hold`), or one of the reject reasons enumerated in `InstallSecretsDecisionReason`
285
287
  *
286
288
  * Pass series are emitted exactly once per resolver call that does any work
287
289
  * (workflows declaring neither `registries:` nor `installEnv:` are NOT
@@ -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.
@@ -202,6 +210,54 @@ export interface OrchestratorHooks {
202
210
  */
203
211
  startupLogMessage: (port: number) => string;
204
212
  }
213
+ /**
214
+ * Normalize a lock job's `needs` array (strings, NeedsEntry objects, or
215
+ * NeedsGroupEntry objects) to the set of upstream BASE job names. Group entries
216
+ * are skipped here — group fan-in is resolved separately by the scheduler.
217
+ */
218
+ export declare function upstreamBaseNamesFromNeeds(needs: unknown): string[];
219
+ /**
220
+ * Build the downstream `upstreamJobOutputs` map keyed by BASE name. A base name
221
+ * that fanned into matrix children (rows with `matrix_values`) gets the
222
+ * `{ byMatrix, merged }` envelope; a single non-fanned row keeps the flat shape.
223
+ * Returns undefined when no upstream produced outputs.
224
+ */
225
+ export declare function buildUpstreamOutputsByBase(baseNames: string[], rows: Array<{
226
+ job_name: string;
227
+ outputs: unknown;
228
+ matrix_values: unknown;
229
+ }>): Record<string, Record<string, unknown>> | undefined;
230
+ /**
231
+ * Group an upstream's child rows into the matrix outputs envelope
232
+ * `{ byMatrix: { '<suffix>': outputs }, merged: <last-write-wins> }`. The suffix
233
+ * is the text inside the `(...)` of each expanded child name; children are
234
+ * merged in name order (deterministic, matching dispatch order).
235
+ */
236
+ export declare function buildMatrixOutputsEnvelope(baseName: string, children: Array<{
237
+ job_name: string;
238
+ parsed: Record<string, unknown>;
239
+ }>): {
240
+ byMatrix: Record<string, Record<string, unknown>>;
241
+ merged: Record<string, unknown>;
242
+ };
243
+ /**
244
+ * Merge plain `outputs` and decrypted `secret outputs` from upstream jobs
245
+ * into the dispatch envelope's `secrets` + `upstreamJobOutputs` fields.
246
+ *
247
+ * For a fanned (matrix) upstream, the downstream sees a keyed envelope
248
+ * `{ byMatrix: { '<suffix>': outputs }, merged: <last-write-wins> }` under the
249
+ * BASE name; a non-fanned upstream keeps the flat outputs shape.
250
+ *
251
+ * The needs-aware scheduler guarantees upstreams are
252
+ * terminal before the downstream is dispatched, so the lookups below
253
+ * always find final values. Errors are swallowed (warn-logged) so a flaky
254
+ * upstream-outputs read doesn't block dispatch — the agent will still get
255
+ * its declared secrets, just without the merged upstream additions.
256
+ */
257
+ export declare function mergeUpstreamOutputs(db: Kysely<Database>, runId: string, jobName: string, needs: unknown, dispatchSecrets: Record<string, string> | undefined, secretKey: string): Promise<{
258
+ mergedSecrets: Record<string, string> | undefined;
259
+ upstreamJobOutputs: Record<string, Record<string, unknown>> | undefined;
260
+ }>;
205
261
  export declare function bootstrapOrchestrator(config: AppConfig, hooks: OrchestratorHooks, options?: {
206
262
  otelSdk?: {
207
263
  shutdown(): Promise<void>;
@@ -91,6 +91,23 @@ export interface WorkflowDispatchContext {
91
91
  export interface DispatchMatchedWorkflowResult {
92
92
  /** Number of jobs successfully dispatched (non-rejected). */
93
93
  dispatchedJobCount: number;
94
+ /** True when the workflow install gate paused the dispatch (held run). */
95
+ held?: boolean;
96
+ }
97
+ /** Options controlling a (re-)dispatch of a matched workflow. */
98
+ export interface DispatchMatchedWorkflowOptions {
99
+ /**
100
+ * Resume path: skip the workflow install protection gate (already satisfied)
101
+ * so secrets resolve directly and the dispatch flows into job dispatch.
102
+ */
103
+ skipInstallProtectionGate?: boolean;
104
+ /** The released held-run id being resumed (for logging / correlation). */
105
+ reuseHeldRunId?: string;
106
+ /**
107
+ * The run id whose `held` execution_runs row should be reused (flipped to
108
+ * pending) instead of inserting a fresh row.
109
+ */
110
+ reuseRunId?: string;
94
111
  }
95
112
  /**
96
113
  * Dispatch a single matched workflow.
@@ -107,5 +124,5 @@ export interface DispatchMatchedWorkflowResult {
107
124
  * I. deferred init dispatch (fire-and-forget per job)
108
125
  * J. deferred dynamic dispatch (fire-and-forget per dynamic entry)
109
126
  */
110
- export declare function dispatchMatchedWorkflow(ctx: WorkflowDispatchContext): Promise<DispatchMatchedWorkflowResult>;
127
+ export declare function dispatchMatchedWorkflow(ctx: WorkflowDispatchContext, opts?: DispatchMatchedWorkflowOptions): Promise<DispatchMatchedWorkflowResult>;
111
128
  //# sourceMappingURL=dispatch-matched-workflow.d.ts.map