@whittlelabs/sifter 0.7.1 → 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 (3) hide show
  1. package/bin.js +650 -56
  2. package/bin.js.map +4 -4
  3. package/package.json +1 -1
package/bin.js CHANGED
@@ -3226,7 +3226,9 @@ var require_device_code = __commonJS({
3226
3226
  body: JSON.stringify({
3227
3227
  client_id: opts.oauthClientId,
3228
3228
  device_code: opts.deviceCode,
3229
- display_name: opts.displayName
3229
+ display_name: opts.displayName,
3230
+ hostname: (0, os_1.hostname)(),
3231
+ os_user: (0, os_1.userInfo)().username
3230
3232
  })
3231
3233
  });
3232
3234
  if (response.status === 202) {
@@ -3254,7 +3256,9 @@ var require_device_code = __commonJS({
3254
3256
  body: JSON.stringify({
3255
3257
  client_id: opts.oauthClientId,
3256
3258
  user_code: opts.userCode,
3257
- display_name: opts.displayName
3259
+ display_name: opts.displayName,
3260
+ hostname: (0, os_1.hostname)(),
3261
+ os_user: (0, os_1.userInfo)().username
3258
3262
  })
3259
3263
  });
3260
3264
  (0, version_check_1.checkResponseMinVersion)({
@@ -15665,6 +15669,21 @@ var require_prompt_execution = __commonJS({
15665
15669
  if (typeof spec.outputSchema !== "object" || spec.outputSchema === null) {
15666
15670
  throw new Error("prompt-execution: spec.outputSchema must be a JSON Schema object");
15667
15671
  }
15672
+ if (spec.workspace !== void 0) {
15673
+ const ws = spec.workspace;
15674
+ if (typeof ws !== "object" || ws === null || Array.isArray(ws)) {
15675
+ throw new Error("prompt-execution: spec.workspace must be an object when present");
15676
+ }
15677
+ const namespaces = Object.entries(ws);
15678
+ if (namespaces.length === 0) {
15679
+ throw new Error("prompt-execution: spec.workspace must carry at least one namespace");
15680
+ }
15681
+ for (const [name, value] of namespaces) {
15682
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
15683
+ throw new Error(`prompt-execution: spec.workspace.${name} must be an object`);
15684
+ }
15685
+ }
15686
+ }
15668
15687
  return spec;
15669
15688
  }
15670
15689
  function renderPromptExecution(inputs) {
@@ -15876,6 +15895,8 @@ var require_prompt_execution2 = __commonJS({
15876
15895
  outputLabel: options.output.label,
15877
15896
  outputSchema: options.output.schema
15878
15897
  };
15898
+ if (options.workspace)
15899
+ spec.workspace = options.workspace;
15879
15900
  if (options.providerHints)
15880
15901
  spec.providerHints = options.providerHints;
15881
15902
  if (options.costCapHint)
@@ -18418,6 +18439,17 @@ var require_client2 = __commonJS({
18418
18439
  async getManagedSifterStatus(userId) {
18419
18440
  return this.request("GET", `/api/users/${encodeURIComponent(userId)}/managed-sifter-status`);
18420
18441
  }
18442
+ // ── GitHub OAuth token ──────────────────────────────────────────
18443
+ /**
18444
+ * Reveal a user's decrypted GitHub OAuth token for a service-to-service
18445
+ * call on their behalf (e.g. the hosted Sifter cloning the repo under
18446
+ * review). Service-token only; requires `keep:oauth-tokens:read`. Treat
18447
+ * the returned token as a single-use, in-memory secret. Throws (404 →
18448
+ * NotFound) when the user has not connected GitHub.
18449
+ */
18450
+ async getUserGithubToken(userId) {
18451
+ return this.request("GET", `/api/users/${encodeURIComponent(userId)}/github-token`);
18452
+ }
18421
18453
  // ── HTTP ────────────────────────────────────────────────────────
18422
18454
  async request(method, path, body) {
18423
18455
  const url = `${this.baseUrl}${path}`;
@@ -18516,7 +18548,15 @@ var require_agent_token = __commonJS({
18516
18548
  "../../packages/keep/dist/agent-token.js"(exports2) {
18517
18549
  "use strict";
18518
18550
  Object.defineProperty(exports2, "__esModule", { value: true });
18519
- exports2.AgentTokenManager = void 0;
18551
+ exports2.AgentTokenManager = exports2.AgentCredentialRevokedError = void 0;
18552
+ var AgentCredentialRevokedError2 = class extends Error {
18553
+ code = "PAIRING_REVOKED";
18554
+ constructor(message) {
18555
+ super(message);
18556
+ this.name = "AgentCredentialRevokedError";
18557
+ }
18558
+ };
18559
+ exports2.AgentCredentialRevokedError = AgentCredentialRevokedError2;
18520
18560
  var AgentTokenManager = class {
18521
18561
  token = null;
18522
18562
  expiresAt = 0;
@@ -18563,6 +18603,9 @@ var require_agent_token = __commonJS({
18563
18603
  const json = await response.json();
18564
18604
  if (!response.ok || !json.success) {
18565
18605
  const msg = json.error?.message ?? `Agent token exchange failed with status ${response.status}`;
18606
+ if (response.status === 401) {
18607
+ throw new AgentCredentialRevokedError2(`[AgentTokenManager] Keep refused this agent's credentials (401): the pairing was revoked or the secret rotated. Re-pair to continue.`);
18608
+ }
18566
18609
  throw new Error(`[AgentTokenManager] ${msg}`);
18567
18610
  }
18568
18611
  this.token = json.data.token;
@@ -18579,7 +18622,7 @@ var require_dist4 = __commonJS({
18579
18622
  "../../packages/keep/dist/index.js"(exports2) {
18580
18623
  "use strict";
18581
18624
  Object.defineProperty(exports2, "__esModule", { value: true });
18582
- exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
18625
+ exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
18583
18626
  var client_1 = require_client2();
18584
18627
  Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
18585
18628
  return client_1.KeepClient;
@@ -18592,6 +18635,9 @@ var require_dist4 = __commonJS({
18592
18635
  Object.defineProperty(exports2, "AgentTokenManager", { enumerable: true, get: function() {
18593
18636
  return agent_token_1.AgentTokenManager;
18594
18637
  } });
18638
+ Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
18639
+ return agent_token_1.AgentCredentialRevokedError;
18640
+ } });
18595
18641
  }
18596
18642
  });
18597
18643
 
@@ -18728,15 +18774,415 @@ var require_validate_output = __commonJS({
18728
18774
  }
18729
18775
  });
18730
18776
 
18777
+ // ../../packages/shuttle/dist/executors/workspace.js
18778
+ var require_workspace = __commonJS({
18779
+ "../../packages/shuttle/dist/executors/workspace.js"(exports2) {
18780
+ "use strict";
18781
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
18782
+ return mod && mod.__esModule ? mod : { "default": mod };
18783
+ };
18784
+ Object.defineProperty(exports2, "__esModule", { value: true });
18785
+ exports2.prepareWorkspace = prepareWorkspace;
18786
+ exports2.buildGitAuthEnv = buildGitAuthEnv;
18787
+ var child_process_1 = require("child_process");
18788
+ var fs_1 = require("fs");
18789
+ var os_1 = __importDefault(require("os"));
18790
+ var path_1 = __importDefault(require("path"));
18791
+ var DEFAULT_GIT_TIMEOUT_MS = 12e4;
18792
+ var DEFAULT_GIT_PROTOCOLS = "https:ssh:git";
18793
+ async function prepareWorkspace(repoDir, workspace, options = {}) {
18794
+ const resolved = readGitWorkspace(workspace);
18795
+ if ("warning" in resolved) {
18796
+ return degradedWorkspace(resolved.warning);
18797
+ }
18798
+ const gitWorkspace = resolved.git;
18799
+ const timeoutMs = options.commandTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
18800
+ const allowedProtocols = options.allowedProtocols ?? DEFAULT_GIT_PROTOCOLS;
18801
+ const signal = options.signal;
18802
+ const authEnv = options.gitToken !== void 0 ? buildGitAuthEnv(gitWorkspace.remote, options.gitToken) : void 0;
18803
+ const git = (args, cwd) => runGit(args, cwd, { timeoutMs, signal, allowedProtocols, extraEnv: authEnv });
18804
+ const gitCleanup = (args, cwd) => runGit(args, cwd, { timeoutMs, allowedProtocols });
18805
+ const hasCommit = (dir) => git(["cat-file", "-e", `${gitWorkspace.commit}^{commit}`], dir).then((r) => r.code === 0);
18806
+ const fetchCommit = async (dir, opts) => {
18807
+ if (await hasCommit(dir))
18808
+ return [];
18809
+ const depth = opts.shallow ? ["--depth=1"] : [];
18810
+ const attempts = [];
18811
+ for (const ref of gitWorkspace.fetchRefs ?? []) {
18812
+ if (opts.includeOrigin) {
18813
+ attempts.push({ source: `origin ${ref}`, args: ["fetch", "--no-tags", ...depth, "origin", ref] });
18814
+ }
18815
+ attempts.push({
18816
+ source: `${gitWorkspace.remote} ${ref}`,
18817
+ args: ["fetch", "--no-tags", ...depth, gitWorkspace.remote, ref]
18818
+ });
18819
+ }
18820
+ attempts.push({
18821
+ source: `${gitWorkspace.remote} commit`,
18822
+ args: ["fetch", "--no-tags", ...depth, gitWorkspace.remote, gitWorkspace.commit]
18823
+ });
18824
+ const failures2 = [];
18825
+ for (const { source, args } of attempts) {
18826
+ const result = await git(args, dir);
18827
+ if (result.code !== 0) {
18828
+ failures2.push(`${source}: ${firstLine(result.stderr)}`);
18829
+ continue;
18830
+ }
18831
+ if (await hasCommit(dir))
18832
+ return [];
18833
+ failures2.push(`${source}: fetch succeeded but ${gitWorkspace.commit} is still absent`);
18834
+ }
18835
+ return failures2;
18836
+ };
18837
+ const notMaterialized = (failures2) => `could not materialize ${gitWorkspace.commit} from ${gitWorkspace.remote}` + (failures2.length > 0 ? ` (${failures2.join("; ")})` : "");
18838
+ const isRepo = (await git(["rev-parse", "--git-dir"], repoDir)).code === 0;
18839
+ if (isRepo) {
18840
+ const failures2 = await fetchCommit(repoDir, { includeOrigin: true, shallow: false });
18841
+ if (!await hasCommit(repoDir))
18842
+ return degradedWorkspace(notMaterialized(failures2));
18843
+ const tempRoot2 = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-ws-"));
18844
+ const worktreeDir = path_1.default.join(tempRoot2, "checkout");
18845
+ const add = await git(["worktree", "add", "--detach", worktreeDir, gitWorkspace.commit], repoDir);
18846
+ if (add.code !== 0) {
18847
+ await fs_1.promises.rm(tempRoot2, { recursive: true, force: true }).catch(() => {
18848
+ });
18849
+ return degradedWorkspace(`git worktree add failed: ${firstLine(add.stderr)}`);
18850
+ }
18851
+ return {
18852
+ cwd: worktreeDir,
18853
+ materialized: true,
18854
+ cleanup: async () => {
18855
+ await gitCleanup(["worktree", "remove", "--force", worktreeDir], repoDir);
18856
+ await gitCleanup(["worktree", "prune"], repoDir);
18857
+ await fs_1.promises.rm(tempRoot2, { recursive: true, force: true }).catch(() => {
18858
+ });
18859
+ }
18860
+ };
18861
+ }
18862
+ const tempRoot = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-ws-"));
18863
+ const checkoutDir = path_1.default.join(tempRoot, "checkout");
18864
+ const removeTempRoot = async () => {
18865
+ await fs_1.promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
18866
+ });
18867
+ };
18868
+ await fs_1.promises.mkdir(checkoutDir);
18869
+ const init = await git(["init", "-q"], checkoutDir);
18870
+ if (init.code !== 0) {
18871
+ await removeTempRoot();
18872
+ return degradedWorkspace(`git init failed: ${firstLine(init.stderr)}`);
18873
+ }
18874
+ const failures = await fetchCommit(checkoutDir, { includeOrigin: false, shallow: true });
18875
+ if (!await hasCommit(checkoutDir)) {
18876
+ await removeTempRoot();
18877
+ return degradedWorkspace(notMaterialized(failures));
18878
+ }
18879
+ const checkout = await git(["checkout", "--detach", gitWorkspace.commit], checkoutDir);
18880
+ if (checkout.code !== 0) {
18881
+ await removeTempRoot();
18882
+ return degradedWorkspace(`git checkout failed: ${firstLine(checkout.stderr)}`);
18883
+ }
18884
+ return { cwd: checkoutDir, materialized: true, cleanup: removeTempRoot };
18885
+ }
18886
+ function buildGitAuthEnv(remote, token) {
18887
+ let origin;
18888
+ try {
18889
+ const url = new URL(remote);
18890
+ if (url.protocol !== "https:")
18891
+ return void 0;
18892
+ origin = `${url.protocol}//${url.host}/`;
18893
+ } catch {
18894
+ return void 0;
18895
+ }
18896
+ const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
18897
+ return {
18898
+ GIT_CONFIG_COUNT: "1",
18899
+ GIT_CONFIG_KEY_0: `http.${origin}.extraheader`,
18900
+ GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`
18901
+ };
18902
+ }
18903
+ var COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
18904
+ var REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.@^~+-]*$/;
18905
+ function readGitWorkspace(workspace) {
18906
+ const git = workspace.git;
18907
+ if (git === void 0) {
18908
+ const offered = Object.keys(workspace).join(", ") || "(none)";
18909
+ return {
18910
+ warning: `workspace carries no namespace this executor understands (offered: ${offered}; supported: git)`
18911
+ };
18912
+ }
18913
+ if (typeof git.remote !== "string" || git.remote.length === 0 || git.remote.startsWith("-")) {
18914
+ return { warning: "workspace.git.remote must be a non-empty remote URL or path" };
18915
+ }
18916
+ if (typeof git.commit !== "string" || !COMMIT_PATTERN.test(git.commit)) {
18917
+ return { warning: "workspace.git.commit must be a full 40-char commit SHA" };
18918
+ }
18919
+ if (git.fetchRefs !== void 0) {
18920
+ if (!Array.isArray(git.fetchRefs) || !git.fetchRefs.every((r) => typeof r === "string")) {
18921
+ return { warning: "workspace.git.fetchRefs must be an array of ref strings" };
18922
+ }
18923
+ const bad = git.fetchRefs.find((r) => !REF_PATTERN.test(r));
18924
+ if (bad !== void 0) {
18925
+ return { warning: `workspace.git.fetchRefs contains an invalid ref: ${JSON.stringify(bad)}` };
18926
+ }
18927
+ }
18928
+ return { git };
18929
+ }
18930
+ async function degradedWorkspace(warning) {
18931
+ const scratchDir = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-ws-degraded-"));
18932
+ return {
18933
+ cwd: scratchDir,
18934
+ materialized: false,
18935
+ warning,
18936
+ cleanup: async () => {
18937
+ await fs_1.promises.rm(scratchDir, { recursive: true, force: true }).catch(() => {
18938
+ });
18939
+ }
18940
+ };
18941
+ }
18942
+ function runGit(args, cwd, opts) {
18943
+ return new Promise((resolve) => {
18944
+ const child = (0, child_process_1.spawn)("git", args, {
18945
+ cwd,
18946
+ stdio: ["ignore", "pipe", "pipe"],
18947
+ // `extraEnv` (e.g. the credential extraheader) goes first so the two
18948
+ // security-critical vars below always win and can't be overridden.
18949
+ env: {
18950
+ ...process.env,
18951
+ ...opts.extraEnv ?? {},
18952
+ GIT_TERMINAL_PROMPT: "0",
18953
+ // Transport allowlist. `remote` passes readGitWorkspace as any
18954
+ // non-empty non-`-` string, which still admits `ext::sh -c …` and
18955
+ // `fd::` remotes that execute arbitrary commands on this host.
18956
+ // Restricting to the transports we actually use turns those into a
18957
+ // fast failure (→ degrade) rather than RCE across the boundary this
18958
+ // executor is meant to defend.
18959
+ GIT_ALLOW_PROTOCOL: opts.allowedProtocols
18960
+ }
18961
+ });
18962
+ const stdout = [];
18963
+ const stderr = [];
18964
+ let settled = false;
18965
+ const settle = (result) => {
18966
+ if (settled)
18967
+ return;
18968
+ settled = true;
18969
+ clearTimeout(timer);
18970
+ opts.signal?.removeEventListener("abort", onAbort);
18971
+ resolve(result);
18972
+ };
18973
+ const killChild = () => {
18974
+ child.kill("SIGTERM");
18975
+ const escalate = setTimeout(() => child.kill("SIGKILL"), 5e3);
18976
+ escalate.unref();
18977
+ child.once("close", () => clearTimeout(escalate));
18978
+ };
18979
+ const timer = setTimeout(() => {
18980
+ killChild();
18981
+ settle({
18982
+ code: -1,
18983
+ stdout: Buffer.concat(stdout).toString("utf-8"),
18984
+ stderr: `git ${args[0]} timed out after ${opts.timeoutMs}ms`
18985
+ });
18986
+ }, opts.timeoutMs);
18987
+ const onAbort = () => {
18988
+ killChild();
18989
+ settle({
18990
+ code: -1,
18991
+ stdout: Buffer.concat(stdout).toString("utf-8"),
18992
+ stderr: "aborted"
18993
+ });
18994
+ };
18995
+ if (opts.signal?.aborted) {
18996
+ onAbort();
18997
+ return;
18998
+ }
18999
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
19000
+ child.stdout.on("data", (c) => stdout.push(c));
19001
+ child.stderr.on("data", (c) => stderr.push(c));
19002
+ child.on("error", (err) => settle({ code: -1, stdout: "", stderr: err.message }));
19003
+ child.on("close", (code) => settle({
19004
+ code: code ?? -1,
19005
+ stdout: Buffer.concat(stdout).toString("utf-8"),
19006
+ stderr: Buffer.concat(stderr).toString("utf-8")
19007
+ }));
19008
+ });
19009
+ }
19010
+ function firstLine(text) {
19011
+ const line = text.trim().split("\n", 1)[0];
19012
+ return line.length > 0 ? line : "(no output)";
19013
+ }
19014
+ }
19015
+ });
19016
+
19017
+ // ../../packages/shuttle/dist/executors/overlay.js
19018
+ var require_overlay = __commonJS({
19019
+ "../../packages/shuttle/dist/executors/overlay.js"(exports2) {
19020
+ "use strict";
19021
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
19022
+ return mod && mod.__esModule ? mod : { "default": mod };
19023
+ };
19024
+ Object.defineProperty(exports2, "__esModule", { value: true });
19025
+ exports2.applyOverlay = applyOverlay;
19026
+ var fs_1 = require("fs");
19027
+ var path_1 = __importDefault(require("path"));
19028
+ var DEFAULT_TIMEOUT_MS = 3e4;
19029
+ var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
19030
+ async function applyOverlay(runDir, overlay, options) {
19031
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
19032
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
19033
+ const allowed = new Set(options.allowedHosts.map(normalizeOrigin).filter((o) => !!o));
19034
+ const rootReal = await fs_1.promises.realpath(runDir);
19035
+ const result = { written: 0, skipped: [] };
19036
+ const files = Array.isArray(overlay.files) ? overlay.files : [];
19037
+ for (const file of files) {
19038
+ const skip = (reason) => result.skipped.push({ path: String(file?.path ?? "?"), reason });
19039
+ if (typeof file?.path !== "string" || typeof file?.url !== "string") {
19040
+ skip("malformed overlay entry (path/url must be strings)");
19041
+ continue;
19042
+ }
19043
+ const dest = safeDestination(rootReal, file.path);
19044
+ if (!dest) {
19045
+ skip("path escapes the workspace root");
19046
+ continue;
19047
+ }
19048
+ let origin;
19049
+ try {
19050
+ const u = new URL(file.url);
19051
+ if (u.protocol !== "https:" && u.protocol !== "http:") {
19052
+ skip(`unsupported url protocol ${u.protocol}`);
19053
+ continue;
19054
+ }
19055
+ origin = normalizeOrigin(u.origin);
19056
+ } catch {
19057
+ skip("invalid url");
19058
+ continue;
19059
+ }
19060
+ if (!origin || !allowed.has(origin)) {
19061
+ skip(`origin not in the overlay allowlist (${origin})`);
19062
+ continue;
19063
+ }
19064
+ try {
19065
+ const content = await fetchCapped(file.url, { timeoutMs, maxBytes, signal: options.signal });
19066
+ await fs_1.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
19067
+ await fs_1.promises.writeFile(dest, content);
19068
+ result.written += 1;
19069
+ } catch (err) {
19070
+ skip(err instanceof Error ? err.message : String(err));
19071
+ }
19072
+ }
19073
+ return result;
19074
+ }
19075
+ function safeDestination(rootReal, relPath) {
19076
+ if (relPath.length === 0 || path_1.default.isAbsolute(relPath))
19077
+ return void 0;
19078
+ const resolved = path_1.default.resolve(rootReal, relPath);
19079
+ const rootWithSep = rootReal.endsWith(path_1.default.sep) ? rootReal : rootReal + path_1.default.sep;
19080
+ if (resolved !== rootReal && !resolved.startsWith(rootWithSep))
19081
+ return void 0;
19082
+ if (relPath.split(/[/\\]/).includes(".."))
19083
+ return void 0;
19084
+ return resolved;
19085
+ }
19086
+ function normalizeOrigin(value) {
19087
+ try {
19088
+ const u = new URL(value.includes("://") ? value : `https://${value}`);
19089
+ return u.origin;
19090
+ } catch {
19091
+ return void 0;
19092
+ }
19093
+ }
19094
+ async function fetchCapped(url, opts) {
19095
+ const controller = new AbortController();
19096
+ const onParentAbort = () => controller.abort();
19097
+ opts.signal?.addEventListener("abort", onParentAbort, { once: true });
19098
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
19099
+ try {
19100
+ const response = await fetch(url, { signal: controller.signal });
19101
+ if (!response.ok) {
19102
+ throw new Error(`overlay fetch returned HTTP ${response.status}`);
19103
+ }
19104
+ if (!response.body) {
19105
+ const buf = Buffer.from(await response.arrayBuffer());
19106
+ if (buf.byteLength > opts.maxBytes)
19107
+ throw new Error("overlay file exceeds size cap");
19108
+ return buf;
19109
+ }
19110
+ const chunks = [];
19111
+ let total = 0;
19112
+ for await (const chunk of response.body) {
19113
+ const buf = Buffer.from(chunk);
19114
+ total += buf.byteLength;
19115
+ if (total > opts.maxBytes)
19116
+ throw new Error("overlay file exceeds size cap");
19117
+ chunks.push(buf);
19118
+ }
19119
+ return Buffer.concat(chunks);
19120
+ } finally {
19121
+ clearTimeout(timer);
19122
+ opts.signal?.removeEventListener("abort", onParentAbort);
19123
+ }
19124
+ }
19125
+ }
19126
+ });
19127
+
19128
+ // ../../packages/shuttle/dist/config/env-ref.js
19129
+ var require_env_ref = __commonJS({
19130
+ "../../packages/shuttle/dist/config/env-ref.js"(exports2) {
19131
+ "use strict";
19132
+ Object.defineProperty(exports2, "__esModule", { value: true });
19133
+ exports2.resolveEnvRef = resolveEnvRef;
19134
+ exports2.isEnvRef = isEnvRef;
19135
+ exports2.resolveConfigStringOrEnv = resolveConfigStringOrEnv;
19136
+ var ENV_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
19137
+ function resolveEnvRef(value, fieldName) {
19138
+ const match = ENV_REF_PATTERN.exec(value);
19139
+ if (!match)
19140
+ return value;
19141
+ const varName = match[1];
19142
+ const resolved = process.env[varName];
19143
+ if (resolved === void 0 || resolved === "") {
19144
+ const subject = fieldName ? `${fieldName} (${value})` : value;
19145
+ throw new Error(`Environment variable "${varName}" referenced by ${subject} is not set. Export it before starting the Sifter, or hard-code the value in the YAML.`);
19146
+ }
19147
+ return resolved;
19148
+ }
19149
+ function isEnvRef(value) {
19150
+ return ENV_REF_PATTERN.test(value);
19151
+ }
19152
+ function resolveConfigStringOrEnv(config, field, envFallback) {
19153
+ const raw = config[field];
19154
+ if (typeof raw === "string" && raw.length > 0) {
19155
+ return resolveEnvRef(raw, field);
19156
+ }
19157
+ const fromEnv = process.env[envFallback];
19158
+ if (fromEnv && fromEnv.length > 0) {
19159
+ return fromEnv;
19160
+ }
19161
+ throw new Error(`dynamic-key mode requires "${field}" in config or the ${envFallback} env var`);
19162
+ }
19163
+ }
19164
+ });
19165
+
18731
19166
  // ../../packages/shuttle/dist/executors/claude-code.js
18732
19167
  var require_claude_code = __commonJS({
18733
19168
  "../../packages/shuttle/dist/executors/claude-code.js"(exports2) {
18734
19169
  "use strict";
19170
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
19171
+ return mod && mod.__esModule ? mod : { "default": mod };
19172
+ };
18735
19173
  Object.defineProperty(exports2, "__esModule", { value: true });
18736
19174
  exports2.createClaudeCodeExecutor = createClaudeCodeExecutor;
19175
+ exports2.extractJsonObject = extractJsonObject;
18737
19176
  var child_process_1 = require("child_process");
19177
+ var fs_1 = require("fs");
19178
+ var os_1 = __importDefault(require("os"));
19179
+ var path_1 = __importDefault(require("path"));
18738
19180
  var loom_1 = require_dist3();
19181
+ var keep_1 = require_dist4();
18739
19182
  var validate_output_1 = require_validate_output();
19183
+ var workspace_1 = require_workspace();
19184
+ var overlay_1 = require_overlay();
19185
+ var env_ref_1 = require_env_ref();
18740
19186
  var apply_1 = require_apply();
18741
19187
  var DEFAULT_TIMEOUT = 6e5;
18742
19188
  var ClaudeCodeExecutor = class {
@@ -18754,13 +19200,77 @@ var require_claude_code = __commonJS({
18754
19200
  outputTokens: spec.costCapHint?.estimatedOutputTokens
18755
19201
  };
18756
19202
  }
18757
- execute(dispatch, signal) {
19203
+ async execute(dispatch, signal) {
18758
19204
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
19205
+ let credentials = null;
19206
+ if (this.config.resolveCredentials) {
19207
+ const hints = (0, loom_1.pickProviderHints)(spec, "anthropic-api");
19208
+ const customerId = typeof hints.customerId === "string" ? hints.customerId : void 0;
19209
+ if (!customerId) {
19210
+ throw new Error("claude-code dynamic-key mode requires providerHints.anthropicApi.customerId on every job");
19211
+ }
19212
+ credentials = await this.config.resolveCredentials({
19213
+ customerId,
19214
+ needGithubToken: !!spec.workspace
19215
+ });
19216
+ }
19217
+ let jobScratch = null;
19218
+ let configDir;
19219
+ if (credentials) {
19220
+ jobScratch = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-cc-"));
19221
+ configDir = path_1.default.join(jobScratch, "claude-config");
19222
+ await fs_1.promises.mkdir(configDir);
19223
+ }
19224
+ let workspace = null;
19225
+ let overlayResult = null;
19226
+ try {
19227
+ if (spec.workspace) {
19228
+ workspace = await (0, workspace_1.prepareWorkspace)(this.config.cwd, spec.workspace, {
19229
+ signal,
19230
+ ...credentials?.githubToken ? { gitToken: credentials.githubToken } : {}
19231
+ });
19232
+ if (spec.workspace.overlay) {
19233
+ overlayResult = await (0, overlay_1.applyOverlay)(workspace.cwd, spec.workspace.overlay, {
19234
+ allowedHosts: this.config.overlayAllowedHosts ?? [],
19235
+ signal
19236
+ });
19237
+ }
19238
+ }
19239
+ const childEnv = buildChildEnv(credentials, configDir);
19240
+ const claudeHints = (0, loom_1.pickProviderHints)(spec, "claude-code");
19241
+ const hintedModel = typeof claudeHints.model === "string" && claudeHints.model.length > 0 ? claudeHints.model : void 0;
19242
+ const response = await this.runClaude(prompt, spec, workspace?.cwd ?? this.config.cwd, childEnv, signal, hintedModel);
19243
+ if (workspace) {
19244
+ response.outputs.push({
19245
+ label: "workspace",
19246
+ content: {
19247
+ materialized: workspace.materialized,
19248
+ ...workspace.warning ? { warning: workspace.warning } : {},
19249
+ ...overlayResult ? {
19250
+ overlay: {
19251
+ written: overlayResult.written,
19252
+ ...overlayResult.skipped.length > 0 ? { skipped: overlayResult.skipped } : {}
19253
+ }
19254
+ } : {}
19255
+ }
19256
+ });
19257
+ }
19258
+ return response;
19259
+ } finally {
19260
+ if (workspace)
19261
+ await workspace.cleanup();
19262
+ if (jobScratch)
19263
+ await fs_1.promises.rm(jobScratch, { recursive: true, force: true }).catch(() => {
19264
+ });
19265
+ }
19266
+ }
19267
+ runClaude(prompt, spec, cwd, childEnv, signal, modelOverride) {
18759
19268
  const outputLabel = spec.outputLabel;
18760
19269
  return new Promise((resolve, reject) => {
18761
19270
  const args = ["--print"];
18762
- if (this.config.model)
18763
- args.push("--model", this.config.model);
19271
+ const model = modelOverride ?? this.config.model;
19272
+ if (model)
19273
+ args.push("--model", model);
18764
19274
  if (this.config.maxTurns !== void 0)
18765
19275
  args.push("--max-turns", String(this.config.maxTurns));
18766
19276
  if (this.config.allowedTools && this.config.allowedTools.length > 0) {
@@ -18769,8 +19279,11 @@ var require_claude_code = __commonJS({
18769
19279
  let child;
18770
19280
  try {
18771
19281
  child = (0, child_process_1.spawn)("claude", args, {
18772
- cwd: this.config.cwd,
18773
- stdio: ["pipe", "pipe", "pipe"]
19282
+ cwd,
19283
+ stdio: ["pipe", "pipe", "pipe"],
19284
+ // Undefined in local mode → inherit the ambient env (and its
19285
+ // `~/.claude` login) unchanged.
19286
+ ...childEnv ? { env: childEnv } : {}
18774
19287
  });
18775
19288
  } catch (err) {
18776
19289
  reject(err);
@@ -18814,7 +19327,7 @@ var require_claude_code = __commonJS({
18814
19327
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
18815
19328
  if (code === 0) {
18816
19329
  try {
18817
- const parsed = safeJsonParse(stdout);
19330
+ const parsed = extractJsonObject(stdout);
18818
19331
  (0, validate_output_1.assertOutputValid)(spec, parsed);
18819
19332
  settle(() => resolve({
18820
19333
  outputs: [{ label: outputLabel, content: parsed ?? { raw: stdout } }],
@@ -18832,7 +19345,7 @@ var require_claude_code = __commonJS({
18832
19345
  });
18833
19346
  }
18834
19347
  };
18835
- function createClaudeCodeExecutor(config, capabilityId = "claude-code") {
19348
+ function createClaudeCodeExecutor(config, capabilityId = "claude-code", deps = {}) {
18836
19349
  const cwdRaw = config.cwd;
18837
19350
  if (typeof cwdRaw !== "string" || cwdRaw.length === 0) {
18838
19351
  throw new Error('ClaudeCodeExecutor requires a non-empty "cwd" string in config');
@@ -18862,12 +19375,108 @@ var require_claude_code = __commonJS({
18862
19375
  }
18863
19376
  validated.timeout = config.timeout;
18864
19377
  }
19378
+ if (config.apiKeyFrom !== void 0) {
19379
+ validated.resolveCredentials = buildCredentialResolver(config, deps);
19380
+ }
19381
+ if (config.overlayAllowedHosts !== void 0) {
19382
+ if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
19383
+ throw new Error('"overlayAllowedHosts" must be an array of strings');
19384
+ }
19385
+ validated.overlayAllowedHosts = config.overlayAllowedHosts;
19386
+ }
18865
19387
  return new ClaudeCodeExecutor(validated);
18866
19388
  }
18867
- function safeJsonParse(s) {
19389
+ function buildChildEnv(credentials, configDir) {
19390
+ if (!credentials && !configDir)
19391
+ return void 0;
19392
+ return {
19393
+ ...process.env,
19394
+ ...credentials ? { ANTHROPIC_API_KEY: credentials.anthropicApiKey } : {},
19395
+ ...configDir ? { CLAUDE_CONFIG_DIR: configDir } : {}
19396
+ };
19397
+ }
19398
+ function buildCredentialResolver(config, deps) {
19399
+ if (config.apiKeyFrom !== "job-metadata") {
19400
+ throw new Error(`Unsupported "apiKeyFrom" value: ${JSON.stringify(config.apiKeyFrom)}. Only "job-metadata" is supported.`);
19401
+ }
19402
+ const keepApiUrl = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepApiUrl", "KEEP_API_URL");
19403
+ const clientId = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepClientId", "KEEP_CLIENT_ID");
19404
+ const clientSecret = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepClientSecret", "KEEP_CLIENT_SECRET");
19405
+ const stmFactory = deps.buildServiceTokenManager ?? ((cfg) => new keep_1.ServiceTokenManager(cfg));
19406
+ const stm = stmFactory({ keepApiUrl, clientId, clientSecret });
19407
+ const keepClientFactory = deps.buildKeepClient ?? ((cfg) => new keep_1.KeepClient(cfg));
19408
+ return async ({ customerId, needGithubToken }) => {
19409
+ const serviceToken = await stm.getToken();
19410
+ if (!serviceToken) {
19411
+ throw new Error("claude-code dynamic-key mode failed to exchange a Keep service token. Check KEEP_CLIENT_ID / KEEP_CLIENT_SECRET.");
19412
+ }
19413
+ const keep = keepClientFactory({ baseUrl: keepApiUrl, serviceToken });
19414
+ const reveal = await keep.revealManagedSifterKey(customerId);
19415
+ const credentials = { anthropicApiKey: reveal.apiKey };
19416
+ if (needGithubToken) {
19417
+ try {
19418
+ const gh = await keep.getUserGithubToken(customerId);
19419
+ credentials.githubToken = gh.access_token;
19420
+ } catch {
19421
+ }
19422
+ }
19423
+ return credentials;
19424
+ };
19425
+ }
19426
+ function extractJsonObject(s) {
19427
+ const exact = tryParseObject(s);
19428
+ if (exact)
19429
+ return exact;
19430
+ let last;
19431
+ let i = 0;
19432
+ while (i < s.length) {
19433
+ if (s[i] !== "{") {
19434
+ i++;
19435
+ continue;
19436
+ }
19437
+ let depth = 0;
19438
+ let inString = false;
19439
+ let escaped = false;
19440
+ let end = -1;
19441
+ for (let j = i; j < s.length; j++) {
19442
+ const ch = s[j];
19443
+ if (escaped) {
19444
+ escaped = false;
19445
+ continue;
19446
+ }
19447
+ if (ch === "\\") {
19448
+ escaped = inString;
19449
+ continue;
19450
+ }
19451
+ if (ch === '"') {
19452
+ inString = !inString;
19453
+ continue;
19454
+ }
19455
+ if (inString)
19456
+ continue;
19457
+ if (ch === "{")
19458
+ depth++;
19459
+ else if (ch === "}") {
19460
+ depth--;
19461
+ if (depth === 0) {
19462
+ end = j;
19463
+ break;
19464
+ }
19465
+ }
19466
+ }
19467
+ if (end === -1)
19468
+ break;
19469
+ const candidate = tryParseObject(s.slice(i, end + 1));
19470
+ if (candidate)
19471
+ last = candidate;
19472
+ i = end + 1;
19473
+ }
19474
+ return last;
19475
+ }
19476
+ function tryParseObject(s) {
18868
19477
  try {
18869
19478
  const parsed = JSON.parse(s);
18870
- return typeof parsed === "object" && parsed !== null ? parsed : void 0;
19479
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
18871
19480
  } catch {
18872
19481
  return void 0;
18873
19482
  }
@@ -19028,32 +19637,6 @@ var require_http_api = __commonJS({
19028
19637
  }
19029
19638
  });
19030
19639
 
19031
- // ../../packages/shuttle/dist/config/env-ref.js
19032
- var require_env_ref = __commonJS({
19033
- "../../packages/shuttle/dist/config/env-ref.js"(exports2) {
19034
- "use strict";
19035
- Object.defineProperty(exports2, "__esModule", { value: true });
19036
- exports2.resolveEnvRef = resolveEnvRef;
19037
- exports2.isEnvRef = isEnvRef;
19038
- var ENV_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
19039
- function resolveEnvRef(value, fieldName) {
19040
- const match = ENV_REF_PATTERN.exec(value);
19041
- if (!match)
19042
- return value;
19043
- const varName = match[1];
19044
- const resolved = process.env[varName];
19045
- if (resolved === void 0 || resolved === "") {
19046
- const subject = fieldName ? `${fieldName} (${value})` : value;
19047
- throw new Error(`Environment variable "${varName}" referenced by ${subject} is not set. Export it before starting the Sifter, or hard-code the value in the YAML.`);
19048
- }
19049
- return resolved;
19050
- }
19051
- function isEnvRef(value) {
19052
- return ENV_REF_PATTERN.test(value);
19053
- }
19054
- }
19055
- });
19056
-
19057
19640
  // ../../packages/shuttle/dist/executors/anthropic-api.js
19058
19641
  var require_anthropic_api = __commonJS({
19059
19642
  "../../packages/shuttle/dist/executors/anthropic-api.js"(exports2) {
@@ -19089,8 +19672,9 @@ var require_anthropic_api = __commonJS({
19089
19672
  const outputLabel = spec.outputLabel;
19090
19673
  const providerHints = (0, loom_1.pickProviderHints)(spec, this.instance.capabilityId);
19091
19674
  const apiKey = await this.instance.resolveKey(providerHints);
19675
+ const hintedModel = providerHints.model;
19092
19676
  const body = {
19093
- model: this.instance.model,
19677
+ model: typeof hintedModel === "string" && hintedModel.length > 0 ? hintedModel : this.instance.model,
19094
19678
  max_tokens: this.instance.maxTokens,
19095
19679
  messages: [{ role: "user", content: prompt }]
19096
19680
  };
@@ -19117,6 +19701,12 @@ var require_anthropic_api = __commonJS({
19117
19701
  bodyText = await response.text();
19118
19702
  } catch {
19119
19703
  }
19704
+ const envelope = bodyText ? safeJsonParse(bodyText) : void 0;
19705
+ const apiErr = envelope?.error;
19706
+ if (apiErr?.message) {
19707
+ const kind = apiErr.type ? ` (${apiErr.type})` : "";
19708
+ throw new Error(`Anthropic API error${kind}: ${apiErr.message} [HTTP ${response.status}]`);
19709
+ }
19120
19710
  throw new Error(`HTTP ${response.status}${bodyText ? `: ${bodyText}` : ""}`);
19121
19711
  }
19122
19712
  const data = await response.json();
@@ -19198,9 +19788,9 @@ var require_anthropic_api = __commonJS({
19198
19788
  if (config.apiKeyFrom !== "job-metadata") {
19199
19789
  throw new Error(`Unsupported "apiKeyFrom" value: ${JSON.stringify(config.apiKeyFrom)}. Only "job-metadata" is supported in v0.2.`);
19200
19790
  }
19201
- const keepApiUrl = resolveStringConfig(config, "keepApiUrl", "KEEP_API_URL");
19202
- const clientId = resolveStringConfig(config, "keepClientId", "KEEP_CLIENT_ID");
19203
- const clientSecret = resolveStringConfig(config, "keepClientSecret", "KEEP_CLIENT_SECRET");
19791
+ const keepApiUrl = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepApiUrl", "KEEP_API_URL");
19792
+ const clientId = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepClientId", "KEEP_CLIENT_ID");
19793
+ const clientSecret = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepClientSecret", "KEEP_CLIENT_SECRET");
19204
19794
  const stmFactory = deps.buildServiceTokenManager ?? ((cfg) => new keep_1.ServiceTokenManager(cfg));
19205
19795
  const stm = stmFactory({ keepApiUrl, clientId, clientSecret });
19206
19796
  const keepClientFactory = deps.buildKeepClient ?? ((cfg) => new keep_1.KeepClient(cfg));
@@ -19219,17 +19809,6 @@ var require_anthropic_api = __commonJS({
19219
19809
  return reveal.apiKey;
19220
19810
  };
19221
19811
  }
19222
- function resolveStringConfig(config, field, envFallback) {
19223
- const raw = config[field];
19224
- if (typeof raw === "string" && raw.length > 0) {
19225
- return (0, env_ref_1.resolveEnvRef)(raw, field);
19226
- }
19227
- const fromEnv = process.env[envFallback];
19228
- if (fromEnv && fromEnv.length > 0) {
19229
- return fromEnv;
19230
- }
19231
- throw new Error(`AnthropicApiExecutor dynamic-key mode requires "${field}" in config or the ${envFallback} env var`);
19232
- }
19233
19812
  function safeJsonParse(s) {
19234
19813
  try {
19235
19814
  const parsed = JSON.parse(s);
@@ -20594,7 +21173,7 @@ var require_dist5 = __commonJS({
20594
21173
  "../../packages/shuttle/dist/index.js"(exports2) {
20595
21174
  "use strict";
20596
21175
  Object.defineProperty(exports2, "__esModule", { value: true });
20597
- exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = 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.ExecutorRegistry = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
21176
+ exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = 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.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
20598
21177
  var cli_1 = require_cli();
20599
21178
  Object.defineProperty(exports2, "createCli", { enumerable: true, get: function() {
20600
21179
  return cli_1.createCli;
@@ -20620,6 +21199,10 @@ var require_dist5 = __commonJS({
20620
21199
  Object.defineProperty(exports2, "checkResponseMinVersion", { enumerable: true, get: function() {
20621
21200
  return version_check_1.checkResponseMinVersion;
20622
21201
  } });
21202
+ var keep_1 = require_dist4();
21203
+ Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
21204
+ return keep_1.AgentCredentialRevokedError;
21205
+ } });
20623
21206
  var registry_1 = require_registry();
20624
21207
  Object.defineProperty(exports2, "ExecutorRegistry", { enumerable: true, get: function() {
20625
21208
  return registry_1.ExecutorRegistry;
@@ -20721,7 +21304,7 @@ var import_shuttle = __toESM(require_dist5());
20721
21304
  // package.json
20722
21305
  var package_default = {
20723
21306
  name: "@whittlelabs/sifter",
20724
- version: "0.7.1",
21307
+ version: "0.9.0",
20725
21308
  description: "Whittle Sifter: paired AI reviewer for Whittle Sift job pools.",
20726
21309
  bin: {
20727
21310
  "whittle-sifter": "./dist/bin.js"
@@ -20905,6 +21488,17 @@ async function promptForCwd(prompt, fallback) {
20905
21488
  console.error("");
20906
21489
  process.exit(2);
20907
21490
  }
21491
+ if (err instanceof import_shuttle2.AgentCredentialRevokedError) {
21492
+ console.error("");
21493
+ console.error(`This ${sifterBrand.product.title}'s pairing has been revoked.`);
21494
+ console.error("");
21495
+ console.error("Keep refused its credentials \u2014 it was decommissioned from the");
21496
+ console.error("web app, or its secret was rotated by a newer pairing.");
21497
+ console.error("");
21498
+ console.error(`Run \`${sifterBrand.product.command} init\` to pair again.`);
21499
+ console.error("");
21500
+ process.exit(3);
21501
+ }
20908
21502
  const message = err instanceof Error ? err.message : String(err);
20909
21503
  console.error(`Fatal: ${message}`);
20910
21504
  process.exit(1);