@kici-dev/orchestrator 0.1.20 → 0.1.22

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 (71) hide show
  1. package/dist/agent/dispatcher.d.ts +44 -0
  2. package/dist/agent/host-roster-reaper.d.ts +2 -1
  3. package/dist/agent/host-roster.d.ts +54 -1
  4. package/dist/agent/registry.d.ts +22 -0
  5. package/dist/app.d.ts +5 -0
  6. package/dist/approvals/step-approval-bridge.d.ts +5 -0
  7. package/dist/cli/commands/source-manifest.d.ts +42 -0
  8. package/dist/cli/loopback-callback.d.ts +24 -0
  9. package/dist/cli/open-browser.d.ts +12 -0
  10. package/dist/cli/service/compose.d.ts +13 -0
  11. package/dist/cli/service/deploy-env.d.ts +31 -0
  12. package/dist/cli.js +1204 -256
  13. package/dist/cluster/coordinator.d.ts +5 -3
  14. package/dist/cluster/index.d.ts +2 -0
  15. package/dist/cluster/join-token.d.ts +27 -5
  16. package/dist/cluster/peer-auth-coordinator.d.ts +36 -0
  17. package/dist/cluster/peer-client.d.ts +34 -14
  18. package/dist/cluster/peer-credentials.d.ts +14 -2
  19. package/dist/cluster/peer-handler.d.ts +2 -2
  20. package/dist/cluster/rerouted-job-guard.d.ts +39 -0
  21. package/dist/config.d.ts +6 -0
  22. package/dist/dashboard/needs-edges.d.ts +4 -3
  23. package/dist/db/migrations/043_rerouted_to_peer.d.ts +4 -0
  24. package/dist/db/migrations/044_check_mode.d.ts +4 -0
  25. package/dist/db/migrations/045_host_properties.d.ts +16 -0
  26. package/dist/db/migrations/046_join_token_consumed_by_instance.d.ts +17 -0
  27. package/dist/db/migrations/047_needs_run_on.d.ts +4 -0
  28. package/dist/db/migrations/048_host_reboot_pending.d.ts +19 -0
  29. package/dist/db/migrations/049_held_runs_payload.d.ts +14 -0
  30. package/dist/db/migrations/050_sources_slug.d.ts +17 -0
  31. package/dist/db/types.d.ts +60 -5
  32. package/dist/deployment/deployment-identity.d.ts +9 -0
  33. package/dist/entry-helpers.d.ts +7 -0
  34. package/dist/environments/held-runs.d.ts +7 -1
  35. package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +77 -0
  36. package/dist/index.d.ts +5 -1
  37. package/dist/index.js +1187 -33
  38. package/dist/lockfile-validate.d.ts +24 -0
  39. package/dist/orchestrator-core.d.ts +13 -1
  40. package/dist/pipeline/decorating-secret-resolver.d.ts +32 -0
  41. package/dist/pipeline/dispatch-matched-workflow.d.ts +127 -3
  42. package/dist/pipeline/install-secrets-resolver.d.ts +2 -2
  43. package/dist/pipeline/needs-scheduler.d.ts +22 -13
  44. package/dist/pipeline/processor.d.ts +2 -2
  45. package/dist/pipeline/test-pipeline.d.ts +37 -58
  46. package/dist/providers/github/manifest-form.d.ts +15 -0
  47. package/dist/providers/github/manifest.d.ts +103 -0
  48. package/dist/reporting/execution-tracker.d.ts +10 -1
  49. package/dist/routes/admin-sources.d.ts +18 -0
  50. package/dist/routes/admin.d.ts +8 -0
  51. package/dist/secrets/pg-secret-store.d.ts +9 -0
  52. package/dist/secrets/secret-resolver.d.ts +16 -1
  53. package/dist/server.js +4373 -2322
  54. package/dist/sources/source-store.d.ts +4 -0
  55. package/dist/sources/source-validator.d.ts +2 -0
  56. package/dist/stale-detector/reboot-deadline-sweep.d.ts +29 -0
  57. package/dist/stale-detector/stale-run-detector.d.ts +8 -0
  58. package/dist/standalone.js +3582 -1932
  59. package/dist/worker/in-memory-job-queue.d.ts +17 -0
  60. package/dist/worker/peer-outbox.d.ts +36 -0
  61. package/dist/worker/worker-outbox-relay.d.ts +13 -0
  62. package/dist/ws/agent-handler.d.ts +11 -6
  63. package/dist/ws/dashboard-fleet-handler.d.ts +33 -0
  64. package/dist/ws/dashboard-fleet-write-handler.d.ts +60 -0
  65. package/dist/ws/fleet-runs-on-all.d.ts +16 -0
  66. package/dist/ws/inventory-api.d.ts +17 -0
  67. package/dist/ws/platform-client.d.ts +13 -1
  68. package/dist/ws/test-relay-handlers.d.ts +4 -2
  69. package/installer-image-digests.json +3 -3
  70. package/package.json +4 -4
  71. package/sbom.spdx.json +77 -128
@@ -10,7 +10,7 @@
10
10
  * - "Peers report step-by-step progress back to coordinator"
11
11
  * - "Cancel mode: graceful -- finish current step, cancel remaining"
12
12
  */
13
- import type { JobReroute, JobProgress, PeerScalerEvent, PeerToPeerMessage, ResourceRequest, LabelMatcher } from '@kici-dev/engine';
13
+ import type { JobReroute, JobProgress, JobProgressAck, PeerScalerEvent, PeerToPeerMessage, ResourceRequest, LabelMatcher } from '@kici-dev/engine';
14
14
  import type { PeerRegistry } from './peer-registry.js';
15
15
  import type { PeerClient } from './peer-client.js';
16
16
  import type { Dispatcher } from '../agent/dispatcher.js';
@@ -142,7 +142,7 @@ export declare class RunCoordinator {
142
142
  * this split, every job-level event was silently funnelled into
143
143
  * `onStepStatus` and the run stayed in `running` forever.
144
144
  */
145
- onPeerJobProgress(msg: JobProgress): void;
145
+ onPeerJobProgress(msg: JobProgress, reply?: (m: JobProgressAck) => void): void;
146
146
  /**
147
147
  * Handle a scaler provisioning event forwarded by a worker peer.
148
148
  *
@@ -211,7 +211,9 @@ export declare class RunCoordinator {
211
211
  */
212
212
  private computeAvailableCapacity;
213
213
  /**
214
- * Track a rerouted job for cancel propagation.
214
+ * Track a rerouted job for cancel propagation, and durably tag the projected
215
+ * `execution_jobs` row with the owning worker peer so run-recovery sweepers
216
+ * do not force-fail the job while its worker is connected.
215
217
  */
216
218
  private trackReroutedJob;
217
219
  }
@@ -8,6 +8,8 @@ export { PeerRegistry } from './peer-registry.js';
8
8
  export type { PeerInfo, PeerAgentInfo, PeerRegistryOptions } from './peer-registry.js';
9
9
  export { PeerClient } from './peer-client.js';
10
10
  export type { PeerClientOptions } from './peer-client.js';
11
+ export { PeerAuthCoordinator } from './peer-auth-coordinator.js';
12
+ export type { AuthDecision, RejectionAction } from './peer-auth-coordinator.js';
11
13
  export { createPeerHandler } from './peer-handler.js';
12
14
  export type { PeerHandlerDeps } from './peer-handler.js';
13
15
  export { PeerCredentialStore, readCredentialFile, writeCredentialFile, } from './peer-credentials.js';
@@ -12,7 +12,7 @@
12
12
  * Config bundle encryption: AES-256-GCM with random 12-byte IV.
13
13
  * Wire format: <12-byte IV><16-byte auth tag><ciphertext>
14
14
  */
15
- import type { Kysely } from 'kysely';
15
+ import { Kysely } from 'kysely';
16
16
  /**
17
17
  * Silence the module-level logger (e.g. when a CLI is emitting JSON on stdout
18
18
  * and a stray log line would break the contract).
@@ -57,15 +57,37 @@ export declare class JoinTokenManager {
57
57
  * `peer_credentials_active_uniq` partial unique index).
58
58
  *
59
59
  * On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
60
- * / already-used so callers can branch on the specific reason — the
61
- * recovery path in peer-handler.ts keys on the "already used" string
62
- * specifically. The follow-up only fires on the unhappy path.
60
+ * / already-used / reusable-by-same-instance so callers can branch on the
61
+ * specific reason — the recovery path in peer-handler.ts keys on the
62
+ * "already used" string specifically. The follow-up only fires on the
63
+ * unhappy path.
64
+ *
65
+ * Self-healing reuse: a join token is re-consumable by the same joining
66
+ * peer (`peerInstanceId`) until its `expires_at`. A peer that lost its
67
+ * credential (transient outage / deleted credential file) re-presents the
68
+ * still-valid join token already in its env; the coordinator accepts the
69
+ * reuse and issues a fresh credential — no operator action, no cluster
70
+ * redeploy. Reuse is bounded by BOTH `expires_at` AND the consuming
71
+ * instanceId, so it never widens a leaked token's usefulness beyond the
72
+ * instance that first consumed it.
63
73
  */
64
- validateAndConsumeToken(token: string, consumedBy: string): Promise<{
74
+ validateAndConsumeToken(token: string, consumedBy: string, peerInstanceId: string): Promise<{
65
75
  routing: TokenRouting;
66
76
  keys: DerivedKeys;
67
77
  }>;
68
78
  }
79
+ /**
80
+ * Build a JoinTokenManager backed by its own connection pool to the given
81
+ * orchestrator database URL. Mirrors `createPeerCredentialStoreFromUrl`;
82
+ * consumed by E2E tests that need to exercise token validation/reuse against
83
+ * the real cluster DB.
84
+ */
85
+ export declare function createJoinTokenManagerFromUrl(databaseUrl: string, opts?: {
86
+ maxConnections?: number;
87
+ }): {
88
+ manager: JoinTokenManager;
89
+ dispose: () => Promise<void>;
90
+ };
69
91
  /**
70
92
  * Narrow detector for the "already been used" error thrown by
71
93
  * validateAndConsumeToken(). Used by peer-handler.ts to branch into the
@@ -0,0 +1,36 @@
1
+ import { type CredentialFileData } from './peer-credentials.js';
2
+ export type AuthDecision = {
3
+ mode: 'credential';
4
+ credential: CredentialFileData;
5
+ } | {
6
+ mode: 'token-join';
7
+ token: string;
8
+ complete: (issued: CredentialFileData | null) => void;
9
+ } | {
10
+ mode: 'no-auth';
11
+ };
12
+ export type RejectionAction = 'retry-credential' | 'rejoin';
13
+ export declare class PeerAuthCoordinator {
14
+ private readonly credentialFile;
15
+ private readonly instanceId;
16
+ private readonly joinToken?;
17
+ private readonly joinWaitTimeoutMs;
18
+ /** Promise-chain mutex tail; every file op awaits the prior one. */
19
+ private lock;
20
+ /** Set while one peer-client is mid token-join; siblings await it. */
21
+ private inFlightJoin;
22
+ constructor(opts: {
23
+ credentialFile: string;
24
+ instanceId: string;
25
+ joinToken?: string;
26
+ joinWaitTimeoutMs?: number;
27
+ });
28
+ /** Run `fn` exclusively against the credential file. */
29
+ private withLock;
30
+ private readValidCredential;
31
+ decideAuth(): Promise<AuthDecision>;
32
+ private makeComplete;
33
+ private awaitInFlightJoin;
34
+ reportRejection(provedCredential: string | null, reason: string): Promise<RejectionAction>;
35
+ }
36
+ //# sourceMappingURL=peer-auth-coordinator.d.ts.map
@@ -10,8 +10,9 @@
10
10
  * Authentication uses ECDH key exchange followed by join token (first connect)
11
11
  * or HMAC credential proof (reconnection).
12
12
  */
13
- import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
13
+ import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
14
14
  import type { PeerRegistry } from './peer-registry.js';
15
+ import type { PeerAuthCoordinator } from './peer-auth-coordinator.js';
15
16
  type PeerConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'authenticating' | 'connected';
16
17
  export interface PeerClientOptions {
17
18
  /** WebSocket URL of the remote peer orchestrator. */
@@ -20,6 +21,8 @@ export interface PeerClientOptions {
20
21
  joinToken?: string;
21
22
  /** Path to the credential file for reconnection. */
22
23
  credentialFile: string;
24
+ /** Shared per-orchestrator coordinator that owns the credential file. */
25
+ authCoordinator: PeerAuthCoordinator;
23
26
  /** This orchestrator's instance ID. */
24
27
  instanceId: string;
25
28
  /** Peer registry to update on heartbeats from remote peer. */
@@ -33,7 +36,14 @@ export interface PeerClientOptions {
33
36
  /** Callback when a job reroute request is received from peer. */
34
37
  onJobReroute: (msg: JobReroute) => Promise<void>;
35
38
  /** Callback when a job progress update is received from peer. */
36
- onJobProgress: (msg: JobProgress) => void;
39
+ onJobProgress: (msg: JobProgress, reply: (m: JobProgressAck) => void) => void;
40
+ /**
41
+ * Callback invoked once this client reaches the `connected` state (initial
42
+ * connect AND every reconnect). Carries this client's peer URL.
43
+ */
44
+ onConnected?: (url: string) => void;
45
+ /** Callback when a coordinator ACKs a terminal job.progress this worker sent. */
46
+ onJobProgressAck?: (msg: JobProgressAck) => void;
37
47
  /** This orchestrator's role. Default: 'coordinator'. */
38
48
  role?: 'coordinator' | 'worker';
39
49
  /** Callback when a job cancel request is received from peer. */
@@ -107,8 +117,13 @@ export declare class PeerClient {
107
117
  /** Correlates peer.logs.collect.request with the peer's chunked subtree response. */
108
118
  private readonly logsCollectWaiters;
109
119
  private readonly url;
110
- private readonly joinToken?;
111
- private readonly credentialFile;
120
+ private readonly authCoordinator;
121
+ /** Set true between deciding token-join and receiving the auth response. */
122
+ private isJoiner;
123
+ /** complete() callback for the in-flight token-join, if this client is joiner. */
124
+ private joinComplete;
125
+ /** The credential string this client last built an HMAC proof with. */
126
+ private lastProvedCredential;
112
127
  private readonly instanceId;
113
128
  private readonly role;
114
129
  private readonly peerRegistry;
@@ -117,6 +132,8 @@ export declare class PeerClient {
117
132
  private readonly maxReconnectDelayMs;
118
133
  private readonly onJobReroute;
119
134
  private readonly onJobProgress;
135
+ private readonly onConnected?;
136
+ private readonly onJobProgressAck?;
120
137
  private readonly onJobCancel;
121
138
  private readonly onPeerLogChunk?;
122
139
  private readonly onPeerCacheUploadRequest?;
@@ -185,6 +202,15 @@ export declare class PeerClient {
185
202
  getReconnectDelay(): number;
186
203
  private doConnect;
187
204
  private handleMessage;
205
+ /**
206
+ * Handle a rejected peer.auth.response. For the three credential-divergence
207
+ * reasons, delegate to the coordinator (which deletes the shared file only if
208
+ * no sibling has refreshed it, preventing a revocation cascade). A failed
209
+ * token-join releases the in-flight join so siblings can retry. Config-error
210
+ * reasons (role mismatch / missing auth method / protocol version) skip the
211
+ * coordinator entirely — deletion would not help.
212
+ */
213
+ private handleAuthRejected;
188
214
  /**
189
215
  * Determine auth method and send encrypted auth request.
190
216
  *
@@ -196,16 +222,10 @@ export declare class PeerClient {
196
222
  * connecting to any peer can use the same credential, as long as the
197
223
  * `instanceId` on disk matches ours.
198
224
  *
199
- * (4-coordinator mesh) bug history: the previous gate was
200
- * `cred.coordinatorUrl === this.coordinatorUrl`, which broke the moment a
201
- * second peer-client on the same orchestrator tried to authenticate to a
202
- * different peer URL it would see the first peer-client's credential,
203
- * reject it (URL mismatch), fall through to the join token, and get
204
- * permanently rejected because the token had already been consumed by the
205
- * first peer-client. The Option B fix (commit 79f93da4) keyed the match on
206
- * `instanceId`; this follow-up (Plan 04 Task 1 cleanup) drops the
207
- * `coordinatorUrl` field from the credential file schema entirely because
208
- * it was dead weight once the match switched to identity scope.
225
+ * The auth-method decision is delegated to the shared `PeerAuthCoordinator`,
226
+ * which serializes sibling peer-clients so exactly one token-joins per
227
+ * reconnect storm (the rest reuse the freshly-written credential). This
228
+ * client never reads or writes the credential file directly.
209
229
  */
210
230
  private sendAuthRequest;
211
231
  private routeMessage;
@@ -36,7 +36,17 @@ export declare class PeerCredentialStore {
36
36
  private readonly db;
37
37
  constructor(db: Kysely<any>);
38
38
  /**
39
- * Save a new peer credential.
39
+ * Save a new peer credential, revoking any prior active credential for the
40
+ * same instanceId (the `peer_credentials_active_uniq` partial unique index
41
+ * permits exactly one active credential per instanceId).
42
+ *
43
+ * Returns `{ revokedCount }` — how many previously-active credentials this
44
+ * save revoked. Because a coordinator's peer-clients share one
45
+ * identity-scoped credential, a `revokedCount > 0` here means every sibling
46
+ * peer-client of `instanceId` that was authenticating with the old
47
+ * credential is now invalidated and will be rejected on its next proof. The
48
+ * caller logs this so a revoke that cascades sibling rejections is visible in
49
+ * the orchestrator logs.
40
50
  */
41
51
  save(opts: {
42
52
  instanceId: string;
@@ -45,7 +55,9 @@ export declare class PeerCredentialStore {
45
55
  routingKeys: string[];
46
56
  sourceTokenHash?: string;
47
57
  expiryDays?: number;
48
- }): Promise<void>;
58
+ }): Promise<{
59
+ revokedCount: number;
60
+ }>;
49
61
  /**
50
62
  * Find a non-revoked, non-expired credential by its hash.
51
63
  */
@@ -6,7 +6,7 @@
6
6
  * proof (reconnection), registers the peer in PeerRegistry, and routes
7
7
  * messages bidirectionally. Sends periodic heartbeats to the connecting peer.
8
8
  */
9
- import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
9
+ import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
10
10
  import type { PeerRegistry } from './peer-registry.js';
11
11
  import type { PeerCredentialStore } from './peer-credentials.js';
12
12
  import { type JoinTokenManager } from './join-token.js';
@@ -40,7 +40,7 @@ export interface PeerHandlerDeps {
40
40
  /** Callback when a job reroute request is received from peer. */
41
41
  onJobReroute: (msg: JobReroute) => Promise<void>;
42
42
  /** Callback when a job progress update is received from peer. */
43
- onJobProgress: (msg: JobProgress) => void;
43
+ onJobProgress: (msg: JobProgress, reply: (m: JobProgressAck) => void) => void;
44
44
  /** Callback when a scaler provisioning event is forwarded by a worker peer. */
45
45
  onPeerScalerEvent?: (msg: PeerScalerEvent) => void;
46
46
  /** Callback when a job cancel request is received from peer. */
@@ -0,0 +1,39 @@
1
+ import type { PeerRegistry } from './peer-registry.js';
2
+ /**
3
+ * Grace window (ms) during which a rerouted job stays deferred from the
4
+ * recovery sweepers even though its worker peer is momentarily disconnected.
5
+ *
6
+ * A worker that completes a job durably buffers the terminal status in its
7
+ * PeerOutbox and replays it on the next reconnect. A coordinator restart or a
8
+ * leadership transition makes the worker's peer-WS flap (an abnormal 1006
9
+ * close) for a few seconds before it reconnects and replays — so sampling
10
+ * `peer.connected` at the exact instant a sweeper runs can catch the peer
11
+ * mid-flap and wrongly force-fail a run whose job already succeeded.
12
+ *
13
+ * The grace is two peer-stale-timeout windows (peerStaleTimeoutMs default
14
+ * 60_000): a peer that flaps and reconnects inside one stale window is always
15
+ * deferred, while a peer absent for two full windows is treated as dead and
16
+ * its job is failed — so a genuinely dead worker can never hang a run forever.
17
+ */
18
+ export declare const DEFAULT_REROUTE_FLAP_GRACE_MS = 120000;
19
+ /**
20
+ * A non-terminal job that was rerouted to a remote worker peer must NOT be
21
+ * force-failed by the run-recovery sweepers (OrphanRecovery, StaleRunDetector)
22
+ * while that worker peer can still replay the job's terminal status from its
23
+ * durable outbox.
24
+ *
25
+ * Returns `true` (defer the sweeper's force-fail) when the job carries a
26
+ * `rerouted_to_peer` marker AND its peer is either currently connected OR was
27
+ * last seen within the flap-grace window (a transient peer-WS reconnect during
28
+ * a coordinator restart / leadership transition). A rerouted job whose peer has
29
+ * been gone longer than the grace window — or is no longer tracked at all — is
30
+ * NOT deferred, so a dead worker's job is still failed and cannot hang forever.
31
+ * Local (non-rerouted) jobs — `rerouted_to_peer === null` — are never deferred.
32
+ */
33
+ export declare function shouldDeferReroutedJob(job: {
34
+ rerouted_to_peer: string | null;
35
+ }, peerRegistry: Pick<PeerRegistry, 'getPeer'>, opts?: {
36
+ nowMs?: number;
37
+ flapGraceMs?: number;
38
+ }): boolean;
39
+ //# sourceMappingURL=rerouted-job-guard.d.ts.map
package/dist/config.d.ts CHANGED
@@ -68,6 +68,7 @@ declare const configSchema: z.ZodObject<{
68
68
  staleDetectorScanIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
69
69
  staleDetectorThresholdMultiplier: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
70
70
  jobHeartbeatIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
71
+ githubAppNameRefreshIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
71
72
  secretKey: z.ZodOptional<z.ZodString>;
72
73
  secretKeyFile: z.ZodOptional<z.ZodString>;
73
74
  secretKeyOld: z.ZodOptional<z.ZodString>;
@@ -84,6 +85,8 @@ declare const configSchema: z.ZodObject<{
84
85
  agentTokenTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
85
86
  rosterGraceMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
86
87
  rosterTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
88
+ hostRebootDeadlineMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
89
+ orchestratorHostAgentId: z.ZodOptional<z.ZodString>;
87
90
  maxFanoutHosts: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
88
91
  eventRouterMaxChainDepth: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
89
92
  eventRouterRateLimitPerWorkflowPerMinute: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -197,11 +200,13 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
197
200
  staleDetectorScanIntervalMs: number;
198
201
  staleDetectorThresholdMultiplier: number;
199
202
  jobHeartbeatIntervalMs: number;
203
+ githubAppNameRefreshIntervalMs: number;
200
204
  pgCustomerSecrets: boolean;
201
205
  agentAuth: "token" | "none";
202
206
  agentTokenTtlMs: number;
203
207
  rosterGraceMs: number;
204
208
  rosterTtlMs: number;
209
+ hostRebootDeadlineMs: number;
205
210
  maxFanoutHosts: number;
206
211
  eventRouterMaxChainDepth: number;
207
212
  eventRouterRateLimitPerWorkflowPerMinute: number;
@@ -265,6 +270,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
265
270
  secretKeyOld?: string | undefined;
266
271
  secretKeyFileOld?: string | undefined;
267
272
  bootstrapAdminToken?: string | undefined;
273
+ orchestratorHostAgentId?: string | undefined;
268
274
  testEventFailFirstN?: string | undefined;
269
275
  otelExporterOtlpEndpoint?: string | undefined;
270
276
  clusterName?: string | undefined;
@@ -1,13 +1,14 @@
1
- import { IfFailedPolicy } from '@kici-dev/engine';
1
+ import type { ExecutionJobStatus } from '@kici-dev/engine';
2
2
  /** A single resolved upstream dependency edge for a job. */
3
3
  export interface JobNeedEdge {
4
4
  upstreamName: string;
5
- ifFailed: IfFailedPolicy;
5
+ /** Upstream terminal statuses that satisfy this edge (the run-on set). */
6
+ runOn: ExecutionJobStatus[];
6
7
  }
7
8
  /** Group raw execution_job_needs rows by downstream job_name. */
8
9
  export declare function groupNeedsByJobName(rows: ReadonlyArray<{
9
10
  job_name: string;
10
11
  upstream_name: string;
11
- if_failed: string;
12
+ run_on: string;
12
13
  }>): Map<string, JobNeedEdge[]>;
13
14
  //# sourceMappingURL=needs-edges.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ export declare function up(db: Kysely<unknown>): Promise<void>;
3
+ export declare function down(db: Kysely<unknown>): Promise<void>;
4
+ //# sourceMappingURL=043_rerouted_to_peer.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ export declare function up(db: Kysely<unknown>): Promise<void>;
3
+ export declare function down(db: Kysely<unknown>): Promise<void>;
4
+ //# sourceMappingURL=044_check_mode.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add the typed host-vars dimension to the host roster:
4
+ *
5
+ * - `host_roster.host_properties jsonb NOT NULL DEFAULT '{}'` — the typed
6
+ * property bag (`string | number | boolean` values) reported by the agent at
7
+ * registration and/or declared by the operator (`kici-admin host declare
8
+ * --prop`). Labels stay the flat-string grouping dimension; properties are
9
+ * the separate queryable host-vars dimension.
10
+ *
11
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
12
+ * the column is a no-op. Staging data is preserved (additive column).
13
+ */
14
+ export declare function up(db: Kysely<unknown>): Promise<void>;
15
+ export declare function down(db: Kysely<unknown>): Promise<void>;
16
+ //# sourceMappingURL=045_host_properties.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Record the joining peer's instanceId at first consumption of a join token:
4
+ *
5
+ * - `join_tokens.consumed_by_instance text` — the `instanceId` of the peer that
6
+ * consumed the token. This is the *joining peer*, distinct from `consumed_by`
7
+ * (the coordinator that processed the claim). It lets a token be reused by the
8
+ * same peer instance until `expires_at`, so a peer that lost its credential
9
+ * (transient outage / deleted credential file) self-heals by re-presenting the
10
+ * join token already in its env — no operator action, no cluster redeploy.
11
+ *
12
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
13
+ * the column is a no-op. Staging data is preserved (additive nullable column).
14
+ */
15
+ export declare function up(db: Kysely<unknown>): Promise<void>;
16
+ export declare function down(db: Kysely<unknown>): Promise<void>;
17
+ //# sourceMappingURL=046_join_token_consumed_by_instance.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ export declare function up(db: Kysely<unknown>): Promise<void>;
3
+ export declare function down(db: Kysely<unknown>): Promise<void>;
4
+ //# sourceMappingURL=047_needs_run_on.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `host_roster.reboot_pending_until timestamptz NULL` — the persisted
4
+ * reboot-pending flag for workflow-level host restart.
5
+ *
6
+ * Set when an agent's `restartHost()` step calls `host.requestReboot` (so it
7
+ * survives an orchestrator restart during the host's reboot window). While the
8
+ * value is in the future it (1) makes the agent's imminent disconnect an
9
+ * expected reboot rather than a recovery-fail, (2) gates the pinned-drain off
10
+ * so the post-restart job is not dispatched into the about-to-reboot box, and
11
+ * (3) clears on the next reconnect (down-then-up), releasing the held job. NULL
12
+ * = no reboot pending.
13
+ *
14
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
15
+ * preserved.
16
+ */
17
+ export declare function up(db: Kysely<unknown>): Promise<void>;
18
+ export declare function down(db: Kysely<unknown>): Promise<void>;
19
+ //# sourceMappingURL=048_host_reboot_pending.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `held_runs.payload jsonb NULL` — the drift payload captured when a
4
+ * `when: 'drift'` step-approval gate fires. Holds `{ summaryMarkdown, drift }`:
5
+ * the author's `summarize(drift)` rendering plus the structured drift blob, so
6
+ * the dashboard approval queue and the CLI render the computed diff the
7
+ * operator approves. NULL for every non-drift hold.
8
+ *
9
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
10
+ * preserved.
11
+ */
12
+ export declare function up(db: Kysely<unknown>): Promise<void>;
13
+ export declare function down(db: Kysely<unknown>): Promise<void>;
14
+ //# sourceMappingURL=049_held_runs_payload.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `sources.slug TEXT NULL` — the GitHub App slug (the URL-safe identifier
4
+ * GitHub assigns, e.g. `my-kici-app`).
5
+ *
6
+ * For GitHub-App sources GitHub is the source of truth for both the display
7
+ * `name` and the `slug`: both are captured at creation and kept fresh by the
8
+ * daily refresher + `kici-admin source refresh`. NULL when the identity fetch
9
+ * hasn't populated it yet (manual `--app-id` flow whose initial fetch failed,
10
+ * or a row created before the rollout).
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=050_sources_slug.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';
2
- import type { ApprovalRequirement, ApproverClause, InitFailure } from '@kici-dev/engine';
2
+ import type { ApprovalRequirement, ApproverClause, InitFailure, StepApprovalPayload } from '@kici-dev/engine';
3
3
  /**
4
4
  * PostgreSQL-only database types.
5
5
  * Column names use snake_case matching the actual database column names.
@@ -86,8 +86,14 @@ export interface JoinTokenTable {
86
86
  expires_at: Date;
87
87
  /** When this token was consumed (null = unused) */
88
88
  consumed_at: Date | null;
89
- /** Who consumed this token (null = unused) */
89
+ /** Who consumed this token (null = unused) — the coordinator that processed the claim */
90
90
  consumed_by: string | null;
91
+ /**
92
+ * The instanceId of the joining peer that consumed this token (null = unused).
93
+ * Distinct from `consumed_by` (the coordinator). Lets the same peer instance
94
+ * reuse the token until `expires_at` to self-heal after a transient outage.
95
+ */
96
+ consumed_by_instance: string | null;
91
97
  }
92
98
  export type JoinToken = Selectable<JoinTokenTable>;
93
99
  export type NewJoinToken = Insertable<JoinTokenTable>;
@@ -300,6 +306,12 @@ export interface ExecutionRunTable {
300
306
  workflow_timeout_ms: ColumnType<number | null, number | null | undefined, number | null>;
301
307
  /** When this record was created */
302
308
  created_at: Generated<Date>;
309
+ /**
310
+ * Run mode for idempotent steps (`apply` | `check` | `check-fail-on-drift`,
311
+ * the `CheckMode` enum). NULL means a legacy/apply run. A non-apply value
312
+ * labels the run a check-mode preview in the dashboard.
313
+ */
314
+ check_mode: string | null;
303
315
  /**
304
316
  * Set inside the cold-store archive transaction before the row is
305
317
  * DELETEd. Survivors carry NULL. Exists so a future
@@ -382,6 +394,8 @@ export interface ExecutionJobTable {
382
394
  wave_max_parallel: number | null;
383
395
  /** The fan-out base's `failFast` policy, stamped on every child. NULL = no bounded wave. */
384
396
  wave_fail_fast: boolean | null;
397
+ /** Instance id of the worker peer this job was rerouted to, or null if local. */
398
+ rerouted_to_peer: string | null;
385
399
  /** When this record was created */
386
400
  created_at: Generated<Date>;
387
401
  /**
@@ -429,6 +443,16 @@ export interface ExecutionStepTable {
429
443
  step_type: Generated<string>;
430
444
  /** JSON array of secret context names accessed by this step. NULL = tracking not available (old runs). */
431
445
  secrets_accessed: string | null;
446
+ /**
447
+ * Idempotent per-step outcome (`CheckStepOutcome`: skipped | applied |
448
+ * declined | dry-run | no_check). NULL when the step ran without a check
449
+ * mode. Orthogonal to `status`.
450
+ */
451
+ check_outcome: string | null;
452
+ /** Human-readable drift summary (`summarize(drift)`). NULL when no drift. */
453
+ drift_summary: string | null;
454
+ /** Structured drift value returned by `check()` (JSONB). NULL when no drift. */
455
+ drift: ColumnType<unknown | null, unknown, unknown>;
432
456
  /** When this record was created */
433
457
  created_at: Generated<Date>;
434
458
  /**
@@ -462,8 +486,13 @@ export interface ExecutionJobNeedsTable {
462
486
  job_name: string;
463
487
  /** Upstream job name (the job that must complete first) */
464
488
  upstream_name: string;
465
- /** Per-edge failure policy: 'skip' (default) or 'run' */
466
- if_failed: Generated<string>;
489
+ /**
490
+ * Per-edge run-on status-set: a JSON-encoded array of upstream terminal
491
+ * statuses (ExecutionJobStatus[]) that satisfy the edge. Default
492
+ * `'["success"]'`. The downstream dispatches when the upstream's terminal
493
+ * status is a member of this set.
494
+ */
495
+ run_on: Generated<string>;
467
496
  }
468
497
  export type ExecutionJobNeeds = Selectable<ExecutionJobNeedsTable>;
469
498
  export type NewExecutionJobNeeds = Insertable<ExecutionJobNeedsTable>;
@@ -671,7 +700,7 @@ export interface HeldRunsTable {
671
700
  step_index: number | null;
672
701
  /**
673
702
  * What created the hold: 'environment' (mandatory env policy) | 'explicit'
674
- * (SDK `requireApproval`). Engine `TriggerSource`.
703
+ * (SDK `approval`). Engine `TriggerSource`.
675
704
  */
676
705
  trigger_source: Generated<string>;
677
706
  /**
@@ -679,6 +708,11 @@ export interface HeldRunsTable {
679
708
  * must satisfy. Null for legacy rows that predate the approval model.
680
709
  */
681
710
  approval_requirement: ColumnType<ApprovalRequirement | null, ApprovalRequirement | string | null | undefined, ApprovalRequirement | string | null>;
711
+ /**
712
+ * Drift payload `{ summaryMarkdown, drift }` captured when a `when: 'drift'`
713
+ * step-approval gate fires. Null for every non-drift hold.
714
+ */
715
+ payload: ColumnType<StepApprovalPayload | null, StepApprovalPayload | string | null | undefined, StepApprovalPayload | string | null>;
682
716
  }
683
717
  export type HeldRun = Selectable<HeldRunsTable>;
684
718
  export type NewHeldRun = Insertable<HeldRunsTable>;
@@ -1191,6 +1225,12 @@ export interface SourcesTable {
1191
1225
  routing_key: string;
1192
1226
  /** JSONB config (non-sensitive, e.g. { appId: '12345' }) */
1193
1227
  config: string;
1228
+ /**
1229
+ * GitHub App slug (the URL-safe identifier GitHub assigns, e.g.
1230
+ * `my-kici-app`). NULL until the GitHub identity fetch populates it. GitHub
1231
+ * is the source of truth for both `name` and `slug` on GitHub-App sources.
1232
+ */
1233
+ slug: string | null;
1194
1234
  /** Customer/org identifier for secret and environment scoping */
1195
1235
  customer_id: Generated<string>;
1196
1236
  /** When this source was created */
@@ -1583,7 +1623,22 @@ export interface HostRosterTable {
1583
1623
  arch: string | null;
1584
1624
  /** Which orchestrator instance holds the live WS; null = disconnected. */
1585
1625
  connected_instance_id: string | null;
1626
+ /**
1627
+ * Typed host-vars dimension (jsonb). A `{ [key]: string | number | boolean }`
1628
+ * bag reported by the agent at registration and/or declared by the operator
1629
+ * (`kici-admin host declare --prop`), shallow-merged on upsert. NOT NULL,
1630
+ * defaults to `{}`. pg returns the parsed object on select; accept a
1631
+ * JSON-stringified value on insert/update.
1632
+ */
1633
+ host_properties: ColumnType<Record<string, string | number | boolean>, Record<string, string | number | boolean> | string | undefined, Record<string, string | number | boolean> | string>;
1586
1634
  last_seen: ColumnType<Date, Date | string | undefined, Date | string>;
1635
+ /**
1636
+ * Reboot-pending flag for workflow-level host restart. When set to a future
1637
+ * timestamp, the host's imminent disconnect is an expected reboot (not a
1638
+ * recovery-fail) and its pinned post-restart job is held until the host
1639
+ * reconnects (down-then-up). NULL = no reboot pending.
1640
+ */
1641
+ reboot_pending_until: ColumnType<Date | null, Date | string | null, Date | string | null>;
1587
1642
  created_at: Generated<Date>;
1588
1643
  updated_at: ColumnType<Date, Date | string | undefined, Date | string>;
1589
1644
  }
@@ -0,0 +1,9 @@
1
+ import { type DeploymentIdentity } from '@kici-dev/engine';
2
+ /**
3
+ * Read the orchestrator's deployment shape from the env the installer injects
4
+ * (`KICI_DEPLOY_MODE` / `KICI_DEPLOY_CONTAINER` / `KICI_DEPLOY_CONTAINER_RUNTIME`).
5
+ * Hand-run / dev orchestrators carry no `KICI_DEPLOY_*` env and report `unknown`.
6
+ * Container fields are kept only for the `compose` mode.
7
+ */
8
+ export declare function readDeploymentIdentity(env?: NodeJS.ProcessEnv): DeploymentIdentity;
9
+ //# sourceMappingURL=deployment-identity.d.ts.map
@@ -19,6 +19,13 @@ export interface ProviderSource {
19
19
  routingKey: string;
20
20
  name: string;
21
21
  subtype: SourceSubtype;
22
+ /**
23
+ * GitHub App slug (URL-safe identifier GitHub assigns). Only set for
24
+ * GitHub-App sources, where it propagates orchestrator → Platform → dashboard
25
+ * alongside the display `name`. Undefined for generic / universal-git / local
26
+ * sources, and for a GitHub source whose identity fetch hasn't run yet.
27
+ */
28
+ slug?: string;
22
29
  }
23
30
  /**
24
31
  * Map a generic_webhook_sources `provider_type` (plus optional `git_config`