@kici-dev/orchestrator 0.5.0 → 0.6.1

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 (149) 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 +23 -0
  4. package/dist/agent/dispatcher.d.ts +81 -13
  5. package/dist/agent/host-roster.d.ts +37 -5
  6. package/dist/agent/registry.d.ts +2 -0
  7. package/dist/app.d.ts +33 -6
  8. package/dist/approvals/apply-decision.d.ts +67 -19
  9. package/dist/approvals/held-run-release-wiring.d.ts +45 -0
  10. package/dist/approvals/independent-wiring.d.ts +55 -0
  11. package/dist/approvals/resume-router.d.ts +9 -0
  12. package/dist/approvals/triggerer-subject.d.ts +37 -0
  13. package/dist/cache/dep-cache.d.ts +54 -10
  14. package/dist/cache/global-eval-round-cache.d.ts +7 -0
  15. package/dist/cli/api-client.d.ts +18 -0
  16. package/dist/cli/commands/cluster-settings.d.ts +11 -0
  17. package/dist/cli/commands/diagnose.d.ts +6 -0
  18. package/dist/cli/commands/held-run.d.ts +58 -0
  19. package/dist/cli/commands/trust-policy.d.ts +117 -12
  20. package/dist/cli/kici-admin.d.ts +10 -0
  21. package/dist/cli.js +2272 -1025
  22. package/dist/cluster/cluster-identity.d.ts +3 -3
  23. package/dist/cluster/cluster-settings-reader.d.ts +7 -1
  24. package/dist/cluster/coordinator.d.ts +44 -0
  25. package/dist/cluster/index.d.ts +4 -0
  26. package/dist/cluster/peer-handler.d.ts +19 -0
  27. package/dist/cluster/peer-registry.d.ts +17 -0
  28. package/dist/cluster/plan-headroom-store.d.ts +26 -0
  29. package/dist/cluster/worker-eviction.d.ts +69 -0
  30. package/dist/cold-store/load-event-log-range.d.ts +10 -3
  31. package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
  32. package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
  33. package/dist/config.d.ts +21 -21
  34. package/dist/contexts/held-runs.d.ts +123 -16
  35. package/dist/contexts/protection/aggregate.d.ts +3 -2
  36. package/dist/contexts/protection/branch-gate.d.ts +15 -0
  37. package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
  38. package/dist/contexts/protection/pipeline.d.ts +18 -0
  39. package/dist/contexts/release-queued-holds.d.ts +35 -0
  40. package/dist/dashboard/attestation-filters.d.ts +8 -0
  41. package/dist/dashboard/handler.d.ts +6 -24
  42. package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
  43. package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
  44. package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
  45. package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
  46. package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
  47. package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
  48. package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
  49. package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
  50. package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
  51. package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
  52. package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
  53. package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
  54. package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
  55. package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
  56. package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
  57. package/dist/db/types.d.ts +368 -10
  58. package/dist/entry-helpers.d.ts +28 -0
  59. package/dist/events/event-emitter.d.ts +22 -0
  60. package/dist/events/event-router.d.ts +87 -1
  61. package/dist/events/types.d.ts +2 -23
  62. package/dist/git/credential-broker.d.ts +94 -0
  63. package/dist/git/inherited-secret.d.ts +24 -0
  64. package/dist/git/installation-token.d.ts +38 -0
  65. package/dist/git/job-context.d.ts +13 -0
  66. package/dist/index.js +324 -84
  67. package/dist/metrics/agent-metrics-aggregator.d.ts +2 -2
  68. package/dist/metrics/prometheus.d.ts +111 -3
  69. package/dist/oidc/oidc-mint-registration.d.ts +7 -3
  70. package/dist/orchestrator-core.d.ts +79 -23
  71. package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
  72. package/dist/pipeline/dispatch-matched-workflow.d.ts +302 -31
  73. package/dist/pipeline/global-eval-round.d.ts +14 -0
  74. package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
  75. package/dist/pipeline/invoke-gate.d.ts +164 -0
  76. package/dist/pipeline/needs-scheduler.d.ts +13 -0
  77. package/dist/pipeline/process-webhook.d.ts +119 -31
  78. package/dist/pipeline/processor.d.ts +226 -21
  79. package/dist/pipeline/rerun.d.ts +21 -0
  80. package/dist/pipeline/resume-workflow.d.ts +40 -7
  81. package/dist/pipeline/route-or-dispatch-jobs.d.ts +9 -0
  82. package/dist/pipeline/security-hold-check.d.ts +217 -0
  83. package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
  84. package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
  85. package/dist/policy/dashboard-write-policy.d.ts +14 -0
  86. package/dist/provider-registry.d.ts +10 -2
  87. package/dist/providers/github/check-status-poster.d.ts +11 -0
  88. package/dist/providers/github/index.d.ts +0 -2
  89. package/dist/providers/github/normalizer.d.ts +3 -2
  90. package/dist/providers/local/normalizer.d.ts +3 -5
  91. package/dist/providers/universal-git/index.d.ts +11 -2
  92. package/dist/queue/job-queue.d.ts +43 -1
  93. package/dist/registration/extractor.d.ts +3 -3
  94. package/dist/registration/registration-index.d.ts +7 -0
  95. package/dist/registration/registration-store.d.ts +22 -0
  96. package/dist/reporting/check-run-reporter.d.ts +94 -1
  97. package/dist/reporting/check-run-summary.d.ts +25 -1
  98. package/dist/reporting/execution-tracker.d.ts +207 -2
  99. package/dist/reporting/run-aggregator.d.ts +4 -14
  100. package/dist/reporting/step-display-order.d.ts +43 -0
  101. package/dist/routes/admin-held-runs.d.ts +142 -0
  102. package/dist/routes/admin-trust-policy.d.ts +34 -6
  103. package/dist/routes/admin.d.ts +9 -0
  104. package/dist/scaler/backend-factory.d.ts +55 -0
  105. package/dist/scaler/bare-metal-backend.d.ts +43 -5
  106. package/dist/scaler/claim-store.d.ts +119 -0
  107. package/dist/scaler/config.d.ts +4 -0
  108. package/dist/scaler/container-backend.d.ts +18 -4
  109. package/dist/scaler/container-routing.d.ts +23 -0
  110. package/dist/scaler/container-spawn.d.ts +28 -0
  111. package/dist/scaler/event-backend.d.ts +124 -0
  112. package/dist/scaler/event-provision-reaper.d.ts +350 -0
  113. package/dist/scaler/failure-tracker.d.ts +1 -1
  114. package/dist/scaler/firecracker-backend.d.ts +33 -6
  115. package/dist/scaler/index.d.ts +11 -1
  116. package/dist/scaler/label-matcher.d.ts +4 -3
  117. package/dist/scaler/manager.d.ts +899 -72
  118. package/dist/scaler/resolve-container-auth.d.ts +45 -0
  119. package/dist/scaler/scaler-events.d.ts +11 -0
  120. package/dist/scaler/scaler-state-store.d.ts +294 -10
  121. package/dist/scaler/types.d.ts +131 -13
  122. package/dist/scaler/warm-pool.d.ts +132 -29
  123. package/dist/security/comment-handler.d.ts +50 -11
  124. package/dist/security/identity-link.d.ts +60 -0
  125. package/dist/security/lock-source.d.ts +11 -13
  126. package/dist/security/reduced-privilege-note.d.ts +59 -0
  127. package/dist/security/trust-directory-store.d.ts +233 -0
  128. package/dist/security/trust-policy-gate.d.ts +94 -59
  129. package/dist/security/trust-policy-store.d.ts +3 -0
  130. package/dist/security/trust-resolver.d.ts +14 -98
  131. package/dist/server.d.ts +11 -1
  132. package/dist/server.js +17684 -7351
  133. package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
  134. package/dist/stale-detector/stale-run-detector.d.ts +61 -3
  135. package/dist/standalone.js +24238 -13822
  136. package/dist/worker/in-memory-job-queue.d.ts +1 -1
  137. package/dist/ws/agent-handler.d.ts +28 -4
  138. package/dist/ws/dashboard-context-handler.d.ts +28 -4
  139. package/dist/ws/dashboard-global-workflows-handler.d.ts +6 -0
  140. package/dist/ws/failure-messages.d.ts +2 -0
  141. package/dist/ws/git-credential-relay.d.ts +39 -0
  142. package/dist/ws/oidc-token-relay.d.ts +6 -11
  143. package/dist/ws/platform-client.d.ts +54 -9
  144. package/installer-image-digests.json +3 -3
  145. package/package.json +24 -19
  146. package/sbom.spdx.json +864 -822
  147. package/dist/approvals/team-membership-lookup.d.ts +0 -13
  148. package/dist/providers/github/contributor-resolver.d.ts +0 -30
  149. package/dist/security/contributor-cache.d.ts +0 -83
@@ -0,0 +1,217 @@
1
+ import { type CheckStatus, type CheckStatusPoster } from '@kici-dev/engine';
2
+ import type { Kysely } from 'kysely';
3
+ import type { Database } from '../db/types.js';
4
+ /** Resolve the check poster of the provider bundle a routing key is served by. */
5
+ export type ResolveCheckStatusPoster = (routingKey: string) => CheckStatusPoster | undefined;
6
+ /**
7
+ * The `held_runs` columns this module reads. A full `HeldRun` row satisfies it,
8
+ * so every caller passes the row it already loaded rather than a projection.
9
+ */
10
+ export interface SecurityCheckHold {
11
+ id: string;
12
+ org_id: string;
13
+ run_id: string;
14
+ job_id: string;
15
+ hold_scope: string;
16
+ hold_type: string;
17
+ approval_requirement: unknown;
18
+ /**
19
+ * Whether the pending post reached the provider. Required rather than
20
+ * optional so a query that projects columns by name cannot omit it and fall
21
+ * back to the shape derivation without the compiler saying so.
22
+ */
23
+ posted_pending_check: boolean | null;
24
+ }
25
+ /** How a hold ended, which decides the conclusion, title and summary. */
26
+ export declare enum HoldOutcome {
27
+ /** Released to run — an approver satisfied it. */
28
+ Approved = "approved",
29
+ /** Refused by an approver. */
30
+ Rejected = "rejected",
31
+ /** Nobody answered inside the approval window. */
32
+ Expired = "expired"
33
+ }
34
+ /** Why {@link settleSecurityHoldCheck} did or did not write the check. */
35
+ export declare enum SecurityCheckSettlement {
36
+ /** A terminal status was written. */
37
+ Posted = "posted",
38
+ /** This hold posted no pending check, so it has none to terminalize. */
39
+ NotOwned = "not-owned",
40
+ /** The caller already wrote this commit's check earlier in the same pass. */
41
+ AlreadySettled = "already-settled",
42
+ /** Another pending hold on the same commit still owns the check. */
43
+ Contended = "contended",
44
+ /** The hold's run row is gone, so the commit cannot be named. */
45
+ NoCommit = "no-commit",
46
+ /** No provider bundle serves the run's routing key any more. */
47
+ NoPoster = "no-poster",
48
+ /**
49
+ * The settlement could not complete — the provider refused the write, or the
50
+ * hold row did not carry the columns the ownership predicate reads.
51
+ */
52
+ Failed = "failed"
53
+ }
54
+ export interface SettleSecurityCheckResult {
55
+ outcome: SecurityCheckSettlement;
56
+ /** Whether a terminal status actually reached the provider. */
57
+ posted: boolean;
58
+ /** `owner/repo@sha`, once the hold's commit resolved. */
59
+ commit?: string;
60
+ }
61
+ /**
62
+ * Whether this hold put a pending `KiCI Security` check on its commit, and so
63
+ * has one to terminalize when it ends.
64
+ *
65
+ * `posted_pending_check` is the answer whenever the row carries one: it is
66
+ * written `true` only after the provider accepted the post, so it records what
67
+ * HAPPENED. Everything below records what the code INTENDED, which is a
68
+ * different question and answers the wrong way twice — a post the provider
69
+ * refused, and a hold reached with no check poster in `ctx.bundle`, both leave
70
+ * a shape that says "posted" and a commit that has nothing. Terminalizing then
71
+ * CREATES the check run, so the mirror of the stuck-check leak is a fabricated
72
+ * failing check on a pull request.
73
+ *
74
+ * The shape derivation stays for `null` — a row written before the column
75
+ * existed, for which no fact was recorded and an inference is all there is.
76
+ *
77
+ * Three sites post that pending status, and the derivation is read off them
78
+ * rather than off the queue type — which cannot make the distinction, because a
79
+ * workflow install gate whose context protection rule is security-typed carries
80
+ * `queue_type = 'security'` and posts nothing at all.
81
+ *
82
+ * | Hold | Written by | Posts pending? | Recognised here by |
83
+ * |---|---|---|---|
84
+ * | org trust policy's PR-wide hold | `holdRunForSecurityPolicy` | yes | `job_id` is a `SECURITY_HOLD_JOB_IDS` sentinel |
85
+ * | workflow install gate | `holdWorkflowForInstallGate` | no | `job_id` is `installGateJobId(name)` |
86
+ * | SDK workflow `requireApproval` | `holdJobForApproval` | yes | carries an `approval_requirement` |
87
+ * | context reviewer hold | `holdJobForApproval` | yes | carries an `approval_requirement` |
88
+ * | SDK job `requireApproval` | `holdJobForApproval` | yes | carries an `approval_requirement` |
89
+ * | context security hold | the per-env gate | yes | job scope + `hold_type = security` |
90
+ * | wait-timer / concurrency hold | the per-env gate | no | none of the above matches |
91
+ * | step approval | `StepApprovalBridge.request` | no | step scope |
92
+ *
93
+ * `approval_requirement` is what `createHold` writes and `create` does not, so
94
+ * it identifies exactly the rows `holdJobForApproval` wrote through the
95
+ * approval path — the site whose post is guarded by `if (hold && …)`. The two
96
+ * other `createHold` callers are excluded above it: the install gate by its job
97
+ * id, the step bridge by its scope.
98
+ */
99
+ export declare function postedPendingSecurityCheck(hold: SecurityCheckHold): boolean;
100
+ /**
101
+ * The terminal summary for a hold that ended, in the one vocabulary both check
102
+ * families use: `cancelled` for a rejection, `timed_out` for an elapsed window,
103
+ * `success` for an approval.
104
+ *
105
+ * The subject follows the hold's scope on every outcome, because the same event
106
+ * means different things at each: a workflow-scoped hold owns the whole run,
107
+ * while a job-scoped one owns one job and says nothing about the run's other
108
+ * jobs — including a step-approval hold that is still pending, which a run-wide
109
+ * "no longer waiting for approval" would contradict.
110
+ *
111
+ * `actor` names whoever ended the hold, on an approve and on a reject alike.
112
+ * Only an expiry has none: nobody answered, which is what expiry means. A
113
+ * rejecter whose surface already writes its own attribution into `reason` (the
114
+ * `/kici reject` handler builds "Rejected by alice via /kici reject") passes no
115
+ * `actor`, so the sentence lands once rather than twice.
116
+ *
117
+ * **`actor` is a display name, and it is published verbatim on a public commit
118
+ * check.** It must never be an opaque identity-provider subject id: a surface
119
+ * that holds one — the dashboard / CLI / MCP applier, which decides by KiCI
120
+ * user id — resolves it against the org's identity directory first and passes
121
+ * nothing when that resolves to no single name. Omitting the attribution reads
122
+ * as "an approver"; leaking the subject reads as `Approved by
123
+ * 8f3a-…-uuid`, on a pull request anyone can open.
124
+ *
125
+ * `reason` is normalized to end in terminal punctuation — a rejecter's own
126
+ * sentence carries none, and without one it runs straight into what follows.
127
+ */
128
+ export declare function buildHoldEndedSummary(args: {
129
+ outcome: HoldOutcome;
130
+ scope: string;
131
+ reason?: string | undefined;
132
+ actor?: string | undefined;
133
+ via?: string | undefined;
134
+ }): string;
135
+ /**
136
+ * Terminalize the `KiCI Security` check of a hold that just ended, choosing the
137
+ * conclusion, title and summary from {@link HoldOutcome}.
138
+ *
139
+ * The single entry point for every surface that ends a hold — the `/kici`
140
+ * comment handler, the dashboard / CLI / MCP applier, and the stale detector's
141
+ * approval-window sweep — so those surfaces cannot render one event three ways.
142
+ */
143
+ export declare function settleSecurityCheckForOutcome(args: {
144
+ db: Kysely<Database> | undefined;
145
+ resolvePoster: ResolveCheckStatusPoster | undefined;
146
+ hold: SecurityCheckHold;
147
+ outcome: HoldOutcome;
148
+ reason?: string | undefined;
149
+ actor?: string | undefined;
150
+ via?: string | undefined;
151
+ excludeHoldIds?: readonly string[] | undefined;
152
+ skipCommits?: ReadonlySet<string> | undefined;
153
+ }): Promise<SettleSecurityCheckResult>;
154
+ /**
155
+ * The dashboard / CLI / MCP applier's entry point into
156
+ * {@link settleSecurityCheckForOutcome}.
157
+ *
158
+ * That applier decides by KiCI user id, so the only actor it holds is an opaque
159
+ * identity-provider subject — and this summary is published verbatim on a public
160
+ * commit check. `resolveDisplayName` maps the subject onto the account name the
161
+ * org's identity directory links it to; when it names none, the attribution is
162
+ * DROPPED rather than falling back to the subject. So an approve reads
163
+ * "Approved by an approver" at worst, never "Approved by 8f3a-…-uuid".
164
+ *
165
+ * Separate from its caller so the resolve-or-drop step is a unit under test
166
+ * rather than a line in the composition root.
167
+ */
168
+ export declare function settleSecurityCheckForDecision(args: {
169
+ db: Kysely<Database> | undefined;
170
+ resolvePoster: ResolveCheckStatusPoster | undefined;
171
+ /** Subject id → the linked account name, or undefined for no single name. */
172
+ resolveDisplayName: (actorSub: string) => string | undefined;
173
+ hold: SecurityCheckHold;
174
+ outcome: HoldOutcome;
175
+ /** Omitted where the summary must match one another writer built without it. */
176
+ actorSub?: string | undefined;
177
+ reason?: string | undefined;
178
+ }): Promise<SettleSecurityCheckResult>;
179
+ /**
180
+ * Terminalize the `KiCI Security` check of a hold that just ended, under a
181
+ * caller-supplied status and copy.
182
+ *
183
+ * Used directly where one summary has to reach BOTH check families — a rejected
184
+ * workflow-scoped hold reports the same sentence on `KiCI Security` and on the
185
+ * `kici/…` runs of the same event, and that sameness is asserted rather than
186
+ * assumed. Everything else goes through {@link settleSecurityCheckForOutcome}.
187
+ *
188
+ * Call it AFTER the hold's own row has left `pending` (every caller does: the
189
+ * applier flips inside `recordAndReject` / `recordAndRelease`, the comment
190
+ * handler inside `reject` / `approveByQueueType`) — and, on an approve, BEFORE
191
+ * the resume, so a replayed dispatch that holds again overwrites this terminal
192
+ * status with its own pending one rather than the other way round. The ending
193
+ * hold's own id is excluded from the contention query regardless, so a sweep
194
+ * that has not yet flipped its batch can name it in `excludeHoldIds`.
195
+ *
196
+ * Never throws: terminalizing a check run is a reporting courtesy on a path
197
+ * whose real job is to terminalize the hold, and a provider error must not stop
198
+ * that. The returned `posted` is what a caller binds its own suppression to —
199
+ * it says a check was written, not merely that a delegate resolved.
200
+ */
201
+ export declare function settleSecurityHoldCheck(args: {
202
+ db: Kysely<Database> | undefined;
203
+ resolvePoster: ResolveCheckStatusPoster | undefined;
204
+ hold: SecurityCheckHold;
205
+ status: CheckStatus;
206
+ title: string;
207
+ summary: string;
208
+ excludeHoldIds?: readonly string[] | undefined;
209
+ /**
210
+ * Commits this caller already wrote in the same pass. A sweep that ends
211
+ * several holds on one commit shares one check run between them, and every
212
+ * hold in it carries the same conclusion and summary — so the second write
213
+ * would be a byte-identical provider round-trip.
214
+ */
215
+ skipCommits?: ReadonlySet<string> | undefined;
216
+ }): Promise<SettleSecurityCheckResult>;
217
+ //# sourceMappingURL=security-hold-check.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { type CheckRunConclusion } from '@kici-dev/engine';
2
+ import type { Kysely } from 'kysely';
3
+ import type { Database } from '../db/types.js';
4
+ import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
5
+ /**
6
+ * Complete the check runs of `runId`'s stored workflow dispatch, if it has one.
7
+ *
8
+ * A run with no pending workflow context never held at workflow scope (or its
9
+ * context was already consumed by a resume), so there is nothing to close and
10
+ * this is a no-op. Call it BEFORE `deletePendingWorkflowContext` — the context
11
+ * is the only place the check-run names can still be derived from.
12
+ *
13
+ * Never throws: closing a check run is a reporting courtesy on a path whose real
14
+ * job is to terminalize the run, and a provider error must not stop that.
15
+ */
16
+ export declare function completeUndispatchedHoldChecks(args: {
17
+ db: Kysely<Database> | undefined;
18
+ checkRunReporter: CheckRunReporter | undefined;
19
+ runId: string;
20
+ conclusion: CheckRunConclusion;
21
+ summary: string;
22
+ }): Promise<void>;
23
+ //# sourceMappingURL=undispatched-hold-checks.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Cross-coordinator propagation of dashboard-write policy changes.
3
+ *
4
+ * Sister to `GenericSourcesChangeListener`
5
+ * (`packages/orchestrator/src/webhook/generic-sources-listener.ts`), which
6
+ * owns the `generic_sources_change` channel. This listener owns
7
+ * `dashboard_write_policy_change` — the channel `setDashboardWritePolicy`
8
+ * emits from inside its own write transaction.
9
+ *
10
+ * The problem it solves: `dashboardWritePolicyEvents` is a per-process
11
+ * `EventEmitter`, so only the coordinator that served the `kici-admin`
12
+ * write learned about a flip. Its siblings kept serving a stale cached map
13
+ * from their own policy gate for up to the cache TTL, and — worse — kept
14
+ * advertising the stale map to the control plane indefinitely, because
15
+ * capabilities are pushed on change and on reconnect, never polled. The
16
+ * control plane caches capabilities per orchestrator connection and resolves
17
+ * a cluster name to whichever of its connections it finds first, so a policy
18
+ * flip could be invisible to the dashboard for as long as the cluster stayed
19
+ * up.
20
+ *
21
+ * On NOTIFY:
22
+ * 1. Queue the affected `customer_id` (the channel payload).
23
+ * 2. Debounce 200 ms so rapid flips coalesce into one drain pass.
24
+ * 3. For each queued customer: drop the cached map, re-read the
25
+ * authoritative one from the database, and emit `'changed'` on
26
+ * `dashboardWritePolicyEvents`. The platform-mode boot already
27
+ * subscribes to that event and re-broadcasts `orch.capabilities`.
28
+ *
29
+ * The payload is the customer id alone: every coordinator shares one
30
+ * orchestrator database, so re-reading is both cheap and authoritative,
31
+ * and no wire copy of the map can go stale in flight.
32
+ *
33
+ * The writing coordinator receives its own NOTIFY too. That is deliberate
34
+ * and harmless — it already emitted the event in-process, and the second
35
+ * emit produces an identical capability broadcast.
36
+ */
37
+ import type pg from 'pg';
38
+ import type { Kysely } from 'kysely';
39
+ import type { Database } from '../db/types.js';
40
+ export interface DashboardWritePolicyChangeListenerOptions {
41
+ /** Raw pg pool — a dedicated PoolClient is checked out for LISTEN. */
42
+ pool: pg.Pool;
43
+ /** Used to re-read the authoritative policy map on each NOTIFY. */
44
+ db: Kysely<Database>;
45
+ /** Coalesce window for rapid NOTIFYs against the same customer. Default 200 ms. */
46
+ debounceMs?: number;
47
+ }
48
+ export declare class DashboardWritePolicyChangeListener {
49
+ private readonly opts;
50
+ private client;
51
+ private readonly pending;
52
+ private debounceTimer;
53
+ private readonly debounceMs;
54
+ constructor(opts: DashboardWritePolicyChangeListenerOptions);
55
+ /** Open the dedicated client and subscribe. */
56
+ start(): Promise<void>;
57
+ stop(): Promise<void>;
58
+ private scheduleDrain;
59
+ /** Visible for tests — drain the queued customer ids synchronously. */
60
+ drain(): Promise<void>;
61
+ }
62
+ //# sourceMappingURL=dashboard-write-policy-listener.d.ts.map
@@ -23,10 +23,24 @@ import { type Kysely } from 'kysely';
23
23
  import { DashboardWriteOperation, type DashboardWritePolicyMap, type DashboardWritePolicyState } from '@kici-dev/engine/protocol/dashboard-write-operations';
24
24
  import type { Database } from '../db/types.js';
25
25
  import type { ActorPrincipal } from '@kici-dev/engine';
26
+ /**
27
+ * Postgres `LISTEN/NOTIFY` channel carrying policy changes across the
28
+ * cluster. The payload is the `customer_id` alone — every coordinator
29
+ * shares one orchestrator database, so each listener re-reads the
30
+ * authoritative map for itself rather than trusting a wire copy.
31
+ *
32
+ * `DashboardWritePolicyChangeListener` (`./dashboard-write-policy-listener.ts`)
33
+ * is the consumer.
34
+ */
35
+ export declare const DASHBOARD_WRITE_POLICY_CHANNEL = "dashboard_write_policy_change";
26
36
  /**
27
37
  * Event bus for policy-change notifications. The WS publisher subscribes
28
38
  * to `'changed'` so it can broadcast a fresh `orch.capabilities` to
29
39
  * Platform whenever the operator flips a switch.
40
+ *
41
+ * The emitter is per-process, so it carries a change only to the peer that
42
+ * served the admin write. The NOTIFY channel above is what reaches the other
43
+ * coordinators, whose listeners then emit this same event locally.
30
44
  */
31
45
  export declare const dashboardWritePolicyEvents: EventEmitter<[never]>;
32
46
  /**
@@ -12,7 +12,7 @@
12
12
  * webhook normalization, lock file fetching, changed files retrieval,
13
13
  * clone token creation, and URL building.
14
14
  */
15
- import type { WebhookNormalizer, LockFileFetcher, FileContentsFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster, ProviderType } from '@kici-dev/engine';
15
+ import type { WebhookNormalizer, LockFileFetcher, FileContentsFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, CheckStatusPoster, ProviderType } from '@kici-dev/engine';
16
16
  /**
17
17
  * Complete set of provider capabilities.
18
18
  *
@@ -44,7 +44,15 @@ export interface ProviderBundle {
44
44
  changedFilesFetcher?: ChangedFilesFetcher;
45
45
  cloneTokenProvider?: CloneTokenProvider;
46
46
  repoUrlBuilder?: RepoUrlBuilder;
47
- contributorResolver?: ContributorResolver;
47
+ /**
48
+ * This provider has a fork model: a head ref can live outside the base repo,
49
+ * so the fork trust policy applies to its pull-request events. True for
50
+ * GitHub. Absent for providers whose trust boundary is something else — a
51
+ * generic source's verification secret, a local source's on-disk ownership —
52
+ * and for universal-git, which reports an `isForkPR` signal that must not
53
+ * gate it.
54
+ */
55
+ hasForkModel?: boolean;
48
56
  checkStatusPoster?: CheckStatusPoster;
49
57
  /**
50
58
  * Local `file://` in-place profile: this bundle's `repoBasePath` is the
@@ -66,5 +66,16 @@ export declare class GitHubCheckStatusPoster implements CheckStatusPoster {
66
66
  * only requires check names it was configured with.
67
67
  */
68
68
  postGlobalEvalFailedCheck(repoIdentifier: string, commitSha: string, summary: string, credentials: unknown): Promise<void>;
69
+ /**
70
+ * Post the success conclusion on the organization-workflow-evaluation check
71
+ * after a re-run of a failed round completed cleanly.
72
+ *
73
+ * Deliberately the SAME check name the failure posts: GitHub keys a check run
74
+ * by its name on a commit, so writing the success under that name is what
75
+ * turns the red check green on the original commit. A second, differently
76
+ * named green check would leave the red one standing and keep a branch
77
+ * protection rule blocked with no way to clear it short of a new commit.
78
+ */
79
+ postGlobalEvalSucceededCheck(repoIdentifier: string, commitSha: string, summary: string, credentials: unknown): Promise<void>;
69
80
  }
70
81
  //# sourceMappingURL=check-status-poster.d.ts.map
@@ -8,7 +8,6 @@
8
8
  * - GitHubChangedFilesFetcher -> ChangedFilesFetcher
9
9
  * - GitHubCloneTokenProvider -> CloneTokenProvider
10
10
  * - GitHubRepoUrlBuilder -> RepoUrlBuilder
11
- * - GitHubContributorResolver -> ContributorResolver
12
11
  */
13
12
  export { GitHubWebhookNormalizer } from './normalizer.js';
14
13
  export { GitHubLockFileFetcher } from './lock-file.js';
@@ -17,6 +16,5 @@ export { GitHubChangedFilesFetcher } from './changed-files.js';
17
16
  export { GitHubCloneTokenProvider, createInstallationOctokit } from './auth.js';
18
17
  export type { GitHubAppConfig, GitHubCredentials } from './auth.js';
19
18
  export { GitHubRepoUrlBuilder } from './repo-url.js';
20
- export { GitHubContributorResolver } from './contributor-resolver.js';
21
19
  export { GitHubCheckStatusPoster } from './check-status-poster.js';
22
20
  //# sourceMappingURL=index.d.ts.map
@@ -74,8 +74,9 @@ export declare class GitHubWebhookNormalizer implements WebhookNormalizer {
74
74
  */
75
75
  extractCredentials(payload: unknown): Record<string, unknown>;
76
76
  /**
77
- * Map GitHub membership-related webhook events to ContributorCache
78
- * invalidations. See WebhookNormalizer.getAccessCacheInvalidations.
77
+ * Map GitHub membership-related webhook events to permission-cache
78
+ * invalidations. See WebhookNormalizer.getAccessCacheInvalidations, which is
79
+ * deprecated and has no caller.
79
80
  *
80
81
  * Covered event types:
81
82
  *
@@ -70,8 +70,9 @@ export declare class LocalWebhookNormalizer implements WebhookNormalizer {
70
70
  */
71
71
  normalizeEvent(eventType: string, _action: string | null, payload: unknown): SimulatedEvent | null;
72
72
  /**
73
- * Map membership-related local-source webhook events to ContributorCache
74
- * invalidations.
73
+ * Map membership-related local-source webhook events to permission-cache
74
+ * invalidations. See WebhookNormalizer.getAccessCacheInvalidations, which is
75
+ * deprecated and has no caller.
75
76
  *
76
77
  * Local-source payloads are GitHub-shaped by design, so the mapping
77
78
  * mirrors the GitHub normalizer exactly:
@@ -80,9 +81,6 @@ export declare class LocalWebhookNormalizer implements WebhookNormalizer {
80
81
  * - `organization`: user-in-org
81
82
  * - `membership`: user-in-org
82
83
  * - `team` (repo-scoped actions only): repo
83
- *
84
- * This lets membership-invalidation paths be exercised through
85
- * `sendLocalWebhook()` without requiring a real GitHub App.
86
84
  */
87
85
  getAccessCacheInvalidations(eventType: string, _action: string | null, payload: unknown): AccessCacheInvalidation[];
88
86
  }
@@ -18,8 +18,17 @@
18
18
  * - `UniversalGitRepoUrlBuilder` — substitutes `{owner}`/`{name}`/`{repo}`
19
19
  * in the source's `gitUrlTemplate`.
20
20
  *
21
- * No `ContributorResolver` or `CheckStatusPoster` is wired for v1 — forge
22
- * API support for those is uneven and adding it is a separate phase.
21
+ * No `CheckStatusPoster` is wired for v1 — forge API support for it is uneven
22
+ * and adding it is a separate phase.
23
+ *
24
+ * The bundle also leaves `hasForkModel` unset. The normalizer does report an
25
+ * `isForkPR`, but it compares two repo names that `extractRepoFullName` reads
26
+ * from fixed keys (`repo.full_name`, then `full_name`) on the payload's `head`
27
+ * and `base` — keys many forges' PR payloads do not carry, and which no
28
+ * `payloadPaths` entry can redirect. It yields `false` whenever either is
29
+ * absent, so it fails toward trust. A pull-request event here therefore
30
+ * resolves no trust tier: it reads the base branch's lock file and writes an
31
+ * isolated cache scope, and the org fork policy never gates it.
23
32
  */
24
33
  import type { ProviderBundle } from '../../provider-registry.js';
25
34
  import type { GenericWebhookSource } from '../../db/types.js';
@@ -180,6 +180,23 @@ export interface QueuedJob {
180
180
  /** For a runsOnAll host-fanout child: the agent this job is pinned to. */
181
181
  pinnedAgentId?: string;
182
182
  }
183
+ /**
184
+ * The resource shape a job declares, read from the one location that always
185
+ * carries it.
186
+ *
187
+ * `jobConfig.resources` is where the declaration is stored and transported: it
188
+ * is what `job_config` holds across the DB round trip and what a worker
189
+ * receives over the wire. The typed `resources` field on {@link QueuedJobInput}
190
+ * and {@link QueuedJob} is a convenience mirror, and it is optional — several
191
+ * dispatch paths build their input without it (the worker's reroute handler and
192
+ * the webhook dispatch path among them). So a consumer that reads only the
193
+ * mirror sees a job that declares nothing, and a shape-sensitive decision made
194
+ * on that reading is wrong rather than merely unaware.
195
+ *
196
+ * Callers that hold a mirror still prefer it — it is the same value — and fall
197
+ * back here.
198
+ */
199
+ export declare function resourcesFromJobConfig(jobConfig: Record<string, unknown> | undefined): ResourceRequest | undefined;
183
200
  /**
184
201
  * DB-backed FIFO job dispatch queue using Kysely (PostgreSQL only).
185
202
  * Uses SQL-based JSONB containment queries (@> operator) for label matching.
@@ -236,9 +253,16 @@ export declare class JobQueue {
236
253
  * @param agentLabels Labels the agent provides.
237
254
  * @param agentMandatoryLabels Mandatory labels the spawning scaler declared
238
255
  * (empty for static / non-scaler agents).
256
+ * @param canServe Optional extra predicate applied to every candidate row
257
+ * before it is claimed. Labels cannot express whether a pre-spawned agent's
258
+ * fixed cpu / memory / image fit a job, so a caller that has to answer that
259
+ * supplies it here — the row must never be claimed and put back, because a
260
+ * claim-then-release both strands it as Dispatched for a window and burns a
261
+ * dispatch attempt. Supplying it opts out of the single-statement fast
262
+ * path, so pass it only for an agent that actually needs the check.
239
263
  * @returns The matching job, or null if none found.
240
264
  */
241
- dequeueForLabels(agentLabels: string[], agentMandatoryLabels?: string[], agentId?: string): Promise<QueuedJob | null>;
265
+ dequeueForLabels(agentLabels: string[], agentMandatoryLabels?: string[], agentId?: string, canServe?: (job: QueuedJob) => boolean): Promise<QueuedJob | null>;
242
266
  /**
243
267
  * The column writes that constitute a claim. Identical to what
244
268
  * {@link markDispatched} sets, so a claim and the caller's follow-up
@@ -309,6 +333,24 @@ export declare class JobQueue {
309
333
  * null, so a lost race costs this agent a candidate and not a whole drain.
310
334
  */
311
335
  private claimWithPatterns;
336
+ /**
337
+ * Both drain passes at once, with an extra JS predicate between the SELECT
338
+ * and the claim.
339
+ *
340
+ * Same shape as {@link claimWithPatterns} and for the same reason: the filter
341
+ * has to run BEFORE the row transitions, so the claim is a conditional
342
+ * `status = Pending` UPDATE by id rather than the embedded sub-select the
343
+ * fast path uses. Claiming first and releasing on a rejection is not an
344
+ * option — it strands the row as Dispatched for the width of the round trip
345
+ * and spends one of the job's bounded dispatch attempts every time an
346
+ * unsuitable agent polls.
347
+ *
348
+ * It covers pattern-free rows too, which the fast path would otherwise take
349
+ * first, so the predicate is applied to every candidate rather than to the
350
+ * pattern-bearing minority. Losing a claim continues to the next candidate,
351
+ * exactly as the pattern pass does.
352
+ */
353
+ private claimWithPostFilter;
312
354
  /**
313
355
  * Atomically claim the oldest pending job pinned to a specific agent. Used by
314
356
  * the eager pin drain when the pinned agent (re)registers or frees a slot —
@@ -29,9 +29,9 @@ export declare function extractGlobalWorkflows(lockFile: LockFile): LockWorkflow
29
29
  * Extract workflows that have at least one registerable trigger, OR that
30
30
  * have repo patterns (global workflows).
31
31
  *
32
- * Since phase 28.5, Git-provider triggers (push, pr, tag, …) are ALSO
33
- * registerable so cross-source dispatch can resolve them via the registration
34
- * index when a generic webhook targets an externally-hosted repo. The
32
+ * Git-provider triggers (push, pr, tag, …) are ALSO registerable, so
33
+ * cross-source dispatch can resolve them via the registration index when a
34
+ * generic webhook targets an externally-hosted repo. The
35
35
  * per-event lock file pipeline remains the primary matching path for
36
36
  * same-source git events; registration is an additive index.
37
37
  *
@@ -21,6 +21,13 @@ export interface RegisteredWorkflow {
21
21
  */
22
22
  customerId: string;
23
23
  commitSha: string | null;
24
+ /**
25
+ * The repository's default branch at last registration. A `__schedule_fire`
26
+ * run executes this branch's lock file, so it is the branch such a run
27
+ * presents to a context's branch restrictions. `null` until the repo's next
28
+ * default-branch push populates it.
29
+ */
30
+ defaultBranch: string | null;
24
31
  sourceFile: string | null;
25
32
  }
26
33
  /**
@@ -24,6 +24,16 @@ export interface RegistrationRow {
24
24
  */
25
25
  customerId: string;
26
26
  commitSha: string | null;
27
+ /**
28
+ * The repository's default branch, captured from the push that last updated
29
+ * this registration. A `__schedule_fire` run executes this branch's lock
30
+ * file, so this is the branch such a run presents to a context's branch
31
+ * restrictions.
32
+ *
33
+ * `null` when the registration predates the column or the payload named no
34
+ * default branch — the run then presents no branch and the gate rejects.
35
+ */
36
+ defaultBranch: string | null;
27
37
  sourceFile: string | null;
28
38
  created_at: Date;
29
39
  updated_at: Date;
@@ -40,6 +50,18 @@ interface ReplaceAllOptions {
40
50
  */
41
51
  customerId: string;
42
52
  commitSha?: string;
53
+ /**
54
+ * The repository's default branch, read off the registering push's payload.
55
+ * Persisted so a `__schedule_fire` run can present it as its own branch.
56
+ *
57
+ * Three states, and the difference between the last two is load-bearing on an
58
+ * UPDATE: a string is written; `null` means the caller LOOKED and the payload
59
+ * named none, so the stored value is cleared; **omitting the field** means the
60
+ * caller does not know, and an already-captured branch is preserved. Only a
61
+ * default-branch push knows a repository's default branch, so a caller that
62
+ * is not one (the manual registration route) must not erase what one proved.
63
+ */
64
+ defaultBranch?: string | null;
43
65
  sourceFile?: string;
44
66
  /** Set of workflow names that should be marked as global */
45
67
  globalWorkflowNames?: Set<string>;