@whittlelabs/sifter 0.20.0 → 0.21.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 +1297 -1011
  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}`);
@@ -18757,815 +19155,698 @@ var require_client2 = __commonJS({
18757
19155
  }
18758
19156
  });
18759
19157
 
18760
- // ../../packages/keep/dist/service-token.js
18761
- var require_service_token = __commonJS({
18762
- "../../packages/keep/dist/service-token.js"(exports2) {
18763
- "use strict";
18764
- Object.defineProperty(exports2, "__esModule", { value: true });
18765
- exports2.ServiceTokenManager = void 0;
18766
- var ServiceTokenManager = class {
18767
- token = null;
18768
- expiresAt = 0;
18769
- pendingExchange = null;
18770
- keepApiUrl;
18771
- clientId;
18772
- clientSecret;
18773
- enabled;
18774
- onResponse;
18775
- constructor(config) {
18776
- this.keepApiUrl = config.keepApiUrl.replace(/\/$/, "");
18777
- this.clientId = config.clientId;
18778
- this.clientSecret = config.clientSecret;
18779
- this.enabled = !!(config.clientId && config.clientSecret && config.keepApiUrl);
18780
- this.onResponse = config.onResponse;
18781
- if (!this.enabled) {
18782
- console.warn("[ServiceTokenManager] Keep credentials not configured (KEEP_CLIENT_ID, KEEP_CLIENT_SECRET, KEEP_API_URL). Service-to-service auth is disabled.");
18783
- }
18784
- }
18785
- /**
18786
- * Returns a valid service token, refreshing if needed.
18787
- * Returns null if Keep credentials are not configured (graceful degradation for local dev).
18788
- */
18789
- async getToken() {
18790
- if (!this.enabled)
18791
- return null;
18792
- if (this.token && Date.now() < this.expiresAt - 3e4) {
18793
- return this.token;
18794
- }
18795
- if (this.pendingExchange) {
18796
- return this.pendingExchange;
18797
- }
18798
- this.pendingExchange = this.exchange();
18799
- try {
18800
- return await this.pendingExchange;
18801
- } finally {
18802
- this.pendingExchange = null;
18803
- }
18804
- }
18805
- async exchange() {
18806
- const url = `${this.keepApiUrl}/api/service-tokens`;
18807
- const response = await fetch(url, {
18808
- method: "POST",
18809
- headers: { "Content-Type": "application/json" },
18810
- body: JSON.stringify({
18811
- clientId: this.clientId,
18812
- clientSecret: this.clientSecret
18813
- })
18814
- });
18815
- this.onResponse?.(response);
18816
- const json = await response.json();
18817
- if (!response.ok || !json.success) {
18818
- const msg = json.error?.message ?? `Token exchange failed with status ${response.status}`;
18819
- throw new Error(`[ServiceTokenManager] ${msg}`);
18820
- }
18821
- this.token = json.data.token;
18822
- this.expiresAt = Date.now() + json.data.expiresIn * 1e3;
18823
- return this.token;
18824
- }
18825
- };
18826
- exports2.ServiceTokenManager = ServiceTokenManager;
18827
- }
18828
- });
18829
-
18830
- // ../../packages/keep/dist/agent-token.js
18831
- var require_agent_token = __commonJS({
18832
- "../../packages/keep/dist/agent-token.js"(exports2) {
18833
- "use strict";
18834
- Object.defineProperty(exports2, "__esModule", { value: true });
18835
- exports2.AgentTokenManager = exports2.AgentCredentialRevokedError = void 0;
18836
- var AgentCredentialRevokedError2 = class extends Error {
18837
- code = "PAIRING_REVOKED";
18838
- constructor(message) {
18839
- super(message);
18840
- this.name = "AgentCredentialRevokedError";
18841
- }
18842
- };
18843
- exports2.AgentCredentialRevokedError = AgentCredentialRevokedError2;
18844
- var AgentTokenManager = class {
18845
- token = null;
18846
- expiresAt = 0;
18847
- pendingExchange = null;
18848
- keepApiUrl;
18849
- clientId;
18850
- clientSecret;
18851
- enabled;
18852
- onResponse;
18853
- constructor(config) {
18854
- this.keepApiUrl = config.keepApiUrl.replace(/\/$/, "");
18855
- this.clientId = config.clientId;
18856
- this.clientSecret = config.clientSecret;
18857
- this.enabled = !!(config.clientId && config.clientSecret && config.keepApiUrl);
18858
- this.onResponse = config.onResponse;
18859
- }
18860
- async getToken(forceRefresh = false) {
18861
- if (!this.enabled)
18862
- return null;
18863
- if (!forceRefresh && this.token && Date.now() < this.expiresAt - 3e4) {
18864
- return this.token;
18865
- }
18866
- if (this.pendingExchange) {
18867
- return this.pendingExchange;
18868
- }
18869
- this.pendingExchange = this.exchange();
18870
- try {
18871
- return await this.pendingExchange;
18872
- } finally {
18873
- this.pendingExchange = null;
18874
- }
18875
- }
18876
- async exchange() {
18877
- const url = `${this.keepApiUrl}/api/agent-tokens`;
18878
- const response = await fetch(url, {
18879
- method: "POST",
18880
- headers: { "Content-Type": "application/json" },
18881
- body: JSON.stringify({
18882
- clientId: this.clientId,
18883
- clientSecret: this.clientSecret
18884
- })
18885
- });
18886
- this.onResponse?.(response);
18887
- const json = await response.json();
18888
- if (!response.ok || !json.success) {
18889
- const msg = json.error?.message ?? `Agent token exchange failed with status ${response.status}`;
18890
- if (response.status === 401) {
18891
- throw new AgentCredentialRevokedError2(`[AgentTokenManager] Keep refused this agent's credentials (401): the pairing was revoked or the secret rotated. Re-pair to continue.`);
18892
- }
18893
- throw new Error(`[AgentTokenManager] ${msg}`);
18894
- }
18895
- this.token = json.data.token;
18896
- this.expiresAt = Date.now() + json.data.expiresIn * 1e3;
18897
- return this.token;
18898
- }
18899
- };
18900
- exports2.AgentTokenManager = AgentTokenManager;
18901
- }
18902
- });
18903
-
18904
- // ../../packages/keep/dist/index.js
18905
- var require_dist4 = __commonJS({
18906
- "../../packages/keep/dist/index.js"(exports2) {
18907
- "use strict";
18908
- Object.defineProperty(exports2, "__esModule", { value: true });
18909
- exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
18910
- var client_1 = require_client2();
18911
- Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
18912
- return client_1.KeepClient;
18913
- } });
18914
- var service_token_1 = require_service_token();
18915
- Object.defineProperty(exports2, "ServiceTokenManager", { enumerable: true, get: function() {
18916
- return service_token_1.ServiceTokenManager;
18917
- } });
18918
- var agent_token_1 = require_agent_token();
18919
- Object.defineProperty(exports2, "AgentTokenManager", { enumerable: true, get: function() {
18920
- return agent_token_1.AgentTokenManager;
18921
- } });
18922
- Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
18923
- return agent_token_1.AgentCredentialRevokedError;
18924
- } });
18925
- }
18926
- });
18927
-
18928
- // ../../packages/shuttle/dist/logging/logger.js
18929
- var require_logger = __commonJS({
18930
- "../../packages/shuttle/dist/logging/logger.js"(exports2) {
18931
- "use strict";
18932
- Object.defineProperty(exports2, "__esModule", { value: true });
18933
- exports2.createLogger = createLogger;
18934
- exports2.setGlobalLogger = setGlobalLogger;
18935
- exports2.getLogger = getLogger;
18936
- var LEVEL_ORDER = {
18937
- debug: 0,
18938
- info: 1,
18939
- warn: 2,
18940
- error: 3
18941
- };
18942
- function formatContext(context) {
18943
- return Object.entries(context).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
18944
- }
18945
- function formatPretty(level, message, context) {
18946
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
18947
- const tag = level.toUpperCase().padEnd(5);
18948
- const base = `[${timestamp}] ${tag} ${message}`;
18949
- if (context && Object.keys(context).length > 0) {
18950
- return `${base} ${formatContext(context)}`;
18951
- }
18952
- return base;
18953
- }
18954
- function formatJson(level, message, context) {
18955
- const entry = {
18956
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18957
- level,
18958
- message,
18959
- ...context
18960
- };
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) {
19158
+ // ../../packages/keep/dist/service-token.js
19159
+ var require_service_token = __commonJS({
19160
+ "../../packages/keep/dist/service-token.js"(exports2) {
18998
19161
  "use strict";
18999
19162
  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(", ")}`);
19163
+ exports2.ServiceTokenManager = void 0;
19164
+ var ServiceTokenManager = class {
19165
+ token = null;
19166
+ expiresAt = 0;
19167
+ pendingExchange = null;
19168
+ keepApiUrl;
19169
+ clientId;
19170
+ clientSecret;
19171
+ enabled;
19172
+ onResponse;
19173
+ constructor(config) {
19174
+ this.keepApiUrl = config.keepApiUrl.replace(/\/$/, "");
19175
+ this.clientId = config.clientId;
19176
+ this.clientSecret = config.clientSecret;
19177
+ this.enabled = !!(config.clientId && config.clientSecret && config.keepApiUrl);
19178
+ this.onResponse = config.onResponse;
19179
+ if (!this.enabled) {
19180
+ console.warn("[ServiceTokenManager] Keep credentials not configured (KEEP_CLIENT_ID, KEEP_CLIENT_SECRET, KEEP_API_URL). Service-to-service auth is disabled.");
19010
19181
  }
19011
- this.factories.set(name, factory);
19012
19182
  }
19013
- create(name, config) {
19014
- const factory = this.factories.get(name);
19015
- if (!factory) {
19016
- throw new Error(`Executor not registered: ${name}`);
19183
+ /**
19184
+ * Returns a valid service token, refreshing if needed.
19185
+ * Returns null if Keep credentials are not configured (graceful degradation for local dev).
19186
+ */
19187
+ async getToken() {
19188
+ if (!this.enabled)
19189
+ return null;
19190
+ if (this.token && Date.now() < this.expiresAt - 3e4) {
19191
+ return this.token;
19017
19192
  }
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));
19193
+ if (this.pendingExchange) {
19194
+ return this.pendingExchange;
19195
+ }
19196
+ this.pendingExchange = this.exchange();
19197
+ try {
19198
+ return await this.pendingExchange;
19199
+ } finally {
19200
+ this.pendingExchange = null;
19034
19201
  }
19035
- return out;
19036
19202
  }
19037
- isAllowed(name) {
19038
- return this.allowlist[0] === "*" || this.allowlist.includes(name);
19203
+ async exchange() {
19204
+ const url = `${this.keepApiUrl}/api/service-tokens`;
19205
+ const response = await fetch(url, {
19206
+ method: "POST",
19207
+ headers: { "Content-Type": "application/json" },
19208
+ body: JSON.stringify({
19209
+ clientId: this.clientId,
19210
+ clientSecret: this.clientSecret
19211
+ })
19212
+ });
19213
+ this.onResponse?.(response);
19214
+ const json = await response.json();
19215
+ if (!response.ok || !json.success) {
19216
+ const msg = json.error?.message ?? `Token exchange failed with status ${response.status}`;
19217
+ throw new Error(`[ServiceTokenManager] ${msg}`);
19218
+ }
19219
+ this.token = json.data.token;
19220
+ this.expiresAt = Date.now() + json.data.expiresIn * 1e3;
19221
+ return this.token;
19039
19222
  }
19040
19223
  };
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
- }
19224
+ exports2.ServiceTokenManager = ServiceTokenManager;
19058
19225
  }
19059
19226
  });
19060
19227
 
19061
- // ../../packages/shuttle/dist/executors/progress.js
19062
- var require_progress = __commonJS({
19063
- "../../packages/shuttle/dist/executors/progress.js"(exports2) {
19228
+ // ../../packages/keep/dist/agent-token.js
19229
+ var require_agent_token = __commonJS({
19230
+ "../../packages/keep/dist/agent-token.js"(exports2) {
19064
19231
  "use strict";
19065
19232
  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
- }
19233
+ exports2.AgentTokenManager = exports2.AgentCredentialRevokedError = void 0;
19234
+ var AgentCredentialRevokedError2 = class extends Error {
19235
+ code = "PAIRING_REVOKED";
19236
+ constructor(message) {
19237
+ super(message);
19238
+ this.name = "AgentCredentialRevokedError";
19142
19239
  }
19143
19240
  };
19144
- exports2.ProgressReporter = ProgressReporter;
19145
- var StreamJsonTap = class {
19146
- handlers;
19147
- buf = "";
19148
- constructor(handlers) {
19149
- this.handlers = handlers;
19241
+ exports2.AgentCredentialRevokedError = AgentCredentialRevokedError2;
19242
+ var AgentTokenManager = class {
19243
+ token = null;
19244
+ expiresAt = 0;
19245
+ pendingExchange = null;
19246
+ keepApiUrl;
19247
+ clientId;
19248
+ clientSecret;
19249
+ enabled;
19250
+ onResponse;
19251
+ constructor(config) {
19252
+ this.keepApiUrl = config.keepApiUrl.replace(/\/$/, "");
19253
+ this.clientId = config.clientId;
19254
+ this.clientSecret = config.clientSecret;
19255
+ this.enabled = !!(config.clientId && config.clientSecret && config.keepApiUrl);
19256
+ this.onResponse = config.onResponse;
19150
19257
  }
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);
19258
+ async getToken(forceRefresh = false) {
19259
+ if (!this.enabled)
19260
+ return null;
19261
+ if (!forceRefresh && this.token && Date.now() < this.expiresAt - 3e4) {
19262
+ return this.token;
19159
19263
  }
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;
19264
+ if (this.pendingExchange) {
19265
+ return this.pendingExchange;
19266
+ }
19267
+ this.pendingExchange = this.exchange();
19172
19268
  try {
19173
- event = JSON.parse(trimmed);
19174
- } catch {
19175
- return;
19269
+ return await this.pendingExchange;
19270
+ } finally {
19271
+ this.pendingExchange = null;
19176
19272
  }
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
- }
19273
+ }
19274
+ async exchange() {
19275
+ const url = `${this.keepApiUrl}/api/agent-tokens`;
19276
+ const response = await fetch(url, {
19277
+ method: "POST",
19278
+ headers: { "Content-Type": "application/json" },
19279
+ body: JSON.stringify({
19280
+ clientId: this.clientId,
19281
+ clientSecret: this.clientSecret
19282
+ })
19283
+ });
19284
+ this.onResponse?.(response);
19285
+ const json = await response.json();
19286
+ if (!response.ok || !json.success) {
19287
+ const msg = json.error?.message ?? `Agent token exchange failed with status ${response.status}`;
19288
+ if (response.status === 401) {
19289
+ throw new AgentCredentialRevokedError2(`[AgentTokenManager] Keep refused this agent's credentials (401): the pairing was revoked or the secret rotated. Re-pair to continue.`);
19190
19290
  }
19191
- } else if (event.type === "result") {
19192
- this.handlers.onResult?.();
19291
+ throw new Error(`[AgentTokenManager] ${msg}`);
19193
19292
  }
19293
+ this.token = json.data.token;
19294
+ this.expiresAt = Date.now() + json.data.expiresIn * 1e3;
19295
+ return this.token;
19194
19296
  }
19195
19297
  };
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;
19298
+ exports2.AgentTokenManager = AgentTokenManager;
19299
+ }
19300
+ });
19301
+
19302
+ // ../../packages/keep/dist/index.js
19303
+ var require_dist4 = __commonJS({
19304
+ "../../packages/keep/dist/index.js"(exports2) {
19305
+ "use strict";
19306
+ Object.defineProperty(exports2, "__esModule", { value: true });
19307
+ exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
19308
+ var client_1 = require_client2();
19309
+ Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
19310
+ return client_1.KeepClient;
19311
+ } });
19312
+ var service_token_1 = require_service_token();
19313
+ Object.defineProperty(exports2, "ServiceTokenManager", { enumerable: true, get: function() {
19314
+ return service_token_1.ServiceTokenManager;
19315
+ } });
19316
+ var agent_token_1 = require_agent_token();
19317
+ Object.defineProperty(exports2, "AgentTokenManager", { enumerable: true, get: function() {
19318
+ return agent_token_1.AgentTokenManager;
19319
+ } });
19320
+ Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
19321
+ return agent_token_1.AgentCredentialRevokedError;
19322
+ } });
19323
+ }
19324
+ });
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.`);
19204
19344
  }
19205
- return void 0;
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`);
19206
19360
  }
19207
19361
  }
19208
19362
  });
19209
19363
 
19210
- // ../../packages/shuttle/dist/executors/workspace.js
19211
- var require_workspace = __commonJS({
19212
- "../../packages/shuttle/dist/executors/workspace.js"(exports2) {
19364
+ // ../../packages/shuttle/dist/credentials/registry.js
19365
+ var require_registry = __commonJS({
19366
+ "../../packages/shuttle/dist/credentials/registry.js"(exports2) {
19213
19367
  "use strict";
19214
- var __importDefault = exports2 && exports2.__importDefault || function(mod) {
19215
- return mod && mod.__esModule ? mod : { "default": mod };
19216
- };
19217
19368
  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);
19231
- }
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`);
19264
- }
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;
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) => {
19294
19383
  try {
19295
- const url = new URL(remote);
19296
- if (url.protocol !== "https:")
19297
- return void 0;
19298
- origin = `${url.protocol}//${url.host}/`;
19384
+ const reveal = await keep.getUserGithubToken(principal);
19385
+ return reveal.access_token;
19299
19386
  } catch {
19300
- return void 0;
19387
+ return null;
19301
19388
  }
19302
- const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
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));
19303
19413
  return {
19304
- GIT_CONFIG_COUNT: "1",
19305
- GIT_CONFIG_KEY_0: `http.${origin}.extraheader`,
19306
- GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`
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
+ }
19307
19425
  };
19308
19426
  }
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
- };
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
+ }
19318
19472
  }
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" };
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;
19321
19482
  }
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" };
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
+ ];
19324
19546
  }
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" };
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;
19328
19555
  }
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)}` };
19556
+ for (const message of primitive(gate.params, output, inputs)) {
19557
+ findings.push({ gate: gate.name, message });
19332
19558
  }
19333
19559
  }
19334
- return { git };
19560
+ return { ok: findings.length === 0, findings };
19335
19561
  }
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
- }
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
+
19572
+ // ../../packages/shuttle/dist/logging/logger.js
19573
+ var require_logger = __commonJS({
19574
+ "../../packages/shuttle/dist/logging/logger.js"(exports2) {
19575
+ "use strict";
19576
+ Object.defineProperty(exports2, "__esModule", { value: true });
19577
+ exports2.createLogger = createLogger;
19578
+ exports2.setGlobalLogger = setGlobalLogger;
19579
+ exports2.getLogger = getLogger;
19580
+ var LEVEL_ORDER = {
19581
+ debug: 0,
19582
+ info: 1,
19583
+ warn: 2,
19584
+ error: 3
19585
+ };
19586
+ function formatContext(context) {
19587
+ return Object.entries(context).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
19588
+ }
19589
+ function formatPretty(level, message, context) {
19590
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19591
+ const tag = level.toUpperCase().padEnd(5);
19592
+ const base = `[${timestamp}] ${tag} ${message}`;
19593
+ if (context && Object.keys(context).length > 0) {
19594
+ return `${base} ${formatContext(context)}`;
19595
+ }
19596
+ return base;
19597
+ }
19598
+ function formatJson(level, message, context) {
19599
+ const entry = {
19600
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19601
+ level,
19602
+ message,
19603
+ ...context
19346
19604
  };
19605
+ return JSON.stringify(entry);
19347
19606
  }
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
19366
- }
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();
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)
19403
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);
19404
19620
  }
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
- });
19621
+ }
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
+ };
19415
19628
  }
19416
- function firstLine(text) {
19417
- const line = text.trim().split("\n", 1)[0];
19418
- return line.length > 0 ? line : "(no output)";
19629
+ var globalLogger = createLogger({ level: "info", format: "pretty" });
19630
+ function setGlobalLogger(logger) {
19631
+ globalLogger = logger;
19632
+ }
19633
+ function getLogger() {
19634
+ return globalLogger;
19419
19635
  }
19420
19636
  }
19421
19637
  });
19422
19638
 
19423
- // ../../packages/shuttle/dist/executors/overlay.js
19424
- var require_overlay = __commonJS({
19425
- "../../packages/shuttle/dist/executors/overlay.js"(exports2) {
19639
+ // ../../packages/shuttle/dist/executors/registry.js
19640
+ var require_registry2 = __commonJS({
19641
+ "../../packages/shuttle/dist/executors/registry.js"(exports2) {
19426
19642
  "use strict";
19427
- var __importDefault = exports2 && exports2.__importDefault || function(mod) {
19428
- return mod && mod.__esModule ? mod : { "default": mod };
19429
- };
19430
19643
  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;
19644
+ exports2.ExecutorRegistry = void 0;
19645
+ var ExecutorRegistry = class {
19646
+ factories = /* @__PURE__ */ new Map();
19647
+ allowlist;
19648
+ constructor(allowlist = ["*"]) {
19649
+ this.allowlist = allowlist;
19650
+ }
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(", ")}`);
19469
19654
  }
19470
- 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));
19655
+ this.factories.set(name, factory);
19656
+ }
19657
+ create(name, config) {
19658
+ const factory = this.factories.get(name);
19659
+ if (!factory) {
19660
+ throw new Error(`Executor not registered: ${name}`);
19478
19661
  }
19662
+ return factory(config, name);
19479
19663
  }
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;
19664
+ has(name) {
19665
+ return this.factories.has(name);
19499
19666
  }
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;
19516
- }
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);
19667
+ get allowedExecutors() {
19668
+ return this.allowlist;
19669
+ }
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.`);
19676
+ }
19677
+ out.push(this.create(name, cfg));
19525
19678
  }
19526
- return Buffer.concat(chunks);
19527
- } finally {
19528
- clearTimeout(timer);
19529
- opts.signal?.removeEventListener("abort", onParentAbort);
19679
+ return out;
19530
19680
  }
19531
- }
19681
+ isAllowed(name) {
19682
+ return this.allowlist[0] === "*" || this.allowlist.includes(name);
19683
+ }
19684
+ };
19685
+ exports2.ExecutorRegistry = ExecutorRegistry;
19532
19686
  }
19533
19687
  });
19534
19688
 
19535
- // ../../packages/shuttle/dist/config/env-ref.js
19536
- var require_env_ref = __commonJS({
19537
- "../../packages/shuttle/dist/config/env-ref.js"(exports2) {
19689
+ // ../../packages/shuttle/dist/executors/validate-output.js
19690
+ var require_validate_output = __commonJS({
19691
+ "../../packages/shuttle/dist/executors/validate-output.js"(exports2) {
19538
19692
  "use strict";
19539
19693
  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.`);
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("; ")}`);
19553
19700
  }
19554
- return resolved;
19555
- }
19556
- function isEnvRef(value) {
19557
- return ENV_REF_PATTERN.test(value);
19558
19701
  }
19559
- function resolveConfigStringOrEnv(config, field, envFallback) {
19560
- const raw = config[field];
19561
- if (typeof raw === "string" && raw.length > 0) {
19562
- return resolveEnvRef(raw, field);
19702
+ }
19703
+ });
19704
+
19705
+ // ../../packages/shuttle/dist/executors/progress.js
19706
+ var require_progress = __commonJS({
19707
+ "../../packages/shuttle/dist/executors/progress.js"(exports2) {
19708
+ "use strict";
19709
+ Object.defineProperty(exports2, "__esModule", { value: true });
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;
19563
19724
  }
19564
- const fromEnv = process.env[envFallback];
19565
- if (fromEnv && fromEnv.length > 0) {
19566
- return fromEnv;
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();
19733
+ }
19567
19734
  }
19568
- throw new Error(`dynamic-key mode requires "${field}" in config or the ${envFallback} env var`);
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 = [];
19752
+ try {
19753
+ this.sink(batch);
19754
+ } catch {
19755
+ }
19756
+ }
19757
+ /** Final flush; no further frames are emitted after this. */
19758
+ close() {
19759
+ this.flush();
19760
+ this.closed = true;
19761
+ this.clearTimer();
19762
+ }
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;
19785
+ }
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);
19803
+ }
19804
+ }
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 = "";
19810
+ }
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
+ }
19838
+ }
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;
19848
+ }
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
  });
@@ -19850,7 +20134,7 @@ var require_claude_code = __commonJS({
19850
20134
  });
19851
20135
  }
19852
20136
  };
19853
- function createClaudeCodeExecutor(config, capabilityId = "claude-code", deps = {}) {
20137
+ function createClaudeCodeExecutor(config, capabilityId = "claude-code") {
19854
20138
  const validated = { capabilityId };
19855
20139
  if (config.tmpDir !== void 0) {
19856
20140
  if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
@@ -19881,9 +20165,6 @@ var require_claude_code = __commonJS({
19881
20165
  }
19882
20166
  validated.timeout = config.timeout;
19883
20167
  }
19884
- if (config.apiKeyFrom !== void 0) {
19885
- validated.resolveCredentials = buildCredentialResolver(config, deps);
19886
- }
19887
20168
  if (config.overlayAllowedHosts !== void 0) {
19888
20169
  if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
19889
20170
  throw new Error('"overlayAllowedHosts" must be an array of strings');
@@ -19892,43 +20173,15 @@ var require_claude_code = __commonJS({
19892
20173
  }
19893
20174
  return new ClaudeCodeExecutor(validated);
19894
20175
  }
19895
- function buildChildEnv(credentials, configDir) {
19896
- if (!credentials && !configDir)
20176
+ function buildChildEnv(modelKey, configDir) {
20177
+ if (!modelKey && !configDir)
19897
20178
  return void 0;
19898
20179
  return {
19899
20180
  ...process.env,
19900
- ...credentials ? { ANTHROPIC_API_KEY: credentials.anthropicApiKey } : {},
20181
+ ...modelKey ? { ANTHROPIC_API_KEY: modelKey } : {},
19901
20182
  ...configDir ? { CLAUDE_CONFIG_DIR: configDir } : {}
19902
20183
  };
19903
20184
  }
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
20185
  function parseStreamJson(stdout) {
19933
20186
  let finalText = null;
19934
20187
  const readPaths = [];
@@ -21173,12 +21426,14 @@ var require_shuttle = __commonJS({
21173
21426
  var decommission_1 = require_decommission();
21174
21427
  var loom_1 = require_dist3();
21175
21428
  var chain_1 = require_chain();
21429
+ var workspace_1 = require_workspace();
21430
+ var credentials_1 = require_credentials();
21176
21431
  var output_gates_1 = require_output_gates();
21177
21432
  var keep_1 = require_dist4();
21178
21433
  var apply_1 = require_apply();
21179
21434
  var version_check_1 = require_version_check();
21180
21435
  var logger_1 = require_logger();
21181
- var registry_1 = require_registry();
21436
+ var registry_1 = require_registry2();
21182
21437
  var claude_code_1 = require_claude_code();
21183
21438
  var http_api_1 = require_http_api();
21184
21439
  var anthropic_api_1 = require_anthropic_api();
@@ -21223,12 +21478,20 @@ var require_shuttle = __commonJS({
21223
21478
  brand;
21224
21479
  config;
21225
21480
  profile;
21481
+ credentialDeps;
21226
21482
  subscription = null;
21227
21483
  spendTracker = null;
21484
+ /**
21485
+ * The runner-owned credential broker (ADR 0027), built once from the
21486
+ * `credentials` config (the reveal service identity). Null in local mode
21487
+ * (no such config) → consumers reveal nothing and use ambient credentials.
21488
+ */
21489
+ broker = null;
21228
21490
  constructor(options) {
21229
21491
  this.brand = options.brand;
21230
21492
  this.config = options.config;
21231
21493
  this.profile = options.profile ?? apply_1.DEFAULT_PROFILE;
21494
+ this.credentialDeps = options.credentialDeps ?? {};
21232
21495
  }
21233
21496
  /** Exposed so the CLI's `caps set` can reload after a write. */
21234
21497
  get spendTrackerHandle() {
@@ -21242,6 +21505,7 @@ var require_shuttle = __commonJS({
21242
21505
  const logger = (0, logger_1.getLogger)();
21243
21506
  logger.info(`${this.brand.product.title} starting...`);
21244
21507
  const paths = (0, apply_1.resolvePaths)(this.brand, this.profile);
21508
+ this.broker = (0, credentials_1.createCredentialBroker)(this.config.credentials, this.credentialDeps);
21245
21509
  const enforceMinVersion = (response) => {
21246
21510
  (0, version_check_1.checkResponseMinVersion)({
21247
21511
  response,
@@ -21315,11 +21579,30 @@ var require_shuttle = __commonJS({
21315
21579
  }
21316
21580
  await observerChain.notify({ kind: "executor.dispatch.started", jobId, attendanceId, executor: executor.capability.id, strategy }, onObserverError);
21317
21581
  const dispatchStartedAt = Date.now();
21582
+ const environment = executor.prepareEnvironment ? await executor.prepareEnvironment(dispatch) : void 0;
21583
+ let preparedWorkspace = null;
21584
+ if (environment?.workspaceRequest) {
21585
+ try {
21586
+ preparedWorkspace = await (0, workspace_1.materializeWorkspace)(environment.workspaceRequest, {
21587
+ ...environment.materializeOptions ?? {},
21588
+ credentials: this.broker,
21589
+ signal
21590
+ });
21591
+ } catch (err) {
21592
+ (0, logger_1.getLogger)().warn("workspace materialization failed; running prompt-only", {
21593
+ jobId,
21594
+ error: err instanceof Error ? err.message : String(err)
21595
+ });
21596
+ preparedWorkspace = null;
21597
+ }
21598
+ }
21318
21599
  const executionCtx = {
21319
21600
  reportProgress: (channel, frames) => {
21320
21601
  void jobsClient.postProgress(jobId, { channel, frames }).then(() => activity.record()).catch(() => {
21321
21602
  });
21322
- }
21603
+ },
21604
+ workspace: preparedWorkspace,
21605
+ credentials: this.broker
21323
21606
  };
21324
21607
  let response;
21325
21608
  try {
@@ -21328,6 +21611,9 @@ var require_shuttle = __commonJS({
21328
21611
  const reason = `executor failed: ${err instanceof Error ? err.message : String(err)}`;
21329
21612
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
21330
21613
  return { status: "failed", reason };
21614
+ } finally {
21615
+ if (preparedWorkspace)
21616
+ await preparedWorkspace.cleanup();
21331
21617
  }
21332
21618
  const gated = await runOutputGates(dispatch, response, executor, signal, executionCtx);
21333
21619
  if (!gated.ok) {
@@ -21911,7 +22197,7 @@ var require_dist5 = __commonJS({
21911
22197
  Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
21912
22198
  return keep_1.AgentCredentialRevokedError;
21913
22199
  } });
21914
- var registry_1 = require_registry();
22200
+ var registry_1 = require_registry2();
21915
22201
  Object.defineProperty(exports2, "ExecutorRegistry", { enumerable: true, get: function() {
21916
22202
  return registry_1.ExecutorRegistry;
21917
22203
  } });
@@ -22007,7 +22293,7 @@ var import_path = require("path");
22007
22293
  var import_promises = require("fs/promises");
22008
22294
  var import_yaml = __toESM(require_dist());
22009
22295
  var import_shuttle = __toESM(require_dist5());
22010
- var buildVersion = true ? "0.20.0" : pkg.version;
22296
+ var buildVersion = true ? "0.21.0" : pkg.version;
22011
22297
  var sifterBrand = {
22012
22298
  product: {
22013
22299
  id: "sifter",