@whittlelabs/sifter 0.30.16 → 0.30.17

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 (4) hide show
  1. package/README.md +16 -0
  2. package/bin.js +69 -28
  3. package/bin.js.map +2 -2
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -69,6 +69,22 @@ Once the backend answers, one more line names the page it sent you to, which is
69
69
 
70
70
  Pairing writes credentials to `~/.sifter/<profile>/config.json` (mode 0600), or `~/.sifter/config.json` for the default profile.
71
71
 
72
+ The environment also decides where a review may fetch your standards documents from: that environment's Sift API and nowhere else. A Sifter paired with production allows `https://api.sift.whittlelabs.com`; `stage` and `test` allow their own `api.sift` host, and `studio` allows `http://localhost:3008`. The Sifter finds its environment from the Jobs URL in its `sifter.yaml`, so an existing pairing needs nothing re-run. `run` and `doctor` print the list in use on their `overlay-hosts` line:
73
+
74
+ ```
75
+ PASS overlay-hosts https://api.sift.whittlelabs.com
76
+ ```
77
+
78
+ A backend this build does not know gets no default, and `overlay-hosts` warns that reviews would run without standards. Name the origin that backend signs standards URLs with under the `claude-code` executor in `sifter.yaml`. Your own list always replaces the environment's:
79
+
80
+ ```yaml
81
+ executors:
82
+ claude-code:
83
+ type: claude-code
84
+ overlayAllowedHosts:
85
+ - http://box.local:3008
86
+ ```
87
+
72
88
  ## Run
73
89
 
74
90
  ```bash
package/bin.js CHANGED
@@ -12261,6 +12261,8 @@ var require_environment = __commonJS({
12261
12261
  Object.defineProperty(exports2, "__esModule", { value: true });
12262
12262
  exports2.resolveEnvironment = resolveEnvironment;
12263
12263
  exports2.applyEnvironment = applyEnvironment;
12264
+ exports2.environmentPolledBy = environmentPolledBy;
12265
+ exports2.executorDefaultsFor = executorDefaultsFor;
12264
12266
  exports2.isWorthReporting = isWorthReporting;
12265
12267
  exports2.renderEnvironmentReport = renderEnvironmentReport;
12266
12268
  exports2.renderVerificationLine = renderVerificationLine;
@@ -12301,6 +12303,28 @@ var require_environment = __commonJS({
12301
12303
  }
12302
12304
  };
12303
12305
  }
12306
+ function environmentPolledBy(brand, jobsApiUrl) {
12307
+ const wanted = normalizeUrl(jobsApiUrl);
12308
+ for (const [name, entry] of Object.entries(brand.environments ?? {})) {
12309
+ if (normalizeUrl(entry.jobsApiUrl) === wanted)
12310
+ return name;
12311
+ }
12312
+ return null;
12313
+ }
12314
+ function executorDefaultsFor(brand, jobsApiUrl) {
12315
+ const name = environmentPolledBy(brand, jobsApiUrl);
12316
+ const tier = name === null ? void 0 : brand.environments?.[name]?.executorDefaults;
12317
+ if (!tier)
12318
+ return brand.executorDefaults;
12319
+ const merged = { ...brand.executorDefaults ?? {} };
12320
+ for (const [executor, settings] of Object.entries(tier)) {
12321
+ merged[executor] = { ...merged[executor] ?? {}, ...settings };
12322
+ }
12323
+ return merged;
12324
+ }
12325
+ function normalizeUrl(url) {
12326
+ return url.replace(/\/+$/, "").toLowerCase();
12327
+ }
12304
12328
  function isWorthReporting(resolved) {
12305
12329
  return resolved.environment !== null || resolved.keep.source === "flag" || resolved.jobs.source === "flag";
12306
12330
  }
@@ -12448,7 +12472,7 @@ var require_checks = __commonJS({
12448
12472
  const user = config.executors[WORKSPACE_EXECUTOR];
12449
12473
  if (user === void 0)
12450
12474
  return null;
12451
- const defaults = subject.brand.executorDefaults?.[WORKSPACE_EXECUTOR] ?? {};
12475
+ const defaults = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[WORKSPACE_EXECUTOR] ?? {};
12452
12476
  return { ...defaults, ...user };
12453
12477
  }
12454
12478
  function runRoot(config, subject, osTmpDir) {
@@ -12485,7 +12509,7 @@ var require_checks = __commonJS({
12485
12509
  return (0, apply_1.expandHome)(subject.requestedTmpDir);
12486
12510
  }
12487
12511
  const settings = {
12488
- ...subject.brand.executorDefaults?.[WORKSPACE_EXECUTOR] ?? {},
12512
+ ...(0, environment_1.executorDefaultsFor)(subject.brand, subject.brand.keepRegistration.jobsApiUrl)?.[WORKSPACE_EXECUTOR] ?? {},
12489
12513
  ...plan.keptBlock ?? {}
12490
12514
  };
12491
12515
  const tmpDir = settings.tmpDir;
@@ -12823,7 +12847,8 @@ var require_checks = __commonJS({
12823
12847
  const hosts = settings.overlayAllowedHosts;
12824
12848
  const origins = Array.isArray(hosts) ? hosts.filter((h) => typeof h === "string") : [];
12825
12849
  if (origins.length === 0) {
12826
- return warn("overlay-hosts", "the overlay allowlist is empty, so every review will run standards-blind", `add overlayAllowedHosts to the ${WORKSPACE_EXECUTOR} executor, or re-run init to restore the defaults`);
12850
+ const fallback = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[WORKSPACE_EXECUTOR]?.overlayAllowedHosts;
12851
+ return warn("overlay-hosts", "the overlay allowlist is empty, so every review will run standards-blind", Array.isArray(fallback) && fallback.length > 0 ? `remove overlayAllowedHosts from the ${WORKSPACE_EXECUTOR} executor to use this tier's default` : `add overlayAllowedHosts to the ${WORKSPACE_EXECUTOR} executor, naming the origin your backend signs standards URLs with (this build names none for Jobs at ${(0, environment_1.hostOf)(config.auth.jobsApiUrl)})`);
12827
12852
  }
12828
12853
  return pass("overlay-hosts", origins.join(", "));
12829
12854
  });
@@ -12904,7 +12929,7 @@ var require_checks = __commonJS({
12904
12929
  };
12905
12930
  function mergedExecutorConfigs(config, subject) {
12906
12931
  const user = config.executors;
12907
- const defaults = subject.brand.executorDefaults;
12932
+ const defaults = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl);
12908
12933
  if (!defaults)
12909
12934
  return user;
12910
12935
  const merged = {};
@@ -25750,6 +25775,7 @@ var require_shuttle = __commonJS({
25750
25775
  var output_gates_1 = require_output_gates();
25751
25776
  var keep_1 = require_dist2();
25752
25777
  var apply_1 = require_apply();
25778
+ var environment_1 = require_environment();
25753
25779
  var version_check_1 = require_version_check();
25754
25780
  var typed_exit_1 = require_typed_exit();
25755
25781
  var logger_1 = require_logger();
@@ -25951,7 +25977,7 @@ var require_shuttle = __commonJS({
25951
25977
  });
25952
25978
  const registry = new registry_1.ExecutorRegistry(this.brand.executorAllowlist);
25953
25979
  registerBuiltInExecutors(registry, this.brand.executorAllowlist);
25954
- const executors = registry.build(mergeExecutorDefaults(this.brand.executorDefaults, this.config.executors));
25980
+ const executors = registry.build(mergeExecutorDefaults((0, environment_1.executorDefaultsFor)(this.brand, this.config.auth.jobsApiUrl), this.config.executors));
25955
25981
  const byStrategy = /* @__PURE__ */ new Map();
25956
25982
  for (const executor of executors) {
25957
25983
  for (const strategy of executor.strategies)
@@ -26734,8 +26760,8 @@ var require_dist5 = __commonJS({
26734
26760
  "../../packages/shuttle/dist/index.js"(exports2) {
26735
26761
  "use strict";
26736
26762
  Object.defineProperty(exports2, "__esModule", { value: true });
26737
- exports2.scaffoldedConfigPath = exports2.planScaffold = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.environmentReportJson = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorEnvironmentError = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
26738
- exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = void 0;
26763
+ exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.executorDefaultsFor = exports2.environmentReportJson = exports2.environmentPolledBy = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorEnvironmentError = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
26764
+ exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.scaffoldedConfigPath = exports2.planScaffold = void 0;
26739
26765
  var cli_1 = require_cli();
26740
26766
  Object.defineProperty(exports2, "createCli", { enumerable: true, get: function() {
26741
26767
  return cli_1.createCli;
@@ -26867,9 +26893,15 @@ var require_dist5 = __commonJS({
26867
26893
  Object.defineProperty(exports2, "applyEnvironment", { enumerable: true, get: function() {
26868
26894
  return environment_1.applyEnvironment;
26869
26895
  } });
26896
+ Object.defineProperty(exports2, "environmentPolledBy", { enumerable: true, get: function() {
26897
+ return environment_1.environmentPolledBy;
26898
+ } });
26870
26899
  Object.defineProperty(exports2, "environmentReportJson", { enumerable: true, get: function() {
26871
26900
  return environment_1.environmentReportJson;
26872
26901
  } });
26902
+ Object.defineProperty(exports2, "executorDefaultsFor", { enumerable: true, get: function() {
26903
+ return environment_1.executorDefaultsFor;
26904
+ } });
26873
26905
  Object.defineProperty(exports2, "hostOf", { enumerable: true, get: function() {
26874
26906
  return environment_1.hostOf;
26875
26907
  } });
@@ -26934,7 +26966,7 @@ var import_path = require("path");
26934
26966
  var import_promises = require("fs/promises");
26935
26967
  var import_yaml = __toESM(require_dist3());
26936
26968
  var import_shuttle = __toESM(require_dist5());
26937
- var buildVersion = true ? "0.30.16" : pkg.version;
26969
+ var buildVersion = true ? "0.30.17" : pkg.version;
26938
26970
  var sifterBrand = {
26939
26971
  product: {
26940
26972
  id: "sifter",
@@ -26975,24 +27007,12 @@ var sifterBrand = {
26975
27007
  git: "2.20.0",
26976
27008
  claude: "2.0.45"
26977
27009
  },
26978
- // Overlay-fetch origins for the signed standards-content URLs each Sift
26979
- // tier signs (SIFT_PUBLIC_API_URL). Without these, the executor's SSRF
26980
- // guard silently skips the standards overlay and every review runs
26981
- // standards-blind (2026-07-13). User config overrides per key.
27010
+ // The overlay-fetch allowlist is NOT here: each tier signs its standards
27011
+ // URLs with its own Sift origin (SIFT_PUBLIC_API_URL), so the origin is a
27012
+ // property of the tier and lives on its `environments` entry below. A
27013
+ // Sifter paired to production allows production's origin and nothing else.
26982
27014
  executorDefaults: {
26983
27015
  "claude-code": {
26984
- overlayAllowedHosts: [
26985
- "https://api.sift.whittlelabs.com",
26986
- "https://api.sift.stage.whittlelabs.com",
26987
- // The test tier signs overlay URLs too, and a Sifter the E2E suite
26988
- // runs on the runner reaches Sift by this host rather than by the
26989
- // in-cluster service name the hosted worker uses. Without it that
26990
- // Sifter passes its own preflight — the check asks only that the list
26991
- // is non-empty — and then runs every review standards-blind, which is
26992
- // the exact silence of 2026-07-13.
26993
- "https://api.sift.test.whittlelabs.com",
26994
- "http://localhost:3008"
26995
- ],
26996
27016
  // Transcript retention is a diagnostic switch a person flips for one
26997
27017
  // session, so it rides the environment rather than a file they would
26998
27018
  // have to remember to unset. The shared package reads no env of its
@@ -27016,22 +27036,40 @@ var sifterBrand = {
27016
27036
  // `studio` is the local dev fleet on this machine, reachable over Tailscale
27017
27037
  // at the `studio.whittlelabs.com` hostname rather than `localhost`, because
27018
27038
  // a pairing made from a phone or a second machine has to resolve too.
27039
+ //
27040
+ // Each entry also carries the origin that tier signs overlay URLs with.
27041
+ // A run matches its tier by the Jobs URL its config polls (written from the
27042
+ // pairing, and held equal to it by `pairing-environment`), so an existing
27043
+ // pairing picks up its own tier's origin without re-pairing, and a user's
27044
+ // own `overlayAllowedHosts` still wins. Without the right origin the
27045
+ // executor's SSRF guard silently skips the standards overlay and every
27046
+ // review runs standards-blind (2026-07-13); tests/overlay-hosts.test.ts
27047
+ // holds every tier to naming its own.
27019
27048
  environments: {
27020
27049
  prod: {
27021
27050
  keepApiUrl: "https://keep.whittlelabs.com",
27022
- jobsApiUrl: "https://jobs.whittlelabs.com"
27051
+ jobsApiUrl: "https://jobs.whittlelabs.com",
27052
+ executorDefaults: overlayOrigin("https://api.sift.whittlelabs.com")
27023
27053
  },
27024
27054
  stage: {
27025
27055
  keepApiUrl: "https://keep.stage.whittlelabs.com",
27026
- jobsApiUrl: "https://jobs.stage.whittlelabs.com"
27056
+ jobsApiUrl: "https://jobs.stage.whittlelabs.com",
27057
+ executorDefaults: overlayOrigin("https://api.sift.stage.whittlelabs.com")
27027
27058
  },
27028
27059
  test: {
27029
27060
  keepApiUrl: "https://keep.test.whittlelabs.com",
27030
- jobsApiUrl: "https://jobs.test.whittlelabs.com"
27061
+ jobsApiUrl: "https://jobs.test.whittlelabs.com",
27062
+ // The public host, because a Sifter the E2E suite runs on the runner
27063
+ // reaches Sift by it rather than by the in-cluster service name the
27064
+ // test tier's hosted worker uses (that worker sets its own list).
27065
+ executorDefaults: overlayOrigin("https://api.sift.test.whittlelabs.com")
27031
27066
  },
27032
27067
  studio: {
27033
27068
  keepApiUrl: "http://studio.whittlelabs.com:3007",
27034
- jobsApiUrl: "http://studio.whittlelabs.com:3005"
27069
+ jobsApiUrl: "http://studio.whittlelabs.com:3005",
27070
+ // The dev fleet's Sift signs with SIFT_PUBLIC_API_URL from its .env,
27071
+ // which is localhost.
27072
+ executorDefaults: overlayOrigin("http://localhost:3008")
27035
27073
  }
27036
27074
  },
27037
27075
  keepRegistration: {
@@ -27119,6 +27157,9 @@ var sifterBrand = {
27119
27157
  }
27120
27158
  ]
27121
27159
  };
27160
+ function overlayOrigin(origin) {
27161
+ return { "claude-code": { overlayAllowedHosts: [origin] } };
27162
+ }
27122
27163
  async function buildExecutorBlock(executor, ctx) {
27123
27164
  switch (executor) {
27124
27165
  case "claude-code": {