@kici-dev/orchestrator 0.8.0 → 0.9.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 (63) hide show
  1. package/dist/__test-helpers__/test-db.d.ts +21 -0
  2. package/dist/app.d.ts +6 -5
  3. package/dist/artifacts/artifact-store.d.ts +6 -10
  4. package/dist/cache/legacy-prefixes.d.ts +19 -0
  5. package/dist/cache/pending-precursor-db-watcher.d.ts +79 -0
  6. package/dist/cache/pending-tracker.d.ts +10 -0
  7. package/dist/cache/precursor-result.d.ts +68 -0
  8. package/dist/cache/source-cache.d.ts +2 -1
  9. package/dist/cache/user-cache.d.ts +37 -58
  10. package/dist/cli/api-client.d.ts +3 -6
  11. package/dist/cli/commands/cache.d.ts +51 -0
  12. package/dist/cli/commands/cluster-settings.d.ts +0 -11
  13. package/dist/cli/commands/trust-policy.d.ts +12 -42
  14. package/dist/cli/service/privilege.d.ts +27 -6
  15. package/dist/cli.js +1138 -845
  16. package/dist/cluster/cluster-settings-reader.d.ts +1 -3
  17. package/dist/cluster/coordinator.d.ts +2 -3
  18. package/dist/cluster/join-client.d.ts +1 -39
  19. package/dist/config.d.ts +0 -6
  20. package/dist/contexts/context-store.d.ts +3 -3
  21. package/dist/contexts/held-runs.d.ts +6 -6
  22. package/dist/contexts/protection/aggregate.d.ts +5 -5
  23. package/dist/dashboard/handler.d.ts +1 -1
  24. package/dist/db/migrations/142_sweep_deprecated_columns.d.ts +4 -0
  25. package/dist/db/migrations/143_execution_jobs_precursor_result.d.ts +26 -0
  26. package/dist/db/migrations/144_execution_runs_registration_window.d.ts +32 -0
  27. package/dist/db/types.d.ts +27 -13
  28. package/dist/index.js +115 -156
  29. package/dist/metrics/prometheus.d.ts +2 -2
  30. package/dist/oidc/id-token-claims.d.ts +9 -10
  31. package/dist/oidc/local-mint.d.ts +9 -12
  32. package/dist/oidc/oidc-mint-registration.d.ts +15 -28
  33. package/dist/oidc/orchestrator-mint.d.ts +10 -10
  34. package/dist/oidc/resolve-signer.d.ts +40 -0
  35. package/dist/orchestrator-core.d.ts +11 -0
  36. package/dist/pipeline/dispatch-matched-workflow.d.ts +1 -2
  37. package/dist/pipeline/job-contexts.d.ts +2 -2
  38. package/dist/provenance/attestation-retrier.d.ts +11 -1
  39. package/dist/provenance/retrier-mint.d.ts +35 -0
  40. package/dist/providers/github/normalizer.d.ts +1 -28
  41. package/dist/providers/local/normalizer.d.ts +1 -15
  42. package/dist/queue/job-queue.d.ts +2 -6
  43. package/dist/queue/terminalize-unroutable.d.ts +1 -1
  44. package/dist/reporting/execution-tracker.d.ts +107 -0
  45. package/dist/scaler/label-matcher.d.ts +0 -1
  46. package/dist/scaler/manager.d.ts +2 -24
  47. package/dist/scaler/scaler-state-store.d.ts +2 -2
  48. package/dist/security/global-workflow-policy.d.ts +3 -28
  49. package/dist/security/lock-source.d.ts +1 -1
  50. package/dist/security/trust-policy-gate.d.ts +0 -14
  51. package/dist/security/trust-policy-store.d.ts +2 -3
  52. package/dist/security/trust-resolver.d.ts +1 -2
  53. package/dist/server.js +2666 -2471
  54. package/dist/stale-detector/all-terminal-runs.d.ts +31 -0
  55. package/dist/stale-detector/stale-run-detector.d.ts +20 -0
  56. package/dist/standalone.js +2818 -2561
  57. package/dist/ws/dashboard-global-workflows-handler.d.ts +3 -4
  58. package/dist/ws/platform-client.d.ts +0 -15
  59. package/installer-image-digests.json +3 -3
  60. package/package.json +18 -17
  61. package/sbom.spdx.json +1178 -1352
  62. package/dist/ws/oidc-token-relay.d.ts +0 -79
  63. package/dist/ws/orch-rpc.d.ts +0 -9
package/dist/index.js CHANGED
@@ -629,7 +629,8 @@ function createCacheStorage(config) {
629
629
  * the workflow entry file alone rather than the tree the tarball holds — the
630
630
  * defect this layout closes — so serving it would serve the bug. For a v6 lock
631
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.
632
+ * Old objects are orphaned. The cache TTL is enforced lazily on access, so
633
+ * nothing here removes them; `kici-admin cache purge-legacy` does.
633
634
  */
634
635
  const logger$3 = createLogger({ prefix: "source-cache" });
635
636
  /**
@@ -1030,10 +1031,32 @@ const DEFAULT_USER_CACHE_QUOTA_BYTES = 5368709120;
1030
1031
  const DEFAULT_USER_CACHE_TTL_MS = 6048e5;
1031
1032
  /** Tarball suffix for committed cache entries. */
1032
1033
  const TAR_SUFFIX = ".tar.gz";
1034
+ /**
1035
+ * Stem prefix of an in-flight upload, `.tmp-<uuid>.tar.gz`: the presigned PUT
1036
+ * target a save lands on before commit copies it to its final key. Shared with
1037
+ * the retired-layout classifier so both agree on what an upload looks like.
1038
+ */
1039
+ const TEMP_UPLOAD_STEM = ".tmp-";
1040
+ /** Whether a listed object is an in-flight upload rather than a cache entry. */
1041
+ function isTempUpload(key) {
1042
+ return key.slice(key.lastIndexOf("/") + 1).startsWith(TEMP_UPLOAD_STEM);
1043
+ }
1033
1044
  /** Matches a stem ending in the fixed-width discriminator this module appends. */
1034
1045
  const KEY_DISCRIMINATOR_TAIL = new RegExp(`^(.*)-([0-9a-f]{32})$`);
1035
1046
  /**
1036
- * Recover the reported cache key from a stored object's stem.
1047
+ * Whether a listed object is a committed entry, `<stem>-<discriminator>.tar.gz`.
1048
+ *
1049
+ * This is the filter the `restoreKeys` prefix scan applies to a listing, and it
1050
+ * is what keeps two other kinds of object out of a restore: an in-flight
1051
+ * `.tmp-<uuid>.tar.gz` upload, which is a partial tarball until commit copies
1052
+ * it to its final key, and an object under the retired un-discriminated layout,
1053
+ * which nothing writes and which `kici-admin cache purge-legacy` removes.
1054
+ */
1055
+ function isCommittedEntry(key) {
1056
+ return key.endsWith(TAR_SUFFIX) && KEY_DISCRIMINATOR_TAIL.test(key.slice(0, -7));
1057
+ }
1058
+ /**
1059
+ * Recover the reported cache key from a committed entry's stem.
1037
1060
  *
1038
1061
  * The value on this path is already the sanitized, lossy form, so this only
1039
1062
  * needs to undo the discriminator the key format appends. A stem that itself
@@ -1043,28 +1066,74 @@ const KEY_DISCRIMINATOR_TAIL = new RegExp(`^(.*)-([0-9a-f]{32})$`);
1043
1066
  function stripKeyDiscriminator(stem) {
1044
1067
  return KEY_DISCRIMINATOR_TAIL.exec(stem)?.[1] ?? stem;
1045
1068
  }
1046
- /** Device names NTFS reserves; a file cannot actually be created under one. */
1047
- const WINDOWS_RESERVED_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/;
1048
- /** Keys that cannot have been part of a case collision in the old key format. */
1049
- const UNAMBIGUOUS_LEGACY_KEY = /^[a-z0-9_][a-z0-9._-]*$/;
1050
1069
  /**
1051
- * Whether a cache key is safe to look up in the pre-discriminator key format.
1070
+ * Sanitize a path segment so a key can never escape its org/repo/scope
1071
+ * namespace. Beyond stripping disallowed characters, a segment consisting
1072
+ * only of dots (`.`, `..`, …) is replaced wholesale: such a segment is a
1073
+ * dot-segment that HTTP/S3 path canonicalization collapses (`a/./b` → `a/b`,
1074
+ * `a/../b` → `b`), which both corrupts the namespace and breaks the SigV4
1075
+ * signature on a pre-signed PUT/GET. Repo identifiers like `.` (the internal
1076
+ * provider's repo id) hit exactly this case, so the all-dots guard keeps the
1077
+ * object key canonical and the namespace boundary intact.
1052
1078
  *
1053
- * Every clause rules out a way the old format could have folded two distinct
1054
- * keys onto one object:
1079
+ * Exported so an operator tool that addresses an org's prefix from a raw org id
1080
+ * (`kici-admin cache purge-legacy --org`) lands on the segment the writer used.
1081
+ */
1082
+ function sanitizeSegment(s) {
1083
+ const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
1084
+ return /^\.+$/.test(cleaned) ? `_${cleaned}` : cleaned;
1085
+ }
1086
+ /** Org-level prefix: the per-tenant isolation boundary and quota scope. */
1087
+ function orgPrefix(ref) {
1088
+ return `cache/${sanitizeSegment(ref.org)}/`;
1089
+ }
1090
+ /** Org + repo prefix shared by every scope of a repo. */
1091
+ function repoPrefix(ref) {
1092
+ return `${orgPrefix(ref)}${sanitizeSegment(ref.repo)}`;
1093
+ }
1094
+ /** Namespace prefix for the WRITE scope of a ref (shared OR per-run isolated). */
1095
+ function writePrefix(ref) {
1096
+ const base = repoPrefix(ref);
1097
+ if (ref.scope === "isolated") {
1098
+ if (!ref.runId) throw new Error("isolated cache scope requires a runId");
1099
+ return `${base}/iso/${sanitizeSegment(ref.runId)}/`;
1100
+ }
1101
+ return `${base}/shared/`;
1102
+ }
1103
+ /** Namespace prefixes the ref may READ, in priority order. Isolated reads its own run scope, then shared. */
1104
+ function readPrefixes(ref) {
1105
+ const base = repoPrefix(ref);
1106
+ if (ref.scope === "isolated") {
1107
+ if (!ref.runId) throw new Error("isolated cache scope requires a runId");
1108
+ return [`${base}/iso/${sanitizeSegment(ref.runId)}/`, `${base}/shared/`];
1109
+ }
1110
+ return [`${base}/shared/`];
1111
+ }
1112
+ /**
1113
+ * Object key for a committed cache entry.
1055
1114
  *
1056
- * - the character class is a subset of what `seg()` preserves, so a match
1057
- * implies the key survives sanitization unchanged (no many-to-one rewrite)
1058
- * and is pure ASCII, which rules out HFS+ Unicode normalisation;
1059
- * - requiring lowercase rules out case folding, the collision this change fixes;
1060
- * - a trailing `.` is stripped by NTFS, and a reserved device name cannot be
1061
- * created there at all.
1115
+ * The trailing discriminator is a hash of the EXACT cache key, and it is what
1116
+ * keeps `build` and `Build` apart on a case-insensitive namespace — the
1117
+ * filesystem backend on a macOS or Windows host, where the two would
1118
+ * otherwise resolve to one object and a restore could return the other key's
1119
+ * tarball.
1062
1120
  *
1063
- * A key failing any clause gets no legacy fallback — it is re-fetched
1064
- * and re-saved under the discriminated key, which is always correct.
1121
+ * It is deliberately a SUFFIX: `restoreByPrefix` matches `restoreKeys` by
1122
+ * string prefix, so appending leaves those semantics untouched. The only
1123
+ * parse that has to know about it is the `matchedKey` slice.
1124
+ */
1125
+ function finalKey(prefix, key) {
1126
+ return `${prefix}${sanitizeSegment(key)}-${keyDiscriminator(key)}${TAR_SUFFIX}`;
1127
+ }
1128
+ /**
1129
+ * The object key a committed save under `key` lands on for this ref. This is
1130
+ * the writer's own path — `beginSave` and `commitSave` address the entry
1131
+ * through it — so anything that must recognise a current-layout key (the
1132
+ * legacy-layout classifier tests) takes it from here rather than restating
1133
+ * the format.
1065
1134
  */
1066
- function unambiguousLegacyKey(key) {
1067
- return UNAMBIGUOUS_LEGACY_KEY.test(key) && !key.endsWith(".") && !WINDOWS_RESERVED_NAME.test(key);
1135
+ function userCacheEntryKey(ref, key) {
1136
+ return finalKey(writePrefix(ref), key);
1068
1137
  }
1069
1138
  var UserCache = class {
1070
1139
  storage;
@@ -1105,105 +1174,10 @@ var UserCache = class {
1105
1174
  ttlMs: limits.ttlMs ?? this.defaultTtlMs
1106
1175
  };
1107
1176
  }
1108
- /**
1109
- * Sanitize a path segment so a key can never escape its org/repo/scope
1110
- * namespace. Beyond stripping disallowed characters, a segment consisting
1111
- * only of dots (`.`, `..`, …) is replaced wholesale: such a segment is a
1112
- * dot-segment that HTTP/S3 path canonicalization collapses (`a/./b` → `a/b`,
1113
- * `a/../b` → `b`), which both corrupts the namespace and breaks the SigV4
1114
- * signature on a pre-signed PUT/GET. Repo identifiers like `.` (the internal
1115
- * provider's repo id) hit exactly this case, so the all-dots guard keeps the
1116
- * object key canonical and the namespace boundary intact.
1117
- */
1118
- seg(s) {
1119
- const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
1120
- return /^\.+$/.test(cleaned) ? `_${cleaned}` : cleaned;
1121
- }
1122
- /** Org-level prefix: the per-tenant isolation boundary and quota scope. */
1123
- orgPrefix(ref) {
1124
- return `cache/${this.seg(ref.org)}/`;
1125
- }
1126
- /** Org + repo prefix shared by every scope of a repo. */
1127
- repoPrefix(ref) {
1128
- return `${this.orgPrefix(ref)}${this.seg(ref.repo)}`;
1129
- }
1130
- /** Namespace prefix for the WRITE scope of a ref (shared OR per-run isolated). */
1131
- writePrefix(ref) {
1132
- const base = this.repoPrefix(ref);
1133
- if (ref.scope === "isolated") {
1134
- if (!ref.runId) throw new Error("isolated cache scope requires a runId");
1135
- return `${base}/iso/${this.seg(ref.runId)}/`;
1136
- }
1137
- return `${base}/shared/`;
1138
- }
1139
- /** Namespace prefixes the ref may READ, in priority order. Isolated reads its own run scope, then shared. */
1140
- readPrefixes(ref) {
1141
- const base = this.repoPrefix(ref);
1142
- if (ref.scope === "isolated") {
1143
- if (!ref.runId) throw new Error("isolated cache scope requires a runId");
1144
- return [`${base}/iso/${this.seg(ref.runId)}/`, `${base}/shared/`];
1145
- }
1146
- return [`${base}/shared/`];
1147
- }
1148
- /**
1149
- * Object key for a committed cache entry.
1150
- *
1151
- * The trailing discriminator is a hash of the EXACT cache key, and it is what
1152
- * keeps `build` and `Build` apart on a case-insensitive namespace — the
1153
- * filesystem backend on a macOS or Windows host, where the two would
1154
- * otherwise resolve to one object and a restore could return the other key's
1155
- * tarball.
1156
- *
1157
- * It is deliberately a SUFFIX: `restoreByPrefix` matches `restoreKeys` by
1158
- * string prefix, so appending leaves those semantics untouched. The only
1159
- * parse that has to know about it is the `matchedKey` slice.
1160
- */
1161
- finalKey(prefix, key) {
1162
- return `${prefix}${this.seg(key)}-${keyDiscriminator(key)}${TAR_SUFFIX}`;
1163
- }
1164
- /**
1165
- * The pre-discriminator object key.
1166
- *
1167
- * @deprecated Read-only compatibility path for entries written before the key
1168
- * carried a discriminator. Nothing writes this format; it is removed at the
1169
- * next major, by which point every such entry has aged out of the cache TTL.
1170
- */
1171
- legacyFinalKey(prefix, key) {
1172
- return `${prefix}${this.seg(key)}${TAR_SUFFIX}`;
1173
- }
1174
- /**
1175
- * Restore an entry still stored in the pre-discriminator key format.
1176
- *
1177
- * Two gates, and BOTH are load-bearing — dropping either re-creates the
1178
- * wrong-cache-hit this change fixes:
1179
- *
1180
- * 1. `unambiguousLegacyKey` statically rules out any key that could have
1181
- * collided with a case variant in the old format.
1182
- * 2. The listed name must match byte-exactly. A case-insensitive backend
1183
- * resolves `getUrl('build')` to an object created as `Build`, but its
1184
- * *listing* reports the real created name — so comparing against the
1185
- * listing is what detects that the object is not really ours.
1186
- *
1187
- * @deprecated Removed at the next major; see `legacyFinalKey`.
1188
- */
1189
- async restoreLegacyExact(ref, prefix, ttlMs) {
1190
- if (!unambiguousLegacyKey(ref.key)) return null;
1191
- const legacyKey = this.legacyFinalKey(prefix, ref.key);
1192
- if (!(await this.storage.list(prefix)).includes(legacyKey)) return null;
1193
- const url = await this.storage.getUrl(legacyKey, ttlMs);
1194
- if (!url) return null;
1195
- await this.storage.touch(legacyKey);
1196
- return {
1197
- hit: true,
1198
- matchedKey: ref.key,
1199
- downloadUrl: url,
1200
- tarHash: await this.readHash(legacyKey)
1201
- };
1202
- }
1203
1177
  /** Restore: try the exact key across read prefixes, then restoreKeys prefix scan (newest wins). */
1204
1178
  async restore(ref) {
1205
1179
  const { ttlMs } = await this.resolveLimits(ref.org);
1206
- const prefixes = this.readPrefixes(ref);
1180
+ const prefixes = readPrefixes(ref);
1207
1181
  const exact = await this.restoreExact(ref, prefixes, ttlMs);
1208
1182
  if (exact) return exact;
1209
1183
  return await this.restoreByPrefix(ref, prefixes, ttlMs) ?? { hit: false };
@@ -1211,7 +1185,7 @@ var UserCache = class {
1211
1185
  /** Try the exact key in read-prefix priority order. */
1212
1186
  async restoreExact(ref, prefixes, ttlMs) {
1213
1187
  for (const prefix of prefixes) {
1214
- const key = this.finalKey(prefix, ref.key);
1188
+ const key = finalKey(prefix, ref.key);
1215
1189
  const url = await this.storage.getUrl(key, ttlMs);
1216
1190
  if (url) {
1217
1191
  await this.storage.touch(key);
@@ -1222,15 +1196,13 @@ var UserCache = class {
1222
1196
  tarHash: await this.readHash(key)
1223
1197
  };
1224
1198
  }
1225
- const legacy = await this.restoreLegacyExact(ref, prefix, ttlMs);
1226
- if (legacy) return legacy;
1227
1199
  }
1228
1200
  return null;
1229
1201
  }
1230
1202
  /** restoreKeys prefix fallback (ordered); within a prefix, list() returns newest-first. */
1231
1203
  async restoreByPrefix(ref, prefixes, ttlMs) {
1232
1204
  for (const rk of ref.restoreKeys ?? []) for (const prefix of prefixes) {
1233
- const matches = (await this.storage.list(`${prefix}${this.seg(rk)}`)).filter((k) => k.endsWith(TAR_SUFFIX));
1205
+ const matches = (await this.storage.list(`${prefix}${sanitizeSegment(rk)}`)).filter(isCommittedEntry);
1234
1206
  if (matches.length === 0) continue;
1235
1207
  const winner = matches[0];
1236
1208
  const url = await this.storage.getUrl(winner, ttlMs);
@@ -1247,13 +1219,12 @@ var UserCache = class {
1247
1219
  }
1248
1220
  /** Begin a save: presigned PUT to a temp key, or skip=true when the immutable key exists. */
1249
1221
  async beginSave(ref) {
1250
- const prefix = this.writePrefix(ref);
1251
- const final = this.finalKey(prefix, ref.key);
1222
+ const final = userCacheEntryKey(ref, ref.key);
1252
1223
  if (await this.storage.has(final)) {
1253
1224
  logger$1.info("user-cache save skipped (immutable key exists)", { key: ref.key });
1254
1225
  return { skip: true };
1255
1226
  }
1256
- const tempKey = `${prefix}.tmp-${randomUUID()}${TAR_SUFFIX}`;
1227
+ const tempKey = `${writePrefix(ref)}${TEMP_UPLOAD_STEM}${randomUUID()}${TAR_SUFFIX}`;
1257
1228
  return {
1258
1229
  skip: false,
1259
1230
  uploadUrl: await this.storage.getUploadUrl(tempKey),
@@ -1262,8 +1233,7 @@ var UserCache = class {
1262
1233
  }
1263
1234
  /** Commit a save: copy temp -> final, init metadata, store companion hash/size, delete temp, enforce quota. */
1264
1235
  async commitSave(ref) {
1265
- const prefix = this.writePrefix(ref);
1266
- const final = this.finalKey(prefix, ref.key);
1236
+ const final = userCacheEntryKey(ref, ref.key);
1267
1237
  if (await this.storage.has(final)) return;
1268
1238
  if (ref.tempKey) {
1269
1239
  await this.storage.copy(ref.tempKey, final);
@@ -1281,11 +1251,18 @@ var UserCache = class {
1281
1251
  async readHash(key) {
1282
1252
  return (await this.storage.get(`${key}.hash`))?.toString("utf-8") || void 0;
1283
1253
  }
1284
- /** Evict least-recently-used entries for the org until total tarball size <= the per-org quota. */
1254
+ /**
1255
+ * Evict least-recently-used entries for the org until total tarball size <= the per-org quota.
1256
+ *
1257
+ * An in-flight `.tmp-` upload is never a candidate. A presigned PUT carries
1258
+ * no metadata, so it would rank oldest and be reclaimed first — freeing
1259
+ * nothing, since it has no `.size` — and the concurrent commit would then
1260
+ * fail on its copy to the final key. Retired un-discriminated entries stay
1261
+ * in the set: they hold accounted bytes and are evictable like any other.
1262
+ */
1285
1263
  async enforceQuota(ref) {
1286
1264
  const { quotaBytes } = await this.resolveLimits(ref.org);
1287
- const orgPrefix = this.orgPrefix(ref);
1288
- const keys = (await this.storage.list(orgPrefix)).filter((k) => k.endsWith(TAR_SUFFIX));
1265
+ const keys = (await this.storage.list(orgPrefix(ref))).filter((k) => k.endsWith(TAR_SUFFIX) && !isTempUpload(k));
1289
1266
  const sized = [];
1290
1267
  let total = 0;
1291
1268
  for (const k of keys) {
@@ -1818,12 +1795,12 @@ function deriveKeys(secret) {
1818
1795
  *
1819
1796
  * - `fork_pr` — the org trust policy's fork switch held the run.
1820
1797
  * - `context_trust` — a context's minimum-trust gate held the run.
1821
- * - `workflow_modification` — deprecated; no longer raised. Modifications to
1822
- * `.kici/` are surfaced on their own informational check and no longer feed
1823
- * a policy arm. Removed at v1.0.0.
1824
- * - `unknown_contributor` — deprecated; no longer raised. The policy turns on
1825
- * whether the pull request came from a fork, not on who opened it. Removed
1826
- * at v1.0.0.
1798
+ * - `workflow_modification` — no longer raised. Modifications to `.kici/`
1799
+ * are surfaced on their own informational check and no longer feed a policy
1800
+ * arm. Kept so stored `held_runs` rows with this reason still render.
1801
+ * - `unknown_contributor` — no longer raised. The policy turns on whether the
1802
+ * pull request came from a fork, not on who opened it. Kept so stored
1803
+ * `held_runs` rows with this reason still render.
1827
1804
  */
1828
1805
  const SecurityHoldReason = z.enum([
1829
1806
  "workflow_modification",
@@ -2176,20 +2153,15 @@ function createHeldRunStoreFromUrl(databaseUrl, opts) {
2176
2153
  }
2177
2154
  };
2178
2155
  }
2179
- z.enum(["policy", "legacy"]);
2180
2156
  /** The fork switch applied when no policy row is stored. */
2181
2157
  const DEFAULT_FORK_POLICY = ForkPolicy.enum.ignore;
2182
2158
  Object.freeze({
2183
2159
  forkPolicy: DEFAULT_FORK_POLICY,
2184
- unknownContributorPolicy: "hold",
2185
- workflowChangePolicy: "hold",
2186
2160
  approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
2187
2161
  approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
2188
2162
  });
2189
2163
  Object.freeze({
2190
2164
  forkPolicy: ForkPolicy.enum.hold,
2191
- unknownContributorPolicy: "hold",
2192
- workflowChangePolicy: "hold",
2193
2165
  approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
2194
2166
  approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
2195
2167
  });
@@ -2220,8 +2192,6 @@ const TrustPolicySource = z.enum(["platform", "local"]);
2220
2192
  */
2221
2193
  const DEFAULT_TRUST_POLICY = {
2222
2194
  forkPolicy: DEFAULT_FORK_POLICY,
2223
- unknownContributorPolicy: "hold",
2224
- workflowChangePolicy: "hold",
2225
2195
  approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
2226
2196
  approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
2227
2197
  };
@@ -2265,8 +2235,6 @@ var TrustPolicyStore = class {
2265
2235
  if (!row) return null;
2266
2236
  return {
2267
2237
  forkPolicy: row.fork_policy,
2268
- unknownContributorPolicy: row.unknown_contributor_policy,
2269
- workflowChangePolicy: row.workflow_change_policy,
2270
2238
  approvalExpiryHours: Number(row.approval_expiry_hours),
2271
2239
  approvalExpirySeconds: approvalExpirySecondsOf({
2272
2240
  approvalExpiryHours: Number(row.approval_expiry_hours),
@@ -2295,9 +2263,8 @@ var TrustPolicyStore = class {
2295
2263
  * COMMITTED two concurrent PATCHes both read the pre-existing row, and the
2296
2264
  * second `ON CONFLICT DO UPDATE` then overwrites every column from its own
2297
2265
  * stale merge — silently dropping the first operator's change (a tightened
2298
- * `unknownContributorPolicy` reverting to whatever the second caller last
2299
- * saw). The lock releases on commit or rollback, so there is no unlock to
2300
- * leak.
2266
+ * `forkPolicy` reverting to whatever the second caller last saw). The lock
2267
+ * releases on commit or rollback, so there is no unlock to leak.
2301
2268
  *
2302
2269
  * `onWrite` receives the same transaction and the merged result, so an audit
2303
2270
  * row written there commits or rolls back with the policy itself — a
@@ -2312,8 +2279,6 @@ var TrustPolicyStore = class {
2312
2279
  const existing = await this.get(orgId, trx);
2313
2280
  const merged = {
2314
2281
  forkPolicy: patch.forkPolicy ?? existing?.forkPolicy ?? DEFAULT_TRUST_POLICY.forkPolicy,
2315
- unknownContributorPolicy: patch.unknownContributorPolicy ?? existing?.unknownContributorPolicy ?? DEFAULT_TRUST_POLICY.unknownContributorPolicy,
2316
- workflowChangePolicy: patch.workflowChangePolicy ?? existing?.workflowChangePolicy ?? DEFAULT_TRUST_POLICY.workflowChangePolicy,
2317
2282
  ...mergedExpiry(patch, existing)
2318
2283
  };
2319
2284
  await this.write(orgId, merged, TrustPolicySource.enum.local, trx);
@@ -2325,8 +2290,6 @@ var TrustPolicyStore = class {
2325
2290
  const seconds = approvalExpirySecondsOf(policy);
2326
2291
  const columns = {
2327
2292
  fork_policy: policy.forkPolicy,
2328
- unknown_contributor_policy: policy.unknownContributorPolicy,
2329
- workflow_change_policy: policy.workflowChangePolicy,
2330
2293
  approval_expiry_hours: approvalExpiryHoursOf(seconds),
2331
2294
  approval_expiry_seconds: seconds,
2332
2295
  source,
@@ -2525,15 +2488,11 @@ var AdminApiClient = class {
2525
2488
  fs.writeFileSync(outPath, buf);
2526
2489
  }
2527
2490
  /**
2528
- * List secret scopes for an org.
2529
- *
2530
- * @param allBackends - Aggregate every registered backend and return scopes
2531
- * in qualified `<backend>:<path>` form. Default (false) returns the bare,
2532
- * pg-only listing; that default flips at v1.0.0.
2491
+ * List an org's secret scopes across every registered backend, each in
2492
+ * qualified `<backend>:<path>` form.
2533
2493
  */
2534
- async listScopes(orgId, allBackends = false) {
2494
+ async listScopes(orgId) {
2535
2495
  const params = new URLSearchParams({ orgId });
2536
- if (allBackends) params.set("allBackends", "true");
2537
2496
  return this.request("GET", `/api/v1/admin/secrets/scopes?${params}`);
2538
2497
  }
2539
2498
  async listKeys(orgId, scope) {
@@ -625,7 +625,7 @@ export declare const installSecretsRegistryUsedTotal: import("@opentelemetry/api
625
625
  * from leaving the orchestrator for those dispatches.
626
626
  *
627
627
  * Labels:
628
- * - trust_tier: unknown | known (the `trusted` tier does not strip and therefore never fires this counter)
628
+ * - trust_tier: unknown (the `trusted` tier does not strip and therefore never fires this counter)
629
629
  */
630
630
  export declare const installSecretsContributorStrippedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
631
631
  /**
@@ -638,7 +638,7 @@ export declare const installSecretsContributorStrippedTotal: import("@openteleme
638
638
  * and registry auth is per-job, so one run can strip both, either, or neither.
639
639
  *
640
640
  * Labels:
641
- * - trust_tier: unknown | known (the `trusted` tier does not strip and therefore never fires this counter)
641
+ * - trust_tier: unknown (the `trusted` tier does not strip and therefore never fires this counter)
642
642
  */
643
643
  export declare const containerRegistryAuthContributorStrippedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
644
644
  /**
@@ -8,7 +8,7 @@
8
8
  * differs only in the local sentinel issuer (`kici-local`) and the org anchor
9
9
  * (`__default__`, the plane's default org).
10
10
  */
11
- import { AttestationOrigin, SourceOrigin, type EventClaims } from '@kici-dev/engine';
11
+ import { AttestationOrigin, ID_TOKEN_CLAIM_NAMES, SourceOrigin, type EventClaims } from '@kici-dev/engine';
12
12
  /** The subset of an execution_runs row the claims need. */
13
13
  export interface RunClaimSource {
14
14
  run_id: string;
@@ -61,15 +61,6 @@ export interface BuildClaimsOpts {
61
61
  statementHash: string;
62
62
  origin: 'deferred' | 'offline-backfill';
63
63
  };
64
- /**
65
- * Restore the pre-split pull-request `sub` — the branch-shaped form that
66
- * collides with a push to the same base branch. An escape hatch for an
67
- * operator whose cloud trust policy breaks mid-migration, set by
68
- * `KICI_OIDC_LEGACY_PR_SUB`.
69
- *
70
- * @deprecated Removed at v1.0.0. Move the policy to `repo:<repo>:pull_request`.
71
- */
72
- legacyPullRequestSubject?: boolean;
73
64
  }
74
65
  /**
75
66
  * Extends {@link EventClaims}, so every token carries the event-context claims
@@ -96,6 +87,14 @@ export interface IdTokenClaims extends EventClaims {
96
87
  statement_hash: string | null;
97
88
  attestation_origin: AttestationOrigin;
98
89
  }
90
+ /**
91
+ * The claim names shared by both discovery documents live in `@kici-dev/engine`;
92
+ * re-exported here for the route and the drift test. The binding below fails
93
+ * to compile when the engine list names a claim this type does not carry, and
94
+ * the drift test in `routes/provenance-oidc.test.ts` fails when the builder
95
+ * emits a claim the list does not name — together they keep the two in step.
96
+ */
97
+ export { ID_TOKEN_CLAIM_NAMES };
99
98
  /**
100
99
  * Build the OIDC ID-token claims for a build job from the orchestrator's own
101
100
  * execution_runs/execution_jobs rows. A live (synchronous) mint leaves
@@ -1,19 +1,16 @@
1
1
  /**
2
2
  * Local dev-signed OIDC mint (independent mode only).
3
3
  *
4
- * The Platform-connected orchestrator relays `oidc.token.request` to the hosted
5
- * Platform, which reads its own run/job rows and mints (`ws/oidc-token-relay.ts`
6
- * `oidc.mint.request`). The offline local dev plane has no Platform, so this
7
- * module mints locally: it reads the orchestrator's OWN execution_runs/
8
- * execution_jobs rows and signs claims with the in-process `LocalDevSigner`,
9
- * issuer `kici-local`.
4
+ * The production sibling is `orchestrator-mint.ts`, which signs with the
5
+ * orchestrator's own long-lived key under its real issuer. The offline local dev
6
+ * plane has no provenance issuer configured, so this module mints with the
7
+ * in-process `LocalDevSigner` under the issuer `kici-local`, reading the
8
+ * orchestrator's OWN execution_runs/execution_jobs rows.
10
9
  *
11
10
  * Security: this path is registered ONLY when the orchestrator runs in
12
- * `independent` mode with a `LocalDevSigner` present, and app.ts registers the
13
- * Platform relay in preference (the local path is registered strictly in the
14
- * `else` branch), so a Platform-connected orchestrator can never reach the local
15
- * signer — it keeps minting via the Platform exactly as today. The agent never
16
- * asserts its own claims; every identity claim is read server-side from the
11
+ * `independent` mode with a `LocalDevSigner` present and no orchestrator-owned
12
+ * signer is configured (`selectOidcMintRegistration`). The agent never asserts
13
+ * its own claims; every identity claim is read server-side from the
17
14
  * orchestrator's rows, and the job-ownership check is against dispatch state.
18
15
  */
19
16
  import type { Kysely } from 'kysely';
@@ -21,7 +18,7 @@ import { type OidcTokenResult } from '@kici-dev/engine/protocol/messages/oidc-to
21
18
  import type { Database } from '../db/types.js';
22
19
  import { type IdTokenClaims } from './id-token-claims.js';
23
20
  import { type LocalSigner } from './local-dev-signer.js';
24
- /** ID-token lifetime (seconds). Matches the Platform's 10-minute cap. */
21
+ /** ID-token lifetime (seconds). Matches the production mint's 10-minute cap. */
25
22
  export declare const LOCAL_ID_TOKEN_TTL_SECONDS = 600;
26
23
  /** The org anchor stamped into a local-plane token (the plane's default org). */
27
24
  export declare const LOCAL_ORG_ID = "__default__";
@@ -6,14 +6,11 @@
6
6
  *
7
7
  * 1. **Orchestrator-owned signing** (a resolved signer + the orchestrator's own
8
8
  * provenance issuer) → the **orchestrator** handler, minting + signing
9
- * locally. Wins whenever signing is configured — even Platform-connected
10
- * because the orchestrator is now the root of trust.
11
- * 2. **Platform-connected** (platformUrl + token + client), no signer → the
12
- * **relay** handler, minting via the hosted Platform (the DEPRECATED path,
13
- * kept for orchestrators with no signer configured).
14
- * 3. **Offline local dev plane** (independent identity + a local signer, AND
15
- * NOT Platform-connected) → the **local** dev-signed handler (`kici-local`).
16
- * 4. None → **no** registration (the method returns "unknown method").
9
+ * locally. Wins whenever signing is configured — the orchestrator is the
10
+ * root of trust.
11
+ * 2. **Offline local dev plane** (independent identity + a local signer) → the
12
+ * **local** dev-signed handler (`kici-local`).
13
+ * 3. None **no** registration (the method returns "unknown method").
17
14
  *
18
15
  * The identity claims are always read SERVER-SIDE from the orchestrator's own
19
16
  * rows and job ownership from dispatch state — no forgeable agent-asserted mint
@@ -22,29 +19,25 @@
22
19
  import type { Kysely } from 'kysely';
23
20
  import type { OidcTokenResult } from '@kici-dev/engine/protocol/messages/oidc-token-relay';
24
21
  import type { Database } from '../db/types.js';
25
- import { type MintPlatformClient } from '../ws/oidc-token-relay.js';
26
22
  import { type LocalMintOwnershipResolver } from './local-mint.js';
27
23
  import type { LocalSigner } from './local-dev-signer.js';
28
24
  import type { Signer } from './signer.js';
29
25
  export type OidcMintHandler = (agentId: string, params: Record<string, unknown>) => Promise<OidcTokenResult>;
30
26
  export interface OidcMintRegistration {
31
27
  /**
32
- * `orchestrator` = orchestrator-owned signing (the new root of trust);
33
- * `relay` = Platform-minted (deprecated); `local` = offline dev-signed.
28
+ * `orchestrator` = orchestrator-owned signing (the root of trust);
29
+ * `local` = offline dev-signed.
34
30
  */
35
- kind: 'orchestrator' | 'relay' | 'local';
31
+ kind: 'orchestrator' | 'local';
36
32
  handler: OidcMintHandler;
37
33
  }
38
34
  export interface OidcMintRegistrationInput {
39
- platformUrl?: string;
40
- platformToken?: string;
41
- platformClient?: MintPlatformClient;
42
35
  independentIdentity: boolean;
43
36
  localOidcSigner?: LocalSigner;
44
37
  /**
45
- * Lazily resolve the orchestrator's own provenance signer (Task 7 reconcile).
46
- * Present whenever orchestrator-owned signing is configured; the signer itself
47
- * is resolved on first mint (leader-election-race-safe).
38
+ * Lazily resolve the orchestrator's own provenance signer. Present whenever
39
+ * orchestrator-owned signing is configured; the signer itself is resolved on
40
+ * first mint (leader-election-race-safe).
48
41
  */
49
42
  resolveOrchestratorSigner?: () => Promise<Signer | null>;
50
43
  /** The orchestrator's own provenance issuer identity (config.provenanceSigningIssuer). */
@@ -57,16 +50,10 @@ export interface OidcMintRegistrationInput {
57
50
  db: Kysely<Database>;
58
51
  orchestratorId: string;
59
52
  /**
60
- * `KICI_OIDC_LEGACY_PR_SUB` restore the pre-split pull-request `sub`.
61
- *
62
- * @deprecated Removed at v1.0.0.
63
- */
64
- legacyPullRequestSubject?: boolean;
65
- /**
66
- * Test-only fault-injection predicate forwarded to the relay mint handler,
67
- * supplied only by the build-time test double. Only ever defers — the
68
- * anti-forgery choke point is preserved. Undefined (the shipped default) means
69
- * no fault injection.
53
+ * Test-only fault-injection predicate forwarded to the orchestrator mint
54
+ * handler, supplied only by the build-time test double. Only ever defers —
55
+ * the anti-forgery choke point is preserved. Undefined (the shipped default)
56
+ * means no fault injection.
70
57
  */
71
58
  initialMintFault?: (audience: string) => boolean;
72
59
  }
@@ -6,7 +6,7 @@
6
6
  * execution_jobs rows, signed with its own long-lived signing key (`Signer`),
7
7
  * issued under its own real issuer. Zero hosted-Platform dependency.
8
8
  *
9
- * Anti-forgery is identical to the Platform-minted path and the local dev plane:
9
+ * Anti-forgery is identical to the local dev plane:
10
10
  * every identity claim (run/job/repo/ref/sha/org) is read SERVER-SIDE from the
11
11
  * orchestrator's own rows — never from the agent's wire input — and job ownership
12
12
  * is resolved from dispatch state, never agent-asserted. The token is never
@@ -19,7 +19,7 @@ import { type IdTokenClaims } from './id-token-claims.js';
19
19
  import type { Signer } from './signer.js';
20
20
  /** The org anchor stamped when a run has no routing key (sourceless / local plane). */
21
21
  export declare const DEFAULT_ORG_ID = "__default__";
22
- /** ID-token lifetime (seconds). Matches the Platform's 10-minute cap. */
22
+ /** ID-token lifetime (seconds). */
23
23
  export declare const ORCHESTRATOR_ID_TOKEN_TTL_SECONDS = 600;
24
24
  export declare class OrchestratorMintRunNotFoundError extends Error {
25
25
  }
@@ -36,14 +36,6 @@ export interface OrchestratorMintDeps {
36
36
  ttlSeconds?: number;
37
37
  /** Injectable clock (seconds). Defaults to wall-clock. */
38
38
  nowSeconds?: () => number;
39
- /**
40
- * Restore the pre-split pull-request `sub`, from `KICI_OIDC_LEGACY_PR_SUB`.
41
- * An escape hatch for an operator whose cloud trust policy breaks mid-
42
- * migration; leaves the collision with a same-branch push in place.
43
- *
44
- * @deprecated Removed at v1.0.0.
45
- */
46
- legacyPullRequestSubject?: boolean;
47
39
  }
48
40
  export interface OrchestratorMintInput {
49
41
  runId: string;
@@ -93,6 +85,14 @@ export interface OrchestratorOidcTokenHandlerDeps {
93
85
  resolveSigner: () => Promise<Signer | null>;
94
86
  /** Mint deps except the signer (supplied lazily via `resolveSigner`). */
95
87
  mint: Omit<OrchestratorMintDeps, 'signer'>;
88
+ /**
89
+ * Test-only fault-injection predicate over the requested OIDC `audience`,
90
+ * supplied only by the build-time test double. Returning `true` short-circuits
91
+ * the *initial* mint to a transient defer BEFORE the signer is resolved — it
92
+ * can only defer, never forge or weaken a signature. Undefined (the shipped
93
+ * default) means no fault injection.
94
+ */
95
+ initialMintFault?: (audience: string) => boolean;
96
96
  }
97
97
  /**
98
98
  * Build the agent.api handler for `OIDC_TOKEN_REQUEST_METHOD` when the