@kici-dev/orchestrator 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +23 -0
- package/dist/agent/dispatcher.d.ts +81 -13
- package/dist/agent/host-roster.d.ts +37 -5
- package/dist/agent/registry.d.ts +2 -0
- package/dist/app.d.ts +33 -6
- 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 +7 -0
- package/dist/cli/api-client.d.ts +18 -0
- package/dist/cli/commands/cluster-settings.d.ts +11 -0
- package/dist/cli/commands/diagnose.d.ts +6 -0
- package/dist/cli/commands/held-run.d.ts +58 -0
- package/dist/cli/commands/trust-policy.d.ts +117 -12
- package/dist/cli/kici-admin.d.ts +10 -0
- package/dist/cli.js +2272 -1025
- package/dist/cluster/cluster-identity.d.ts +3 -3
- package/dist/cluster/cluster-settings-reader.d.ts +7 -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 +21 -21
- 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/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 +368 -10
- 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 +2 -2
- package/dist/metrics/prometheus.d.ts +111 -3
- package/dist/oidc/oidc-mint-registration.d.ts +7 -3
- package/dist/orchestrator-core.d.ts +79 -23
- package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +302 -31
- package/dist/pipeline/global-eval-round.d.ts +14 -0
- package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
- package/dist/pipeline/invoke-gate.d.ts +164 -0
- package/dist/pipeline/needs-scheduler.d.ts +13 -0
- package/dist/pipeline/process-webhook.d.ts +119 -31
- package/dist/pipeline/processor.d.ts +226 -21
- 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 +9 -0
- package/dist/pipeline/security-hold-check.d.ts +217 -0
- package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
- package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
- package/dist/policy/dashboard-write-policy.d.ts +14 -0
- package/dist/provider-registry.d.ts +10 -2
- package/dist/providers/github/check-status-poster.d.ts +11 -0
- package/dist/providers/github/index.d.ts +0 -2
- package/dist/providers/github/normalizer.d.ts +3 -2
- package/dist/providers/local/normalizer.d.ts +3 -5
- package/dist/providers/universal-git/index.d.ts +11 -2
- package/dist/queue/job-queue.d.ts +43 -1
- package/dist/registration/extractor.d.ts +3 -3
- package/dist/registration/registration-index.d.ts +7 -0
- package/dist/registration/registration-store.d.ts +22 -0
- package/dist/reporting/check-run-reporter.d.ts +94 -1
- package/dist/reporting/check-run-summary.d.ts +25 -1
- package/dist/reporting/execution-tracker.d.ts +207 -2
- 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-trust-policy.d.ts +34 -6
- package/dist/routes/admin.d.ts +9 -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 +899 -72
- 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/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 +17684 -7351
- 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 +24238 -13822
- package/dist/worker/in-memory-job-queue.d.ts +1 -1
- package/dist/ws/agent-handler.d.ts +28 -4
- package/dist/ws/dashboard-context-handler.d.ts +28 -4
- package/dist/ws/dashboard-global-workflows-handler.d.ts +6 -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 +54 -9
- package/installer-image-digests.json +3 -3
- package/package.json +24 -19
- package/sbom.spdx.json +864 -822
- package/dist/approvals/team-membership-lookup.d.ts +0 -13
- package/dist/providers/github/contributor-resolver.d.ts +0 -30
- package/dist/security/contributor-cache.d.ts +0 -83
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
|
11
11
|
import { Kysely, PostgresDialect, sql } from "kysely";
|
|
12
12
|
import pg from "pg";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
-
import { DEFAULT_APPROVAL_EXPIRY_HOURS, HoldScope, HoldType, TriggerSource, persistedHoldTypeSpellings } from "@kici-dev/engine";
|
|
14
|
+
import { DEFAULT_APPROVAL_EXPIRY_HOURS, DEFAULT_APPROVAL_EXPIRY_SECONDS, ForkPolicy, HoldScope, HoldType, TriggerSource, approvalExpiryHoursOf, approvalExpirySecondsOf, persistedHoldTypeSpellings } from "@kici-dev/engine";
|
|
15
15
|
import { platform, tmpdir } from "node:os";
|
|
16
16
|
import { createInterface } from "node:readline";
|
|
17
17
|
import { Octokit } from "@octokit/rest";
|
|
@@ -302,7 +302,7 @@ var S3CacheStorage = class {
|
|
|
302
302
|
* restart — fine for the filesystem backend's E2E / single-host use case.
|
|
303
303
|
*/
|
|
304
304
|
/** Default URL lifetime: 1 hour (matches S3 backend's PUT-URL lifetime). */
|
|
305
|
-
const DEFAULT_SIGN_URL_TTL_MS =
|
|
305
|
+
const DEFAULT_SIGN_URL_TTL_MS = 36e5;
|
|
306
306
|
function computeSig(secret, method, key, expiresMs) {
|
|
307
307
|
const hmac = createHmac("sha256", secret);
|
|
308
308
|
hmac.update(`${method}:${key}:${expiresMs}`);
|
|
@@ -668,11 +668,13 @@ var SourceCache = class {
|
|
|
668
668
|
/**
|
|
669
669
|
* Dependency-specific cache layer wrapping CacheStorage.
|
|
670
670
|
*
|
|
671
|
-
* Stores
|
|
672
|
-
*
|
|
673
|
-
* Refreshes TTL on reads
|
|
671
|
+
* Stores dependency tarballs under their own content hash, with a small pointer
|
|
672
|
+
* resolving lockfileHash + platform + arch to that hash. Shared CacheStorage
|
|
673
|
+
* backend with SourceCache (same S3 bucket). Refreshes TTL on reads
|
|
674
|
+
* (touch-on-read).
|
|
674
675
|
*
|
|
675
|
-
*
|
|
676
|
+
* Keys: `deps/{platform}-{arch}/{depsHash}.tar.gz` (immutable) and
|
|
677
|
+
* `deps/{platform}-{arch}/{lockfileHash}.hash` (the pointer).
|
|
676
678
|
*/
|
|
677
679
|
const logger$2 = createLogger({ prefix: "dep-cache" });
|
|
678
680
|
/** Default max tarball size: 500MB */
|
|
@@ -680,11 +682,35 @@ const DEFAULT_MAX_TARBALL_BYTES = 524288e3;
|
|
|
680
682
|
/** Default dependency-cache entry TTL: 30 days. */
|
|
681
683
|
const DEFAULT_CACHE_TTL_DAYS = 30;
|
|
682
684
|
const MS_PER_DAY = 864e5;
|
|
683
|
-
/**
|
|
684
|
-
|
|
685
|
-
|
|
685
|
+
/**
|
|
686
|
+
* Cache key for a dependency tarball, addressed by the tarball's OWN content
|
|
687
|
+
* hash: `deps/{platform}-{arch}/{depsHash}.tar.gz`.
|
|
688
|
+
*
|
|
689
|
+
* Content-addressing is what makes a mismatched pair unrepresentable. When the
|
|
690
|
+
* tarball lived at a lockfile-derived name, it and its companion `.hash` were
|
|
691
|
+
* two independently-written objects under keys that any two builds sharing a
|
|
692
|
+
* lockfile + platform + arch also share — so concurrent builders could leave the
|
|
693
|
+
* `.tar.gz` from one and the `.hash` from the other, and the reader failed
|
|
694
|
+
* verification durably. Naming the object by its own hash means every pair a
|
|
695
|
+
* reader can observe is self-consistent, and the bytes at a given key can never
|
|
696
|
+
* change after a URL is signed for it.
|
|
697
|
+
*/
|
|
698
|
+
function depTarballKey(depsHash, platform, arch) {
|
|
699
|
+
return `deps/${platform}-${arch}/${depsHash}.tar.gz`;
|
|
686
700
|
}
|
|
687
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Cache key for the pointer that resolves a lockfile to the content hash of the
|
|
703
|
+
* tarball built from it: `deps/{platform}-{arch}/{lockfileHash}.hash`.
|
|
704
|
+
*
|
|
705
|
+
* This is the one mutable object in the scheme. A concurrent write replaces a
|
|
706
|
+
* pointer wholesale — there is no window in which it is half-written — so the
|
|
707
|
+
* worst a racing pair of builders can do is leave whichever pointer landed last,
|
|
708
|
+
* and both tarballs remain valid and fetchable.
|
|
709
|
+
*/
|
|
710
|
+
function depPointerKey(lockfileHash, platform, arch) {
|
|
711
|
+
return `deps/${platform}-${arch}/${lockfileHash}.hash`;
|
|
712
|
+
}
|
|
713
|
+
var DepCache = class DepCache {
|
|
688
714
|
storage;
|
|
689
715
|
maxTarballBytes;
|
|
690
716
|
cacheTtlDaysFallback;
|
|
@@ -706,10 +732,30 @@ var DepCache = class {
|
|
|
706
732
|
if (!this.clusterSettings) return void 0;
|
|
707
733
|
return await this.clusterSettings.getNumber("cache_ttl_days", this.cacheTtlDaysFallback) * MS_PER_DAY;
|
|
708
734
|
}
|
|
709
|
-
/**
|
|
735
|
+
/**
|
|
736
|
+
* Resolve a lockfile to the content hash of the tarball built from it, or
|
|
737
|
+
* null when no pointer exists. The single place the indirection is read.
|
|
738
|
+
*/
|
|
739
|
+
async resolvePointer(lockfileHash, platform, arch, ttlMsOverride) {
|
|
740
|
+
return (await this.storage.get(depPointerKey(lockfileHash, platform, arch), ttlMsOverride))?.toString("utf-8").trim() || null;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Check if a dep tarball exists in cache.
|
|
744
|
+
*
|
|
745
|
+
* Both halves must be present: a pointer whose tarball has aged out is a miss,
|
|
746
|
+
* not a hit, or the caller skips a rebuild and dispatches a URL that 404s.
|
|
747
|
+
*/
|
|
710
748
|
async has(lockfileHash, platform, arch) {
|
|
711
|
-
const
|
|
712
|
-
const
|
|
749
|
+
const ttlMsOverride = await this.resolveTtlMsOverride();
|
|
750
|
+
const depsHash = await this.resolvePointer(lockfileHash, platform, arch, ttlMsOverride);
|
|
751
|
+
if (!depsHash) {
|
|
752
|
+
logger$2.debug(`has(${lockfileHash}): false (no pointer)`, {
|
|
753
|
+
platform,
|
|
754
|
+
arch
|
|
755
|
+
});
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
const exists = await this.storage.has(depTarballKey(depsHash, platform, arch), ttlMsOverride);
|
|
713
759
|
logger$2.debug(`has(${lockfileHash}): ${exists}`, {
|
|
714
760
|
platform,
|
|
715
761
|
arch
|
|
@@ -721,42 +767,40 @@ var DepCache = class {
|
|
|
721
767
|
* Refreshes TTL on hit.
|
|
722
768
|
*/
|
|
723
769
|
async getUrl(lockfileHash, platform, arch) {
|
|
724
|
-
|
|
725
|
-
const url = await this.storage.getUrl(key, await this.resolveTtlMsOverride());
|
|
726
|
-
if (url) {
|
|
727
|
-
await this.storage.touch(key);
|
|
728
|
-
logger$2.debug(`getUrl(${lockfileHash}): hit`, {
|
|
729
|
-
platform,
|
|
730
|
-
arch
|
|
731
|
-
});
|
|
732
|
-
} else logger$2.debug(`getUrl(${lockfileHash}): miss`, {
|
|
733
|
-
platform,
|
|
734
|
-
arch
|
|
735
|
-
});
|
|
736
|
-
return url;
|
|
770
|
+
return (await this.getUrlAndHash(lockfileHash, platform, arch))?.url ?? null;
|
|
737
771
|
}
|
|
738
772
|
/**
|
|
739
|
-
* Get a pre-signed download URL and the tarball content hash
|
|
740
|
-
*
|
|
741
|
-
*
|
|
773
|
+
* Get a pre-signed download URL and the tarball content hash.
|
|
774
|
+
*
|
|
775
|
+
* Returns null on cache miss — including an entry with no pointer, which is
|
|
776
|
+
* unverifiable and so is deliberately not served.
|
|
742
777
|
*/
|
|
743
778
|
async getUrlAndHash(lockfileHash, platform, arch) {
|
|
744
|
-
const
|
|
745
|
-
const
|
|
779
|
+
const ttlMsOverride = await this.resolveTtlMsOverride();
|
|
780
|
+
const pointerKey = depPointerKey(lockfileHash, platform, arch);
|
|
781
|
+
const hash = (await this.storage.get(pointerKey, ttlMsOverride))?.toString("utf-8").trim() || void 0;
|
|
782
|
+
if (!hash) {
|
|
783
|
+
logger$2.debug(`getUrlAndHash(${lockfileHash}): miss (no pointer)`, {
|
|
784
|
+
platform,
|
|
785
|
+
arch
|
|
786
|
+
});
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
const key = depTarballKey(hash, platform, arch);
|
|
790
|
+
const url = await this.storage.getUrl(key, ttlMsOverride);
|
|
746
791
|
if (!url) {
|
|
747
|
-
logger$2.debug(`getUrlAndHash(${lockfileHash}): miss`, {
|
|
792
|
+
logger$2.debug(`getUrlAndHash(${lockfileHash}): miss (dangling pointer)`, {
|
|
748
793
|
platform,
|
|
749
794
|
arch
|
|
750
795
|
});
|
|
751
796
|
return null;
|
|
752
797
|
}
|
|
753
798
|
await this.storage.touch(key);
|
|
754
|
-
|
|
755
|
-
const hash = (await this.storage.get(hashKey))?.toString("utf-8") || void 0;
|
|
799
|
+
await this.storage.touch(pointerKey);
|
|
756
800
|
logger$2.debug(`getUrlAndHash(${lockfileHash}): hit`, {
|
|
757
801
|
platform,
|
|
758
802
|
arch,
|
|
759
|
-
hasHash:
|
|
803
|
+
hasHash: true
|
|
760
804
|
});
|
|
761
805
|
return {
|
|
762
806
|
url,
|
|
@@ -766,9 +810,21 @@ var DepCache = class {
|
|
|
766
810
|
/**
|
|
767
811
|
* Get a pre-signed upload URL for direct agent-to-S3 upload.
|
|
768
812
|
*/
|
|
769
|
-
async getUploadUrl(
|
|
770
|
-
|
|
771
|
-
|
|
813
|
+
async getUploadUrl(depsHash, platform, arch) {
|
|
814
|
+
return this.storage.getUploadUrl(depTarballKey(depsHash, platform, arch));
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Publish the pointer that makes an uploaded tarball discoverable by lockfile.
|
|
818
|
+
*
|
|
819
|
+
* Called only after the agent confirms its upload completed. Publishing before
|
|
820
|
+
* the bytes land would let a reader follow the pointer to a missing object.
|
|
821
|
+
*/
|
|
822
|
+
async publishPointer(lockfileHash, platform, arch, depsHash) {
|
|
823
|
+
await this.storage.put(depPointerKey(lockfileHash, platform, arch), depsHash);
|
|
824
|
+
logger$2.info(`publishPointer(${lockfileHash}) -> ${depsHash.slice(0, 12)}`, {
|
|
825
|
+
platform,
|
|
826
|
+
arch
|
|
827
|
+
});
|
|
772
828
|
}
|
|
773
829
|
/**
|
|
774
830
|
* Store a dep tarball in cache.
|
|
@@ -777,12 +833,14 @@ var DepCache = class {
|
|
|
777
833
|
async store(lockfileHash, platform, arch, tarballData) {
|
|
778
834
|
const maxTarballBytes = this.clusterSettings ? await this.clusterSettings.getNumber("cache_max_tarball_bytes", this.maxTarballBytes) : this.maxTarballBytes;
|
|
779
835
|
if (tarballData.length > maxTarballBytes) throw new Error(`Dep tarball exceeds max size: ${tarballData.length} bytes > ${maxTarballBytes} bytes limit`);
|
|
780
|
-
const
|
|
781
|
-
await this.storage.put(
|
|
836
|
+
const depsHash = DepCache.computeHash(tarballData);
|
|
837
|
+
await this.storage.put(depTarballKey(depsHash, platform, arch), tarballData);
|
|
838
|
+
await this.storage.put(depPointerKey(lockfileHash, platform, arch), depsHash);
|
|
782
839
|
logger$2.info(`store: ${tarballData.length} bytes`, {
|
|
783
840
|
lockfileHash,
|
|
784
841
|
platform,
|
|
785
|
-
arch
|
|
842
|
+
arch,
|
|
843
|
+
depsHash: depsHash.slice(0, 12)
|
|
786
844
|
});
|
|
787
845
|
}
|
|
788
846
|
/**
|
|
@@ -792,10 +850,11 @@ var DepCache = class {
|
|
|
792
850
|
static computeHash(data) {
|
|
793
851
|
return sha256(data);
|
|
794
852
|
}
|
|
795
|
-
/** Remove a dep tarball from cache. */
|
|
853
|
+
/** Remove a dep tarball and its pointer from cache. */
|
|
796
854
|
async remove(lockfileHash, platform, arch) {
|
|
797
|
-
const
|
|
798
|
-
|
|
855
|
+
const depsHash = await this.resolvePointer(lockfileHash, platform, arch, await this.resolveTtlMsOverride());
|
|
856
|
+
await this.storage.delete(depPointerKey(lockfileHash, platform, arch));
|
|
857
|
+
const removed = depsHash ? await this.storage.delete(depTarballKey(depsHash, platform, arch)) : false;
|
|
799
858
|
logger$2.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
|
|
800
859
|
platform,
|
|
801
860
|
arch
|
|
@@ -846,13 +905,13 @@ const logger$1 = createLogger({ prefix: "user-cache" });
|
|
|
846
905
|
* per-org override in `org_settings.user_cache_quota_bytes`. The cluster-wide
|
|
847
906
|
* value is itself operator-configurable via KICI_USER_CACHE_QUOTA_BYTES.
|
|
848
907
|
*/
|
|
849
|
-
const DEFAULT_USER_CACHE_QUOTA_BYTES =
|
|
908
|
+
const DEFAULT_USER_CACHE_QUOTA_BYTES = 5368709120;
|
|
850
909
|
/**
|
|
851
910
|
* Cluster-wide default entry TTL: 7 days. Fallback when an org has no per-org
|
|
852
911
|
* override in `org_settings.user_cache_ttl_ms`. The cluster-wide value is
|
|
853
912
|
* operator-configurable via KICI_USER_CACHE_TTL_MS.
|
|
854
913
|
*/
|
|
855
|
-
const DEFAULT_USER_CACHE_TTL_MS =
|
|
914
|
+
const DEFAULT_USER_CACHE_TTL_MS = 6048e5;
|
|
856
915
|
/** Tarball suffix for committed cache entries. */
|
|
857
916
|
const TAR_SUFFIX = ".tar.gz";
|
|
858
917
|
/** Matches a stem ending in the fixed-width discriminator this module appends. */
|
|
@@ -1280,11 +1339,13 @@ function mapRow(row) {
|
|
|
1280
1339
|
* @param data - Credential data to persist
|
|
1281
1340
|
*/
|
|
1282
1341
|
async function writeCredentialFile(filePath, data) {
|
|
1283
|
-
|
|
1342
|
+
const dir = dirname(filePath);
|
|
1343
|
+
await mkdir(dir, {
|
|
1284
1344
|
recursive: true,
|
|
1285
1345
|
mode: 448
|
|
1286
1346
|
});
|
|
1287
|
-
|
|
1347
|
+
const json = JSON.stringify(data, null, 2) + "\n";
|
|
1348
|
+
await writeFile(filePath, json, { mode: 384 });
|
|
1288
1349
|
await chmod(filePath, 384);
|
|
1289
1350
|
}
|
|
1290
1351
|
/**
|
|
@@ -1312,10 +1373,11 @@ async function readCredentialFile(filePath) {
|
|
|
1312
1373
|
* shared with the orchestrator.
|
|
1313
1374
|
*/
|
|
1314
1375
|
async function createPeerCredentialStoreFromUrl(databaseUrl, opts) {
|
|
1315
|
-
const
|
|
1376
|
+
const pool = new pg.Pool({
|
|
1316
1377
|
connectionString: databaseUrl,
|
|
1317
1378
|
max: opts?.maxConnections ?? 3
|
|
1318
|
-
})
|
|
1379
|
+
});
|
|
1380
|
+
const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
|
|
1319
1381
|
return {
|
|
1320
1382
|
store: new PeerCredentialStore(db),
|
|
1321
1383
|
dispose: async () => {
|
|
@@ -1586,10 +1648,11 @@ var JoinTokenManager = class {
|
|
|
1586
1648
|
* the real cluster DB.
|
|
1587
1649
|
*/
|
|
1588
1650
|
function createJoinTokenManagerFromUrl(databaseUrl, opts) {
|
|
1589
|
-
const
|
|
1651
|
+
const pool = new pg.Pool({
|
|
1590
1652
|
connectionString: databaseUrl,
|
|
1591
1653
|
max: opts?.maxConnections ?? 3
|
|
1592
|
-
})
|
|
1654
|
+
});
|
|
1655
|
+
const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
|
|
1593
1656
|
return {
|
|
1594
1657
|
manager: new JoinTokenManager({ db }),
|
|
1595
1658
|
dispose: async () => {
|
|
@@ -1626,7 +1689,25 @@ function deriveKeys(secret) {
|
|
|
1626
1689
|
}
|
|
1627
1690
|
/**
|
|
1628
1691
|
* Reason a run was held in the security queue. Persisted verbatim in
|
|
1629
|
-
* `held_runs.reason` and
|
|
1692
|
+
* `held_runs.reason` and whose vocabulary `buildSecurityHoldSummary` switches
|
|
1693
|
+
* on.
|
|
1694
|
+
*
|
|
1695
|
+
* Each value names why a run sits in the queue. The enum stays whole because
|
|
1696
|
+
* compiling code still names all four: `buildSecurityHoldSummary` renders one
|
|
1697
|
+
* branch per value. Dropping a member breaks that, and breaks the parity test
|
|
1698
|
+
* that pins `SECURITY_HOLD_JOB_IDS`' three policy-reason keys to this enum's
|
|
1699
|
+
* options — the engine constant cannot import the enum, so that coupling lives
|
|
1700
|
+
* only in the orchestrator's test. Nothing validates `held_runs.reason` against
|
|
1701
|
+
* this enum, so a stored value is not rejected anywhere.
|
|
1702
|
+
*
|
|
1703
|
+
* - `fork_pr` — the org trust policy's fork switch held the run.
|
|
1704
|
+
* - `context_trust` — a context's minimum-trust gate held the run.
|
|
1705
|
+
* - `workflow_modification` — deprecated; no longer raised. Modifications to
|
|
1706
|
+
* `.kici/` are surfaced on their own informational check and no longer feed
|
|
1707
|
+
* a policy arm. Removed at v1.0.0.
|
|
1708
|
+
* - `unknown_contributor` — deprecated; no longer raised. The policy turns on
|
|
1709
|
+
* whether the pull request came from a fork, not on who opened it. Removed
|
|
1710
|
+
* at v1.0.0.
|
|
1630
1711
|
*/
|
|
1631
1712
|
const SecurityHoldReason = z.enum([
|
|
1632
1713
|
"workflow_modification",
|
|
@@ -1640,9 +1721,19 @@ var HeldRunStore = class {
|
|
|
1640
1721
|
constructor(db) {
|
|
1641
1722
|
this.db = db;
|
|
1642
1723
|
}
|
|
1643
|
-
/**
|
|
1644
|
-
|
|
1645
|
-
|
|
1724
|
+
/**
|
|
1725
|
+
* Create a new held run with pending status.
|
|
1726
|
+
*
|
|
1727
|
+
* `exec` is the executor the INSERT runs through, defaulting to the store's
|
|
1728
|
+
* own connection. A caller that writes something the row cannot exist without
|
|
1729
|
+
* — the job's pending dispatch context, without which the hold can never be
|
|
1730
|
+
* resumed — passes its enclosing transaction, so the two land or roll back
|
|
1731
|
+
* together. Handed in rather than taken from an ambient scope: Kysely has no
|
|
1732
|
+
* such scope, so a `this.db` insert inside a `db.transaction()` callback runs
|
|
1733
|
+
* on a different connection and commits on its own.
|
|
1734
|
+
*/
|
|
1735
|
+
async create(orgId, data, exec = this.db) {
|
|
1736
|
+
return exec.insertInto("held_runs").values({
|
|
1646
1737
|
org_id: orgId,
|
|
1647
1738
|
run_id: data.runId,
|
|
1648
1739
|
job_id: data.jobId,
|
|
@@ -1650,16 +1741,21 @@ var HeldRunStore = class {
|
|
|
1650
1741
|
hold_type: data.holdType,
|
|
1651
1742
|
queue_type: data.queueType ?? "context",
|
|
1652
1743
|
reason: data.reason,
|
|
1653
|
-
expires_at: data.expiresAt
|
|
1744
|
+
expires_at: data.expiresAt,
|
|
1745
|
+
posted_pending_check: false,
|
|
1746
|
+
...data.scope !== void 0 && { hold_scope: data.scope },
|
|
1747
|
+
...data.triggerSource !== void 0 && { trigger_source: data.triggerSource }
|
|
1654
1748
|
}).returningAll().executeTakeFirstOrThrow();
|
|
1655
1749
|
}
|
|
1656
1750
|
/**
|
|
1657
1751
|
* Create a generalized approval hold (workflow/job/step scope, explicit or
|
|
1658
1752
|
* context trigger) carrying a normalized `ApprovalRequirement`. Returns
|
|
1659
1753
|
* the created row.
|
|
1754
|
+
*
|
|
1755
|
+
* `exec` carries the same meaning it does on {@link create}.
|
|
1660
1756
|
*/
|
|
1661
|
-
async createHold(orgId, data) {
|
|
1662
|
-
return
|
|
1757
|
+
async createHold(orgId, data, exec = this.db) {
|
|
1758
|
+
return exec.insertInto("held_runs").values({
|
|
1663
1759
|
org_id: orgId,
|
|
1664
1760
|
run_id: data.runId,
|
|
1665
1761
|
job_id: data.jobId,
|
|
@@ -1668,6 +1764,7 @@ var HeldRunStore = class {
|
|
|
1668
1764
|
queue_type: data.queueType ?? "context",
|
|
1669
1765
|
reason: data.requirement.reason,
|
|
1670
1766
|
expires_at: new Date(data.requirement.expiresAt),
|
|
1767
|
+
posted_pending_check: false,
|
|
1671
1768
|
hold_scope: data.scope,
|
|
1672
1769
|
step_index: data.stepIndex ?? null,
|
|
1673
1770
|
trigger_source: data.triggerSource,
|
|
@@ -1675,6 +1772,40 @@ var HeldRunStore = class {
|
|
|
1675
1772
|
...data.payload !== void 0 && { payload: JSON.stringify(data.payload) }
|
|
1676
1773
|
}).returningAll().executeTakeFirstOrThrow();
|
|
1677
1774
|
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Record that the pending `KiCI Security` check reached the provider, for
|
|
1777
|
+
* every hold that gates it, so the settle that ends one knows it has a check
|
|
1778
|
+
* to terminalize.
|
|
1779
|
+
*
|
|
1780
|
+
* Written AFTER the post returns, never before. The two orders fail
|
|
1781
|
+
* differently and the failures are not equivalent: recording first and dying
|
|
1782
|
+
* before the post leaves a row claiming a check the commit does not have, and
|
|
1783
|
+
* the settle then CREATES one — a completed `KiCI Security` run appearing on a
|
|
1784
|
+
* commit nothing ever held. Recording second leaves the opposite residue, a
|
|
1785
|
+
* real pending check the settle declines to close, which is the same stuck
|
|
1786
|
+
* check the fire-and-forget post could already produce. A fabricated failing
|
|
1787
|
+
* check on a pull request is worse than a stuck one, so the write goes last.
|
|
1788
|
+
*
|
|
1789
|
+
* **One statement for all of them, not one per hold.** A commit's check run is
|
|
1790
|
+
* shared by every hold on it, and the settle asks the contention query which
|
|
1791
|
+
* of them still owns it. Marking them in a loop admits a PARTIAL mark: mark
|
|
1792
|
+
* the reviewer hold, fail on the security hold, and the security row keeps
|
|
1793
|
+
* `posted_pending_check: false` — so it is not counted as a contender, and
|
|
1794
|
+
* approving the reviewer hold terminalizes the shared check `success` while
|
|
1795
|
+
* the trust hold still gates the job. That is a fabricated PASSING check,
|
|
1796
|
+
* which is the worse direction, reached without any process dying: a
|
|
1797
|
+
* deadlock, a statement timeout or a lost connection between the two UPDATEs
|
|
1798
|
+
* is enough. A single `WHERE id IN (…)` either marks every hold or none.
|
|
1799
|
+
*
|
|
1800
|
+
* The residual window is therefore one statement issued immediately after the
|
|
1801
|
+
* provider call returns — narrow, and not only reachable by a process death,
|
|
1802
|
+
* which is why the caller logs its failure rather than treating it as
|
|
1803
|
+
* impossible.
|
|
1804
|
+
*/
|
|
1805
|
+
async markPendingCheckPosted(orgId, heldRunIds) {
|
|
1806
|
+
if (heldRunIds.length === 0) return;
|
|
1807
|
+
await this.db.updateTable("held_runs").set({ posted_pending_check: true }).where("id", "in", [...heldRunIds]).where("org_id", "=", orgId).execute();
|
|
1808
|
+
}
|
|
1678
1809
|
/** INSERT one decision row using the given executor (root or transaction). */
|
|
1679
1810
|
insertDecisionRow(exec, heldRunId, data) {
|
|
1680
1811
|
const clausesSatisfied = data.clausesSatisfied != null ? JSON.stringify(data.clausesSatisfied) : null;
|
|
@@ -1839,33 +1970,63 @@ var HeldRunStore = class {
|
|
|
1839
1970
|
return this.db.selectFrom("held_runs").selectAll().where("status", "=", "pending").where("expires_at", "<", sql`now()`).execute();
|
|
1840
1971
|
}
|
|
1841
1972
|
/**
|
|
1842
|
-
* Release overdue
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
* `
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
* expire-and-fail sweep sees them.
|
|
1973
|
+
* Release overdue timer holds at ANY scope. A wait action pauses its element
|
|
1974
|
+
* as a held run; on timer expiry it must RESUME (not fail like a reviewer-hold
|
|
1975
|
+
* expiry). Flips each overdue pending timer row to `released` and returns a
|
|
1976
|
+
* `ReleaseSignal` per row, carrying the row's own scope so the caller can
|
|
1977
|
+
* route it — `routeRelease` sends a workflow-scoped one to the install-gate
|
|
1978
|
+
* rebuild and a job-scoped one to the job re-dispatch path.
|
|
1849
1979
|
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1980
|
+
* Runs BEFORE `expireOverdue()` so these rows leave the pending pool before
|
|
1981
|
+
* the expire-and-fail sweep sees them. That ordering is load-bearing, not
|
|
1982
|
+
* incidental: `expireOverdue` is not scope-filtered, so a released-but-not-yet-
|
|
1983
|
+
* resumed row would otherwise be expired out from under its resume.
|
|
1984
|
+
*
|
|
1985
|
+
* The filter matches every persisted spelling of the timer hold type, so a row
|
|
1986
|
+
* an un-upgraded orchestrator wrote as `wait_timer` still resumes rather than
|
|
1987
|
+
* falling through to the expire-and-fail sweep. It deliberately does NOT
|
|
1988
|
+
* filter on `hold_scope`: a job-scoped timer hold used to be excluded here,
|
|
1989
|
+
* which left it with no release path at all — created, never released,
|
|
1990
|
+
* eventually expired, its job never dispatched.
|
|
1854
1991
|
*/
|
|
1855
1992
|
async releaseDueWaitHolds() {
|
|
1856
1993
|
return (await this.db.updateTable("held_runs").set({
|
|
1857
1994
|
status: "released",
|
|
1858
1995
|
resolved_at: sql`now()`
|
|
1859
|
-
}).where("status", "=", "pending").where("hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.timer)).where("
|
|
1996
|
+
}).where("status", "=", "pending").where("hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.timer)).where("expires_at", "<", sql`now()`).returningAll().execute()).map((row) => ({
|
|
1860
1997
|
holdId: row.id,
|
|
1861
1998
|
runId: row.run_id,
|
|
1862
1999
|
jobId: row.job_id,
|
|
1863
|
-
scope: row.hold_scope
|
|
2000
|
+
scope: row.hold_scope,
|
|
1864
2001
|
stepIndex: row.step_index,
|
|
1865
2002
|
triggerSource: row.trigger_source ?? TriggerSource.enum.context
|
|
1866
2003
|
}));
|
|
1867
2004
|
}
|
|
1868
2005
|
/**
|
|
2006
|
+
* Every pending queued (concurrency) hold, with the org and concurrency group
|
|
2007
|
+
* it belongs to — the input to the periodic release sweep, which needs to know
|
|
2008
|
+
* WHICH groups have someone waiting before it looks up any limits.
|
|
2009
|
+
*
|
|
2010
|
+
* Returns the pair rather than full rows: the sweep only groups by it, and
|
|
2011
|
+
* `listQueuedHoldsForContext` fetches the rows it actually releases.
|
|
2012
|
+
*/
|
|
2013
|
+
async listAllQueuedHolds() {
|
|
2014
|
+
return await this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").select(["held_runs.org_id as orgId", "execution_runs.context as concurrencyGroup"]).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.concurrency)).execute();
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* List the pending queued (concurrency) holds for one context's concurrency
|
|
2018
|
+
* group, oldest first.
|
|
2019
|
+
*
|
|
2020
|
+
* Joined to `execution_runs` and filtered by `customer_id` as well as
|
|
2021
|
+
* `context`: a context NAME is not unique across tenants, so without the org
|
|
2022
|
+
* predicate one org's completing job could release another org's queued hold.
|
|
2023
|
+
* Oldest-first is the release order — a queue that released newest-first would
|
|
2024
|
+
* starve whoever waited longest.
|
|
2025
|
+
*/
|
|
2026
|
+
async listQueuedHoldsForContext(orgId, concurrencyGroup) {
|
|
2027
|
+
return this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").selectAll("held_runs").where("held_runs.org_id", "=", orgId).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.concurrency)).where("execution_runs.context", "=", concurrencyGroup).where("execution_runs.customer_id", "=", orgId).orderBy("held_runs.created_at", "asc").execute();
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
1869
2030
|
* Expire overdue pending runs. Called by the stale detector.
|
|
1870
2031
|
* Sets status to 'expired' and resolved_at to now() for all
|
|
1871
2032
|
* pending runs past their expires_at.
|
|
@@ -1887,10 +2048,11 @@ var HeldRunStore = class {
|
|
|
1887
2048
|
* PR-scoped hold selection against the real deployed orchestrator DB.
|
|
1888
2049
|
*/
|
|
1889
2050
|
function createHeldRunStoreFromUrl(databaseUrl, opts) {
|
|
1890
|
-
const
|
|
2051
|
+
const pool = new pg.Pool({
|
|
1891
2052
|
connectionString: databaseUrl,
|
|
1892
2053
|
max: opts?.maxConnections ?? 3
|
|
1893
|
-
})
|
|
2054
|
+
});
|
|
2055
|
+
const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
|
|
1894
2056
|
return {
|
|
1895
2057
|
store: new HeldRunStore(db),
|
|
1896
2058
|
dispose: async () => {
|
|
@@ -1898,6 +2060,23 @@ function createHeldRunStoreFromUrl(databaseUrl, opts) {
|
|
|
1898
2060
|
}
|
|
1899
2061
|
};
|
|
1900
2062
|
}
|
|
2063
|
+
z.enum(["policy", "legacy"]);
|
|
2064
|
+
/** The fork switch applied when no policy row is stored. */
|
|
2065
|
+
const DEFAULT_FORK_POLICY = ForkPolicy.enum.ignore;
|
|
2066
|
+
Object.freeze({
|
|
2067
|
+
forkPolicy: DEFAULT_FORK_POLICY,
|
|
2068
|
+
unknownContributorPolicy: "hold",
|
|
2069
|
+
workflowChangePolicy: "hold",
|
|
2070
|
+
approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
|
|
2071
|
+
approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
2072
|
+
});
|
|
2073
|
+
Object.freeze({
|
|
2074
|
+
forkPolicy: ForkPolicy.enum.hold,
|
|
2075
|
+
unknownContributorPolicy: "hold",
|
|
2076
|
+
workflowChangePolicy: "hold",
|
|
2077
|
+
approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
|
|
2078
|
+
approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
2079
|
+
});
|
|
1901
2080
|
//#endregion
|
|
1902
2081
|
//#region src/security/trust-policy-store.ts
|
|
1903
2082
|
/**
|
|
@@ -1914,14 +2093,42 @@ const TrustPolicySource = z.enum(["platform", "local"]);
|
|
|
1914
2093
|
/**
|
|
1915
2094
|
* The strictest documented defaults. Used to fill gaps on a local merge; the
|
|
1916
2095
|
* fail-closed policy an unknown org resolves to lives in `trust-policy-gate.ts`.
|
|
2096
|
+
*
|
|
2097
|
+
* `forkPolicy` reads that module's `DEFAULT_FORK_POLICY` so a partial local
|
|
2098
|
+
* merge and an absent row settle on the same fork switch.
|
|
1917
2099
|
*/
|
|
1918
2100
|
const DEFAULT_TRUST_POLICY = {
|
|
1919
|
-
forkPolicy:
|
|
2101
|
+
forkPolicy: DEFAULT_FORK_POLICY,
|
|
1920
2102
|
unknownContributorPolicy: "hold",
|
|
1921
2103
|
workflowChangePolicy: "hold",
|
|
1922
|
-
approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS
|
|
2104
|
+
approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
|
|
2105
|
+
approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
1923
2106
|
};
|
|
1924
2107
|
/**
|
|
2108
|
+
* Resolve a local patch's approval-expiry window against what is already stored.
|
|
2109
|
+
*
|
|
2110
|
+
* One value is being set, spelled two ways, so the pair is merged as a unit and
|
|
2111
|
+
* always re-derived from the winning seconds value:
|
|
2112
|
+
*
|
|
2113
|
+
* - a patch naming `approvalExpirySeconds` sets the window, and the hours field
|
|
2114
|
+
* is recomputed from it — a stale hours value left behind would be the coarse
|
|
2115
|
+
* view of a window that is no longer stored;
|
|
2116
|
+
* - a patch naming only `approvalExpiryHours` sets the window too, and the
|
|
2117
|
+
* seconds field is recomputed from it — keeping the old seconds value would
|
|
2118
|
+
* let it out-rank the operator's explicit hours and silently discard the
|
|
2119
|
+
* write;
|
|
2120
|
+
* - a patch naming both takes the seconds value, because it is the more
|
|
2121
|
+
* specific of the two (`approvalExpirySecondsOf`);
|
|
2122
|
+
* - a patch naming neither leaves the stored window untouched.
|
|
2123
|
+
*/
|
|
2124
|
+
function mergedExpiry(patch, existing) {
|
|
2125
|
+
const seconds = (patch.approvalExpirySeconds != null || patch.approvalExpiryHours != null ? approvalExpirySecondsOf(patch) : null) ?? existing?.approvalExpirySeconds ?? DEFAULT_TRUST_POLICY.approvalExpirySeconds ?? DEFAULT_APPROVAL_EXPIRY_SECONDS;
|
|
2126
|
+
return {
|
|
2127
|
+
approvalExpiryHours: approvalExpiryHoursOf(seconds),
|
|
2128
|
+
approvalExpirySeconds: seconds
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
1925
2132
|
* Namespace for the per-org advisory lock taken by `upsertLocal`, so the key
|
|
1926
2133
|
* cannot collide with another feature's advisory lock in the same database.
|
|
1927
2134
|
*/
|
|
@@ -1940,6 +2147,10 @@ var TrustPolicyStore = class {
|
|
|
1940
2147
|
unknownContributorPolicy: row.unknown_contributor_policy,
|
|
1941
2148
|
workflowChangePolicy: row.workflow_change_policy,
|
|
1942
2149
|
approvalExpiryHours: Number(row.approval_expiry_hours),
|
|
2150
|
+
approvalExpirySeconds: approvalExpirySecondsOf({
|
|
2151
|
+
approvalExpiryHours: Number(row.approval_expiry_hours),
|
|
2152
|
+
approvalExpirySeconds: row.approval_expiry_seconds == null ? null : Number(row.approval_expiry_seconds)
|
|
2153
|
+
}),
|
|
1943
2154
|
source: row.source,
|
|
1944
2155
|
updatedAt: row.updated_at
|
|
1945
2156
|
};
|
|
@@ -1982,7 +2193,7 @@ var TrustPolicyStore = class {
|
|
|
1982
2193
|
forkPolicy: patch.forkPolicy ?? existing?.forkPolicy ?? DEFAULT_TRUST_POLICY.forkPolicy,
|
|
1983
2194
|
unknownContributorPolicy: patch.unknownContributorPolicy ?? existing?.unknownContributorPolicy ?? DEFAULT_TRUST_POLICY.unknownContributorPolicy,
|
|
1984
2195
|
workflowChangePolicy: patch.workflowChangePolicy ?? existing?.workflowChangePolicy ?? DEFAULT_TRUST_POLICY.workflowChangePolicy,
|
|
1985
|
-
|
|
2196
|
+
...mergedExpiry(patch, existing)
|
|
1986
2197
|
};
|
|
1987
2198
|
await this.write(orgId, merged, TrustPolicySource.enum.local, trx);
|
|
1988
2199
|
await onWrite?.(trx, merged);
|
|
@@ -1990,11 +2201,13 @@ var TrustPolicyStore = class {
|
|
|
1990
2201
|
});
|
|
1991
2202
|
}
|
|
1992
2203
|
async write(orgId, policy, source, executor = this.db) {
|
|
2204
|
+
const seconds = approvalExpirySecondsOf(policy);
|
|
1993
2205
|
const columns = {
|
|
1994
2206
|
fork_policy: policy.forkPolicy,
|
|
1995
2207
|
unknown_contributor_policy: policy.unknownContributorPolicy,
|
|
1996
2208
|
workflow_change_policy: policy.workflowChangePolicy,
|
|
1997
|
-
approval_expiry_hours:
|
|
2209
|
+
approval_expiry_hours: approvalExpiryHoursOf(seconds),
|
|
2210
|
+
approval_expiry_seconds: seconds,
|
|
1998
2211
|
source,
|
|
1999
2212
|
updated_at: sql`now()`
|
|
2000
2213
|
};
|
|
@@ -2011,10 +2224,11 @@ var TrustPolicyStore = class {
|
|
|
2011
2224
|
* orchestrator DB rather than held in process memory.
|
|
2012
2225
|
*/
|
|
2013
2226
|
function createTrustPolicyStoreFromUrl(databaseUrl, opts) {
|
|
2014
|
-
const
|
|
2227
|
+
const pool = new pg.Pool({
|
|
2015
2228
|
connectionString: databaseUrl,
|
|
2016
2229
|
max: opts?.maxConnections ?? 3
|
|
2017
|
-
})
|
|
2230
|
+
});
|
|
2231
|
+
const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
|
|
2018
2232
|
return {
|
|
2019
2233
|
store: new TrustPolicyStore(db),
|
|
2020
2234
|
dispose: async () => {
|
|
@@ -2062,10 +2276,34 @@ function firstCauseMessage(err) {
|
|
|
2062
2276
|
}
|
|
2063
2277
|
return "";
|
|
2064
2278
|
}
|
|
2279
|
+
/**
|
|
2280
|
+
* True when a rejected `fetch` failed at the transport layer, as opposed to
|
|
2281
|
+
* failing while the request was still being built.
|
|
2282
|
+
*
|
|
2283
|
+
* `fetch` uses a `TypeError` for both, so they are easy to conflate — but only
|
|
2284
|
+
* a transport failure means the orchestrator was actually dialled. Measured
|
|
2285
|
+
* against Node 24's undici:
|
|
2286
|
+
*
|
|
2287
|
+
* closed port → TypeError('fetch failed') cause: Error(ECONNREFUSED)
|
|
2288
|
+
* invalid header → TypeError('Headers.append: … invalid header …') cause: undefined
|
|
2289
|
+
* invalid URL → TypeError('Failed to parse URL from …') cause: Error(ERR_INVALID_URL)
|
|
2290
|
+
*
|
|
2291
|
+
* The discriminator is therefore the message, NOT the presence of a `cause`:
|
|
2292
|
+
* the invalid-URL case carries one. undici uses exactly `fetch failed` for a
|
|
2293
|
+
* transport failure and a descriptive message for everything it rejects before
|
|
2294
|
+
* opening a connection.
|
|
2295
|
+
*/
|
|
2296
|
+
function isTransportFailure(err) {
|
|
2297
|
+
return err instanceof TypeError && err.message === "fetch failed";
|
|
2298
|
+
}
|
|
2065
2299
|
async function fetchAdminApi(url, init, baseUrl) {
|
|
2066
2300
|
try {
|
|
2067
2301
|
return await fetch(url, init);
|
|
2068
2302
|
} catch (err) {
|
|
2303
|
+
if (!isTransportFailure(err)) {
|
|
2304
|
+
const detail = (err instanceof Error ? err.message : String(err)).replace(/\.$/, "");
|
|
2305
|
+
throw new Error(`the admin API request to ${baseUrl} could not be built: ${detail}. The orchestrator was NOT contacted — this is a malformed request, not a connectivity problem. A token or URL carrying a newline or stray whitespace is the usual cause; check KICI_ADMIN_TOKEN / --token and KICI_ADMIN_URL / --url.`, { cause: err });
|
|
2306
|
+
}
|
|
2069
2307
|
const detail = firstCauseMessage(err);
|
|
2070
2308
|
const cause = detail ? ` (${detail})` : "";
|
|
2071
2309
|
throw new Error(`cannot reach the orchestrator admin API at ${baseUrl}${cause}. Set KICI_ADMIN_URL to the orchestrator's HTTP address, or pass --base-url where the subcommand accepts it. Note KICI_ORCHESTRATOR_URL is NOT read by this CLI; if other subcommands appear to work, they are using the --database-url / KICI_DATABASE_URL direct-DB path rather than HTTP.`, { cause: err });
|
|
@@ -2765,7 +3003,8 @@ function openBrowserBestEffort(url) {
|
|
|
2765
3003
|
exec(override.replace("{url}", url), () => {});
|
|
2766
3004
|
return;
|
|
2767
3005
|
}
|
|
2768
|
-
|
|
3006
|
+
const cmd = platform() === "darwin" ? `open ${JSON.stringify(url)}` : platform() === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
|
|
3007
|
+
exec(cmd, () => {});
|
|
2769
3008
|
}
|
|
2770
3009
|
//#endregion
|
|
2771
3010
|
//#region src/cli/commands/source-manifest.ts
|
|
@@ -2784,9 +3023,9 @@ function openBrowserBestEffort(url) {
|
|
|
2784
3023
|
/** Static marketing-site page the CLI points at in headless paste-code mode. */
|
|
2785
3024
|
const STATIC_CALLBACK_URL = "https://kici.dev/gh-manifest-callback";
|
|
2786
3025
|
/** How long to wait for the operator to click "Create" on GitHub. */
|
|
2787
|
-
const CREATE_TIMEOUT_MS =
|
|
3026
|
+
const CREATE_TIMEOUT_MS = 3e5;
|
|
2788
3027
|
/** How long to wait for the operator to install the App on at least one repo. */
|
|
2789
|
-
const INSTALL_TIMEOUT_MS =
|
|
3028
|
+
const INSTALL_TIMEOUT_MS = 3e5;
|
|
2790
3029
|
const INSTALL_POLL_MS = 3e3;
|
|
2791
3030
|
function defaultReadLine(prompt) {
|
|
2792
3031
|
const rl = createInterface({
|
|
@@ -2801,7 +3040,8 @@ function defaultReadLine(prompt) {
|
|
|
2801
3040
|
});
|
|
2802
3041
|
}
|
|
2803
3042
|
function defaultWriteRecoveryFile(appId, pem) {
|
|
2804
|
-
const
|
|
3043
|
+
const dir = mkdtempSync(join(tmpdir(), "kici-gh-app-"));
|
|
3044
|
+
const file = join(dir, `app-${appId}.private-key.pem`);
|
|
2805
3045
|
writeFileSync(file, pem, { mode: 384 });
|
|
2806
3046
|
return file;
|
|
2807
3047
|
}
|