@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
@@ -1,79 +1,182 @@
1
1
  /**
2
2
  * Warm pool manager for pre-provisioned idle agents.
3
3
  *
4
- * Tracks idle agents per label set, handles idle timeout expiry,
5
- * and triggers replenishment when a warm agent is consumed.
6
- * Uses callback pattern to avoid circular dependencies with ScalerManager.
4
+ * Each configured label set has a target `size`. On every tick the manager asks
5
+ * the agent registry how many agents could already serve a job with those
6
+ * labels, subtracts the spawns it has issued but not yet seen register, and
7
+ * starts the difference. It holds no list of its own: the agent registry is the
8
+ * single source of truth for what is ready, which is also what the dispatcher
9
+ * reads, so the two can never disagree.
10
+ *
11
+ * Uses a callback pattern to avoid circular dependencies with ScalerManager.
7
12
  */
8
13
  export interface WarmPoolCallbacks {
9
14
  /** Request the ScalerManager to spawn a new agent for the given label set */
10
15
  onSpawnRequest: (labelSet: string[], backendName: string) => Promise<void>;
11
16
  /** Request the ScalerManager to destroy an idle agent */
12
17
  onDestroyRequest: (managedId: string, backendName: string) => Promise<void>;
18
+ /**
19
+ * How many registered agents could serve a job with this label set right
20
+ * now. Backed by `AgentRegistry.findAvailable`, which is the same query the
21
+ * dispatcher uses — so the warm pool and the dispatcher can never disagree
22
+ * about what "ready" means.
23
+ *
24
+ * A static agent that matches the labels counts: it removes the need for a
25
+ * warm one just as well as a spawned agent does.
26
+ */
27
+ countAvailable: (labels: string[]) => number;
28
+ /**
29
+ * Agents this backend spawned that match the label set and have never run a
30
+ * job, with the timestamp they registered at. `registeredAt` doubles as
31
+ * "idle since": scaler agents are single-use, so an agent still registered
32
+ * with no active job has been idle since it came up.
33
+ *
34
+ * Scoped to the backend because the result feeds the reaper, and the reaper
35
+ * destroys what it is given. A static agent that happens to match the labels
36
+ * is ready capacity, not this pool's to tear down.
37
+ */
38
+ listIdle: (labels: string[], backendName: string) => Array<{
39
+ agentId: string;
40
+ registeredAt: number;
41
+ }>;
42
+ /**
43
+ * How many more agents this backend may still start before it reaches the
44
+ * scaler's `maxAgents` or the orchestrator's `globalMaxAgents`, whichever
45
+ * binds first. The deficit is clamped to it, so a pool that cannot reach its
46
+ * target stops asking instead of issuing a spawn the caps refuse every tick.
47
+ */
48
+ capacityRemaining: (backendName: string) => number;
49
+ /**
50
+ * Called at the end of each tick, once the reap and the deficit pass have
51
+ * run. The ScalerManager publishes the warm-pool gauges from it, so they
52
+ * follow the pool rather than the scrape.
53
+ */
54
+ onTick?: () => void;
55
+ }
56
+ /** One label set's fill state, as the warm-pool metrics report it. */
57
+ export interface WarmPoolStats {
58
+ /**
59
+ * The pool's identity: its normalized DECLARED label set, which is the
60
+ * `configs` map key.
61
+ *
62
+ * This — not `labels` — is what a per-pool metric dimension keys on. Two
63
+ * pools on one scaler can widen to the SAME query set (one label set
64
+ * declaring the plain platform label the other only gets from the taint), so
65
+ * a dimension built from `labels` collapses them into one series and silently
66
+ * drops a pool's fill state.
67
+ */
68
+ key: string;
69
+ /** The widened query set the `ready` count was measured with. */
70
+ labels: string[];
71
+ backendName: string;
72
+ target: number;
73
+ ready: number;
74
+ inFlight: number;
13
75
  }
14
76
  export declare class WarmPoolManager {
15
77
  private readonly callbacks;
16
- /** Pools of idle agents keyed by normalized label set string */
17
- private readonly pools;
18
78
  /** Configuration per normalized label set */
19
79
  private readonly configs;
20
- /** Periodic idle check interval */
80
+ /**
81
+ * Warm spawns issued but not yet registered, per normalized label set.
82
+ *
83
+ * Load-bearing: without it, two passes landing before the first spawn
84
+ * registers would each see the full deficit and issue `size` spawns apiece.
85
+ */
86
+ private readonly inFlight;
87
+ /** Periodic deficit + idle check interval */
21
88
  private idleCheckInterval;
22
89
  constructor(callbacks: WarmPoolCallbacks);
23
90
  /**
24
91
  * Configure the warm pool for a label set.
25
92
  * Called during ScalerManager initialization based on YAML config.
93
+ *
94
+ * `normalizedLabels` keys the pool by its DECLARED label set, which is what
95
+ * every in-flight release path normalizes back to.
26
96
  */
27
97
  configure(normalizedLabels: string, backendName: string, config: {
28
98
  size: number;
29
99
  idleTimeoutSeconds: number;
30
100
  labels: string[];
101
+ spawnLabels: string[];
31
102
  }): void;
32
103
  /**
33
- * Add an agent to the warm pool.
34
- * Called when a freshly spawned warm pool agent has registered but has no job.
35
- */
36
- addIdleAgent(normalizedLabels: string, managedId: string, backendName: string): void;
37
- /**
38
- * Consume an agent from the warm pool for the given label set.
39
- * Returns the managedId (FIFO order) or null if pool is empty.
40
- * After consuming, schedules replenishment on next tick.
104
+ * One deficit pass. For each configured label set, top the pool up to its
105
+ * target: `size - ready - inFlight`, clamped by the backend's remaining cap
106
+ * headroom.
107
+ *
108
+ * The per-backend budget is tracked across the whole pass: two label sets on
109
+ * one scaler draw from the same `maxAgents`, so clamping each against the
110
+ * same starting headroom would let them jointly overshoot it.
41
111
  */
42
- consumeAgent(normalizedLabels: string): string | null;
112
+ evaluate(): void;
113
+ /** A warm spawn registered: release its in-flight slot. */
114
+ onWarmAgentRegistered(labels: string[]): void;
43
115
  /**
44
- * Check if pool is below configured size and request spawns for the deficit.
116
+ * A warm spawn failed, timed out, or was pruned as never-registered: release
117
+ * its in-flight slot so the next pass retries it. Without this the slot is
118
+ * held forever and the pool sits permanently below target.
45
119
  */
46
- replenish(normalizedLabels: string): void;
120
+ onWarmSpawnFailed(labels: string[]): void;
121
+ private releaseInFlight;
122
+ /** Per-label-set fill state, for the warm-pool metrics. */
123
+ getStats(): WarmPoolStats[];
47
124
  /**
48
- * Start periodic idle check (every 30 seconds).
49
- * Destroys agents that have been idle past their timeout.
125
+ * Start the periodic tick: reap surplus agents past their idle timeout, then
126
+ * top the pools back up.
127
+ *
128
+ * The first pass runs immediately rather than a tick later. Both hosts call
129
+ * this only after `ensureHostsReady()`, so spawning here is safe — and
130
+ * waiting would leave the pool empty for 30 seconds after every restart,
131
+ * which is precisely the cold start it exists to remove.
50
132
  */
51
133
  start(): void;
52
134
  /**
53
- * Stop the periodic idle check interval.
135
+ * Stop the periodic tick.
54
136
  */
55
137
  stop(): void;
56
138
  /**
57
- * Return current warm pool count for a label set.
58
- */
59
- getPoolSize(normalizedLabels: string): number;
60
- /**
61
- * Return total warm agents across all pools.
139
+ * Total ready agents across every configured pool. Surfaced by
140
+ * `getStatus().warmPoolCount`.
62
141
  */
63
142
  getTotalPoolSize(): number;
64
143
  /**
65
- * Update configs. Drain excess agents from pools that shrunk.
66
- * Does NOT remove agents from pools whose config was removed --
67
- * let idle timeout handle that gracefully.
144
+ * Update configs. Destroy surplus agents in pools that shrunk, and drop
145
+ * pools whose config disappeared (the scaler was removed from the config,
146
+ * or its `warmPool.enabled` flipped to false) — a warm agent must not
147
+ * outlive the config that asked for it.
68
148
  */
69
149
  reload(newConfigs: Map<string, {
70
150
  backendName: string;
71
151
  size: number;
72
152
  idleTimeoutSeconds: number;
73
153
  labels: string[];
154
+ spawnLabels: string[];
74
155
  }>): void;
75
156
  /**
76
- * Check for idle agents past their timeout and destroy them.
157
+ * Destroy this pool's idle agents beyond `keep`, oldest first.
158
+ *
159
+ * Oldest-first because the survivors are the ones with the most time left
160
+ * before the idle reaper takes them, which is the set an arriving job is
161
+ * most likely to find still ready.
162
+ */
163
+ private destroyIdle;
164
+ /**
165
+ * Destroy agents that are BOTH surplus to the pool's target AND idle past
166
+ * its timeout. `registeredAt` is the idle-since stamp: scaler agents are
167
+ * single-use, so an agent still registered with no active job has never run
168
+ * one.
169
+ *
170
+ * The target check is what stops the pool churning against itself. Reaping
171
+ * on age alone means a quiet pool destroys its own agent every
172
+ * `idleTimeoutSeconds` and immediately re-spawns it: the provisioning
173
+ * workflow runs forever on zero traffic, and — because an event-backend
174
+ * replacement takes minutes to boot — the pool is below target for much of
175
+ * every cycle. A warm pool that is cold most of the time is worse than none.
176
+ * So the timeout trims surplus only: a pool sitting at `size` never reaps.
177
+ *
178
+ * `size: 0` still drains, because then every ready agent is surplus.
179
+ *
77
180
  * Exposed for testing.
78
181
  */
79
182
  checkIdleTimeouts(): void;
@@ -5,9 +5,11 @@
5
5
  * approval/rejection of security holds. Verifies commenter identity
6
6
  * via the trust policy cache and checks ci_trust:write+ before acting.
7
7
  */
8
- import type { HeldRunStore } from '../contexts/held-runs.js';
9
- import { type IdentityLink, type PermissionLevel } from './trust-resolver.js';
10
- import type { CheckStatusPoster as EngineCheckStatusPoster } from '@kici-dev/engine';
8
+ import type { Kysely } from 'kysely';
9
+ import type { HeldRunStore, ReleaseSignal } from '../contexts/held-runs.js';
10
+ import type { Database, HeldRun } from '../db/types.js';
11
+ import { type IdentityLink, type PermissionLevel } from './identity-link.js';
12
+ import { type ResolveCheckStatusPoster } from '../pipeline/security-hold-check.js';
11
13
  /** Parsed /kici command from a comment body. */
12
14
  interface CommentCommand {
13
15
  action: 'approve' | 'reject';
@@ -19,9 +21,8 @@ export interface HandleApprovalCommentParams {
19
21
  commentBody: string;
20
22
  commenterUsername: string;
21
23
  /**
22
- * Commenter immutable IDP-side numeric id from the webhook event. Used to
23
- * match the identity link by id first, falling back to username during the
24
- * backfill window (see trust-resolver.findIdentityLink).
24
+ * Commenter immutable IDP-side numeric id from the webhook event. The only
25
+ * field an identity link is matched on (see `identity-link.findIdentityLink`).
25
26
  */
26
27
  commenterUserId?: string;
27
28
  provider: string;
@@ -31,11 +32,49 @@ export interface HandleApprovalCommentParams {
31
32
  identityLinks: IdentityLink[];
32
33
  orgMemberPermissions: Map<string, PermissionLevel>;
33
34
  heldRunStore: HeldRunStore;
34
- /** Check status poster for updating GitHub checks after approval/rejection. */
35
- checkStatusPoster?: EngineCheckStatusPoster;
36
- /** Commit SHA for the PR head (needed for check status updates). */
37
- commitSha?: string;
38
- credentials: unknown;
35
+ /**
36
+ * Orchestrator database, for resolving each ended hold's own commit and the
37
+ * other holds still pending on it. Optional so a store-less deployment still
38
+ * processes the command.
39
+ */
40
+ db?: Kysely<Database> | undefined;
41
+ /**
42
+ * Resolve the check poster of the provider bundle serving a routing key, so
43
+ * an ended hold's `KiCI Security` check can be terminalized on the commit the
44
+ * hold's own run acted on — which is not necessarily the PR head at comment
45
+ * time, and posting on the head would create a second check run there while
46
+ * leaving the real one pending.
47
+ */
48
+ resolvePoster?: ResolveCheckStatusPoster;
49
+ /**
50
+ * Resume a job whose security hold was just approved, by re-dispatching it.
51
+ *
52
+ * Approving must RUN the gated work, not merely flip the row and post a green
53
+ * check. The signal is routed through the same `routeRelease` the dashboard
54
+ * applier and the stale detector use, so a job-scoped hold lands here and a
55
+ * workflow-scoped one lands on `onWorkflowRelease`. Optional so an
56
+ * orchestrator without the dispatch wiring degrades to flip-and-report rather
57
+ * than failing the comment.
58
+ */
59
+ onJobRelease?: (signal: ReleaseSignal) => Promise<void>;
60
+ /**
61
+ * Resume a workflow-scoped hold that was just approved, by replaying its
62
+ * stored dispatch context. This is the path the org trust policy's PR-wide
63
+ * hold takes: it fires before any job is materialized, so there is no job to
64
+ * re-dispatch.
65
+ */
66
+ onWorkflowRelease?: (signal: ReleaseSignal) => Promise<void>;
67
+ /**
68
+ * Cancel a workflow-scoped hold that was just rejected, dropping its stored
69
+ * dispatch context. Without it a rejected PR-wide hold leaves its run alive in
70
+ * `held` forever and strands the context row that would have replayed it.
71
+ *
72
+ * It also completes the hold's `KiCI Security` check and resolves to whether
73
+ * it actually wrote one, so this handler reports only the holds no write
74
+ * covered — suppression bound to a check being written, not to a delegate
75
+ * resolving.
76
+ */
77
+ onWorkflowReject?: (hold: HeldRun, reason: string) => Promise<boolean>;
39
78
  }
40
79
  /** Result of handling a comment. */
41
80
  interface HandleApprovalResult {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Identity-link lookup: map a provider identity from a webhook event onto the
3
+ * KiCI user the Platform linked it to.
4
+ *
5
+ * The comment-approval path uses this to decide whether a `/kici approve`
6
+ * commenter is a linked member with a high enough `ci_trust` level.
7
+ */
8
+ import type { CiTrustLevel } from '@kici-dev/engine';
9
+ /**
10
+ * RBAC permission levels from the Platform permission system.
11
+ *
12
+ * The same four values the wire schema carries for `memberCiTrustLevels`, so
13
+ * this aliases {@link CiTrustLevel} rather than restating them — a member's CI
14
+ * trust level IS the permission level this module's callers compare against.
15
+ */
16
+ export type PermissionLevel = CiTrustLevel;
17
+ /** Identity link mapping a provider identity to a KiCI user. */
18
+ export interface IdentityLink {
19
+ userId: string;
20
+ provider: string;
21
+ providerUsername: string;
22
+ /**
23
+ * Immutable IDP-side numeric id (e.g. GitHub's `sender.id`). The only field
24
+ * a link is matched on; a link that carries no id can never match, so it
25
+ * grants nothing.
26
+ */
27
+ providerUserId?: string | null;
28
+ }
29
+ /**
30
+ * The provider username the org's identity directory links a KiCI user id to,
31
+ * or undefined when it names none unambiguously.
32
+ *
33
+ * The reverse of {@link findIdentityLink}: that maps a provider identity onto a
34
+ * KiCI user, this maps a KiCI user back onto a name a human recognises. Used
35
+ * where a decision made in the dashboard / CLI / MCP — which carries only the
36
+ * actor's opaque subject id — has to be attributed in copy a contributor reads
37
+ * on a public commit check.
38
+ *
39
+ * A user may hold links on several providers. When they agree on the username
40
+ * the answer is unambiguous; when they disagree there is no way to tell here
41
+ * which provider serves the commit, so this answers undefined rather than
42
+ * naming the wrong account. Undefined is a safe answer at every call site: the
43
+ * attribution is simply omitted, never replaced by the raw subject id.
44
+ */
45
+ export declare function resolveLinkedUsername(identityLinks: IdentityLink[], userId: string): string | undefined;
46
+ /**
47
+ * Match an identity link strictly by `(provider, providerUserId)`.
48
+ *
49
+ * Returns `null` whenever the numeric id is missing on either side or when no
50
+ * link's id matches the event's id. Matching by username is not supported, so
51
+ * renaming a provider account cannot be used to impersonate a linked member.
52
+ * A refused match is recorded under
53
+ * `kici_orch_trust_match_refused_no_id_total{reason}` so the rate of refusals
54
+ * stays observable.
55
+ *
56
+ * Pre-condition for callers: the Platform has filled `provider_user_id` for
57
+ * every row it pushes in `identity_links` — a row without one is inert here.
58
+ */
59
+ export declare function findIdentityLink(identityLinks: IdentityLink[], provider: string, providerUsername: string, providerUserId: string | undefined): IdentityLink | null;
60
+ //# sourceMappingURL=identity-link.d.ts.map
@@ -4,11 +4,10 @@
4
4
  * For pull-request events, the orchestrator must decide whether to fetch
5
5
  * the workflow lock file from the PR head branch (the contributor's
6
6
  * proposed change) or from the base branch (the protected target). Only
7
- * trusted contributors get their HEAD lock evaluated; everyone else
7
+ * a trusted ref gets its HEAD lock evaluated; everything else
8
8
  * (unknown / known / undefined trust resolution) falls back to the base
9
- * branch's lock so an untrusted contributor cannot inject triggers,
10
- * jobs, or environment claims that the project's maintainers haven't
11
- * vetted.
9
+ * branch's lock so a fork ref cannot inject triggers, jobs, or
10
+ * environment claims that the project's maintainers haven't vetted.
12
11
  *
13
12
  * The default for non-PR events is HEAD, because there is no untrusted
14
13
  * "incoming" contribution to gate against — push events come directly
@@ -19,16 +18,15 @@ import type { TrustTier } from '@kici-dev/engine';
19
18
  * Select which branch's lock file to fetch.
20
19
  *
21
20
  * Invariant (customer-isolation): for any pull-request event, an
22
- * untrusted contributor (`tier === 'unknown' | 'known' | undefined`)
23
- * MUST NOT have their HEAD lock file evaluated by the orchestrator.
24
- * The base-branch lock — controlled by the project's trusted
25
- * maintainers — is the source of truth for trigger evaluation,
26
- * trust-tier-based environment access, and contributor-controlled
27
- * fields that downstream secret resolution depends on.
21
+ * untrusted ref (`tier === 'unknown' | 'known' | undefined`) MUST NOT
22
+ * have its HEAD lock file evaluated by the orchestrator. The
23
+ * base-branch lock — controlled by the project's trusted maintainers —
24
+ * is the source of truth for trigger evaluation, trust-tier-based
25
+ * environment access, and contributor-controlled fields that downstream
26
+ * secret resolution depends on.
28
27
  *
29
- * Only `tier === 'trusted'` (an identity-linked contributor with
30
- * provider write+ permission AND ci_trust write+ permission, per
31
- * `TrustResolver.resolveTrustTier`) can have their HEAD lock evaluated.
28
+ * Only `tier === 'trusted'` (a ref that lives in the base repo, per
29
+ * `resolveRefTrust`) can have its HEAD lock evaluated.
32
30
  */
33
31
  export declare function selectLockFileSource(isPREvent: boolean, tier: TrustTier | undefined): 'head' | 'base';
34
32
  //# sourceMappingURL=lock-source.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The one rendering of a run's reduced-privilege posture, for the provider
3
+ * checks a contributor reads on a pull request.
4
+ *
5
+ * It lives beside `trust-resolver.ts` and `lock-source.ts` — the two modules
6
+ * that decide the values it reads — rather than in the pipeline, because both
7
+ * the dispatch pipeline and the check-run reporter render it and
8
+ * `pipeline/processor.ts` already imports from `reporting/`.
9
+ */
10
+ /**
11
+ * Lead-in of the reduced-privilege note. Exported so every surface that renders
12
+ * it, and their tests, name one string rather than a copy each.
13
+ */
14
+ export declare const REDUCED_PRIVILEGE_MARKER = "**Reduced privileges (untrusted ref).**";
15
+ /**
16
+ * The reduced-privilege posture of a run whose ref resolved to a tier other
17
+ * than `trusted`.
18
+ *
19
+ * Attach it only where the run the check describes actually executes. It is NOT
20
+ * attached to the trust-policy REJECTION: that run is never dispatched, so on
21
+ * that check the clauses below are vacuously true and read as a promise about a
22
+ * run that will not happen. The trust-policy HOLD does carry it — approving one
23
+ * replays the dispatch under the same trust resolution, so the reductions the
24
+ * clauses name are the ones the resumed run really runs under. Storing a resume
25
+ * context is not by itself the trigger: `holdWorkflowForInstallGate` stores one
26
+ * and posts no check at all, so it has nothing to attach the note to. See the
27
+ * call sites in `dispatch-matched-workflow.ts` for which checks a resuming hold
28
+ * actually posts.
29
+ *
30
+ * Returns null for a trusted ref and for a run whose trust never resolved. An
31
+ * absent tier is deliberately NOT treated as untrusted: `isUntrustedTier` in
32
+ * `install-secrets-resolver.ts` reads it leniently and leaves the install
33
+ * secrets in place, so a run with no tier has no withholding to report. The
34
+ * predicate for a resolved tier is "other than `trusted`", matching
35
+ * `isUntrustedTier` and `selectLockFileSource`. `deriveCacheRefScope` is
36
+ * stricter — it isolates an absent tier too — so an unresolved run's cache
37
+ * really is isolated while this note stays silent about it. Under-reporting is
38
+ * the safe direction: the note never claims a reduction that did not happen.
39
+ *
40
+ * Each clause names something already decided for THIS run rather than a
41
+ * mechanism:
42
+ *
43
+ * - `resolveInstallSecrets` returns `npmRegistries: undefined` and
44
+ * `installEnvSecrets: undefined` for a resolved non-trusted tier, so the run
45
+ * carries neither.
46
+ * - `deriveCacheRefScope` returns the isolated scope for the same tiers, and
47
+ * `UserCache` confines WRITES to it; a restore still falls back to the shared
48
+ * scope, so the clause is about what the run saves, not what it can read.
49
+ * - the base-branch clause is keyed on the run's recorded `lockFileSource`, not
50
+ * on the tier: `selectLockFileSource` returns `base` only for a pull-request
51
+ * event, and an untrusted tier also reaches an internal-event child run that
52
+ * inherited it.
53
+ *
54
+ * Kept in step with the dashboard's reduced-privilege banner
55
+ * (`packages/dashboard/src/components/run-detail/degraded-run-banner.tsx`),
56
+ * which describes the same run and may be read beside these checks.
57
+ */
58
+ export declare function buildReducedPrivilegeNote(tier: string | null | undefined, lockFileSource?: string | null): string | null;
59
+ //# sourceMappingURL=reduced-privilege-note.d.ts.map