@kici-dev/orchestrator 0.6.0 → 0.7.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 (178) hide show
  1. package/dist/agent/dispatcher.d.ts +10 -1
  2. package/dist/agent/host-roster.d.ts +8 -3
  3. package/dist/agent/registry.d.ts +60 -5
  4. package/dist/app.d.ts +10 -5
  5. package/dist/cache/dep-cache.d.ts +14 -6
  6. package/dist/cache/global-eval-round-cache.d.ts +1 -1
  7. package/dist/cache/index.d.ts +1 -1
  8. package/dist/cache/source-cache.d.ts +92 -15
  9. package/dist/cancel/cancel-run.d.ts +20 -2
  10. package/dist/cancel/sweep-stuck-cancelling.d.ts +30 -0
  11. package/dist/cli/api-client.d.ts +10 -0
  12. package/dist/cli/commands/agent-service/logs.d.ts +1 -1
  13. package/dist/cli/commands/agent-service/restart.d.ts +1 -1
  14. package/dist/cli/commands/agent-service/start.d.ts +1 -1
  15. package/dist/cli/commands/agent-service/status.d.ts +1 -1
  16. package/dist/cli/commands/agent-service/stop.d.ts +1 -1
  17. package/dist/cli/commands/agent-service/uninstall.d.ts +1 -1
  18. package/dist/cli/commands/agent.d.ts +25 -0
  19. package/dist/cli/commands/config.d.ts +16 -0
  20. package/dist/cli/commands/db-backup.d.ts +46 -0
  21. package/dist/cli/commands/held-run.d.ts +9 -0
  22. package/dist/cli/commands/orchestrator-service/drain.d.ts +2 -19
  23. package/dist/cli/commands/orchestrator-service/index.d.ts +9 -1
  24. package/dist/cli/commands/orchestrator-service/install-env.d.ts +43 -2
  25. package/dist/cli/commands/orchestrator-service/logs.d.ts +1 -1
  26. package/dist/cli/commands/orchestrator-service/restart.d.ts +1 -1
  27. package/dist/cli/commands/orchestrator-service/start.d.ts +1 -1
  28. package/dist/cli/commands/orchestrator-service/status.d.ts +17 -1
  29. package/dist/cli/commands/orchestrator-service/stop.d.ts +1 -1
  30. package/dist/cli/commands/orchestrator-service/uninstall.d.ts +1 -1
  31. package/dist/cli/commands/orchestrator-service/upgrade.d.ts +2 -1
  32. package/dist/cli/commands/rotate.d.ts +1 -1
  33. package/dist/cli/commands/shared/env-file-mode.d.ts +16 -0
  34. package/dist/cli/commands/shared/upgrade-hooks.d.ts +32 -0
  35. package/dist/cli/commands/shared/upgrade-safety.d.ts +81 -0
  36. package/dist/cli/commands/shared/versioned-upgrade.d.ts +67 -12
  37. package/dist/cli/commands/source.d.ts +1 -0
  38. package/dist/cli/commands/token.d.ts +4 -0
  39. package/dist/cli/commands/trust-policy.d.ts +57 -1
  40. package/dist/cli/join.d.ts +1 -1
  41. package/dist/cli/service/backup-timer.d.ts +108 -0
  42. package/dist/cli/service/compose-path.d.ts +11 -0
  43. package/dist/cli/service/compose.d.ts +22 -0
  44. package/dist/cli/service/deploy-env.d.ts +10 -11
  45. package/dist/cli/service/env-assignment.d.ts +24 -0
  46. package/dist/cli/service/index.d.ts +2 -2
  47. package/dist/cli/service/instance/resolve.d.ts +71 -7
  48. package/dist/cli/service/instance/types.d.ts +15 -0
  49. package/dist/cli/service/launchd.d.ts +7 -0
  50. package/dist/cli/service/systemd.d.ts +1 -0
  51. package/dist/cli/service/types.d.ts +36 -0
  52. package/dist/cli/service/windows.d.ts +16 -0
  53. package/dist/cli/wizard/orchestrator-wizard.d.ts +12 -3
  54. package/dist/cli/wizard/prompts.d.ts +7 -0
  55. package/dist/cli.js +12525 -9298
  56. package/dist/cluster/cluster-settings-reader.d.ts +2 -2
  57. package/dist/cluster/coordinator.d.ts +11 -0
  58. package/dist/cluster/instance-heartbeat.d.ts +96 -0
  59. package/dist/cluster/join-client.d.ts +55 -5
  60. package/dist/cluster/join-handler.d.ts +1 -0
  61. package/dist/cluster/orphan-recovery.d.ts +16 -0
  62. package/dist/cluster/peer-registry.d.ts +10 -0
  63. package/dist/concurrency/dispatch-next-queued.d.ts +1 -13
  64. package/dist/concurrency/index.d.ts +1 -2
  65. package/dist/concurrency/queue-manager.d.ts +63 -18
  66. package/dist/config/env-overlay.d.ts +3 -2
  67. package/dist/config.d.ts +24 -0
  68. package/dist/contexts/release-queued-holds.d.ts +23 -1
  69. package/dist/dashboard/attestation-filters.d.ts +2 -0
  70. package/dist/dashboard/handler.d.ts +22 -2
  71. package/dist/db/deferred-indexes.d.ts +72 -0
  72. package/dist/db/migration-test-harness.d.ts +22 -0
  73. package/dist/db/migrations/131_execution_jobs_git_credentials.d.ts +30 -0
  74. package/dist/db/migrations/132_execution_runs_trigger_event.d.ts +23 -0
  75. package/dist/db/migrations/133_dispatch_queue_source_tar_digest.d.ts +23 -0
  76. package/dist/db/migrations/134_dispatch_queue_ownership.d.ts +4 -0
  77. package/dist/db/migrations/135_cluster_settings_retention_days.d.ts +4 -0
  78. package/dist/db/migrations/136_master_key_wrapped_tables_key_version.d.ts +4 -0
  79. package/dist/db/migrations/137_execution_runs_pr_head_context.d.ts +4 -0
  80. package/dist/db/migrations/138_execution_runs_subject_trigger_event.d.ts +4 -0
  81. package/dist/db/migrations/139_admin_token_subject.d.ts +4 -0
  82. package/dist/db/migrations/140_dispatch_queue_canonical_labels.d.ts +54 -0
  83. package/dist/db/migrations/141_event_log_shed.d.ts +15 -0
  84. package/dist/db/migrator.d.ts +47 -0
  85. package/dist/db/notify-listener.d.ts +60 -0
  86. package/dist/db/synthetic-job-ids.d.ts +24 -0
  87. package/dist/db/types.d.ts +169 -4
  88. package/dist/deployment/deployment-identity.d.ts +31 -1
  89. package/dist/diagnostics/checks/index.d.ts +2 -1
  90. package/dist/diagnostics/checks/retention.d.ts +24 -0
  91. package/dist/diagnostics/types.d.ts +7 -0
  92. package/dist/events/event-router.d.ts +1 -1
  93. package/dist/fault-injection-types.d.ts +53 -0
  94. package/dist/firecracker/host-network.d.ts +63 -8
  95. package/dist/git/credential-broker.d.ts +35 -2
  96. package/dist/git/inherited-secret.d.ts +7 -2
  97. package/dist/git/job-context.d.ts +7 -4
  98. package/dist/helpers/secure-write.d.ts +35 -0
  99. package/dist/index.js +196 -67
  100. package/dist/metrics/prometheus.d.ts +116 -1
  101. package/dist/oidc/db-signer.d.ts +27 -5
  102. package/dist/oidc/id-token-claims.d.ts +33 -2
  103. package/dist/oidc/oidc-mint-registration.d.ts +6 -0
  104. package/dist/oidc/orchestrator-mint.d.ts +8 -0
  105. package/dist/oidc/reconcile-signing-key.d.ts +37 -0
  106. package/dist/orchestrator-core.d.ts +9 -3
  107. package/dist/pipeline/decorating-secret-resolver.d.ts +1 -1
  108. package/dist/pipeline/dispatch-matched-workflow.d.ts +65 -14
  109. package/dist/pipeline/manual-schedule.d.ts +1 -0
  110. package/dist/pipeline/process-webhook.d.ts +20 -2
  111. package/dist/pipeline/route-or-dispatch-jobs.d.ts +7 -0
  112. package/dist/pipeline/run-event-context.d.ts +39 -0
  113. package/dist/policy/dashboard-write-policy-listener.d.ts +1 -1
  114. package/dist/policy/dashboard-write-policy.d.ts +47 -1
  115. package/dist/provenance/dispatch-context.d.ts +46 -0
  116. package/dist/provenance/verify-deferred-capture.d.ts +39 -0
  117. package/dist/providers/github/auth.d.ts +9 -0
  118. package/dist/providers/github/deliveries.d.ts +118 -0
  119. package/dist/providers/universal-git/config.d.ts +1 -1
  120. package/dist/queue/cleanup.d.ts +13 -0
  121. package/dist/queue/job-queue.d.ts +162 -14
  122. package/dist/queue/retention.d.ts +105 -0
  123. package/dist/reporting/execution-tracker.d.ts +87 -36
  124. package/dist/routes/admin-db.d.ts +5 -0
  125. package/dist/routes/admin-events.d.ts +13 -0
  126. package/dist/routes/admin-held-runs.d.ts +11 -1
  127. package/dist/routes/admin-org-settings.d.ts +9 -0
  128. package/dist/routes/admin-runs.d.ts +7 -4
  129. package/dist/routes/admin-sources.d.ts +15 -0
  130. package/dist/routes/admin-trust-policy.d.ts +11 -0
  131. package/dist/routes/admin.d.ts +25 -0
  132. package/dist/scaler/backend-factory.d.ts +16 -0
  133. package/dist/scaler/bare-metal-backend.d.ts +22 -4
  134. package/dist/scaler/config.d.ts +6 -3
  135. package/dist/scaler/container-backend.d.ts +102 -3
  136. package/dist/scaler/container-hostconfig.d.ts +60 -0
  137. package/dist/scaler/file-tail.d.ts +25 -2
  138. package/dist/scaler/firecracker-api.d.ts +9 -1
  139. package/dist/scaler/firecracker-backend.d.ts +135 -2
  140. package/dist/scaler/host-access.d.ts +59 -0
  141. package/dist/scaler/index.d.ts +1 -0
  142. package/dist/scaler/manager.d.ts +33 -0
  143. package/dist/scaler/scaler-state-store.d.ts +6 -0
  144. package/dist/scaler/types.d.ts +5 -7
  145. package/dist/secrets/config.d.ts +48 -0
  146. package/dist/secrets/dashboard-encryption-key.d.ts +19 -0
  147. package/dist/secrets/ephemeral-keys.d.ts +9 -3
  148. package/dist/secrets/index.d.ts +3 -1
  149. package/dist/secrets/job-secret-gate.d.ts +76 -0
  150. package/dist/secrets/master-key-rotation.d.ts +65 -0
  151. package/dist/secrets/secret-output-crypto.d.ts +17 -0
  152. package/dist/secrets/secret-resolver.d.ts +23 -6
  153. package/dist/secrets/source-credentials.d.ts +1 -1
  154. package/dist/secrets/token-manager.d.ts +4 -1
  155. package/dist/security/reduced-privilege-note.d.ts +5 -0
  156. package/dist/security/trust-policy-gate.d.ts +70 -6
  157. package/dist/security/trust-tier.d.ts +31 -0
  158. package/dist/server.d.ts +1 -1
  159. package/dist/server.js +10674 -5283
  160. package/dist/sources/source-manager.d.ts +1 -1
  161. package/dist/stale-detector/stale-dispatch-candidates.d.ts +43 -0
  162. package/dist/standalone.js +9258 -4122
  163. package/dist/storage/loopback-guard.d.ts +53 -6
  164. package/dist/webhook/event-log.d.ts +14 -1
  165. package/dist/webhook/generic-sources-listener.d.ts +1 -1
  166. package/dist/webhook/ingest-admission.d.ts +24 -0
  167. package/dist/webhook/ingest-overflow-replayer.d.ts +78 -14
  168. package/dist/webhook/ingest-overflow-types.d.ts +13 -1
  169. package/dist/webhook/register-source-bundle.d.ts +23 -0
  170. package/dist/webhook/relay-reinject.d.ts +11 -12
  171. package/dist/webhook/shed-breadcrumb.d.ts +15 -0
  172. package/dist/ws/git-credential-relay.d.ts +41 -12
  173. package/dist/ws/platform-client.d.ts +28 -5
  174. package/installer-image-digests.json +3 -3
  175. package/package.json +4 -4
  176. package/sbom.spdx.json +53 -53
  177. package/dist/concurrency/group-tracker.d.ts +0 -51
  178. package/dist/scaler/nftables.d.ts +0 -111
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Writing a credential file without ever exposing it.
3
+ *
4
+ * `writeFile(path, content, { mode })` applies the mode only when it *creates*
5
+ * the file: writing over a destination that already sits at 0644 leaves the
6
+ * secret world-readable for as long as it takes the following `chmod` to run.
7
+ * `copyFile` is worse — it reproduces the source file's mode, so a 0644 source
8
+ * yields a 0644 destination.
9
+ *
10
+ * These helpers write a sibling temporary file at the target mode and rename it
11
+ * over the destination. `rename(2)` is atomic within a directory and carries
12
+ * the temporary file's mode with it, so the destination never exists with
13
+ * permissions looser than the content requires — not for a window, not at all.
14
+ *
15
+ * A mode is only ever narrowed by the process umask, so the created file is at
16
+ * most as permissive as `mode`; the explicit `chmod` on the temporary file then
17
+ * restores exactly `mode` under an unusually restrictive umask.
18
+ */
19
+ /**
20
+ * Write `content` to `filePath` at `mode`, atomically and never more permissive.
21
+ *
22
+ * `wx` refuses a pre-existing temporary path, so a planted file or symlink at
23
+ * the staging name is an error rather than a write through it.
24
+ */
25
+ export declare function writeFileSecurelySync(filePath: string, content: string, mode: number): void;
26
+ /** Promise-returning {@link writeFileSecurelySync}. */
27
+ export declare function writeFileSecurely(filePath: string, content: string, mode: number): Promise<void>;
28
+ /**
29
+ * Copy `sourcePath` to `filePath` at `mode`, never inheriting the source's.
30
+ *
31
+ * Reads the source into memory: an env file is a handful of lines, and the
32
+ * alternative (copy then chmod) is the window this module exists to close.
33
+ */
34
+ export declare function copyFileSecurelySync(sourcePath: string, filePath: string, mode: number): void;
35
+ //# sourceMappingURL=secure-write.d.ts.map
package/dist/index.js CHANGED
@@ -599,67 +599,175 @@ function createCacheStorage(config) {
599
599
  /**
600
600
  * Source-tarball cache layer wrapping CacheStorage.
601
601
  *
602
- * Provides content-hash-keyed operations for customer-workflow source tarballs
603
- * (the `.kici/` directory minus `node_modules/`, packed by the agent's build
604
- * job via `source-packer.ts`). Tarballs are platform-independent: raw TypeScript
605
- * source files are identical across architectures. Refreshes TTL on reads
602
+ * Stores customer-workflow source tarballs — the `.kici/` directory minus
603
+ * `node_modules/`, packed by the agent's build job via `source-packer.ts` —
604
+ * under the tarball's OWN content hash, with a small pointer resolving an org's
605
+ * workflow `contentHash` to that hash. Tarballs are platform-independent: raw
606
+ * TypeScript source is identical across architectures. Refreshes TTL on reads
606
607
  * (touch-on-read) so actively used sources stay in cache longer.
607
608
  *
608
- * Cache key format: source/{contentHash}.tar.gz
609
+ * Keys: `source/v2/{orgId}/{sourceTarDigest}.tar.gz` (immutable) and
610
+ * `source/v2/{orgId}/{contentHash}.hash` (the pointer) — the shape `dep-cache.ts`
611
+ * already uses, for the same reasons — content addressing and org scoping.
609
612
  *
610
- * Replaces the former `BundleCache` / `bundles/{contentHash}.js` layout that
611
- * stored Rolldown-bundled `.compiled.mjs` artifacts the new execution path
612
- * downloads the source tarball, extracts it, and imports the workflow entry
613
- * via the shared oxc-transform ESM loader hook.
613
+ * **Content addressing** makes a mismatched pair unrepresentable. At
614
+ * `source/{contentHash}.tar.gz` the object's key was derived from something
615
+ * other than its bytes, so nothing a reader fetched could be verified against
616
+ * anything, and the agent restored whatever the signed URL returned.
617
+ *
618
+ * **Org scoping** makes the key a tenant's key. `contentHash` alone is a
619
+ * property of a `.kici/` tree, so two repositories scaffolded from the same
620
+ * `kici init` template — or a fork and its upstream — produced one object, and
621
+ * whichever built first won for the life of that object. On an orchestrator
622
+ * serving more than one org that is a cross-tenant read of `.kici/` contents.
623
+ * The org is the tenant boundary and the segment the user cache already scopes
624
+ * by; inside one org, sharing a key now means the two trees are byte-identical,
625
+ * so a monorepo or a same-org fork keeps its warm cache and pays nothing.
626
+ *
627
+ * Legacy `source/{contentHash}.tar.gz` objects are **not read**. Only a
628
+ * `compileSchemaVersion: 5` lock can name one, and that `contentHash` covers
629
+ * the workflow entry file alone rather than the tree the tarball holds — the
630
+ * defect this layout closes — so serving it would serve the bug. For a v6 lock
631
+ * the fallback is unreachable anyway, since the schema bump moves every hash.
632
+ * Old objects are orphaned and aged out by the normal cache TTL.
614
633
  */
615
634
  const logger$3 = createLogger({ prefix: "source-cache" });
616
- /** Cache key format: source/{contentHash}.tar.gz */
617
- function sourceKey(contentHash) {
618
- return `source/${contentHash}.tar.gz`;
635
+ /**
636
+ * Cache key for a source tarball, addressed by the tarball's OWN content hash:
637
+ * `source/v2/{orgId}/{sourceTarDigest}.tar.gz`.
638
+ *
639
+ * Naming the object by its own hash means every pair a reader can observe is
640
+ * self-consistent, and the bytes at a given key can never change after a URL is
641
+ * signed for it.
642
+ */
643
+ function sourceTarballKey(orgId, sourceTarDigest) {
644
+ return `source/v2/${orgId}/${sourceTarDigest}.tar.gz`;
619
645
  }
620
- var SourceCache = class {
646
+ /**
647
+ * Cache key for the pointer that resolves a workflow `contentHash` to the
648
+ * content hash of the tarball built from it: `source/v2/{orgId}/{contentHash}.hash`.
649
+ *
650
+ * This is the one mutable object in the scheme. A concurrent write replaces a
651
+ * pointer wholesale — there is no half-written window — so the worst a racing
652
+ * pair of builders can do is leave whichever pointer landed last, and both
653
+ * tarballs stay valid and fetchable.
654
+ */
655
+ function sourcePointerKey(orgId, contentHash) {
656
+ return `source/v2/${orgId}/${contentHash}.hash`;
657
+ }
658
+ var SourceCache = class SourceCache {
621
659
  storage;
622
660
  constructor(options) {
623
661
  this.storage = options.storage;
624
662
  }
625
- async has(contentHash) {
626
- const key = sourceKey(contentHash);
627
- const exists = await this.storage.has(key);
628
- logger$3.debug(`has(${contentHash}): ${exists}`);
663
+ /** The single place the pointer indirection is read. */
664
+ async resolvePointer(orgId, contentHash) {
665
+ return (await this.storage.get(sourcePointerKey(orgId, contentHash)))?.toString("utf-8").trim() || null;
666
+ }
667
+ /**
668
+ * Whether a source tarball for this org's `contentHash` exists.
669
+ *
670
+ * Both halves must be present: a pointer whose tarball has aged out is a
671
+ * miss, not a hit, or the caller skips a rebuild and dispatches a URL that
672
+ * 404s on the agent.
673
+ */
674
+ async has(orgId, contentHash) {
675
+ const digest = await this.resolvePointer(orgId, contentHash);
676
+ if (!digest) {
677
+ logger$3.debug(`has(${contentHash}): false (no pointer)`, { orgId });
678
+ return false;
679
+ }
680
+ const exists = await this.storage.has(sourceTarballKey(orgId, digest));
681
+ logger$3.debug(`has(${contentHash}): ${exists}`, { orgId });
629
682
  return exists;
630
683
  }
631
- async get(contentHash) {
632
- const key = sourceKey(contentHash);
684
+ async get(orgId, contentHash) {
685
+ const digest = await this.resolvePointer(orgId, contentHash);
686
+ if (!digest) {
687
+ logger$3.debug(`get(${contentHash}): miss (no pointer)`, { orgId });
688
+ return null;
689
+ }
690
+ const key = sourceTarballKey(orgId, digest);
633
691
  const data = await this.storage.get(key);
634
692
  if (data) {
635
693
  await this.storage.touch(key);
636
- logger$3.debug(`get(${contentHash}): hit (${data.length} bytes)`);
637
- } else logger$3.debug(`get(${contentHash}): miss`);
694
+ logger$3.debug(`get(${contentHash}): hit (${data.length} bytes)`, { orgId });
695
+ } else logger$3.debug(`get(${contentHash}): miss (dangling pointer)`, { orgId });
638
696
  return data;
639
697
  }
640
- async getUrl(contentHash) {
641
- const key = sourceKey(contentHash);
698
+ /**
699
+ * A pre-signed download URL and the tarball's own digest.
700
+ *
701
+ * The pointer is resolved FIRST: it names the content hash, which is also the
702
+ * tarball's key, so the url and the digest returned here are read from a
703
+ * single source of truth and cannot disagree. Returns null on a miss —
704
+ * including a pointer with no tarball behind it, which is unverifiable and so
705
+ * is deliberately not served.
706
+ */
707
+ async getUrlAndDigest(orgId, contentHash) {
708
+ const pointerKey = sourcePointerKey(orgId, contentHash);
709
+ const digest = (await this.storage.get(pointerKey))?.toString("utf-8").trim() || null;
710
+ if (!digest) {
711
+ logger$3.debug(`getUrlAndDigest(${contentHash}): miss (no pointer)`, { orgId });
712
+ return null;
713
+ }
714
+ const key = sourceTarballKey(orgId, digest);
642
715
  const url = await this.storage.getUrl(key);
643
- if (url) {
644
- await this.storage.touch(key);
645
- logger$3.debug(`getUrl(${contentHash}): hit`);
646
- } else logger$3.debug(`getUrl(${contentHash}): miss`);
647
- return url;
648
- }
649
- async getUploadUrl(contentHash) {
650
- const key = sourceKey(contentHash);
651
- return this.storage.getUploadUrl(key);
652
- }
653
- async store(contentHash, tarball) {
654
- const key = sourceKey(contentHash);
655
- await this.storage.put(key, tarball);
656
- const size = typeof tarball === "string" ? Buffer.byteLength(tarball) : tarball.length;
657
- logger$3.info(`store(${contentHash}): stored (${size} bytes)`);
658
- }
659
- async remove(contentHash) {
660
- const key = sourceKey(contentHash);
661
- const removed = await this.storage.delete(key);
662
- logger$3.info(`remove(${contentHash}): ${removed ? "removed" : "not found"}`);
716
+ if (!url) {
717
+ logger$3.debug(`getUrlAndDigest(${contentHash}): miss (dangling pointer)`, { orgId });
718
+ return null;
719
+ }
720
+ await this.storage.touch(key);
721
+ await this.storage.touch(pointerKey);
722
+ logger$3.debug(`getUrlAndDigest(${contentHash}): hit`, { orgId });
723
+ return {
724
+ url,
725
+ digest
726
+ };
727
+ }
728
+ async getUrl(orgId, contentHash) {
729
+ return (await this.getUrlAndDigest(orgId, contentHash))?.url ?? null;
730
+ }
731
+ /**
732
+ * A pre-signed upload URL for direct agent-to-storage upload.
733
+ *
734
+ * Takes the tarball's OWN digest, not the workflow `contentHash`: the agent
735
+ * computes it before asking for a URL, and the object is named by it.
736
+ */
737
+ async getUploadUrl(orgId, sourceTarDigest) {
738
+ return this.storage.getUploadUrl(sourceTarballKey(orgId, sourceTarDigest));
739
+ }
740
+ /**
741
+ * Publish the pointer that makes an uploaded tarball discoverable by
742
+ * `contentHash`. Called only after the agent confirms its upload completed —
743
+ * publishing before the bytes land would let a reader follow the pointer to a
744
+ * missing object.
745
+ */
746
+ async publishPointer(orgId, contentHash, sourceTarDigest) {
747
+ await this.storage.put(sourcePointerKey(orgId, contentHash), sourceTarDigest);
748
+ logger$3.info(`publishPointer(${contentHash}) -> ${sourceTarDigest.slice(0, 12)}`, { orgId });
749
+ }
750
+ /** Store a tarball and the pointer that names it. */
751
+ async store(orgId, contentHash, tarball) {
752
+ const data = typeof tarball === "string" ? Buffer.from(tarball) : tarball;
753
+ const digest = SourceCache.computeDigest(data);
754
+ await this.storage.put(sourceTarballKey(orgId, digest), data);
755
+ await this.storage.put(sourcePointerKey(orgId, contentHash), digest);
756
+ logger$3.info(`store(${contentHash}): stored (${data.length} bytes)`, {
757
+ orgId,
758
+ digest: digest.slice(0, 12)
759
+ });
760
+ }
761
+ /** SHA-256 of a tarball's bytes. The agent computes the same value. */
762
+ static computeDigest(data) {
763
+ return sha256(data);
764
+ }
765
+ /** Remove a source tarball and its pointer. */
766
+ async remove(orgId, contentHash) {
767
+ const digest = await this.resolvePointer(orgId, contentHash);
768
+ await this.storage.delete(sourcePointerKey(orgId, contentHash));
769
+ const removed = digest ? await this.storage.delete(sourceTarballKey(orgId, digest)) : false;
770
+ logger$3.info(`remove(${contentHash}): ${removed ? "removed" : "not found"}`, { orgId });
663
771
  return removed;
664
772
  }
665
773
  };
@@ -700,15 +808,23 @@ function depTarballKey(depsHash, platform, arch) {
700
808
  }
701
809
  /**
702
810
  * 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`.
811
+ * tarball built from it: `deps/{platform}-{arch}/{lockfileHash}.hash`, or
812
+ * `…/{lockfileHash}-{siblingsDigest}.hash` when the lock carries one.
813
+ *
814
+ * `siblingsDigest` is part of the key because the deps tarball carries in-repo
815
+ * `workspace:` sibling directories WITH their built output, and editing a
816
+ * sibling's source moves no package-manager lock file — so a `lockfileHash`-only
817
+ * key restored the sibling's stale build over the fresh clone on every run. A
818
+ * lock with no in-repo sibling carries no digest and keys exactly as before, so
819
+ * existing pointers stay live.
704
820
  *
705
821
  * This is the one mutable object in the scheme. A concurrent write replaces a
706
822
  * pointer wholesale — there is no window in which it is half-written — so the
707
823
  * worst a racing pair of builders can do is leave whichever pointer landed last,
708
824
  * and both tarballs remain valid and fetchable.
709
825
  */
710
- function depPointerKey(lockfileHash, platform, arch) {
711
- return `deps/${platform}-${arch}/${lockfileHash}.hash`;
826
+ function depPointerKey(lockfileHash, platform, arch, siblingsDigest) {
827
+ return `deps/${platform}-${arch}/${siblingsDigest ? `${lockfileHash}-${siblingsDigest}` : lockfileHash}.hash`;
712
828
  }
713
829
  var DepCache = class DepCache {
714
830
  storage;
@@ -736,8 +852,8 @@ var DepCache = class DepCache {
736
852
  * Resolve a lockfile to the content hash of the tarball built from it, or
737
853
  * null when no pointer exists. The single place the indirection is read.
738
854
  */
739
- async resolvePointer(lockfileHash, platform, arch, ttlMsOverride) {
740
- return (await this.storage.get(depPointerKey(lockfileHash, platform, arch), ttlMsOverride))?.toString("utf-8").trim() || null;
855
+ async resolvePointer(lockfileHash, platform, arch, ttlMsOverride, siblingsDigest) {
856
+ return (await this.storage.get(depPointerKey(lockfileHash, platform, arch, siblingsDigest), ttlMsOverride))?.toString("utf-8").trim() || null;
741
857
  }
742
858
  /**
743
859
  * Check if a dep tarball exists in cache.
@@ -745,9 +861,9 @@ var DepCache = class DepCache {
745
861
  * Both halves must be present: a pointer whose tarball has aged out is a miss,
746
862
  * not a hit, or the caller skips a rebuild and dispatches a URL that 404s.
747
863
  */
748
- async has(lockfileHash, platform, arch) {
864
+ async has(lockfileHash, platform, arch, siblingsDigest) {
749
865
  const ttlMsOverride = await this.resolveTtlMsOverride();
750
- const depsHash = await this.resolvePointer(lockfileHash, platform, arch, ttlMsOverride);
866
+ const depsHash = await this.resolvePointer(lockfileHash, platform, arch, ttlMsOverride, siblingsDigest);
751
867
  if (!depsHash) {
752
868
  logger$2.debug(`has(${lockfileHash}): false (no pointer)`, {
753
869
  platform,
@@ -766,8 +882,8 @@ var DepCache = class DepCache {
766
882
  * Get a pre-signed download URL for the dep tarball (for agent delivery).
767
883
  * Refreshes TTL on hit.
768
884
  */
769
- async getUrl(lockfileHash, platform, arch) {
770
- return (await this.getUrlAndHash(lockfileHash, platform, arch))?.url ?? null;
885
+ async getUrl(lockfileHash, platform, arch, siblingsDigest) {
886
+ return (await this.getUrlAndHash(lockfileHash, platform, arch, siblingsDigest))?.url ?? null;
771
887
  }
772
888
  /**
773
889
  * Get a pre-signed download URL and the tarball content hash.
@@ -775,9 +891,9 @@ var DepCache = class DepCache {
775
891
  * Returns null on cache miss — including an entry with no pointer, which is
776
892
  * unverifiable and so is deliberately not served.
777
893
  */
778
- async getUrlAndHash(lockfileHash, platform, arch) {
894
+ async getUrlAndHash(lockfileHash, platform, arch, siblingsDigest) {
779
895
  const ttlMsOverride = await this.resolveTtlMsOverride();
780
- const pointerKey = depPointerKey(lockfileHash, platform, arch);
896
+ const pointerKey = depPointerKey(lockfileHash, platform, arch, siblingsDigest);
781
897
  const hash = (await this.storage.get(pointerKey, ttlMsOverride))?.toString("utf-8").trim() || void 0;
782
898
  if (!hash) {
783
899
  logger$2.debug(`getUrlAndHash(${lockfileHash}): miss (no pointer)`, {
@@ -819,8 +935,8 @@ var DepCache = class DepCache {
819
935
  * Called only after the agent confirms its upload completed. Publishing before
820
936
  * the bytes land would let a reader follow the pointer to a missing object.
821
937
  */
822
- async publishPointer(lockfileHash, platform, arch, depsHash) {
823
- await this.storage.put(depPointerKey(lockfileHash, platform, arch), depsHash);
938
+ async publishPointer(lockfileHash, platform, arch, depsHash, siblingsDigest) {
939
+ await this.storage.put(depPointerKey(lockfileHash, platform, arch, siblingsDigest), depsHash);
824
940
  logger$2.info(`publishPointer(${lockfileHash}) -> ${depsHash.slice(0, 12)}`, {
825
941
  platform,
826
942
  arch
@@ -944,7 +1060,7 @@ const UNAMBIGUOUS_LEGACY_KEY = /^[a-z0-9_][a-z0-9._-]*$/;
944
1060
  * - a trailing `.` is stripped by NTFS, and a reserved device name cannot be
945
1061
  * created there at all.
946
1062
  *
947
- * A key failing any clause simply gets no legacy fallback — it is re-fetched
1063
+ * A key failing any clause gets no legacy fallback — it is re-fetched
948
1064
  * and re-saved under the discriminated key, which is always correct.
949
1065
  */
950
1066
  function unambiguousLegacyKey(key) {
@@ -2077,6 +2193,11 @@ Object.freeze({
2077
2193
  approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
2078
2194
  approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
2079
2195
  });
2196
+ z.enum([
2197
+ "stored",
2198
+ "unconfigured",
2199
+ "read-failure"
2200
+ ]);
2080
2201
  //#endregion
2081
2202
  //#region src/security/trust-policy-store.ts
2082
2203
  /**
@@ -2744,6 +2865,23 @@ var AdminApiClient = class {
2744
2865
  * Uses GitHub App installation tokens for repository access.
2745
2866
  */
2746
2867
  /**
2868
+ * Create an Octokit instance authenticated as the GitHub App itself (a JWT
2869
+ * signed with the App private key), not as one of its installations.
2870
+ *
2871
+ * App-level endpoints — `GET /app`, `GET /app/installations`, and the webhook
2872
+ * delivery endpoints under `/app/hook/deliveries` — accept only this
2873
+ * credential. Installation tokens are rejected there.
2874
+ */
2875
+ function createAppOctokit(config) {
2876
+ return new Octokit({
2877
+ authStrategy: createAppAuth,
2878
+ auth: {
2879
+ appId: config.appId,
2880
+ privateKey: config.privateKey
2881
+ }
2882
+ });
2883
+ }
2884
+ /**
2747
2885
  * Create an Octokit instance authenticated as a GitHub App installation.
2748
2886
  *
2749
2887
  * Uses @octokit/auth-app to handle:
@@ -2842,22 +2980,13 @@ async function convertManifestCode(code, deps = {}) {
2842
2980
  htmlUrl: d.html_url
2843
2981
  };
2844
2982
  }
2845
- function appOctokitFor(creds) {
2846
- return new Octokit({
2847
- authStrategy: createAppAuth,
2848
- auth: {
2849
- appId: creds.appId,
2850
- privateKey: creds.privateKey
2851
- }
2852
- });
2853
- }
2854
2983
  /**
2855
2984
  * Poll GitHub (as the App, via a JWT) until at least one installation exists,
2856
2985
  * returning the first installation's id + account login. Throws on timeout.
2857
2986
  */
2858
2987
  async function waitForInstallation(creds, opts) {
2859
2988
  const now = opts.now ?? Date.now;
2860
- const octokit = opts.appOctokit ?? appOctokitFor(creds);
2989
+ const octokit = opts.appOctokit ?? createAppOctokit(creds);
2861
2990
  const deadline = now() + opts.timeoutMs;
2862
2991
  for (;;) {
2863
2992
  const { data } = await octokit.request("GET /app/installations", { per_page: 1 });
@@ -15,6 +15,39 @@ export declare function setPendingAttestations(value: number): void;
15
15
  export declare function setPendingAttestationOldestAgeSeconds(value: number): void;
16
16
  /** Set the current number of terminally-rejected deferred attestations. */
17
17
  export declare function setRejectedAttestations(value: number): void;
18
+ /**
19
+ * Why a deferred attestation was refused at capture, BEFORE it reached the
20
+ * outbox. Distinct from `kici_orch_rejected_attestations_current`, which counts
21
+ * rows the retrier later gave up on: this counts statements the orchestrator
22
+ * declined to sign anything about, so no row was ever written.
23
+ *
24
+ * `build_context_mismatch` is the reason an agent too old to receive the job's
25
+ * canonical build context produces, so it also counts an upgrade the fleet has
26
+ * not taken yet. Every other reason means a job produced a statement that
27
+ * contradicts the orchestrator's own run row — investigate the job, not the
28
+ * orchestrator.
29
+ */
30
+ export declare const DeferredAttestationRejectReason: {
31
+ /** The DSSE payload did not decode, or did not parse as a KiCI statement. */
32
+ readonly Unparseable: 'unparseable';
33
+ /** The agent-supplied `statementHash` did not match the envelope's bytes. */
34
+ readonly StatementHashMismatch: 'statement_hash_mismatch';
35
+ /** The frozen statement disagrees with the run row on a build fact. */
36
+ readonly BuildContextMismatch: 'build_context_mismatch';
37
+ /** The reported subject digest appears in no subject of the statement. */
38
+ readonly SubjectDigestMismatch: 'subject_digest_mismatch';
39
+ /** The run or job named by the capture does not exist. */
40
+ readonly RunNotFound: 'run_not_found';
41
+ };
42
+ export type DeferredAttestationRejectReason = (typeof DeferredAttestationRejectReason)[keyof typeof DeferredAttestationRejectReason];
43
+ /**
44
+ * Deferred provenance attestations refused at capture.
45
+ * Labels:
46
+ * - reason: one of `DeferredAttestationRejectReason`
47
+ */
48
+ export declare const deferredAttestationRejectedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
49
+ /** Record one refused deferred-attestation capture. */
50
+ export declare function incDeferredAttestationRejected(reason: DeferredAttestationRejectReason): void;
18
51
  /** Record the collation-drift flag (1 = drift, 0 = clean) for a database. */
19
52
  export declare function setDbCollationDrift(database: string, drifted: boolean): void;
20
53
  interface ScalerUsageRow {
@@ -193,8 +226,21 @@ export declare function resetIngestOverflowMetricState(): void;
193
226
  export declare const ingestOverflowCapturedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
194
227
  /** Buffered deliveries successfully replayed through normal ingest. */
195
228
  export declare const ingestOverflowReplayedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
196
- /** Buffered deliveries permanently dropped. Labels: reason (cap_full|max_attempts). */
229
+ /** Buffered deliveries permanently dropped. Labels: reason (cap_full|max_attempts|max_age). */
197
230
  export declare const ingestOverflowDroppedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
231
+ /**
232
+ * Replay reservations the admission controller refused. This is the drain's own
233
+ * backpressure signal, kept off `kici_orch_ingest_shed_total` because that one
234
+ * means "a sender was refused" and a background drain deciding not to start is
235
+ * not that.
236
+ *
237
+ * It is also the evidence that the gate can still stop the replayer: a drain
238
+ * that converges proves the mechanism works, while a non-zero count here during
239
+ * that drain proves the reservation is refusable rather than an exempt lane.
240
+ *
241
+ * Labels: reason (loop_overload|queue_full).
242
+ */
243
+ export declare const ingestOverflowReplayRefusedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
198
244
  /**
199
245
  * Total webhooks received by the orchestrator.
200
246
  * Labels:
@@ -232,6 +278,15 @@ export declare const trustPolicyDecisionsTotal: import("@opentelemetry/api").Cou
232
278
  * shows as a divergence between the two rather than as silence.
233
279
  */
234
280
  export declare const forkEventsIgnoredTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
281
+ /**
282
+ * Record when the approval directory this orchestrator authorizes against was
283
+ * last refreshed — the cached row's write time at boot, and the arrival time on
284
+ * every later Platform push.
285
+ *
286
+ * Never called on an orchestrator that has received no directory, which is what
287
+ * leaves the gauge below unset rather than reporting an age of zero.
288
+ */
289
+ export declare function setTrustDirectoryUpdatedAt(updatedAt: Date): void;
235
290
  /**
236
291
  * Duration of trigger matching operations (lock file evaluation).
237
292
  * Buckets: 1ms to 5s covering fast rejects through complex evaluations.
@@ -249,6 +304,22 @@ export declare const dedupHitsTotal: import("@opentelemetry/api").Counter<import
249
304
  * - source: idle-pool | client
250
305
  */
251
306
  export declare const pgPoolClientErrorsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
307
+ /**
308
+ * Post-boot `CREATE INDEX CONCURRENTLY` builds, which run outside the
309
+ * migration set so an index on a volume-scaling table never locks that table
310
+ * during an upgrade.
311
+ * Labels:
312
+ * - name: index name
313
+ * - outcome: ok | error
314
+ */
315
+ export declare const deferredIndexBuildsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
316
+ /**
317
+ * Rows removed by the database-side retention sweep, which prunes the warm
318
+ * tables directly for installs that run no object store.
319
+ * Labels:
320
+ * - table: the table the rows came from
321
+ */
322
+ export declare const retentionRowsDeletedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
252
323
  /**
253
324
  * Total source cache hits (`.kici/` source tarball found for content hash).
254
325
  */
@@ -557,6 +628,19 @@ export declare const installSecretsRegistryUsedTotal: import("@opentelemetry/api
557
628
  * - trust_tier: unknown | known (the `trusted` tier does not strip and therefore never fires this counter)
558
629
  */
559
630
  export declare const installSecretsContributorStrippedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
631
+ /**
632
+ * Dispatches where a job's container-registry credentials were withheld because
633
+ * the contributor tier was not `trusted`.
634
+ *
635
+ * The sibling of `installSecretsContributorStrippedTotal`, for the other class
636
+ * of credential a dispatch resolves orchestrator-side. Counted separately
637
+ * because the two fire on different jobs: install secrets are workflow-level
638
+ * and registry auth is per-job, so one run can strip both, either, or neither.
639
+ *
640
+ * Labels:
641
+ * - trust_tier: unknown | known (the `trusted` tier does not strip and therefore never fires this counter)
642
+ */
643
+ export declare const containerRegistryAuthContributorStrippedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
560
644
  /**
561
645
  * Per-environment latency of `secretResolver.resolveForJob` calls in the
562
646
  * install-secrets path. Buckets mirror `triggerMatchDurationSeconds`:
@@ -566,6 +650,29 @@ export declare const installSecretsContributorStrippedTotal: import("@openteleme
566
650
  * - environment: the environment name referenced in the qualified `<environment>:<secret>` ref (per-org count is typically <10)
567
651
  */
568
652
  export declare const installSecretsTokenResolutionDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
653
+ /**
654
+ * Recovery, ack-requeue and cancel actions this coordinator declined to take
655
+ * because the DB says the row is not its to act on — owned by a live sibling,
656
+ * already reclaimed, or already acked elsewhere.
657
+ *
658
+ * Labels:
659
+ * - action: startup_recovery | recovery_expiry | ack_expiry | ack_sweep
660
+ * - reason: owned_elsewhere | unknown_owner | already_resolved
661
+ */
662
+ export declare const dispatchOwnershipSkippedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
663
+ /**
664
+ * `execution_jobs` status writes the monotonicity guard rejected where the
665
+ * stored row contradicts the frame. A non-zero rate means an agent is still
666
+ * reporting on a job this orchestrator already reaped, with a different verdict.
667
+ *
668
+ * A rejected write whose stored row AGREES with the frame is not counted: an
669
+ * orchestrator-side writer reached the same verdict first, which is a race the
670
+ * cancel path takes deliberately, not a divergence.
671
+ *
672
+ * Labels:
673
+ * - rejected_status: the status the late frame carried
674
+ */
675
+ export declare const nonMonotonicJobStatusWritesTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
569
676
  /**
570
677
  * Terminal dispatch_queue rows deleted by the retention sweep (the cleanup
571
678
  * tick's `pruneTerminalDispatchRows`).
@@ -610,5 +717,13 @@ export declare const wsPlatformCapabilityGapTotal: import("@opentelemetry/api").
610
717
  * Platform push. Idempotent: repeat calls are no-ops.
611
718
  */
612
719
  export declare function registerOrchestratorMetrics(): void;
720
+ /** Record whether the LISTEN subscription on `channel` currently holds a client. */
721
+ export declare function setPgListenConnected(channel: string, connected: boolean): void;
722
+ /**
723
+ * LISTEN subscriptions re-established after losing their connection.
724
+ * Labels:
725
+ * - channel: the LISTEN channel name
726
+ */
727
+ export declare const pgListenReconnectsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
613
728
  export {};
614
729
  //# sourceMappingURL=prometheus.d.ts.map
@@ -16,8 +16,27 @@ import type { Signer } from './signer.js';
16
16
  export declare const SIGNING_KEY_AAD = "orchestrator-signing-key";
17
17
  /** Encrypt a private JWK with the master key. Returns the base64 wrapped blob. */
18
18
  export declare function wrapPrivateJwk(privateJwk: JWK, secretKey: string): string;
19
- /** Decrypt a wrapped private JWK with the master key. Throws on wrong key / tamper. */
20
- export declare function unwrapPrivateJwk(encrypted: string, secretKey: string): JWK;
19
+ /**
20
+ * Decrypt a wrapped private JWK with the master key, falling back to the old
21
+ * key during a rotation grace window. Same dual-key pattern as
22
+ * `PgSecretStore.decryptWithFallback` and `decryptConfigFields`. Throws on
23
+ * wrong key(s) / tamper.
24
+ *
25
+ * The fallback is what keeps `rotate-key` from bricking the next boot: the
26
+ * signing key is master-key-wrapped, so a rotation that re-encrypts it leaves
27
+ * every already-running node reading it under the old key, and a rotation that
28
+ * does not still leaves this row sealed under the previous generation.
29
+ */
30
+ export declare function unwrapPrivateJwk(encrypted: string, secretKey: string, oldSecretKey?: string): JWK;
31
+ /**
32
+ * Unwrap a private JWK and report which key opened it, so a caller that can
33
+ * re-seal the row (the boot self-heal) knows the row is stranded under the old
34
+ * key rather than current.
35
+ */
36
+ export declare function unwrapPrivateJwkWithKeyAge(encrypted: string, secretKey: string, oldSecretKey?: string): {
37
+ privateJwk: JWK;
38
+ usedOldKey: boolean;
39
+ };
21
40
  export declare class DbSigner implements Signer {
22
41
  private readonly privateKey;
23
42
  private readonly publicJwk;
@@ -26,7 +45,7 @@ export declare class DbSigner implements Signer {
26
45
  readonly keyRef: null;
27
46
  private constructor();
28
47
  /** Import an EC P-256 private JWK NON-EXTRACTABLE and derive its public half. */
29
- private static fromPrivateJwk;
48
+ static fromPrivateJwk(privateJwk: JWK): Promise<DbSigner>;
30
49
  /**
31
50
  * Generate a fresh ES256 keypair, wrap the private half with the master key,
32
51
  * and return the signer + the artifacts to persist. The generated private key
@@ -39,8 +58,11 @@ export declare class DbSigner implements Signer {
39
58
  encryptedPrivateJwk: string;
40
59
  kid: string;
41
60
  }>;
42
- /** Reconstruct a signer from a wrapped private JWK. */
43
- static fromEncrypted(encryptedPrivateJwk: string, secretKey: string): Promise<DbSigner>;
61
+ /**
62
+ * Reconstruct a signer from a wrapped private JWK, accepting the old master
63
+ * key during a rotation grace window.
64
+ */
65
+ static fromEncrypted(encryptedPrivateJwk: string, secretKey: string, oldSecretKey?: string): Promise<DbSigner>;
44
66
  sign(data: Uint8Array): Promise<Uint8Array>;
45
67
  getPublicJwk(): Promise<JWK>;
46
68
  getKid(): Promise<string>;