@kici-dev/orchestrator 0.1.22 → 0.1.23

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.
@@ -0,0 +1,18 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add a per-host dimension to environment secret bindings:
4
+ *
5
+ * - `environment_bindings.host_pattern text NOT NULL DEFAULT '**'` — the host
6
+ * selector a binding applies to (exact / glob / regex, matched against a
7
+ * fan-out child's agentId / hostname / labels). `'**'` matches every host, so
8
+ * existing rows are backfilled to `'**'` and keep their fleet-wide behaviour.
9
+ * - The binding unique key widens from `(environment_id, scope_pattern)` to
10
+ * `(environment_id, scope_pattern, host_pattern)` so the same scope can carry
11
+ * distinct per-host selectors.
12
+ *
13
+ * Idempotent: re-running on a DB that already has the column / index is a no-op.
14
+ * Additive — staging data is preserved.
15
+ */
16
+ export declare function up(db: Kysely<unknown>): Promise<void>;
17
+ export declare function down(db: Kysely<unknown>): Promise<void>;
18
+ //# sourceMappingURL=051_binding_host_pattern.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add pre-agent reach metadata to the host roster so a declared host (no agent
4
+ * yet) can be reached over SSH for bootstrap bring-up:
5
+ *
6
+ * - `host_roster.address text NULL` — IP / hostname to SSH to.
7
+ * - `host_roster.ssh_user text NULL` — SSH login user (defaults to `root` at use site).
8
+ * - `host_roster.ssh_port int NULL` — SSH port (defaults to 22 at use site).
9
+ * - `host_roster.ssh_key_secret text NULL` — scoped-secret ref (`scope/key`)
10
+ * holding the bring-up private key. The orchestrator resolves it server-side;
11
+ * the key never lives in the roster.
12
+ *
13
+ * All nullable: a host with no reach metadata simply cannot be bootstrapped and
14
+ * behaves exactly as before. Idempotent (`ADD COLUMN IF NOT EXISTS`); additive,
15
+ * so staging data is preserved.
16
+ */
17
+ export declare function up(db: Kysely<unknown>): Promise<void>;
18
+ export declare function down(db: Kysely<unknown>): Promise<void>;
19
+ //# sourceMappingURL=052_host_reach_metadata.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add a single-use marker to agent tokens for the bootstrap (init-runner)
4
+ * bring-up flow:
5
+ *
6
+ * - `agent_tokens.consumed_at timestamptz NULL` — set the first time a
7
+ * single-use bootstrap token is consumed (at `agent.register`). A second
8
+ * register with the same token is rejected. NULL = never consumed (the
9
+ * default for every existing static / ephemeral token, which stay reusable
10
+ * until expiry).
11
+ *
12
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
13
+ * preserved.
14
+ */
15
+ export declare function up(db: Kysely<unknown>): Promise<void>;
16
+ export declare function down(db: Kysely<unknown>): Promise<void>;
17
+ //# sourceMappingURL=053_agent_token_single_use.d.ts.map
@@ -605,6 +605,12 @@ export interface EnvironmentBindingsTable {
605
605
  environment_id: string;
606
606
  /** Scope pattern for matching (e.g. workflow name glob, repo pattern) */
607
607
  scope_pattern: string;
608
+ /**
609
+ * Host selector this binding applies to (exact / glob / regex over a fan-out
610
+ * child's agentId / hostname / labels). `'**'` (the default) matches every
611
+ * host.
612
+ */
613
+ host_pattern: Generated<string>;
608
614
  /** When this binding was created */
609
615
  created_at: Generated<Date>;
610
616
  }
@@ -863,6 +869,13 @@ export interface AgentTokenTable {
863
869
  revoked_at: Date | null;
864
870
  /** When this token expires (null = never, static tokens) */
865
871
  expires_at: Date | null;
872
+ /**
873
+ * Single-use marker for bootstrap (init-runner) tokens. Set the first time
874
+ * the token is consumed at `agent.register`; a second register is rejected.
875
+ * NULL = never consumed (every static / ephemeral token stays reusable until
876
+ * expiry).
877
+ */
878
+ consumed_at: ColumnType<Date | null, Date | string | null | undefined, Date | string | null>;
866
879
  }
867
880
  export type AgentTokenRow = Selectable<AgentTokenTable>;
868
881
  export type NewAgentTokenRow = Insertable<AgentTokenTable>;
@@ -1639,6 +1652,16 @@ export interface HostRosterTable {
1639
1652
  * reconnects (down-then-up). NULL = no reboot pending.
1640
1653
  */
1641
1654
  reboot_pending_until: ColumnType<Date | null, Date | string | null, Date | string | null>;
1655
+ /**
1656
+ * Pre-agent reach metadata: how to SSH to a declared host before it has a
1657
+ * KiCI agent, for bootstrap bring-up. All nullable — a host with no reach
1658
+ * metadata cannot be bootstrapped and behaves exactly as before.
1659
+ */
1660
+ address: ColumnType<string | null, string | null | undefined, string | null>;
1661
+ ssh_user: ColumnType<string | null, string | null | undefined, string | null>;
1662
+ ssh_port: ColumnType<number | null, number | null | undefined, number | null>;
1663
+ /** Scoped-secret ref (`scope/key`) holding the bring-up private key. */
1664
+ ssh_key_secret: ColumnType<string | null, string | null | undefined, string | null>;
1642
1665
  created_at: Generated<Date>;
1643
1666
  updated_at: ColumnType<Date, Date | string | undefined, Date | string>;
1644
1667
  }
@@ -6,6 +6,14 @@
6
6
  */
7
7
  import type { Kysely } from 'kysely';
8
8
  import type { Database, EnvironmentBinding } from '../db/types.js';
9
+ /**
10
+ * A scope→environment binding with its host selector. `hostPattern` defaults to
11
+ * `'**'` (all hosts) when omitted.
12
+ */
13
+ export interface BindingInput {
14
+ scopePattern: string;
15
+ hostPattern?: string;
16
+ }
9
17
  /**
10
18
  * Data access layer for environment bindings.
11
19
  */
@@ -17,10 +25,11 @@ export declare class BindingStore {
17
25
  /**
18
26
  * Replace all bindings for an environment in a transaction.
19
27
  *
20
- * Deletes existing bindings and inserts the new set atomically.
21
- * Pass an empty array to clear all bindings.
28
+ * Deletes existing bindings and inserts the new set atomically. Each binding
29
+ * carries a `scopePattern` and an optional `hostPattern` (defaulting to
30
+ * `'**'`). Pass an empty array to clear all bindings.
22
31
  */
23
- set(orgId: string, environmentId: string, scopePatterns: string[]): Promise<void>;
32
+ set(orgId: string, environmentId: string, bindings: BindingInput[]): Promise<void>;
24
33
  /** Find bindings for an environment (alias for list, used by secret resolver). */
25
34
  findBindingsForEnvironment(orgId: string, environmentId: string): Promise<EnvironmentBinding[]>;
26
35
  }
@@ -104,8 +104,32 @@ export declare class HeldRunStore {
104
104
  * the created row.
105
105
  */
106
106
  createHold(orgId: string, data: CreateHoldData): Promise<HeldRun>;
107
+ /** INSERT one decision row using the given executor (root or transaction). */
108
+ private insertDecisionRow;
109
+ /** Flip a pending hold to 'approved' using the given executor. Undefined if not pending. */
110
+ private flipToApproved;
111
+ /** Flip a pending hold to 'rejected' using the given executor. Undefined if not pending. */
112
+ private flipToRejected;
113
+ /** Map a released held_runs row to the resume ReleaseSignal. */
114
+ private toReleaseSignal;
107
115
  /** Record one approve/reject decision against a hold. */
108
116
  recordDecision(heldRunId: string, data: RecordDecisionData): Promise<HeldRunApproval>;
117
+ /**
118
+ * Atomically record an approve decision and release the (now-satisfied) hold.
119
+ * The INSERT into `held_run_approvals` and the `held_runs` → approved UPDATE
120
+ * run in a single transaction, so a crash between them cannot strand the hold
121
+ * `pending` with a recorded approve. Throws if the hold is not found or no
122
+ * longer pending (the whole transaction rolls back).
123
+ */
124
+ recordAndRelease(orgId: string, heldRunId: string, data: RecordDecisionData): Promise<ReleaseSignal>;
125
+ /**
126
+ * Atomically record a reject decision and reject the hold. The INSERT and the
127
+ * `held_runs` → rejected UPDATE run in a single transaction, so a crash
128
+ * between them cannot strand the hold `pending` with a recorded reject (which
129
+ * would poison `evaluate()` forever). Throws if the hold is not found or no
130
+ * longer pending (the whole transaction rolls back).
131
+ */
132
+ recordAndReject(orgId: string, heldRunId: string, data: RecordDecisionData, reason?: string): Promise<HeldRun>;
109
133
  /** List the recorded decisions for a hold, oldest first. */
110
134
  listDecisions(heldRunId: string): Promise<HeldRunApproval[]>;
111
135
  /** Get a single held run by id (org-scoped). Returns null if absent. */
@@ -8,6 +8,8 @@ export declare function setDeclaredHostsUnreachable(value: number): void;
8
8
  export declare function setStaleRunsCurrent(value: number): void;
9
9
  interface ScalerUsageRow {
10
10
  scaler: string;
11
+ /** Backend type for this scaler (rollup dimension). `__global__` for the orchestrator-wide row. */
12
+ scalerType?: string;
11
13
  machinePool?: string;
12
14
  cpus: number;
13
15
  memBytes: number;
@@ -14,10 +14,12 @@
14
14
  * results through the pipeline.
15
15
  */
16
16
  import { ExecutionJobStatus, InitFailureCategory, CacheRefScope } from '@kici-dev/engine';
17
- import type { LabelMatcher, LockWorkflow, LockJob, HostTargetSelector, SimulatedEvent, WorkflowDecision, MaterializedJob, ResolvedHostAgent } from '@kici-dev/engine';
17
+ import type { LabelMatcher, LockWorkflow, LockJob, HostTargetSelector, SimulatedEvent, WorkflowDecision, MaterializedJob, ResolvedHostAgent, HostFacts } from '@kici-dev/engine';
18
18
  import { type HostRosterStore } from '../agent/host-roster.js';
19
19
  import type { WebhookInfo } from '../webhook/handler.js';
20
20
  import type { ProviderBundle } from '../provider-registry.js';
21
+ import type { Dispatcher } from '../agent/dispatcher.js';
22
+ import type { QueuedJobInput } from '../queue/job-queue.js';
21
23
  import type { TrustResolution } from '../security/trust-resolver.js';
22
24
  import { type ProcessingDeps } from './processor.js';
23
25
  /**
@@ -120,6 +122,12 @@ export interface WorkflowDispatchContext {
120
122
  * runsOnAll ∩ target. Narrow-only. Undefined for webhook runs (no narrowing).
121
123
  */
122
124
  target?: HostTargetSelector;
125
+ /**
126
+ * Resolved (coerced + defaulted + validated) workflow-dispatch inputs from
127
+ * `kici run --input`. Carried onto every dispatched job's request so the agent
128
+ * exposes them as `ctx.dispatchInputs`. Undefined for webhook runs.
129
+ */
130
+ dispatchInputs?: Record<string, unknown>;
123
131
  }
124
132
  export interface DispatchMatchedWorkflowResult {
125
133
  /** Number of jobs successfully dispatched (non-rejected). */
@@ -144,6 +152,30 @@ export interface DispatchMatchedWorkflowOptions {
144
152
  */
145
153
  reuseRunId?: string;
146
154
  }
155
+ type DispatchFn = (input: QueuedJobInput) => ReturnType<Dispatcher['dispatch']>;
156
+ interface DispatchSetup {
157
+ /** Wrapped dispatcher that injects ctx.extraJobConfig into every dispatch. */
158
+ dispatcher: {
159
+ dispatch: DispatchFn;
160
+ };
161
+ /** WebhookInfo overlaid with effective routing key + provider. */
162
+ info: WebhookInfo;
163
+ /** Composite delivery id on cross-source, otherwise info.deliveryId. */
164
+ effectiveDeliveryId: string;
165
+ workflowConcurrency: {
166
+ cancelInProgress?: boolean;
167
+ max?: number;
168
+ } | undefined;
169
+ workflowTimeoutMs: number | undefined;
170
+ /**
171
+ * Run mode for idempotent steps (`apply` | `check` | `check-fail-on-drift`),
172
+ * carried on `ctx.extraJobConfig` by the test-trigger / `kici run --check`
173
+ * path. Persisted onto the `execution_runs` row so `computeRunStatus` can
174
+ * fail a `check-fail-on-drift` run that detected drift. Undefined for the
175
+ * default apply-mode webhook path.
176
+ */
177
+ checkMode: string | undefined;
178
+ }
147
179
  interface RejectedJob {
148
180
  jobId: string;
149
181
  jobName: string;
@@ -157,6 +189,20 @@ interface RejectedJob {
157
189
  */
158
190
  terminalStatus?: ExecutionJobStatus;
159
191
  }
192
+ /**
193
+ * Build the QueuedJobInput for a synthetic `__bringup__` job: the orchestrator
194
+ * dispatches one per declared-but-un-agented `includeUninitialized` child to an
195
+ * agent holding `kici:capability:ssh-transport`, which runs the agent-side
196
+ * `ensureInitRunner(targetAgentId)` over SSH. The init-runner then connects
197
+ * under `targetAgentId`, and the child's pinned-hold (already queued with that
198
+ * pin) drains its bootstrap steps onto it. The bring-up job clones nothing and
199
+ * runs no sandbox (`bringupOnly`).
200
+ */
201
+ export declare function buildBringupJobInput(args: {
202
+ ctx: WorkflowDispatchContext;
203
+ setup: DispatchSetup;
204
+ targetAgentId: string;
205
+ }): QueuedJobInput;
160
206
  /**
161
207
  * Phase B orchestrator: probe caches, dispatch the build job (if needed),
162
208
  * and surface enough state for downstream phases to skip / continue
@@ -227,8 +273,9 @@ export interface WavePlan {
227
273
  * Compute the rolling-wave plan for a materialized job set.
228
274
  *
229
275
  * For each base job declaring `maxParallel` whose fan-out produced more than one
230
- * child, children are ordered deterministically by `variant_label` (the matrix
231
- * suffix / hostname, via `expandedName`) and every child at index `>=
276
+ * child, children are ordered deterministically by `fanoutIndex` (the
277
+ * agentId / variant-label rank assigned at materialization; falling back to
278
+ * `expandedName` for children with no index) and every child at index `>=
232
279
  * maxParallel` is held (`wave_gated=true`). The first `maxParallel` dispatch
233
280
  * immediately; held children release one-per-terminal via the wave-scheduler.
234
281
  * Every child of a bounded-wave base — held or not — gets a `policy` entry so
@@ -241,6 +288,14 @@ export declare function materializeStaticJobsSafe(staticJobs: readonly LockJob[]
241
288
  expansionMap: Map<string, readonly string[]>;
242
289
  matrixFailures: RejectedJob[];
243
290
  }>;
291
+ /**
292
+ * Build the per-host secret-resolution context for a materialized child. A
293
+ * `runsOnAll` host child carries its identity on `mat.agent` (preferred) or
294
+ * `mat.pinnedAgentId`/`mat.host`; a non-host child (matrix/static) has none, so
295
+ * resolution stays fleet-wide (`'**'`-only). Returns `undefined` when there are
296
+ * no host facts to scope by.
297
+ */
298
+ export declare function hostCtxFromMat(mat: MaterializedJob): HostFacts | undefined;
244
299
  export interface GeneratedJobConfig {
245
300
  /**
246
301
  * The generated lock job with its `name` and `needs` rewritten to expanded
@@ -73,6 +73,12 @@ export interface TestTriggerInput {
73
73
  * context, where it post-filters each runsOnAll job's matched roster.
74
74
  */
75
75
  target?: HostTargetSelector;
76
+ /**
77
+ * Raw operator-supplied `kici run --input KEY=VALUE` pairs (not coerced /
78
+ * defaulted). Validated + coerced + defaulted here against the matched
79
+ * workflow's lock dispatch descriptor before dispatch.
80
+ */
81
+ dispatchInputs?: Record<string, string>;
76
82
  }
77
83
  /**
78
84
  * Result of processing a test trigger.
@@ -21,7 +21,26 @@ export declare class LogWriter {
21
21
  private readonly logStorage;
22
22
  private readonly observerRegistry?;
23
23
  private readonly isTestRun?;
24
+ /**
25
+ * In-flight `logStorage.append` promises, keyed by runId. `appendChunk` is
26
+ * called fire-and-forget from the agent WS handler (it does not await the
27
+ * storage write), so a run can flip to a terminal status while the final
28
+ * log chunk's append is still pending. `drain(runId)` lets a reader (the
29
+ * test-run logs cursor endpoint) wait for those pending writes before it
30
+ * reports the stream as fully drained — without this, the `done` flag can
31
+ * be returned `true` while the last user-visible log line is not yet on
32
+ * disk, dropping it from a blocking `kici run remote` follow.
33
+ */
34
+ private readonly pendingAppends;
24
35
  constructor(deps: LogWriterDeps);
36
+ /**
37
+ * Await every in-flight log append for `runId` that was registered before
38
+ * this call. Settles even if an append rejected (errors are already logged
39
+ * by `appendChunk`); the point is ordering, not error propagation. New
40
+ * appends started after this call are not waited on — a terminal run emits
41
+ * no further chunks, so the snapshot taken at call time is complete.
42
+ */
43
+ drain(runId: string): Promise<void>;
25
44
  /**
26
45
  * Append log lines from an agent to storage in JSONL format.
27
46
  *
@@ -39,6 +39,23 @@ export interface ResolvedResources {
39
39
  memBytes: number;
40
40
  };
41
41
  }
42
+ /**
43
+ * Build the scaler-usage metric rows: one per active scaler (stamped with its
44
+ * backend type) plus a `__global__` rollup row. Pure so it is unit-testable
45
+ * without constructing a full ScalerManager.
46
+ */
47
+ export declare function buildScalerUsageRows(perScalerUsage: ReadonlyMap<string, {
48
+ cpus: number;
49
+ memBytes: number;
50
+ }>, globalUsage: {
51
+ cpus: number;
52
+ memBytes: number;
53
+ }, scalerTypeOf: (name: string) => string | undefined): Array<{
54
+ scaler: string;
55
+ scalerType?: string;
56
+ cpus: number;
57
+ memBytes: number;
58
+ }>;
42
59
  /**
43
60
  * Status summary for metrics and health endpoints.
44
61
  */
@@ -15,7 +15,7 @@
15
15
  * longest-path-wins uses scope path after stripping backend prefix.
16
16
  * audit log includes backend name.
17
17
  */
18
- import { type EnvironmentBinding, type ScopedSecret } from '@kici-dev/engine';
18
+ import { type EnvironmentBinding, type HostFacts, type ScopedSecret } from '@kici-dev/engine';
19
19
  import type { Logger } from '@kici-dev/shared';
20
20
  import type { AuditLogger } from './audit-logger.js';
21
21
  /**
@@ -69,13 +69,13 @@ export interface ResolvedSecretMeta {
69
69
  * can flow through `ProcessingDeps.secretResolver`.
70
70
  */
71
71
  export interface SecretResolverApi {
72
- resolveForJob(orgId: string, environmentName: string): Promise<Record<string, string>>;
72
+ resolveForJob(orgId: string, environmentName: string, hostCtx?: HostFacts): Promise<Record<string, string>>;
73
73
  resolveNamed(orgId: string, scope: string, key: string, opts?: {
74
74
  store?: string;
75
75
  runId?: string;
76
76
  jobId?: string;
77
77
  }): Promise<string | null>;
78
- resolveForJobWithMeta(orgId: string, environmentName: string): Promise<Record<string, ResolvedSecretMeta>>;
78
+ resolveForJobWithMeta(orgId: string, environmentName: string, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
79
79
  }
80
80
  /**
81
81
  * Resolves secrets for a job by matching environment bindings against scoped secrets
@@ -98,9 +98,13 @@ export declare class SecretResolver implements SecretResolverApi {
98
98
  *
99
99
  * @param orgId - Organization ID
100
100
  * @param environmentName - Environment name to resolve secrets for
101
+ * @param hostCtx - Optional fan-out child identity for per-host resolution.
102
+ * When supplied, each binding is gated by its `host_pattern` and its
103
+ * `scope_pattern` is templated per-child; when omitted, only fleet-wide
104
+ * (`'**'`) non-templated bindings contribute.
101
105
  * @returns Flat map of decrypted secret key-value pairs
102
106
  */
103
- resolveForJob(orgId: string, environmentName: string): Promise<Record<string, string>>;
107
+ resolveForJob(orgId: string, environmentName: string, hostCtx?: HostFacts): Promise<Record<string, string>>;
104
108
  /**
105
109
  * Resolve a single named secret by (orgId, scope, key), optionally scoped to
106
110
  * a specific backend. Bypasses environment bindings — this is a direct
@@ -126,7 +130,7 @@ export declare class SecretResolver implements SecretResolverApi {
126
130
  *
127
131
  * Returns the secret value along with which backend and scope provided it.
128
132
  */
129
- resolveForJobWithMeta(orgId: string, environmentName: string): Promise<Record<string, ResolvedSecretMeta>>;
133
+ resolveForJobWithMeta(orgId: string, environmentName: string, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
130
134
  /**
131
135
  * Collect all secrets from all backend stores, prefixing scopes.
132
136
  * Per /: unreachable backends are tracked but not fatal here.
@@ -145,8 +149,17 @@ export declare class SecretResolver implements SecretResolverApi {
145
149
  */
146
150
  private checkScopedFailure;
147
151
  /**
148
- * Find the winning secret for a given key (highest scope depth after prefix strip).
152
+ * Find the winning secret for a given key, mirroring the engine scope
153
+ * resolver's host-aware matching and `(host specificity, scope depth)`
154
+ * precedence so the enriched metadata reports the same secret the flat
155
+ * resolution selected.
149
156
  */
150
157
  private findWinningSecret;
158
+ /**
159
+ * Resolve the effective scope pattern a binding contributes for a host,
160
+ * applying the host gate and per-child scope templating. Mirrors the engine
161
+ * scope resolver's `bindingScopeForHost`. Returns `null` to skip the binding.
162
+ */
163
+ private bindingScopeForHost;
151
164
  }
152
165
  //# sourceMappingURL=secret-resolver.d.ts.map