@kici-dev/orchestrator 0.4.0 → 0.6.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 (183) hide show
  1. package/dist/__test-helpers__/fake-scaler-state-store.d.ts +24 -0
  2. package/dist/__test-helpers__/mock-db-query.d.ts +130 -0
  3. package/dist/__test-helpers__/mock-db.d.ts +27 -0
  4. package/dist/agent/agent-version.d.ts +34 -0
  5. package/dist/agent/dispatcher.d.ts +151 -13
  6. package/dist/agent/host-roster.d.ts +37 -5
  7. package/dist/agent/registry.d.ts +2 -0
  8. package/dist/app.d.ts +56 -7
  9. package/dist/approvals/apply-decision.d.ts +67 -19
  10. package/dist/approvals/held-run-release-wiring.d.ts +45 -0
  11. package/dist/approvals/independent-wiring.d.ts +55 -0
  12. package/dist/approvals/resume-router.d.ts +9 -0
  13. package/dist/approvals/triggerer-subject.d.ts +37 -0
  14. package/dist/cache/dep-cache.d.ts +54 -10
  15. package/dist/cache/global-eval-round-cache.d.ts +95 -0
  16. package/dist/cache/index.d.ts +3 -0
  17. package/dist/cache/pending-global-evals.d.ts +42 -0
  18. package/dist/cache/pending-inits.d.ts +10 -0
  19. package/dist/cli/api-client.d.ts +18 -0
  20. package/dist/cli/commands/cluster-settings.d.ts +52 -3
  21. package/dist/cli/commands/diagnose.d.ts +6 -0
  22. package/dist/cli/commands/held-run.d.ts +58 -0
  23. package/dist/cli/commands/runs.d.ts +1 -0
  24. package/dist/cli/commands/trust-policy.d.ts +117 -12
  25. package/dist/cli/kici-admin.d.ts +10 -0
  26. package/dist/cli.js +2770 -938
  27. package/dist/cluster/cluster-identity.d.ts +3 -3
  28. package/dist/cluster/cluster-settings-reader.d.ts +59 -1
  29. package/dist/cluster/coordinator.d.ts +44 -0
  30. package/dist/cluster/index.d.ts +4 -0
  31. package/dist/cluster/peer-handler.d.ts +19 -0
  32. package/dist/cluster/peer-registry.d.ts +17 -0
  33. package/dist/cluster/plan-headroom-store.d.ts +26 -0
  34. package/dist/cluster/worker-eviction.d.ts +69 -0
  35. package/dist/cold-store/load-event-log-range.d.ts +10 -3
  36. package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
  37. package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
  38. package/dist/config.d.ts +48 -21
  39. package/dist/content-requirements-cache.d.ts +55 -0
  40. package/dist/contexts/held-runs.d.ts +123 -16
  41. package/dist/contexts/protection/aggregate.d.ts +3 -2
  42. package/dist/contexts/protection/branch-gate.d.ts +15 -0
  43. package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
  44. package/dist/contexts/protection/pipeline.d.ts +18 -0
  45. package/dist/contexts/release-queued-holds.d.ts +35 -0
  46. package/dist/dashboard/attestation-filters.d.ts +8 -0
  47. package/dist/dashboard/handler.d.ts +6 -24
  48. package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
  49. package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
  50. package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
  51. package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
  52. package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
  53. package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
  54. package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
  55. package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
  56. package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
  57. package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
  58. package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
  59. package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
  60. package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
  61. package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
  62. package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
  63. package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
  64. package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
  65. package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
  66. package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
  67. package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
  68. package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
  69. package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
  70. package/dist/db/types.d.ts +428 -12
  71. package/dist/entry-helpers.d.ts +28 -0
  72. package/dist/events/event-emitter.d.ts +22 -0
  73. package/dist/events/event-router.d.ts +87 -1
  74. package/dist/events/types.d.ts +2 -23
  75. package/dist/git/credential-broker.d.ts +94 -0
  76. package/dist/git/inherited-secret.d.ts +24 -0
  77. package/dist/git/installation-token.d.ts +38 -0
  78. package/dist/git/job-context.d.ts +13 -0
  79. package/dist/index.js +324 -84
  80. package/dist/metrics/agent-metrics-aggregator.d.ts +4 -4
  81. package/dist/metrics/prometheus.d.ts +170 -3
  82. package/dist/oidc/oidc-mint-registration.d.ts +7 -3
  83. package/dist/orchestrator-core.d.ts +91 -24
  84. package/dist/pipeline/content-filter.d.ts +71 -0
  85. package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
  86. package/dist/pipeline/dispatch-matched-workflow.d.ts +521 -11
  87. package/dist/pipeline/global-eval-round.d.ts +307 -0
  88. package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
  89. package/dist/pipeline/invoke-gate.d.ts +164 -0
  90. package/dist/pipeline/job-contexts.d.ts +16 -17
  91. package/dist/pipeline/needs-scheduler.d.ts +13 -0
  92. package/dist/pipeline/process-webhook.d.ts +126 -31
  93. package/dist/pipeline/processor.d.ts +282 -23
  94. package/dist/pipeline/rerun.d.ts +21 -0
  95. package/dist/pipeline/resume-workflow.d.ts +40 -7
  96. package/dist/pipeline/route-or-dispatch-jobs.d.ts +15 -0
  97. package/dist/pipeline/security-hold-check.d.ts +217 -0
  98. package/dist/pipeline/test-pipeline.d.ts +12 -0
  99. package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
  100. package/dist/pipeline/webhook-payload-store.d.ts +20 -0
  101. package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
  102. package/dist/policy/dashboard-write-policy.d.ts +14 -0
  103. package/dist/provenance/backfill-run.d.ts +10 -1
  104. package/dist/provider-registry.d.ts +47 -4
  105. package/dist/providers/github/check-status-poster.d.ts +33 -3
  106. package/dist/providers/github/commit-message.d.ts +20 -0
  107. package/dist/providers/github/file-contents.d.ts +40 -0
  108. package/dist/providers/github/index.d.ts +2 -2
  109. package/dist/providers/github/normalizer.d.ts +3 -2
  110. package/dist/providers/local/normalizer.d.ts +3 -5
  111. package/dist/providers/universal-git/config.d.ts +2 -0
  112. package/dist/providers/universal-git/index.d.ts +11 -2
  113. package/dist/providers/universal-git/normalizer.d.ts +10 -0
  114. package/dist/queue/cleanup.d.ts +7 -1
  115. package/dist/queue/job-queue.d.ts +112 -7
  116. package/dist/queue/terminalize-unroutable.d.ts +13 -0
  117. package/dist/registration/extractor.d.ts +3 -3
  118. package/dist/registration/registration-index.d.ts +7 -0
  119. package/dist/registration/registration-run-match.d.ts +47 -0
  120. package/dist/registration/registration-store.d.ts +22 -0
  121. package/dist/reporting/check-run-reporter.d.ts +146 -2
  122. package/dist/reporting/check-run-summary.d.ts +25 -1
  123. package/dist/reporting/execution-tracker.d.ts +324 -9
  124. package/dist/reporting/log-chunk-sink.d.ts +8 -5
  125. package/dist/reporting/run-aggregator.d.ts +4 -14
  126. package/dist/reporting/step-display-order.d.ts +43 -0
  127. package/dist/routes/admin-held-runs.d.ts +142 -0
  128. package/dist/routes/admin-org-settings.d.ts +5 -0
  129. package/dist/routes/admin-trust-policy.d.ts +34 -6
  130. package/dist/routes/admin.d.ts +15 -0
  131. package/dist/scaler/backend-factory.d.ts +55 -0
  132. package/dist/scaler/bare-metal-backend.d.ts +43 -5
  133. package/dist/scaler/claim-store.d.ts +119 -0
  134. package/dist/scaler/config.d.ts +4 -0
  135. package/dist/scaler/container-backend.d.ts +18 -4
  136. package/dist/scaler/container-routing.d.ts +23 -0
  137. package/dist/scaler/container-spawn.d.ts +28 -0
  138. package/dist/scaler/event-backend.d.ts +124 -0
  139. package/dist/scaler/event-provision-reaper.d.ts +350 -0
  140. package/dist/scaler/failure-tracker.d.ts +1 -1
  141. package/dist/scaler/firecracker-backend.d.ts +33 -6
  142. package/dist/scaler/index.d.ts +11 -1
  143. package/dist/scaler/label-matcher.d.ts +4 -3
  144. package/dist/scaler/manager.d.ts +905 -76
  145. package/dist/scaler/resolve-container-auth.d.ts +45 -0
  146. package/dist/scaler/scaler-events.d.ts +11 -0
  147. package/dist/scaler/scaler-state-store.d.ts +294 -10
  148. package/dist/scaler/types.d.ts +131 -13
  149. package/dist/scaler/warm-pool.d.ts +132 -29
  150. package/dist/security/comment-handler.d.ts +50 -11
  151. package/dist/security/global-workflow-policy.d.ts +52 -12
  152. package/dist/security/identity-link.d.ts +60 -0
  153. package/dist/security/lock-source.d.ts +11 -13
  154. package/dist/security/reduced-privilege-note.d.ts +59 -0
  155. package/dist/security/trust-directory-store.d.ts +233 -0
  156. package/dist/security/trust-policy-gate.d.ts +94 -59
  157. package/dist/security/trust-policy-store.d.ts +3 -0
  158. package/dist/security/trust-resolver.d.ts +14 -98
  159. package/dist/server.d.ts +11 -1
  160. package/dist/server.js +56329 -42234
  161. package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
  162. package/dist/stale-detector/stale-run-detector.d.ts +61 -3
  163. package/dist/standalone.js +37619 -23570
  164. package/dist/webhook/ingest-accept.d.ts +70 -0
  165. package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
  166. package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
  167. package/dist/worker/in-memory-job-queue.d.ts +1 -1
  168. package/dist/ws/agent-handler.d.ts +31 -4
  169. package/dist/ws/dashboard-context-handler.d.ts +28 -4
  170. package/dist/ws/dashboard-global-workflows-handler.d.ts +36 -9
  171. package/dist/ws/execution-status-frame.d.ts +32 -0
  172. package/dist/ws/failure-messages.d.ts +2 -0
  173. package/dist/ws/git-credential-relay.d.ts +39 -0
  174. package/dist/ws/oidc-token-relay.d.ts +6 -11
  175. package/dist/ws/platform-client.d.ts +68 -9
  176. package/dist/ws/test-relay-handlers.d.ts +35 -10
  177. package/installer-image-digests.json +3 -3
  178. package/package.json +24 -19
  179. package/sbom.spdx.json +868 -821
  180. package/dist/approvals/team-membership-lookup.d.ts +0 -13
  181. package/dist/pipeline/inline-eval.d.ts +0 -44
  182. package/dist/providers/github/contributor-resolver.d.ts +0 -30
  183. package/dist/security/contributor-cache.d.ts +0 -83
@@ -0,0 +1,24 @@
1
+ /**
2
+ * In-memory stand-in for the `scaler_pending_claims` half of `ScalerStateStore`.
3
+ *
4
+ * It reproduces the three properties the real table enforces in SQL — a claim
5
+ * is consumable exactly once, a consume past `expires_at` finds nothing, and a
6
+ * per-agent invalidation removes every row for that agent — against an injected
7
+ * clock. That lets a test drive the whole register → redeem round trip without a
8
+ * database, so the single-use and TTL properties are executed rather than
9
+ * hand-fed through a stubbed answer.
10
+ *
11
+ * Only the pending-claim methods are implemented. Reaching any other
12
+ * `ScalerStateStore` method is a `TypeError` at the call site, not a silent
13
+ * no-op.
14
+ */
15
+ import type { ScalerStateStore } from '../scaler/scaler-state-store.js';
16
+ /**
17
+ * Build an in-memory pending-claim store.
18
+ *
19
+ * @param now Clock (epoch ms) the TTL is evaluated against. Pass the same clock
20
+ * the `ClaimStore` under test uses, or its `expiresAt` lands in a different
21
+ * era than this store's `now()` and every redeem reads as expired.
22
+ */
23
+ export declare function makeFakeScalerStateStore(now?: () => number): ScalerStateStore;
24
+ //# sourceMappingURL=fake-scaler-state-store.d.ts.map
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Predicate + projection evaluation for the shared `createMockDb` harness.
3
+ *
4
+ * Kept BYTE-IDENTICAL in two places —
5
+ * `packages/orchestrator/src/__test-helpers__/mock-db-query.ts` and
6
+ * `packages/platform/src/__test-helpers__/mock-db-query.ts`. Each package owns an
7
+ * independent `createMockDb` builder and neither package may import the other, so
8
+ * the evaluator is duplicated rather than shared. `mock-db-mirror.test.ts` in both
9
+ * packages fails the moment the copies diverge.
10
+ *
11
+ * `createMockDb` used to return its configured rows verbatim, whatever the
12
+ * query asked for: a `.where(...)` or a `.select([...])` was recorded on a spy
13
+ * and then discarded. A test therefore observed only the rows it had itself
14
+ * configured, so dropping a filter or a projected column from the code under
15
+ * test changed nothing the test could see. This module is the evaluator that
16
+ * closes that: the mock now *applies* the query it was handed.
17
+ *
18
+ * Scope, stated precisely because the guarantee is partial by construction:
19
+ *
20
+ * - **Binary predicates are evaluated.** `.where(column, op, value)` for the
21
+ * operators in {@link MockDbOperator}. That is the overwhelming majority of
22
+ * the predicates this repo issues.
23
+ * - **Anything else is opaque and is NOT evaluated.** An expression-builder
24
+ * callback (`.where((eb) => …)`) and a raw `sql` fragment carry their meaning
25
+ * in code the mock cannot read, so they are recorded and skipped. A filter
26
+ * written in one of those shapes is still unpinned.
27
+ * - **A predicate on a column the fixture row does not declare is NOT
28
+ * evaluated.** The fixture, not the schema, is the source of truth here: a
29
+ * test row is a partial literal, and a missing key means "this test did not
30
+ * model that column", not "this column is NULL". Treating absence as NULL
31
+ * would fail nearly every fixture in the repo for saying too little, while
32
+ * catching nothing a fixture that *does* declare the column would not
33
+ * already catch. The consequence is real and worth naming: to pin a filter,
34
+ * a fixture row must mention the column that filter reads.
35
+ *
36
+ * Everything here is pure — no vitest, no chain state — so it is unit-tested
37
+ * directly in `mock-db-query.test.ts` rather than only through the mock.
38
+ */
39
+ /** SQL comparison operators the mock evaluates. Anything else is opaque. */
40
+ export declare enum MockDbOperator {
41
+ eq = "=",
42
+ neq = "!=",
43
+ neqAnsi = "<>",
44
+ lt = "<",
45
+ lte = "<=",
46
+ gt = ">",
47
+ gte = ">=",
48
+ in = "in",
49
+ notIn = "not in",
50
+ is = "is",
51
+ isNot = "is not",
52
+ like = "like",
53
+ notLike = "not like",
54
+ ilike = "ilike",
55
+ notILike = "not ilike"
56
+ }
57
+ /** A `.where(column, op, value)` predicate the mock can evaluate. */
58
+ export interface BinaryPredicate {
59
+ kind: 'binary';
60
+ /** Column name with any `table.` qualifier stripped. */
61
+ column: string;
62
+ op: MockDbOperator;
63
+ value: unknown;
64
+ }
65
+ /** A predicate shape the mock cannot read (eb callback, raw sql, unknown op). */
66
+ export interface OpaquePredicate {
67
+ kind: 'opaque';
68
+ /** Why it could not be parsed — surfaced in `explainSkippedPredicates`. */
69
+ reason: string;
70
+ }
71
+ export type MockDbPredicate = BinaryPredicate | OpaquePredicate;
72
+ /** Drop a `table.` / `"table".` qualifier so a predicate keys into a flat row. */
73
+ export declare function unqualify(column: string): string;
74
+ /**
75
+ * Parse the arguments of one `.where(...)` call into a predicate.
76
+ *
77
+ * Only the 3-argument `(column, op, value)` form with a string column and a
78
+ * recognized operator is evaluable; everything else is opaque.
79
+ */
80
+ export declare function parseWhereArgs(args: readonly unknown[]): MockDbPredicate;
81
+ /**
82
+ * Evaluate one binary predicate against a row.
83
+ *
84
+ * Returns `undefined` when the predicate cannot be decided — the row does not
85
+ * declare the column, or the values are not comparable. An undecided predicate
86
+ * does not exclude the row (see the module docblock).
87
+ */
88
+ export declare function evaluatePredicate(row: Record<string, unknown>, predicate: BinaryPredicate): boolean | undefined;
89
+ /** True when the row satisfies every predicate the mock could decide. */
90
+ export declare function rowMatches(row: unknown, predicates: readonly MockDbPredicate[]): boolean;
91
+ /** Keep only the rows satisfying every decidable predicate. */
92
+ export declare function filterRows<T>(rows: readonly T[], predicates: readonly MockDbPredicate[]): T[];
93
+ /** One projected column: the row key read, and the key it is emitted under. */
94
+ export interface ProjectedColumn {
95
+ source: string;
96
+ alias: string;
97
+ }
98
+ /**
99
+ * Parse `.select(...)` arguments into a projection.
100
+ *
101
+ * Returns `undefined` when the projection cannot be modelled — an aggregate
102
+ * expression, an `eb` callback, a raw `sql` fragment. An unmodelled projection
103
+ * leaves rows untouched rather than guessing at their shape.
104
+ */
105
+ export declare function parseSelectArgs(args: readonly unknown[]): ProjectedColumn[] | undefined;
106
+ /**
107
+ * Narrow a row to the projected columns.
108
+ *
109
+ * A projected column the fixture row does not declare is omitted rather than
110
+ * emitted as `undefined`, so `'key' in row` keeps meaning "the fixture modelled
111
+ * this" — the same convention {@link evaluatePredicate} reads.
112
+ *
113
+ * A column is read under its `source` name and, failing that, under its
114
+ * `alias`. Fixtures in this repo are written both ways — some model the raw
115
+ * table row (`storage_key`), others the row the aliased query returns
116
+ * (`storageKey`) — and the projection has to narrow both. Reading either name
117
+ * costs nothing: a column dropped from the projection disappears from the
118
+ * output under both conventions, which is the regression this exists to catch.
119
+ */
120
+ export declare function projectRow<T>(row: T, columns: readonly ProjectedColumn[]): T;
121
+ /** Narrow every row to the projected columns. */
122
+ export declare function projectRows<T>(rows: readonly T[], columns: readonly ProjectedColumn[]): T[];
123
+ /**
124
+ * Describe the predicates the mock recorded but could not evaluate.
125
+ *
126
+ * Exposed so a test that cares whether its filter was actually pinned can
127
+ * assert the mock understood it, instead of assuming it did.
128
+ */
129
+ export declare function explainSkippedPredicates(predicates: readonly MockDbPredicate[]): string[];
130
+ //# sourceMappingURL=mock-db-query.d.ts.map
@@ -15,6 +15,29 @@
15
15
  *
16
16
  * Each test can configure return values via options and access the
17
17
  * underlying vi.fn() mocks for assertions via the `mocks` property.
18
+ *
19
+ * ## The select chain APPLIES the query (contract)
20
+ *
21
+ * A select chain does not return `selectRows` / `selectFirstRow` verbatim. It
22
+ * evaluates the `.where(...)` predicates and the `.select([...])` projection the
23
+ * code under test issued, so a test observes the query instead of restating it:
24
+ * dropping a filter or a projected column changes what the test sees.
25
+ *
26
+ * - **Per query, not per test.** Every `db.selectFrom(...)` builds a fresh chain
27
+ * with its own predicate list, so two queries in one test cannot inherit each
28
+ * other's filters. The `mocks.select*` spies stay shared across the whole
29
+ * test, so existing clause assertions are unaffected.
30
+ * - **Only the shapes the evaluator reads.** `.where(column, op, value)` is
31
+ * evaluated; an `eb` callback or a raw `sql` fragment is recorded and skipped.
32
+ * `.select()` is applied only when every argument is a plain column string, so
33
+ * an aggregate or `eb` projection leaves rows untouched.
34
+ * - **A predicate on a column the fixture omits does not exclude the row.** To
35
+ * pin a filter, the fixture row must declare the column that filter reads.
36
+ * - **`executeTakeFirstOrThrow` yields `{}`** when the configured row does not
37
+ * satisfy the query, rather than throwing as Kysely would.
38
+ *
39
+ * `mock-db-query.ts` holds the evaluator and states the same contract with its
40
+ * reasoning; `mock-db-query.test.ts` tests it directly.
18
41
  */
19
42
  import { vi } from 'vitest';
20
43
  export interface MockDbOptions {
@@ -72,6 +95,10 @@ export interface MockDbMocks {
72
95
  insertExecute: ReturnType<typeof vi.fn>;
73
96
  insertExecuteTakeFirstOrThrow: ReturnType<typeof vi.fn>;
74
97
  onConflict: ReturnType<typeof vi.fn>;
98
+ /** The upsert's ON CONFLICT DO UPDATE SET payload — shared across both the
99
+ * column() and columns() shapes, so a test can assert what an existing row
100
+ * is actually updated with. */
101
+ doUpdateSet: ReturnType<typeof vi.fn>;
75
102
  updateTable: ReturnType<typeof vi.fn>;
76
103
  updateSet: ReturnType<typeof vi.fn>;
77
104
  updateWhere: ReturnType<typeof vi.fn>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Comparing a self-reported agent version against a feature's minimum.
3
+ *
4
+ * An agent reports its own package version in `agent.register`
5
+ * (`agentRegisterSchema.version`), and the field is optional — an agent old
6
+ * enough to omit it predates every feature this module gates on. The
7
+ * orchestrator and the agent are deployed and upgraded independently
8
+ * (`.claude/rules/compatibility.md`), so a feature the orchestrator ships is
9
+ * routinely dispatched to a fleet that cannot run it, and the version is the
10
+ * only fact the orchestrator has about what the fleet understands.
11
+ */
12
+ /** A version's release triple, with any prerelease suffix dropped. */
13
+ type VersionBase = [number, number, number];
14
+ /**
15
+ * Parse `MAJOR.MINOR.PATCH`, ignoring any `-prerelease` suffix, or `null` when
16
+ * the string is not a version at all.
17
+ *
18
+ * The prerelease suffix is dropped rather than ordered because the suffixes in
19
+ * play are build counters from the dev registry (`0.5.0-9159`), not semver
20
+ * release candidates. Strict semver orders those BELOW `0.5.0`, which would
21
+ * read every staging agent as too old for a feature it in fact carries.
22
+ */
23
+ export declare function parseVersionBase(version: string): VersionBase | null;
24
+ /**
25
+ * True when `version` is at least `minimum`, comparing release bases only.
26
+ *
27
+ * An absent or unparseable version returns `false`: the orchestrator learns
28
+ * nothing from it, and a feature that needs a specific agent build must not
29
+ * treat "we cannot tell" as "yes". Callers decide what an all-unknown fleet
30
+ * means for them — this function only reports what the version proves.
31
+ */
32
+ export declare function agentVersionAtLeast(version: string | null | undefined, minimum: string): boolean;
33
+ export {};
34
+ //# sourceMappingURL=agent-version.d.ts.map
@@ -1,7 +1,28 @@
1
1
  import type { AgentRegistry } from './registry.js';
2
2
  import { type JobQueue, type QueuedJob, type QueuedJobInput } from '../queue/job-queue.js';
3
3
  import type { ScaleResult, ScalerRedispatchTrigger } from '../scaler/types.js';
4
+ import type { ResolvedContainerSpawn } from '../scaler/types.js';
4
5
  import type { ResourceRequest } from '@kici-dev/engine';
6
+ /**
7
+ * Why a freed agent's queue drain claimed nothing.
8
+ *
9
+ * Named rather than inlined because the value travels: it is the `reason`
10
+ * field of the drain-declined log line and the assertion subject of the tests
11
+ * that pin each exit. The five members are the five ways
12
+ * `Dispatcher.drainForAgent` can return without a dispatch.
13
+ */
14
+ export declare enum AgentDrainDecline {
15
+ /** The agent id is not in the in-memory registry (already disconnected). */
16
+ NotRegistered = "agent-not-registered",
17
+ /** This coordinator is draining and must not claim new work. */
18
+ CoordinatorDraining = "coordinator-draining",
19
+ /** The agent is already at `maxConcurrency`. */
20
+ NoCapacity = "no-capacity",
21
+ /** The host is flagged reboot-pending, so anything dispatched would be lost. */
22
+ RebootPending = "reboot-pending",
23
+ /** Nothing in the queue matched this agent's labels or pin. */
24
+ NoMatchingJob = "no-matching-job"
25
+ }
5
26
  /**
6
27
  * Default per-pass cap on how many pending jobs a capacity-freed re-drive
7
28
  * re-offers to the scaler. Bounds the burst so a single free event cannot storm
@@ -60,24 +81,24 @@ type DispatchResult = {
60
81
  status: 'rejected';
61
82
  reason: string;
62
83
  };
63
- /**
64
- * Dispatch coordinator connecting the agent registry and job queue.
65
- *
66
- * Routes incoming jobs to matching agents based on label requirements,
67
- * falling back to the persistent queue when no agent is available.
68
- * Handles agent lifecycle events (connect, disconnect, job completion).
69
- *
70
- * The dispatcher does NOT directly access WebSocket connections --
71
- * it uses the onDispatch callback provided at construction. The caller
72
- * (app.ts or server.ts) provides an onDispatch that looks up the agent's
73
- * WS from the registry and sends the job.dispatch message.
74
- */
84
+ export declare function containerSpawnFor(jobConfig: Record<string, unknown> | undefined): {
85
+ image: string;
86
+ authconfig?: {
87
+ username: string;
88
+ password: string;
89
+ serveraddress: string;
90
+ } | undefined;
91
+ } | undefined;
75
92
  export declare class Dispatcher {
76
93
  private readonly registry;
77
94
  private readonly queue;
78
95
  private readonly metrics;
79
96
  private readonly onDispatch;
80
97
  private readonly onNoMatchingAgent?;
98
+ /** See the constructor dep of the same name. */
99
+ private readonly canPrespawnedAgentServe?;
100
+ /** See the constructor dep of the same name. */
101
+ private readonly isPrespawnedAgent?;
81
102
  /**
82
103
  * Single-flight guard for `retryPendingScaleRequests`. The capacity-freed
83
104
  * hook and the leader-gated sweep can both fire concurrently; this prevents
@@ -85,6 +106,12 @@ export declare class Dispatcher {
85
106
  * the same tick.
86
107
  */
87
108
  private redriveInFlight;
109
+ /**
110
+ * Single-flight guard for `redrivePendingToConnectedAgents`. The per-coord
111
+ * safety-net tick must not overlap itself, so a slow re-drive can't be
112
+ * re-entered by the next interval fire and double-scan the same pending rows.
113
+ */
114
+ private pendingRedriveInFlight;
88
115
  /**
89
116
  * Tracks which jobs are dispatched to which agents.
90
117
  * Used for marking jobs as failed on agent disconnect.
@@ -170,7 +197,34 @@ export declare class Dispatcher {
170
197
  /** Optional hook called when no agent matches the job's labels.
171
198
  * When set, receives the per-job `resources` so the scaler can apply
172
199
  * per-scaler / per-orchestrator / per-machine caps before spawning. */
173
- onNoMatchingAgent?: (labels: string[], jobId: string, runId: string, excludeLabels: string[], resources?: ResourceRequest, orgId?: string) => Promise<ScaleResult>;
200
+ onNoMatchingAgent?: (labels: string[], jobId: string, runId: string, excludeLabels: string[], resources?: ResourceRequest, orgId?: string,
201
+ /**
202
+ * The job's own container image plus registry credentials, already
203
+ * resolved at dispatch. Present means the backend spawns THAT image with
204
+ * the KiCI runtime injected instead of the pool's fixed agent image.
205
+ */
206
+ containerSpawn?: ResolvedContainerSpawn) => Promise<ScaleResult>;
207
+ /**
208
+ * Optional scaler predicate: may this pre-spawned (warm) agent serve this
209
+ * job? A warm agent is generic — started before the job existed, at the
210
+ * pool's shape and running the pool's image, both of which are fixed when
211
+ * it starts. When the predicate returns false the agent is skipped and the
212
+ * job falls through to `onNoMatchingAgent`, which spawns one that fits.
213
+ * Absent (no scaler) means every agent is eligible.
214
+ */
215
+ canPrespawnedAgentServe?: (agentId: string, job: {
216
+ resources?: ResourceRequest;
217
+ hasOwnContainerImage: boolean;
218
+ }) => boolean;
219
+ /**
220
+ * Whether this scaler pre-spawned the agent, i.e. whether
221
+ * `canPrespawnedAgentServe` can ever answer false for it. The queue drain
222
+ * (agent asks for work, rather than job looks for an agent) uses it to
223
+ * decide whether the suitability predicate is worth carrying into the
224
+ * claim: for every ordinary agent it is not, and the drain keeps its
225
+ * single-statement fast path.
226
+ */
227
+ isPrespawnedAgent?: (agentId: string) => boolean;
174
228
  /** Max reconnection delay from agent config (default 60s). Used to derive grace period. */
175
229
  maxReconnectDelayMs?: number;
176
230
  /** Callback fired when a job is permanently failed before/outside agent execution. */
@@ -259,6 +313,39 @@ export declare class Dispatcher {
259
313
  * that is about to reboot. No-op when no roster store is wired.
260
314
  */
261
315
  private filterRebootPending;
316
+ /**
317
+ * Drop pre-spawned (warm) agents that cannot serve this job. Sibling of
318
+ * {@link filterRebootPending}: both remove candidates `findAvailable` matched
319
+ * on labels but that are unusable for a reason labels cannot express.
320
+ *
321
+ * A warm agent's cpu, memory and container image are all set when it starts
322
+ * and cannot change afterwards, so a job asking for something else has to get
323
+ * an agent of its own. Dropping the candidate here is what sends it down the
324
+ * ordinary `onNoMatchingAgent` scale path. No-op when no scaler is wired.
325
+ */
326
+ private filterUnsuitablePrespawned;
327
+ /**
328
+ * The fit question for one job: the shape it declares, and whether it brings
329
+ * its own container image.
330
+ *
331
+ * The shape comes from `jobConfig.resources` when the typed `resources`
332
+ * mirror is absent, because the mirror is optional and several dispatch paths
333
+ * never fill it — a job reaching {@link dispatch} straight off the webhook
334
+ * pipeline or off a worker's reroute handler carries its declaration in
335
+ * `jobConfig` alone. Reading only the mirror there reports a job that
336
+ * declares nothing, which admits it onto a pre-spawned agent of some other
337
+ * size — the exact mismatch this gate exists to refuse.
338
+ */
339
+ private prespawnedFitFor;
340
+ /**
341
+ * The queue-drain half of {@link filterUnsuitablePrespawned}: a per-job
342
+ * predicate for one agent, or undefined when this agent needs no check.
343
+ *
344
+ * Undefined is the common answer — an ordinary agent is not pre-spawned, so
345
+ * the predicate could only ever say yes, and returning one would cost the
346
+ * drain its single-statement fast path for nothing.
347
+ */
348
+ private prespawnedFitFilterFor;
262
349
  /**
263
350
  * Release a reboot-pending host on its real reconnect (down-then-up). Clears
264
351
  * the persisted flag so the very next `onAgentAvailable` drain dispatches the
@@ -268,6 +355,12 @@ export declare class Dispatcher {
268
355
  */
269
356
  releaseRebootPending(agentId: string): Promise<void>;
270
357
  onAgentAvailable(agentId: string): Promise<void>;
358
+ /**
359
+ * Claim at most one queued job for a freed agent.
360
+ *
361
+ * Returns the reason nothing was dispatched, or null when a job was.
362
+ */
363
+ private drainForAgent;
271
364
  /** Record that a job began executing on its agent. */
272
365
  markJobStarted(jobId: string): void;
273
366
  /**
@@ -316,6 +409,51 @@ export declare class Dispatcher {
316
409
  * fresh agent is spawned bound to this job.
317
410
  */
318
411
  private redispatch;
412
+ /**
413
+ * Safety-net re-drive: deliver every pending job that a currently-connected,
414
+ * idle, matching agent could take, through the same atomic claim the drain
415
+ * uses (`dispatchBoundJob` → `dequeueById`).
416
+ *
417
+ * Runs per-coordinator and is NOT leader-gated: each coordinator drains the
418
+ * shared queue onto its OWN connected agents, and the atomic claim guarantees
419
+ * at most one agent (on any coordinator) wins each job, so concurrent ticks
420
+ * cannot double-dispatch.
421
+ *
422
+ * It closes the requeue re-drive gap. A job requeued by `handleAckExpiry` /
423
+ * `onJobRejected` / the leader ack sweep gets exactly one delivery attempt —
424
+ * `redispatch`'s single `findAvailable` + `dispatchBoundJob`. An idle matching
425
+ * agent whose own drain trigger (registration / completion / status) already
426
+ * fired before the requeue has no further trigger, so if that one attempt
427
+ * transiently misses the agent — the agent is momentarily at capacity while an
428
+ * in-flight drain holds its eagerly-claimed slot, or the agent is connected to
429
+ * a different coordinator than the one that ran the expiry — the requeued
430
+ * pending job would otherwise sit undelivered until it expired. This tick
431
+ * re-attempts delivery onto connected agents so the miss recovers on the next
432
+ * sweep.
433
+ *
434
+ * It never consults the scaler (that is `retryPendingScaleRequests`) and never
435
+ * spawns: it only places jobs onto agents already connected here.
436
+ *
437
+ * A failure propagates to the caller rather than being swallowed here — the
438
+ * per-coord interval wrapper is the single error-log site, matching the
439
+ * sibling recovery/ack sweeps that share its cadence. The `finally` only
440
+ * releases the single-flight guard.
441
+ */
442
+ redrivePendingToConnectedAgents(maxJobs?: number): Promise<number>;
443
+ /**
444
+ * Pick a connected agent that may take a pending job, or null. A pinned
445
+ * host-fanout child may run ONLY on its pinned agent — and `dequeueById`
446
+ * ignores the pin — so a pinned job is routed to its own agent and never
447
+ * offered to `findAvailable`, which would mis-deliver it to any label match.
448
+ *
449
+ * Reboot-pending gate: a host whose `restart` job just completed is still
450
+ * connected but about to reboot, so its held post-restart job must NOT be
451
+ * re-driven into the about-to-die box. This is the safety-net re-drive's
452
+ * counterpart of the same gate in `dispatch()` / `dispatchPinned` /
453
+ * `drainForAgent`; without it this path re-drives the held job onto the
454
+ * reboot-pending host and defeats the hold.
455
+ */
456
+ private selectConnectedTargetForPending;
319
457
  /**
320
458
  * Handle agent disconnect. Starts per-job recovery timers instead of
321
459
  * immediately failing jobs, giving the agent a grace period to reconnect.
@@ -77,6 +77,13 @@ export interface UpsertHostInput {
77
77
  agentId: string;
78
78
  tokenId: string | null;
79
79
  lifecycleClass: LifecycleClass;
80
+ /**
81
+ * True when an auto-scaler backend spawned this agent, so `runsOnAll`
82
+ * fan-out skips it. Derived from the scaler manager's registration lookup,
83
+ * never from `lifecycleClass` (which is `ephemeral` for every agent when the
84
+ * auth mode is `none`). Omitted ⇒ false.
85
+ */
86
+ scalerManaged?: boolean;
80
87
  labels: string[];
81
88
  hostname: string | null;
82
89
  platform: string;
@@ -176,13 +183,38 @@ export declare class HostRosterStore {
176
183
  getStagedVersion(agentId: string): Promise<string | null>;
177
184
  listAll(): Promise<HostRosterRow[]>;
178
185
  /**
179
- * Resolve every roster host matching a `runsOnAll` predicate (OR-of-AND
180
- * include groups, minus exclude labels), tagged with its derived status. This
181
- * is the host-fanout resolver: it returns declared-but-absent static hosts
182
- * (status `unreachable`) so the caller can apply `onUnreachable` the live
183
- * registry alone cannot name an expected-but-absent host.
186
+ * Resolve EVERY roster host matching a label predicate (OR-of-AND include
187
+ * groups, minus exclude labels), tagged with its derived status. Returns
188
+ * declared-but-absent static hosts (status `unreachable`) so a caller can
189
+ * apply `onUnreachable` the live registry alone cannot name an
190
+ * expected-but-absent host.
191
+ *
192
+ * This is the INVENTORY query: it includes auto-scaler-spawned hosts, whose
193
+ * `lifecycleClass` is what the SDK's `ctx.kici.inventory` exposes to tell
194
+ * them apart. `runsOnAll` fan-out uses {@link findFanoutTargets} instead.
184
195
  */
185
196
  findMatching(include: readonly (readonly LabelMatcher[])[], exclude: readonly LabelMatcher[], graceMs: number): Promise<MatchedHost[]>;
197
+ /**
198
+ * Resolve the `runsOnAll` fan-out target set: {@link findMatching}, minus
199
+ * every auto-scaler-spawned host.
200
+ *
201
+ * Fan-out targets DECLARED fleet members. An auto-scaler agent is spawned to
202
+ * a pool's fixed shape, so a child pinned to one runs at that shape rather
203
+ * than its own — the same defect the dispatcher's warm-pool suitability gate
204
+ * removes, on the path a pin deliberately leaves ungated.
205
+ *
206
+ * Separate from `findMatching` on purpose: `findMatching` also backs the
207
+ * SDK's `inventory.query`, where a host the caller asked for must never go
208
+ * missing (a selector returning FEWER hosts than no selector at all).
209
+ */
210
+ findFanoutTargets(include: readonly (readonly LabelMatcher[])[], exclude: readonly LabelMatcher[], graceMs: number): Promise<MatchedHost[]>;
211
+ /**
212
+ * Shared row loop behind {@link findMatching} and {@link findFanoutTargets}.
213
+ * `excludeScalerManaged` stays private to the store — each public method
214
+ * names its own semantics, so a call site cannot silently pick the wrong
215
+ * population by passing a bare boolean.
216
+ */
217
+ private matchRows;
186
218
  /**
187
219
  * Map a roster row to the canonical {@link HostInventoryEntry} — the queryable
188
220
  * shape returned by the `inventory.query`/`inventory.get` RPC and typed on the
@@ -137,6 +137,8 @@ export interface RosterReconciler {
137
137
  agentId: string;
138
138
  tokenId: string | null;
139
139
  lifecycleClass: 'static' | 'ephemeral';
140
+ /** True when a scaler backend spawned this agent (kept out of `runsOnAll`). */
141
+ scalerManaged?: boolean;
140
142
  labels: string[];
141
143
  hostname: string | null;
142
144
  platform: string;
package/dist/app.d.ts CHANGED
@@ -26,6 +26,7 @@ import type { PeerRegistry } from './cluster/peer-registry.js';
26
26
  import type { JobQueue } from './queue/job-queue.js';
27
27
  import type { DedupCache } from './webhook/dedup.js';
28
28
  import type { LockFileCache } from './lockfile-cache.js';
29
+ import type { ContentRequirementsCache } from './content-requirements-cache.js';
29
30
  import type { PlatformClient } from './ws/platform-client.js';
30
31
  import type { ScalerManager } from './scaler/manager.js';
31
32
  import type { ProviderRegistry } from './provider-registry.js';
@@ -37,7 +38,8 @@ import type { ArtifactStore } from './artifacts/artifact-store.js';
37
38
  import type { DispatchCacheRefTracker } from './cache/index.js';
38
39
  import type { PendingBuildTracker } from './cache/index.js';
39
40
  import type { PendingInitTracker } from './cache/index.js';
40
- import type { PendingDynamicTracker } from './cache/index.js';
41
+ import type { PendingDynamicTracker, PendingGlobalEvalTracker } from './cache/index.js';
42
+ import type { GlobalEvalRoundCache } from './cache/index.js';
41
43
  import type { CacheStorage } from './storage/types.js';
42
44
  import type { ProvenanceTrustRoot } from './provenance/trust-root.js';
43
45
  import type { LocalSigner } from './oidc/local-dev-signer.js';
@@ -64,8 +66,11 @@ import type { RegistrationStore } from './registration/registration-store.js';
64
66
  import type { RegistrationIndex } from './registration/registration-index.js';
65
67
  import type { CronScheduler } from './cron/cron-scheduler.js';
66
68
  import { type ConfigRouteDeps } from './routes/admin-config.js';
69
+ import type { ProcessingDeps } from './pipeline/processor.js';
70
+ import type { InvokeGateDeps } from './pipeline/invoke-gate.js';
67
71
  import { WebhookIngestOutcome } from './pipeline/process-webhook.js';
68
72
  import type { IngestOverflowBuffer } from './webhook/ingest-overflow-buffer.js';
73
+ import type { IngestOverflowReplayer } from './webhook/ingest-overflow-replayer.js';
69
74
  import { type OverflowDelivery } from './webhook/ingest-overflow-types.js';
70
75
  import type { IngestAdmissionController } from './webhook/ingest-admission.js';
71
76
  import type { OrgIngestCapReader } from './webhook/org-ingest-cap-reader.js';
@@ -82,9 +87,8 @@ import type { GenericSourceManager } from './webhook/generic-sources.js';
82
87
  import type { TrustStore } from './events/trust-store.js';
83
88
  import type { ContextStore } from './contexts/context-store.js';
84
89
  import type { VariableStore } from './contexts/variable-store.js';
85
- import type { HeldRunStore } from './contexts/held-runs.js';
90
+ import type { HeldRunStore, ReleaseSignal } from './contexts/held-runs.js';
86
91
  import type { StepApprovalBridge } from './approvals/step-approval-bridge.js';
87
- import type { ContributorCache } from './security/contributor-cache.js';
88
92
  import { AgentMetricsAggregator } from './metrics/agent-metrics-aggregator.js';
89
93
  /**
90
94
  * All dependencies needed to create the orchestrator Hono app.
@@ -108,6 +112,7 @@ export interface AppDependencies {
108
112
  jobQueue: JobQueue;
109
113
  dedup: DedupCache;
110
114
  lockFileCache: LockFileCache;
115
+ contentRequirementsCache: ContentRequirementsCache;
111
116
  providerRegistry: ProviderRegistry;
112
117
  platformClient?: PlatformClient;
113
118
  /**
@@ -126,6 +131,13 @@ export interface AppDependencies {
126
131
  * for later replay; absent → capture disabled.
127
132
  */
128
133
  ingestOverflowBuffer?: IngestOverflowBuffer;
134
+ /**
135
+ * Drain loop for the durable ingest queue. The accept path borrows its
136
+ * claim-release so a failed pipeline hands the row back for retry through the
137
+ * same attempt ceiling the drain uses; absent → the accept path degrades to
138
+ * inline ingestion.
139
+ */
140
+ ingestOverflowReplayer?: IngestOverflowReplayer;
129
141
  /**
130
142
  * Fulfil deferred attestations on demand (mints in this process, which owns
131
143
  * the Platform WS). Backs `POST /api/v1/admin/attestations/retry`. Wired only
@@ -168,6 +180,14 @@ export interface AppDependencies {
168
180
  * is registered so `ctx.kici.oidc.token()` mints a `kici-local` dev token.
169
181
  */
170
182
  localOidcSigner?: LocalSigner;
183
+ /**
184
+ * Test-only fault-injection policy, threaded from a test-only entrypoint via
185
+ * `bootstrapOrchestrator`. Undefined in production. Consumed here only for the
186
+ * initial-mint provenance seam (`initialMintFault`); the other seams live in
187
+ * the composition root / server hooks. Type-only reference — the runtime
188
+ * module never enters the shipped bundle.
189
+ */
190
+ faultInjection?: import('./testing/fault-injection.js').OrchestratorFaultInjection;
171
191
  /**
172
192
  * Orchestrator-owned provenance signing (Phase 1 root of trust). Present when
173
193
  * `KICI_ORCHESTRATOR_PROVENANCE_ISSUER` is configured: the orchestrator mints +
@@ -208,6 +228,8 @@ export interface AppDependencies {
208
228
  pendingInits?: PendingInitTracker;
209
229
  /** Pending dynamic tracker for DynamicJobFn evaluation coordination. */
210
230
  pendingDynamics?: PendingDynamicTracker;
231
+ pendingGlobalEvals?: PendingGlobalEvalTracker;
232
+ globalEvalCache?: GlobalEvalRoundCache;
211
233
  /** Commit status reporter for setting pending/success/failure/error on commits. Optional. */
212
234
  checkRunReporter?: CheckRunReporter;
213
235
  /** Execution tracker for DB persistence of execution state. Optional — requires database. */
@@ -244,12 +266,33 @@ export interface AppDependencies {
244
266
  configRouteDeps?: ConfigRouteDeps;
245
267
  /** Event router for internal event delivery. Optional -- if not set, event routing is inactive. */
246
268
  eventRouter?: EventRouter;
269
+ /** Invoke-gate dependencies (summon callback + chain-depth bound). Optional -- if not set, invoke gates fail loudly. */
270
+ invokeGateDeps?: InvokeGateDeps;
271
+ /**
272
+ * Late-binding handle the internal-event dispatch adapter reads to obtain the
273
+ * live `ProcessingDeps` bag. `createApp` populates it, because the bag can
274
+ * only be assembled here while the event router that dispatches internal
275
+ * events is constructed earlier in the bootstrap. Left unset by callers that
276
+ * never route internal events (tests constructing a bare app).
277
+ */
278
+ processingDepsRef?: {
279
+ current: (() => ProcessingDeps) | null;
280
+ };
247
281
  /** Event store. Optional -- mounted when admin DLQ admin route should be available. */
248
282
  eventStore?: EventStore;
249
283
  /** Event emitter for system events (workflow/job complete). Optional -- if not set, system events are skipped. */
250
284
  eventEmitter?: EventEmitter;
251
285
  /** Generic webhook source manager. Optional -- if not set, generic webhooks are disabled. */
252
286
  genericSourceManager?: GenericSourceManager;
287
+ /**
288
+ * Re-register a generic source's provider bundle from its database row,
289
+ * resolving to true when a per-routing-key bundle is now registered.
290
+ *
291
+ * Built in orchestrator-core so it shares the exact dependency bag the
292
+ * startup enumeration and the LISTEN/NOTIFY drain already register with.
293
+ * Optional -- wirings without one keep the previous behaviour.
294
+ */
295
+ ensureProviderBundle?: (routingKey: string) => Promise<boolean>;
253
296
  /** GitHub direct-ingress route deps (hybrid/independent only). Optional -- if not set, the direct GitHub ingress route is not mounted. */
254
297
  githubSourceStore?: SourceStore;
255
298
  githubVerifyDeps?: VerifyInboundDeps;
@@ -275,6 +318,16 @@ export interface AppDependencies {
275
318
  heldRunStore?: HeldRunStore;
276
319
  /** Step-approval bridge — opens step-scoped holds and relays their resolution back to the waiting agent. Optional. */
277
320
  stepApprovalBridge?: StepApprovalBridge;
321
+ /**
322
+ * Re-dispatch a job whose hold was released. Supplied by both mode hooks
323
+ * through `appDepsExtras`; declared here so the admin held-run routes can
324
+ * read it type-safely rather than off an untyped spread. Optional — an app
325
+ * assembled without it mounts no local held-run decision surface, because a
326
+ * release that cannot dispatch is a row flip pretending to be a release.
327
+ */
328
+ onJobRelease?: (signal: ReleaseSignal) => Promise<void>;
329
+ /** Replay the stored dispatch context of a released workflow-scoped hold. Same provenance as `onJobRelease`. */
330
+ onWorkflowRelease?: (signal: ReleaseSignal) => Promise<void>;
278
331
  /** Global workflow policy for org-level permission enforcement. Optional -- if not set, global workflows are dispatched without permission checks. */
279
332
  globalWorkflowPolicy?: GlobalWorkflowPolicy;
280
333
  /** Inbound webhook delivery log writer. Optional -- if not set, deliveries are not persisted to event_log. */
@@ -308,10 +361,6 @@ export interface AppDependencies {
308
361
  coordinator?: RunCoordinator;
309
362
  /** Peer registry for aggregating infrastructure across cluster. Optional. */
310
363
  peerRegistry?: PeerRegistry;
311
- /** Contributor permission cache. Optional -- threaded through from server.ts
312
- * so membership-related webhooks invalidate matching entries immediately
313
- * (instead of waiting for the 15-minute TTL). */
314
- contributorCache?: ContributorCache;
315
364
  /**
316
365
  * Shared aggregator for agent-pushed metrics. When omitted, app.ts
317
366
  * constructs its own — kept optional so existing tests still work