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