@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.
- package/dist/agent/dispatcher.d.ts +18 -1
- package/dist/app.d.ts +25 -0
- package/dist/approvals/apply-decision.d.ts +70 -0
- package/dist/approvals/approval-resolver.d.ts +66 -0
- package/dist/approvals/step-approval-bridge.d.ts +72 -0
- package/dist/approvals/team-membership-lookup.d.ts +13 -0
- package/dist/cache/pending-inits.d.ts +5 -0
- package/dist/cache/user-cache.d.ts +1 -1
- package/dist/cli/api-client.d.ts +35 -0
- package/dist/cli/commands/firecracker/index.d.ts +11 -0
- package/dist/cli/commands/firecracker/provision.d.ts +13 -0
- package/dist/cli/commands/firecracker/teardown.d.ts +3 -0
- package/dist/cli/commands/firecracker/verify.d.ts +3 -0
- package/dist/cli/commands/local-hook.d.ts +15 -0
- package/dist/cli/commands/local-trigger.d.ts +24 -0
- package/dist/cli/commands/remote-source.d.ts +17 -0
- package/dist/cli/commands/scaler.d.ts +18 -0
- package/dist/cli/commands/shared/versioned-upgrade.d.ts +14 -0
- package/dist/cli/kici-admin.d.ts +23 -1
- package/dist/cli/service/image-digests.d.ts +21 -0
- package/dist/cli.js +4320 -490
- package/dist/cluster/peer-client.d.ts +23 -1
- package/dist/cluster/peer-handler.d.ts +9 -1
- package/dist/config.d.ts +0 -4
- package/dist/dashboard/handler.d.ts +26 -2
- package/dist/dashboard/needs-edges.d.ts +13 -0
- package/dist/db/migrations/033_org_settings_approval.d.ts +19 -0
- package/dist/db/migrations/034_held_runs_generalize.d.ts +24 -0
- package/dist/db/migrations/035_pending_workflow_contexts.d.ts +11 -0
- package/dist/db/migrations/036_attestations.d.ts +15 -0
- package/dist/db/migrations/037_generic_sources_provider_type_local.d.ts +19 -0
- package/dist/db/migrations/038_remote_sources.d.ts +14 -0
- package/dist/db/types.d.ts +135 -11
- package/dist/diagnostics/bundle-writer.d.ts +1 -17
- package/dist/diagnostics/checks/firecracker-network.d.ts +13 -0
- package/dist/diagnostics/checks/index.d.ts +2 -1
- package/dist/diagnostics/fleet-collector.d.ts +52 -0
- package/dist/diagnostics/fleet-constants.d.ts +8 -0
- package/dist/diagnostics/fleet-selection.d.ts +15 -0
- package/dist/diagnostics/fleet-topology.d.ts +47 -0
- package/dist/diagnostics/fleet-wiring.d.ts +60 -0
- package/dist/entry-helpers.d.ts +7 -29
- package/dist/environments/held-runs.d.ts +93 -1
- package/dist/firecracker/host-network.d.ts +83 -0
- package/dist/firecracker/persist.d.ts +14 -0
- package/dist/index.js +25 -6
- package/dist/metrics/prometheus.d.ts +4 -2
- package/dist/orchestrator-core.d.ts +57 -1
- package/dist/pipeline/dispatch-matched-workflow.d.ts +18 -1
- package/dist/pipeline/install-secrets-resolver.d.ts +32 -5
- package/dist/pipeline/needs-scheduler.d.ts +12 -10
- package/dist/pipeline/pending-workflow-context.d.ts +44 -0
- package/dist/pipeline/processor.d.ts +28 -4
- package/dist/pipeline/remote-source-store.d.ts +21 -0
- package/dist/pipeline/resume-workflow.d.ts +26 -0
- package/dist/providers/local/index.d.ts +33 -0
- package/dist/providers/local/local-source-config.d.ts +17 -0
- package/dist/providers/{internal → local}/lock-file-fetcher.d.ts +6 -6
- package/dist/providers/{internal → local}/normalizer.d.ts +28 -28
- package/dist/providers/{internal → local}/repo-url-builder.d.ts +6 -6
- package/dist/reporting/execution-tracker.d.ts +33 -0
- package/dist/routes/admin-events.d.ts +3 -4
- package/dist/routes/fleet.d.ts +20 -0
- package/dist/routes/uploads.d.ts +36 -26
- package/dist/scaler/config.d.ts +4 -2
- package/dist/scaler/disk-guard.d.ts +27 -0
- package/dist/scaler/firecracker-backend.d.ts +18 -0
- package/dist/scaler/manager.d.ts +6 -0
- package/dist/scaler/nftables.d.ts +4 -0
- package/dist/scaler/reap-orphans.d.ts +27 -0
- package/dist/scaler/types.d.ts +2 -0
- package/dist/server.js +26929 -23511
- package/dist/sources/build-platform-sources.d.ts +4 -2
- package/dist/stale-detector/stale-run-detector.d.ts +32 -1
- package/dist/standalone.js +16941 -15329
- package/dist/storage/filesystem.d.ts +2 -1
- package/dist/storage/s3.d.ts +2 -1
- package/dist/storage/types.d.ts +7 -0
- package/dist/webhook/generic-sources-listener.d.ts +4 -0
- package/dist/webhook/generic-sources.d.ts +37 -11
- package/dist/webhook/register-source-bundle.d.ts +14 -4
- package/dist/ws/agent-handler.d.ts +49 -0
- package/dist/ws/dashboard-dispatch-guard.d.ts +21 -0
- package/dist/ws/dashboard-env-handler.d.ts +47 -0
- package/dist/ws/fleet-agent-collector.d.ts +23 -0
- package/dist/ws/oidc-token-relay.d.ts +59 -0
- package/dist/ws/platform-client.d.ts +30 -1
- package/dist/ws/test-relay-handlers.d.ts +112 -0
- package/installer-image-digests.json +7 -0
- package/package.json +6 -5
- package/sbom.spdx.json +112 -57
- package/dist/providers/internal/index.d.ts +0 -32
- package/dist/routes/test-trigger.d.ts +0 -41
- 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
|
package/dist/entry-helpers.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
* - `'
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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,
|
|
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
|
|
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,
|
|
745
|
-
* entries (
|
|
746
|
-
*
|
|
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
|
|
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
|
-
|
|
944
|
-
|
|
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
|