@whittlelabs/sifter 0.20.0 → 0.22.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 +1149 -854
  2. package/bin.js.map +4 -4
  3. package/package.json +1 -1
package/bin.js CHANGED
@@ -15184,6 +15184,20 @@ var require_schema4 = __commonJS({
15184
15184
  identityId: zod_1.z.string().uuid(),
15185
15185
  metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional()
15186
15186
  }),
15187
+ /**
15188
+ * Runner-owned per-dispatch credential reveal (ADR 0027): the deployment's
15189
+ * reveal service identity. Present ⇒ hosted (the runner builds a credential
15190
+ * broker and reveals customer keys/tokens per dispatch); absent ⇒ local (no
15191
+ * reveals, ambient credentials). Distinct from the pairing-derived AGENT
15192
+ * credential (`auth`), which authenticates the claim loop. `keepClientId` /
15193
+ * `keepClientSecret` fall back to the KEEP_CLIENT_ID / KEEP_CLIENT_SECRET env.
15194
+ */
15195
+ credentials: zod_1.z.object({
15196
+ apiKeyFrom: zod_1.z.literal("job-metadata"),
15197
+ keepApiUrl: zod_1.z.string().url(),
15198
+ keepClientId: zod_1.z.string().optional(),
15199
+ keepClientSecret: zod_1.z.string().optional()
15200
+ }).passthrough().optional(),
15187
15201
  pools: zod_1.z.array(poolSchema).min(1, "At least one pool is required"),
15188
15202
  polling: zod_1.z.object({
15189
15203
  intervalMs: zod_1.z.number().int().positive().default(defaults_1.DEFAULTS.polling.intervalMs),
@@ -18435,226 +18449,610 @@ var require_chain = __commonJS({
18435
18449
  }
18436
18450
  });
18437
18451
 
18438
- // ../../packages/shuttle/dist/executors/output-gates.js
18439
- var require_output_gates = __commonJS({
18440
- "../../packages/shuttle/dist/executors/output-gates.js"(exports2) {
18452
+ // ../../packages/shuttle/dist/workspace/git.js
18453
+ var require_git = __commonJS({
18454
+ "../../packages/shuttle/dist/workspace/git.js"(exports2) {
18441
18455
  "use strict";
18456
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
18457
+ return mod && mod.__esModule ? mod : { "default": mod };
18458
+ };
18442
18459
  Object.defineProperty(exports2, "__esModule", { value: true });
18443
- exports2.resolvePath = resolvePath;
18444
- exports2.evaluateOutputGates = evaluateOutputGates;
18445
- exports2.renderRepairPrompt = renderRepairPrompt;
18446
- var INPUT_REF = "input:";
18447
- function resolvePath(root, path) {
18448
- let ctx = root;
18449
- for (const seg of path.split(".")) {
18450
- const project = seg.endsWith("[*]");
18451
- const key = project ? seg.slice(0, -3) : seg;
18452
- if (Array.isArray(ctx)) {
18453
- ctx = ctx.map((el) => key ? el?.[key] : el);
18454
- } else {
18455
- ctx = key ? ctx?.[key] : ctx;
18456
- }
18457
- if (project && !Array.isArray(ctx)) {
18458
- ctx = ctx == null ? [] : [ctx];
18460
+ exports2.gitMaterializer = void 0;
18461
+ exports2.buildGitAuthEnv = buildGitAuthEnv;
18462
+ var child_process_1 = require("child_process");
18463
+ var fs_1 = require("fs");
18464
+ var path_1 = __importDefault(require("path"));
18465
+ var DEFAULT_GIT_TIMEOUT_MS = 12e4;
18466
+ var DEFAULT_GIT_PROTOCOLS = "https:ssh:git";
18467
+ var DEFAULT_MAX_CONTEXT_REPOS = 5;
18468
+ exports2.gitMaterializer = {
18469
+ namespace: "git",
18470
+ materialize: materializeGit
18471
+ };
18472
+ async function materializeGit(request, runRoot, options) {
18473
+ const resolvedPrimary = readGitWorkspace(request);
18474
+ const primaryDir = path_1.default.join(runRoot, "primary");
18475
+ await fs_1.promises.mkdir(primaryDir, { recursive: true });
18476
+ if ("warning" in resolvedPrimary) {
18477
+ return { materialized: false, warning: resolvedPrimary.warning, context: [] };
18478
+ }
18479
+ const primary = resolvedPrimary.git;
18480
+ const timeoutMs = options.commandTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
18481
+ const allowedProtocols = options.allowedProtocols ?? DEFAULT_GIT_PROTOCOLS;
18482
+ const signal = options.signal;
18483
+ const maxContext = options.maxContextRepos ?? DEFAULT_MAX_CONTEXT_REPOS;
18484
+ const contextPlan = planContextRepos(primary.context, maxContext);
18485
+ const ref = primary.fetchCredential;
18486
+ let gitToken = null;
18487
+ if (ref && typeof ref.name === "string" && typeof ref.principal === "string" && options.credentials) {
18488
+ try {
18489
+ gitToken = await options.credentials.reveal(ref);
18490
+ } catch {
18491
+ gitToken = null;
18459
18492
  }
18460
18493
  }
18461
- return ctx;
18462
- }
18463
- function resolveRef(ref, output, inputs) {
18464
- if (ref.startsWith(INPUT_REF)) {
18465
- const label = ref.slice(INPUT_REF.length);
18466
- const row = inputs.find((i) => i.label === label) ?? inputs.find((i) => i.label === "artifacts" && i.payload.label === label);
18467
- if (!row)
18468
- return void 0;
18469
- return Object.prototype.hasOwnProperty.call(row.payload, "value") ? row.payload.value : row.payload;
18494
+ const authEnv = gitToken !== null ? buildGitAuthEnv([primary.remote, ...contextPlan.materialize.map((c) => c.git.remote)], gitToken) : void 0;
18495
+ const git = (args, cwd) => runGit(args, cwd, { timeoutMs, signal, allowedProtocols, extraEnv: authEnv });
18496
+ const primaryResult = await materializeCommit(primary, primaryDir, git);
18497
+ if (!primaryResult.ok) {
18498
+ await fs_1.promises.rm(primaryDir, { recursive: true, force: true }).catch(() => {
18499
+ });
18500
+ await fs_1.promises.mkdir(primaryDir).catch(() => {
18501
+ });
18502
+ return { materialized: false, warning: primaryResult.warning, context: [] };
18503
+ }
18504
+ const context = [];
18505
+ for (const planned of contextPlan.skip) {
18506
+ context.push({ ...planned, materialized: false });
18507
+ }
18508
+ if (contextPlan.materialize.length > 0) {
18509
+ const contextRoot = path_1.default.join(runRoot, "context");
18510
+ await fs_1.promises.mkdir(contextRoot, { recursive: true });
18511
+ for (const entry of contextPlan.materialize) {
18512
+ const dir = path_1.default.join(contextRoot, entry.name);
18513
+ await fs_1.promises.mkdir(dir);
18514
+ const result = await materializeCommit(entry.git, dir, git);
18515
+ context.push({
18516
+ name: entry.name,
18517
+ requestedName: entry.requestedName,
18518
+ materialized: result.ok,
18519
+ ...result.ok ? {} : { warning: result.warning }
18520
+ });
18521
+ }
18470
18522
  }
18471
- return resolvePath(output, ref);
18472
- }
18473
- function asStrings(value) {
18474
- if (Array.isArray(value))
18475
- return value.map((v) => String(v));
18476
- if (value == null)
18477
- return [];
18478
- return [String(value)];
18479
- }
18480
- var PRIMITIVES = {
18481
- /**
18482
- * `coverage` the values at `got` must exactly cover the set at `want`:
18483
- * every wanted value present, nothing invented, none repeated. Params:
18484
- * `{ got: <output path>, want: <ref>, subject?: <noun> }`.
18485
- */
18486
- coverage(params, output, inputs) {
18487
- const subject = typeof params.subject === "string" ? params.subject : "item";
18488
- const got = asStrings(resolveRef(String(params.got), output, inputs));
18489
- const want = asStrings(resolveRef(String(params.want), output, inputs));
18490
- const gotSet = new Set(got);
18491
- const wantSet = new Set(want);
18492
- const missing = want.filter((v) => !gotSet.has(v));
18493
- const unknown = [...new Set(got.filter((v) => !wantSet.has(v)))];
18494
- const seen = /* @__PURE__ */ new Set();
18495
- const duplicated = [];
18496
- for (const v of got) {
18497
- if (seen.has(v))
18498
- duplicated.push(v);
18499
- else
18500
- seen.add(v);
18523
+ context.sort((a, b) => contextOrder(contextPlan, a.requestedName) - contextOrder(contextPlan, b.requestedName));
18524
+ return { materialized: true, primaryDir, context };
18525
+ }
18526
+ function planContextRepos(raw, maxContext) {
18527
+ const plan = { materialize: [], skip: [], order: /* @__PURE__ */ new Map() };
18528
+ if (raw === void 0)
18529
+ return plan;
18530
+ if (!Array.isArray(raw))
18531
+ return plan;
18532
+ const usedNames = /* @__PURE__ */ new Set();
18533
+ raw.forEach((entry, index) => {
18534
+ const requestedName = entry && typeof entry.name === "string" ? entry.name : `context-${index}`;
18535
+ plan.order.set(requestedName, index);
18536
+ const resolved = readContextRepo(entry, index);
18537
+ if ("warning" in resolved) {
18538
+ plan.skip.push({ name: sanitizeMountName(requestedName, usedNames), requestedName, warning: resolved.warning });
18539
+ return;
18501
18540
  }
18502
- if (missing.length === 0 && unknown.length === 0 && duplicated.length === 0)
18503
- return [];
18504
- const parts = [];
18505
- if (missing.length)
18506
- parts.push(`missing ${subject}(s): [${missing.join(", ")}]`);
18507
- if (unknown.length)
18508
- parts.push(`not a known ${subject}: [${unknown.join(", ")}]`);
18509
- if (duplicated.length)
18510
- parts.push(`assessed more than once: [${[...new Set(duplicated)].join(", ")}]`);
18511
- return [
18512
- `every ${subject} must be covered exactly once \u2014 ${parts.join("; ")}. Return one entry per ${subject}, using its exact id.`
18513
- ];
18514
- },
18515
- /**
18516
- * `non_empty_when` — the array at `path` must be non-empty, optionally only
18517
- * when `when.path` equals `when.equals`. Params:
18518
- * `{ path: <output path>, when?: { path: <output path>, equals: <value> } }`.
18519
- */
18520
- non_empty_when(params, output) {
18521
- const when = params.when;
18522
- if (when && typeof when.path === "string") {
18523
- if (resolvePath(output, when.path) !== when.equals)
18524
- return [];
18541
+ if (plan.materialize.length >= maxContext) {
18542
+ plan.skip.push({
18543
+ name: sanitizeMountName(requestedName, usedNames),
18544
+ requestedName,
18545
+ warning: `skipped: exceeds the ${maxContext}-context-repo cap`
18546
+ });
18547
+ return;
18525
18548
  }
18526
- const path = String(params.path);
18527
- const value = resolvePath(output, path);
18528
- if (Array.isArray(value) && value.length > 0)
18529
- return [];
18530
- const cond = when && typeof when.path === "string" ? ` when ${when.path} is ${JSON.stringify(when.equals)}` : "";
18531
- return [
18532
- `'${path}' must be a non-empty array${cond}; it was ${Array.isArray(value) ? "empty" : "absent"}.`
18533
- ];
18549
+ plan.materialize.push({
18550
+ git: resolved.git,
18551
+ name: sanitizeMountName(requestedName, usedNames),
18552
+ requestedName
18553
+ });
18554
+ });
18555
+ return plan;
18556
+ }
18557
+ function contextOrder(plan, requestedName) {
18558
+ return plan.order.get(requestedName) ?? Number.MAX_SAFE_INTEGER;
18559
+ }
18560
+ async function materializeCommit(gitWorkspace, dir, git) {
18561
+ const hasCommit = () => git(["cat-file", "-e", `${gitWorkspace.commit}^{commit}`], dir).then((r) => r.code === 0);
18562
+ const init = await git(["init", "-q"], dir);
18563
+ if (init.code !== 0)
18564
+ return { ok: false, warning: `git init failed: ${firstLine(init.stderr)}` };
18565
+ const attempts = [];
18566
+ for (const ref of gitWorkspace.fetchRefs ?? []) {
18567
+ attempts.push({
18568
+ source: `${gitWorkspace.remote} ${ref}`,
18569
+ args: ["fetch", "--no-tags", "--depth=1", gitWorkspace.remote, ref]
18570
+ });
18534
18571
  }
18535
- };
18536
- function evaluateOutputGates(gates, output, inputs) {
18537
- const findings = [];
18538
- for (const gate of gates) {
18539
- const primitive = PRIMITIVES[gate.primitive];
18540
- if (!primitive) {
18541
- findings.push({ gate: gate.name, message: `unknown gate primitive '${gate.primitive}'` });
18572
+ attempts.push({
18573
+ source: `${gitWorkspace.remote} commit`,
18574
+ args: ["fetch", "--no-tags", "--depth=1", gitWorkspace.remote, gitWorkspace.commit]
18575
+ });
18576
+ const failures = [];
18577
+ let present = await hasCommit();
18578
+ for (const { source, args } of attempts) {
18579
+ if (present)
18580
+ break;
18581
+ const result = await git(args, dir);
18582
+ if (result.code !== 0) {
18583
+ failures.push(`${source}: ${firstLine(result.stderr)}`);
18542
18584
  continue;
18543
18585
  }
18544
- for (const message of primitive(gate.params, output, inputs)) {
18545
- findings.push({ gate: gate.name, message });
18546
- }
18586
+ present = await hasCommit();
18587
+ if (!present)
18588
+ failures.push(`${source}: fetch succeeded but ${gitWorkspace.commit} is still absent`);
18547
18589
  }
18548
- return { ok: findings.length === 0, findings };
18549
- }
18550
- function renderRepairPrompt(findings) {
18551
- return [
18552
- "Your previous answer did not satisfy these output checks. Return the corrected, complete answer that resolves every item below. Produce the same output shape with the problems fixed, and nothing else \u2014 no commentary, no explanation of the changes.",
18553
- "",
18554
- ...findings.map((f) => `- [${f.gate}] ${f.message}`)
18555
- ].join("\n");
18590
+ if (!present) {
18591
+ return {
18592
+ ok: false,
18593
+ warning: `could not materialize ${gitWorkspace.commit} from ${gitWorkspace.remote}` + (failures.length > 0 ? ` (${failures.join("; ")})` : "")
18594
+ };
18595
+ }
18596
+ const checkout = await git(["checkout", "--detach", gitWorkspace.commit], dir);
18597
+ if (checkout.code !== 0)
18598
+ return { ok: false, warning: `git checkout failed: ${firstLine(checkout.stderr)}` };
18599
+ return { ok: true };
18556
18600
  }
18557
- }
18558
- });
18559
-
18560
- // ../../packages/keep/dist/client.js
18561
- var require_client2 = __commonJS({
18562
- "../../packages/keep/dist/client.js"(exports2) {
18563
- "use strict";
18564
- Object.defineProperty(exports2, "__esModule", { value: true });
18565
- exports2.KeepClient = void 0;
18566
- var KeepClient = class {
18567
- baseUrl;
18568
- headers;
18569
- constructor(config) {
18570
- this.baseUrl = config.baseUrl.replace(/\/$/, "");
18571
- this.headers = { "Content-Type": "application/json" };
18572
- if (config.serviceToken) {
18573
- this.headers["Authorization"] = `Bearer ${config.serviceToken}`;
18574
- } else if (config.accessToken) {
18575
- this.headers["Authorization"] = `Bearer ${config.accessToken}`;
18601
+ function buildGitAuthEnv(remotes, token) {
18602
+ const origins = [];
18603
+ for (const remote of remotes) {
18604
+ try {
18605
+ const url = new URL(remote);
18606
+ if (url.protocol !== "https:")
18607
+ continue;
18608
+ const origin = `${url.protocol}//${url.host}/`;
18609
+ if (!origins.includes(origin))
18610
+ origins.push(origin);
18611
+ } catch {
18612
+ continue;
18576
18613
  }
18577
18614
  }
18578
- // ── Actor Tokens ────────────────────────────────────────────────
18579
- /**
18580
- * Mint a short-lived `act_` token for an end user, with this client's
18581
- * service identity recorded as the conduit. Requires this client to be
18582
- * constructed with a service token holding `keep:actor-tokens:write`.
18583
- */
18584
- async mintActorToken(actor) {
18585
- return this.request("POST", "/api/actor-tokens", { actor });
18586
- }
18587
- // ── Identity ────────────────────────────────────────────────────
18588
- async createAccount(email, password, name) {
18589
- return this.request("POST", "/api/accounts", { email, password, name });
18590
- }
18591
- async createSession(email, password) {
18592
- return this.request("POST", "/api/sessions", { email, password });
18593
- }
18594
- async destroySession(refreshToken) {
18595
- await this.request("DELETE", "/api/sessions", refreshToken ? { refreshToken } : void 0);
18596
- }
18597
- async refreshSession(refreshToken) {
18598
- return this.request("POST", "/api/sessions/renewal", { refreshToken });
18599
- }
18600
- async getCurrentUser() {
18601
- return this.request("GET", "/api/accounts/me");
18602
- }
18603
- async updateProfile(updates) {
18604
- return this.request("PATCH", "/api/accounts/me", updates);
18605
- }
18606
- async confirmEmail(email, code) {
18607
- await this.request("POST", "/api/email-confirmations", { email, code });
18608
- }
18609
- async requestVerificationCode(email) {
18610
- await this.request("POST", "/api/verification-codes", { email });
18611
- }
18612
- async requestPasswordReset(email) {
18613
- await this.request("POST", "/api/password-resets", { email });
18614
- }
18615
- async resetPassword(email, code, newPassword) {
18616
- await this.request("PUT", `/api/password-resets/${code}`, { email, newPassword });
18617
- }
18618
- async changePassword(currentPassword, newPassword) {
18619
- await this.request("PUT", "/api/passwords", { currentPassword, newPassword });
18620
- }
18621
- // ── Organizations ───────────────────────────────────────────────
18622
- async listOrganizations() {
18623
- const { organizations } = await this.request("GET", "/api/organizations");
18624
- return organizations;
18625
- }
18626
- async getOrganization(orgId) {
18627
- return this.request("GET", `/api/organizations/${orgId}`);
18628
- }
18629
- async createOrganization(name, description) {
18630
- return this.request("POST", "/api/organizations", { name, description });
18631
- }
18632
- async updateOrganization(orgId, updates) {
18633
- return this.request("PATCH", `/api/organizations/${orgId}`, updates);
18634
- }
18635
- async deleteOrganization(orgId) {
18636
- await this.request("DELETE", `/api/organizations/${orgId}`);
18637
- }
18638
- async listMembers(orgId) {
18639
- return this.request("GET", `/api/organizations/${orgId}/members`);
18615
+ if (origins.length === 0)
18616
+ return void 0;
18617
+ const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
18618
+ const env = { GIT_CONFIG_COUNT: String(origins.length) };
18619
+ origins.forEach((origin, i) => {
18620
+ env[`GIT_CONFIG_KEY_${i}`] = `http.${origin}.extraheader`;
18621
+ env[`GIT_CONFIG_VALUE_${i}`] = `Authorization: Basic ${basic}`;
18622
+ });
18623
+ return env;
18624
+ }
18625
+ var COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
18626
+ var REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.@^~+-]*$/;
18627
+ function readGitWorkspace(request) {
18628
+ if (typeof request !== "object" || request === null || Array.isArray(request)) {
18629
+ return { warning: "git workspace request must be an object" };
18640
18630
  }
18641
- async removeMember(orgId, userId) {
18642
- await this.request("DELETE", `/api/organizations/${orgId}/members/${userId}`);
18631
+ return validateGitShape(request);
18632
+ }
18633
+ function readContextRepo(entry, index) {
18634
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
18635
+ return { warning: `context[${index}] must be an object` };
18643
18636
  }
18644
- async listTeams(orgId) {
18645
- return this.request("GET", `/api/organizations/${orgId}/teams`);
18637
+ const e = entry;
18638
+ if (typeof e.name !== "string" || e.name.length === 0) {
18639
+ return { warning: `context[${index}].name must be a non-empty string` };
18646
18640
  }
18647
- async createTeam(orgId, name, description) {
18648
- return this.request("POST", `/api/organizations/${orgId}/teams`, { name, description });
18641
+ const shape = validateGitShape(e);
18642
+ if ("warning" in shape)
18643
+ return { warning: `context[${index}] ${shape.warning}` };
18644
+ return { git: { ...shape.git, name: e.name } };
18645
+ }
18646
+ function validateGitShape(git) {
18647
+ if (typeof git.remote !== "string" || git.remote.length === 0 || git.remote.startsWith("-")) {
18648
+ return { warning: "git.remote must be a non-empty remote URL or path" };
18649
18649
  }
18650
- async listTeamMembers(orgId, teamId) {
18651
- return this.request("GET", `/api/organizations/${orgId}/teams/${teamId}/members`);
18650
+ if (typeof git.commit !== "string" || !COMMIT_PATTERN.test(git.commit)) {
18651
+ return { warning: "git.commit must be a full 40-char commit SHA" };
18652
18652
  }
18653
- async addTeamMember(orgId, teamId, userId) {
18654
- return this.request("POST", `/api/organizations/${orgId}/teams/${teamId}/members`, { userId });
18653
+ if (git.fetchRefs !== void 0) {
18654
+ if (!Array.isArray(git.fetchRefs) || !git.fetchRefs.every((r) => typeof r === "string")) {
18655
+ return { warning: "git.fetchRefs must be an array of ref strings" };
18656
+ }
18657
+ const bad = git.fetchRefs.find((r) => !REF_PATTERN.test(r));
18658
+ if (bad !== void 0) {
18659
+ return { warning: `git.fetchRefs contains an invalid ref: ${JSON.stringify(bad)}` };
18660
+ }
18655
18661
  }
18656
- async createInvitation(orgId, email, roleGrants) {
18657
- return this.request("POST", `/api/organizations/${orgId}/invitations`, { email, roleGrants });
18662
+ return { git };
18663
+ }
18664
+ function sanitizeMountName(requested, used) {
18665
+ let base = requested.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "").replace(/-+$/, "").slice(0, 100);
18666
+ if (base.length === 0)
18667
+ base = "context";
18668
+ let name = base;
18669
+ let n = 2;
18670
+ while (used.has(name)) {
18671
+ name = `${base}-${n}`;
18672
+ n += 1;
18673
+ }
18674
+ used.add(name);
18675
+ return name;
18676
+ }
18677
+ function runGit(args, cwd, opts) {
18678
+ return new Promise((resolve) => {
18679
+ const child = (0, child_process_1.spawn)("git", args, {
18680
+ cwd,
18681
+ stdio: ["ignore", "pipe", "pipe"],
18682
+ env: {
18683
+ ...process.env,
18684
+ ...opts.extraEnv ?? {},
18685
+ GIT_TERMINAL_PROMPT: "0",
18686
+ GIT_ALLOW_PROTOCOL: opts.allowedProtocols
18687
+ }
18688
+ });
18689
+ const stdout = [];
18690
+ const stderr = [];
18691
+ let settled = false;
18692
+ const settle = (result) => {
18693
+ if (settled)
18694
+ return;
18695
+ settled = true;
18696
+ clearTimeout(timer);
18697
+ opts.signal?.removeEventListener("abort", onAbort);
18698
+ resolve(result);
18699
+ };
18700
+ const killChild = () => {
18701
+ child.kill("SIGTERM");
18702
+ const escalate = setTimeout(() => child.kill("SIGKILL"), 5e3);
18703
+ escalate.unref();
18704
+ child.once("close", () => clearTimeout(escalate));
18705
+ };
18706
+ const timer = setTimeout(() => {
18707
+ killChild();
18708
+ settle({ code: -1, stdout: Buffer.concat(stdout).toString("utf-8"), stderr: `git ${args[0]} timed out after ${opts.timeoutMs}ms` });
18709
+ }, opts.timeoutMs);
18710
+ const onAbort = () => {
18711
+ killChild();
18712
+ settle({ code: -1, stdout: Buffer.concat(stdout).toString("utf-8"), stderr: "aborted" });
18713
+ };
18714
+ if (opts.signal?.aborted) {
18715
+ onAbort();
18716
+ return;
18717
+ }
18718
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
18719
+ child.stdout.on("data", (c) => stdout.push(c));
18720
+ child.stderr.on("data", (c) => stderr.push(c));
18721
+ child.on("error", (err) => settle({ code: -1, stdout: "", stderr: err.message }));
18722
+ child.on("close", (code) => settle({ code: code ?? -1, stdout: Buffer.concat(stdout).toString("utf-8"), stderr: Buffer.concat(stderr).toString("utf-8") }));
18723
+ });
18724
+ }
18725
+ function firstLine(text) {
18726
+ const line = text.trim().split("\n", 1)[0];
18727
+ return line.length > 0 ? line : "(no output)";
18728
+ }
18729
+ }
18730
+ });
18731
+
18732
+ // ../../packages/shuttle/dist/workspace/overlay.js
18733
+ var require_overlay = __commonJS({
18734
+ "../../packages/shuttle/dist/workspace/overlay.js"(exports2) {
18735
+ "use strict";
18736
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
18737
+ return mod && mod.__esModule ? mod : { "default": mod };
18738
+ };
18739
+ Object.defineProperty(exports2, "__esModule", { value: true });
18740
+ exports2.overlayModifier = void 0;
18741
+ exports2.applyOverlay = applyOverlay;
18742
+ var fs_1 = require("fs");
18743
+ var path_1 = __importDefault(require("path"));
18744
+ var DEFAULT_TIMEOUT_MS = 3e4;
18745
+ var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
18746
+ exports2.overlayModifier = {
18747
+ namespace: "overlay",
18748
+ apply: applyOverlay
18749
+ };
18750
+ async function applyOverlay(request, runDir, options) {
18751
+ const timeoutMs = DEFAULT_TIMEOUT_MS;
18752
+ const maxBytes = options.overlayMaxBytes ?? DEFAULT_MAX_BYTES;
18753
+ const allowed = new Set((options.overlayAllowedHosts ?? []).map(normalizeOrigin).filter((o) => !!o));
18754
+ const rootReal = await fs_1.promises.realpath(runDir);
18755
+ const result = { written: 0, writtenPaths: [], skipped: [] };
18756
+ const files = request && typeof request === "object" && Array.isArray(request.files) ? request.files : [];
18757
+ for (const file of files) {
18758
+ const skip = (reason) => result.skipped.push({ path: String(file?.path ?? "?"), reason });
18759
+ if (typeof file?.path !== "string" || typeof file?.url !== "string") {
18760
+ skip("malformed overlay entry (path/url must be strings)");
18761
+ continue;
18762
+ }
18763
+ const dest = safeDestination(rootReal, file.path);
18764
+ if (!dest) {
18765
+ skip("path escapes the workspace root");
18766
+ continue;
18767
+ }
18768
+ let origin;
18769
+ try {
18770
+ const u = new URL(file.url);
18771
+ if (u.protocol !== "https:" && u.protocol !== "http:") {
18772
+ skip(`unsupported url protocol ${u.protocol}`);
18773
+ continue;
18774
+ }
18775
+ origin = normalizeOrigin(u.origin);
18776
+ } catch {
18777
+ skip("invalid url");
18778
+ continue;
18779
+ }
18780
+ if (!origin || !allowed.has(origin)) {
18781
+ skip(`origin not in the overlay allowlist (${origin})`);
18782
+ continue;
18783
+ }
18784
+ try {
18785
+ const content = await fetchCapped(file.url, { timeoutMs, maxBytes, signal: options.signal });
18786
+ const parent = path_1.default.dirname(dest);
18787
+ await fs_1.promises.mkdir(parent, { recursive: true });
18788
+ const parentReal = await fs_1.promises.realpath(parent);
18789
+ if (parentReal !== rootReal && !parentReal.startsWith(rootReal + path_1.default.sep)) {
18790
+ skip("destination parent escapes the workspace root");
18791
+ continue;
18792
+ }
18793
+ const handle = await fs_1.promises.open(dest, fs_1.constants.O_WRONLY | fs_1.constants.O_CREAT | fs_1.constants.O_TRUNC | fs_1.constants.O_NOFOLLOW, 420);
18794
+ try {
18795
+ await handle.writeFile(content);
18796
+ } finally {
18797
+ await handle.close();
18798
+ }
18799
+ result.written += 1;
18800
+ result.writtenPaths.push(file.path);
18801
+ } catch (err) {
18802
+ skip(err instanceof Error ? err.message : String(err));
18803
+ }
18804
+ }
18805
+ return result;
18806
+ }
18807
+ function safeDestination(rootReal, relPath) {
18808
+ if (relPath.length === 0 || path_1.default.isAbsolute(relPath))
18809
+ return void 0;
18810
+ const resolved = path_1.default.resolve(rootReal, relPath);
18811
+ const rootWithSep = rootReal.endsWith(path_1.default.sep) ? rootReal : rootReal + path_1.default.sep;
18812
+ if (resolved !== rootReal && !resolved.startsWith(rootWithSep))
18813
+ return void 0;
18814
+ if (relPath.split(/[/\\]/).includes(".."))
18815
+ return void 0;
18816
+ return resolved;
18817
+ }
18818
+ function normalizeOrigin(value) {
18819
+ try {
18820
+ const u = new URL(value.includes("://") ? value : `https://${value}`);
18821
+ return u.origin;
18822
+ } catch {
18823
+ return void 0;
18824
+ }
18825
+ }
18826
+ async function fetchCapped(url, opts) {
18827
+ const controller = new AbortController();
18828
+ const onParentAbort = () => controller.abort();
18829
+ opts.signal?.addEventListener("abort", onParentAbort, { once: true });
18830
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
18831
+ try {
18832
+ const response = await fetch(url, { signal: controller.signal });
18833
+ if (!response.ok)
18834
+ throw new Error(`overlay fetch returned HTTP ${response.status}`);
18835
+ if (!response.body) {
18836
+ const buf = Buffer.from(await response.arrayBuffer());
18837
+ if (buf.byteLength > opts.maxBytes)
18838
+ throw new Error("overlay file exceeds size cap");
18839
+ return buf;
18840
+ }
18841
+ const chunks = [];
18842
+ let total = 0;
18843
+ for await (const chunk of response.body) {
18844
+ const buf = Buffer.from(chunk);
18845
+ total += buf.byteLength;
18846
+ if (total > opts.maxBytes)
18847
+ throw new Error("overlay file exceeds size cap");
18848
+ chunks.push(buf);
18849
+ }
18850
+ return Buffer.concat(chunks);
18851
+ } finally {
18852
+ clearTimeout(timer);
18853
+ opts.signal?.removeEventListener("abort", onParentAbort);
18854
+ }
18855
+ }
18856
+ }
18857
+ });
18858
+
18859
+ // ../../packages/shuttle/dist/workspace/materialize.js
18860
+ var require_materialize = __commonJS({
18861
+ "../../packages/shuttle/dist/workspace/materialize.js"(exports2) {
18862
+ "use strict";
18863
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
18864
+ return mod && mod.__esModule ? mod : { "default": mod };
18865
+ };
18866
+ Object.defineProperty(exports2, "__esModule", { value: true });
18867
+ exports2.registerMaterializer = registerMaterializer;
18868
+ exports2.registerModifier = registerModifier;
18869
+ exports2.materializeWorkspace = materializeWorkspace;
18870
+ var fs_1 = require("fs");
18871
+ var os_1 = __importDefault(require("os"));
18872
+ var path_1 = __importDefault(require("path"));
18873
+ var git_1 = require_git();
18874
+ var overlay_1 = require_overlay();
18875
+ var materializers = /* @__PURE__ */ new Map();
18876
+ var modifiers = /* @__PURE__ */ new Map();
18877
+ function registerMaterializer(provider) {
18878
+ materializers.set(provider.namespace, provider);
18879
+ }
18880
+ function registerModifier(provider) {
18881
+ modifiers.set(provider.namespace, provider);
18882
+ }
18883
+ registerMaterializer(git_1.gitMaterializer);
18884
+ registerModifier(overlay_1.overlayModifier);
18885
+ async function materializeWorkspace(bag, options = {}) {
18886
+ const tmpBase = options.tmpDir ?? os_1.default.tmpdir();
18887
+ const runRoot = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-ws-"));
18888
+ const cleanup = async () => {
18889
+ await fs_1.promises.rm(runRoot, { recursive: true, force: true }).catch(() => {
18890
+ });
18891
+ };
18892
+ let materialized = false;
18893
+ let primaryDir;
18894
+ let warning;
18895
+ let context = [];
18896
+ let ranBase = false;
18897
+ for (const [namespace, provider] of materializers) {
18898
+ if (bag[namespace] !== void 0) {
18899
+ const outcome = await provider.materialize(bag[namespace], runRoot, options);
18900
+ materialized = outcome.materialized;
18901
+ primaryDir = outcome.primaryDir;
18902
+ warning = outcome.warning;
18903
+ context = outcome.context;
18904
+ ranBase = true;
18905
+ break;
18906
+ }
18907
+ }
18908
+ if (!ranBase) {
18909
+ const offered = Object.keys(bag).join(", ") || "(none)";
18910
+ await fs_1.promises.mkdir(path_1.default.join(runRoot, "primary"), { recursive: true }).catch(() => {
18911
+ });
18912
+ warning = `workspace carries no materializer this runner understands (offered: ${offered}; supported: ${[...materializers.keys()].join(", ")})`;
18913
+ }
18914
+ let overlay;
18915
+ for (const [namespace, provider] of modifiers) {
18916
+ if (bag[namespace] !== void 0) {
18917
+ overlay = await provider.apply(bag[namespace], runRoot, options);
18918
+ }
18919
+ }
18920
+ return { cwd: runRoot, materialized, primaryDir, warning, context, overlay, cleanup };
18921
+ }
18922
+ }
18923
+ });
18924
+
18925
+ // ../../packages/shuttle/dist/workspace/index.js
18926
+ var require_workspace = __commonJS({
18927
+ "../../packages/shuttle/dist/workspace/index.js"(exports2) {
18928
+ "use strict";
18929
+ Object.defineProperty(exports2, "__esModule", { value: true });
18930
+ exports2.applyOverlay = exports2.overlayModifier = exports2.buildGitAuthEnv = exports2.gitMaterializer = exports2.registerModifier = exports2.registerMaterializer = exports2.materializeWorkspace = void 0;
18931
+ var materialize_1 = require_materialize();
18932
+ Object.defineProperty(exports2, "materializeWorkspace", { enumerable: true, get: function() {
18933
+ return materialize_1.materializeWorkspace;
18934
+ } });
18935
+ Object.defineProperty(exports2, "registerMaterializer", { enumerable: true, get: function() {
18936
+ return materialize_1.registerMaterializer;
18937
+ } });
18938
+ Object.defineProperty(exports2, "registerModifier", { enumerable: true, get: function() {
18939
+ return materialize_1.registerModifier;
18940
+ } });
18941
+ var git_1 = require_git();
18942
+ Object.defineProperty(exports2, "gitMaterializer", { enumerable: true, get: function() {
18943
+ return git_1.gitMaterializer;
18944
+ } });
18945
+ Object.defineProperty(exports2, "buildGitAuthEnv", { enumerable: true, get: function() {
18946
+ return git_1.buildGitAuthEnv;
18947
+ } });
18948
+ var overlay_1 = require_overlay();
18949
+ Object.defineProperty(exports2, "overlayModifier", { enumerable: true, get: function() {
18950
+ return overlay_1.overlayModifier;
18951
+ } });
18952
+ Object.defineProperty(exports2, "applyOverlay", { enumerable: true, get: function() {
18953
+ return overlay_1.applyOverlay;
18954
+ } });
18955
+ }
18956
+ });
18957
+
18958
+ // ../../packages/keep/dist/client.js
18959
+ var require_client2 = __commonJS({
18960
+ "../../packages/keep/dist/client.js"(exports2) {
18961
+ "use strict";
18962
+ Object.defineProperty(exports2, "__esModule", { value: true });
18963
+ exports2.KeepClient = void 0;
18964
+ var KeepClient = class {
18965
+ baseUrl;
18966
+ headers;
18967
+ constructor(config) {
18968
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
18969
+ this.headers = { "Content-Type": "application/json" };
18970
+ if (config.serviceToken) {
18971
+ this.headers["Authorization"] = `Bearer ${config.serviceToken}`;
18972
+ } else if (config.accessToken) {
18973
+ this.headers["Authorization"] = `Bearer ${config.accessToken}`;
18974
+ }
18975
+ }
18976
+ // ── Actor Tokens ────────────────────────────────────────────────
18977
+ /**
18978
+ * Mint a short-lived `act_` token for an end user, with this client's
18979
+ * service identity recorded as the conduit. Requires this client to be
18980
+ * constructed with a service token holding `keep:actor-tokens:write`.
18981
+ */
18982
+ async mintActorToken(actor) {
18983
+ return this.request("POST", "/api/actor-tokens", { actor });
18984
+ }
18985
+ // ── Identity ────────────────────────────────────────────────────
18986
+ async createAccount(email, password, name) {
18987
+ return this.request("POST", "/api/accounts", { email, password, name });
18988
+ }
18989
+ async createSession(email, password) {
18990
+ return this.request("POST", "/api/sessions", { email, password });
18991
+ }
18992
+ async destroySession(refreshToken) {
18993
+ await this.request("DELETE", "/api/sessions", refreshToken ? { refreshToken } : void 0);
18994
+ }
18995
+ async refreshSession(refreshToken) {
18996
+ return this.request("POST", "/api/sessions/renewal", { refreshToken });
18997
+ }
18998
+ async getCurrentUser() {
18999
+ return this.request("GET", "/api/accounts/me");
19000
+ }
19001
+ async updateProfile(updates) {
19002
+ return this.request("PATCH", "/api/accounts/me", updates);
19003
+ }
19004
+ async confirmEmail(email, code) {
19005
+ await this.request("POST", "/api/email-confirmations", { email, code });
19006
+ }
19007
+ async requestVerificationCode(email) {
19008
+ await this.request("POST", "/api/verification-codes", { email });
19009
+ }
19010
+ async requestPasswordReset(email) {
19011
+ await this.request("POST", "/api/password-resets", { email });
19012
+ }
19013
+ async resetPassword(email, code, newPassword) {
19014
+ await this.request("PUT", `/api/password-resets/${code}`, { email, newPassword });
19015
+ }
19016
+ async changePassword(currentPassword, newPassword) {
19017
+ await this.request("PUT", "/api/passwords", { currentPassword, newPassword });
19018
+ }
19019
+ // ── Organizations ───────────────────────────────────────────────
19020
+ async listOrganizations() {
19021
+ const { organizations } = await this.request("GET", "/api/organizations");
19022
+ return organizations;
19023
+ }
19024
+ async getOrganization(orgId) {
19025
+ return this.request("GET", `/api/organizations/${orgId}`);
19026
+ }
19027
+ async createOrganization(name, description) {
19028
+ return this.request("POST", "/api/organizations", { name, description });
19029
+ }
19030
+ async updateOrganization(orgId, updates) {
19031
+ return this.request("PATCH", `/api/organizations/${orgId}`, updates);
19032
+ }
19033
+ async deleteOrganization(orgId) {
19034
+ await this.request("DELETE", `/api/organizations/${orgId}`);
19035
+ }
19036
+ async listMembers(orgId) {
19037
+ return this.request("GET", `/api/organizations/${orgId}/members`);
19038
+ }
19039
+ async removeMember(orgId, userId) {
19040
+ await this.request("DELETE", `/api/organizations/${orgId}/members/${userId}`);
19041
+ }
19042
+ async listTeams(orgId) {
19043
+ return this.request("GET", `/api/organizations/${orgId}/teams`);
19044
+ }
19045
+ async createTeam(orgId, name, description) {
19046
+ return this.request("POST", `/api/organizations/${orgId}/teams`, { name, description });
19047
+ }
19048
+ async listTeamMembers(orgId, teamId) {
19049
+ return this.request("GET", `/api/organizations/${orgId}/teams/${teamId}/members`);
19050
+ }
19051
+ async addTeamMember(orgId, teamId, userId) {
19052
+ return this.request("POST", `/api/organizations/${orgId}/teams/${teamId}/members`, { userId });
19053
+ }
19054
+ async createInvitation(orgId, email, roleGrants) {
19055
+ return this.request("POST", `/api/organizations/${orgId}/invitations`, { email, roleGrants });
18658
19056
  }
18659
19057
  async getInvitation(token) {
18660
19058
  return this.request("GET", `/api/invitations/${token}`);
@@ -18925,6 +19323,252 @@ var require_dist4 = __commonJS({
18925
19323
  }
18926
19324
  });
18927
19325
 
19326
+ // ../../packages/shuttle/dist/config/env-ref.js
19327
+ var require_env_ref = __commonJS({
19328
+ "../../packages/shuttle/dist/config/env-ref.js"(exports2) {
19329
+ "use strict";
19330
+ Object.defineProperty(exports2, "__esModule", { value: true });
19331
+ exports2.resolveEnvRef = resolveEnvRef;
19332
+ exports2.isEnvRef = isEnvRef;
19333
+ exports2.resolveConfigStringOrEnv = resolveConfigStringOrEnv;
19334
+ var ENV_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
19335
+ function resolveEnvRef(value, fieldName) {
19336
+ const match = ENV_REF_PATTERN.exec(value);
19337
+ if (!match)
19338
+ return value;
19339
+ const varName = match[1];
19340
+ const resolved = process.env[varName];
19341
+ if (resolved === void 0 || resolved === "") {
19342
+ const subject = fieldName ? `${fieldName} (${value})` : value;
19343
+ 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.`);
19344
+ }
19345
+ return resolved;
19346
+ }
19347
+ function isEnvRef(value) {
19348
+ return ENV_REF_PATTERN.test(value);
19349
+ }
19350
+ function resolveConfigStringOrEnv(config, field, envFallback) {
19351
+ const raw = config[field];
19352
+ if (typeof raw === "string" && raw.length > 0) {
19353
+ return resolveEnvRef(raw, field);
19354
+ }
19355
+ const fromEnv = process.env[envFallback];
19356
+ if (fromEnv && fromEnv.length > 0) {
19357
+ return fromEnv;
19358
+ }
19359
+ throw new Error(`dynamic-key mode requires "${field}" in config or the ${envFallback} env var`);
19360
+ }
19361
+ }
19362
+ });
19363
+
19364
+ // ../../packages/shuttle/dist/credentials/registry.js
19365
+ var require_registry = __commonJS({
19366
+ "../../packages/shuttle/dist/credentials/registry.js"(exports2) {
19367
+ "use strict";
19368
+ Object.defineProperty(exports2, "__esModule", { value: true });
19369
+ exports2.registerCredentialResolver = registerCredentialResolver;
19370
+ exports2.getCredentialResolver = getCredentialResolver;
19371
+ var resolvers = /* @__PURE__ */ new Map();
19372
+ function registerCredentialResolver(name, resolver) {
19373
+ resolvers.set(name, resolver);
19374
+ }
19375
+ function getCredentialResolver(name) {
19376
+ return resolvers.get(name);
19377
+ }
19378
+ registerCredentialResolver("managed-model-key", async (keep, principal) => {
19379
+ const reveal = await keep.revealManagedSifterKey(principal);
19380
+ return reveal.apiKey;
19381
+ });
19382
+ registerCredentialResolver("github-user-token", async (keep, principal) => {
19383
+ try {
19384
+ const reveal = await keep.getUserGithubToken(principal);
19385
+ return reveal.access_token;
19386
+ } catch {
19387
+ return null;
19388
+ }
19389
+ });
19390
+ }
19391
+ });
19392
+
19393
+ // ../../packages/shuttle/dist/credentials/broker.js
19394
+ var require_broker = __commonJS({
19395
+ "../../packages/shuttle/dist/credentials/broker.js"(exports2) {
19396
+ "use strict";
19397
+ Object.defineProperty(exports2, "__esModule", { value: true });
19398
+ exports2.createCredentialBroker = createCredentialBroker;
19399
+ var keep_1 = require_dist4();
19400
+ var env_ref_1 = require_env_ref();
19401
+ var registry_1 = require_registry();
19402
+ function createCredentialBroker(config, deps = {}) {
19403
+ if (typeof config !== "object" || config === null || config.apiKeyFrom !== "job-metadata") {
19404
+ return null;
19405
+ }
19406
+ const cfg = config;
19407
+ const keepApiUrl = (0, env_ref_1.resolveConfigStringOrEnv)(cfg, "keepApiUrl", "KEEP_API_URL");
19408
+ const clientId = (0, env_ref_1.resolveConfigStringOrEnv)(cfg, "keepClientId", "KEEP_CLIENT_ID");
19409
+ const clientSecret = (0, env_ref_1.resolveConfigStringOrEnv)(cfg, "keepClientSecret", "KEEP_CLIENT_SECRET");
19410
+ const stmFactory = deps.buildServiceTokenManager ?? ((c) => new keep_1.ServiceTokenManager(c));
19411
+ const stm = stmFactory({ keepApiUrl, clientId, clientSecret });
19412
+ const keepFactory = deps.buildKeepClient ?? ((c) => new keep_1.KeepClient(c));
19413
+ return {
19414
+ async reveal({ name, principal }) {
19415
+ const resolver = (0, registry_1.getCredentialResolver)(name);
19416
+ if (!resolver)
19417
+ return null;
19418
+ const serviceToken = await stm.getToken();
19419
+ if (!serviceToken) {
19420
+ throw new Error("credential broker: Keep service-token exchange failed. Check KEEP_CLIENT_ID / KEEP_CLIENT_SECRET.");
19421
+ }
19422
+ const keep = keepFactory({ baseUrl: keepApiUrl, serviceToken });
19423
+ return resolver(keep, principal);
19424
+ }
19425
+ };
19426
+ }
19427
+ }
19428
+ });
19429
+
19430
+ // ../../packages/shuttle/dist/credentials/index.js
19431
+ var require_credentials = __commonJS({
19432
+ "../../packages/shuttle/dist/credentials/index.js"(exports2) {
19433
+ "use strict";
19434
+ Object.defineProperty(exports2, "__esModule", { value: true });
19435
+ exports2.getCredentialResolver = exports2.registerCredentialResolver = exports2.createCredentialBroker = void 0;
19436
+ var broker_1 = require_broker();
19437
+ Object.defineProperty(exports2, "createCredentialBroker", { enumerable: true, get: function() {
19438
+ return broker_1.createCredentialBroker;
19439
+ } });
19440
+ var registry_1 = require_registry();
19441
+ Object.defineProperty(exports2, "registerCredentialResolver", { enumerable: true, get: function() {
19442
+ return registry_1.registerCredentialResolver;
19443
+ } });
19444
+ Object.defineProperty(exports2, "getCredentialResolver", { enumerable: true, get: function() {
19445
+ return registry_1.getCredentialResolver;
19446
+ } });
19447
+ }
19448
+ });
19449
+
19450
+ // ../../packages/shuttle/dist/executors/output-gates.js
19451
+ var require_output_gates = __commonJS({
19452
+ "../../packages/shuttle/dist/executors/output-gates.js"(exports2) {
19453
+ "use strict";
19454
+ Object.defineProperty(exports2, "__esModule", { value: true });
19455
+ exports2.resolvePath = resolvePath;
19456
+ exports2.evaluateOutputGates = evaluateOutputGates;
19457
+ exports2.renderRepairPrompt = renderRepairPrompt;
19458
+ var INPUT_REF = "input:";
19459
+ function resolvePath(root, path) {
19460
+ let ctx = root;
19461
+ for (const seg of path.split(".")) {
19462
+ const project = seg.endsWith("[*]");
19463
+ const key = project ? seg.slice(0, -3) : seg;
19464
+ if (Array.isArray(ctx)) {
19465
+ ctx = ctx.map((el) => key ? el?.[key] : el);
19466
+ } else {
19467
+ ctx = key ? ctx?.[key] : ctx;
19468
+ }
19469
+ if (project && !Array.isArray(ctx)) {
19470
+ ctx = ctx == null ? [] : [ctx];
19471
+ }
19472
+ }
19473
+ return ctx;
19474
+ }
19475
+ function resolveRef(ref, output, inputs) {
19476
+ if (ref.startsWith(INPUT_REF)) {
19477
+ const label = ref.slice(INPUT_REF.length);
19478
+ const row = inputs.find((i) => i.label === label) ?? inputs.find((i) => i.label === "artifacts" && i.payload.label === label);
19479
+ if (!row)
19480
+ return void 0;
19481
+ return Object.prototype.hasOwnProperty.call(row.payload, "value") ? row.payload.value : row.payload;
19482
+ }
19483
+ return resolvePath(output, ref);
19484
+ }
19485
+ function asStrings(value) {
19486
+ if (Array.isArray(value))
19487
+ return value.map((v) => String(v));
19488
+ if (value == null)
19489
+ return [];
19490
+ return [String(value)];
19491
+ }
19492
+ var PRIMITIVES = {
19493
+ /**
19494
+ * `coverage` — the values at `got` must exactly cover the set at `want`:
19495
+ * every wanted value present, nothing invented, none repeated. Params:
19496
+ * `{ got: <output path>, want: <ref>, subject?: <noun> }`.
19497
+ */
19498
+ coverage(params, output, inputs) {
19499
+ const subject = typeof params.subject === "string" ? params.subject : "item";
19500
+ const got = asStrings(resolveRef(String(params.got), output, inputs));
19501
+ const want = asStrings(resolveRef(String(params.want), output, inputs));
19502
+ const gotSet = new Set(got);
19503
+ const wantSet = new Set(want);
19504
+ const missing = want.filter((v) => !gotSet.has(v));
19505
+ const unknown = [...new Set(got.filter((v) => !wantSet.has(v)))];
19506
+ const seen = /* @__PURE__ */ new Set();
19507
+ const duplicated = [];
19508
+ for (const v of got) {
19509
+ if (seen.has(v))
19510
+ duplicated.push(v);
19511
+ else
19512
+ seen.add(v);
19513
+ }
19514
+ if (missing.length === 0 && unknown.length === 0 && duplicated.length === 0)
19515
+ return [];
19516
+ const parts = [];
19517
+ if (missing.length)
19518
+ parts.push(`missing ${subject}(s): [${missing.join(", ")}]`);
19519
+ if (unknown.length)
19520
+ parts.push(`not a known ${subject}: [${unknown.join(", ")}]`);
19521
+ if (duplicated.length)
19522
+ parts.push(`assessed more than once: [${[...new Set(duplicated)].join(", ")}]`);
19523
+ return [
19524
+ `every ${subject} must be covered exactly once \u2014 ${parts.join("; ")}. Return one entry per ${subject}, using its exact id.`
19525
+ ];
19526
+ },
19527
+ /**
19528
+ * `non_empty_when` — the array at `path` must be non-empty, optionally only
19529
+ * when `when.path` equals `when.equals`. Params:
19530
+ * `{ path: <output path>, when?: { path: <output path>, equals: <value> } }`.
19531
+ */
19532
+ non_empty_when(params, output) {
19533
+ const when = params.when;
19534
+ if (when && typeof when.path === "string") {
19535
+ if (resolvePath(output, when.path) !== when.equals)
19536
+ return [];
19537
+ }
19538
+ const path = String(params.path);
19539
+ const value = resolvePath(output, path);
19540
+ if (Array.isArray(value) && value.length > 0)
19541
+ return [];
19542
+ const cond = when && typeof when.path === "string" ? ` when ${when.path} is ${JSON.stringify(when.equals)}` : "";
19543
+ return [
19544
+ `'${path}' must be a non-empty array${cond}; it was ${Array.isArray(value) ? "empty" : "absent"}.`
19545
+ ];
19546
+ }
19547
+ };
19548
+ function evaluateOutputGates(gates, output, inputs) {
19549
+ const findings = [];
19550
+ for (const gate of gates) {
19551
+ const primitive = PRIMITIVES[gate.primitive];
19552
+ if (!primitive) {
19553
+ findings.push({ gate: gate.name, message: `unknown gate primitive '${gate.primitive}'` });
19554
+ continue;
19555
+ }
19556
+ for (const message of primitive(gate.params, output, inputs)) {
19557
+ findings.push({ gate: gate.name, message });
19558
+ }
19559
+ }
19560
+ return { ok: findings.length === 0, findings };
19561
+ }
19562
+ function renderRepairPrompt(findings) {
19563
+ return [
19564
+ "Your previous answer did not satisfy these output checks. Return the corrected, complete answer that resolves every item below. Produce the same output shape with the problems fixed, and nothing else \u2014 no commentary, no explanation of the changes.",
19565
+ "",
19566
+ ...findings.map((f) => `- [${f.gate}] ${f.message}`)
19567
+ ].join("\n");
19568
+ }
19569
+ }
19570
+ });
19571
+
18928
19572
  // ../../packages/shuttle/dist/logging/logger.js
18929
19573
  var require_logger = __commonJS({
18930
19574
  "../../packages/shuttle/dist/logging/logger.js"(exports2) {
@@ -18958,614 +19602,251 @@ var require_logger = __commonJS({
18958
19602
  message,
18959
19603
  ...context
18960
19604
  };
18961
- return JSON.stringify(entry);
18962
- }
18963
- function createLogger(options) {
18964
- const threshold = LEVEL_ORDER[options.level];
18965
- const format = options.format === "json" ? formatJson : formatPretty;
18966
- function emit(level, message, context) {
18967
- if (LEVEL_ORDER[level] < threshold)
18968
- return;
18969
- const line = format(level, message, context);
18970
- if (level === "error") {
18971
- console.error(line);
18972
- } else if (level === "warn") {
18973
- console.warn(line);
18974
- } else {
18975
- console.log(line);
18976
- }
18977
- }
18978
- return {
18979
- debug: (message, context) => emit("debug", message, context),
18980
- info: (message, context) => emit("info", message, context),
18981
- warn: (message, context) => emit("warn", message, context),
18982
- error: (message, context) => emit("error", message, context)
18983
- };
18984
- }
18985
- var globalLogger = createLogger({ level: "info", format: "pretty" });
18986
- function setGlobalLogger(logger) {
18987
- globalLogger = logger;
18988
- }
18989
- function getLogger() {
18990
- return globalLogger;
18991
- }
18992
- }
18993
- });
18994
-
18995
- // ../../packages/shuttle/dist/executors/registry.js
18996
- var require_registry = __commonJS({
18997
- "../../packages/shuttle/dist/executors/registry.js"(exports2) {
18998
- "use strict";
18999
- Object.defineProperty(exports2, "__esModule", { value: true });
19000
- exports2.ExecutorRegistry = void 0;
19001
- var ExecutorRegistry = class {
19002
- factories = /* @__PURE__ */ new Map();
19003
- allowlist;
19004
- constructor(allowlist = ["*"]) {
19005
- this.allowlist = allowlist;
19006
- }
19007
- register(name, factory) {
19008
- if (!this.isAllowed(name)) {
19009
- throw new Error(`Executor "${name}" is not enabled for this build. Allowed executors: ${this.allowlist.join(", ")}`);
19010
- }
19011
- this.factories.set(name, factory);
19012
- }
19013
- create(name, config) {
19014
- const factory = this.factories.get(name);
19015
- if (!factory) {
19016
- throw new Error(`Executor not registered: ${name}`);
19017
- }
19018
- return factory(config, name);
19019
- }
19020
- has(name) {
19021
- return this.factories.has(name);
19022
- }
19023
- get allowedExecutors() {
19024
- return this.allowlist;
19025
- }
19026
- /** All registered factories as JobExecutors built with their default config. */
19027
- build(configs) {
19028
- const out = [];
19029
- for (const [name, cfg] of Object.entries(configs)) {
19030
- if (!this.factories.has(name)) {
19031
- throw new Error(`Executor "${name}" in config is not registered or not enabled for this build.`);
19032
- }
19033
- out.push(this.create(name, cfg));
19034
- }
19035
- return out;
19036
- }
19037
- isAllowed(name) {
19038
- return this.allowlist[0] === "*" || this.allowlist.includes(name);
19039
- }
19040
- };
19041
- exports2.ExecutorRegistry = ExecutorRegistry;
19042
- }
19043
- });
19044
-
19045
- // ../../packages/shuttle/dist/executors/validate-output.js
19046
- var require_validate_output = __commonJS({
19047
- "../../packages/shuttle/dist/executors/validate-output.js"(exports2) {
19048
- "use strict";
19049
- Object.defineProperty(exports2, "__esModule", { value: true });
19050
- exports2.assertOutputValid = assertOutputValid;
19051
- var loom_1 = require_dist3();
19052
- function assertOutputValid(spec, parsed) {
19053
- const { valid, errors } = (0, loom_1.validateAgainstSchema)(parsed ?? null, spec.outputSchema);
19054
- if (!valid) {
19055
- throw new Error(`prompt-execution output failed schema validation: ${errors.join("; ")}`);
19056
- }
19057
- }
19058
- }
19059
- });
19060
-
19061
- // ../../packages/shuttle/dist/executors/progress.js
19062
- var require_progress = __commonJS({
19063
- "../../packages/shuttle/dist/executors/progress.js"(exports2) {
19064
- "use strict";
19065
- Object.defineProperty(exports2, "__esModule", { value: true });
19066
- exports2.StreamJsonTap = exports2.ProgressReporter = void 0;
19067
- var ProgressReporter = class {
19068
- sink;
19069
- seq = 0;
19070
- pending = [];
19071
- textBuf = "";
19072
- timer = null;
19073
- closed = false;
19074
- flushMs;
19075
- maxChars;
19076
- constructor(sink, options = {}) {
19077
- this.sink = sink;
19078
- this.flushMs = options.flushMs ?? 400;
19079
- this.maxChars = options.maxChars ?? 1200;
19080
- }
19081
- reasoning(text) {
19082
- if (this.closed || !text)
19083
- return;
19084
- this.textBuf += text;
19085
- if (this.textBuf.length >= this.maxChars) {
19086
- this.flush();
19087
- } else {
19088
- this.arm();
19089
- }
19090
- }
19091
- tool(name, target) {
19092
- if (this.closed || !name)
19093
- return;
19094
- this.drainText();
19095
- this.push({ kind: "tool", tool: target ? { name, target } : { name } });
19096
- this.flush();
19097
- }
19098
- /** Flush pending text + frames now. */
19099
- flush() {
19100
- if (this.closed)
19101
- return;
19102
- this.drainText();
19103
- this.clearTimer();
19104
- if (this.pending.length === 0)
19105
- return;
19106
- const batch = this.pending;
19107
- this.pending = [];
19108
- try {
19109
- this.sink(batch);
19110
- } catch {
19111
- }
19112
- }
19113
- /** Final flush; no further frames are emitted after this. */
19114
- close() {
19115
- this.flush();
19116
- this.closed = true;
19117
- this.clearTimer();
19118
- }
19119
- drainText() {
19120
- if (this.textBuf.length === 0)
19121
- return;
19122
- this.push({ kind: "reasoning", text: this.textBuf });
19123
- this.textBuf = "";
19124
- }
19125
- push(frame) {
19126
- this.pending.push({ seq: this.seq++, at: (/* @__PURE__ */ new Date()).toISOString(), ...frame });
19127
- }
19128
- arm() {
19129
- if (this.timer)
19130
- return;
19131
- this.timer = setTimeout(() => {
19132
- this.timer = null;
19133
- this.flush();
19134
- }, this.flushMs);
19135
- this.timer.unref?.();
19136
- }
19137
- clearTimer() {
19138
- if (this.timer) {
19139
- clearTimeout(this.timer);
19140
- this.timer = null;
19141
- }
19142
- }
19143
- };
19144
- exports2.ProgressReporter = ProgressReporter;
19145
- var StreamJsonTap = class {
19146
- handlers;
19147
- buf = "";
19148
- constructor(handlers) {
19149
- this.handlers = handlers;
19150
- }
19151
- /** Feed a raw stdout chunk. Emits handlers for every complete line parsed. */
19152
- push(chunk) {
19153
- this.buf += chunk;
19154
- let nl;
19155
- while ((nl = this.buf.indexOf("\n")) !== -1) {
19156
- const line = this.buf.slice(0, nl);
19157
- this.buf = this.buf.slice(nl + 1);
19158
- this.consumeLine(line);
19159
- }
19160
- }
19161
- /** Flush a trailing partial line (best-effort; usually empty at process close). */
19162
- end() {
19163
- if (this.buf.trim().length > 0)
19164
- this.consumeLine(this.buf);
19165
- this.buf = "";
19166
- }
19167
- consumeLine(line) {
19168
- const trimmed = line.trim();
19169
- if (!trimmed.startsWith("{"))
19170
- return;
19171
- let event;
19172
- try {
19173
- event = JSON.parse(trimmed);
19174
- } catch {
19175
- return;
19176
- }
19177
- if (event.type === "assistant") {
19178
- const content = event.message?.content;
19179
- if (!Array.isArray(content))
19180
- return;
19181
- for (const block of content) {
19182
- const b = block;
19183
- if (b?.type === "text" && typeof b.text === "string" && b.text.length > 0) {
19184
- this.handlers.onReasoning?.(b.text);
19185
- } else if (b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
19186
- this.handlers.onReasoning?.(b.thinking);
19187
- } else if (b?.type === "tool_use" && typeof b.name === "string") {
19188
- this.handlers.onTool?.(b.name, toolTarget(b.input));
19189
- }
19190
- }
19191
- } else if (event.type === "result") {
19192
- this.handlers.onResult?.();
19605
+ return JSON.stringify(entry);
19606
+ }
19607
+ function createLogger(options) {
19608
+ const threshold = LEVEL_ORDER[options.level];
19609
+ const format = options.format === "json" ? formatJson : formatPretty;
19610
+ function emit(level, message, context) {
19611
+ if (LEVEL_ORDER[level] < threshold)
19612
+ return;
19613
+ const line = format(level, message, context);
19614
+ if (level === "error") {
19615
+ console.error(line);
19616
+ } else if (level === "warn") {
19617
+ console.warn(line);
19618
+ } else {
19619
+ console.log(line);
19193
19620
  }
19194
19621
  }
19195
- };
19196
- exports2.StreamJsonTap = StreamJsonTap;
19197
- function toolTarget(input) {
19198
- if (!input)
19199
- return void 0;
19200
- for (const key of ["file_path", "path", "pattern", "command", "url", "query", "notebook_path"]) {
19201
- const v = input[key];
19202
- if (typeof v === "string" && v.length > 0)
19203
- return v;
19204
- }
19205
- return void 0;
19622
+ return {
19623
+ debug: (message, context) => emit("debug", message, context),
19624
+ info: (message, context) => emit("info", message, context),
19625
+ warn: (message, context) => emit("warn", message, context),
19626
+ error: (message, context) => emit("error", message, context)
19627
+ };
19628
+ }
19629
+ var globalLogger = createLogger({ level: "info", format: "pretty" });
19630
+ function setGlobalLogger(logger) {
19631
+ globalLogger = logger;
19632
+ }
19633
+ function getLogger() {
19634
+ return globalLogger;
19206
19635
  }
19207
19636
  }
19208
19637
  });
19209
19638
 
19210
- // ../../packages/shuttle/dist/executors/workspace.js
19211
- var require_workspace = __commonJS({
19212
- "../../packages/shuttle/dist/executors/workspace.js"(exports2) {
19639
+ // ../../packages/shuttle/dist/executors/registry.js
19640
+ var require_registry2 = __commonJS({
19641
+ "../../packages/shuttle/dist/executors/registry.js"(exports2) {
19213
19642
  "use strict";
19214
- var __importDefault = exports2 && exports2.__importDefault || function(mod) {
19215
- return mod && mod.__esModule ? mod : { "default": mod };
19216
- };
19217
19643
  Object.defineProperty(exports2, "__esModule", { value: true });
19218
- exports2.prepareWorkspace = prepareWorkspace;
19219
- exports2.buildGitAuthEnv = buildGitAuthEnv;
19220
- var child_process_1 = require("child_process");
19221
- var fs_1 = require("fs");
19222
- var os_1 = __importDefault(require("os"));
19223
- var path_1 = __importDefault(require("path"));
19224
- var DEFAULT_GIT_TIMEOUT_MS = 12e4;
19225
- var DEFAULT_GIT_PROTOCOLS = "https:ssh:git";
19226
- async function prepareWorkspace(workspace, options = {}) {
19227
- const tmpBase = options.tmpDir ?? os_1.default.tmpdir();
19228
- const resolved = readGitWorkspace(workspace);
19229
- if ("warning" in resolved) {
19230
- return degradedWorkspace(resolved.warning, tmpBase);
19644
+ exports2.ExecutorRegistry = void 0;
19645
+ var ExecutorRegistry = class {
19646
+ factories = /* @__PURE__ */ new Map();
19647
+ allowlist;
19648
+ constructor(allowlist = ["*"]) {
19649
+ this.allowlist = allowlist;
19231
19650
  }
19232
- const gitWorkspace = resolved.git;
19233
- const timeoutMs = options.commandTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
19234
- const allowedProtocols = options.allowedProtocols ?? DEFAULT_GIT_PROTOCOLS;
19235
- const signal = options.signal;
19236
- const authEnv = options.gitToken !== void 0 ? buildGitAuthEnv(gitWorkspace.remote, options.gitToken) : void 0;
19237
- const git = (args, cwd) => runGit(args, cwd, { timeoutMs, signal, allowedProtocols, extraEnv: authEnv });
19238
- const hasCommit = (dir) => git(["cat-file", "-e", `${gitWorkspace.commit}^{commit}`], dir).then((r) => r.code === 0);
19239
- const fetchCommit = async (dir, opts) => {
19240
- if (await hasCommit(dir))
19241
- return [];
19242
- const depth = opts.shallow ? ["--depth=1"] : [];
19243
- const attempts = [];
19244
- for (const ref of gitWorkspace.fetchRefs ?? []) {
19245
- attempts.push({
19246
- source: `${gitWorkspace.remote} ${ref}`,
19247
- args: ["fetch", "--no-tags", ...depth, gitWorkspace.remote, ref]
19248
- });
19249
- }
19250
- attempts.push({
19251
- source: `${gitWorkspace.remote} commit`,
19252
- args: ["fetch", "--no-tags", ...depth, gitWorkspace.remote, gitWorkspace.commit]
19253
- });
19254
- const failures2 = [];
19255
- for (const { source, args } of attempts) {
19256
- const result = await git(args, dir);
19257
- if (result.code !== 0) {
19258
- failures2.push(`${source}: ${firstLine(result.stderr)}`);
19259
- continue;
19260
- }
19261
- if (await hasCommit(dir))
19262
- return [];
19263
- failures2.push(`${source}: fetch succeeded but ${gitWorkspace.commit} is still absent`);
19651
+ register(name, factory) {
19652
+ if (!this.isAllowed(name)) {
19653
+ throw new Error(`Executor "${name}" is not enabled for this build. Allowed executors: ${this.allowlist.join(", ")}`);
19264
19654
  }
19265
- return failures2;
19266
- };
19267
- const notMaterialized = (failures2) => `could not materialize ${gitWorkspace.commit} from ${gitWorkspace.remote}` + (failures2.length > 0 ? ` (${failures2.join("; ")})` : "");
19268
- const tempRoot = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-ws-"));
19269
- const checkoutDir = path_1.default.join(tempRoot, "checkout");
19270
- const removeTempRoot = async () => {
19271
- await fs_1.promises.rm(tempRoot, { recursive: true, force: true }).catch(() => {
19272
- });
19273
- };
19274
- await fs_1.promises.mkdir(checkoutDir);
19275
- const init = await git(["init", "-q"], checkoutDir);
19276
- if (init.code !== 0) {
19277
- await removeTempRoot();
19278
- return degradedWorkspace(`git init failed: ${firstLine(init.stderr)}`, tmpBase);
19279
- }
19280
- const failures = await fetchCommit(checkoutDir, { shallow: true });
19281
- if (!await hasCommit(checkoutDir)) {
19282
- await removeTempRoot();
19283
- return degradedWorkspace(notMaterialized(failures), tmpBase);
19284
- }
19285
- const checkout = await git(["checkout", "--detach", gitWorkspace.commit], checkoutDir);
19286
- if (checkout.code !== 0) {
19287
- await removeTempRoot();
19288
- return degradedWorkspace(`git checkout failed: ${firstLine(checkout.stderr)}`, tmpBase);
19289
- }
19290
- return { cwd: checkoutDir, materialized: true, cleanup: removeTempRoot };
19291
- }
19292
- function buildGitAuthEnv(remote, token) {
19293
- let origin;
19294
- try {
19295
- const url = new URL(remote);
19296
- if (url.protocol !== "https:")
19297
- return void 0;
19298
- origin = `${url.protocol}//${url.host}/`;
19299
- } catch {
19300
- return void 0;
19301
- }
19302
- const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
19303
- return {
19304
- GIT_CONFIG_COUNT: "1",
19305
- GIT_CONFIG_KEY_0: `http.${origin}.extraheader`,
19306
- GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`
19307
- };
19308
- }
19309
- var COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
19310
- var REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.@^~+-]*$/;
19311
- function readGitWorkspace(workspace) {
19312
- const git = workspace.git;
19313
- if (git === void 0) {
19314
- const offered = Object.keys(workspace).join(", ") || "(none)";
19315
- return {
19316
- warning: `workspace carries no namespace this executor understands (offered: ${offered}; supported: git)`
19317
- };
19655
+ this.factories.set(name, factory);
19318
19656
  }
19319
- if (typeof git.remote !== "string" || git.remote.length === 0 || git.remote.startsWith("-")) {
19320
- return { warning: "workspace.git.remote must be a non-empty remote URL or path" };
19657
+ create(name, config) {
19658
+ const factory = this.factories.get(name);
19659
+ if (!factory) {
19660
+ throw new Error(`Executor not registered: ${name}`);
19661
+ }
19662
+ return factory(config, name);
19321
19663
  }
19322
- if (typeof git.commit !== "string" || !COMMIT_PATTERN.test(git.commit)) {
19323
- return { warning: "workspace.git.commit must be a full 40-char commit SHA" };
19664
+ has(name) {
19665
+ return this.factories.has(name);
19324
19666
  }
19325
- if (git.fetchRefs !== void 0) {
19326
- if (!Array.isArray(git.fetchRefs) || !git.fetchRefs.every((r) => typeof r === "string")) {
19327
- return { warning: "workspace.git.fetchRefs must be an array of ref strings" };
19328
- }
19329
- const bad = git.fetchRefs.find((r) => !REF_PATTERN.test(r));
19330
- if (bad !== void 0) {
19331
- return { warning: `workspace.git.fetchRefs contains an invalid ref: ${JSON.stringify(bad)}` };
19332
- }
19667
+ get allowedExecutors() {
19668
+ return this.allowlist;
19333
19669
  }
19334
- return { git };
19335
- }
19336
- async function degradedWorkspace(warning, tmpBase = os_1.default.tmpdir()) {
19337
- const scratchDir = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-ws-degraded-"));
19338
- return {
19339
- cwd: scratchDir,
19340
- materialized: false,
19341
- warning,
19342
- cleanup: async () => {
19343
- await fs_1.promises.rm(scratchDir, { recursive: true, force: true }).catch(() => {
19344
- });
19345
- }
19346
- };
19347
- }
19348
- function runGit(args, cwd, opts) {
19349
- return new Promise((resolve) => {
19350
- const child = (0, child_process_1.spawn)("git", args, {
19351
- cwd,
19352
- stdio: ["ignore", "pipe", "pipe"],
19353
- // `extraEnv` (e.g. the credential extraheader) goes first so the two
19354
- // security-critical vars below always win and can't be overridden.
19355
- env: {
19356
- ...process.env,
19357
- ...opts.extraEnv ?? {},
19358
- GIT_TERMINAL_PROMPT: "0",
19359
- // Transport allowlist. `remote` passes readGitWorkspace as any
19360
- // non-empty non-`-` string, which still admits `ext::sh -c …` and
19361
- // `fd::` remotes that execute arbitrary commands on this host.
19362
- // Restricting to the transports we actually use turns those into a
19363
- // fast failure (→ degrade) rather than RCE across the boundary this
19364
- // executor is meant to defend.
19365
- GIT_ALLOW_PROTOCOL: opts.allowedProtocols
19670
+ /** All registered factories as JobExecutors built with their default config. */
19671
+ build(configs) {
19672
+ const out = [];
19673
+ for (const [name, cfg] of Object.entries(configs)) {
19674
+ if (!this.factories.has(name)) {
19675
+ throw new Error(`Executor "${name}" in config is not registered or not enabled for this build.`);
19366
19676
  }
19367
- });
19368
- const stdout = [];
19369
- const stderr = [];
19370
- let settled = false;
19371
- const settle = (result) => {
19372
- if (settled)
19373
- return;
19374
- settled = true;
19375
- clearTimeout(timer);
19376
- opts.signal?.removeEventListener("abort", onAbort);
19377
- resolve(result);
19378
- };
19379
- const killChild = () => {
19380
- child.kill("SIGTERM");
19381
- const escalate = setTimeout(() => child.kill("SIGKILL"), 5e3);
19382
- escalate.unref();
19383
- child.once("close", () => clearTimeout(escalate));
19384
- };
19385
- const timer = setTimeout(() => {
19386
- killChild();
19387
- settle({
19388
- code: -1,
19389
- stdout: Buffer.concat(stdout).toString("utf-8"),
19390
- stderr: `git ${args[0]} timed out after ${opts.timeoutMs}ms`
19391
- });
19392
- }, opts.timeoutMs);
19393
- const onAbort = () => {
19394
- killChild();
19395
- settle({
19396
- code: -1,
19397
- stdout: Buffer.concat(stdout).toString("utf-8"),
19398
- stderr: "aborted"
19399
- });
19400
- };
19401
- if (opts.signal?.aborted) {
19402
- onAbort();
19403
- return;
19677
+ out.push(this.create(name, cfg));
19404
19678
  }
19405
- opts.signal?.addEventListener("abort", onAbort, { once: true });
19406
- child.stdout.on("data", (c) => stdout.push(c));
19407
- child.stderr.on("data", (c) => stderr.push(c));
19408
- child.on("error", (err) => settle({ code: -1, stdout: "", stderr: err.message }));
19409
- child.on("close", (code) => settle({
19410
- code: code ?? -1,
19411
- stdout: Buffer.concat(stdout).toString("utf-8"),
19412
- stderr: Buffer.concat(stderr).toString("utf-8")
19413
- }));
19414
- });
19415
- }
19416
- function firstLine(text) {
19417
- const line = text.trim().split("\n", 1)[0];
19418
- return line.length > 0 ? line : "(no output)";
19679
+ return out;
19680
+ }
19681
+ isAllowed(name) {
19682
+ return this.allowlist[0] === "*" || this.allowlist.includes(name);
19683
+ }
19684
+ };
19685
+ exports2.ExecutorRegistry = ExecutorRegistry;
19686
+ }
19687
+ });
19688
+
19689
+ // ../../packages/shuttle/dist/executors/validate-output.js
19690
+ var require_validate_output = __commonJS({
19691
+ "../../packages/shuttle/dist/executors/validate-output.js"(exports2) {
19692
+ "use strict";
19693
+ Object.defineProperty(exports2, "__esModule", { value: true });
19694
+ exports2.assertOutputValid = assertOutputValid;
19695
+ var loom_1 = require_dist3();
19696
+ function assertOutputValid(spec, parsed) {
19697
+ const { valid, errors } = (0, loom_1.validateAgainstSchema)(parsed ?? null, spec.outputSchema);
19698
+ if (!valid) {
19699
+ throw new Error(`prompt-execution output failed schema validation: ${errors.join("; ")}`);
19700
+ }
19419
19701
  }
19420
19702
  }
19421
19703
  });
19422
19704
 
19423
- // ../../packages/shuttle/dist/executors/overlay.js
19424
- var require_overlay = __commonJS({
19425
- "../../packages/shuttle/dist/executors/overlay.js"(exports2) {
19705
+ // ../../packages/shuttle/dist/executors/progress.js
19706
+ var require_progress = __commonJS({
19707
+ "../../packages/shuttle/dist/executors/progress.js"(exports2) {
19426
19708
  "use strict";
19427
- var __importDefault = exports2 && exports2.__importDefault || function(mod) {
19428
- return mod && mod.__esModule ? mod : { "default": mod };
19429
- };
19430
19709
  Object.defineProperty(exports2, "__esModule", { value: true });
19431
- exports2.applyOverlay = applyOverlay;
19432
- var fs_1 = require("fs");
19433
- var path_1 = __importDefault(require("path"));
19434
- var DEFAULT_TIMEOUT_MS = 3e4;
19435
- var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
19436
- async function applyOverlay(runDir, overlay, options) {
19437
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
19438
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
19439
- const allowed = new Set(options.allowedHosts.map(normalizeOrigin).filter((o) => !!o));
19440
- const rootReal = await fs_1.promises.realpath(runDir);
19441
- const result = { written: 0, writtenPaths: [], skipped: [] };
19442
- const files = Array.isArray(overlay.files) ? overlay.files : [];
19443
- for (const file of files) {
19444
- const skip = (reason) => result.skipped.push({ path: String(file?.path ?? "?"), reason });
19445
- if (typeof file?.path !== "string" || typeof file?.url !== "string") {
19446
- skip("malformed overlay entry (path/url must be strings)");
19447
- continue;
19448
- }
19449
- const dest = safeDestination(rootReal, file.path);
19450
- if (!dest) {
19451
- skip("path escapes the workspace root");
19452
- continue;
19453
- }
19454
- let origin;
19455
- try {
19456
- const u = new URL(file.url);
19457
- if (u.protocol !== "https:" && u.protocol !== "http:") {
19458
- skip(`unsupported url protocol ${u.protocol}`);
19459
- continue;
19460
- }
19461
- origin = normalizeOrigin(u.origin);
19462
- } catch {
19463
- skip("invalid url");
19464
- continue;
19465
- }
19466
- if (!origin || !allowed.has(origin)) {
19467
- skip(`origin not in the overlay allowlist (${origin})`);
19468
- continue;
19710
+ exports2.StreamJsonTap = exports2.ProgressReporter = void 0;
19711
+ var ProgressReporter = class {
19712
+ sink;
19713
+ seq = 0;
19714
+ pending = [];
19715
+ textBuf = "";
19716
+ timer = null;
19717
+ closed = false;
19718
+ flushMs;
19719
+ maxChars;
19720
+ constructor(sink, options = {}) {
19721
+ this.sink = sink;
19722
+ this.flushMs = options.flushMs ?? 400;
19723
+ this.maxChars = options.maxChars ?? 1200;
19724
+ }
19725
+ reasoning(text) {
19726
+ if (this.closed || !text)
19727
+ return;
19728
+ this.textBuf += text;
19729
+ if (this.textBuf.length >= this.maxChars) {
19730
+ this.flush();
19731
+ } else {
19732
+ this.arm();
19469
19733
  }
19734
+ }
19735
+ tool(name, target) {
19736
+ if (this.closed || !name)
19737
+ return;
19738
+ this.drainText();
19739
+ this.push({ kind: "tool", tool: target ? { name, target } : { name } });
19740
+ this.flush();
19741
+ }
19742
+ /** Flush pending text + frames now. */
19743
+ flush() {
19744
+ if (this.closed)
19745
+ return;
19746
+ this.drainText();
19747
+ this.clearTimer();
19748
+ if (this.pending.length === 0)
19749
+ return;
19750
+ const batch = this.pending;
19751
+ this.pending = [];
19470
19752
  try {
19471
- const content = await fetchCapped(file.url, { timeoutMs, maxBytes, signal: options.signal });
19472
- await fs_1.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
19473
- await fs_1.promises.writeFile(dest, content);
19474
- result.written += 1;
19475
- result.writtenPaths.push(file.path);
19476
- } catch (err) {
19477
- skip(err instanceof Error ? err.message : String(err));
19753
+ this.sink(batch);
19754
+ } catch {
19478
19755
  }
19479
19756
  }
19480
- return result;
19481
- }
19482
- function safeDestination(rootReal, relPath) {
19483
- if (relPath.length === 0 || path_1.default.isAbsolute(relPath))
19484
- return void 0;
19485
- const resolved = path_1.default.resolve(rootReal, relPath);
19486
- const rootWithSep = rootReal.endsWith(path_1.default.sep) ? rootReal : rootReal + path_1.default.sep;
19487
- if (resolved !== rootReal && !resolved.startsWith(rootWithSep))
19488
- return void 0;
19489
- if (relPath.split(/[/\\]/).includes(".."))
19490
- return void 0;
19491
- return resolved;
19492
- }
19493
- function normalizeOrigin(value) {
19494
- try {
19495
- const u = new URL(value.includes("://") ? value : `https://${value}`);
19496
- return u.origin;
19497
- } catch {
19498
- return void 0;
19757
+ /** Final flush; no further frames are emitted after this. */
19758
+ close() {
19759
+ this.flush();
19760
+ this.closed = true;
19761
+ this.clearTimer();
19499
19762
  }
19500
- }
19501
- async function fetchCapped(url, opts) {
19502
- const controller = new AbortController();
19503
- const onParentAbort = () => controller.abort();
19504
- opts.signal?.addEventListener("abort", onParentAbort, { once: true });
19505
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
19506
- try {
19507
- const response = await fetch(url, { signal: controller.signal });
19508
- if (!response.ok) {
19509
- throw new Error(`overlay fetch returned HTTP ${response.status}`);
19510
- }
19511
- if (!response.body) {
19512
- const buf = Buffer.from(await response.arrayBuffer());
19513
- if (buf.byteLength > opts.maxBytes)
19514
- throw new Error("overlay file exceeds size cap");
19515
- return buf;
19763
+ drainText() {
19764
+ if (this.textBuf.length === 0)
19765
+ return;
19766
+ this.push({ kind: "reasoning", text: this.textBuf });
19767
+ this.textBuf = "";
19768
+ }
19769
+ push(frame) {
19770
+ this.pending.push({ seq: this.seq++, at: (/* @__PURE__ */ new Date()).toISOString(), ...frame });
19771
+ }
19772
+ arm() {
19773
+ if (this.timer)
19774
+ return;
19775
+ this.timer = setTimeout(() => {
19776
+ this.timer = null;
19777
+ this.flush();
19778
+ }, this.flushMs);
19779
+ this.timer.unref?.();
19780
+ }
19781
+ clearTimer() {
19782
+ if (this.timer) {
19783
+ clearTimeout(this.timer);
19784
+ this.timer = null;
19516
19785
  }
19517
- const chunks = [];
19518
- let total = 0;
19519
- for await (const chunk of response.body) {
19520
- const buf = Buffer.from(chunk);
19521
- total += buf.byteLength;
19522
- if (total > opts.maxBytes)
19523
- throw new Error("overlay file exceeds size cap");
19524
- chunks.push(buf);
19786
+ }
19787
+ };
19788
+ exports2.ProgressReporter = ProgressReporter;
19789
+ var StreamJsonTap = class {
19790
+ handlers;
19791
+ buf = "";
19792
+ constructor(handlers) {
19793
+ this.handlers = handlers;
19794
+ }
19795
+ /** Feed a raw stdout chunk. Emits handlers for every complete line parsed. */
19796
+ push(chunk) {
19797
+ this.buf += chunk;
19798
+ let nl;
19799
+ while ((nl = this.buf.indexOf("\n")) !== -1) {
19800
+ const line = this.buf.slice(0, nl);
19801
+ this.buf = this.buf.slice(nl + 1);
19802
+ this.consumeLine(line);
19525
19803
  }
19526
- return Buffer.concat(chunks);
19527
- } finally {
19528
- clearTimeout(timer);
19529
- opts.signal?.removeEventListener("abort", onParentAbort);
19530
19804
  }
19531
- }
19532
- }
19533
- });
19534
-
19535
- // ../../packages/shuttle/dist/config/env-ref.js
19536
- var require_env_ref = __commonJS({
19537
- "../../packages/shuttle/dist/config/env-ref.js"(exports2) {
19538
- "use strict";
19539
- Object.defineProperty(exports2, "__esModule", { value: true });
19540
- exports2.resolveEnvRef = resolveEnvRef;
19541
- exports2.isEnvRef = isEnvRef;
19542
- exports2.resolveConfigStringOrEnv = resolveConfigStringOrEnv;
19543
- var ENV_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
19544
- function resolveEnvRef(value, fieldName) {
19545
- const match = ENV_REF_PATTERN.exec(value);
19546
- if (!match)
19547
- return value;
19548
- const varName = match[1];
19549
- const resolved = process.env[varName];
19550
- if (resolved === void 0 || resolved === "") {
19551
- const subject = fieldName ? `${fieldName} (${value})` : value;
19552
- 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.`);
19805
+ /** Flush a trailing partial line (best-effort; usually empty at process close). */
19806
+ end() {
19807
+ if (this.buf.trim().length > 0)
19808
+ this.consumeLine(this.buf);
19809
+ this.buf = "";
19553
19810
  }
19554
- return resolved;
19555
- }
19556
- function isEnvRef(value) {
19557
- return ENV_REF_PATTERN.test(value);
19558
- }
19559
- function resolveConfigStringOrEnv(config, field, envFallback) {
19560
- const raw = config[field];
19561
- if (typeof raw === "string" && raw.length > 0) {
19562
- return resolveEnvRef(raw, field);
19811
+ consumeLine(line) {
19812
+ const trimmed = line.trim();
19813
+ if (!trimmed.startsWith("{"))
19814
+ return;
19815
+ let event;
19816
+ try {
19817
+ event = JSON.parse(trimmed);
19818
+ } catch {
19819
+ return;
19820
+ }
19821
+ if (event.type === "assistant") {
19822
+ const content = event.message?.content;
19823
+ if (!Array.isArray(content))
19824
+ return;
19825
+ for (const block of content) {
19826
+ const b = block;
19827
+ if (b?.type === "text" && typeof b.text === "string" && b.text.length > 0) {
19828
+ this.handlers.onReasoning?.(b.text);
19829
+ } else if (b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
19830
+ this.handlers.onReasoning?.(b.thinking);
19831
+ } else if (b?.type === "tool_use" && typeof b.name === "string") {
19832
+ this.handlers.onTool?.(b.name, toolTarget(b.input));
19833
+ }
19834
+ }
19835
+ } else if (event.type === "result") {
19836
+ this.handlers.onResult?.();
19837
+ }
19563
19838
  }
19564
- const fromEnv = process.env[envFallback];
19565
- if (fromEnv && fromEnv.length > 0) {
19566
- return fromEnv;
19839
+ };
19840
+ exports2.StreamJsonTap = StreamJsonTap;
19841
+ function toolTarget(input) {
19842
+ if (!input)
19843
+ return void 0;
19844
+ for (const key of ["file_path", "path", "pattern", "command", "url", "query", "notebook_path"]) {
19845
+ const v = input[key];
19846
+ if (typeof v === "string" && v.length > 0)
19847
+ return v;
19567
19848
  }
19568
- throw new Error(`dynamic-key mode requires "${field}" in config or the ${envFallback} env var`);
19849
+ return void 0;
19569
19850
  }
19570
19851
  }
19571
19852
  });
@@ -19588,12 +19869,8 @@ var require_claude_code = __commonJS({
19588
19869
  var path_1 = __importDefault(require("path"));
19589
19870
  var string_decoder_1 = require("string_decoder");
19590
19871
  var loom_1 = require_dist3();
19591
- var keep_1 = require_dist4();
19592
19872
  var validate_output_1 = require_validate_output();
19593
19873
  var progress_1 = require_progress();
19594
- var workspace_1 = require_workspace();
19595
- var overlay_1 = require_overlay();
19596
- var env_ref_1 = require_env_ref();
19597
19874
  var apply_1 = require_apply();
19598
19875
  var DEFAULT_TIMEOUT = 6e5;
19599
19876
  var ClaudeCodeExecutor = class {
@@ -19611,47 +19888,49 @@ var require_claude_code = __commonJS({
19611
19888
  outputTokens: spec.costCapHint?.estimatedOutputTokens
19612
19889
  };
19613
19890
  }
19891
+ /**
19892
+ * Declare the run environment to the runner (ADR 0026): the opaque workspace
19893
+ * bag plus the materialization config (tmp dir, overlay allowlist). No
19894
+ * credentials here (ADR 0027) — the runner's broker reveals them on demand:
19895
+ * this executor pulls its model key from `ctx.credentials` in `execute()`,
19896
+ * and the git request references its own fetch credential. The executor
19897
+ * touches neither git nor Keep.
19898
+ */
19899
+ async prepareEnvironment(dispatch) {
19900
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
19901
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : void 0;
19902
+ return {
19903
+ ...spec.workspace ? { workspaceRequest: spec.workspace } : {},
19904
+ materializeOptions: {
19905
+ ...tmpBase ? { tmpDir: tmpBase } : {},
19906
+ overlayAllowedHosts: this.config.overlayAllowedHosts ?? []
19907
+ }
19908
+ };
19909
+ }
19614
19910
  async execute(dispatch, signal, ctx) {
19615
19911
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
19616
- let credentials = null;
19617
- if (this.config.resolveCredentials) {
19912
+ const workspace = ctx?.workspace ?? null;
19913
+ let modelKey = null;
19914
+ if (ctx?.credentials) {
19618
19915
  const hints = (0, loom_1.pickProviderHints)(spec, "anthropic-api");
19619
19916
  const customerId = typeof hints.customerId === "string" ? hints.customerId : void 0;
19620
19917
  if (!customerId) {
19621
19918
  throw new Error("claude-code dynamic-key mode requires providerHints.anthropicApi.customerId on every job");
19622
19919
  }
19623
- credentials = await this.config.resolveCredentials({
19624
- customerId,
19625
- needGithubToken: !!spec.workspace
19626
- });
19920
+ modelKey = await ctx.credentials.reveal({ name: "managed-model-key", principal: customerId });
19627
19921
  }
19628
19922
  let jobScratch = null;
19629
19923
  let configDir;
19630
- if (credentials) {
19924
+ if (modelKey) {
19631
19925
  jobScratch = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-cc-"));
19632
19926
  configDir = path_1.default.join(jobScratch, "claude-config");
19633
19927
  await fs_1.promises.mkdir(configDir);
19634
19928
  }
19635
19929
  const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
19636
- let workspace = null;
19637
19930
  let promptOnlyScratch = null;
19638
- let overlayResult = null;
19639
19931
  const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
19640
19932
  try {
19641
- if (spec.workspace) {
19642
- workspace = await (0, workspace_1.prepareWorkspace)(spec.workspace, {
19643
- signal,
19644
- tmpDir: tmpBase,
19645
- ...credentials?.githubToken ? { gitToken: credentials.githubToken } : {}
19646
- });
19647
- if (spec.workspace.overlay) {
19648
- overlayResult = await (0, overlay_1.applyOverlay)(workspace.cwd, spec.workspace.overlay, {
19649
- allowedHosts: this.config.overlayAllowedHosts ?? [],
19650
- signal
19651
- });
19652
- }
19653
- }
19654
- const childEnv = buildChildEnv(credentials, configDir);
19933
+ const childEnv = buildChildEnv(modelKey, configDir);
19655
19934
  const claudeHints = (0, loom_1.pickProviderHints)(spec, "claude-code");
19656
19935
  const hintedModel = typeof claudeHints.model === "string" && claudeHints.model.length > 0 ? claudeHints.model : void 0;
19657
19936
  if (!workspace) {
@@ -19661,7 +19940,7 @@ var require_claude_code = __commonJS({
19661
19940
  const { response, readPaths, toolText } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel, reporter);
19662
19941
  if (workspace) {
19663
19942
  const reads = [...readPaths];
19664
- for (const rel of overlayResult?.writtenPaths ?? []) {
19943
+ for (const rel of workspace.overlay?.writtenPaths ?? []) {
19665
19944
  if (toolText.includes(rel))
19666
19945
  reads.push(path_1.default.join(runCwd, rel));
19667
19946
  }
@@ -19674,10 +19953,17 @@ var require_claude_code = __commonJS({
19674
19953
  content: {
19675
19954
  materialized: workspace.materialized,
19676
19955
  ...workspace.warning ? { warning: workspace.warning } : {},
19677
- ...overlayResult ? {
19956
+ ...workspace.context.length > 0 ? {
19957
+ context: workspace.context.map((c) => ({
19958
+ name: c.requestedName,
19959
+ materialized: c.materialized,
19960
+ ...c.warning ? { warning: c.warning } : {}
19961
+ }))
19962
+ } : {},
19963
+ ...workspace.overlay ? {
19678
19964
  overlay: {
19679
- written: overlayResult.written,
19680
- ...overlayResult.skipped.length > 0 ? { skipped: overlayResult.skipped } : {}
19965
+ written: workspace.overlay.written,
19966
+ ...workspace.overlay.skipped.length > 0 ? { skipped: workspace.overlay.skipped } : {}
19681
19967
  }
19682
19968
  } : {}
19683
19969
  }
@@ -19686,8 +19972,6 @@ var require_claude_code = __commonJS({
19686
19972
  return response;
19687
19973
  } finally {
19688
19974
  reporter?.close();
19689
- if (workspace)
19690
- await workspace.cleanup();
19691
19975
  if (promptOnlyScratch)
19692
19976
  await fs_1.promises.rm(promptOnlyScratch, { recursive: true, force: true }).catch(() => {
19693
19977
  });
@@ -19767,18 +20051,27 @@ var require_claude_code = __commonJS({
19767
20051
  onResult: () => reporter.flush()
19768
20052
  }) : null;
19769
20053
  const decoder = tap ? new string_decoder_1.StringDecoder("utf8") : null;
20054
+ child.stdin.write(prompt);
20055
+ child.stdin.end();
20056
+ const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
20057
+ let timer;
20058
+ const armIdleTimer = () => {
20059
+ if (settled)
20060
+ return;
20061
+ if (timer)
20062
+ clearTimeout(timer);
20063
+ timer = setTimeout(() => {
20064
+ child.kill("SIGTERM");
20065
+ settle(() => reject(new Error(`Claude Code produced no output for ${timeout}ms; killed as hung`)));
20066
+ }, timeout);
20067
+ };
20068
+ armIdleTimer();
19770
20069
  child.stdout.on("data", (chunk) => {
19771
20070
  stdoutChunks.push(chunk);
20071
+ armIdleTimer();
19772
20072
  if (tap && decoder)
19773
20073
  tap.push(decoder.write(chunk));
19774
20074
  });
19775
- child.stdin.write(prompt);
19776
- child.stdin.end();
19777
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
19778
- const timer = setTimeout(() => {
19779
- child.kill("SIGTERM");
19780
- settle(() => reject(new Error(`Claude Code timed out after ${timeout}ms`)));
19781
- }, timeout);
19782
20075
  function onAbort() {
19783
20076
  child.kill("SIGTERM");
19784
20077
  settle(() => reject(new Error("Dispatch aborted")));
@@ -19850,7 +20143,7 @@ var require_claude_code = __commonJS({
19850
20143
  });
19851
20144
  }
19852
20145
  };
19853
- function createClaudeCodeExecutor(config, capabilityId = "claude-code", deps = {}) {
20146
+ function createClaudeCodeExecutor(config, capabilityId = "claude-code") {
19854
20147
  const validated = { capabilityId };
19855
20148
  if (config.tmpDir !== void 0) {
19856
20149
  if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
@@ -19881,9 +20174,6 @@ var require_claude_code = __commonJS({
19881
20174
  }
19882
20175
  validated.timeout = config.timeout;
19883
20176
  }
19884
- if (config.apiKeyFrom !== void 0) {
19885
- validated.resolveCredentials = buildCredentialResolver(config, deps);
19886
- }
19887
20177
  if (config.overlayAllowedHosts !== void 0) {
19888
20178
  if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
19889
20179
  throw new Error('"overlayAllowedHosts" must be an array of strings');
@@ -19892,43 +20182,15 @@ var require_claude_code = __commonJS({
19892
20182
  }
19893
20183
  return new ClaudeCodeExecutor(validated);
19894
20184
  }
19895
- function buildChildEnv(credentials, configDir) {
19896
- if (!credentials && !configDir)
20185
+ function buildChildEnv(modelKey, configDir) {
20186
+ if (!modelKey && !configDir)
19897
20187
  return void 0;
19898
20188
  return {
19899
20189
  ...process.env,
19900
- ...credentials ? { ANTHROPIC_API_KEY: credentials.anthropicApiKey } : {},
20190
+ ...modelKey ? { ANTHROPIC_API_KEY: modelKey } : {},
19901
20191
  ...configDir ? { CLAUDE_CONFIG_DIR: configDir } : {}
19902
20192
  };
19903
20193
  }
19904
- function buildCredentialResolver(config, deps) {
19905
- if (config.apiKeyFrom !== "job-metadata") {
19906
- throw new Error(`Unsupported "apiKeyFrom" value: ${JSON.stringify(config.apiKeyFrom)}. Only "job-metadata" is supported.`);
19907
- }
19908
- const keepApiUrl = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepApiUrl", "KEEP_API_URL");
19909
- const clientId = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepClientId", "KEEP_CLIENT_ID");
19910
- const clientSecret = (0, env_ref_1.resolveConfigStringOrEnv)(config, "keepClientSecret", "KEEP_CLIENT_SECRET");
19911
- const stmFactory = deps.buildServiceTokenManager ?? ((cfg) => new keep_1.ServiceTokenManager(cfg));
19912
- const stm = stmFactory({ keepApiUrl, clientId, clientSecret });
19913
- const keepClientFactory = deps.buildKeepClient ?? ((cfg) => new keep_1.KeepClient(cfg));
19914
- return async ({ customerId, needGithubToken }) => {
19915
- const serviceToken = await stm.getToken();
19916
- if (!serviceToken) {
19917
- throw new Error("claude-code dynamic-key mode failed to exchange a Keep service token. Check KEEP_CLIENT_ID / KEEP_CLIENT_SECRET.");
19918
- }
19919
- const keep = keepClientFactory({ baseUrl: keepApiUrl, serviceToken });
19920
- const reveal = await keep.revealManagedSifterKey(customerId);
19921
- const credentials = { anthropicApiKey: reveal.apiKey };
19922
- if (needGithubToken) {
19923
- try {
19924
- const gh = await keep.getUserGithubToken(customerId);
19925
- credentials.githubToken = gh.access_token;
19926
- } catch {
19927
- }
19928
- }
19929
- return credentials;
19930
- };
19931
- }
19932
20194
  function parseStreamJson(stdout) {
19933
20195
  let finalText = null;
19934
20196
  const readPaths = [];
@@ -21173,12 +21435,14 @@ var require_shuttle = __commonJS({
21173
21435
  var decommission_1 = require_decommission();
21174
21436
  var loom_1 = require_dist3();
21175
21437
  var chain_1 = require_chain();
21438
+ var workspace_1 = require_workspace();
21439
+ var credentials_1 = require_credentials();
21176
21440
  var output_gates_1 = require_output_gates();
21177
21441
  var keep_1 = require_dist4();
21178
21442
  var apply_1 = require_apply();
21179
21443
  var version_check_1 = require_version_check();
21180
21444
  var logger_1 = require_logger();
21181
- var registry_1 = require_registry();
21445
+ var registry_1 = require_registry2();
21182
21446
  var claude_code_1 = require_claude_code();
21183
21447
  var http_api_1 = require_http_api();
21184
21448
  var anthropic_api_1 = require_anthropic_api();
@@ -21223,12 +21487,20 @@ var require_shuttle = __commonJS({
21223
21487
  brand;
21224
21488
  config;
21225
21489
  profile;
21490
+ credentialDeps;
21226
21491
  subscription = null;
21227
21492
  spendTracker = null;
21493
+ /**
21494
+ * The runner-owned credential broker (ADR 0027), built once from the
21495
+ * `credentials` config (the reveal service identity). Null in local mode
21496
+ * (no such config) → consumers reveal nothing and use ambient credentials.
21497
+ */
21498
+ broker = null;
21228
21499
  constructor(options) {
21229
21500
  this.brand = options.brand;
21230
21501
  this.config = options.config;
21231
21502
  this.profile = options.profile ?? apply_1.DEFAULT_PROFILE;
21503
+ this.credentialDeps = options.credentialDeps ?? {};
21232
21504
  }
21233
21505
  /** Exposed so the CLI's `caps set` can reload after a write. */
21234
21506
  get spendTrackerHandle() {
@@ -21242,6 +21514,7 @@ var require_shuttle = __commonJS({
21242
21514
  const logger = (0, logger_1.getLogger)();
21243
21515
  logger.info(`${this.brand.product.title} starting...`);
21244
21516
  const paths = (0, apply_1.resolvePaths)(this.brand, this.profile);
21517
+ this.broker = (0, credentials_1.createCredentialBroker)(this.config.credentials, this.credentialDeps);
21245
21518
  const enforceMinVersion = (response) => {
21246
21519
  (0, version_check_1.checkResponseMinVersion)({
21247
21520
  response,
@@ -21315,11 +21588,30 @@ var require_shuttle = __commonJS({
21315
21588
  }
21316
21589
  await observerChain.notify({ kind: "executor.dispatch.started", jobId, attendanceId, executor: executor.capability.id, strategy }, onObserverError);
21317
21590
  const dispatchStartedAt = Date.now();
21591
+ const environment = executor.prepareEnvironment ? await executor.prepareEnvironment(dispatch) : void 0;
21592
+ let preparedWorkspace = null;
21593
+ if (environment?.workspaceRequest) {
21594
+ try {
21595
+ preparedWorkspace = await (0, workspace_1.materializeWorkspace)(environment.workspaceRequest, {
21596
+ ...environment.materializeOptions ?? {},
21597
+ credentials: this.broker,
21598
+ signal
21599
+ });
21600
+ } catch (err) {
21601
+ (0, logger_1.getLogger)().warn("workspace materialization failed; running prompt-only", {
21602
+ jobId,
21603
+ error: err instanceof Error ? err.message : String(err)
21604
+ });
21605
+ preparedWorkspace = null;
21606
+ }
21607
+ }
21318
21608
  const executionCtx = {
21319
21609
  reportProgress: (channel, frames) => {
21320
21610
  void jobsClient.postProgress(jobId, { channel, frames }).then(() => activity.record()).catch(() => {
21321
21611
  });
21322
- }
21612
+ },
21613
+ workspace: preparedWorkspace,
21614
+ credentials: this.broker
21323
21615
  };
21324
21616
  let response;
21325
21617
  try {
@@ -21328,6 +21620,9 @@ var require_shuttle = __commonJS({
21328
21620
  const reason = `executor failed: ${err instanceof Error ? err.message : String(err)}`;
21329
21621
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
21330
21622
  return { status: "failed", reason };
21623
+ } finally {
21624
+ if (preparedWorkspace)
21625
+ await preparedWorkspace.cleanup();
21331
21626
  }
21332
21627
  const gated = await runOutputGates(dispatch, response, executor, signal, executionCtx);
21333
21628
  if (!gated.ok) {
@@ -21911,7 +22206,7 @@ var require_dist5 = __commonJS({
21911
22206
  Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
21912
22207
  return keep_1.AgentCredentialRevokedError;
21913
22208
  } });
21914
- var registry_1 = require_registry();
22209
+ var registry_1 = require_registry2();
21915
22210
  Object.defineProperty(exports2, "ExecutorRegistry", { enumerable: true, get: function() {
21916
22211
  return registry_1.ExecutorRegistry;
21917
22212
  } });
@@ -22007,7 +22302,7 @@ var import_path = require("path");
22007
22302
  var import_promises = require("fs/promises");
22008
22303
  var import_yaml = __toESM(require_dist());
22009
22304
  var import_shuttle = __toESM(require_dist5());
22010
- var buildVersion = true ? "0.20.0" : pkg.version;
22305
+ var buildVersion = true ? "0.22.0" : pkg.version;
22011
22306
  var sifterBrand = {
22012
22307
  product: {
22013
22308
  id: "sifter",