@kici-dev/orchestrator 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__test-helpers__/fake-scaler-state-store.d.ts +24 -0
- package/dist/__test-helpers__/mock-db-query.d.ts +130 -0
- package/dist/__test-helpers__/mock-db.d.ts +27 -0
- package/dist/agent/agent-version.d.ts +34 -0
- package/dist/agent/dispatcher.d.ts +151 -13
- package/dist/agent/host-roster.d.ts +37 -5
- package/dist/agent/registry.d.ts +2 -0
- package/dist/app.d.ts +56 -7
- package/dist/approvals/apply-decision.d.ts +67 -19
- package/dist/approvals/held-run-release-wiring.d.ts +45 -0
- package/dist/approvals/independent-wiring.d.ts +55 -0
- package/dist/approvals/resume-router.d.ts +9 -0
- package/dist/approvals/triggerer-subject.d.ts +37 -0
- package/dist/cache/dep-cache.d.ts +54 -10
- package/dist/cache/global-eval-round-cache.d.ts +95 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/pending-global-evals.d.ts +42 -0
- package/dist/cache/pending-inits.d.ts +10 -0
- package/dist/cli/api-client.d.ts +18 -0
- package/dist/cli/commands/cluster-settings.d.ts +52 -3
- package/dist/cli/commands/diagnose.d.ts +6 -0
- package/dist/cli/commands/held-run.d.ts +58 -0
- package/dist/cli/commands/runs.d.ts +1 -0
- package/dist/cli/commands/trust-policy.d.ts +117 -12
- package/dist/cli/kici-admin.d.ts +10 -0
- package/dist/cli.js +2770 -938
- package/dist/cluster/cluster-identity.d.ts +3 -3
- package/dist/cluster/cluster-settings-reader.d.ts +59 -1
- package/dist/cluster/coordinator.d.ts +44 -0
- package/dist/cluster/index.d.ts +4 -0
- package/dist/cluster/peer-handler.d.ts +19 -0
- package/dist/cluster/peer-registry.d.ts +17 -0
- package/dist/cluster/plan-headroom-store.d.ts +26 -0
- package/dist/cluster/worker-eviction.d.ts +69 -0
- package/dist/cold-store/load-event-log-range.d.ts +10 -3
- package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
- package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
- package/dist/config.d.ts +48 -21
- package/dist/content-requirements-cache.d.ts +55 -0
- package/dist/contexts/held-runs.d.ts +123 -16
- package/dist/contexts/protection/aggregate.d.ts +3 -2
- package/dist/contexts/protection/branch-gate.d.ts +15 -0
- package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
- package/dist/contexts/protection/pipeline.d.ts +18 -0
- package/dist/contexts/release-queued-holds.d.ts +35 -0
- package/dist/dashboard/attestation-filters.d.ts +8 -0
- package/dist/dashboard/handler.d.ts +6 -24
- package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
- package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
- package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
- package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
- package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
- package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
- package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
- package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
- package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
- package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
- package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
- package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
- package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
- package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
- package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
- package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
- package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
- package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
- package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
- package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
- package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
- package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
- package/dist/db/types.d.ts +428 -12
- package/dist/entry-helpers.d.ts +28 -0
- package/dist/events/event-emitter.d.ts +22 -0
- package/dist/events/event-router.d.ts +87 -1
- package/dist/events/types.d.ts +2 -23
- package/dist/git/credential-broker.d.ts +94 -0
- package/dist/git/inherited-secret.d.ts +24 -0
- package/dist/git/installation-token.d.ts +38 -0
- package/dist/git/job-context.d.ts +13 -0
- package/dist/index.js +324 -84
- package/dist/metrics/agent-metrics-aggregator.d.ts +4 -4
- package/dist/metrics/prometheus.d.ts +170 -3
- package/dist/oidc/oidc-mint-registration.d.ts +7 -3
- package/dist/orchestrator-core.d.ts +91 -24
- package/dist/pipeline/content-filter.d.ts +71 -0
- package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +521 -11
- package/dist/pipeline/global-eval-round.d.ts +307 -0
- package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
- package/dist/pipeline/invoke-gate.d.ts +164 -0
- package/dist/pipeline/job-contexts.d.ts +16 -17
- package/dist/pipeline/needs-scheduler.d.ts +13 -0
- package/dist/pipeline/process-webhook.d.ts +126 -31
- package/dist/pipeline/processor.d.ts +282 -23
- package/dist/pipeline/rerun.d.ts +21 -0
- package/dist/pipeline/resume-workflow.d.ts +40 -7
- package/dist/pipeline/route-or-dispatch-jobs.d.ts +15 -0
- package/dist/pipeline/security-hold-check.d.ts +217 -0
- package/dist/pipeline/test-pipeline.d.ts +12 -0
- package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
- package/dist/pipeline/webhook-payload-store.d.ts +20 -0
- package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
- package/dist/policy/dashboard-write-policy.d.ts +14 -0
- package/dist/provenance/backfill-run.d.ts +10 -1
- package/dist/provider-registry.d.ts +47 -4
- package/dist/providers/github/check-status-poster.d.ts +33 -3
- package/dist/providers/github/commit-message.d.ts +20 -0
- package/dist/providers/github/file-contents.d.ts +40 -0
- package/dist/providers/github/index.d.ts +2 -2
- package/dist/providers/github/normalizer.d.ts +3 -2
- package/dist/providers/local/normalizer.d.ts +3 -5
- package/dist/providers/universal-git/config.d.ts +2 -0
- package/dist/providers/universal-git/index.d.ts +11 -2
- package/dist/providers/universal-git/normalizer.d.ts +10 -0
- package/dist/queue/cleanup.d.ts +7 -1
- package/dist/queue/job-queue.d.ts +112 -7
- package/dist/queue/terminalize-unroutable.d.ts +13 -0
- package/dist/registration/extractor.d.ts +3 -3
- package/dist/registration/registration-index.d.ts +7 -0
- package/dist/registration/registration-run-match.d.ts +47 -0
- package/dist/registration/registration-store.d.ts +22 -0
- package/dist/reporting/check-run-reporter.d.ts +146 -2
- package/dist/reporting/check-run-summary.d.ts +25 -1
- package/dist/reporting/execution-tracker.d.ts +324 -9
- package/dist/reporting/log-chunk-sink.d.ts +8 -5
- package/dist/reporting/run-aggregator.d.ts +4 -14
- package/dist/reporting/step-display-order.d.ts +43 -0
- package/dist/routes/admin-held-runs.d.ts +142 -0
- package/dist/routes/admin-org-settings.d.ts +5 -0
- package/dist/routes/admin-trust-policy.d.ts +34 -6
- package/dist/routes/admin.d.ts +15 -0
- package/dist/scaler/backend-factory.d.ts +55 -0
- package/dist/scaler/bare-metal-backend.d.ts +43 -5
- package/dist/scaler/claim-store.d.ts +119 -0
- package/dist/scaler/config.d.ts +4 -0
- package/dist/scaler/container-backend.d.ts +18 -4
- package/dist/scaler/container-routing.d.ts +23 -0
- package/dist/scaler/container-spawn.d.ts +28 -0
- package/dist/scaler/event-backend.d.ts +124 -0
- package/dist/scaler/event-provision-reaper.d.ts +350 -0
- package/dist/scaler/failure-tracker.d.ts +1 -1
- package/dist/scaler/firecracker-backend.d.ts +33 -6
- package/dist/scaler/index.d.ts +11 -1
- package/dist/scaler/label-matcher.d.ts +4 -3
- package/dist/scaler/manager.d.ts +905 -76
- package/dist/scaler/resolve-container-auth.d.ts +45 -0
- package/dist/scaler/scaler-events.d.ts +11 -0
- package/dist/scaler/scaler-state-store.d.ts +294 -10
- package/dist/scaler/types.d.ts +131 -13
- package/dist/scaler/warm-pool.d.ts +132 -29
- package/dist/security/comment-handler.d.ts +50 -11
- package/dist/security/global-workflow-policy.d.ts +52 -12
- package/dist/security/identity-link.d.ts +60 -0
- package/dist/security/lock-source.d.ts +11 -13
- package/dist/security/reduced-privilege-note.d.ts +59 -0
- package/dist/security/trust-directory-store.d.ts +233 -0
- package/dist/security/trust-policy-gate.d.ts +94 -59
- package/dist/security/trust-policy-store.d.ts +3 -0
- package/dist/security/trust-resolver.d.ts +14 -98
- package/dist/server.d.ts +11 -1
- package/dist/server.js +56329 -42234
- package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
- package/dist/stale-detector/stale-run-detector.d.ts +61 -3
- package/dist/standalone.js +37619 -23570
- package/dist/webhook/ingest-accept.d.ts +70 -0
- package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
- package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
- package/dist/worker/in-memory-job-queue.d.ts +1 -1
- package/dist/ws/agent-handler.d.ts +31 -4
- package/dist/ws/dashboard-context-handler.d.ts +28 -4
- package/dist/ws/dashboard-global-workflows-handler.d.ts +36 -9
- package/dist/ws/execution-status-frame.d.ts +32 -0
- package/dist/ws/failure-messages.d.ts +2 -0
- package/dist/ws/git-credential-relay.d.ts +39 -0
- package/dist/ws/oidc-token-relay.d.ts +6 -11
- package/dist/ws/platform-client.d.ts +68 -9
- package/dist/ws/test-relay-handlers.d.ts +35 -10
- package/installer-image-digests.json +3 -3
- package/package.json +24 -19
- package/sbom.spdx.json +868 -821
- package/dist/approvals/team-membership-lookup.d.ts +0 -13
- package/dist/pipeline/inline-eval.d.ts +0 -44
- package/dist/providers/github/contributor-resolver.d.ts +0 -30
- 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
|
|
3
|
-
*
|
|
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
|
|
7
|
-
*
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
26
|
+
* The enforcement vocabulary the admin API reports and `kici-admin
|
|
27
|
+
* trust-policy show` renders.
|
|
20
28
|
*
|
|
21
|
-
* `policy`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
39
|
+
/** The per-PR facts the fork switch is evaluated against. */
|
|
33
40
|
export interface TrustPolicySignals {
|
|
34
|
-
/** Resolved contributor tier; undefined when
|
|
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
|
|
47
|
+
* The reason the ORG TRUST POLICY itself raises.
|
|
43
48
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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 =
|
|
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
|
-
*
|
|
60
|
-
* that produced this verdict.
|
|
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
|
|
67
|
-
*
|
|
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
|
-
|
|
86
|
+
approvalExpirySeconds: number | null;
|
|
70
87
|
} | {
|
|
71
88
|
action: 'reject';
|
|
72
89
|
reason: TrustPolicyHoldReason;
|
|
73
90
|
message: string;
|
|
74
91
|
};
|
|
75
92
|
/**
|
|
76
|
-
* The
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
*
|
|
97
|
-
*
|
|
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
|
|
129
|
+
export declare const READ_FAILURE_POLICY: TrustPolicy;
|
|
100
130
|
/**
|
|
101
|
-
*
|
|
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
|
-
*
|
|
110
|
-
*
|
|
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 `
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
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
|
-
*
|
|
3
|
-
* into a trust tier decision.
|
|
2
|
+
* Ref-based trust: may this git ref reach shared state and real secrets?
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
31
|
-
/**
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|