@kici-dev/orchestrator 0.1.16 → 0.1.18

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 (83) hide show
  1. package/dist/agent/dispatcher.d.ts +27 -1
  2. package/dist/agent/host-roster-reaper.d.ts +42 -0
  3. package/dist/agent/host-roster.d.ts +110 -0
  4. package/dist/agent/registry.d.ts +65 -2
  5. package/dist/app.d.ts +3 -0
  6. package/dist/approvals/apply-decision.d.ts +11 -1
  7. package/dist/cache/pending-inits.d.ts +5 -0
  8. package/dist/cli/api-client.d.ts +11 -0
  9. package/dist/cli/commands/host.d.ts +13 -0
  10. package/dist/cli/commands/local-hook.d.ts +15 -0
  11. package/dist/cli/commands/local-trigger.d.ts +24 -0
  12. package/dist/cli/commands/remote-source.d.ts +17 -0
  13. package/dist/cli/commands/shared/versioned-upgrade.d.ts +37 -0
  14. package/dist/cli/kici-admin.d.ts +13 -0
  15. package/dist/cli/service/compose.d.ts +2 -1
  16. package/dist/cli/service/index.d.ts +2 -2
  17. package/dist/cli/service/instance/manifest.d.ts +12 -0
  18. package/dist/cli/service/instance/resolve.d.ts +12 -2
  19. package/dist/cli/service/launchd.d.ts +4 -1
  20. package/dist/cli/service/platform-detect.d.ts +5 -0
  21. package/dist/cli/service/systemd.d.ts +2 -1
  22. package/dist/cli/service/types.d.ts +37 -0
  23. package/dist/cli/service/windows.d.ts +2 -1
  24. package/dist/cli.js +1340 -198
  25. package/dist/cluster/coordinator.d.ts +5 -1
  26. package/dist/config/schema.d.ts +4 -0
  27. package/dist/config/types.d.ts +9 -0
  28. package/dist/config.d.ts +6 -4
  29. package/dist/dashboard/handler.d.ts +26 -2
  30. package/dist/dashboard/needs-edges.d.ts +13 -0
  31. package/dist/db/migrations/035_pending_workflow_contexts.d.ts +11 -0
  32. package/dist/db/migrations/036_attestations.d.ts +15 -0
  33. package/dist/db/migrations/037_generic_sources_provider_type_local.d.ts +19 -0
  34. package/dist/db/migrations/038_remote_sources.d.ts +14 -0
  35. package/dist/db/migrations/039_host_roster.d.ts +19 -0
  36. package/dist/db/migrations/040_runsonall_pin.d.ts +4 -0
  37. package/dist/db/migrations/041_wave_gated.d.ts +4 -0
  38. package/dist/db/migrations/042_dispatch_queue_patterns.d.ts +4 -0
  39. package/dist/db/types.d.ts +144 -10
  40. package/dist/diagnostics/fleet-collector.d.ts +1 -1
  41. package/dist/entry-helpers.d.ts +7 -29
  42. package/dist/environments/held-runs.d.ts +33 -0
  43. package/dist/index.js +1 -0
  44. package/dist/lockfile-redos-guard.d.ts +19 -0
  45. package/dist/metrics/prometheus.d.ts +17 -2
  46. package/dist/metrics/scheduled-jobs.d.ts +2 -2
  47. package/dist/orchestrator-core.d.ts +94 -1
  48. package/dist/pipeline/dispatch-matched-workflow.d.ts +60 -2
  49. package/dist/pipeline/install-secrets-resolver.d.ts +32 -5
  50. package/dist/pipeline/needs-scheduler.d.ts +12 -10
  51. package/dist/pipeline/pending-workflow-context.d.ts +44 -0
  52. package/dist/pipeline/processor.d.ts +31 -4
  53. package/dist/pipeline/remote-source-store.d.ts +21 -0
  54. package/dist/pipeline/resume-workflow.d.ts +26 -0
  55. package/dist/pipeline/wave-scheduler.d.ts +60 -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/queue/job-queue.d.ts +61 -2
  62. package/dist/reporting/execution-tracker.d.ts +61 -0
  63. package/dist/routes/admin-events.d.ts +3 -4
  64. package/dist/routes/uploads.d.ts +36 -26
  65. package/dist/server.js +53755 -50888
  66. package/dist/sources/build-platform-sources.d.ts +4 -2
  67. package/dist/stale-detector/stale-run-detector.d.ts +14 -1
  68. package/dist/standalone.js +17234 -15725
  69. package/dist/webhook/generic-sources-listener.d.ts +4 -0
  70. package/dist/webhook/generic-sources.d.ts +37 -11
  71. package/dist/webhook/register-source-bundle.d.ts +14 -4
  72. package/dist/ws/agent-handler.d.ts +17 -0
  73. package/dist/ws/dashboard-dispatch-guard.d.ts +21 -0
  74. package/dist/ws/dashboard-env-handler.d.ts +13 -1
  75. package/dist/ws/oidc-token-relay.d.ts +59 -0
  76. package/dist/ws/platform-client.d.ts +30 -1
  77. package/dist/ws/test-relay-handlers.d.ts +112 -0
  78. package/installer-image-digests.json +3 -3
  79. package/package.json +22 -22
  80. package/sbom.spdx.json +3228 -6633
  81. package/dist/providers/internal/index.d.ts +0 -32
  82. package/dist/routes/test-trigger.d.ts +0 -41
  83. package/dist/ws/observer-handler.d.ts +0 -42
@@ -55,6 +55,13 @@ export declare class Dispatcher {
55
55
  private readonly agentJobs;
56
56
  /** Reverse map: jobId -> agentId, for cancel-run lookups. */
57
57
  private readonly jobToAgent;
58
+ /**
59
+ * jobId -> runId for currently-tracked jobs. Populated alongside
60
+ * `jobToAgent`/`agentJobs` at dispatch time and cleared with them, so the
61
+ * provenance token relay can resolve a job's runId from the dispatcher's own
62
+ * state (`resolveOwnedJob`) rather than trusting an agent-asserted value.
63
+ */
64
+ private readonly jobRunIds;
58
65
  /**
59
66
  * Grace window for recently completed jobs.
60
67
  * Allows late messages (log.chunk, step.status) to be accepted for a short
@@ -143,6 +150,15 @@ export declare class Dispatcher {
143
150
  * 4. If queue full: return 'rejected'.
144
151
  */
145
152
  dispatch(job: QueuedJobInput): Promise<DispatchResult>;
153
+ /**
154
+ * Dispatch a host-fanout pinned child. The job targets exactly
155
+ * `job.pinnedAgentId`: if that agent is locally connected, satisfies the
156
+ * runsOn/exclude/mandatory gate, and has capacity, dispatch immediately;
157
+ * otherwise queue it WITH the pin so the pin-aware drain delivers it when the
158
+ * agent frees up or (re)connects. A pinned job never falls through to a
159
+ * different agent.
160
+ */
161
+ private dispatchPinned;
146
162
  /**
147
163
  * Eagerly dispatch a specific bound job to a freshly-registered
148
164
  * scaler-managed agent.
@@ -296,7 +312,7 @@ export declare class Dispatcher {
296
312
  * reconnecting agent reporting the job in-flight means it had started,
297
313
  * so mark it started for disconnect triage.
298
314
  */
299
- restoreJobForAgent(agentId: string, jobId: string): void;
315
+ restoreJobForAgent(agentId: string, jobId: string, runId?: string): void;
300
316
  /**
301
317
  * Start a recovery timer for a job found in 'dispatched' state on startup.
302
318
  * Called by server.ts/standalone.ts to create recovery timers for jobs
@@ -349,6 +365,16 @@ export declare class Dispatcher {
349
365
  /** Remove expired entries from the completedJobs grace window map. */
350
366
  private cleanupExpiredGraceEntries;
351
367
  private trackJobForAgent;
368
+ /**
369
+ * Verify `agentId` currently owns `jobId`; if so, return the job's runId from
370
+ * the dispatcher's own tracking. Used by the provenance token relay so the
371
+ * orchestrator binds a minted token to a job the agent actually owns — the
372
+ * agent never asserts a runId. Returns `undefined` when the agent does not
373
+ * own the job (or the runId is no longer tracked).
374
+ */
375
+ resolveOwnedJob(agentId: string, jobId: string): {
376
+ runId: string;
377
+ } | undefined;
352
378
  /**
353
379
  * Cancel a queued job (mark as failed in the dispatch queue).
354
380
  * Used by the coordinator to clean up local fallback entries when a peer
@@ -0,0 +1,42 @@
1
+ import type { HostRosterStore } from './host-roster.js';
2
+ export interface HostRosterReaperOptions {
3
+ store: Pick<HostRosterStore, 'reapEphemeralPastTtl' | 'countStaticUnreachable'>;
4
+ ttlMs: number;
5
+ /** Grace window for the connected-but-stale case in `countStaticUnreachable`. */
6
+ graceMs: number;
7
+ scanIntervalMs: number;
8
+ /** Setter for the `kici_orch_declared_hosts_unreachable` gauge. */
9
+ setUnreachableGauge: (value: number) => void;
10
+ }
11
+ /**
12
+ * Leader-only host-roster reaper.
13
+ *
14
+ * Modeled on `EventRetryScanner`: started/stopped via the Raft leadership
15
+ * callbacks, a single timer cleared on `stop()`. Each leader tick it:
16
+ *
17
+ * - DELETEs `ephemeral` host_roster rows whose `last_seen` is older than the ttl
18
+ * (scaled-down autoscale agents — silent GC, no alarm); and
19
+ * - counts `static` (declared) hosts whose derived status is `unreachable` and
20
+ * publishes that count to the `kici_orch_declared_hosts_unreachable` gauge, so
21
+ * a Mimir ruler alert can page an operator when a declared box goes dark.
22
+ *
23
+ * `static` unreachability is read-derived by `deriveHostStatus` (no write here),
24
+ * so the reaper never deletes a static row. Followers never run the reaper, so
25
+ * the gauge series exists only on the leader (the alert uses `max by ()`).
26
+ */
27
+ export declare class HostRosterReaper {
28
+ private readonly store;
29
+ private readonly ttlMs;
30
+ private readonly graceMs;
31
+ private readonly scanIntervalMs;
32
+ private readonly setUnreachableGauge;
33
+ private timer;
34
+ private isLeader;
35
+ constructor(opts: HostRosterReaperOptions);
36
+ onBecomeLeader(): void;
37
+ onLoseLeadership(): void;
38
+ stop(): void;
39
+ /** One reap pass. Public for tests. */
40
+ tick(): Promise<void>;
41
+ }
42
+ //# sourceMappingURL=host-roster-reaper.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { type Kysely } from 'kysely';
2
+ import { type LabelMatcher } from '@kici-dev/engine';
3
+ import type { Database, HostRosterRow } from '../db/types.js';
4
+ /**
5
+ * Derived (read-time) status of a roster host. Never a stored mutable column:
6
+ * status is computed from the shared `last_seen` + `connected_instance_id` so
7
+ * every cluster instance agrees regardless of which one holds the live WS.
8
+ */
9
+ export declare enum HostStatus {
10
+ /** Connected to some instance AND heartbeat fresh. */
11
+ ready = "ready",
12
+ /** Static host that is not currently live (declared, rebooting, or gone). */
13
+ unreachable = "unreachable",
14
+ /** Ephemeral host past ttl but not yet reaped (scaled down). */
15
+ stale = "stale"
16
+ }
17
+ /** The lifecycle class snapshot from the auth token's `agent_type`. */
18
+ export type LifecycleClass = 'static' | 'ephemeral';
19
+ /**
20
+ * A roster host that matched a `runsOnAll` predicate, with its derived status.
21
+ * The host-fanout resolver consumes this: `ready` hosts become pinned children;
22
+ * `unreachable` static hosts are subject to the `onUnreachable` policy; `stale`
23
+ * ephemeral hosts are skipped.
24
+ */
25
+ export interface MatchedHost {
26
+ agentId: string;
27
+ /** Hostname (falls back to agentId). */
28
+ host: string;
29
+ labels: string[];
30
+ lifecycleClass: LifecycleClass;
31
+ /** Which orchestrator holds the live WS (null = disconnected). For the cross-cluster pin. */
32
+ connectedInstanceId: string | null;
33
+ status: HostStatus;
34
+ platform: string | null;
35
+ arch: string | null;
36
+ }
37
+ export interface UpsertHostInput {
38
+ agentId: string;
39
+ tokenId: string | null;
40
+ lifecycleClass: LifecycleClass;
41
+ labels: string[];
42
+ hostname: string | null;
43
+ platform: string;
44
+ arch: string;
45
+ instanceId: string;
46
+ }
47
+ /** Minimal row shape `deriveHostStatus` reads (the store row or the CLI row). */
48
+ export interface HostStatusRow {
49
+ connected_instance_id: string | null;
50
+ lifecycle_class: string;
51
+ last_seen: Date | string;
52
+ }
53
+ /**
54
+ * The ONE status-derivation function — used by the store and the kici-admin
55
+ * host CLI. `ready` requires the host to be genuinely live (connected to some
56
+ * instance AND heartbeat fresh — the freshness check catches a crashed
57
+ * instance that never cleared `connected_instance_id`). It never returns
58
+ * `ready` for a not-currently-live host: a not-live `static` reads
59
+ * `unreachable` (the declared-but-absent alarm), a not-live `ephemeral` reads
60
+ * `stale` (scaled down, awaiting reap).
61
+ */
62
+ export declare function deriveHostStatus(row: HostStatusRow, nowMs: number, graceMs: number): HostStatus;
63
+ /**
64
+ * Durable, cluster-shared roster of every agent the cluster has ever enrolled.
65
+ *
66
+ * The in-memory `AgentRegistry` reconciles into this table on register
67
+ * (`upsert`) / unregister (`markDisconnected`), with a coarse heartbeat
68
+ * (`stampLastSeen`). All liveness writes are owner-guarded by `instanceId` so a
69
+ * stale disconnect from an old instance can never clobber a row a different
70
+ * instance now owns. The leader-only reaper deletes `ephemeral` rows past their
71
+ * ttl via `reapEphemeralPastTtl`; `static` rows persist and read `unreachable`.
72
+ */
73
+ export declare class HostRosterStore {
74
+ private readonly db;
75
+ constructor(db: Kysely<Database>);
76
+ /** Idempotent upsert on agent_id; stamps connected_instance_id + last_seen. */
77
+ upsert(input: UpsertHostInput): Promise<void>;
78
+ /** Clear liveness on disconnect — but only if THIS instance still owns it. */
79
+ markDisconnected(agentId: string, instanceId: string): Promise<void>;
80
+ /** Coarse heartbeat stamp — same owner-guard as markDisconnected. */
81
+ stampLastSeen(agentId: string, instanceId: string): Promise<void>;
82
+ /** Operator pre-declare of a static host before its agent dials in. */
83
+ declareStatic(input: {
84
+ agentId: string;
85
+ labels: string[];
86
+ hostname?: string;
87
+ }): Promise<void>;
88
+ get(agentId: string): Promise<HostRosterRow | null>;
89
+ listAll(): Promise<HostRosterRow[]>;
90
+ /**
91
+ * Resolve every roster host matching a `runsOnAll` predicate (OR-of-AND
92
+ * include groups, minus exclude labels), tagged with its derived status. This
93
+ * is the host-fanout resolver: it returns declared-but-absent static hosts
94
+ * (status `unreachable`) so the caller can apply `onUnreachable` — the live
95
+ * registry alone cannot name an expected-but-absent host.
96
+ */
97
+ findMatching(include: readonly (readonly LabelMatcher[])[], exclude: readonly LabelMatcher[], graceMs: number): Promise<MatchedHost[]>;
98
+ /**
99
+ * Count `static` (declared) hosts whose derived status is `unreachable` —
100
+ * the "declared-but-absent" alarm population. Reuses the single-source
101
+ * {@link deriveHostStatus} so the count never diverges from what
102
+ * `kici-admin host list` shows. A not-currently-connected static host reads
103
+ * `unreachable` regardless of grace (only the connected-but-stale case
104
+ * depends on `graceMs`).
105
+ */
106
+ countStaticUnreachable(graceMs: number): Promise<number>;
107
+ /** Delete ephemeral rows whose last_seen is older than ttl. Returns count. */
108
+ reapEphemeralPastTtl(ttlMs: number): Promise<number>;
109
+ }
110
+ //# sourceMappingURL=host-roster.d.ts.map
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * No persistence needed -- agents re-register on reconnect.
10
10
  */
11
+ import { type LabelMatcher } from '@kici-dev/engine';
11
12
  import type { WsLike } from '@kici-dev/engine';
12
13
  export type { WsLike } from '@kici-dev/engine';
13
14
  /**
@@ -76,6 +77,13 @@ export interface AgentEntry {
76
77
  memoryUsedMb: number | null;
77
78
  memoryAvailableMb: number | null;
78
79
  uptimeSeconds: number | null;
80
+ /**
81
+ * The auth token's `agent_type` (`'static'` | `'ephemeral'`) snapshot, or
82
+ * `null` when auth mode is `none`. Drives the host roster's
83
+ * `lifecycle_class` — `static` rows persist + alarm on absence, `ephemeral`
84
+ * rows are GC'd past their ttl.
85
+ */
86
+ tokenAgentType: 'static' | 'ephemeral' | null;
79
87
  }
80
88
  /** Optional static metadata from the agent.register message. */
81
89
  interface AgentMetadata {
@@ -106,6 +114,36 @@ interface AgentMetadata {
106
114
  * scaler registration metadata.
107
115
  */
108
116
  scalerManaged?: boolean;
117
+ /**
118
+ * The auth token's `agent_type` (`'static'` | `'ephemeral'`), threaded from
119
+ * `agent-handler.ts` so the host roster can snapshot it as the row's
120
+ * `lifecycle_class`. `null` / undefined when auth mode is `none`.
121
+ */
122
+ tokenAgentType?: 'static' | 'ephemeral' | null;
123
+ }
124
+ /**
125
+ * Subset of `HostRosterStore` the registry reconciles into. Optional — when no
126
+ * store is injected (e.g. workers, which have no DB), the reconcile hooks are
127
+ * a no-op and the in-memory registry behaves exactly as before.
128
+ */
129
+ export interface RosterReconciler {
130
+ upsert(input: {
131
+ agentId: string;
132
+ tokenId: string | null;
133
+ lifecycleClass: 'static' | 'ephemeral';
134
+ labels: string[];
135
+ hostname: string | null;
136
+ platform: string;
137
+ arch: string;
138
+ instanceId: string;
139
+ }): Promise<void>;
140
+ markDisconnected(agentId: string, instanceId: string): Promise<void>;
141
+ stampLastSeen(agentId: string, instanceId: string): Promise<void>;
142
+ }
143
+ /** Optional dependencies that enable host-roster reconciliation. */
144
+ export interface AgentRegistryDeps {
145
+ rosterStore?: RosterReconciler;
146
+ instanceId?: string;
109
147
  }
110
148
  export declare class AgentRegistry {
111
149
  /** Primary: agentId -> AgentEntry */
@@ -143,6 +181,24 @@ export declare class AgentRegistry {
143
181
  * propagation path to in-flight WS connections.
144
182
  */
145
183
  private readonly tokenExpiryTimers;
184
+ /**
185
+ * Per-agent last roster `last_seen` stamp time (epoch ms). Throttles the
186
+ * coarse heartbeat write so we don't issue a DB write per agent per
187
+ * heartbeat — `grace`/`ttl` are minute-scale, so a throttled stamp is
188
+ * sufficient. Cleaned up alongside the agent in `removeFromIndexes`.
189
+ */
190
+ private readonly lastRosterStampAt;
191
+ /** Minimum interval between coarse roster `last_seen` stamps per agent. */
192
+ private readonly ROSTER_STAMP_THROTTLE_MS;
193
+ /** Optional host-roster reconciler + this instance's id (see AgentRegistryDeps). */
194
+ private readonly rosterStore?;
195
+ private readonly instanceId?;
196
+ /**
197
+ * The roster reconcile seam is optional: when no store is injected (workers
198
+ * with no DB, unit tests), every reconcile hook is a no-op and the in-memory
199
+ * registry behaves exactly as before.
200
+ */
201
+ constructor(deps?: AgentRegistryDeps);
146
202
  /**
147
203
  * Register an agent. If the agentId already exists, updates the existing
148
204
  * entry (agent reconnection scenario).
@@ -209,7 +265,14 @@ export declare class AgentRegistry {
209
265
  * @param requiredLabels - All labels the agent must have (intersection semantics).
210
266
  * @returns Array of matching available AgentEntry objects.
211
267
  */
212
- findAvailable(requiredLabels: string[], excludeLabels?: string[]): AgentEntry[];
268
+ findAvailable(requiredLabels: string[], requiredPatterns?: LabelMatcher[], excludeLabels?: string[], excludePatterns?: LabelMatcher[]): AgentEntry[];
269
+ /**
270
+ * Whether a specific agent satisfies the same label / exclude / mandatory
271
+ * gate `findAvailable` applies per-agent, ignoring capacity. Used by the
272
+ * host-fanout pin to verify the resolved agent still matches before pinning
273
+ * (guards against roster label drift between resolution and dispatch).
274
+ */
275
+ agentSatisfies(agent: AgentEntry, requiredLabels: string[], requiredPatterns?: LabelMatcher[], excludeLabels?: string[], excludePatterns?: LabelMatcher[]): boolean;
213
276
  /**
214
277
  * Check if ANY registered agent matches the required labels, regardless of capacity.
215
278
  * Used to decide whether to queue (agent exists but busy) vs reject (no agent at all).
@@ -221,7 +284,7 @@ export declare class AgentRegistry {
221
284
  * an off-gate gated agent as "matching but busy" and skip the peer
222
285
  * reroute path even though the local agent can never accept the job.
223
286
  */
224
- hasMatchingAgent(requiredLabels: string[], excludeLabels?: string[]): boolean;
287
+ hasMatchingAgent(requiredLabels: string[], requiredPatterns?: LabelMatcher[], excludeLabels?: string[], excludePatterns?: LabelMatcher[]): boolean;
225
288
  /**
226
289
  * Internal helper: intersect the per-label candidate sets to find agentIds
227
290
  * that satisfy `requiredLabels`. Returns `null` when any required label
package/dist/app.d.ts CHANGED
@@ -16,6 +16,7 @@ import type { Kysely } from 'kysely';
16
16
  import type pg from 'pg';
17
17
  import { type ColdStore } from '@kici-dev/shared';
18
18
  import type { AppConfig } from './config.js';
19
+ import type { HostRosterStore } from './agent/host-roster.js';
19
20
  import type { Database } from './db/types.js';
20
21
  import type { AgentRegistry } from './agent/registry.js';
21
22
  import type { Dispatcher } from './agent/dispatcher.js';
@@ -81,6 +82,8 @@ export interface AppDependencies {
81
82
  db: Kysely<Database>;
82
83
  pool: pg.Pool;
83
84
  registry: AgentRegistry;
85
+ /** Host roster store for runsOnAll fan-out resolution. */
86
+ hostRosterStore?: HostRosterStore;
84
87
  dispatcher: Dispatcher;
85
88
  jobQueue: JobQueue;
86
89
  dedup: DedupCache;
@@ -40,15 +40,25 @@ export interface ApplyDecisionDeps {
40
40
  allowSelfApproval: boolean;
41
41
  /** Resolve the Keycloak sub of the user who triggered a run (for the self-approval gate). */
42
42
  resolveTriggererSub: (runId: string) => Promise<string | undefined>;
43
- /** Called when a job/workflow hold is released — re-dispatch the element. */
43
+ /** Called when a job hold is released — re-dispatch the element. */
44
44
  onJobRelease: (signal: ReleaseSignal) => Promise<void>;
45
45
  /** Called when a step hold is released — notify the waiting agent (approved). */
46
46
  onStepRelease?: (signal: ReleaseSignal) => Promise<void>;
47
+ /**
48
+ * Called when a workflow-scoped hold is released (install gate approved) —
49
+ * rebuild the dispatch context and resume the workflow from the install gate.
50
+ */
51
+ onWorkflowRelease?: (signal: ReleaseSignal) => Promise<void>;
47
52
  /**
48
53
  * Called when a step-scoped hold is rejected — notify the waiting agent so it
49
54
  * fails the step instead of blocking until expiry. Carries the holdId.
50
55
  */
51
56
  onStepReject?: (heldRunId: string, reason?: string) => Promise<void> | void;
57
+ /**
58
+ * Called when a workflow-scoped hold is rejected (install gate rejected) —
59
+ * cancel the run and drop the pending workflow context. Carries the runId.
60
+ */
61
+ onWorkflowReject?: (runId: string) => Promise<void>;
52
62
  }
53
63
  /** Apply a single approve/reject decision to a hold. */
54
64
  export declare function applyDecision(deps: ApplyDecisionDeps, args: {
@@ -12,6 +12,11 @@ export interface InitResult {
12
12
  environmentName?: string;
13
13
  env?: Record<string, string>;
14
14
  concurrencyGroup?: string;
15
+ /**
16
+ * Resolved matrix combinations when the target job's matrix is a dynamic
17
+ * function. The dispatch path re-materializes these into N execution jobs.
18
+ */
19
+ matrixValues?: Array<Record<string, string | undefined>>;
15
20
  }
16
21
  export declare class PendingInitTracker extends PendingTracker<InitResult> {
17
22
  constructor();
@@ -25,6 +25,14 @@ export interface FleetTopologyResponse {
25
25
  * avoid a cross-package import in the CLI wire type.
26
26
  */
27
27
  export type GenericSourceGitConfigPayload = Record<string, unknown>;
28
+ /**
29
+ * Wire shape for a local filesystem source's config (`{ repoBasePath, cloneUrlBase? }`).
30
+ * Matches `LocalSourceConfigSchema` in `providers/local/local-source-config.ts`.
31
+ */
32
+ export interface GenericSourceLocalConfigPayload {
33
+ repoBasePath: string;
34
+ cloneUrlBase?: string;
35
+ }
28
36
  /**
29
37
  * Response shape for generic webhook sources from the admin API.
30
38
  */
@@ -134,6 +142,7 @@ export declare class AdminApiClient {
134
142
  rateLimitRpm?: number;
135
143
  providerType?: string;
136
144
  gitConfig?: GenericSourceGitConfigPayload;
145
+ localConfig?: GenericSourceLocalConfigPayload;
137
146
  }): Promise<{
138
147
  source: GenericSourceResponse;
139
148
  }>;
@@ -159,6 +168,8 @@ export declare class AdminApiClient {
159
168
  providerType?: string;
160
169
  /** `null` clears the config; omit to leave unchanged. */
161
170
  gitConfig?: GenericSourceGitConfigPayload | null;
171
+ /** `null` clears the config; omit to leave unchanged. */
172
+ localConfig?: GenericSourceLocalConfigPayload | null;
162
173
  }): Promise<{
163
174
  source: GenericSourceResponse;
164
175
  }>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Host roster management commands for kici-admin (orchestrator data plane).
3
+ *
4
+ * host list List all roster hosts with derived status
5
+ * host get Show one roster host
6
+ * host declare Pre-declare a static host before its agent connects
7
+ *
8
+ * Reads/writes the orchestrator DB directly (via withDb), so this lives in
9
+ * kici-admin — never in the public `kici` or the Platform `kici-platform-admin`.
10
+ */
11
+ import type { Command } from 'commander';
12
+ export declare function registerHostCommands(program: Command): void;
13
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Render the post-receive hook script. For each pushed ref the hook calls
3
+ * `kici-admin source trigger-local <id> --ref <ref> --sha <new> --base-url <url>`,
4
+ * so a push to any branch dispatches a run for that ref.
5
+ */
6
+ export declare function renderPostReceiveHook(opts: {
7
+ sourceId: string;
8
+ baseUrl: string;
9
+ }): string;
10
+ /**
11
+ * Write the rendered hook into the repo's `.git/hooks/post-receive` and make it
12
+ * executable. Throws when the path is not a git repo (no `.git/hooks` dir).
13
+ */
14
+ export declare function installPostReceiveHook(repoPath: string, script: string): string;
15
+ //# sourceMappingURL=local-hook.d.ts.map
@@ -0,0 +1,24 @@
1
+ export interface LocalTriggerInput {
2
+ orgId: string;
3
+ sourceId: string;
4
+ repoFullName: string;
5
+ event: 'push' | 'pull_request';
6
+ ref: string;
7
+ sha: string;
8
+ defaultBranch: string;
9
+ }
10
+ export interface LocalTriggerRequest {
11
+ path: string;
12
+ headers: Record<string, string>;
13
+ body: string;
14
+ }
15
+ /** Build the GitHub-shaped webhook request the local provider normalizer expects. */
16
+ export declare function buildLocalTriggerRequest(input: LocalTriggerInput): LocalTriggerRequest;
17
+ /** Read HEAD ref + sha from a local git repo (used when the operator omits --ref/--sha). */
18
+ export declare function readRepoHead(repoPath: string): {
19
+ ref: string;
20
+ sha: string;
21
+ };
22
+ /** POST the trigger request to the orchestrator base URL. Returns the HTTP status. */
23
+ export declare function sendLocalTrigger(baseUrl: string, req: LocalTriggerRequest): Promise<number>;
24
+ //# sourceMappingURL=local-trigger.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Remote-source inspection command for kici-admin.
3
+ *
4
+ * Subcommand namespace: `kici-admin remote-source show`.
5
+ *
6
+ * The remote-source anchor (`remote_sources` table) maps the deterministic
7
+ * routing key `remote:<orgId>` to the orchestrator's canonical org id so a
8
+ * Platform-relayed `kici run remote` resolves the real tenant. It is
9
+ * auto-provisioned on Platform auth; this command lets an operator inspect or
10
+ * confirm the row when debugging org-anchor issues on a hidden orchestrator.
11
+ *
12
+ * Reads the orchestrator DB directly (on-host operator inspection), so it stays
13
+ * usable even when Platform is unreachable.
14
+ */
15
+ import type { Command } from 'commander';
16
+ export declare function registerRemoteSourceCommands(program: Command): void;
17
+ //# sourceMappingURL=remote-source.d.ts.map
@@ -85,6 +85,43 @@ export declare function getInstallBase(platform: ServicePlatform, name: string):
85
85
  * 5. Update symlink (Unix) or service registration (Windows)
86
86
  * 6. Start service
87
87
  */
88
+ /**
89
+ * Resolve the target version for an npm-source upgrade (no --from/--url).
90
+ *
91
+ * The npm-source flow assumes the operator already ran
92
+ * `npm install -g @kici-dev/<pkg>@<version>`, which overwrites the global
93
+ * package in place. The running `kici-admin` binary is therefore the new
94
+ * version, and `running` is its self-reported version. We default the target
95
+ * to it, or validate an explicitly-passed --version matches — a mismatch means
96
+ * the npm install did not actually update the global binary.
97
+ */
98
+ export declare function resolveNpmSourceVersion(opts: {
99
+ requested: string | undefined;
100
+ running: string;
101
+ }): string;
102
+ /** Verdict from {@link verifyNpmSourceLaunch}. */
103
+ export type NpmSourceLaunchVerdict = {
104
+ ok: true;
105
+ version: string;
106
+ manifestVersion: string | null;
107
+ } | {
108
+ ok: false;
109
+ reason: string;
110
+ };
111
+ /**
112
+ * Decide whether an npm-source upgrade may proceed, given the version the
113
+ * invoking CLI is (`invoked`) and the version the installed unit will actually
114
+ * launch (`launched`, or null when unresolvable — e.g. an opaque --binary
115
+ * install). On success `manifestVersion` is the value to persist (null = leave
116
+ * the manifest's kiciVersion unchanged because we couldn't verify it).
117
+ */
118
+ export declare function verifyNpmSourceLaunch(opts: {
119
+ component: UpgradeComponent;
120
+ invoked: string;
121
+ launched: string | null;
122
+ launchedPath: string | null;
123
+ force: boolean;
124
+ }): NpmSourceLaunchVerdict;
88
125
  export declare function performVersionedUpgrade(component: UpgradeComponent, opts: VersionedUpgradeOptions): Promise<void>;
89
126
  export {};
90
127
  //# sourceMappingURL=versioned-upgrade.d.ts.map
@@ -19,4 +19,17 @@ import { Command } from 'commander';
19
19
  export declare function buildProgram(): Command;
20
20
  /** Build the program and parse argv — the bin-shim entry point. */
21
21
  export declare function runCli(argv?: string[]): void;
22
+ /**
23
+ * Canonicalize a path through any symlinks, falling back to a plain `resolve`
24
+ * when the target does not exist on disk. The entry-point guard compares the
25
+ * invoked script path against this module's own path; both sides must be fully
26
+ * canonicalized or a symlinked invocation path silently fails the match. The
27
+ * canonical example is the light-package launcher running
28
+ * `node /tmp/.../kici-admin.cjs` on macOS, where `/tmp` is a symlink to
29
+ * `/private/tmp`: `process.argv[1]` keeps the `/tmp` form while the bundle's
30
+ * `import.meta.url` resolves to the real `/private/tmp` path, so an
31
+ * un-canonicalized comparison is always false and the CLI exits 0 without
32
+ * running any command.
33
+ */
34
+ export declare function canonicalize(p: string): string;
22
35
  //# sourceMappingURL=kici-admin.d.ts.map
@@ -6,7 +6,7 @@
6
6
  * a container-based service management backend that works on any
7
7
  * platform with Docker or Podman installed.
8
8
  */
9
- import type { ServiceManager, ServiceConfig, ServiceStatus, LogOptions, DiscoveredInstance } from './types.js';
9
+ import type { ServiceManager, ServiceConfig, ServiceStatus, LogOptions, DiscoveredInstance, LaunchSpec } from './types.js';
10
10
  export declare class ComposeServiceManager implements ServiceManager {
11
11
  private runtime;
12
12
  /** Get the runtime binary (`podman` or `docker`), detecting it if not already done. */
@@ -22,5 +22,6 @@ export declare class ComposeServiceManager implements ServiceManager {
22
22
  logs(config: ServiceConfig, options: LogOptions): Promise<void>;
23
23
  isInstalled(config: ServiceConfig): Promise<boolean>;
24
24
  list(isUserLevel: boolean): Promise<DiscoveredInstance[]>;
25
+ readLaunchSpec(_config: ServiceConfig): Promise<LaunchSpec | null>;
25
26
  }
26
27
  //# sourceMappingURL=compose.d.ts.map
@@ -6,13 +6,13 @@
6
6
  * specified) platform. Uses dynamic imports to avoid loading all
7
7
  * platform-specific code on every platform.
8
8
  */
9
- export type { ServiceManager, ServiceConfig, ServiceStatus, ServiceState, ServicePlatform, RestartPolicy, LogOptions, } from './types.js';
9
+ export type { ServiceManager, ServiceConfig, LaunchSpec, ServiceStatus, ServiceState, ServicePlatform, RestartPolicy, LogOptions, } from './types.js';
10
10
  export { DEFAULT_RESTART_POLICY } from './types.js';
11
11
  export { detectPlatform, isRoot, kiciConfigRoot, getConfigDir, getLogDir, getCacheDir, } from './platform-detect.js';
12
12
  export { resolveUserLevel, type PrivilegeOpts } from './privilege.js';
13
13
  export type { Component, InstanceManifest, IndexEntry, ResolveOptions, ResolvedInstance, } from './instance/types.js';
14
14
  export { COMPONENTS, isComponent } from './instance/types.js';
15
- export { manifestFilename, manifestPath, readManifest, writeManifest, readKiciVersion, } from './instance/manifest.js';
15
+ export { manifestFilename, manifestPath, readManifest, writeManifest, readKiciVersion, resolveVersionFromLaunchSpec, } from './instance/manifest.js';
16
16
  export { indexPath, readIndex, writeIndex, appendIndexEntry, removeIndexEntry, } from './instance/index-file.js';
17
17
  export { listInstances, resolveInstance, formatRefusal } from './instance/resolve.js';
18
18
  export type { ListedInstance, ResolveArgs, ListInstancesArgs } from './instance/resolve.js';
@@ -4,6 +4,7 @@
4
4
  * every lifecycle command to reconstruct the ServiceConfig.
5
5
  */
6
6
  import type { Component, InstanceManifest } from './types.js';
7
+ import type { LaunchSpec } from '../types.js';
7
8
  /** Per-component manifest filename. */
8
9
  export declare function manifestFilename(component: Component): string;
9
10
  /** Resolve the manifest path inside an instance directory. */
@@ -27,4 +28,15 @@ export declare function writeManifest(instanceDir: string, manifest: InstanceMan
27
28
  * reliable source.
28
29
  */
29
30
  export declare function readKiciVersion(): string;
31
+ /**
32
+ * Resolve the `@kici-dev/<component>` package version that a {@link LaunchSpec}
33
+ * will actually execute, by locating the component's entry script among the
34
+ * spec's tokens and reading the version from the package.json that owns it.
35
+ *
36
+ * Returns null when no resolvable entry script is present (e.g. an opaque
37
+ * `--binary` install) or the owning package.json is missing / name-mismatched.
38
+ * This is what lets the npm-source upgrade refuse to report a version it can't
39
+ * stand behind.
40
+ */
41
+ export declare function resolveVersionFromLaunchSpec(spec: LaunchSpec, component: Component): string | null;
30
42
  //# sourceMappingURL=manifest.d.ts.map
@@ -27,8 +27,18 @@ export interface ListInstancesArgs {
27
27
  }
28
28
  /**
29
29
  * Reconcile <kiciRoot>/instances.json with the driver's native scan, then
30
- * rewrite the index dropping entries whose units no longer exist. Returns
31
- * the merged list filtered to the requested component + isUserLevel.
30
+ * rewrite the index so it mirrors the scanned instances that have a known
31
+ * instanceDir. Two self-heal directions happen here:
32
+ *
33
+ * - backward: drop index entries whose unit no longer exists.
34
+ * - forward: adopt an instanceDir the driver recovered from the unit marker
35
+ * when the index entry is missing or carries no dir — so a lost or emptied
36
+ * index rebuilds itself from the init system on the next read.
37
+ *
38
+ * The init system is therefore the source of truth for the name→folder mapping
39
+ * (via the X-KiCI-InstanceDir / KiCIInstanceDir / dev.kici.instance-dir / [KiCI-DIR]
40
+ * markers), and the index is a pure cache. Returns the merged list filtered to
41
+ * the requested component + isUserLevel.
32
42
  */
33
43
  export declare function listInstances(args: ListInstancesArgs): Promise<ListedInstance[]>;
34
44
  export interface ResolveArgs {
@@ -5,7 +5,7 @@
5
5
  * Supports both system-level (/Library/LaunchDaemons/) and
6
6
  * user-level (~/Library/LaunchAgents/) agents.
7
7
  */
8
- import type { ServiceConfig, ServiceManager, ServiceStatus, LogOptions, DiscoveredInstance } from './types.js';
8
+ import type { ServiceConfig, ServiceManager, ServiceStatus, LogOptions, DiscoveredInstance, LaunchSpec } from './types.js';
9
9
  export declare class LaunchdServiceManager implements ServiceManager {
10
10
  /** Build the launchd label for a service. */
11
11
  private label;
@@ -17,6 +17,8 @@ export declare class LaunchdServiceManager implements ServiceManager {
17
17
  private parseEnvFile;
18
18
  /** Escape XML special characters. */
19
19
  private escapeXml;
20
+ /** Inverse of {@link escapeXml} — decode the entities back to literals. */
21
+ private unescapeXml;
20
22
  /**
21
23
  * Generate a launchd plist XML string from a service config.
22
24
  * Visible for testing.
@@ -67,5 +69,6 @@ export declare class LaunchdServiceManager implements ServiceManager {
67
69
  logs(config: ServiceConfig, options: LogOptions): Promise<void>;
68
70
  isInstalled(config: ServiceConfig): Promise<boolean>;
69
71
  list(isUserLevel: boolean): Promise<DiscoveredInstance[]>;
72
+ readLaunchSpec(config: ServiceConfig): Promise<LaunchSpec | null>;
70
73
  }
71
74
  //# sourceMappingURL=launchd.d.ts.map