@kici-dev/orchestrator 0.9.1 → 0.11.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 (101) hide show
  1. package/dist/__test-helpers__/test-db.d.ts +14 -1
  2. package/dist/agent/dispatcher.d.ts +137 -41
  3. package/dist/agent/registry.d.ts +79 -2
  4. package/dist/app.d.ts +2 -0
  5. package/dist/cancel/cancel-held-run.d.ts +68 -0
  6. package/dist/cancel/cancel-route-answer.d.ts +21 -0
  7. package/dist/cancel/cancel-run.d.ts +32 -0
  8. package/dist/cli/api-client.d.ts +23 -1
  9. package/dist/cli/commands/attestations.d.ts +10 -0
  10. package/dist/cli/commands/cold-store-impl.d.ts +3 -3
  11. package/dist/cli/commands/cold-store.d.ts +6 -3
  12. package/dist/cli/commands/context.d.ts +6 -2
  13. package/dist/cli/commands/dashboard-encryption-key.d.ts +18 -0
  14. package/dist/cli/commands/event-dlq.d.ts +2 -2
  15. package/dist/cli/commands/registration.d.ts +8 -4
  16. package/dist/cli/commands/runs.d.ts +7 -0
  17. package/dist/cli/commands/secret.d.ts +1 -0
  18. package/dist/cli/commands/shared/confirm.d.ts +19 -0
  19. package/dist/cli/commands/shared/unbound-context-warning.d.ts +43 -0
  20. package/dist/cli/commands/signing-key.d.ts +22 -1
  21. package/dist/cli.js +1522 -871
  22. package/dist/cluster/cluster-settings-reader.d.ts +1 -1
  23. package/dist/cluster/coordinator.d.ts +2 -0
  24. package/dist/cluster/live-coordinator-peer.d.ts +13 -0
  25. package/dist/config.d.ts +9 -0
  26. package/dist/contexts/context-store.d.ts +3 -2
  27. package/dist/contexts/protection/satisfiability.d.ts +6 -3
  28. package/dist/contexts/variable-store.d.ts +2 -0
  29. package/dist/db/migrations/145_execution_runs_workflow_provenance.d.ts +4 -0
  30. package/dist/db/migrations/146_pending_job_contexts_context_resolution.d.ts +4 -0
  31. package/dist/db/migrations/147_sealed_job_secrets.d.ts +19 -0
  32. package/dist/db/migrations/148_cluster_settings_sealed_secrets_retry_backoff_ms.d.ts +21 -0
  33. package/dist/db/migrations/149_workflow_registrations_dep_cache_key.d.ts +4 -0
  34. package/dist/db/migrations/150_execution_runs_status_epoch.d.ts +18 -0
  35. package/dist/db/migrations/151_workflow_registrations_dep_cache_key_sha.d.ts +4 -0
  36. package/dist/db/repos/signing-keys-repo.d.ts +48 -3
  37. package/dist/db/types.d.ts +81 -1
  38. package/dist/git/clone-token-auth.d.ts +7 -0
  39. package/dist/git/dispatch-git-auth.d.ts +63 -0
  40. package/dist/index.js +26 -0
  41. package/dist/lockfile-validate.d.ts +4 -2
  42. package/dist/metrics/prometheus.d.ts +1 -1
  43. package/dist/oidc/id-token-claims.d.ts +10 -0
  44. package/dist/oidc/oidc-mint-registration.d.ts +2 -0
  45. package/dist/oidc/orchestrator-mint.d.ts +25 -4
  46. package/dist/oidc/reconcile-signing-key.d.ts +31 -6
  47. package/dist/oidc/resolve-signer.d.ts +12 -6
  48. package/dist/pipeline/decorating-secret-resolver.d.ts +5 -3
  49. package/dist/pipeline/dispatch-matched-workflow.d.ts +105 -13
  50. package/dist/pipeline/dispatch-matched-workflow.test-helpers.d.ts +121 -0
  51. package/dist/pipeline/global-dispatch-identity.d.ts +76 -0
  52. package/dist/pipeline/global-dispatch.d.ts +91 -0
  53. package/dist/pipeline/global-eval-round.d.ts +56 -4
  54. package/dist/pipeline/global-round-hold.d.ts +41 -0
  55. package/dist/pipeline/held-context-data.d.ts +143 -0
  56. package/dist/pipeline/held-round-registrations.d.ts +40 -0
  57. package/dist/pipeline/install-gate.d.ts +93 -0
  58. package/dist/pipeline/install-secrets-resolver.d.ts +28 -13
  59. package/dist/pipeline/job-contexts.d.ts +21 -6
  60. package/dist/pipeline/pending-workflow-context.d.ts +15 -3
  61. package/dist/pipeline/process-webhook.d.ts +38 -1
  62. package/dist/pipeline/processor.d.ts +20 -0
  63. package/dist/pipeline/rerun-global.d.ts +28 -0
  64. package/dist/pipeline/rerun-inputs.d.ts +88 -0
  65. package/dist/pipeline/rerun.d.ts +25 -2
  66. package/dist/pipeline/resume-workflow.d.ts +36 -2
  67. package/dist/pipeline/unbound-job-context.d.ts +34 -0
  68. package/dist/pipeline/workflow-lock-at-sha.d.ts +67 -0
  69. package/dist/provenance/backfill-run.d.ts +5 -0
  70. package/dist/providers/github/check-status-poster.d.ts +4 -16
  71. package/dist/queue/job-queue.d.ts +127 -10
  72. package/dist/queue/terminalize-unroutable.d.ts +37 -4
  73. package/dist/registration/registration-index.d.ts +13 -1
  74. package/dist/registration/registration-store.d.ts +10 -1
  75. package/dist/reporting/execution-tracker.d.ts +149 -8
  76. package/dist/reporting/log-chunk-sink.d.ts +1 -1
  77. package/dist/reporting/log-writer.d.ts +14 -0
  78. package/dist/reporting/step-log-path.d.ts +8 -0
  79. package/dist/reporting/step-log-reader.d.ts +21 -3
  80. package/dist/routes/admin-contexts.d.ts +6 -0
  81. package/dist/routes/admin-registrations.d.ts +2 -2
  82. package/dist/routes/admin-signing-keys.d.ts +33 -0
  83. package/dist/scaler/agent-fit.d.ts +159 -0
  84. package/dist/scaler/container-routing.d.ts +16 -16
  85. package/dist/scaler/manager.d.ts +23 -17
  86. package/dist/secrets/audit-logger.d.ts +1 -1
  87. package/dist/secrets/context-secret-resolver.d.ts +1 -1
  88. package/dist/secrets/index.d.ts +1 -1
  89. package/dist/secrets/job-secret-gate.d.ts +25 -3
  90. package/dist/secrets/job-secret-seal.d.ts +36 -0
  91. package/dist/secrets/master-key-rotation.d.ts +7 -3
  92. package/dist/secrets/secret-resolver.d.ts +42 -23
  93. package/dist/server.js +27611 -23219
  94. package/dist/standalone.js +58327 -53119
  95. package/dist/worker/in-memory-job-queue.d.ts +12 -3
  96. package/dist/worker/reroute-dispatch.d.ts +35 -0
  97. package/dist/ws/agent-handler.d.ts +23 -10
  98. package/dist/ws/git-credential-relay.d.ts +16 -3
  99. package/installer-image-digests.json +3 -3
  100. package/package.json +4 -4
  101. package/sbom.spdx.json +53 -53
@@ -59,6 +59,7 @@ import { type Context as EngineContext } from '@kici-dev/engine';
59
59
  import type { OrchestratorMode, WorkflowDecision } from '@kici-dev/engine';
60
60
  import type { TrustPolicyStore } from '../security/trust-policy-store.js';
61
61
  import type { TrustDirectoryStore } from '../security/trust-directory-store.js';
62
+ import { type ContextDataDeps, type DeferredContextResolution } from './held-context-data.js';
62
63
  /**
63
64
  * Pending dispatch context for jobs gated by the needs scheduler.
64
65
  * Keyed by `${runId}:${jobName}`. Populated at processWebhook time for all
@@ -76,6 +77,19 @@ interface PendingJobContext {
76
77
  * to an agent.
77
78
  */
78
79
  invoke?: InvokeGateParams;
80
+ /**
81
+ * Set for a job bound to a context: what `dispatchReadyJob` resolves the
82
+ * job's context variables and secrets from when the job leaves its pending
83
+ * context, since `jobInput` is stored without them. Absent for a job with no
84
+ * bound context, which dispatches its stored input unchanged.
85
+ */
86
+ contextResolution?: DeferredContextResolution;
87
+ /**
88
+ * Set when the stored row's sealed secret fields could not be decrypted:
89
+ * `dispatchReadyJob` fails the job with this reason instead of dispatching
90
+ * it without them.
91
+ */
92
+ secretsUnavailable?: string;
79
93
  }
80
94
  /**
81
95
  * Register an eval gate and return a promise that resolves when the scheduler
@@ -580,6 +594,12 @@ export interface ReadyDispatchGateDeps {
580
594
  accessLogWriter?: Pick<AccessLogWriter, 'record'>;
581
595
  /** The routing key the audit row is attributed to, when the call site knows it. */
582
596
  routingKey?: string | null;
597
+ /**
598
+ * The stores a released held job's context variables and secrets resolve
599
+ * against. Called at release time, so a call site whose stores are built after
600
+ * it is wired can still supply them.
601
+ */
602
+ contextData?: () => ContextDataDeps;
583
603
  }
584
604
  /** What {@link resolveRunConcurrency} needs to evaluate the concurrency gate. */
585
605
  export interface RunConcurrency {
@@ -0,0 +1,28 @@
1
+ import type { OriginalRunRow, RerunDeps } from './rerun.js';
2
+ /** True when the run executed a workflow defined in another repository. */
3
+ export declare function isCrossRepoGlobalRun(originalRun: OriginalRunRow): boolean;
4
+ /**
5
+ * Re-run an organization-wide run against the commits it recorded: the source
6
+ * repository at `sha`, and the workflow repository at `workflow_sha`.
7
+ *
8
+ * The workflow is read from the workflow repository's lock file at the recorded
9
+ * commit, through that repository's registration — its provider bundle and
10
+ * provider context — never from the registration's current lock entry, which
11
+ * may have moved on. The run goes through the same pipeline a first global run
12
+ * does, so contexts, holds, approval gates, and the workflow repository's
13
+ * credentials apply exactly as they did.
14
+ *
15
+ * Refuses, naming the workflow repository, when the run recorded no workflow
16
+ * commit or the workflow is no longer registered there. Nothing falls back to a
17
+ * current lock file.
18
+ */
19
+ export declare function rerunCrossRepoGlobalRun(opts: {
20
+ originalRun: OriginalRunRow;
21
+ triggeredBy: string | null;
22
+ triggeredByAgentLabel: string | null;
23
+ deps: RerunDeps;
24
+ requestId: string;
25
+ }): Promise<{
26
+ newRunId: string;
27
+ }>;
28
+ //# sourceMappingURL=rerun-global.d.ts.map
@@ -0,0 +1,88 @@
1
+ import type { SimulatedEvent } from '@kici-dev/engine';
2
+ import type { ProviderRegistry } from '../provider-registry.js';
3
+ import type { TrustPolicyOutcome } from '../security/trust-policy-gate.js';
4
+ import type { WebhookInfo } from '../webhook/handler.js';
5
+ import type { OriginalRunRow, RerunDeps } from './rerun.js';
6
+ /**
7
+ * Load the original webhook payload from object storage. Returns
8
+ * null for cron/schedule runs (no payload was stored) or when the payload
9
+ * cannot be parsed.
10
+ */
11
+ export declare function loadWebhookPayload(originalRunId: string, deps: Pick<RerunDeps, 'logStorage'>): Promise<Record<string, unknown> | null>;
12
+ /** The provider bundle + context a re-run resolves out of the original run's row. */
13
+ export interface RerunProviderBinding {
14
+ providerBundle: NonNullable<ReturnType<ProviderRegistry['getByRoutingKey']>>;
15
+ providerContext: Record<string, unknown>;
16
+ routingKey: string;
17
+ }
18
+ /**
19
+ * Resolve the provider bundle and stored provider context for a run being
20
+ * re-run.
21
+ *
22
+ * Shared by the workflow re-run and the evaluation-round re-run so the two
23
+ * cannot disagree about which source a run belongs to, or about how its
24
+ * `provider_context` column is parsed.
25
+ */
26
+ export declare function resolveRerunProviderBinding(originalRun: OriginalRunRow, deps: Pick<RerunDeps, 'providerRegistry'>): RerunProviderBinding;
27
+ /** How a refusal to rebuild a re-run's inputs names the run and the way forward. */
28
+ export interface RerunRefusal {
29
+ /** The sentence's opening, naming the run: `Cannot re-run …`. */
30
+ subject: string;
31
+ /** How the run relates to its event: `was deciding` for a round, `ran for` for a run. */
32
+ eventVerb: string;
33
+ /** What the caller can do instead. */
34
+ remedy: string;
35
+ }
36
+ /** The refusal wording for a global evaluation round. */
37
+ export declare function roundRefusal(originalRun: OriginalRunRow): RerunRefusal;
38
+ /**
39
+ * The provider event name + action the original delivery carried.
40
+ *
41
+ * `execution_runs` records neither: the event name arrives in a provider header,
42
+ * not in the payload, so it cannot be recovered from the stored payload either.
43
+ * The delivery's own `event_log` row is where it lives, and a round's run row
44
+ * carries the delivery id that addresses it. That row is written at the end of
45
+ * the delivery, after the round's failure is recorded, so it is present for
46
+ * every re-run an operator can actually reach.
47
+ *
48
+ * Addressed by `(org_id, delivery_id)` — the table's own uniqueness — never by
49
+ * the delivery id alone. A generic source's delivery id is taken verbatim from
50
+ * a sender-supplied header, so one tenant can choose an id another tenant's
51
+ * round already carries; a lookup by id alone would then re-evaluate one org's
52
+ * global workflows against another org's event shape, and with no `ORDER BY`
53
+ * the row it picked would not even be stable.
54
+ */
55
+ export declare function loadDeliveryEventName(originalRun: OriginalRunRow, deps: Pick<RerunDeps, 'db'>, refusal?: RerunRefusal): Promise<{
56
+ event: string;
57
+ action: string | null;
58
+ }>;
59
+ /** The verdict an approved security hold stands for. */
60
+ export declare const RELEASED_DECISION: TrustPolicyOutcome;
61
+ /**
62
+ * The normalized form of the delivery the round was deciding.
63
+ *
64
+ * Read-only, and it throws — so it runs before the requestId claim, never
65
+ * inside the re-evaluation.
66
+ */
67
+ export declare function normalizeRoundEvent(originalRun: OriginalRunRow, providerBundle: NonNullable<ReturnType<ProviderRegistry['getByRoutingKey']>>, delivery: {
68
+ event: string;
69
+ action: string | null;
70
+ }, payload: Record<string, unknown>, refusal?: RerunRefusal): SimulatedEvent;
71
+ /**
72
+ * Stamp the re-evaluated event with the source repository's changed files.
73
+ *
74
+ * Unconditional, unlike the delivery path's fetch: that path skips the fetch
75
+ * when no trigger in the source repo's lock file uses path patterns, and a
76
+ * scoped re-evaluation has no such lock file to read. An error carries
77
+ * `unavailable`, which every path filter downstream already treats
78
+ * conservatively.
79
+ */
80
+ export declare function withChangedFiles(opts: {
81
+ event: SimulatedEvent;
82
+ bundle: NonNullable<ReturnType<ProviderRegistry['getByRoutingKey']>>;
83
+ info: WebhookInfo;
84
+ payload: Record<string, unknown>;
85
+ credentials: Record<string, unknown>;
86
+ repoIdentifier: string;
87
+ }): Promise<SimulatedEvent>;
88
+ //# sourceMappingURL=rerun-inputs.d.ts.map
@@ -10,8 +10,8 @@
10
10
  * It goes directly to: lock file parse -> job expansion -> dispatch.
11
11
  */
12
12
  import { type ColdStore } from '@kici-dev/shared';
13
- import type { Kysely } from 'kysely';
14
- import type { Database } from '../db/types.js';
13
+ import type { Kysely, Selectable } from 'kysely';
14
+ import type { Database, ExecutionRunTable } from '../db/types.js';
15
15
  import type { LogStorage } from '../reporting/log-storage.js';
16
16
  import type { ProviderRegistry } from '../provider-registry.js';
17
17
  import type { Dispatcher } from '../agent/dispatcher.js';
@@ -78,6 +78,12 @@ export interface RerunDeps {
78
78
  */
79
79
  processingDeps?: (() => ProcessingDeps) | null;
80
80
  }
81
+ /**
82
+ * Original run row from execution_runs (selectAll).
83
+ * Aliased to the Kysely Selectable so all column names stay typed
84
+ * as the underlying schema.
85
+ */
86
+ export type OriginalRunRow = Selectable<ExecutionRunTable>;
81
87
  export declare function handleRerun(originalRunId: string, triggeredBy: string | null, triggeredByAgentLabel: string | null, deps: RerunDeps,
82
88
  /**
83
89
  * Platform-minted `requestId` for this rerun. Stable across an HA relay
@@ -104,4 +110,21 @@ routingKeyHint?: string): Promise<{
104
110
  * deterministically instead of racing the microtask queue.
105
111
  */
106
112
  export declare function settlePendingRoundReevaluations(): Promise<void>;
113
+ /**
114
+ * Release a held global evaluation round: re-evaluate the round of its workflow
115
+ * repository from the stored webhook payload with the approved event treated as
116
+ * passing, dispatch what it admits, and settle the held run.
117
+ *
118
+ * Reached from the security queue's workflow release (`resumeWorkflow`), which
119
+ * recognises the held round by its run row. The held row stores no dispatch
120
+ * context: everything is rebuilt from the row, the payload and the delivery's
121
+ * event log entry — the same reconstruction a re-run of a failed round uses —
122
+ * so the release does not depend on how a workflow dispatch context would be
123
+ * replayed.
124
+ *
125
+ * Never throws. A round that cannot be rebuilt or decided fails the held run
126
+ * with the reason, which is the record an approver can read. A release that
127
+ * cannot claim the held row — another release already did — does nothing.
128
+ */
129
+ export declare function releaseHeldGlobalEvalRound(runId: string, deps: ProcessingDeps): Promise<void>;
107
130
  //# sourceMappingURL=rerun.d.ts.map
@@ -1,16 +1,42 @@
1
1
  import type { Kysely } from 'kysely';
2
2
  import type { Database } from '../db/types.js';
3
3
  import type { ProcessingDeps } from './processor.js';
4
+ import type { ProviderBundle } from '../provider-registry.js';
4
5
  import type { ReleaseSignal } from '../contexts/held-runs.js';
5
6
  import { type WorkflowDispatchContext } from './dispatch-matched-workflow.js';
6
7
  import { type SerializableWorkflowDispatchInputs } from './pending-workflow-context.js';
8
+ import type { GlobalDispatchIdentity } from './global-dispatch-identity.js';
7
9
  import { type SecurityCheckHold } from './security-hold-check.js';
10
+ /**
11
+ * A global run's identity rebuilt from its stored hold, before the workflow
12
+ * repository's credentials are minted. It has no `workflowCredentials`, so a
13
+ * context carrying it is not a {@link WorkflowDispatchContext}.
14
+ */
15
+ export type UnmintedGlobalIdentity = Omit<GlobalDispatchIdentity, 'workflowCredentials' | 'workflowBundle'> & {
16
+ readonly workflowBundle: ProviderBundle;
17
+ };
18
+ /**
19
+ * A dispatch context rebuilt from a stored hold. `dispatchMatchedWorkflow`
20
+ * does not accept it: only {@link withWorkflowRepoCredentials} turns it into a
21
+ * {@link WorkflowDispatchContext}, so a resume cannot dispatch a global run
22
+ * without a freshly minted clone token.
23
+ */
24
+ export type RebuiltWorkflowDispatchContext = Omit<WorkflowDispatchContext, 'global'> & {
25
+ global?: UnmintedGlobalIdentity;
26
+ };
8
27
  /**
9
28
  * Rebuild a live `WorkflowDispatchContext` from the persisted serializable
10
29
  * inputs by re-attaching the orchestrator's live `deps` and reconstructing the
11
30
  * provider `bundle` from the live registry (keyed by the stored routing key).
12
31
  * Returns null when the provider bundle can no longer be resolved.
13
32
  *
33
+ * A global run's identity is rebuilt from what was stored at hold time — the
34
+ * workflow commit, branch and provider context it was held at — never from the
35
+ * live registration, which may have moved to a newer commit while the run
36
+ * waited. Its workflow bundle is looked up by the stored workflow routing key,
37
+ * and it has no credentials until {@link withWorkflowRepoCredentials} mints
38
+ * them. Returns null when the workflow bundle can no longer be resolved either.
39
+ *
14
40
  * The key it looks the bundle up by is `effectiveRoutingKey ?? info.routingKey`
15
41
  * — the post-overlay key, the same one `setupDispatchContext` builds
16
42
  * `setup.info` from and the same one the run row records. `info.routingKey` is
@@ -19,7 +45,13 @@ import { type SecurityCheckHold } from './security-hold-check.js';
19
45
  * than the source the workflow belongs to: the resumed run would get the wrong
20
46
  * app, the wrong credentials, and a check poster writing to the wrong place.
21
47
  */
22
- export declare function rebuildWorkflowDispatchContext(inputs: SerializableWorkflowDispatchInputs, deps: ProcessingDeps): WorkflowDispatchContext | null;
48
+ export declare function rebuildWorkflowDispatchContext(inputs: SerializableWorkflowDispatchInputs, deps: ProcessingDeps): RebuiltWorkflowDispatchContext | null;
49
+ /**
50
+ * The rebuilt context with fresh credentials for a global run's workflow
51
+ * repository, minted by its bundle the way the first dispatch minted them. A
52
+ * same-repo context gets no global identity. Throws when the mint fails.
53
+ */
54
+ export declare function withWorkflowRepoCredentials(rebuilt: RebuiltWorkflowDispatchContext): Promise<WorkflowDispatchContext>;
23
55
  /**
24
56
  * Resume a released workflow-scoped hold. Loads the pending context, rebuilds
25
57
  * the dispatch context, and re-dispatches. On a lost pending context (or
@@ -55,5 +87,7 @@ export declare function resumeWorkflow(signal: ReleaseSignal, deps: ProcessingDe
55
87
  * a rejection this declines to report (an install gate, or a commit whose other
56
88
  * holds are still pending) leaves the caller free to decide for itself.
57
89
  */
58
- export declare function rejectWorkflow(hold: SecurityCheckHold, deps: ProcessingDeps, db: Kysely<Database> | undefined, reason: string): Promise<boolean>;
90
+ export declare function rejectWorkflow(hold: SecurityCheckHold, deps: ProcessingDeps, db: Kysely<Database> | undefined, reason: string, opts?: {
91
+ runHeld?: boolean;
92
+ }): Promise<boolean>;
59
93
  //# sourceMappingURL=resume-workflow.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The dispatch-time warning for a job context that has no scope binding.
3
+ *
4
+ * A job receives a context's secrets only through the context's scope
5
+ * bindings, so a context with none delivers no secret, and nothing fails until
6
+ * a step reads one. This names each such context in the orchestrator log when
7
+ * the job's context data resolves. Advisory only: it runs after resolution, a
8
+ * failed lookup logs nothing, and the dispatch proceeds unchanged.
9
+ */
10
+ import type { Context } from '@kici-dev/engine';
11
+ import type { SecretResolverApi } from '../secrets/secret-resolver.js';
12
+ /** The warning line's message; the structured fields carry the identities. */
13
+ export declare const UNBOUND_JOB_CONTEXT_MESSAGE = "Job binds a context that has no scope binding; it receives no secrets from it";
14
+ /**
15
+ * Warn once for every context in `entries` that resolved no secret and has no
16
+ * scope binding. A context that resolved at least one secret is skipped without
17
+ * a lookup, so a bound context costs nothing here.
18
+ */
19
+ export declare function warnUnboundJobContexts(args: {
20
+ secretResolver: SecretResolverApi | undefined;
21
+ orgId: string;
22
+ entries: ReadonlyArray<{
23
+ name: string;
24
+ env: Context;
25
+ }>;
26
+ /** Per-context resolved secrets, keyed by the declared name; empty maps are absent. */
27
+ resolvedByName: Readonly<Record<string, Record<string, string>>> | undefined;
28
+ log: {
29
+ runId: string;
30
+ workflow: string;
31
+ job: string;
32
+ };
33
+ }): Promise<void>;
34
+ //# sourceMappingURL=unbound-job-context.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Load a workflow repository's lock entries as they were at a recorded commit.
3
+ *
4
+ * An organization-wide run records the commit of the repository that defines
5
+ * its workflow (`execution_runs.workflow_sha`). Two paths act on such a run
6
+ * later, after that repository may have moved on: the re-run of a finished
7
+ * global run, and the release of a held evaluation round. Both must run the
8
+ * workflow version the run was created against, never the registration's
9
+ * current one. This module is the one place that resolves it.
10
+ */
11
+ import type { LockFile, LockWorkflow } from '@kici-dev/engine';
12
+ import type { ProviderRegistry } from '../provider-registry.js';
13
+ import type { RegisteredWorkflow } from '../registration/registration-index.js';
14
+ import { type DepCacheKey } from '@kici-dev/shared';
15
+ /**
16
+ * Whether a lock entry is an organization-wide workflow: a trigger carries
17
+ * `repos:`. The re-run and the held-round release both judge a workflow by its
18
+ * entry at the recorded commit through this one rule, never by the current
19
+ * registration.
20
+ */
21
+ export declare function isOrganizationWideEntry(entry: LockWorkflow): boolean;
22
+ /**
23
+ * The whole lock file of a registration's repository at `sha`, fetched through
24
+ * the registration's bundle. Throws, naming the repository, when it cannot be
25
+ * read.
26
+ */
27
+ export declare function loadWorkflowLockFileAtSha(args: {
28
+ registration: WorkflowLockRegistration;
29
+ sha: string;
30
+ providerRegistry: Pick<ProviderRegistry, 'getByRoutingKey'>;
31
+ }): Promise<LockFile>;
32
+ /** The registration fields that address a workflow repository's lock file. */
33
+ export type WorkflowLockRegistration = Pick<RegisteredWorkflow, 'repoIdentifier' | 'workflowName' | 'routingKey' | 'providerContext' | 'commitSha' | 'lockEntry' | 'lockfileHash' | 'siblingsDigest'>;
34
+ /** One workflow's lock entry at a commit, with the dependency-cache key of the lock file it comes from. */
35
+ export interface WorkflowLockEntryAtSha {
36
+ lockEntry: LockWorkflow;
37
+ depCacheKey: DepCacheKey;
38
+ }
39
+ /**
40
+ * The lock entries of `registrations` at `sha`, keyed by workflow name.
41
+ *
42
+ * Every registration must belong to the same workflow repository. When each
43
+ * registration is already at `sha`, its stored lock entry is that version and
44
+ * nothing is fetched. Otherwise the lock file is fetched at `sha` through the
45
+ * registration's own provider bundle, with the registration's provider context
46
+ * — the same fetcher a per-repository re-run reads its own lock file with. A
47
+ * workflow the lock file at `sha` does not define is absent from the result.
48
+ *
49
+ * Throws, naming the workflow repository, when the lock file cannot be read:
50
+ * the caller refuses rather than falling back to any other version.
51
+ */
52
+ export declare function loadWorkflowLockEntriesAtSha(args: {
53
+ registrations: readonly WorkflowLockRegistration[];
54
+ sha: string;
55
+ providerRegistry: Pick<ProviderRegistry, 'getByRoutingKey'>;
56
+ }): Promise<Map<string, LockWorkflow>>;
57
+ /**
58
+ * The lock entry of one registered workflow at `sha`, with the dependency-cache
59
+ * key of the lock file it comes from. Throws, naming the workflow repository,
60
+ * when the lock file at `sha` does not define it.
61
+ */
62
+ export declare function loadWorkflowLockEntryAtSha(args: {
63
+ registration: WorkflowLockRegistration;
64
+ sha: string;
65
+ providerRegistry: Pick<ProviderRegistry, 'getByRoutingKey'>;
66
+ }): Promise<WorkflowLockEntryAtSha>;
67
+ //# sourceMappingURL=workflow-lock-at-sha.d.ts.map
@@ -20,6 +20,11 @@ export interface BackfillRunRow {
20
20
  started_at: Date | null;
21
21
  completed_at: Date | null;
22
22
  duration_ms: number | null;
23
+ /**
24
+ * The run's status generation. Forwarded so the Platform mirror records the
25
+ * generation the backfilled status belongs to. Absent reads as 0.
26
+ */
27
+ status_epoch?: number | null;
23
28
  }
24
29
  /** The local `execution_jobs` fields the backfill needs. */
25
30
  export interface BackfillJobRow {
@@ -4,12 +4,11 @@
4
4
  * Posts check statuses to GitHub PRs using the Checks API for:
5
5
  * - Security holds (pending): "KiCI Security" — "Held for approval"
6
6
  * - Workflow modifications (neutral): "KiCI: Workflow changes"
7
- * - Org globals skipped by the trust policy (neutral): "KiCI: Organization workflows"
8
7
  * - Org global evaluation failed (failure): "KiCI: Organization workflow evaluation"
9
8
  * - Approved runs (success) / Rejected/expired runs (failure)
10
9
  *
11
- * `postWorkflowModificationCheck`, `postGlobalWorkflowsSkippedCheck`, and
12
- * `postGlobalEvalFailedCheck` are interface-backed (the `CheckStatusPoster`
10
+ * `postWorkflowModificationCheck` and `postGlobalEvalFailedCheck` are
11
+ * interface-backed (the `CheckStatusPoster`
13
12
  * contract in `@kici-dev/engine`) and post on their own check names so none of
14
13
  * them overwrites the security-hold check.
15
14
  *
@@ -38,23 +37,12 @@ export declare class GitHubCheckStatusPoster implements CheckStatusPoster {
38
37
  * conflict with the security hold check. Always posted as neutral/completed.
39
38
  */
40
39
  postWorkflowModificationCheck(repoIdentifier: string, commitSha: string, modifications: WorkflowModification[], credentials: unknown): Promise<void>;
41
- /**
42
- * Post the informational check recording that the org's global workflows were
43
- * skipped because the trust policy held or rejected the event.
44
- *
45
- * Uses a separate check name ("KiCI: Organization workflows") so it never
46
- * overwrites the security-hold check: that one is a single named run per
47
- * commit which the hold posts as pending and approve / reject later complete,
48
- * so writing this notice onto it would resolve the hold's check to neutral
49
- * while the run is still held. Always posted as neutral/completed.
50
- */
51
- postGlobalWorkflowsSkippedCheck(repoIdentifier: string, commitSha: string, summary: string, credentials: unknown): Promise<void>;
52
40
  /**
53
41
  * Post the check recording that the pre-run evaluation of the organization's
54
42
  * global workflows failed, so none of the workflows it was deciding on ran.
55
43
  *
56
- * Its own check name ("KiCI: Organization workflow evaluation") for the same
57
- * reason as the notice above: the security-hold check is a single named run
44
+ * Its own check name ("KiCI: Organization workflow evaluation") because the
45
+ * security-hold check is a single named run
58
46
  * per commit which a hold posts as pending and approve / reject later
59
47
  * complete, so writing this through `postCheckStatus` would resolve a
60
48
  * still-held run's check and unblock a branch protection rule that requires
@@ -2,6 +2,7 @@ import { type Kysely, type SqlBool } from 'kysely';
2
2
  import { type CanonicalLabel, type CanonicalMatcher, type LabelMatcher, type ResourceRequest } from '@kici-dev/engine';
3
3
  import type { Database } from '../db/types.js';
4
4
  import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
5
+ import { JobContainerNeed } from '../scaler/agent-fit.js';
5
6
  /**
6
7
  * The ownership predicate, in its two forms. Both come from here so they cannot
7
8
  * drift apart, because they answer the same question from opposite directions.
@@ -35,6 +36,8 @@ export interface OwnershipPredicateOptions {
35
36
  */
36
37
  livePeerIds?: readonly string[];
37
38
  }
39
+ /** Recorded on a job whose direct dispatch was refused because its run stopped. */
40
+ export declare const STOPPED_RUN_DISPATCH_REASON = "the run stopped before this job was dispatched";
38
41
  /**
39
42
  * Strict selection: the rows a cluster-wide sweep may recover. Excludes a NULL
40
43
  * owner and excludes any row whose owner is still live.
@@ -67,6 +70,12 @@ export interface ExpiredJobInfo {
67
70
  runsOnPatterns: CanonicalMatcher[];
68
71
  excludeLabels: CanonicalLabel[];
69
72
  excludePatterns: CanonicalMatcher[];
73
+ /**
74
+ * What the job's `container:` asks of the agent that runs it. A container job
75
+ * reaches only an agent that can start one, so an agent that merely matches
76
+ * its labels may still be no route at all. Absent reads as no container.
77
+ */
78
+ container?: JobContainerNeed;
70
79
  }
71
80
  /**
72
81
  * A pending job as the unroutable probe sees it: the same routing facts the
@@ -109,11 +118,23 @@ export declare enum DispatchQueueStatus {
109
118
  Expired = "expired",
110
119
  Recovering = "recovering"
111
120
  }
121
+ /**
122
+ * The `dispatch_queue` statuses a row never leaves. `cancelled` is not a
123
+ * {@link DispatchQueueStatus}, but existing databases hold rows the test-run
124
+ * cancel stamped with it; those rows are terminal too, so the prune and the
125
+ * sealed-secrets scrub cover them.
126
+ */
127
+ export declare const TERMINAL_DISPATCH_STATUSES: string[];
112
128
  /**
113
129
  * Maximum delivery attempts for a single dispatch_queue job. A job whose
114
130
  * `dispatch_attempts` reaches this value is failed permanently instead of
115
131
  * being requeued again. Bounds requeue loops from repeated job.reject /
116
- * pre-start agent loss; `expires_at` is the time-based backstop.
132
+ * pre-start agent loss. A busy job.reject is not counted while the agent keeps
133
+ * reporting an empty slot between rejections, but is counted once the agent is
134
+ * re-picked only because its busy hold expired, so this cap still bounds an
135
+ * agent whose job count is stuck. `expires_at` is no backstop for this: a
136
+ * job dispatched straight to an agent carries none, and requeue does not set
137
+ * one.
117
138
  */
118
139
  export declare const MAX_DISPATCH_ATTEMPTS = 5;
119
140
  /**
@@ -231,6 +252,12 @@ export interface QueuedJob {
231
252
  resources?: ResourceRequest;
232
253
  /** For a runsOnAll host-fanout child: the agent this job is pinned to. */
233
254
  pinnedAgentId?: string;
255
+ /**
256
+ * Set when the row's sealed secret fields could not be decrypted. The
257
+ * dispatcher never sends such a job: it puts the job back so a coordinator
258
+ * holding the key can take it, or fails it with this reason when none can.
259
+ */
260
+ secretsUnavailable?: string;
234
261
  }
235
262
  /**
236
263
  * The resource shape a job declares, read from the one location that always
@@ -272,6 +299,19 @@ export declare class JobQueue {
272
299
  * gauge callback never issues an extra COUNT per scrape.
273
300
  */
274
301
  private breakdownCache;
302
+ /**
303
+ * Jobs this coordinator put back because it could not open their sealed
304
+ * secrets (sealed with a master key it does not hold), keyed to the time it
305
+ * may claim them again. Until then every claim path and the re-drive readers
306
+ * skip them, so during a rolling key rotation a coordinator that holds the
307
+ * key can take the job in that window. After the back-off this coordinator
308
+ * can claim the job again, and each claim spends another attempt, so a job
309
+ * no coordinator can open still reaches {@link MAX_DISPATCH_ATTEMPTS} and
310
+ * fails. Bounded; an evicted entry only ends its back-off early.
311
+ */
312
+ private readonly deferredUnopenable;
313
+ /** The configured default for `sealed_secrets_retry_backoff_ms`. */
314
+ private readonly sealedSecretsRetryBackoffMs;
275
315
  constructor(db: Kysely<Database>, options: {
276
316
  maxDepth: number;
277
317
  defaultTimeoutMs: number;
@@ -293,6 +333,11 @@ export declare class JobQueue {
293
333
  * recovery grace period.
294
334
  */
295
335
  ownershipGraceMs?: number;
336
+ /**
337
+ * Default back-off before this coordinator claims again a job whose
338
+ * sealed secrets it could not open; `cluster_settings` overrides it.
339
+ */
340
+ sealedSecretsRetryBackoffMs?: number;
296
341
  });
297
342
  /** Ownership predicate inputs, in the shape both predicate builders take. */
298
343
  private ownershipOptions;
@@ -324,12 +369,14 @@ export declare class JobQueue {
324
369
  * @param agentMandatoryLabels Mandatory labels the spawning scaler declared
325
370
  * (empty for static / non-scaler agents).
326
371
  * @param canServe Optional extra predicate applied to every candidate row
327
- * before it is claimed. Labels cannot express whether a pre-spawned agent's
328
- * fixed cpu / memory / image fit a job, so a caller that has to answer that
329
- * supplies it here — the row must never be claimed and put back, because a
330
- * claim-then-release both strands it as Dispatched for a window and burns a
331
- * dispatch attempt. Supplying it opts out of the single-statement fast
332
- * path, so pass it only for an agent that actually needs the check.
372
+ * before it is claimed. Labels cannot express whether an agent may run a
373
+ * job — a pre-spawned agent's fixed shape, a container job on an agent with
374
+ * no runtime, an agent started inside another job's image — so a caller
375
+ * that has to answer that supplies it here. The row must never be claimed
376
+ * and put back, because a claim-then-release both strands it as Dispatched
377
+ * for a window and burns a dispatch attempt. Supplying it opts out of the
378
+ * single-statement fast path, so pass it only for an agent that actually
379
+ * needs the check.
333
380
  * @returns The matching job, or null if none found.
334
381
  */
335
382
  dequeueForLabels(agentLabels: string[], agentMandatoryLabels?: string[], agentId?: string, canServe?: (job: QueuedJob) => boolean): Promise<QueuedJob | null>;
@@ -491,12 +538,38 @@ export declare class JobQueue {
491
538
  * it can avoid double-dispatching an already-present job. The conflicting row
492
539
  * keeps the owner the winning writer recorded.
493
540
  *
494
- * @returns The job ID and whether a new row was inserted (false = row already existed).
541
+ * A job whose run has already stopped is refused, with the same stopped-run
542
+ * guard the queue drain applies (`notTerminalRunSql`). The row is still
543
+ * written, then settled `expired` in a guarded update — the state a cancel
544
+ * leaves a queued job of its run in — and the job's `execution_jobs` row, if
545
+ * one exists yet, is cancelled. `runStopped` tells the caller not to send it.
546
+ * Checking after the insert rather than before closes the race with a cancel:
547
+ * a cancel that lands after the check sees the dispatched row and cancels it
548
+ * through the agent, and one that lands before is seen by the check.
549
+ *
550
+ * @returns The job ID, whether a new row was inserted (false = row already
551
+ * existed), and whether the dispatch was refused because the run stopped.
495
552
  */
496
553
  insertDispatched(job: QueuedJobInput, agentId: string): Promise<{
497
554
  id: string;
498
555
  inserted: boolean;
556
+ runStopped?: boolean;
499
557
  }>;
558
+ /**
559
+ * Settle a claimed row whose run stopped after the claim: expire it and
560
+ * cancel its pending job row, the way the cancel path ends a stopped run's
561
+ * work. {@link requeue} refuses a stopped run's row, so a caller that claimed
562
+ * a job to put it back uses this when that requeue returns null; otherwise
563
+ * the row stays dispatched to no agent until a sweep finds it. Returns
564
+ * whether it settled the row: false when the row is no longer dispatched or
565
+ * its run is still going.
566
+ */
567
+ settleClaimOfStoppedRun(id: string, runId: string): Promise<boolean>;
568
+ /**
569
+ * Settle a dispatched row `expired` when its run has stopped, and cancel the
570
+ * job's `execution_jobs` row. Returns whether it did.
571
+ */
572
+ private expireDispatchOfStoppedRun;
500
573
  /**
501
574
  * Mark a job as dispatched and record the agent it went to.
502
575
  *
@@ -555,6 +628,15 @@ export declare class JobQueue {
555
628
  * @returns Number of affected rows.
556
629
  */
557
630
  cancelByRunId(runId: string): Promise<number>;
631
+ /**
632
+ * Drop the sealed secret fields of every terminal row. A completed, failed or
633
+ * expired row is never dispatched again (a requeue moves only a dispatched
634
+ * row), so its secrets have no reader left; the row stays for its retention
635
+ * window without them.
636
+ *
637
+ * @returns Number of rows scrubbed.
638
+ */
639
+ scrubTerminalSealedSecrets(): Promise<number>;
558
640
  /**
559
641
  * Delete terminal dispatch_queue rows older than `retentionDays`.
560
642
  *
@@ -739,10 +821,17 @@ export declare class JobQueue {
739
821
  * job started. Only flips rows still in 'dispatched' — a job that was
740
822
  * concurrently completed / failed / cancelled is left untouched.
741
823
  *
742
- * @returns the post-increment dispatch_attempts, or null when the row
824
+ * `countAttempt: false` leaves the counter alone — used for a busy rejection
825
+ * from an agent still tearing down its previous job (see
826
+ * {@link MAX_DISPATCH_ATTEMPTS} for when a busy rejection does count).
827
+ *
828
+ * @returns the dispatch_attempts after the requeue, or null when the row
743
829
  * was not in 'dispatched' state (nothing requeued).
744
830
  */
745
- requeue(jobId: string): Promise<number | null>;
831
+ requeue(jobId: string, opts?: {
832
+ countAttempt?: boolean;
833
+ provisioningError?: string;
834
+ }): Promise<number | null>;
746
835
  /**
747
836
  * Requeue a dispatched job **only** when its ack genuinely never landed.
748
837
  *
@@ -876,6 +965,34 @@ export declare class JobQueue {
876
965
  * dispatched, cancelled, expired, or claimed by another coordinator.
877
966
  */
878
967
  claimUnroutable(id: string): Promise<boolean>;
968
+ /**
969
+ * Leave a job alone for the sealed-secrets back-off: the dispatcher calls
970
+ * this when it puts back a job whose sealed secrets this coordinator could not
971
+ * open. See {@link deferredUnopenable}.
972
+ */
973
+ deferUnopenable(jobId: string): Promise<void>;
974
+ /** Whether `jobId` is still inside its sealed-secrets back-off on this coordinator. */
975
+ isDeferredUnopenable(jobId: string): boolean;
976
+ /**
977
+ * Claim a specific pending job for no agent: Pending -> Dispatched, with no
978
+ * `agent_id`. The dispatcher calls this for a job whose sealed secrets this
979
+ * coordinator cannot open, when a re-drive would otherwise scale for it, so
980
+ * the job gets the put-back or fail decision a drain claim would give it.
981
+ *
982
+ * The guards of the other claim paths apply: the row is still pending and
983
+ * unexpired, its run has not stopped, and it is outside this coordinator's
984
+ * sealed-secrets back-off. The `status = Pending` guard is the arbiter, so a
985
+ * concurrent claimant makes this return false.
986
+ *
987
+ * @returns whether this call claimed the row.
988
+ */
989
+ claimUnopenableById(jobId: string): Promise<boolean>;
990
+ /**
991
+ * The query condition that skips jobs still inside their sealed-secrets
992
+ * back-off, or undefined when none is (the common case adds no predicate).
993
+ * Expired entries are dropped here, which is what lets a job be claimed again.
994
+ */
995
+ private deferredFilter;
879
996
  /**
880
997
  * Convert a DB row to a QueuedJob object.
881
998
  * Handles both auto-parsed JSONB arrays (from pg driver) and JSON strings (from tests).