@kici-dev/orchestrator 0.6.1 → 0.8.0

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 (187) hide show
  1. package/dist/agent/dispatcher.d.ts +10 -1
  2. package/dist/agent/host-roster.d.ts +8 -3
  3. package/dist/agent/registry.d.ts +60 -5
  4. package/dist/app.d.ts +10 -5
  5. package/dist/cache/dep-cache.d.ts +14 -6
  6. package/dist/cache/global-eval-round-cache.d.ts +1 -1
  7. package/dist/cache/index.d.ts +1 -1
  8. package/dist/cache/source-cache.d.ts +92 -15
  9. package/dist/cancel/cancel-run.d.ts +20 -2
  10. package/dist/cancel/sweep-stuck-cancelling.d.ts +30 -0
  11. package/dist/cli/api-client.d.ts +10 -0
  12. package/dist/cli/commands/agent-service/logs.d.ts +1 -1
  13. package/dist/cli/commands/agent-service/restart.d.ts +1 -1
  14. package/dist/cli/commands/agent-service/start.d.ts +1 -1
  15. package/dist/cli/commands/agent-service/status.d.ts +1 -1
  16. package/dist/cli/commands/agent-service/stop.d.ts +1 -1
  17. package/dist/cli/commands/agent-service/uninstall.d.ts +1 -1
  18. package/dist/cli/commands/agent.d.ts +25 -0
  19. package/dist/cli/commands/config.d.ts +16 -0
  20. package/dist/cli/commands/db-backup.d.ts +46 -0
  21. package/dist/cli/commands/held-run.d.ts +9 -0
  22. package/dist/cli/commands/orchestrator-service/drain.d.ts +2 -19
  23. package/dist/cli/commands/orchestrator-service/index.d.ts +9 -1
  24. package/dist/cli/commands/orchestrator-service/install-env.d.ts +43 -2
  25. package/dist/cli/commands/orchestrator-service/logs.d.ts +1 -1
  26. package/dist/cli/commands/orchestrator-service/restart.d.ts +1 -1
  27. package/dist/cli/commands/orchestrator-service/start.d.ts +1 -1
  28. package/dist/cli/commands/orchestrator-service/status.d.ts +17 -1
  29. package/dist/cli/commands/orchestrator-service/stop.d.ts +1 -1
  30. package/dist/cli/commands/orchestrator-service/uninstall.d.ts +1 -1
  31. package/dist/cli/commands/orchestrator-service/upgrade.d.ts +2 -1
  32. package/dist/cli/commands/rotate.d.ts +1 -1
  33. package/dist/cli/commands/shared/env-file-mode.d.ts +16 -0
  34. package/dist/cli/commands/shared/upgrade-hooks.d.ts +32 -0
  35. package/dist/cli/commands/shared/upgrade-safety.d.ts +81 -0
  36. package/dist/cli/commands/shared/versioned-upgrade.d.ts +67 -12
  37. package/dist/cli/commands/source.d.ts +1 -0
  38. package/dist/cli/commands/token.d.ts +4 -0
  39. package/dist/cli/commands/trust-policy.d.ts +57 -1
  40. package/dist/cli/join.d.ts +1 -1
  41. package/dist/cli/service/backup-timer.d.ts +108 -0
  42. package/dist/cli/service/compose-path.d.ts +11 -0
  43. package/dist/cli/service/compose.d.ts +22 -0
  44. package/dist/cli/service/deploy-env.d.ts +10 -11
  45. package/dist/cli/service/env-assignment.d.ts +24 -0
  46. package/dist/cli/service/index.d.ts +2 -2
  47. package/dist/cli/service/instance/resolve.d.ts +71 -7
  48. package/dist/cli/service/instance/types.d.ts +15 -0
  49. package/dist/cli/service/launchd.d.ts +7 -0
  50. package/dist/cli/service/systemd.d.ts +1 -0
  51. package/dist/cli/service/types.d.ts +36 -0
  52. package/dist/cli/service/windows.d.ts +16 -0
  53. package/dist/cli/wizard/orchestrator-wizard.d.ts +12 -3
  54. package/dist/cli/wizard/prompts.d.ts +7 -0
  55. package/dist/cli.js +12544 -9299
  56. package/dist/cluster/cluster-settings-reader.d.ts +2 -2
  57. package/dist/cluster/coordinator.d.ts +11 -0
  58. package/dist/cluster/instance-heartbeat.d.ts +96 -0
  59. package/dist/cluster/join-client.d.ts +55 -5
  60. package/dist/cluster/join-handler.d.ts +1 -0
  61. package/dist/cluster/orphan-recovery.d.ts +16 -0
  62. package/dist/cluster/peer-registry.d.ts +10 -0
  63. package/dist/concurrency/dispatch-next-queued.d.ts +1 -13
  64. package/dist/concurrency/index.d.ts +1 -2
  65. package/dist/concurrency/queue-manager.d.ts +63 -18
  66. package/dist/config/env-overlay.d.ts +3 -2
  67. package/dist/config.d.ts +24 -0
  68. package/dist/contexts/release-queued-holds.d.ts +23 -1
  69. package/dist/dashboard/attestation-filters.d.ts +2 -0
  70. package/dist/dashboard/handler.d.ts +22 -2
  71. package/dist/db/deferred-indexes.d.ts +72 -0
  72. package/dist/db/migration-test-harness.d.ts +22 -0
  73. package/dist/db/migrations/131_execution_jobs_git_credentials.d.ts +30 -0
  74. package/dist/db/migrations/132_execution_runs_trigger_event.d.ts +23 -0
  75. package/dist/db/migrations/133_dispatch_queue_source_tar_digest.d.ts +23 -0
  76. package/dist/db/migrations/134_dispatch_queue_ownership.d.ts +4 -0
  77. package/dist/db/migrations/135_cluster_settings_retention_days.d.ts +4 -0
  78. package/dist/db/migrations/136_master_key_wrapped_tables_key_version.d.ts +4 -0
  79. package/dist/db/migrations/137_execution_runs_pr_head_context.d.ts +4 -0
  80. package/dist/db/migrations/138_execution_runs_subject_trigger_event.d.ts +4 -0
  81. package/dist/db/migrations/139_admin_token_subject.d.ts +4 -0
  82. package/dist/db/migrations/140_dispatch_queue_canonical_labels.d.ts +54 -0
  83. package/dist/db/migrations/141_event_log_shed.d.ts +15 -0
  84. package/dist/db/migrator.d.ts +47 -0
  85. package/dist/db/notify-listener.d.ts +60 -0
  86. package/dist/db/synthetic-job-ids.d.ts +24 -0
  87. package/dist/db/types.d.ts +169 -4
  88. package/dist/deployment/deployment-identity.d.ts +31 -1
  89. package/dist/diagnostics/checks/index.d.ts +2 -1
  90. package/dist/diagnostics/checks/retention.d.ts +24 -0
  91. package/dist/diagnostics/types.d.ts +7 -0
  92. package/dist/events/event-router.d.ts +1 -1
  93. package/dist/fault-injection-types.d.ts +53 -0
  94. package/dist/firecracker/host-network.d.ts +63 -8
  95. package/dist/git/credential-broker.d.ts +35 -2
  96. package/dist/git/inherited-secret.d.ts +7 -2
  97. package/dist/git/job-context.d.ts +7 -4
  98. package/dist/helpers/secure-write.d.ts +35 -0
  99. package/dist/index.js +196 -67
  100. package/dist/metrics/prometheus.d.ts +116 -1
  101. package/dist/oidc/db-signer.d.ts +27 -5
  102. package/dist/oidc/id-token-claims.d.ts +33 -2
  103. package/dist/oidc/oidc-mint-registration.d.ts +6 -0
  104. package/dist/oidc/orchestrator-mint.d.ts +8 -0
  105. package/dist/oidc/reconcile-signing-key.d.ts +37 -0
  106. package/dist/orchestrator-core.d.ts +9 -3
  107. package/dist/pipeline/decorating-secret-resolver.d.ts +1 -1
  108. package/dist/pipeline/dispatch-matched-workflow.d.ts +65 -14
  109. package/dist/pipeline/manual-schedule.d.ts +1 -0
  110. package/dist/pipeline/process-webhook.d.ts +20 -2
  111. package/dist/pipeline/route-or-dispatch-jobs.d.ts +7 -0
  112. package/dist/pipeline/run-event-context.d.ts +39 -0
  113. package/dist/policy/dashboard-write-policy-listener.d.ts +1 -1
  114. package/dist/policy/dashboard-write-policy.d.ts +47 -1
  115. package/dist/provenance/dispatch-context.d.ts +46 -0
  116. package/dist/provenance/verify-deferred-capture.d.ts +39 -0
  117. package/dist/providers/github/auth.d.ts +9 -0
  118. package/dist/providers/github/deliveries.d.ts +118 -0
  119. package/dist/providers/local/lock-file-fetcher.d.ts +8 -0
  120. package/dist/providers/universal-git/config.d.ts +1 -1
  121. package/dist/queue/cleanup.d.ts +13 -0
  122. package/dist/queue/job-queue.d.ts +162 -14
  123. package/dist/queue/retention.d.ts +105 -0
  124. package/dist/reporting/execution-tracker.d.ts +87 -36
  125. package/dist/routes/admin-db.d.ts +5 -0
  126. package/dist/routes/admin-events.d.ts +13 -0
  127. package/dist/routes/admin-held-runs.d.ts +11 -1
  128. package/dist/routes/admin-org-settings.d.ts +9 -0
  129. package/dist/routes/admin-runs.d.ts +7 -4
  130. package/dist/routes/admin-sources.d.ts +15 -0
  131. package/dist/routes/admin-trust-policy.d.ts +11 -0
  132. package/dist/routes/admin.d.ts +25 -0
  133. package/dist/scaler/backend-factory.d.ts +16 -0
  134. package/dist/scaler/bare-metal-backend.d.ts +22 -4
  135. package/dist/scaler/claim-store.d.ts +0 -2
  136. package/dist/scaler/config.d.ts +6 -3
  137. package/dist/scaler/container-backend.d.ts +102 -3
  138. package/dist/scaler/container-hostconfig.d.ts +60 -0
  139. package/dist/scaler/event-backend.d.ts +7 -6
  140. package/dist/scaler/file-tail.d.ts +25 -2
  141. package/dist/scaler/firecracker-api.d.ts +9 -1
  142. package/dist/scaler/firecracker-backend.d.ts +135 -2
  143. package/dist/scaler/host-access.d.ts +59 -0
  144. package/dist/scaler/index.d.ts +1 -0
  145. package/dist/scaler/manager.d.ts +51 -2
  146. package/dist/scaler/scaler-state-store.d.ts +13 -0
  147. package/dist/scaler/types.d.ts +15 -7
  148. package/dist/scaler/warm-pool.d.ts +6 -4
  149. package/dist/secrets/config.d.ts +48 -0
  150. package/dist/secrets/dashboard-encryption-key.d.ts +19 -0
  151. package/dist/secrets/ephemeral-keys.d.ts +9 -3
  152. package/dist/secrets/index.d.ts +3 -1
  153. package/dist/secrets/job-secret-gate.d.ts +76 -0
  154. package/dist/secrets/master-key-rotation.d.ts +65 -0
  155. package/dist/secrets/secret-output-crypto.d.ts +17 -0
  156. package/dist/secrets/secret-resolver.d.ts +23 -6
  157. package/dist/secrets/source-credentials.d.ts +1 -1
  158. package/dist/secrets/token-manager.d.ts +4 -1
  159. package/dist/security/identity-link.d.ts +1 -1
  160. package/dist/security/reduced-privilege-note.d.ts +5 -0
  161. package/dist/security/trust-directory-store.d.ts +1 -1
  162. package/dist/security/trust-policy-gate.d.ts +70 -6
  163. package/dist/security/trust-tier.d.ts +31 -0
  164. package/dist/server.d.ts +1 -1
  165. package/dist/server.js +10793 -5323
  166. package/dist/sources/source-manager.d.ts +1 -1
  167. package/dist/stale-detector/stale-dispatch-candidates.d.ts +43 -0
  168. package/dist/standalone.js +9385 -4160
  169. package/dist/storage/loopback-guard.d.ts +53 -6
  170. package/dist/webhook/event-log.d.ts +14 -1
  171. package/dist/webhook/generic-sources-listener.d.ts +1 -1
  172. package/dist/webhook/ingest-admission.d.ts +24 -0
  173. package/dist/webhook/ingest-overflow-replayer.d.ts +78 -14
  174. package/dist/webhook/ingest-overflow-types.d.ts +13 -1
  175. package/dist/webhook/register-source-bundle.d.ts +23 -0
  176. package/dist/webhook/relay-reinject.d.ts +11 -12
  177. package/dist/webhook/shed-breadcrumb.d.ts +15 -0
  178. package/dist/worker/in-memory-job-queue.d.ts +9 -0
  179. package/dist/ws/fleet-agent-collector.d.ts +7 -0
  180. package/dist/ws/git-credential-relay.d.ts +41 -12
  181. package/dist/ws/platform-client.d.ts +28 -5
  182. package/dist/ws/test-relay-handlers.d.ts +2 -0
  183. package/installer-image-digests.json +3 -3
  184. package/package.json +4 -4
  185. package/sbom.spdx.json +53 -53
  186. package/dist/concurrency/group-tracker.d.ts +0 -51
  187. package/dist/scaler/nftables.d.ts +0 -111
@@ -27,7 +27,7 @@ export declare enum AgentDrainDecline {
27
27
  * Default per-pass cap on how many pending jobs a capacity-freed re-drive
28
28
  * re-offers to the scaler. Bounds the burst so a single free event cannot storm
29
29
  * the scaler; the per-backend spawn semaphore bounds actual provisioning, and a
30
- * job that gets `at-capacity` again simply stays pending for the next tick.
30
+ * job that gets `at-capacity` again stays pending for the next tick.
31
31
  */
32
32
  export declare const DEFAULT_REDRIVE_BATCH = 10;
33
33
  /**
@@ -403,6 +403,15 @@ export declare class Dispatcher {
403
403
  * triage can surface failed job IDs to the caller.
404
404
  */
405
405
  private requeueOrFail;
406
+ /**
407
+ * The half of {@link requeueOrFail} that runs once the row has been flipped:
408
+ * fail the job when its attempt budget is exhausted, otherwise re-dispatch.
409
+ *
410
+ * Split out so the ack-expiry paths can drive it from the attempt count their
411
+ * own guarded requeue (`requeueIfAwaitingAck`) returned, instead of flipping
412
+ * the row a second time.
413
+ */
414
+ private finishRequeue;
406
415
  /**
407
416
  * Try to deliver a requeued pending job: prefer an idle matching agent
408
417
  * (atomic claim via dispatchBoundJob), otherwise consult the scaler so a
@@ -1,5 +1,5 @@
1
1
  import { type Kysely } from 'kysely';
2
- import { type HostInventoryEntry, type InventorySelector, type LabelMatcher } from '@kici-dev/engine';
2
+ import { type CanonicalLabel, type HostInventoryEntry, type InventorySelector, type LabelMatcher } from '@kici-dev/engine';
3
3
  import type { Database, HostRosterRow } from '../db/types.js';
4
4
  /** Typed host-vars bag carried by roster rows (`string | number | boolean`). */
5
5
  export type HostProperties = Record<string, string | number | boolean>;
@@ -61,9 +61,14 @@ export type LifecycleClass = 'static' | 'ephemeral';
61
61
  */
62
62
  export interface MatchedHost {
63
63
  agentId: string;
64
- /** Hostname (falls back to agentId). */
64
+ /** Canonical hostname; falls back to the raw agentId when the row has none. */
65
65
  host: string;
66
- labels: string[];
66
+ /**
67
+ * The row's labels, folded on read. A row written before labels were
68
+ * canonicalized still carries the case the operator typed, and this is the
69
+ * only place that case would otherwise reach a matcher.
70
+ */
71
+ labels: CanonicalLabel[];
67
72
  lifecycleClass: LifecycleClass;
68
73
  /** Which orchestrator holds the live WS (null = disconnected). For the cross-cluster pin. */
69
74
  connectedInstanceId: string | null;
@@ -8,7 +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
+ import { type CanonicalLabel, type LabelMatcher } from '@kici-dev/engine';
12
12
  import type { WsLike } from '@kici-dev/engine';
13
13
  export type { WsLike } from '@kici-dev/engine';
14
14
  /**
@@ -19,8 +19,12 @@ export interface AgentEntry {
19
19
  agentId: string;
20
20
  /** The underlying WebSocket (or mock). */
21
21
  ws: WsLike;
22
- /** Set of labels this agent provides (e.g., "linux", "gpu", "docker"). */
23
- labels: Set<string>;
22
+ /**
23
+ * Set of labels this agent provides (e.g., "linux", "gpu", "docker"), folded
24
+ * to canonical form by `register()`. The label reverse index is keyed the
25
+ * same way, so a lookup must fold its query before it reaches either.
26
+ */
27
+ labels: Set<CanonicalLabel>;
24
28
  /**
25
29
  * Kubernetes-taint-style mandatory labels inherited from the scaler that
26
30
  * spawned this agent. A job is only dispatched to this agent when every
@@ -35,8 +39,11 @@ export interface AgentEntry {
35
39
  * (`onAgentAvailable` → `dequeueForLabels`) sees only the agent's labels
36
40
  * and would otherwise pull a queued job whose `runsOn` is a subset of the
37
41
  * agent's labels but does not satisfy the gate).
42
+ *
43
+ * Folded to canonical form by `register()`, so a scaler that declared `GPU`
44
+ * gates the same jobs a scaler that declared `gpu` does.
38
45
  */
39
- mandatoryLabels: Set<string>;
46
+ mandatoryLabels: Set<CanonicalLabel>;
40
47
  /**
41
48
  * True when this agent was spawned by a scaler backend. Scaler-managed
42
49
  * agents are single-use: the scaler destroys them on disconnect, so they
@@ -45,6 +52,22 @@ export interface AgentEntry {
45
52
  * recovery window that cannot succeed.
46
53
  */
47
54
  scalerManaged: boolean;
55
+ /**
56
+ * True while this agent is being torn down and must receive no new work.
57
+ *
58
+ * The warm pool decides an agent is idle, marks it, then awaits a destroy
59
+ * that takes seconds — a container `stop({ t: 5 })`, or a Firecracker
60
+ * `SendCtrlAltDel` plus its grace. The registry entry survives until the
61
+ * agent's WebSocket actually closes, so without this flag `findAvailable`
62
+ * kept returning the agent throughout: a job arriving in that window was
63
+ * dispatched onto a container that was already exiting, and failed as a
64
+ * spurious infrastructure error on a healthy system.
65
+ *
66
+ * In memory rather than DB-backed, unlike reboot-pending: a warm-pool
67
+ * destroy is issued by this process, lasts seconds, and ends with the
68
+ * WebSocket close that removes the entry — there is nothing to outlive.
69
+ */
70
+ draining: boolean;
48
71
  /** Number of jobs currently executing on this agent. */
49
72
  activeJobs: number;
50
73
  /** Maximum concurrent jobs this agent can handle (default 1). */
@@ -211,8 +234,16 @@ export declare class AgentRegistry {
211
234
  /**
212
235
  * Register an agent. If the agentId already exists, updates the existing
213
236
  * entry (agent reconnection scenario).
237
+ *
238
+ * @returns the socket the previous entry was bound to, when this register
239
+ * replaced one bound to a *different* socket. The caller owns that socket's
240
+ * per-connection state and must retire it: a half-open connection can
241
+ * outlive the reconnect, and its later close carries the same agent id, so
242
+ * a caller that keeps treating it as live tears down the registration that
243
+ * replaced it. `undefined` for a first register or a re-register on the
244
+ * same socket.
214
245
  */
215
- register(agentId: string, ws: WsLike, labels: string[], platform?: string, arch?: string, version?: string, maxConcurrency?: number, metadata?: AgentMetadata): void;
246
+ register(agentId: string, ws: WsLike, labels: string[], platform?: string, arch?: string, version?: string, maxConcurrency?: number, metadata?: AgentMetadata): WsLike | undefined;
216
247
  /**
217
248
  * Unregister an agent by ID. Removes from all indexes.
218
249
  * @returns The removed AgentEntry, or undefined if not found.
@@ -300,6 +331,9 @@ export declare class AgentRegistry {
300
331
  * matches zero agents (so callers can short-circuit). Returns an empty
301
332
  * Set on `requiredLabels.length === 0` since the caller decides the
302
333
  * walk strategy in that case.
334
+ *
335
+ * Takes canonical labels: the index it reads is keyed on the canonical form,
336
+ * so a caller folds before it gets here.
303
337
  */
304
338
  private intersectLabelCandidates;
305
339
  /**
@@ -334,6 +368,27 @@ export declare class AgentRegistry {
334
368
  /**
335
369
  * Get a single agent entry by ID.
336
370
  */
371
+ /**
372
+ * Whether one agent id is currently registered.
373
+ *
374
+ * Distinct from `findAvailable`, which answers "which agents could take this
375
+ * job" — the container orphan sweep needs to know only whether a running
376
+ * container's agent is present at all, including one that is busy or gated.
377
+ */
378
+ isRegistered(agentId: string): boolean;
379
+ /**
380
+ * Condemn an agent: it stays registered and keeps finishing its current work,
381
+ * but takes no new dispatches.
382
+ *
383
+ * Called before a warm-pool destroy is issued. Returns the agent's current
384
+ * `activeJobs` so the caller can see a job that was claimed between its idle
385
+ * check and this mark, and back out.
386
+ *
387
+ * @returns the agent's active-job count, or `undefined` when it is not registered
388
+ */
389
+ markDraining(agentId: string): number | undefined;
390
+ /** Undo {@link markDraining} — a destroy that was rejected leaves the agent alive. */
391
+ clearDraining(agentId: string): void;
337
392
  get(agentId: string): AgentEntry | undefined;
338
393
  /**
339
394
  * Get an agent entry by its WebSocket reference.
package/dist/app.d.ts CHANGED
@@ -57,6 +57,7 @@ import type { OwnershipTracker } from './agent/ownership-tracker.js';
57
57
  import type { ObserverRegistry } from './ws/observer-registry.js';
58
58
  import type { FleetAgentCollector } from './ws/fleet-agent-collector.js';
59
59
  import type { FleetTopology } from './diagnostics/fleet-topology.js';
60
+ import type { ResolvedMasterKeys } from './secrets/config.js';
60
61
  import type { TokenManager } from './secrets/token-manager.js';
61
62
  import type { SecretResolver } from './secrets/secret-resolver.js';
62
63
  import type { VerifyInboundDeps } from './webhook/verify-inbound.js';
@@ -75,7 +76,6 @@ import { type OverflowDelivery } from './webhook/ingest-overflow-types.js';
75
76
  import type { IngestAdmissionController } from './webhook/ingest-admission.js';
76
77
  import type { OrgIngestCapReader } from './webhook/org-ingest-cap-reader.js';
77
78
  import type { SandboxAllowListReader } from './pipeline/sandbox-allowlist-reader.js';
78
- import type { ConcurrencyGroupTracker } from './concurrency/group-tracker.js';
79
79
  import type { ConcurrencyQueueManager } from './concurrency/queue-manager.js';
80
80
  import type { EventRouter } from './events/event-router.js';
81
81
  import type { EventStore } from './events/event-store.js';
@@ -85,7 +85,7 @@ import type { EventLogWriter } from './webhook/event-log.js';
85
85
  import type { AccessLogWriter } from './audit/access-log.js';
86
86
  import type { GenericSourceManager } from './webhook/generic-sources.js';
87
87
  import type { TrustStore } from './events/trust-store.js';
88
- import type { ContextStore } from './contexts/context-store.js';
88
+ import { ContextStore } from './contexts/context-store.js';
89
89
  import type { VariableStore } from './contexts/variable-store.js';
90
90
  import type { HeldRunStore, ReleaseSignal } from './contexts/held-runs.js';
91
91
  import type { StepApprovalBridge } from './approvals/step-approval-bridge.js';
@@ -97,6 +97,13 @@ export interface AppDependencies {
97
97
  config: AppConfig;
98
98
  db: Kysely<Database>;
99
99
  pool: pg.Pool;
100
+ /**
101
+ * The orchestrator master key in both forms, plus the previous generation
102
+ * during a rotation grace window. Null when no master key is configured.
103
+ * Resolved once at boot so every master-key-wrapped store agrees, and so
104
+ * `KICI_SECRET_KEY_FILE` reaches all of them.
105
+ */
106
+ masterKeys?: ResolvedMasterKeys | null;
100
107
  /** Fleet-wide settings reader; threads the live lock-file cap into warm-added universal-git sources. */
101
108
  clusterSettings?: ClusterSettingsReader;
102
109
  registry: AgentRegistry;
@@ -187,7 +194,7 @@ export interface AppDependencies {
187
194
  * the composition root / server hooks. Type-only reference — the runtime
188
195
  * module never enters the shipped bundle.
189
196
  */
190
- faultInjection?: import('./testing/fault-injection.js').OrchestratorFaultInjection;
197
+ faultInjection?: import('./fault-injection-types.js').OrchestratorFaultInjection;
191
198
  /**
192
199
  * Orchestrator-owned provenance signing (Phase 1 root of trust). Present when
193
200
  * `KICI_ORCHESTRATOR_PROVENANCE_ISSUER` is configured: the orchestrator mints +
@@ -336,8 +343,6 @@ export interface AppDependencies {
336
343
  coldStore?: ColdStore | null;
337
344
  /** Read + mutation attribution log writer. Optional -- if not set, access_log rows are not written. */
338
345
  accessLogWriter?: AccessLogWriter;
339
- /** In-memory concurrency group tracker. Optional -- if not set, concurrency is disabled. */
340
- concurrencyTracker?: ConcurrencyGroupTracker;
341
346
  /** DB-backed concurrency queue manager. Optional -- if not set, concurrency is disabled. */
342
347
  concurrencyQueueManager?: ConcurrencyQueueManager;
343
348
  /** Optional callback when agent sends encrypted secret outputs on job success. */
@@ -27,14 +27,22 @@ import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.j
27
27
  export declare function depTarballKey(depsHash: string, platform: string, arch: string): string;
28
28
  /**
29
29
  * Cache key for the pointer that resolves a lockfile to the content hash of the
30
- * tarball built from it: `deps/{platform}-{arch}/{lockfileHash}.hash`.
30
+ * tarball built from it: `deps/{platform}-{arch}/{lockfileHash}.hash`, or
31
+ * `…/{lockfileHash}-{siblingsDigest}.hash` when the lock carries one.
32
+ *
33
+ * `siblingsDigest` is part of the key because the deps tarball carries in-repo
34
+ * `workspace:` sibling directories WITH their built output, and editing a
35
+ * sibling's source moves no package-manager lock file — so a `lockfileHash`-only
36
+ * key restored the sibling's stale build over the fresh clone on every run. A
37
+ * lock with no in-repo sibling carries no digest and keys exactly as before, so
38
+ * existing pointers stay live.
31
39
  *
32
40
  * This is the one mutable object in the scheme. A concurrent write replaces a
33
41
  * pointer wholesale — there is no window in which it is half-written — so the
34
42
  * worst a racing pair of builders can do is leave whichever pointer landed last,
35
43
  * and both tarballs remain valid and fetchable.
36
44
  */
37
- export declare function depPointerKey(lockfileHash: string, platform: string, arch: string): string;
45
+ export declare function depPointerKey(lockfileHash: string, platform: string, arch: string, siblingsDigest?: string): string;
38
46
  export declare class DepCache {
39
47
  private readonly storage;
40
48
  private readonly maxTarballBytes;
@@ -67,19 +75,19 @@ export declare class DepCache {
67
75
  * Both halves must be present: a pointer whose tarball has aged out is a miss,
68
76
  * not a hit, or the caller skips a rebuild and dispatches a URL that 404s.
69
77
  */
70
- has(lockfileHash: string, platform: string, arch: string): Promise<boolean>;
78
+ has(lockfileHash: string, platform: string, arch: string, siblingsDigest?: string): Promise<boolean>;
71
79
  /**
72
80
  * Get a pre-signed download URL for the dep tarball (for agent delivery).
73
81
  * Refreshes TTL on hit.
74
82
  */
75
- getUrl(lockfileHash: string, platform: string, arch: string): Promise<string | null>;
83
+ getUrl(lockfileHash: string, platform: string, arch: string, siblingsDigest?: string): Promise<string | null>;
76
84
  /**
77
85
  * Get a pre-signed download URL and the tarball content hash.
78
86
  *
79
87
  * Returns null on cache miss — including an entry with no pointer, which is
80
88
  * unverifiable and so is deliberately not served.
81
89
  */
82
- getUrlAndHash(lockfileHash: string, platform: string, arch: string): Promise<{
90
+ getUrlAndHash(lockfileHash: string, platform: string, arch: string, siblingsDigest?: string): Promise<{
83
91
  url: string;
84
92
  hash?: string;
85
93
  } | null>;
@@ -93,7 +101,7 @@ export declare class DepCache {
93
101
  * Called only after the agent confirms its upload completed. Publishing before
94
102
  * the bytes land would let a reader follow the pointer to a missing object.
95
103
  */
96
- publishPointer(lockfileHash: string, platform: string, arch: string, depsHash: string): Promise<void>;
104
+ publishPointer(lockfileHash: string, platform: string, arch: string, depsHash: string, siblingsDigest?: string): Promise<void>;
97
105
  /**
98
106
  * Store a dep tarball in cache.
99
107
  * Throws if tarball exceeds max size (per user decision).
@@ -42,7 +42,7 @@ import type { GlobalEvalRoundResult } from '@kici-dev/engine';
42
42
  * than a wrong verdict.
43
43
  *
44
44
  * Returns `null` when the inputs cannot be serialized. A round that cannot be
45
- * keyed is simply not cached — never keyed on a partial input, which is how a
45
+ * keyed is not cached — never keyed on a partial input, which is how a
46
46
  * cache starts answering questions it was not asked.
47
47
  */
48
48
  export declare function globalEvalRoundCacheKey(args: {
@@ -2,7 +2,7 @@
2
2
  * Cache module for customer-workflow source tarballs and dependency tarballs.
3
3
  *
4
4
  * SourceCache: content-hash-keyed storage for `.kici/` source tarballs
5
- * (`source/{contentHash}.tar.gz`).
5
+ * (`source/v2/{orgId}/{sourceTarDigest}.tar.gz`).
6
6
  * BuildCoordinator: deduplicates concurrent build requests.
7
7
  * DepCache: lockfileHash-keyed storage for dependency tarballs.
8
8
  */
@@ -1,30 +1,107 @@
1
1
  /**
2
2
  * Source-tarball cache layer wrapping CacheStorage.
3
3
  *
4
- * Provides content-hash-keyed operations for customer-workflow source tarballs
5
- * (the `.kici/` directory minus `node_modules/`, packed by the agent's build
6
- * job via `source-packer.ts`). Tarballs are platform-independent: raw TypeScript
7
- * source files are identical across architectures. Refreshes TTL on reads
4
+ * Stores customer-workflow source tarballs — the `.kici/` directory minus
5
+ * `node_modules/`, packed by the agent's build job via `source-packer.ts` —
6
+ * under the tarball's OWN content hash, with a small pointer resolving an org's
7
+ * workflow `contentHash` to that hash. Tarballs are platform-independent: raw
8
+ * TypeScript source is identical across architectures. Refreshes TTL on reads
8
9
  * (touch-on-read) so actively used sources stay in cache longer.
9
10
  *
10
- * Cache key format: source/{contentHash}.tar.gz
11
+ * Keys: `source/v2/{orgId}/{sourceTarDigest}.tar.gz` (immutable) and
12
+ * `source/v2/{orgId}/{contentHash}.hash` (the pointer) — the shape `dep-cache.ts`
13
+ * already uses, for the same reasons — content addressing and org scoping.
11
14
  *
12
- * Replaces the former `BundleCache` / `bundles/{contentHash}.js` layout that
13
- * stored Rolldown-bundled `.compiled.mjs` artifacts the new execution path
14
- * downloads the source tarball, extracts it, and imports the workflow entry
15
- * via the shared oxc-transform ESM loader hook.
15
+ * **Content addressing** makes a mismatched pair unrepresentable. At
16
+ * `source/{contentHash}.tar.gz` the object's key was derived from something
17
+ * other than its bytes, so nothing a reader fetched could be verified against
18
+ * anything, and the agent restored whatever the signed URL returned.
19
+ *
20
+ * **Org scoping** makes the key a tenant's key. `contentHash` alone is a
21
+ * property of a `.kici/` tree, so two repositories scaffolded from the same
22
+ * `kici init` template — or a fork and its upstream — produced one object, and
23
+ * whichever built first won for the life of that object. On an orchestrator
24
+ * serving more than one org that is a cross-tenant read of `.kici/` contents.
25
+ * The org is the tenant boundary and the segment the user cache already scopes
26
+ * by; inside one org, sharing a key now means the two trees are byte-identical,
27
+ * so a monorepo or a same-org fork keeps its warm cache and pays nothing.
28
+ *
29
+ * Legacy `source/{contentHash}.tar.gz` objects are **not read**. Only a
30
+ * `compileSchemaVersion: 5` lock can name one, and that `contentHash` covers
31
+ * the workflow entry file alone rather than the tree the tarball holds — the
32
+ * defect this layout closes — so serving it would serve the bug. For a v6 lock
33
+ * the fallback is unreachable anyway, since the schema bump moves every hash.
34
+ * Old objects are orphaned and aged out by the normal cache TTL.
16
35
  */
17
36
  import type { CacheStorage } from '../storage/types.js';
37
+ /**
38
+ * Cache key for a source tarball, addressed by the tarball's OWN content hash:
39
+ * `source/v2/{orgId}/{sourceTarDigest}.tar.gz`.
40
+ *
41
+ * Naming the object by its own hash means every pair a reader can observe is
42
+ * self-consistent, and the bytes at a given key can never change after a URL is
43
+ * signed for it.
44
+ */
45
+ export declare function sourceTarballKey(orgId: string, sourceTarDigest: string): string;
46
+ /**
47
+ * Cache key for the pointer that resolves a workflow `contentHash` to the
48
+ * content hash of the tarball built from it: `source/v2/{orgId}/{contentHash}.hash`.
49
+ *
50
+ * This is the one mutable object in the scheme. A concurrent write replaces a
51
+ * pointer wholesale — there is no half-written window — so the worst a racing
52
+ * pair of builders can do is leave whichever pointer landed last, and both
53
+ * tarballs stay valid and fetchable.
54
+ */
55
+ export declare function sourcePointerKey(orgId: string, contentHash: string): string;
18
56
  export declare class SourceCache {
19
57
  private readonly storage;
20
58
  constructor(options: {
21
59
  storage: CacheStorage;
22
60
  });
23
- has(contentHash: string): Promise<boolean>;
24
- get(contentHash: string): Promise<Buffer | null>;
25
- getUrl(contentHash: string): Promise<string | null>;
26
- getUploadUrl(contentHash: string): Promise<string>;
27
- store(contentHash: string, tarball: Buffer | string): Promise<void>;
28
- remove(contentHash: string): Promise<boolean>;
61
+ /** The single place the pointer indirection is read. */
62
+ private resolvePointer;
63
+ /**
64
+ * Whether a source tarball for this org's `contentHash` exists.
65
+ *
66
+ * Both halves must be present: a pointer whose tarball has aged out is a
67
+ * miss, not a hit, or the caller skips a rebuild and dispatches a URL that
68
+ * 404s on the agent.
69
+ */
70
+ has(orgId: string, contentHash: string): Promise<boolean>;
71
+ get(orgId: string, contentHash: string): Promise<Buffer | null>;
72
+ /**
73
+ * A pre-signed download URL and the tarball's own digest.
74
+ *
75
+ * The pointer is resolved FIRST: it names the content hash, which is also the
76
+ * tarball's key, so the url and the digest returned here are read from a
77
+ * single source of truth and cannot disagree. Returns null on a miss —
78
+ * including a pointer with no tarball behind it, which is unverifiable and so
79
+ * is deliberately not served.
80
+ */
81
+ getUrlAndDigest(orgId: string, contentHash: string): Promise<{
82
+ url: string;
83
+ digest: string;
84
+ } | null>;
85
+ getUrl(orgId: string, contentHash: string): Promise<string | null>;
86
+ /**
87
+ * A pre-signed upload URL for direct agent-to-storage upload.
88
+ *
89
+ * Takes the tarball's OWN digest, not the workflow `contentHash`: the agent
90
+ * computes it before asking for a URL, and the object is named by it.
91
+ */
92
+ getUploadUrl(orgId: string, sourceTarDigest: string): Promise<string>;
93
+ /**
94
+ * Publish the pointer that makes an uploaded tarball discoverable by
95
+ * `contentHash`. Called only after the agent confirms its upload completed —
96
+ * publishing before the bytes land would let a reader follow the pointer to a
97
+ * missing object.
98
+ */
99
+ publishPointer(orgId: string, contentHash: string, sourceTarDigest: string): Promise<void>;
100
+ /** Store a tarball and the pointer that names it. */
101
+ store(orgId: string, contentHash: string, tarball: Buffer | string): Promise<void>;
102
+ /** SHA-256 of a tarball's bytes. The agent computes the same value. */
103
+ static computeDigest(data: Buffer): string;
104
+ /** Remove a source tarball and its pointer. */
105
+ remove(orgId: string, contentHash: string): Promise<boolean>;
29
106
  }
30
107
  //# sourceMappingURL=source-cache.d.ts.map
@@ -18,15 +18,26 @@
18
18
  import type { Kysely } from 'kysely';
19
19
  import type { Database } from '../db/types.js';
20
20
  import type { JobQueue } from '../queue/job-queue.js';
21
- import type { Dispatcher } from '../agent/dispatcher.js';
22
21
  import type { AgentRegistry } from '../agent/registry.js';
23
22
  import type { ExecutionTracker } from '../reporting/execution-tracker.js';
24
23
  export interface CancelRunDeps {
25
24
  db: Kysely<Database>;
26
25
  jobQueue: JobQueue;
27
- dispatcher: Dispatcher;
28
26
  registry: AgentRegistry;
29
27
  executionTracker: ExecutionTracker;
28
+ /**
29
+ * This coordinator's instance id. Undefined ⇒ no cluster identity, so every
30
+ * job resolves to "mine" and the behaviour is the single-coordinator one.
31
+ */
32
+ instanceId?: string;
33
+ /**
34
+ * Forward `job.cancel` to the coordinator holding the agent's socket.
35
+ * Returns whether the send left this process. Undefined ⇒ no peer transport,
36
+ * so a job owned by a sibling is unreachable rather than orphaned.
37
+ */
38
+ cancelJobOnPeer?: (peerId: string, runId: string, jobId: string, reason: string) => boolean;
39
+ /** How stale a `cluster_instances` heartbeat may be and still read as live. */
40
+ ownershipGraceMs?: number;
30
41
  }
31
42
  export interface CancelRunOptions {
32
43
  /** Force immediate SIGKILL on the agent (skip graceful hooks). */
@@ -39,6 +50,13 @@ export interface CancelRunOptions {
39
50
  export interface CancelRunResult {
40
51
  /** Number of `job.cancel` messages dispatched to agents (running jobs). */
41
52
  agentsNotified: number;
53
+ /**
54
+ * Jobs whose owning coordinator is alive but could not be reached — the peer
55
+ * send failed, or there is no peer transport. They are NOT notified and they
56
+ * are NOT orphaned: the job may well still be running, so the run stays
57
+ * `cancelling` and the re-drive sweep tries again.
58
+ */
59
+ unreachable: number;
42
60
  /** Number of pending/queued execution_jobs rows marked cancelled. */
43
61
  pendingCancelled: number;
44
62
  /**
@@ -0,0 +1,30 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { Database } from '../db/types.js';
3
+ export interface SweepStuckCancellingDeps {
4
+ db: Kysely<Database>;
5
+ /** The canonical cancel path, bound to this orchestrator's deps. */
6
+ cancelRun: (runId: string, reason: string) => Promise<unknown>;
7
+ /**
8
+ * How long a run may sit in `cancelling` before it is re-driven. Twice the
9
+ * recovery grace period: long enough that a graceful agent teardown finishes
10
+ * on its own, short enough that a dropped cancel does not sit for an hour.
11
+ */
12
+ stuckAfterMs: number;
13
+ }
14
+ /**
15
+ * Re-drive the cancel for runs left in `cancelling`.
16
+ *
17
+ * A cancel reaches its target over the peer channel, and a peer blip drops it.
18
+ * The run then sits in `cancelling` with its jobs still executing, and nothing
19
+ * retries: the workflow-deadline detector re-scans `cancelling` runs, but only
20
+ * ones that HAVE a workflow timeout, so a run without one waits forever. This
21
+ * extends the same coverage to every run.
22
+ *
23
+ * Leader-gated by its caller, and idempotent by construction — re-driving a
24
+ * cancel that already landed re-sends `job.cancel` to an agent that is already
25
+ * unwinding, and re-driving one whose jobs are all terminal completes the run.
26
+ *
27
+ * @returns how many runs were re-driven.
28
+ */
29
+ export declare function sweepStuckCancelling(deps: SweepStuckCancellingDeps): Promise<number>;
30
+ //# sourceMappingURL=sweep-stuck-cancelling.d.ts.map
@@ -199,6 +199,14 @@ export declare class AdminApiClient {
199
199
  skippedConfigs: number;
200
200
  reEncryptedBackends: number;
201
201
  skippedBackends: number;
202
+ reEncryptedSigningKeys: number;
203
+ skippedSigningKeys: number;
204
+ reEncryptedDashboardKeys: number;
205
+ skippedDashboardKeys: number;
206
+ reEncryptedEphemeralKeys: number;
207
+ skippedEphemeralKeys: number;
208
+ reEncryptedSecretOutputs: number;
209
+ skippedSecretOutputs: number;
202
210
  }>;
203
211
  createGenericSource(data: {
204
212
  orgId: string;
@@ -274,6 +282,8 @@ export declare class AdminApiClient {
274
282
  routingKey?: string;
275
283
  /** Optional absolute expiry as an ISO datetime string. */
276
284
  expiresAt?: string;
285
+ /** Optional intended holder (an OIDC `sub` or an email). Advisory only. */
286
+ subject?: string;
277
287
  }): Promise<{
278
288
  token: string;
279
289
  id: string;
@@ -5,7 +5,7 @@
5
5
  * and output format. Delegates to the platform-specific service
6
6
  * manager's logs() method.
7
7
  *
8
- * Target resolution goes through resolveInstance — the same priority chain
8
+ * Target resolution goes through resolveInstanceTarget — the same priority chain
9
9
  * every lifecycle command uses (--instance-dir > --name > CWD manifest >
10
10
  * refusal with candidate list).
11
11
  */
@@ -2,7 +2,7 @@
2
2
  * `kici-admin agent restart` command.
3
3
  *
4
4
  * Restarts the agent service. Target resolution goes through
5
- * resolveInstance — the same priority chain every lifecycle command uses
5
+ * resolveInstanceTarget — the same priority chain every lifecycle command uses
6
6
  * (--instance-dir > --name > CWD manifest > refusal with candidate list).
7
7
  * `--name` has no default; every invocation must resolve via one of the
8
8
  * above paths.
@@ -2,7 +2,7 @@
2
2
  * `kici-admin agent start` command.
3
3
  *
4
4
  * Starts the agent service. Target resolution goes through
5
- * resolveInstance — the same priority chain every lifecycle command uses
5
+ * resolveInstanceTarget — the same priority chain every lifecycle command uses
6
6
  * (--instance-dir > --name > CWD manifest > refusal with candidate list).
7
7
  * `--name` has no default; every invocation must resolve via one of the
8
8
  * above paths.
@@ -5,7 +5,7 @@
5
5
  * the running agent's health API for agent-specific info (labels,
6
6
  * orchestrator connection, current job).
7
7
  *
8
- * Target resolution goes through resolveInstance — the same priority chain
8
+ * Target resolution goes through resolveInstanceTarget — the same priority chain
9
9
  * every lifecycle command uses (--instance-dir > --name > CWD manifest >
10
10
  * refusal with candidate list).
11
11
  */
@@ -2,7 +2,7 @@
2
2
  * `kici-admin agent stop` command.
3
3
  *
4
4
  * Stops the agent service. Target resolution goes through
5
- * resolveInstance — the same priority chain every lifecycle command uses
5
+ * resolveInstanceTarget — the same priority chain every lifecycle command uses
6
6
  * (--instance-dir > --name > CWD manifest > refusal with candidate list).
7
7
  * `--name` has no default; every invocation must resolve via one of the
8
8
  * above paths.
@@ -2,7 +2,7 @@
2
2
  * `kici-admin agent uninstall` command.
3
3
  *
4
4
  * Removes the agent service registration. Target resolution goes
5
- * through resolveInstance — the same priority chain every lifecycle command
5
+ * through resolveInstanceTarget — the same priority chain every lifecycle command
6
6
  * uses (--instance-dir > --name > CWD manifest > refusal with candidate list).
7
7
  * The manifest stays on disk so subsequent commands can still reference the
8
8
  * deploy folder; the host-wide index entry is dropped unconditionally so a
@@ -6,5 +6,30 @@
6
6
  */
7
7
  import type { Command } from 'commander';
8
8
  import type { AdminApiClient } from '../api-client.js';
9
+ /**
10
+ * The authorized label scope a static agent token is minted with.
11
+ *
12
+ * Beyond the operator's own `--labels` set, the authorized scope carries:
13
+ *
14
+ * - A mandatory (taint) label must also be advertisable (the selector side),
15
+ * so union it into the authorized labels — a taint the agent can't even
16
+ * advertise would be incoherent.
17
+ * - Every real agent derives its `kici:role:*` labels from its own host and
18
+ * advertises them at registration, so a scope omitting them can never admit
19
+ * the agent that presents it: the orchestrator's register-time scope gate
20
+ * closes the connection with `Agent labels exceed token-bound scope`. A role
21
+ * the agent can't even advertise is incoherent for the same reason a taint
22
+ * is. The mint site cannot know which roles the target host will derive — the
23
+ * agent discovers them from its own capabilities — so the whole role
24
+ * namespace is authorized and the agent advertises the subset it has.
25
+ *
26
+ * The operator's own labels still bound everything else: a token minted with
27
+ * `--labels linux,x64` still refuses an agent advertising `gpu`. An agent that
28
+ * must carry no roles at all sets `KICI_ROLES=` on its own side.
29
+ *
30
+ * Returns undefined when the operator supplied no labels and no taint — an
31
+ * unscoped token, which the register-time gate skips entirely.
32
+ */
33
+ export declare function authorizedAgentTokenLabels(baseLabels: readonly string[], mandatoryLabels: readonly string[] | undefined): string[] | undefined;
9
34
  export declare function registerAgentCommands(program: Command, getClient: () => AdminApiClient): void;
10
35
  //# sourceMappingURL=agent.d.ts.map
@@ -12,5 +12,21 @@
12
12
  */
13
13
  import type { Command } from 'commander';
14
14
  import type { AdminApiClient } from '../api-client.js';
15
+ /**
16
+ * Whether a shared-config path is one a joining orchestrator receives.
17
+ *
18
+ * True for the consumed paths themselves and for anything below them, so
19
+ * `storage.bucket` counts and `storageQuota` does not.
20
+ */
21
+ export declare function isConsumedSharedConfigPath(path: string): boolean;
22
+ /**
23
+ * Warning text for a `config set` on a path nothing reads back, or null when
24
+ * the path is consumed.
25
+ *
26
+ * The write itself succeeds and is versioned — the operator is told which
27
+ * mechanism actually changes a running orchestrator, so a stored-but-inert
28
+ * value is not mistaken for an applied one.
29
+ */
30
+ export declare function sharedConfigSetWarning(path: string): string | null;
15
31
  export declare function registerConfigCommands(program: Command, getClient: () => AdminApiClient): void;
16
32
  //# sourceMappingURL=config.d.ts.map