@kici-dev/orchestrator 0.5.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 (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 +17671 -7350
  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,233 @@
1
+ /**
2
+ * Cache of the Platform-owned approval directory.
3
+ *
4
+ * The Platform pushes three documents next to the trust policy on
5
+ * `trust_policy.update`: the org's identity links (an array), each member's CI
6
+ * trust level (a user-id-keyed map), and the operator-defined teams (an array).
7
+ * They feed approval authorization: the `/kici approve` comment handler
8
+ * resolves a commenter to a KiCI user through the identity links and reads that
9
+ * user's CI trust level, and the approval resolver matches a `{team}` clause
10
+ * against the team memberships.
11
+ *
12
+ * Held only in memory, all three are empty after a restart until the next push
13
+ * lands, so a `/kici approve` comment in that window cannot be attributed to
14
+ * anyone and is refused. This store persists them so the directory survives the
15
+ * restart.
16
+ *
17
+ * Wherever a Platform is attached it is the sole writer, exactly as it is for
18
+ * `TrustPolicyStore`'s platform-sourced rows: a push replaces the cached
19
+ * directory wholesale. An independent orchestrator has no Platform and so no
20
+ * upstream authority, which is where `upsertLocalMember` / `removeLocalMember`
21
+ * come in — the operator registers approvers themselves. The two writers never
22
+ * overlap, because the admin route refuses the local write on any
23
+ * Platform-attached orchestrator, so a row still only ever has one writer for
24
+ * the lifetime of a deployment.
25
+ *
26
+ * ## The staleness trade-off
27
+ *
28
+ * Persisting the directory trades a fail-closed default for availability, and
29
+ * the trade runs in both directions.
30
+ *
31
+ * The gain: approvals keep working across a restart instead of being refused
32
+ * until the Platform reconnects.
33
+ *
34
+ * The cost: a restart restores whoever the last push named, so a member whose
35
+ * CI trust the Platform revoked while this orchestrator was down can still
36
+ * approve until the next push overwrites the cache. The Platform sends that
37
+ * push immediately after the orchestrator's WebSocket authenticates, so the
38
+ * window is normally the handshake — but an orchestrator that cannot reach the
39
+ * Platform at all never gets the push, so during a Platform outage the window
40
+ * is unbounded.
41
+ *
42
+ * The cache is deliberately not bounded by a TTL. An expired directory would
43
+ * fail closed exactly when the Platform is unreachable, which is when frozen
44
+ * approvals hurt most, and it would undo the restart-survival this store
45
+ * exists for. `TrustPolicyStore` caches the policy on the same terms.
46
+ * `updated_at` is stored, and read today only to order `loadLastPushed` and to
47
+ * report the cache's age in the boot log line — nothing compares it against a
48
+ * deadline — so a bound can be added later without a migration.
49
+ */
50
+ import { Kysely, type Transaction } from 'kysely';
51
+ import { z } from 'zod';
52
+ import { type CiTrustLevel } from '@kici-dev/engine';
53
+ import type { Database } from '../db/types.js';
54
+ /**
55
+ * The three directory fields of `trust_policy.update`, taken from the wire
56
+ * schema itself.
57
+ *
58
+ * What that pins: these three field *names*. Rename one upstream or drop it and
59
+ * this schema stops compiling, because the `.shape` lookup no longer resolves.
60
+ * A change *inside* one of them — an element gaining, losing, or retyping a
61
+ * field — compiles fine and propagates through `z.infer`, so the persisted
62
+ * document follows the pushed one automatically rather than by a compile error.
63
+ *
64
+ * What it does not pin: the field *set*. A fourth directory field added to
65
+ * `trustPolicyUpdateSchema` is not picked up here, and `server.ts` hand-builds
66
+ * the value it passes to `upsertFromPlatform`, so nothing would fail — the new
67
+ * field would simply go unpersisted. Adding one means extending this object and
68
+ * that literal together.
69
+ */
70
+ export declare const trustDirectorySchema: z.ZodObject<{
71
+ identityLinks: z.ZodArray<z.ZodObject<{
72
+ userId: z.ZodString;
73
+ provider: z.ZodString;
74
+ providerUsername: z.ZodString;
75
+ providerUserId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
76
+ }, z.core.$strip>>;
77
+ memberCiTrustLevels: z.ZodRecord<z.ZodString, z.ZodEnum<{
78
+ admin: "admin";
79
+ none: "none";
80
+ read: "read";
81
+ write: "write";
82
+ }>>;
83
+ teamMemberships: z.ZodDefault<z.ZodArray<z.ZodObject<{
84
+ teamName: z.ZodString;
85
+ memberUserIds: z.ZodArray<z.ZodString>;
86
+ }, z.core.$strip>>>;
87
+ }, z.core.$strip>;
88
+ export type TrustDirectory = z.infer<typeof trustDirectorySchema>;
89
+ /** A directory as stored, with the write timestamp. */
90
+ export interface StoredTrustDirectory extends TrustDirectory {
91
+ updatedAt: Date;
92
+ }
93
+ /** A stored directory carrying the org id it was written under. */
94
+ export interface KeyedTrustDirectory extends StoredTrustDirectory {
95
+ orgId: string;
96
+ }
97
+ /**
98
+ * A directory with nothing in it — what an org with no stored row starts from.
99
+ *
100
+ * A factory rather than a shared constant: the merge helpers below pass
101
+ * `teamMemberships` through by reference, so a single shared instance would
102
+ * hand every caller the same array and let one of them mutate what the next
103
+ * one reads as empty.
104
+ */
105
+ export declare function emptyTrustDirectory(): TrustDirectory;
106
+ /**
107
+ * One member's approval registration, as an operator supplies it on an
108
+ * independent orchestrator.
109
+ *
110
+ * `providerUserId` is REQUIRED here, unlike the nullish field on a pushed link.
111
+ * `findIdentityLink` matches on `(provider, providerUserId)` and never falls
112
+ * back to the mutable username, so a link registered without a numeric id would
113
+ * be inert — accepted, stored, and silently unable to authorize anyone. The
114
+ * caller is made to supply one rather than discover that from a refused
115
+ * approval.
116
+ */
117
+ export interface DirectoryMemberRegistration {
118
+ /** KiCI user id the approval is attributed to. */
119
+ userId: string;
120
+ /** Provider the link is for, e.g. `github`. */
121
+ provider: string;
122
+ /** Provider-side username. Display only — never matched on. */
123
+ providerUsername: string;
124
+ /** Immutable provider-side numeric id. The only field a link is matched on. */
125
+ providerUserId: string;
126
+ /** CI trust level to record for `userId`. `write` or `admin` may approve. */
127
+ ciTrust: CiTrustLevel;
128
+ }
129
+ /**
130
+ * Register one member into a directory, returning a new directory.
131
+ *
132
+ * Two existing links are displaced, not one. The obvious key is
133
+ * `(provider, providerUserId)` — the pair `findIdentityLink` matches on — but
134
+ * `(provider, userId)` has to go too: re-registering a member whose provider
135
+ * account changed would otherwise leave their OLD numeric id in the directory,
136
+ * still resolving to their user id and still carrying their CI trust. Whoever
137
+ * holds that id at the provider now would inherit the ability to approve.
138
+ *
139
+ * `teamMemberships` is passed through untouched — teams are not operator-
140
+ * writable here, and dropping them would silently break every `{team}` clause.
141
+ */
142
+ export declare function applyMemberRegistration(current: TrustDirectory, registration: DirectoryMemberRegistration): TrustDirectory;
143
+ /**
144
+ * Remove one member from a directory, returning a new directory and whether
145
+ * anything was actually removed.
146
+ *
147
+ * The inverse of {@link applyMemberRegistration}: every link the member holds
148
+ * on every provider goes, together with their CI trust level. Leaving the trust
149
+ * level behind would be harmless only until the member is re-registered on a
150
+ * different provider account, at which point the revoked level would silently
151
+ * come back with them.
152
+ */
153
+ export declare function removeMemberFromDirectory(current: TrustDirectory, userId: string): {
154
+ directory: TrustDirectory;
155
+ removed: boolean;
156
+ };
157
+ export declare class TrustDirectoryStore {
158
+ private readonly db;
159
+ constructor(db: Kysely<Database>);
160
+ /**
161
+ * Replace the cached directory for `orgId`.
162
+ *
163
+ * Needs no lock and no read-merge: a push carries all three lists in full, so
164
+ * concurrent pushes correctly settle on the last one — the same reasoning as
165
+ * `TrustPolicyStore.upsertFromPlatform`.
166
+ */
167
+ upsertFromPlatform(orgId: string, directory: TrustDirectory): Promise<void>;
168
+ /**
169
+ * Register (or re-register) one member in an operator-owned directory.
170
+ *
171
+ * Only reachable in independent mode — the admin route refuses on a
172
+ * Platform-attached orchestrator, because the next push replaces the whole
173
+ * document and would clobber the write.
174
+ *
175
+ * The read-merge-write is serialised per org by a transaction-scoped advisory
176
+ * lock, for the same reason `TrustPolicyStore.upsertLocal` takes one: the
177
+ * transaction alone gives nothing under READ COMMITTED, so two concurrent
178
+ * registrations would both read the pre-existing document and the second
179
+ * would overwrite the first's member out of its own stale copy.
180
+ *
181
+ * `onWrite` receives the same transaction and the merged directory, so an
182
+ * audit row written there commits or rolls back with the directory itself — a
183
+ * self-granted `write` level can never land unattributed.
184
+ *
185
+ * Returns the merged directory so the caller does not need a second read
186
+ * (which would be outside the transaction and could observe a later write).
187
+ */
188
+ upsertLocalMember(orgId: string, registration: DirectoryMemberRegistration, onWrite?: (trx: Transaction<Database>, merged: TrustDirectory) => Promise<void>): Promise<TrustDirectory>;
189
+ /**
190
+ * Remove one member from an operator-owned directory. Same mode restriction,
191
+ * locking, and audit contract as {@link upsertLocalMember}.
192
+ *
193
+ * `removed` is false when the member held no link and no CI trust level, so
194
+ * the caller can tell "revoked" from "was never registered" — the row is
195
+ * still rewritten either way, which keeps the operation idempotent. It is
196
+ * handed to `onWrite` as well as returned, because the audit row is written
197
+ * before this method returns and cannot read its own result.
198
+ */
199
+ removeLocalMember(orgId: string, userId: string, onWrite?: (trx: Transaction<Database>, merged: TrustDirectory, removed: boolean) => Promise<void>): Promise<{
200
+ directory: TrustDirectory;
201
+ removed: boolean;
202
+ }>;
203
+ /** Read the cached directory for `orgId`, or null when nothing was ever pushed. */
204
+ load(orgId: string): Promise<StoredTrustDirectory | null>;
205
+ /**
206
+ * Read the most recently written cached directory together with the org id it
207
+ * is keyed by.
208
+ *
209
+ * At process start the orchestrator has no org id in hand to call `load()`
210
+ * with: none is configured, and the DB-derived resolution that exists is
211
+ * written inline in `server.ts`'s auth path rather than extracted. The row
212
+ * itself carries one — the org of the last push, which is by construction the
213
+ * org this directory belongs to — so the boot-time seed is keyed by real data
214
+ * instead of a hardcoded or invented id. An
215
+ * orchestrator serves exactly one org, so ordering by `updated_at` is a
216
+ * tiebreak that in practice never has to break a tie; it keeps the read
217
+ * deterministic if a row for a second org is ever present.
218
+ */
219
+ loadLastPushed(): Promise<KeyedTrustDirectory | null>;
220
+ /**
221
+ * The shared read-merge-write both local writers run: take the per-org
222
+ * advisory lock, read the current document (an absent row reads as
223
+ * {@link emptyTrustDirectory}), apply `merge`, write it back, then invoke
224
+ * `onWrite` inside the same transaction.
225
+ *
226
+ * The lock MUST precede the read: it is what orders this merge against a
227
+ * concurrent one, so taking it after the SELECT would order nothing.
228
+ */
229
+ private mergeLocally;
230
+ /** Replace the whole stored document for `orgId`. */
231
+ private write;
232
+ }
233
+ //# sourceMappingURL=trust-directory-store.d.ts.map
@@ -1,117 +1,152 @@
1
1
  /**
2
- * Org trust-policy gate — turns the Platform-owned policy plus three per-PR
3
- * signals into exactly one outcome.
2
+ * Org trust-policy gate — turns the org's fork switch plus the per-PR signals
3
+ * into exactly one outcome.
4
4
  *
5
5
  * Pure: no I/O, no DB, no clock. The caller supplies the effective policy (see
6
- * `resolveEffectivePolicy`) and the signals; enforcement lives in the dispatch
7
- * gate.
6
+ * `resolveEffectivePolicy`) and the signals; enforcement lives elsewhere the
7
+ * webhook pipeline drops an `ignore`d event before it can create a run, and the
8
+ * dispatch gate acts on the remaining verdicts.
8
9
  *
9
- * Scope: the caller only invokes this for sources whose provider bundle carries
10
- * a `ContributorResolver` (GitHub today). Every other source is trusted by
11
- * construction and never reaches here.
10
+ * Scope: the pipeline reaches this through `evaluateSecurityPolicy`, which
11
+ * short-circuits to `pass` for a source whose provider bundle leaves
12
+ * `hasForkModel` unset — so a source with no fork model never reaches the
13
+ * switch below by that route. That short-circuit is not a trust claim: a
14
+ * PR from a fork-less provider resolves NO tier, because the fork signal such a
15
+ * provider computes reads `false` whenever the payload keys it compares are
16
+ * absent. Those sources skip the switch because its one condition — "the PR
17
+ * came from a fork" — cannot be established for them, not because they are
18
+ * trusted.
12
19
  */
13
20
  import { z } from 'zod';
21
+ import { ForkPolicy } from '@kici-dev/engine';
14
22
  import type { OrchestratorMode, TrustPolicy, TrustTier } from '@kici-dev/engine';
15
23
  import { SecurityHoldReason } from '../contexts/held-runs.js';
16
24
  import type { StoredTrustPolicy } from './trust-policy-store.js';
17
25
  /**
18
- * Which arm of the gate is actually in force for an org — the vocabulary the
19
- * admin API reports and `kici-admin trust-policy show` renders.
26
+ * The enforcement vocabulary the admin API reports and `kici-admin
27
+ * trust-policy show` renders.
20
28
  *
21
- * `policy` means `resolveEffectivePolicy` produced a policy and all three arms
22
- * apply; `legacy` means it produced `null`, so only the legacy
23
- * workflow-modification rule runs and there are no policy values to report.
24
- * Named here, next to the resolver that decides between them, so the route and
25
- * the CLI cannot drift apart on a bare string literal.
29
+ * @deprecated The route reports `policy` unconditionally: `resolveEffectivePolicy`
30
+ * returns a policy for every input, so there is no state left in which the
31
+ * values are absent. The field and this enum stay so an older `kici-admin`
32
+ * binary keeps parsing the response. Removed at v1.0.0.
26
33
  */
27
34
  export declare const TrustPolicyEnforcement: z.ZodEnum<{
28
35
  legacy: "legacy";
29
36
  policy: "policy";
30
37
  }>;
31
38
  export type TrustPolicyEnforcement = z.infer<typeof TrustPolicyEnforcement>;
32
- /** The per-PR facts the policy is evaluated against. */
39
+ /** The per-PR facts the fork switch is evaluated against. */
33
40
  export interface TrustPolicySignals {
34
- /** Resolved contributor tier; undefined when trust resolution failed. */
41
+ /** Resolved contributor tier; undefined when no tier was resolved. */
35
42
  tier: TrustTier | undefined;
36
43
  /** The PR's head repo differs from its base repo. */
37
44
  isForkPR: boolean;
38
- /** The PR changes `.kici/` workflow definitions. */
39
- hasWorkflowModifications: boolean;
40
45
  }
41
46
  /**
42
- * The reasons the ORG TRUST POLICY itself can raise — its three arms.
47
+ * The reason the ORG TRUST POLICY itself raises.
43
48
  *
44
- * `context_trust` is excluded at the type level rather than merely by
45
- * convention: that reason belongs to the per-context minimum-trust gate, which
46
- * holds an individual job under its real name. Narrowing here is what lets
47
- * `SECURITY_HOLD_JOB_IDS` drop its phantom `context_trust` sentinel a value
48
- * that existed only to satisfy a `Record<SecurityHoldReason, ...>` and was never
49
- * written to a single row.
49
+ * The fork switch is the policy's only arm, so `fork_pr` is the only reason it
50
+ * can produce. The `SecurityHoldReason` Zod enum deliberately keeps its other
51
+ * members: `held_runs` rows written by earlier builds still carry them, and the
52
+ * per-context minimum-trust gate still writes `context_trust` under its own
53
+ * name. Narrowing here is a statement about what this gate emits, not about
54
+ * what the column may hold.
50
55
  */
51
- export type TrustPolicyHoldReason = Exclude<SecurityHoldReason, 'context_trust'>;
56
+ export type TrustPolicyHoldReason = Extract<SecurityHoldReason, 'fork_pr'>;
52
57
  export type TrustPolicyOutcome = {
53
58
  action: 'pass';
59
+ }
60
+ /**
61
+ * Drop the event entirely: no run row, no check status, nothing dispatched.
62
+ * The pipeline enforces this before it fetches a lock file, so an ignored
63
+ * event leaves no trace a contributor can see.
64
+ */
65
+ | {
66
+ action: 'ignore';
54
67
  } | {
55
68
  action: 'hold';
56
69
  reason: TrustPolicyHoldReason;
57
70
  message: string;
58
71
  /**
59
- * Hours the resulting hold stays approvable, taken from the SAME policy
60
- * that produced this verdict. `null` in independent mode, where there is
61
- * no upstream policy and therefore no operator-set window.
72
+ * Seconds the resulting hold stays approvable, taken from the SAME policy
73
+ * that produced this verdict.
62
74
  *
63
75
  * Carried on the outcome rather than re-read at the hold site: a second,
64
76
  * independent read bypassed `resolveEffectivePolicy`, so it was both a
65
77
  * TOCTOU (the policy could change between deciding and sizing) and a
66
- * divergence independent mode got the 72h default where the decision
67
- * path deliberately has no policy at all.
78
+ * divergence between deciding and sizing. `null` means "no window came
79
+ * with this verdict", and the hold site falls back to
80
+ * `DEFAULT_APPROVAL_EXPIRY_SECONDS`.
81
+ *
82
+ * Seconds, not hours, because this is what the hold site actually needs:
83
+ * an hours-only window cannot express the sub-hour hold the policy may now
84
+ * carry, and rounding it here would silently lengthen it.
68
85
  */
69
- approvalExpiryHours: number | null;
86
+ approvalExpirySeconds: number | null;
70
87
  } | {
71
88
  action: 'reject';
72
89
  reason: TrustPolicyHoldReason;
73
90
  message: string;
74
91
  };
75
92
  /**
76
- * The policy a Platform-attached orchestrator applies when it has no stored
77
- * row. Every arm holds — a hold is recoverable by approval, and these are the
78
- * documented org defaults, so an org that never changed its policy gets the
79
- * behavior its dashboard shows.
93
+ * The reason a verdict carries, or `undefined` for one that carries none.
94
+ *
95
+ * Logging and check-status call sites take any outcome, so they need the reason
96
+ * without narrowing the union themselves — and a call site that reached for
97
+ * `.reason` on a reasonless verdict would print `undefined` rather than fail.
98
+ */
99
+ export declare function trustPolicyOutcomeReason(outcome: TrustPolicyOutcome): TrustPolicyHoldReason | undefined;
100
+ /** The fork switch applied when no policy row is stored. */
101
+ export declare const DEFAULT_FORK_POLICY: ForkPolicy;
102
+ /**
103
+ * The policy applied when no row is stored — in every mode. Ignoring fork
104
+ * events is the fail-closed posture: nothing foreign dispatches, and the event
105
+ * is dropped rather than parked in a queue nobody is watching.
80
106
  *
81
107
  * This is NOT necessarily a brief transient. The Platform sends
82
108
  * `trust_policy.update` only when the org has a `trust_policies` row, and that
83
109
  * row is created lazily on a dashboard read — so an org that has never opened
84
110
  * Settings > CI trust receives no push at all and stays on these values
85
- * indefinitely. The expiry is therefore the generous documented default rather
86
- * than a short one: a short window would auto-fail legitimate runs.
111
+ * indefinitely.
87
112
  */
88
113
  export declare const FAIL_CLOSED_POLICY: TrustPolicy;
89
114
  /**
90
- * Pick the policy to evaluate. A stored row always wins. Without one, a
91
- * Platform-attached orchestrator fails closed (a push is imminent); an
92
- * independent orchestrator has no upstream authority at all, so it gets `null`
93
- * and the evaluator applies only the legacy rule — which is what keeps this
94
- * change from silently gating existing independent deployments on upgrade.
115
+ * The policy applied when the stored row could not be READ — a thrown query, a
116
+ * dropped connection.
117
+ *
118
+ * Distinct from `FAIL_CLOSED_POLICY`, which answers a different question: that
119
+ * one is what an org with no stored row has chosen by not choosing. A read
120
+ * failure says nothing about what the org chose, and an org that chose `hold`
121
+ * or `allow` would have its fork PRs dropped with no trace if the two cases
122
+ * shared an answer — a transient database blip turning a recoverable,
123
+ * contributor-visible hold into a silent disappearance.
95
124
  *
96
- * `PLATFORM_CONNECTED_MODES` is the shared definition of "expects a Platform
97
- * push", so a mode added there is fail-closed here without a second edit.
125
+ * Holding is fail-closed on the same terms: nothing untrusted dispatches. It is
126
+ * also recoverable the contributor sees the security check, and an operator
127
+ * can approve it — which an ignored event is not.
98
128
  */
99
- export declare function resolveEffectivePolicy(stored: StoredTrustPolicy | null, mode: OrchestratorMode): TrustPolicy | null;
129
+ export declare const READ_FAILURE_POLICY: TrustPolicy;
100
130
  /**
101
- * Evaluate the policy.
102
- *
103
- * Arms are evaluated in a fixed order — workflow_modification, fork_pr,
104
- * unknown_contributor — and then any `reject` beats any `hold`, with the first
105
- * arm in that order supplying the reason among equals. That makes a PR tripping
106
- * several arms produce one outcome with a stable reason rather than a verdict
107
- * that depends on evaluation accident.
131
+ * Pick the policy to evaluate. A stored row always wins; without one every
132
+ * orchestrator gets the fail-closed policy above.
108
133
  *
109
- * An unresolved tier (`undefined`) counts as `unknown`, never as a pass: a
110
- * trust resolution that could not be answered is not evidence of trust.
134
+ * `mode` no longer selects between two postures an independent orchestrator
135
+ * has no upstream authority, which is a reason to be stricter rather than more
136
+ * permissive — but it stays on the signature so callers that legitimately hold
137
+ * a mode do not have to change, and so a future per-mode difference has a place
138
+ * to land.
139
+ */
140
+ export declare function resolveEffectivePolicy(stored: StoredTrustPolicy | null, _mode: OrchestratorMode): TrustPolicy;
141
+ /**
142
+ * Evaluate the fork switch.
111
143
  *
112
- * A `null` policy applies only the legacy rule (workflow modifications by a
113
- * non-trusted contributor hold), reproducing behavior from before the policy was
114
- * enforced.
144
+ * Two guards precede it. A `trusted` tier passes: the ref lives in the base
145
+ * repo, so only a write-or-higher contributor could have put it there. A
146
+ * non-fork event passes too — the switch names one condition, and an event that
147
+ * does not meet it has no verdict to receive here. Reduced privilege for a
148
+ * non-trusted contributor is derived from the tier further down the pipeline,
149
+ * not from this outcome.
115
150
  */
116
- export declare function evaluateTrustPolicy(policy: TrustPolicy | null, signals: TrustPolicySignals): TrustPolicyOutcome;
151
+ export declare function evaluateTrustPolicy(policy: TrustPolicy, signals: TrustPolicySignals): TrustPolicyOutcome;
117
152
  //# sourceMappingURL=trust-policy-gate.d.ts.map
@@ -20,6 +20,9 @@ export type TrustPolicySource = z.infer<typeof TrustPolicySource>;
20
20
  /**
21
21
  * The strictest documented defaults. Used to fill gaps on a local merge; the
22
22
  * fail-closed policy an unknown org resolves to lives in `trust-policy-gate.ts`.
23
+ *
24
+ * `forkPolicy` reads that module's `DEFAULT_FORK_POLICY` so a partial local
25
+ * merge and an absent row settle on the same fork switch.
23
26
  */
24
27
  export declare const DEFAULT_TRUST_POLICY: TrustPolicy;
25
28
  /** A policy as stored, with its provenance. */
@@ -1,108 +1,24 @@
1
1
  /**
2
- * TrustResolver -- combines identity links, RBAC ci_trust, and provider permissions
3
- * into a trust tier decision.
2
+ * Ref-based trust: may this git ref reach shared state and real secrets?
4
3
  *
5
- * The TrustResolver is the core decision engine for CI security. It determines
6
- * whether a contributor is trusted, known, or unknown based on:
7
- * 1. Identity link lookup (provider username -> KiCI user ID)
8
- * 2. ci_trust RBAC level for that user in the org
9
- * 3. Provider API permission (via ContributorResolver + cache)
10
- */
11
- import type { ContributorPermission, ContributorResolver } from '@kici-dev/engine';
12
- import type { TrustTier } from '@kici-dev/engine';
13
- import type { ContributorCache } from './contributor-cache.js';
14
- /**
15
- * Match an identity link strictly by `(provider, providerUserId)`.
16
- *
17
- * Returns `null` whenever the numeric id is missing on either side or when no
18
- * link's id matches the event's id. This is the strict end-state contract:
19
- * username fallback is gone, mutable-username impersonation cannot grant
20
- * trust, and a refused match is recorded under
21
- * `kici_orch_trust_match_refused_no_id_total{reason}` so the rate of refusals
22
- * stays observable.
4
+ * Trusted the ref lives in the base repo (any same-repo push or PR
5
+ * only a write-or-higher contributor can put a ref there). Untrusted ⇔ the
6
+ * ref comes from a fork. Resolved locally from the webhook payload; no
7
+ * provider API call, no identity lookup.
23
8
  *
24
- * Pre-conditions for callers: Platform's reconcile job has filled
25
- * `provider_user_id` for every row in `identity_links` (verified by the
26
- * `kici_platform_identity_links_missing_provider_user_id` gauge being 0 for
27
- * ≥4 cycles before this strict policy was deployed). See
28
- * `.claude/plans/cryptic-jumping-narwhal.md` for the rollout journey.
9
+ * The stored/wire tier vocabulary keeps 'unknown' as the name for
10
+ * "untrusted" and 'trusted' for "trusted"; 'known' is legacy vocabulary
11
+ * that is no longer produced.
29
12
  */
30
- export declare function findIdentityLink(identityLinks: IdentityLink[], provider: string, providerUsername: string, providerUserId: string | undefined): IdentityLink | null;
31
- /** RBAC permission levels from the Platform permission system. */
32
- export type PermissionLevel = 'none' | 'read' | 'write' | 'admin';
33
- /** Identity link mapping a provider identity to a KiCI user. */
34
- export interface IdentityLink {
35
- userId: string;
36
- provider: string;
37
- providerUsername: string;
38
- /**
39
- * Immutable IDP-side numeric id (e.g. GitHub's `sender.id`).
40
- * Nullable during the backfill window for legacy rows that
41
- * predate Platform migration 009. Trust resolver matches on
42
- * this field first; once backfill completes the strict policy
43
- * refuses trust when this is null.
44
- */
45
- providerUserId?: string | null;
46
- }
47
- /** Parameters for trust tier resolution. */
48
- interface TrustResolutionParams {
49
- /** Sender username from the webhook event. */
50
- providerUsername: string;
51
- /**
52
- * Sender immutable IDP-side numeric id from the webhook event (e.g.
53
- * GitHub's `sender.id` coerced to string). Optional during the backfill
54
- * window; the strict end-state refuses trust when this is missing.
55
- */
56
- providerUserId?: string;
57
- /** Provider type (e.g. 'github'). */
58
- provider: string;
59
- /** Repository identifier (e.g. 'owner/repo'). */
60
- repoIdentifier: string;
61
- /** Whether the PR comes from a fork. */
62
- isForkPR: boolean;
63
- /** Organization ID this webhook routes to. */
64
- orgId: string;
65
- /** Cached identity links from Platform push. */
66
- identityLinks: IdentityLink[];
67
- /** ci_trust permission levels per user ID from Platform push. */
68
- orgMemberPermissions: Map<string, PermissionLevel>;
69
- /** Provider-specific contributor resolver. */
70
- contributorResolver: ContributorResolver;
71
- /** Credentials for provider API calls. */
72
- credentials: unknown;
73
- }
74
- /** Result of trust tier resolution with full audit trail. */
13
+ import type { TrustTier } from '@kici-dev/engine';
14
+ /** Result of ref-based trust resolution, with the reason recorded for audit. */
75
15
  export interface TrustResolution {
76
16
  tier: TrustTier;
77
17
  contributorUsername: string;
78
- identityLinked: boolean;
79
- userId?: string;
80
- providerPermission: ContributorPermission;
81
- ciTrustLevel?: PermissionLevel;
82
18
  reason: string;
83
19
  }
84
- /**
85
- * TrustResolver combines identity links, ci_trust RBAC, and provider permissions
86
- * into a trust tier.
87
- *
88
- * Decision matrix:
89
- * 1. Fork PR -> always unknown
90
- * 2. No identity link -> provider API fallback (never trusted)
91
- * - read+ -> known
92
- * - none -> unknown
93
- * 3. Identity linked -> combine ci_trust + provider permission:
94
- * - provider write+ AND ci_trust write+ -> trusted
95
- * - provider write+ AND ci_trust none/read -> known
96
- * - provider read -> known
97
- * - provider none -> unknown
98
- */
99
- export declare class TrustResolver {
100
- private readonly contributorCache;
101
- constructor(contributorCache: ContributorCache);
102
- /**
103
- * Resolve the trust tier for a contributor.
104
- */
105
- resolveTrustTier(params: TrustResolutionParams): Promise<TrustResolution>;
106
- }
107
- export {};
20
+ export declare function resolveRefTrust(args: {
21
+ isForkPR: boolean;
22
+ contributorUsername: string;
23
+ }): TrustResolution;
108
24
  //# sourceMappingURL=trust-resolver.d.ts.map
package/dist/server.d.ts CHANGED
@@ -13,5 +13,15 @@
13
13
  * Graceful shutdown in reverse order:
14
14
  * Platform client -> agent WS -> heartbeat -> HTTP -> DB
15
15
  */
16
- export {};
16
+ import type { OrchestratorFaultInjection } from './testing/fault-injection.js';
17
+ /**
18
+ * Boot the Platform-connected orchestrator. Factored out of the module entry so
19
+ * both this file's production entry (which passes no fault injection) and the
20
+ * test-only `server-test.ts` entry (which passes an injected fault-injection
21
+ * policy) run the identical bootstrap through one code path. Exported solely for
22
+ * that single test-only caller; nothing else imports it.
23
+ */
24
+ export declare function runServer(opts?: {
25
+ faultInjection?: OrchestratorFaultInjection;
26
+ }): Promise<void>;
17
27
  //# sourceMappingURL=server.d.ts.map