@sentry/junior 0.263.0 → 0.265.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 (67) hide show
  1. package/README.md +1 -1
  2. package/dist/{adapter-BIacI0Bt.js → adapter-cUkPH3Om.js} +1 -1
  3. package/dist/{admin-C7vUk7s9.js → admin-Cf0rDmWV.js} +1 -1
  4. package/dist/{agent-runner-BUBas7Yy.js → agent-runner-C6MwHJEs.js} +296 -238
  5. package/dist/api/schema.d.ts +213 -16
  6. package/dist/api/schema.js +3 -3
  7. package/dist/api.js +107 -40
  8. package/dist/{app-Bo0kB987.d.ts → app-Csbi9dW7.d.ts} +4 -3
  9. package/dist/app.d.ts +1 -1
  10. package/dist/app.js +101 -393
  11. package/dist/{assistant-reply-BFmjgJjD.js → assistant-reply-UPA8vKlC.js} +1 -1
  12. package/dist/{automation-CWX0qL8U.js → automation-2RbKzpMT.js} +30 -9
  13. package/dist/{briefs-h55v5lX9.js → briefs-DRjx8kQ9.js} +3 -3
  14. package/dist/{catalog-runtime-Suf-aLlh.js → catalog-runtime-D3qbJlYz.js} +1 -1
  15. package/dist/cli/chat.js +15 -15
  16. package/dist/cli/check.js +3 -3
  17. package/dist/cli/init.js +4 -18
  18. package/dist/cli/main.js +4 -4
  19. package/dist/cli/plugins.js +4 -4
  20. package/dist/cli/snapshot-create.js +4 -4
  21. package/dist/cli/upgrade.js +3 -3
  22. package/dist/{client-DZBCBj1M.js → client-DX34VNtH.js} +3 -8
  23. package/dist/{config-Dnv8KmNs.js → config-BayQ9hHH.js} +100 -3
  24. package/dist/{connections-Br0_xv27.js → connections-mpN5Lsnj.js} +11 -10
  25. package/dist/{conversation-privacy-FY8yc2q3.js → conversation-privacy-frjQCJa9.js} +1 -1
  26. package/dist/{credential-sync-BvkiyWF5.js → credential-sync-CZmyDNdR.js} +20 -20
  27. package/dist/{agent-hooks-CqnZfuCt.js → credentialed-C6pKlB1p.js} +3197 -66
  28. package/dist/{executor-J9k2sp9m.js → executor-CAr7xQr2.js} +10 -300
  29. package/dist/{images-DV2FlEGL.js → images-B2yA3HWE.js} +2 -2
  30. package/dist/instrumentation.js +41 -25
  31. package/dist/{job-queue-B4yTgklN.js → job-queue-Cjo6Yg_b.js} +53 -14
  32. package/dist/{logging-Dk_FpF36.js → logging-D2QBriUo.js} +19 -32
  33. package/dist/{message-BzseAFH1.js → message-C9ymnpBS.js} +4 -4
  34. package/dist/nitro.d.ts +17 -2
  35. package/dist/nitro.js +26 -27
  36. package/dist/{oauth-callback-server-bTspTMCC.js → oauth-callback-server-BtMf3TPZ.js} +2 -2
  37. package/dist/{prompt-CxatQVaj.js → prompt-CvaDdSX3.js} +1 -1
  38. package/dist/{registry-BFzQFV_y.js → registry-COTeZ2cV.js} +1 -1
  39. package/dist/{resolve-CnRHib0U.js → resolve-g9PKz9pH.js} +7 -106
  40. package/dist/{result-DczfmNBa.js → result-COnC42GR.js} +8 -8
  41. package/dist/{runner-CJd9Wec-.js → runner-BnTNS7x_.js} +10 -10
  42. package/dist/{sandbox-egress-signals-DUmVkOx4.js → sandbox-egress-signals-C_IIdLFk.js} +1 -1
  43. package/dist/{schema-QidbykYl.js → schema-D5Amlzkp.js} +44 -3
  44. package/dist/{sentry-BBUlHih9.js → sentry-MDkYAVPV.js} +3 -6
  45. package/dist/{service-account-BI2kQDrb.js → service-account-CwvToidT.js} +3 -3
  46. package/dist/{service-account-x2QZXm4X.js → service-account-YdYftwV2.js} +5 -5
  47. package/dist/{session-stop-CtyJsnEj.js → session-stop-CpCUy1ZJ.js} +121 -81
  48. package/dist/{skills-Dbzx9-rS.js → skills-Bb2KaJ3m.js} +2 -2
  49. package/dist/{spawn-D8WU99UV.js → spawn-mbTcrLb3.js} +2 -2
  50. package/dist/{store-TKg1HVKx.js → store-CqRnaZSq.js} +3 -3
  51. package/dist/{store-UlOrW6JC.js → store-DdT74Lvq.js} +18 -9
  52. package/dist/{task-queue-BRB49H-J.js → task-queue-8XjDIORF.js} +14 -19
  53. package/dist/{task-runner-C575f2Dd.js → task-runner-6mLmG6g_.js} +46 -28
  54. package/dist/{tasks-LOItiYgL.js → tasks-DGbrur6x.js} +14 -2075
  55. package/dist/{thread-state-IDHdZA7R.js → thread-state-D45lz-oC.js} +5 -5
  56. package/dist/{turn-failure-response-h_LSXJ-M.js → turn-failure-response-CaEPLSBn.js} +7 -7
  57. package/dist/{turn-lifecycle-Cvi6Sb8Y.js → turn-lifecycle-C35IyRGp.js} +1 -1
  58. package/dist/{turn-session-routing-5Nyv4fso.js → turn-session-routing-BKx6rfVd.js} +1 -1
  59. package/dist/{validation-BeTriWC2.js → validation-C0X3CX51.js} +1 -1
  60. package/dist/{version-BQX_hLDC.js → version-BPHgJwnm.js} +1 -1
  61. package/dist/version.js +1 -1
  62. package/dist/{viewer-DmNRM88V.js → viewer-B2wXNspU.js} +21 -5
  63. package/dist/{web-input-BHcGXSox.js → web-input-Y6aXvmfa.js} +145 -46
  64. package/dist/{work-DLJMV02b.js → work-DMbqK0kT.js} +10 -10
  65. package/dist/{worker-vfG2ESg1.js → worker-B-rDJ3XN.js} +8 -6
  66. package/package.json +10 -9
  67. package/dist/profile-Dq5I7TV1.js +0 -582
@@ -1,29 +1,30 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
2
- import { i as completeObject, o as embedTexts } from "./client-DZBCBj1M.js";
3
- import { T as withSpan, a as extractGenAiUsageSummary, b as setSpanStatus, f as logException, m as logWarn, p as logInfo, y as setSpanAttributes, z as isRecord$1 } from "./logging-Dk_FpF36.js";
4
- import { a as getClient$1 } from "./sentry-BBUlHih9.js";
5
- import { t as canExposeConversationPayload } from "./conversation-privacy-FY8yc2q3.js";
6
- import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-Dnv8KmNs.js";
2
+ import { C as toOptionalTrimmed, i as completeObject, o as embedTexts } from "./client-DX34VNtH.js";
3
+ import { i as getChatConfig, n as botConfig, o as getSlackBotToken, v as normalizeSlackEmojiName } from "./config-BayQ9hHH.js";
4
+ import { T as withSpan, a as extractGenAiUsageSummary, f as logException, m as logWarn, p as logInfo, s as getLogContextAttributes, u as logAudit, v as setSentryUser, y as setSpanAttributes, z as isRecord$2 } from "./logging-D2QBriUo.js";
5
+ import { a as getClient$1, n as captureMessage, r as continueTrace } from "./sentry-MDkYAVPV.js";
6
+ import { t as canExposeConversationPayload } from "./conversation-privacy-frjQCJa9.js";
7
7
  import { r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
8
- import { d as parseSlackTeamId, g as parseSlackMessageTs, i as isActorUserId, o as parseActorUserId, u as parseSlackChannelReferenceId } from "./actor-tlxzqaRe.js";
9
- import { D as hasAgentTurnUsage, M as readUserTimezone, P as saveUserTimezone, c as getConversationEventStore, d as getDb, f as getSqlExecutor, g as createSqlStore, i as resolveViewerUser, r as readActorIdentity } from "./viewer-DmNRM88V.js";
8
+ import { d as parseSlackTeamId, f as parseSlackUserId, g as parseSlackMessageTs, i as isActorUserId, o as parseActorUserId, u as parseSlackChannelReferenceId } from "./actor-tlxzqaRe.js";
9
+ import { F as saveUserTimezone, N as readUserTimezone, O as hasAgentTurnUsage, _ as createSqlStore, a as resolveViewerUser, f as getDb, i as readIdentityById, l as getConversationEventStore, p as getSqlExecutor, r as readActorIdentity, z as parseDestination } from "./viewer-B2wXNspU.js";
10
10
  import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
11
- import { _ as juniorConversationAnnotations, b as juniorAttachments, f as juniorConversationBriefs, h as juniorConversationEvents, i as juniorWorkspaces, n as juniorSnapshots, r as juniorWorkspaceRepos, u as juniorConversationParticipants, v as juniorCodeChanges, y as juniorCodeRepositories } from "./executor-J9k2sp9m.js";
11
+ import { _ as juniorConversationAnnotations, b as juniorAttachments, f as juniorConversationBriefs, h as juniorConversationEvents, i as juniorWorkspaces, n as juniorSnapshots, r as juniorWorkspaceRepos, u as juniorConversationParticipants, v as juniorCodeChanges, y as juniorCodeRepositories } from "./executor-CAr7xQr2.js";
12
12
  import { o as decodeStoredConversationEvent } from "./history-AY8rs-FR.js";
13
- import { h as credentialSubjectSchema } from "./registry-BFzQFV_y.js";
14
- import { n as getDefaultRedisStateAdapterFor, r as getStateAdapter, s as withLock } from "./adapter-BIacI0Bt.js";
13
+ import { _ as CredentialUnavailableError, a as resolveAuthTokenPlaceholder, h as credentialSubjectSchema, m as credentialLogContext, o as resolvePluginCommandEnv, p as credentialContextSchema } from "./registry-COTeZ2cV.js";
14
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-D3qbJlYz.js";
15
+ import { a as acquireActiveLock, n as getDefaultRedisStateAdapterFor, r as getStateAdapter, s as withLock } from "./adapter-cUkPH3Om.js";
15
16
  import { i as isValidTimeZone } from "./cadence-R7Em4RjE.js";
16
17
  import { B as readMessageAttachments, C as conversationReportEventSchema, F as readMessageCardRefs, J as parseBriefInput, q as conversationBriefSchema, w as conversationReportMessageEventSchema } from "./conversation-B0baEwoA.js";
17
- import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-CxatQVaj.js";
18
- import { D as SANDBOX_WORKSPACE_ROOT, S as runNonInteractiveCommand, l as isSandboxMissingError, n as hash, v as getVercelSandboxCredentials, y as sleep$1 } from "./profile-Dq5I7TV1.js";
18
+ import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-CvaDdSX3.js";
19
19
  import { AsyncLocalStorage } from "node:async_hooks";
20
20
  import { z } from "zod";
21
- import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
21
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
22
22
  import { and, arrayContains, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, max, ne, notExists, or, sql } from "drizzle-orm";
23
- import { codeChangeInputSchema, conversationAnnotationInputSchema, conversationEventPresentationSchema, defineConversationEvent, eventInputSchema, eventMatchSchema, eventMatches, eventTypeSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectAnnotationSchema, objectIconPath, objectTypeSchema, pluginEventsSchema, promptContextSchema, promptMessageSchema, stableEventMatchKey } from "@sentry/junior-plugin-api";
23
+ import { EgressAuthRequired, EgressPolicyDenied, actorSchema, codeChangeInputSchema, conversationAnnotationInputSchema, conversationEventPresentationSchema, defineConversationEvent, eventInputSchema, eventMatchSchema, eventMatches, eventTypeSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectAnnotationSchema, objectIconPath, objectTypeSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginEventsSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, promptContextSchema, promptMessageSchema, sourceSchema, stableEventMatchKey } from "@sentry/junior-plugin-api";
24
24
  import { alias, union } from "drizzle-orm/pg-core";
25
25
  import { WebClient } from "@slack/web-api";
26
- import { Sandbox } from "@vercel/sandbox";
26
+ import { FileSystem, Sandbox } from "@vercel/sandbox";
27
+ import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
27
28
  // src/chat/conversations/structured-events.ts
28
29
  /** Reserved namespace for Junior-owned conversation metadata events. */
29
30
  const JUNIOR_NATIVE_EVENT_NAMESPACE = "junior";
@@ -394,8 +395,8 @@ async function saveObjectAnnotations(conversationId, plugin, value) {
394
395
  }
395
396
  /** Save annotations from a successful plugin tool result and attach their cards. */
396
397
  async function annotateToolResult(conversationId, plugin, result) {
397
- if (!isRecord$1(result) || result.isError === true) return result;
398
- const details = isRecord$1(result.details) ? result.details : result;
398
+ if (!isRecord$2(result) || result.isError === true) return result;
399
+ const details = isRecord$2(result.details) ? result.details : result;
399
400
  if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
400
401
  const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
401
402
  const { objectAnnotations: _annotations, ...rest } = details;
@@ -403,7 +404,7 @@ async function annotateToolResult(conversationId, plugin, result) {
403
404
  ...rest,
404
405
  objectCards: cards
405
406
  };
406
- if (isRecord$1(result.details)) return {
407
+ if (isRecord$2(result.details)) return {
407
408
  ...result,
408
409
  details: annotated
409
410
  };
@@ -1832,6 +1833,100 @@ function createPluginState(plugin, adapter) {
1832
1833
  }
1833
1834
  };
1834
1835
  }
1836
+ // src/chat/sandbox/paths.ts
1837
+ function normalizeWorkspaceRoot(input) {
1838
+ const candidate = (input ?? "").trim();
1839
+ if (!candidate) return "/vercel/sandbox";
1840
+ const normalized = candidate.replace(/\/+$/, "");
1841
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
1842
+ }
1843
+ const SANDBOX_WORKSPACE_ROOT = normalizeWorkspaceRoot(process.env.VERCEL_SANDBOX_WORKSPACE_DIR);
1844
+ const SANDBOX_REPOS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/repos`;
1845
+ const SANDBOX_SKILLS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/skills`;
1846
+ const SANDBOX_DATA_ROOT = `${SANDBOX_WORKSPACE_ROOT}/data`;
1847
+ /**
1848
+ * PATH for sandbox commands. Plugin runtime binaries come last so they never
1849
+ * shadow repo or Workspace tools with the same name. Repo tools often start
1850
+ * child processes through PATH, so only PATH order can protect them. Plugin
1851
+ * binaries stay reachable by name or by absolute path.
1852
+ */
1853
+ const SANDBOX_COMMAND_PATH = `$PATH:${`${SANDBOX_WORKSPACE_ROOT}/.junior/bin`}`;
1854
+ function sandboxSkillDir(skillName) {
1855
+ return `${SANDBOX_SKILLS_ROOT}/${skillName}`;
1856
+ }
1857
+ function sandboxSkillFile(skillName) {
1858
+ return `${sandboxSkillDir(skillName)}/SKILL.md`;
1859
+ }
1860
+ /** Explain how the agent should resolve files and commands owned by a skill. */
1861
+ function sandboxSkillPathResolution(skillName) {
1862
+ const skillDir = sandboxSkillDir(skillName);
1863
+ return `Resolve relative paths in this skill against ${skillDir}. For bash commands from this skill, cd to ${skillDir} first or use absolute paths.`;
1864
+ }
1865
+ // src/chat/sandbox/noninteractive-command.ts
1866
+ const NON_INTERACTIVE_ENV = {
1867
+ CI: "1",
1868
+ TERM: "dumb",
1869
+ NO_COLOR: "1",
1870
+ PAGER: "cat",
1871
+ GIT_PAGER: "cat",
1872
+ GH_PROMPT_DISABLED: "1",
1873
+ GH_NO_UPDATE_NOTIFIER: "1",
1874
+ GH_NO_EXTENSION_UPDATE_NOTIFIER: "1",
1875
+ GH_SPINNER_DISABLED: "1",
1876
+ GIT_TERMINAL_PROMPT: "0",
1877
+ GCM_INTERACTIVE: "never",
1878
+ DEBIAN_FRONTEND: "noninteractive",
1879
+ GIT_ASKPASS: "/bin/true",
1880
+ GIT_CONFIG_NOSYSTEM: "1",
1881
+ GIT_CONFIG_COUNT: "2",
1882
+ GIT_CONFIG_KEY_0: "credential.helper",
1883
+ GIT_CONFIG_VALUE_0: "",
1884
+ GIT_CONFIG_KEY_1: "http.emptyAuth",
1885
+ GIT_CONFIG_VALUE_1: "true"
1886
+ };
1887
+ function shellQuote(value) {
1888
+ return `'${value.replace(/'/g, "'\\''")}'`;
1889
+ }
1890
+ function buildEnvExports(options) {
1891
+ const lines = [];
1892
+ if (options.path) lines.push(`export PATH="${options.path}"`);
1893
+ for (const [key, value] of Object.entries(NON_INTERACTIVE_ENV)) lines.push(`export ${key}=${shellQuote(value)}`);
1894
+ for (const [key, value] of Object.entries(options.env ?? {})) lines.push(`export ${key}=${shellQuote(value)}`);
1895
+ return lines;
1896
+ }
1897
+ /** Render one argv command as a safely quoted shell script. */
1898
+ function buildCommandScript(input) {
1899
+ return [shellQuote(input.cmd), ...(input.args ?? []).map(shellQuote)].join(" ");
1900
+ }
1901
+ /** Build one shell entrypoint that never waits on terminal input. */
1902
+ function buildNonInteractiveShellScript(script, options = {}) {
1903
+ return [
1904
+ ...buildEnvExports(options),
1905
+ "exec </dev/null",
1906
+ script
1907
+ ].join(" && ");
1908
+ }
1909
+ /** Wrap argv-style commands so every sandbox subprocess runs in non-interactive mode. */
1910
+ function buildNonInteractiveCommand(input) {
1911
+ return {
1912
+ cmd: "bash",
1913
+ args: [input.login ? "-lc" : "-c", buildNonInteractiveShellScript(buildCommandScript(input), {
1914
+ env: input.env,
1915
+ path: input.path
1916
+ })]
1917
+ };
1918
+ }
1919
+ /** Run a subprocess through one enforced non-interactive entrypoint. */
1920
+ async function runNonInteractiveCommand(sandbox, input) {
1921
+ const command = {
1922
+ ...buildNonInteractiveCommand(input),
1923
+ ...input.cwd ? { cwd: input.cwd } : void 0,
1924
+ ...input.signal ? { signal: input.signal } : void 0,
1925
+ ...input.sudo !== void 0 ? { sudo: input.sudo } : void 0,
1926
+ ...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : void 0
1927
+ };
1928
+ return await sandbox.runCommand(command);
1929
+ }
1835
1930
  // src/chat/tools/execution/plugin-tool-denied-error.ts
1836
1931
  const PLUGIN_DENIAL_INSTRUCTIONS = [
1837
1932
  "This action was blocked by policy and was not executed.",
@@ -1988,6 +2083,26 @@ function runWithWorkspaceTeamId(teamId, fn) {
1988
2083
  function getWorkspaceTeamId() {
1989
2084
  return workspaceTeamIdStorage.getStore();
1990
2085
  }
2086
+ // src/chat/sleep.ts
2087
+ /** Wait for a duration, rejecting with the owning signal reason when cancelled. */
2088
+ function sleep$1(ms, signal) {
2089
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
2090
+ return new Promise((resolve, reject) => {
2091
+ if (signal.aborted) {
2092
+ reject(signal.reason);
2093
+ return;
2094
+ }
2095
+ const timeoutId = setTimeout(() => {
2096
+ signal.removeEventListener("abort", onAbort);
2097
+ resolve();
2098
+ }, ms);
2099
+ const onAbort = () => {
2100
+ clearTimeout(timeoutId);
2101
+ reject(signal.reason);
2102
+ };
2103
+ signal.addEventListener("abort", onAbort, { once: true });
2104
+ });
2105
+ }
1991
2106
  // src/chat/slack/client.ts
1992
2107
  var SlackActionError = class extends Error {
1993
2108
  code;
@@ -2209,7 +2324,6 @@ async function withSlackRetries(task, maxAttempts = 3, context = {}) {
2209
2324
  if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
2210
2325
  } else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
2211
2326
  setSpanAttributes(errorAttrs);
2212
- setSpanStatus("error");
2213
2327
  throw error;
2214
2328
  }
2215
2329
  }, {
@@ -2310,21 +2424,9 @@ async function getFilePermalink(fileId) {
2310
2424
  }
2311
2425
  async function downloadPrivateSlackFile(url) {
2312
2426
  const token = resolveSlackToken();
2313
- return withSpan("GET files.slack.com", "http.client", {}, async () => {
2314
- const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
2315
- setSpanAttributes({ "http.response.status_code": response.status });
2316
- if (!response.ok) {
2317
- setSpanAttributes({ "error.type": String(response.status) });
2318
- setSpanStatus("error");
2319
- throw new Error(`Slack file download failed: ${response.status}`);
2320
- }
2321
- return Buffer.from(await response.arrayBuffer());
2322
- }, {
2323
- "http.request.method": "GET",
2324
- "server.address": "files.slack.com",
2325
- "url.scheme": "https",
2326
- "app.slack.method": "files.download"
2327
- });
2427
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
2428
+ if (!response.ok) throw new Error(`Slack file download failed: ${response.status}`);
2429
+ return Buffer.from(await response.arrayBuffer());
2328
2430
  }
2329
2431
  // src/chat/credentials/subject.ts
2330
2432
  const CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
@@ -2336,11 +2438,11 @@ function getCredentialSubjectSecret() {
2336
2438
  function buildPayload(parts) {
2337
2439
  return [CREDENTIAL_SUBJECT_HMAC_CONTEXT, ...parts].join("\0");
2338
2440
  }
2339
- function signPayload(secret, payload) {
2441
+ function signPayload$1(secret, payload) {
2340
2442
  const digest = createHmac("sha256", secret).update(payload).digest("hex");
2341
2443
  return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
2342
2444
  }
2343
- function timingSafeMatch(expected, actual) {
2445
+ function timingSafeMatch$1(expected, actual) {
2344
2446
  const expectedBuffer = Buffer.from(expected);
2345
2447
  const actualBuffer = Buffer.from(actual);
2346
2448
  if (expectedBuffer.length !== actualBuffer.length) return false;
@@ -2374,7 +2476,7 @@ function bindSlackDirectCredentialSubject(input) {
2374
2476
  type: "slack-direct-conversation",
2375
2477
  teamId,
2376
2478
  channelId,
2377
- signature: signPayload(secret, buildPayload([
2479
+ signature: signPayload$1(secret, buildPayload([
2378
2480
  subject.allowedWhen,
2379
2481
  teamId,
2380
2482
  channelId,
@@ -2391,7 +2493,7 @@ function verifySlackDirectCredentialSubject(input) {
2391
2493
  const { subject } = input;
2392
2494
  const binding = subject.binding;
2393
2495
  if (subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "private-direct-conversation" || !binding || binding.type !== "slack-direct-conversation" || typeof binding.signature !== "string" || !binding.signature || binding.teamId !== input.teamId || binding.channelId !== channelId) return false;
2394
- return timingSafeMatch(signPayload(secret, buildPayload([
2496
+ return timingSafeMatch$1(signPayload$1(secret, buildPayload([
2395
2497
  subject.allowedWhen,
2396
2498
  binding.teamId,
2397
2499
  binding.channelId,
@@ -2415,7 +2517,7 @@ function bindScheduledAutomationCredentialSubject(input) {
2415
2517
  type: "scheduled-automation",
2416
2518
  plugin,
2417
2519
  taskId,
2418
- signature: signPayload(secret, buildPayload([
2520
+ signature: signPayload$1(secret, buildPayload([
2419
2521
  "scheduled-automation",
2420
2522
  plugin,
2421
2523
  taskId,
@@ -2430,7 +2532,7 @@ function verifyScheduledAutomationCredentialSubject(input) {
2430
2532
  const { subject } = input;
2431
2533
  const binding = subject.binding;
2432
2534
  if (!secret || input.plugin !== "scheduler" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "scheduled-automation" || !subject.taskId || binding.type !== "scheduled-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
2433
- return timingSafeMatch(signPayload(secret, buildPayload([
2535
+ return timingSafeMatch$1(signPayload$1(secret, buildPayload([
2434
2536
  "scheduled-automation",
2435
2537
  binding.plugin,
2436
2538
  binding.taskId,
@@ -2454,7 +2556,7 @@ function bindEventAutomationCredentialSubject(input) {
2454
2556
  type: "event-automation",
2455
2557
  plugin,
2456
2558
  taskId,
2457
- signature: signPayload(secret, buildPayload([
2559
+ signature: signPayload$1(secret, buildPayload([
2458
2560
  "event-automation",
2459
2561
  plugin,
2460
2562
  taskId,
@@ -2469,7 +2571,7 @@ function verifyEventAutomationCredentialSubject(input) {
2469
2571
  const { subject } = input;
2470
2572
  const binding = subject.binding;
2471
2573
  if (!secret || input.plugin !== "junior" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "event-automation" || !subject.taskId || binding.type !== "event-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
2472
- return timingSafeMatch(signPayload(secret, buildPayload([
2574
+ return timingSafeMatch$1(signPayload$1(secret, buildPayload([
2473
2575
  "event-automation",
2474
2576
  binding.plugin,
2475
2577
  binding.taskId,
@@ -2490,7 +2592,7 @@ function bindTimerWatchCredentialSubject(input) {
2490
2592
  type: "timer-watch",
2491
2593
  conversationId,
2492
2594
  watchId,
2493
- signature: signPayload(secret, buildPayload([
2595
+ signature: signPayload$1(secret, buildPayload([
2494
2596
  "timer-watch",
2495
2597
  conversationId,
2496
2598
  watchId,
@@ -2505,7 +2607,7 @@ function verifyTimerWatchCredentialSubject(input) {
2505
2607
  const { subject } = input;
2506
2608
  const binding = subject.binding;
2507
2609
  if (!secret || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "timer-watch" || binding.type !== "timer-watch" || binding.conversationId !== input.conversationId || binding.watchId !== input.watchId) return false;
2508
- return timingSafeMatch(signPayload(secret, buildPayload([
2610
+ return timingSafeMatch$1(signPayload$1(secret, buildPayload([
2509
2611
  "timer-watch",
2510
2612
  binding.conversationId,
2511
2613
  binding.watchId,
@@ -2970,36 +3072,2143 @@ function workspaceRepoCheckoutPath(repo) {
2970
3072
  if (!path) throw new Error(`Invalid repository name for checkout path: ${repo}`);
2971
3073
  return path;
2972
3074
  }
2973
- // src/chat/sandbox/snapshot/builder-sandbox.ts
3075
+ // src/chat/sandbox/vercel/credentials.ts
3076
+ /**
3077
+ * Resolve explicit Vercel Sandbox credentials, or return undefined to let
3078
+ * the SDK read ambient Vercel auth from its own environment detection.
3079
+ */
3080
+ function getVercelSandboxCredentials() {
3081
+ const token = toOptionalTrimmed(process.env.VERCEL_TOKEN);
3082
+ const teamId = toOptionalTrimmed(process.env.VERCEL_TEAM_ID);
3083
+ const projectId = toOptionalTrimmed(process.env.VERCEL_PROJECT_ID);
3084
+ if (token && teamId && projectId) return {
3085
+ token,
3086
+ teamId,
3087
+ projectId
3088
+ };
3089
+ }
3090
+ // src/chat/slack/post-warning.ts
3091
+ const codes = /* @__PURE__ */ new Set([
3092
+ "invalid_metadata_format",
3093
+ "invalid_metadata_schema",
3094
+ "metadata_too_large",
3095
+ "invalid_arguments",
3096
+ "invalid_blocks",
3097
+ "missing_scope",
3098
+ "missing_charset",
3099
+ "superfluous_charset",
3100
+ "message_truncated"
3101
+ ]);
3102
+ const pathFields = /* @__PURE__ */ new Set([
3103
+ "metadata",
3104
+ "event_type",
3105
+ "event_payload",
3106
+ "entities",
3107
+ "entity_type",
3108
+ "entity_payload",
3109
+ "external_ref",
3110
+ "id",
3111
+ "type",
3112
+ "url",
3113
+ "attributes",
3114
+ "title",
3115
+ "text",
3116
+ "display_id",
3117
+ "display_type",
3118
+ "product_name",
3119
+ "fields",
3120
+ "status",
3121
+ "custom_fields",
3122
+ "key",
3123
+ "label",
3124
+ "value",
3125
+ "long",
3126
+ "format",
3127
+ "link",
3128
+ "display_order",
3129
+ "app_unfurl_url",
3130
+ "app_id",
3131
+ "created_by",
3132
+ "created_at",
3133
+ "updated_by",
3134
+ "updated_at",
3135
+ "full_size_preview",
3136
+ "preview_url",
3137
+ "mime_type"
3138
+ ]);
3139
+ const diagnosticWords = new Set(`WARN ERROR Message message metadata was incorrectly formatted The the will be
3140
+ ignored as a result For for event entity refer to following errors error
3141
+ missing required field fields property properties additional unexpected unknown
3142
+ unsupported invalid expected received actual type types must should match matches
3143
+ matching does not is are has have of at in on and or an only allowed allow
3144
+ value values string number integer boolean object array null minimum maximum
3145
+ length min max items item empty nonempty too long short large small size limit
3146
+ exceeded exceeds less greater than equal valid format schema validation failed
3147
+ failure cannot contain contains found one any all none enum pattern definition
3148
+ defined constraint constraints satisfy satisfies with without provided specified
3149
+ unrecognized unrecognised recognized recognised extraneous disallowed duplicate
3150
+ unique malformed nested root exactly needs need requires requirement supported
3151
+ json-pointer input different schema_uri schema_id schemas additionalProperties
3152
+ minLength maxLength minItems maxItems anyOf oneOf allOf`.split(/\s+/));
3153
+ function summarizeDiagnostic(value) {
3154
+ return value.slice(0, 2e3).replace(/\b(?:https?:\/\/|www\.)[^\s"'`<>]+/gi, "[value]").replace(/\/[^\s"'`[\],;()]*|[\p{L}\p{N}\p{M}_]+(?:[-./:@=+%~][\p{L}\p{N}\p{M}_]+)*|[^\s\w[\]{}():,;."'`/*=<>!?+-]/gu, (token) => {
3155
+ if (token.startsWith("/")) return token.split("/").slice(0, 17).map((part, index) => index === 0 ? "" : pathFields.has(part) ? part : "*").join("/");
3156
+ return codes.has(token) || pathFields.has(token) || diagnosticWords.has(token) ? token : "[value]";
3157
+ }).replace(/\s+/g, " ").slice(0, 2e3);
3158
+ }
3159
+ /** Report Slack warnings once, on the request span, without retrying the post. */
3160
+ function captureSlackPostWarning(response, attributes) {
3161
+ const warnings = response.response_metadata?.warnings ?? [];
3162
+ const messages = response.response_metadata?.messages ?? [];
3163
+ const knownCodes = [...new Set(warnings.slice(0, 20).filter((code) => codes.has(code)))].sort();
3164
+ const diagnostics = {
3165
+ "app.slack.warning_count": warnings.length,
3166
+ "app.slack.response_message_count": messages.length,
3167
+ "app.slack.diagnostic_codes": knownCodes
3168
+ };
3169
+ setSpanAttributes(diagnostics);
3170
+ if (!warnings.length && !messages.length) return;
3171
+ captureMessage("Slack chat.postMessage returned warnings", {
3172
+ level: "warning",
3173
+ fingerprint: ["slack.chat.postMessage.warning", ...knownCodes.length ? knownCodes : ["unknown"]],
3174
+ tags: { "app.slack.method": "chat.postMessage" },
3175
+ extra: {
3176
+ ...getLogContextAttributes(),
3177
+ ...attributes,
3178
+ ...diagnostics,
3179
+ "app.slack.response_diagnostics": {
3180
+ warnings: warnings.slice(0, 20).map(summarizeDiagnostic),
3181
+ messages: messages.slice(0, 20).map(summarizeDiagnostic)
3182
+ }
3183
+ }
3184
+ });
3185
+ }
3186
+ // src/chat/slack/work-object.ts
3187
+ const url = z.url({ protocol: /^https?$/ });
3188
+ const textField = z.strictObject({
3189
+ type: z.literal("string"),
3190
+ key: z.string().min(1),
3191
+ label: z.string().min(1),
3192
+ value: z.string(),
3193
+ long: z.boolean().optional(),
3194
+ format: z.literal("markdown").optional(),
3195
+ link: url.optional()
3196
+ }).refine((field) => !field.format || !field.link, { message: "Slack string fields cannot combine format and link" });
3197
+ const customField = z.discriminatedUnion("type", [textField, z.strictObject({
3198
+ type: z.literal("slack#/types/timestamp"),
3199
+ key: z.string().min(1),
3200
+ label: z.string().min(1),
3201
+ value: z.number().int(),
3202
+ link: url.optional()
3203
+ })]);
3204
+ const payload = z.strictObject({
3205
+ attributes: z.strictObject({
3206
+ title: z.strictObject({ text: z.string().min(1) }),
3207
+ display_id: z.string().optional(),
3208
+ display_type: z.string().optional(),
3209
+ product_name: z.string().optional(),
3210
+ product_icon: z.strictObject({
3211
+ url,
3212
+ alt_text: z.string().min(1)
3213
+ }).optional()
3214
+ }),
3215
+ custom_fields: z.array(customField).optional(),
3216
+ display_order: z.array(z.string()).optional()
3217
+ });
3218
+ /** Limit IDs to Junior's safe subset; Slack silently rejects JSON punctuation. */
3219
+ const slackExternalRefIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, "Unsupported Slack external reference ID");
3220
+ const entity = z.strictObject({
3221
+ external_ref: z.strictObject({
3222
+ id: slackExternalRefIdSchema,
3223
+ type: z.string().min(1).optional()
3224
+ }),
3225
+ url
3226
+ });
3227
+ /** Validate notification and detail metadata before Slack can ignore bad fields. */
3228
+ const slackEntitySchema = z.discriminatedUnion("entity_type", [entity.extend({
3229
+ entity_type: z.literal("slack#/entities/item"),
3230
+ entity_payload: payload.extend({ fields: z.never().optional() })
3231
+ }), entity.extend({
3232
+ entity_type: z.literal("slack#/entities/task"),
3233
+ entity_payload: payload.extend({ fields: z.strictObject({ status: z.strictObject({ value: z.string() }).optional() }).optional() })
3234
+ })]);
3235
+ // src/chat/slack/outbound.ts
3236
+ const MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
3237
+ function requireSlackConversationId(channelId, action) {
3238
+ const normalized = normalizeSlackConversationId(channelId);
3239
+ if (!normalized) throw new Error(`${action} requires a valid channel ID`);
3240
+ return normalized;
3241
+ }
3242
+ function requireSlackThreadTimestamp(threadTs, action) {
3243
+ const normalized = threadTs.trim();
3244
+ if (!normalized) throw new Error(`${action} requires a thread timestamp`);
3245
+ return normalized;
3246
+ }
3247
+ function requireSlackMessageTimestamp(timestamp, action) {
3248
+ const normalized = parseSlackMessageTs(timestamp);
3249
+ if (!normalized) throw new Error(`${action} requires a target message timestamp`);
3250
+ return normalized;
3251
+ }
3252
+ function requireSlackMessageText(text, action) {
3253
+ if (text.trim().length === 0) throw new Error(`${action} requires non-empty text`);
3254
+ if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) throw new Error(`${action} text exceeds Slack's 40000 character truncation limit`);
3255
+ return text;
3256
+ }
3257
+ /** Resolve a Slack message permalink without making read-only workflows fail on lookup errors. */
3258
+ async function getSlackMessagePermalink(args) {
3259
+ try {
3260
+ return (await withSlackRetries(() => getSlackClient().chat.getPermalink({
3261
+ channel: args.channelId,
3262
+ message_ts: args.messageTs
3263
+ }), 3, {
3264
+ action: "chat.getPermalink",
3265
+ idempotent: true,
3266
+ spanAttributes: {
3267
+ "app.slack.channel_id": args.channelId,
3268
+ "app.slack.message_ts": args.messageTs
3269
+ }
3270
+ })).permalink;
3271
+ } catch {
3272
+ return;
3273
+ }
3274
+ }
3275
+ /** Open the bot's direct message with one Slack user and return its channel ID. */
3276
+ async function openSlackDirectMessage(userId) {
3277
+ const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
3278
+ if (!channelId) throw new Error("Slack did not return a direct message channel.");
3279
+ return channelId;
3280
+ }
3281
+ /** Post Slack `mrkdwn` text to a conversation or thread via the shared outbound boundary. */
3282
+ async function postSlackMessage(input) {
3283
+ const channelId = requireSlackConversationId(input.channelId, "Slack message posting");
3284
+ const text = requireSlackMessageText(input.text, "Slack message posting");
3285
+ const entities = input.entities?.length ? slackEntitySchema.array().parse(input.entities) : void 0;
3286
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack thread message posting") : void 0;
3287
+ const attributes = {
3288
+ "app.slack.channel_id": channelId,
3289
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0,
3290
+ "app.slack.work_object.count": entities?.length ?? 0
3291
+ };
3292
+ const response = await withSlackRetries(async () => {
3293
+ const response = await getSlackClient().chat.postMessage({
3294
+ channel: channelId,
3295
+ text,
3296
+ unfurl_links: false,
3297
+ unfurl_media: false,
3298
+ ...input.blocks?.length ? { blocks: input.blocks } : void 0,
3299
+ ...entities ? { metadata: { entities } } : void 0,
3300
+ ...threadTs ? { thread_ts: threadTs } : void 0
3301
+ });
3302
+ const messageId = parseSlackMessageTs(response.ts);
3303
+ setSpanAttributes({ "messaging.message.id": messageId });
3304
+ captureSlackPostWarning(response, {
3305
+ ...attributes,
3306
+ "messaging.message.id": messageId
3307
+ });
3308
+ return response;
3309
+ }, 3, {
3310
+ action: "chat.postMessage",
3311
+ attributes
3312
+ });
3313
+ const messageTs = parseSlackMessageTs(response.ts);
3314
+ if (!messageTs) throw new Error("Slack message posted without ts");
3315
+ return {
3316
+ ts: messageTs,
3317
+ ...input.includePermalink ? { permalink: await getSlackMessagePermalink({
3318
+ channelId,
3319
+ messageTs
3320
+ }) } : void 0
3321
+ };
3322
+ }
3323
+ /**
3324
+ * Post an ephemeral Slack message. Delivery is best-effort on Slack's side, but
3325
+ * request validation and Web API behavior are centralized here.
3326
+ */
3327
+ async function postSlackEphemeralMessage(input) {
3328
+ const channelId = requireSlackConversationId(input.channelId, "Slack ephemeral message posting");
3329
+ const userId = parseSlackUserId(input.userId);
3330
+ if (!userId) throw new Error("Slack ephemeral message posting requires a user ID");
3331
+ const text = requireSlackMessageText(input.text, "Slack ephemeral message posting");
3332
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack ephemeral thread message posting") : void 0;
3333
+ return { messageTs: (await withSlackRetries(() => getSlackClient().chat.postEphemeral({
3334
+ channel: channelId,
3335
+ user: userId,
3336
+ text,
3337
+ ...input.blocks?.length ? { blocks: input.blocks } : void 0,
3338
+ ...threadTs ? { thread_ts: threadTs } : void 0
3339
+ }), 3, {
3340
+ action: "chat.postEphemeral",
3341
+ spanAttributes: {
3342
+ "app.slack.channel_id": channelId,
3343
+ "app.slack.user_id": userId,
3344
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
3345
+ }
3346
+ })).message_ts };
3347
+ }
3348
+ /** Upload files into a Slack conversation or thread via the shared outbound file boundary. */
3349
+ async function uploadFilesToConversation(input) {
3350
+ const channelId = requireSlackConversationId(input.channelId, "Slack file upload");
3351
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack file upload") : void 0;
3352
+ if (input.files.length === 0) throw new Error("Slack file upload requires at least one file");
3353
+ const fileUploads = input.files.map((file) => {
3354
+ const filename = file.filename.trim();
3355
+ if (!filename) throw new Error("Slack file upload requires every file to have a filename");
3356
+ return {
3357
+ file: file.data,
3358
+ filename
3359
+ };
3360
+ });
3361
+ return { files: ((await withSlackRetries(() => getSlackClient().filesUploadV2({
3362
+ channel_id: channelId,
3363
+ ...threadTs ? { thread_ts: threadTs } : void 0,
3364
+ file_uploads: fileUploads
3365
+ }), 3, {
3366
+ action: "filesUploadV2",
3367
+ spanAttributes: {
3368
+ "app.slack.channel_id": channelId,
3369
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
3370
+ }
3371
+ })).files ?? []).flatMap((completion) => (completion.files ?? []).map((file) => file.id ? { id: file.id } : {})) };
3372
+ }
3373
+ /** Add a reaction to a Slack message, treating `already_reacted` as idempotent success. */
3374
+ async function addReactionToMessage(input) {
3375
+ const channelId = requireSlackConversationId(input.channelId, "Slack reaction");
3376
+ const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction");
3377
+ const emoji = normalizeSlackEmojiName(input.emoji);
3378
+ if (!emoji) throw new Error("Slack reaction requires a valid emoji alias name");
3379
+ try {
3380
+ await withSlackRetries(() => getSlackClient(input.timeoutMs === void 0 ? void 0 : { timeoutMs: input.timeoutMs }).reactions.add({
3381
+ channel: channelId,
3382
+ timestamp,
3383
+ name: emoji
3384
+ }), input.timeoutMs === void 0 ? 3 : 1, {
3385
+ action: "reactions.add",
3386
+ idempotent: true,
3387
+ spanAttributes: {
3388
+ "app.slack.channel_id": channelId,
3389
+ "app.slack.message_ts": timestamp,
3390
+ "app.slack.reaction": emoji
3391
+ }
3392
+ });
3393
+ } catch (error) {
3394
+ if (error instanceof SlackActionError && error.code === "already_reacted") return { ok: true };
3395
+ throw error;
3396
+ }
3397
+ return { ok: true };
3398
+ }
3399
+ /** Remove a reaction from a Slack message, treating `no_reaction` as idempotent success. */
3400
+ async function removeReactionFromMessage(input) {
3401
+ const channelId = requireSlackConversationId(input.channelId, "Slack reaction removal");
3402
+ const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction removal");
3403
+ const emoji = normalizeSlackEmojiName(input.emoji);
3404
+ if (!emoji) throw new Error("Slack reaction removal requires a valid emoji alias name");
3405
+ try {
3406
+ await withSlackRetries(() => getSlackClient().reactions.remove({
3407
+ channel: channelId,
3408
+ timestamp,
3409
+ name: emoji
3410
+ }), 3, {
3411
+ action: "reactions.remove",
3412
+ idempotent: true,
3413
+ spanAttributes: {
3414
+ "app.slack.channel_id": channelId,
3415
+ "app.slack.message_ts": timestamp,
3416
+ "app.slack.reaction": emoji
3417
+ }
3418
+ });
3419
+ } catch (error) {
3420
+ if (error instanceof SlackActionError && error.code === "no_reaction") return { ok: true };
3421
+ throw error;
3422
+ }
3423
+ return { ok: true };
3424
+ }
3425
+ // src/chat/slack/status-format.ts
3426
+ const SLACK_STATUS_MAX_LENGTH = 50;
3427
+ /** Truncate a status string to fit Slack's 50-char assistant status limit. */
3428
+ function truncateStatusText(text) {
3429
+ const trimmed = text.trim();
3430
+ if (!trimmed) return "";
3431
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) return trimmed;
3432
+ return `${trimmed.slice(0, 47).trimEnd()}...`;
3433
+ }
3434
+ // src/chat/slack/mrkdwn.ts
3435
+ /** Escape dynamic text for Slack mrkdwn without changing intended formatting. */
3436
+ function escapeSlackMrkdwnText(text) {
3437
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3438
+ }
3439
+ /** Escape a URL for Slack explicit link syntax while preserving query semantics. */
3440
+ function escapeSlackLinkUrl(url) {
3441
+ return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
3442
+ }
3443
+ /** Build a Slack explicit link from dynamic URL and label values. */
3444
+ function formatSlackLink(url, label) {
3445
+ return `<${escapeSlackLinkUrl(url)}|${escapeSlackMrkdwnText(label)}>`;
3446
+ }
3447
+ function readInlineCodeSpan(line, start) {
3448
+ if (line[start] !== "`") return;
3449
+ let n = 1;
3450
+ while (line[start + n] === "`") n++;
3451
+ const marker = "`".repeat(n);
3452
+ let search = start + n;
3453
+ while (search < line.length) {
3454
+ const close = line.indexOf(marker, search);
3455
+ if (close === -1) return;
3456
+ const after = close + n;
3457
+ if (line[after] !== "`") return {
3458
+ text: line.slice(start, after),
3459
+ end: after
3460
+ };
3461
+ search = after + 1;
3462
+ }
3463
+ }
3464
+ function readExistingSlackAngleToken(line, start) {
3465
+ if (line[start] !== "<") return;
3466
+ const close = line.indexOf(">", start + 1);
3467
+ if (close === -1) return;
3468
+ const body = line.slice(start + 1, close);
3469
+ if (/^(?:https?:\/\/|@|#|!)/.test(body)) return {
3470
+ text: line.slice(start, close + 1),
3471
+ end: close + 1
3472
+ };
3473
+ }
3474
+ function readMarkdownLink(line, start) {
3475
+ if (line[start] !== "[") return;
3476
+ const labelEnd = line.indexOf("](", start + 1);
3477
+ if (labelEnd === -1) return;
3478
+ const destStart = labelEnd + 2;
3479
+ if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) return;
3480
+ const closeParens = line.indexOf(")", destStart);
3481
+ if (closeParens === -1) return;
3482
+ return {
3483
+ text: line.slice(start, closeParens + 1),
3484
+ end: closeParens + 1
3485
+ };
3486
+ }
3487
+ function hasUnmatchedClosingParen(text) {
3488
+ let balance = 0;
3489
+ for (const ch of text) if (ch === "(") balance++;
3490
+ else if (ch === ")") balance--;
3491
+ return balance < 0;
3492
+ }
3493
+ function readBareUrl(line, start) {
3494
+ let end = start;
3495
+ while (end < line.length) {
3496
+ const ch = line.charAt(end);
3497
+ if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === "\"" || ch === "`" || ch === "|" || ch === "*") break;
3498
+ end++;
3499
+ }
3500
+ if (end === start) return;
3501
+ let raw = line.slice(start, end);
3502
+ let suffix = "";
3503
+ const peel = () => {
3504
+ suffix = raw.slice(-1) + suffix;
3505
+ raw = raw.slice(0, -1);
3506
+ };
3507
+ const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
3508
+ while (raw.length > 0 && shouldPeel()) peel();
3509
+ if (!/^https?:\/\/.+/.test(raw)) return;
3510
+ return {
3511
+ url: raw,
3512
+ suffix,
3513
+ end
3514
+ };
3515
+ }
3516
+ const BARE_SLACK_USER_ID_RE = /^@([UW](?=[A-Z0-9]*\d)[A-Z0-9]{8,})(?![\w])/;
3517
+ function readBareSlackUserMention(line, start) {
3518
+ if (line[start] !== "@" || /\w/.test(line[start - 1] ?? "")) return;
3519
+ const match = BARE_SLACK_USER_ID_RE.exec(line.slice(start));
3520
+ if (!match) return;
3521
+ return {
3522
+ text: `<@${match[1]}>`,
3523
+ end: start + match[0].length
3524
+ };
3525
+ }
3526
+ /**
3527
+ * Wrap bare http(s) URLs and bare `@U123` Slack user IDs on a single line as
3528
+ * Slack explicit `<url>` links and `<@U123>` mentions.
3529
+ */
3530
+ function wrapBareReferencesOnLine(line) {
3531
+ let result = "";
3532
+ let i = 0;
3533
+ while (i < line.length) {
3534
+ const codeSpan = readInlineCodeSpan(line, i);
3535
+ if (codeSpan) {
3536
+ result += codeSpan.text;
3537
+ i = codeSpan.end;
3538
+ continue;
3539
+ }
3540
+ const angleToken = readExistingSlackAngleToken(line, i);
3541
+ if (angleToken) {
3542
+ result += angleToken.text;
3543
+ i = angleToken.end;
3544
+ continue;
3545
+ }
3546
+ const mdLink = readMarkdownLink(line, i);
3547
+ if (mdLink) {
3548
+ result += mdLink.text;
3549
+ i = mdLink.end;
3550
+ continue;
3551
+ }
3552
+ const userMention = readBareSlackUserMention(line, i);
3553
+ if (userMention) {
3554
+ result += userMention.text;
3555
+ i = userMention.end;
3556
+ continue;
3557
+ }
3558
+ if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
3559
+ const parsed = readBareUrl(line, i);
3560
+ if (parsed) {
3561
+ result += `<${parsed.url}>${parsed.suffix}`;
3562
+ i = parsed.end;
3563
+ continue;
3564
+ }
3565
+ }
3566
+ result += line[i];
3567
+ i++;
3568
+ }
3569
+ return result;
3570
+ }
3571
+ /**
3572
+ * Pre-wrap bare http(s) URLs outside fenced code blocks as Slack explicit
3573
+ * links, preventing Slack's auto-linker from consuming adjacent formatting
3574
+ * markers into the URL. Bare `@U123` user IDs become `<@U123>` mentions,
3575
+ * because Slack shows a bare ID as literal text.
3576
+ *
3577
+ * Uses the same fence-toggle rule as `ensureBlockSpacing` so both passes
3578
+ * agree on which lines are code.
3579
+ */
3580
+ function wrapBareReferences(text) {
3581
+ const lines = text.split("\n");
3582
+ const out = [];
3583
+ let inCodeBlock = false;
3584
+ for (const line of lines) {
3585
+ if (line.trimStart().startsWith("```")) {
3586
+ inCodeBlock = !inCodeBlock;
3587
+ out.push(line);
3588
+ continue;
3589
+ }
3590
+ out.push(inCodeBlock ? line : wrapBareReferencesOnLine(line));
3591
+ }
3592
+ return out.join("\n");
3593
+ }
3594
+ const LIST_ITEM_PATTERN = /^[-*•]\s|^\d+\.\s/;
3595
+ /**
3596
+ * Turn a follow-up paragraph inside a list item into a bullet at the same
3597
+ * indent.
3598
+ *
3599
+ * Slack's `markdown` block keeps only one paragraph per list item. It joins
3600
+ * the other paragraphs to the top-level item's text with no space and renders
3601
+ * nested lists after them. As a bullet, the paragraph keeps its place and
3602
+ * nesting.
3603
+ */
3604
+ function bulletListFollowUpParagraphs(text) {
3605
+ const lines = text.split("\n");
3606
+ let inCodeBlock = false;
3607
+ let inList = false;
3608
+ for (const [i, line] of lines.entries()) {
3609
+ const trimmed = line.trimStart();
3610
+ const isFence = trimmed.startsWith("```");
3611
+ if (!inCodeBlock && trimmed && trimmed.length === line.length) inList = LIST_ITEM_PATTERN.test(line);
3612
+ else if (!inCodeBlock && inList && trimmed && !isFence && lines[i - 1]?.trim() === "" && !LIST_ITEM_PATTERN.test(trimmed)) lines[i] = `${line.slice(0, line.length - trimmed.length)}- ${trimmed}`;
3613
+ if (isFence) inCodeBlock = !inCodeBlock;
3614
+ }
3615
+ return lines.join("\n");
3616
+ }
3617
+ /** Insert blank lines between content blocks so Slack renders them with visual separation. */
3618
+ function ensureBlockSpacing(text) {
3619
+ const codeBlockPattern = /^```/;
3620
+ const lines = text.split("\n");
3621
+ const result = [];
3622
+ let inCodeBlock = false;
3623
+ for (const line of lines) {
3624
+ if (codeBlockPattern.test(line.trimStart())) {
3625
+ if (!inCodeBlock) {
3626
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
3627
+ if (prev !== void 0 && prev.trim() !== "") result.push("");
3628
+ }
3629
+ inCodeBlock = !inCodeBlock;
3630
+ result.push(line);
3631
+ continue;
3632
+ }
3633
+ if (inCodeBlock) {
3634
+ result.push(line);
3635
+ continue;
3636
+ }
3637
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
3638
+ const isListItem = LIST_ITEM_PATTERN.test(line.trimStart());
3639
+ const isListContinuation = prev !== void 0 && !isListItem && /^\s/.test(line) && (/^\s/.test(prev) || LIST_ITEM_PATTERN.test(prev));
3640
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !isListContinuation && !(LIST_ITEM_PATTERN.test(prev.trimStart()) && isListItem)) result.push("");
3641
+ result.push(line);
3642
+ }
3643
+ return result.join("\n");
3644
+ }
3645
+ /**
3646
+ * Normalize model-authored Slack markdown for delivery via `markdown_text`
3647
+ * or `{ type: "markdown" }` blocks.
3648
+ *
3649
+ * Plugins may first rewrite ordinary Markdown. Core then applies Slack-only
3650
+ * delivery shaping: bare URL wrapping, block spacing, and list follow-up
3651
+ * paragraph bullets. Slack reply delivery owns chunking and continuation
3652
+ * markers separately.
3653
+ */
3654
+ function normalizeSlackReplyMarkdown(text) {
3655
+ let normalized = applyPluginFormatMarkdown(text).replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
3656
+ normalized = wrapBareReferences(normalized);
3657
+ normalized = ensureBlockSpacing(normalized);
3658
+ normalized = bulletListFollowUpParagraphs(normalized);
3659
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
3660
+ }
3661
+ /** Normalize assistant status text before handing it to Slack. */
3662
+ function normalizeSlackStatusText(text) {
3663
+ const trimmed = text.trim();
3664
+ if (!trimmed) return "";
3665
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
3666
+ }
3667
+ /** Render a Slack user mention, which Slack shows as the person's name. */
3668
+ function slackMention(userId) {
3669
+ return `<@${userId}>`;
3670
+ }
3671
+ // src/chat/slack/oauth-authorization-message.ts
3672
+ const ACCOUNT_SCOPE_NOTE = "This connects only your account. I can see only what you can see.";
3673
+ /** Present private OAuth authorization with a clear action and text fallback. */
3674
+ function buildSlackOAuthAuthorizationMessage(args) {
3675
+ return {
3676
+ text: `${formatSlackLink(args.authorizationUrl, args.label)}. ${ACCOUNT_SCOPE_NOTE} ${args.completionText}`,
3677
+ blocks: [{
3678
+ type: "section",
3679
+ text: {
3680
+ type: "mrkdwn",
3681
+ text: `*${escapeSlackMrkdwnText(args.label)}*\n${ACCOUNT_SCOPE_NOTE}\n${escapeSlackMrkdwnText(args.completionText)}`
3682
+ },
3683
+ accessory: {
3684
+ type: "button",
3685
+ text: {
3686
+ type: "plain_text",
3687
+ text: "Connect"
3688
+ },
3689
+ style: "primary",
3690
+ url: args.authorizationUrl,
3691
+ action_id: "oauth_connect"
3692
+ }
3693
+ }]
3694
+ };
3695
+ }
3696
+ // src/chat/oauth-flow.ts
3697
+ const OAUTH_STATE_TTL_MS = 6e5;
3698
+ function optionalString(value) {
3699
+ return typeof value === "string" ? value : void 0;
3700
+ }
3701
+ /** Parse OAuth callback state that was persisted before a provider redirect. */
3702
+ function parseOAuthStatePayload(value) {
3703
+ if (!isRecord$2(value)) return;
3704
+ if (typeof value.userId !== "string" || typeof value.provider !== "string") return;
3705
+ const actor = value.actor === void 0 ? void 0 : actorSchema.safeParse(value.actor);
3706
+ if (value.actor !== void 0 && (!actor || !actor.success)) return;
3707
+ const destination = parseDestination(value.destination);
3708
+ if (value.destination !== void 0 && !destination) return;
3709
+ const source = value.source === void 0 ? void 0 : sourceSchema.safeParse(value.source);
3710
+ if (value.source !== void 0 && (!source || !source.success)) return;
3711
+ return {
3712
+ userId: value.userId,
3713
+ provider: value.provider,
3714
+ ...actor?.success ? { actor: actor.data } : void 0,
3715
+ ...optionalString(value.channelId) ? { channelId: optionalString(value.channelId) } : void 0,
3716
+ ...destination ? { destination } : void 0,
3717
+ ...source?.success ? { source: source.data } : void 0,
3718
+ ...optionalString(value.threadTs) ? { threadTs: optionalString(value.threadTs) } : void 0,
3719
+ ...optionalString(value.resumeConversationId) ? { resumeConversationId: optionalString(value.resumeConversationId) } : void 0,
3720
+ ...optionalString(value.resumeSessionId) ? { resumeSessionId: optionalString(value.resumeSessionId) } : void 0,
3721
+ ...optionalString(value.scope) ? { scope: optionalString(value.scope) } : void 0
3722
+ };
3723
+ }
3724
+ /** Return the manifest-owned display label for a provider. */
3725
+ function formatProviderLabel(provider) {
3726
+ const displayName = pluginCatalogRuntime.getDisplayName(provider);
3727
+ if (!displayName) throw new Error(`Unknown plugin provider display name: "${provider}"`);
3728
+ return displayName;
3729
+ }
3730
+ /** Resolve the public base URL from environment variables (JUNIOR_BASE_URL or Vercel). */
3731
+ function resolveBaseUrl() {
3732
+ const explicit = process.env.JUNIOR_BASE_URL?.trim();
3733
+ if (explicit) return explicit;
3734
+ const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
3735
+ if (vercelProd) return `https://${vercelProd}`;
3736
+ const vercelUrl = process.env.VERCEL_URL?.trim();
3737
+ if (vercelUrl) return `https://${vercelUrl}`;
3738
+ }
3739
+ /**
3740
+ * Authorization links must only be visible to the requesting user.
3741
+ * Try in-context private delivery first, then fall back to a DM.
3742
+ */
3743
+ async function deliverPrivateMessage(input) {
3744
+ let client;
3745
+ try {
3746
+ client = getSlackClient();
3747
+ } catch {
3748
+ logWarn("oauth.private_delivery.skipped", { "app.reason": "missing_bot_token" });
3749
+ return false;
3750
+ }
3751
+ if (input.channelId) try {
3752
+ if (isDmChannel(input.channelId)) await postSlackMessage({
3753
+ channelId: input.channelId,
3754
+ text: input.text,
3755
+ blocks: input.blocks,
3756
+ threadTs: input.threadTs
3757
+ });
3758
+ else await postSlackEphemeralMessage({
3759
+ channelId: input.channelId,
3760
+ userId: input.userId,
3761
+ text: input.text,
3762
+ blocks: input.blocks,
3763
+ threadTs: input.threadTs
3764
+ });
3765
+ return "in_context";
3766
+ } catch (error) {
3767
+ logWarn("oauth.private_delivery.failed", {
3768
+ "app.slack.error": error instanceof Error ? error.message : String(error),
3769
+ "app.slack.channel": input.channelId
3770
+ });
3771
+ }
3772
+ try {
3773
+ const dmChannelId = (await withSlackRetries(() => client.conversations.open({ users: input.userId }), 3, {
3774
+ action: "conversations.open",
3775
+ spanAttributes: { "app.slack.user_id": input.userId }
3776
+ })).channel?.id;
3777
+ if (!dmChannelId) {
3778
+ logWarn("oauth.dm.fallback.failed", { "app.reason": "no_dm_channel_id" });
3779
+ return false;
3780
+ }
3781
+ await postSlackMessage({
3782
+ channelId: dmChannelId,
3783
+ text: input.text,
3784
+ blocks: input.blocks
3785
+ });
3786
+ return "fallback_dm";
3787
+ } catch (error) {
3788
+ logWarn("oauth.dm.fallback.failed", { "app.slack.error": error instanceof Error ? error.message : String(error) });
3789
+ return false;
3790
+ }
3791
+ }
3792
+ /** Present one OAuth request through the active surface's private delivery. */
3793
+ async function deliverOAuthAuthorization(request, input) {
3794
+ if (input.authorization) {
3795
+ await input.authorization.deliver(request);
3796
+ return "in_context";
3797
+ }
3798
+ return await deliverPrivateMessage({
3799
+ channelId: input.channelId,
3800
+ threadTs: input.threadTs,
3801
+ userId: input.userId,
3802
+ ...buildSlackOAuthAuthorizationMessage(request)
3803
+ });
3804
+ }
3805
+ /** Initiate an OAuth authorization code flow for a provider and deliver the auth link to the user. */
3806
+ async function startOAuthFlow(provider, input) {
3807
+ const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
3808
+ if (!providerConfig) return {
3809
+ ok: false,
3810
+ error: `Provider "${provider}" does not support OAuth authorization`
3811
+ };
3812
+ const clientId = process.env[providerConfig.clientIdEnv]?.trim();
3813
+ if (!clientId) return {
3814
+ ok: false,
3815
+ error: `Missing ${providerConfig.clientIdEnv} environment variable`
3816
+ };
3817
+ const baseUrl = resolveBaseUrl();
3818
+ if (!baseUrl) return {
3819
+ ok: false,
3820
+ error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
3821
+ };
3822
+ const state = input.authorization ? await input.authorization.createState() : randomBytes(32).toString("hex");
3823
+ const requestedScope = input.scope ?? providerConfig.scope;
3824
+ await getStateAdapter().set(`oauth-state:${state}`, {
3825
+ userId: input.actorId,
3826
+ provider,
3827
+ ...input.actor ? { actor: input.actor } : void 0,
3828
+ ...input.channelId ? { channelId: input.channelId } : void 0,
3829
+ ...input.destination ? { destination: input.destination } : void 0,
3830
+ ...input.source ? { source: input.source } : void 0,
3831
+ ...input.threadTs ? { threadTs: input.threadTs } : void 0,
3832
+ ...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : void 0,
3833
+ ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : void 0,
3834
+ ...requestedScope ? { scope: requestedScope } : void 0
3835
+ }, OAUTH_STATE_TTL_MS);
3836
+ const authorizeParams = new URLSearchParams({
3837
+ client_id: clientId,
3838
+ state,
3839
+ redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
3840
+ response_type: "code"
3841
+ });
3842
+ if (requestedScope) authorizeParams.set("scope", requestedScope);
3843
+ for (const [key, value] of Object.entries(providerConfig.authorizeParams ?? {})) authorizeParams.set(key, value);
3844
+ logInfo("jr_rpc.oauth.started", {
3845
+ "app.credential.provider": provider,
3846
+ ...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : void 0
3847
+ });
3848
+ return {
3849
+ ok: true,
3850
+ delivery: await deliverOAuthAuthorization({
3851
+ authorizationUrl: `${providerConfig.authorizeEndpoint}?${authorizeParams.toString()}`,
3852
+ label: `Connect your ${formatProviderLabel(provider)} account`,
3853
+ completionText: input.resumeSessionId ? "Once you've authorized, I'll continue automatically." : "Once you've authorized, you'll see a confirmation in Slack."
3854
+ }, {
3855
+ authorization: input.authorization,
3856
+ channelId: input.channelId,
3857
+ threadTs: input.threadTs,
3858
+ userId: input.actorId
3859
+ })
3860
+ };
3861
+ }
3862
+ // src/chat/sandbox/egress/oidc.ts
3863
+ const OIDC_DISCOVERY_CACHE_TTL_MS = 36e5;
3864
+ const OIDC_DISCOVERY_CACHE_MAX_ENTRIES = 8;
3865
+ const jwksByIssuer = /* @__PURE__ */ new Map();
3866
+ function buildDiscoveryUrl(issuer) {
3867
+ const url = new URL(issuer);
3868
+ if (url.protocol !== "https:" || url.hostname !== "oidc.vercel.com") throw new Error("Unexpected Vercel OIDC issuer");
3869
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/.well-known/openid-configuration`;
3870
+ url.search = "";
3871
+ url.hash = "";
3872
+ return url;
3873
+ }
3874
+ function buildJwksUrl(value) {
3875
+ const url = new URL(value);
3876
+ if (url.protocol !== "https:") throw new Error("Vercel OIDC discovery jwks_uri must use HTTPS");
3877
+ return url;
3878
+ }
3879
+ async function getJwks(issuer) {
3880
+ const now = Date.now();
3881
+ const cached = jwksByIssuer.get(issuer);
3882
+ if (cached && cached.expiresAtMs > now) return cached.jwks;
3883
+ if (cached) jwksByIssuer.delete(issuer);
3884
+ const response = await fetch(buildDiscoveryUrl(issuer), { redirect: "error" });
3885
+ if (!response.ok) throw new Error("Unable to load Vercel OIDC discovery metadata");
3886
+ const config = await response.json();
3887
+ if (!config.jwks_uri) throw new Error("Vercel OIDC discovery metadata did not include jwks_uri");
3888
+ const jwks = createRemoteJWKSet(buildJwksUrl(config.jwks_uri));
3889
+ if (!jwksByIssuer.has(issuer) && jwksByIssuer.size >= OIDC_DISCOVERY_CACHE_MAX_ENTRIES) {
3890
+ const oldestIssuer = jwksByIssuer.keys().next().value;
3891
+ if (oldestIssuer) jwksByIssuer.delete(oldestIssuer);
3892
+ }
3893
+ jwksByIssuer.set(issuer, {
3894
+ jwks,
3895
+ expiresAtMs: now + OIDC_DISCOVERY_CACHE_TTL_MS
3896
+ });
3897
+ return jwks;
3898
+ }
3899
+ /**
3900
+ * Verify Vercel signed this Sandbox firewall proxy request for the active VM session.
3901
+ *
3902
+ * `currentDate` is the time for the `exp` and `nbf` checks. It defaults to
3903
+ * now. Evals pass the real time because they replace `Date`.
3904
+ */
3905
+ async function verifyVercelSandboxOidcToken(token, options = {}) {
3906
+ const unverified = decodeJwt(token);
3907
+ if (typeof unverified.iss !== "string") throw new Error("Vercel OIDC token did not include an issuer");
3908
+ const jwks = await getJwks(unverified.iss);
3909
+ return (await jwtVerify(token, jwks, {
3910
+ issuer: unverified.iss,
3911
+ ...options.currentDate ? { currentDate: options.currentDate } : void 0
3912
+ })).payload;
3913
+ }
3914
+ // src/chat/sandbox/egress/tracing.ts
3915
+ function isValidDomainPattern(domain) {
3916
+ if (domain.includes("*")) return domain.startsWith("*.") && domain.indexOf("*", 1) === -1;
3917
+ return true;
3918
+ }
3919
+ /** Normalize exact and leading-wildcard sandbox egress trace domains. */
3920
+ function normalizeSandboxEgressTracePropagationDomains(domains) {
3921
+ if (domains === void 0) return [];
3922
+ if (!Array.isArray(domains)) throw new Error("sandbox.egressTracePropagationDomains must be an array");
3923
+ return [...new Set(domains.map((domain) => {
3924
+ if (typeof domain !== "string") throw new Error("sandbox.egressTracePropagationDomains entries must be strings");
3925
+ const normalized = domain.trim().toLowerCase();
3926
+ if (!normalized) throw new Error("sandbox.egressTracePropagationDomains entries must be non-empty");
3927
+ if (!isValidDomainPattern(normalized)) throw new Error("sandbox.egressTracePropagationDomains entries must be exact domains or leading wildcard domains");
3928
+ return normalized;
3929
+ }))].sort((left, right) => left.localeCompare(right));
3930
+ }
3931
+ /** Return whether a host may carry sandbox egress trace propagation headers. */
3932
+ function shouldPropagateSandboxEgressTrace(host, config = {}) {
3933
+ const normalizedHost = host.trim().toLowerCase();
3934
+ return (config.domains ?? []).some((domain) => {
3935
+ if (domain.startsWith("*.")) {
3936
+ const suffix = domain.slice(1);
3937
+ return normalizedHost.endsWith(suffix) && normalizedHost !== domain.slice(2);
3938
+ }
3939
+ return domain === normalizedHost;
3940
+ });
3941
+ }
3942
+ // src/chat/sandbox/egress/policy.ts
3943
+ /** Return whether an outbound host is covered by a provider domain rule. */
3944
+ function matchesSandboxEgressDomain(host, domain) {
3945
+ return host.toLowerCase() === domain.toLowerCase();
3946
+ }
3947
+ function manifestDomains(manifest) {
3948
+ return [.../* @__PURE__ */ new Set([...manifest.credentials?.domains ?? [], ...manifest.domains ?? []])].sort((left, right) => left.localeCompare(right));
3949
+ }
3950
+ function providerEntries() {
3951
+ return pluginCatalogRuntime.getProviders().map((plugin) => ({
3952
+ provider: plugin.manifest.name,
3953
+ domains: manifestDomains(plugin.manifest)
3954
+ })).filter((entry) => entry.domains.length > 0).sort((left, right) => left.provider.localeCompare(right.provider));
3955
+ }
3956
+ /**
3957
+ * Resolve the plugin provider responsible for an outbound sandbox host.
3958
+ *
3959
+ * Only plugin-declared provider domains are eligible for credentialed sandbox
3960
+ * egress. Hosts without a matching provider are blocked by the proxy even if a
3961
+ * malformed or stale network policy tried to forward them.
3962
+ */
3963
+ function resolveSandboxEgressProviderForHost(host) {
3964
+ return providerEntries().find((entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain)))?.provider;
3965
+ }
3966
+ /**
3967
+ * Build the provider-neutral egress policy for one sandbox command context.
3968
+ *
3969
+ * Provider domains go through Junior's credentialed egress when a signed
3970
+ * credential token is given. The token binds each request to the actor and
3971
+ * sandbox VM that created the command. Configured trace-only domains get trace
3972
+ * headers in place. The sandbox receives placeholder env vars, not provider
3973
+ * credentials.
3974
+ */
3975
+ function buildSandboxEgressNetworkPolicy(input) {
3976
+ const credentialToken = input?.credentialToken;
3977
+ const credentialDomains = credentialToken ? [...new Set(providerEntries().flatMap((entry) => entry.domains))].sort((left, right) => left.localeCompare(right)) : [];
3978
+ const traceHeaders = Object.fromEntries(Object.entries(input?.traceHeaders ?? {}).filter(([, value]) => typeof value === "string" && value.trim()));
3979
+ const traceDomains = Object.keys(traceHeaders).length > 0 ? [...new Set(input?.traceConfig?.domains ?? [])].filter((domain) => !credentialDomains.includes(domain) && shouldPropagateSandboxEgressTrace(domain, input?.traceConfig)).sort((left, right) => left.localeCompare(right)) : [];
3980
+ return {
3981
+ ...credentialToken && credentialDomains.length > 0 ? { credentialEgress: {
3982
+ credentialToken,
3983
+ domains: credentialDomains
3984
+ } } : void 0,
3985
+ ...traceDomains.length > 0 ? { traceHeaders: {
3986
+ domains: traceDomains,
3987
+ headers: traceHeaders
3988
+ } } : void 0
3989
+ };
3990
+ }
3991
+ /**
3992
+ * Resolve non-secret command environment values for registered sandbox providers.
3993
+ *
3994
+ * Credential env vars are placeholders that tell tools which auth variable name
3995
+ * exists; real credential headers are issued lazily by host egress.
3996
+ */
3997
+ async function resolveSandboxCommandEnvironment() {
3998
+ const env = {};
3999
+ for (const plugin of pluginCatalogRuntime.getProviders().sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))) {
4000
+ Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
4001
+ const credentials = plugin.manifest.credentials;
4002
+ if (credentials?.authTokenEnv) env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
4003
+ }
4004
+ return env;
4005
+ }
4006
+ // src/chat/sandbox/egress/schemas.ts
4007
+ const finiteNumberSchema = z.number().refine(Number.isFinite);
4008
+ const httpStatusSchema = z.number().int().min(100).max(599);
4009
+ const providerNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
4010
+ const credentialSignalKindSchema = z.enum(["auth_required", "unavailable"]);
4011
+ const sandboxEgressGrantSchema = pluginGrantSchema;
4012
+ const sandboxEgressCredentialContextSchema = z.object({
4013
+ credentials: credentialContextSchema,
4014
+ /** Conversation that the credentials act for. Telemetry only; it grants nothing. */
4015
+ conversationId: z.string().min(1).optional(),
4016
+ egressId: z.string().min(1),
4017
+ expiresAtMs: finiteNumberSchema,
4018
+ contextId: z.string().min(1)
4019
+ }).strict();
4020
+ const sandboxEgressCredentialLeaseSchema = z.object({
4021
+ account: pluginProviderAccountSchema.optional(),
4022
+ authorization: pluginAuthorizationSchema.optional(),
4023
+ grant: sandboxEgressGrantSchema,
4024
+ provider: providerNameSchema,
4025
+ expiresAt: z.string().min(1),
4026
+ headerTransforms: z.array(pluginCredentialHeaderTransformSchema).min(1)
4027
+ }).strict();
4028
+ const sandboxEgressAuthRequiredSignalSchema = z.object({
4029
+ authorization: pluginAuthorizationSchema.optional(),
4030
+ grant: sandboxEgressGrantSchema,
4031
+ kind: credentialSignalKindSchema.default("auth_required"),
4032
+ provider: providerNameSchema,
4033
+ message: z.string().optional(),
4034
+ createdAtMs: finiteNumberSchema
4035
+ }).strict().superRefine((signal, ctx) => {
4036
+ if (signal.authorization && signal.authorization.provider !== signal.provider) ctx.addIssue({
4037
+ code: z.ZodIssueCode.custom,
4038
+ message: "Auth signal authorization provider must match provider",
4039
+ path: ["authorization", "provider"]
4040
+ });
4041
+ });
4042
+ const sandboxEgressPermissionDeniedSignalSchema = z.object({
4043
+ account: pluginProviderAccountSchema.optional(),
4044
+ grant: sandboxEgressGrantSchema,
4045
+ message: z.string().min(1),
4046
+ provider: providerNameSchema,
4047
+ source: z.literal("upstream"),
4048
+ status: httpStatusSchema,
4049
+ upstreamHost: z.string().min(1),
4050
+ upstreamPath: z.string().min(1),
4051
+ createdAtMs: finiteNumberSchema
4052
+ }).strict();
4053
+ const sandboxEgressSignalsResponseSchema = z.object({
4054
+ auth_required: sandboxEgressAuthRequiredSignalSchema.optional(),
4055
+ permission_denied: sandboxEgressPermissionDeniedSignalSchema.optional()
4056
+ }).strict();
4057
+ /** Parse a host-owned sandbox egress auth signal from state or tool results. */
4058
+ function parseSandboxEgressAuthRequiredSignal(value) {
4059
+ const result = sandboxEgressAuthRequiredSignalSchema.safeParse(value);
4060
+ return result.success ? result.data : void 0;
4061
+ }
4062
+ /** Parse a host-owned sandbox egress permission-denied signal from state or tool results. */
4063
+ function parseSandboxEgressPermissionDeniedSignal(value) {
4064
+ const result = sandboxEgressPermissionDeniedSignalSchema.safeParse(value);
4065
+ return result.success ? result.data : void 0;
4066
+ }
4067
+ // src/chat/sandbox/egress/session.ts
4068
+ const SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
4069
+ const SANDBOX_EGRESS_TOKEN_VERSION = "v1";
4070
+ const SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
4071
+ const SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX = "sandbox-egress-auth-required";
4072
+ const SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX = "sandbox-egress-permission-denied";
4073
+ const SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
4074
+ const DEFAULT_SESSION_TTL_MS = 18e5;
4075
+ /** Treat a cached lease as expired this long before its provider expiry. */
4076
+ const LEASE_REFRESH_BUFFER_MS = 3e5;
4077
+ /** True when this grant may keep shared auth headers on the host. */
4078
+ function isSharedInstallationGrant(grant) {
4079
+ return grant.name.startsWith("installation-");
4080
+ }
4081
+ /**
4082
+ * Build the host key for a shared installation grant.
4083
+ *
4084
+ * Only installation grants are remembered. User and other grants issue live
4085
+ * headers each time, so they never write a host key.
4086
+ */
4087
+ function leaseKey(provider, grant) {
4088
+ return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${grant.name}:shared`;
4089
+ }
4090
+ /**
4091
+ * Build the command signal key for credential issuance failures by access level.
4092
+ */
4093
+ function authSignalKey(egressId, access) {
4094
+ return `${SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX}:${egressId}:${access}`;
4095
+ }
4096
+ /**
4097
+ * Build the command signal key for upstream permission denials by access level.
4098
+ */
4099
+ function permissionSignalKey(egressId, access) {
4100
+ return `${SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX}:${egressId}:${access}`;
4101
+ }
4102
+ function base64Url(input) {
4103
+ return Buffer.from(input, "utf8").toString("base64url");
4104
+ }
4105
+ function fromBase64Url(input) {
4106
+ return Buffer.from(input, "base64url").toString("utf8");
4107
+ }
4108
+ /**
4109
+ * Sign a versioned context payload in the sandbox egress HMAC domain.
4110
+ *
4111
+ * `JUNIOR_SECRET` is the shared secret so proxy URLs cannot be forged by code
4112
+ * running inside the sandbox.
4113
+ */
4114
+ function signPayload(payload) {
4115
+ return createHmac("sha256", getSandboxEgressSecret()).update(`${SANDBOX_EGRESS_HMAC_CONTEXT}:${payload}`).digest("base64url");
4116
+ }
4117
+ function timingSafeMatch(expected, actual) {
4118
+ const expectedBuffer = Buffer.from(expected);
4119
+ const actualBuffer = Buffer.from(actual);
4120
+ if (expectedBuffer.length !== actualBuffer.length) return false;
4121
+ return timingSafeEqual(expectedBuffer, actualBuffer);
4122
+ }
4123
+ function parseSandboxEgressContext(value) {
4124
+ const result = sandboxEgressCredentialContextSchema.safeParse(value);
4125
+ if (!result.success) return;
4126
+ if (result.data.expiresAtMs <= Date.now()) return;
4127
+ return result.data;
4128
+ }
4129
+ function parseLease(value) {
4130
+ const result = sandboxEgressCredentialLeaseSchema.safeParse(value);
4131
+ if (!result.success) return;
4132
+ const expiresAtMs = Date.parse(result.data.expiresAt);
4133
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now() + LEASE_REFRESH_BUFFER_MS) return;
4134
+ return result.data;
4135
+ }
4136
+ function getSandboxEgressSecret() {
4137
+ const secret = process.env.JUNIOR_SECRET?.trim();
4138
+ if (secret) return secret;
4139
+ throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
4140
+ }
4141
+ /**
4142
+ * Create the signed Junior credential context embedded in the proxy URL.
4143
+ *
4144
+ * The token binds actor credentials to a specific sandbox egress id and a
4145
+ * short expiry. Vercel OIDC proves which VM sent a request; this token proves
4146
+ * which Junior actor and credential context that VM is allowed to use.
4147
+ */
4148
+ function createSandboxEgressCredentialToken(input) {
4149
+ const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
4150
+ const now = Date.now();
4151
+ const context = {
4152
+ ...input.conversationId ? { conversationId: input.conversationId } : void 0,
4153
+ credentials: input.credentials,
4154
+ egressId: input.egressId,
4155
+ expiresAtMs: now + ttlMs,
4156
+ contextId: randomUUID()
4157
+ };
4158
+ const payload = `${SANDBOX_EGRESS_TOKEN_VERSION}.${base64Url(JSON.stringify(context))}`;
4159
+ return `${payload}.${signPayload(payload)}`;
4160
+ }
4161
+ /**
4162
+ * Verify the signed Junior credential context from the proxy URL.
4163
+ *
4164
+ * Returning `undefined` means the token is missing, expired, malformed, or not
4165
+ * signed with the current `JUNIOR_SECRET`.
4166
+ */
4167
+ function parseSandboxEgressCredentialToken(token) {
4168
+ if (!token) return;
4169
+ const parts = token.split(".");
4170
+ if (parts.length !== 3 || parts[0] !== SANDBOX_EGRESS_TOKEN_VERSION) return;
4171
+ const encodedSession = parts[1];
4172
+ const signature = parts[2];
4173
+ if (!encodedSession || !signature) return;
4174
+ if (!timingSafeMatch(signPayload(`${parts[0]}.${encodedSession}`), signature)) return;
4175
+ try {
4176
+ return parseSandboxEgressContext(JSON.parse(fromBase64Url(encodedSession)));
4177
+ } catch {
4178
+ return;
4179
+ }
4180
+ }
4181
+ /**
4182
+ * Remember auth headers for a shared installation grant.
4183
+ *
4184
+ * User and other grants are ignored here. TTL follows the provider expiry.
4185
+ * Sandbox context expiry still decides whether a request may use the headers;
4186
+ * it does not shorten a shared installation grant for other sandboxes.
4187
+ */
4188
+ async function setSandboxEgressCredentialLease(_context, lease) {
4189
+ if (!isSharedInstallationGrant(lease.grant)) return;
4190
+ const leaseExpiresAtMs = Date.parse(lease.expiresAt);
4191
+ if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) return;
4192
+ const ttlMs = Math.max(1, leaseExpiresAtMs - Date.now());
4193
+ const state = getStateAdapter();
4194
+ await state.connect();
4195
+ await state.set(leaseKey(lease.provider, lease.grant), lease, ttlMs);
4196
+ }
4197
+ /** Load remembered auth headers for a shared installation grant. */
4198
+ async function getSandboxEgressCredentialLease(provider, grant, _context) {
4199
+ if (!isSharedInstallationGrant(grant)) return;
4200
+ const state = getStateAdapter();
4201
+ await state.connect();
4202
+ return parseLease(await state.get(leaseKey(provider, grant)));
4203
+ }
4204
+ /** Drop remembered installation headers after the upstream rejects them. */
4205
+ async function clearSandboxEgressCredentialLease(provider, grant, _context) {
4206
+ if (!isSharedInstallationGrant(grant)) return;
4207
+ const state = getStateAdapter();
4208
+ await state.connect();
4209
+ await state.delete(leaseKey(provider, grant));
4210
+ }
4211
+ /**
4212
+ * Record that credential issuance needs user authorization for this command.
4213
+ *
4214
+ * The command runner consumes this after the sandbox tool finishes so the agent
4215
+ * can present a normal authorization-required response instead of a raw proxy
4216
+ * failure.
4217
+ */
4218
+ async function setSandboxEgressAuthRequiredSignal(context, signal) {
4219
+ const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
4220
+ const state = getStateAdapter();
4221
+ await state.connect();
4222
+ await state.set(authSignalKey(context.egressId, signal.grant.access), {
4223
+ ...signal,
4224
+ createdAtMs: Date.now()
4225
+ }, ttlMs);
4226
+ }
4227
+ /**
4228
+ * Record that the provider rejected an issued credential with permission denial.
4229
+ */
4230
+ async function setSandboxEgressPermissionDeniedSignal(context, signal) {
4231
+ const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
4232
+ const state = getStateAdapter();
4233
+ await state.connect();
4234
+ await state.set(permissionSignalKey(context.egressId, signal.grant.access), {
4235
+ ...signal,
4236
+ createdAtMs: Date.now()
4237
+ }, ttlMs);
4238
+ }
4239
+ /** Remove pending auth/permission signals before or after a sandbox command. */
4240
+ async function clearSandboxEgressSignals(egressId) {
4241
+ if (!egressId) return;
4242
+ const state = getStateAdapter();
4243
+ await state.connect();
4244
+ await Promise.all([
4245
+ state.delete(authSignalKey(egressId, "read")),
4246
+ state.delete(authSignalKey(egressId, "write")),
4247
+ state.delete(permissionSignalKey(egressId, "read")),
4248
+ state.delete(permissionSignalKey(egressId, "write"))
4249
+ ]);
4250
+ }
4251
+ /**
4252
+ * Consume the auth signal produced during a sandbox command, preferring writes.
4253
+ */
4254
+ async function consumeSandboxEgressAuthRequiredSignal(egressId) {
4255
+ if (!egressId) return;
4256
+ const state = getStateAdapter();
4257
+ await state.connect();
4258
+ const [writeSignal, readSignal] = await Promise.all([state.get(authSignalKey(egressId, "write")), state.get(authSignalKey(egressId, "read"))]);
4259
+ const signal = parseSandboxEgressAuthRequiredSignal(writeSignal) ?? parseSandboxEgressAuthRequiredSignal(readSignal);
4260
+ await Promise.all([state.delete(authSignalKey(egressId, "read")), state.delete(authSignalKey(egressId, "write"))]);
4261
+ return signal;
4262
+ }
4263
+ /**
4264
+ * Consume the permission signal produced during a sandbox command, preferring writes.
4265
+ */
4266
+ async function consumeSandboxEgressPermissionDeniedSignal(egressId) {
4267
+ if (!egressId) return;
4268
+ const state = getStateAdapter();
4269
+ await state.connect();
4270
+ const [writeSignal, readSignal] = await Promise.all([state.get(permissionSignalKey(egressId, "write")), state.get(permissionSignalKey(egressId, "read"))]);
4271
+ const signal = parseSandboxEgressPermissionDeniedSignal(writeSignal) ?? parseSandboxEgressPermissionDeniedSignal(readSignal);
4272
+ await Promise.all([state.delete(permissionSignalKey(egressId, "read")), state.delete(permissionSignalKey(egressId, "write"))]);
4273
+ return signal;
4274
+ }
4275
+ // src/chat/sandbox/egress/proxy.ts
4276
+ function jsonError$1(message, status) {
4277
+ return Response.json({ error: message }, { status });
4278
+ }
4279
+ /** Return log attributes that identify one sandbox egress request. */
4280
+ function sandboxEgressAttributes(input) {
4281
+ return {
4282
+ ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : void 0,
4283
+ ...input.provider ? { "app.provider.name": input.provider } : void 0,
4284
+ ...input.host ? { "server.address": input.host } : void 0,
4285
+ ...input.method ? { "http.request.method": input.method } : void 0,
4286
+ ...input.path ? { "url.path": input.path } : void 0,
4287
+ ...input.status ? { "http.response.status_code": input.status } : void 0
4288
+ };
4289
+ }
4290
+ /** Hide the credential token in a sandbox egress proxy path before logging. */
4291
+ function redactSandboxEgressProxyPath(pathname) {
4292
+ if (pathname.startsWith(`/api/internal/sandbox-egress/`)) return `${SANDBOX_EGRESS_PROXY_PATH}/<token>`;
4293
+ return pathname;
4294
+ }
4295
+ /** Return log attributes for the proxy route and the upstream path. */
4296
+ function sandboxEgressRoutingAttributes(request, upstreamUrl) {
4297
+ const attributes = { "app.sandbox.egress.proxy_path": redactSandboxEgressProxyPath(new URL(request.url).pathname) };
4298
+ if (upstreamUrl) {
4299
+ attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
4300
+ const gitService = upstreamUrl.searchParams.get("service");
4301
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) attributes["app.sandbox.egress.git_service"] = gitService;
4302
+ }
4303
+ return attributes;
4304
+ }
4305
+ /**
4306
+ * Name the Conversation and the accountable person from the verified token.
4307
+ * Each proxy request is a new HTTP request with no run log context, so the
4308
+ * proxy binds the same fields that the run binds.
4309
+ */
4310
+ function sandboxEgressLogContext(context) {
4311
+ return {
4312
+ ...context.conversationId ? { conversationId: context.conversationId } : void 0,
4313
+ ...credentialLogContext(context.credentials)
4314
+ };
4315
+ }
4316
+ /** Continue inbound trace context only after sandbox egress verification succeeds. */
4317
+ function continueSandboxEgressTrace(request, upstreamHost, tracePropagation, credentialContext, callback) {
4318
+ const sentryTrace = request.headers.get("sentry-trace") ?? void 0;
4319
+ const baggage = request.headers.get("baggage") ?? void 0;
4320
+ const logContext = sandboxEgressLogContext(credentialContext);
4321
+ const run = () => withSpan("sandbox.egress", "http.server", logContext, async () => {
4322
+ setSentryUser(logContext.userId ? { id: logContext.userId } : void 0);
4323
+ return await callback();
4324
+ }, {
4325
+ "http.request.method": request.method,
4326
+ "url.path": redactSandboxEgressProxyPath(new URL(request.url).pathname)
4327
+ });
4328
+ if (!shouldPropagateSandboxEgressTrace(upstreamHost, tracePropagation) || !sentryTrace && !baggage) return run();
4329
+ return continueTrace({
4330
+ sentryTrace,
4331
+ baggage
4332
+ }, run);
4333
+ }
4334
+ /**
4335
+ * Run one sandbox egress request that its sandbox provider authenticated.
4336
+ *
4337
+ * This is the shared authorization boundary. It rejects upstream hosts that no
4338
+ * plugin owns, rejects signed credential contexts that do not match the
4339
+ * verified VM id, and only then asks the credentialed egress engine to issue
4340
+ * credentials and contact the upstream provider.
4341
+ */
4342
+ async function executeSandboxEgressRequest(input, deps = {}) {
4343
+ const { request, upstreamUrl } = input;
4344
+ const activeEgressId = input.egressId;
4345
+ const provider = resolveSandboxEgressProviderForHost(upstreamUrl.hostname);
4346
+ if (!provider) {
4347
+ logWarn("sandbox.egress.provider.unresolved", {
4348
+ ...sandboxEgressAttributes({
4349
+ egressId: activeEgressId,
4350
+ host: upstreamUrl.hostname,
4351
+ method: request.method,
4352
+ path: upstreamUrl.pathname,
4353
+ status: 403
4354
+ }),
4355
+ ...sandboxEgressRoutingAttributes(request, upstreamUrl)
4356
+ });
4357
+ return jsonError$1("No provider owns forwarded host", 403);
4358
+ }
4359
+ const credentialContext = parseSandboxEgressCredentialToken(input.credentialToken);
4360
+ if (!credentialContext || credentialContext.egressId !== activeEgressId) {
4361
+ logWarn("sandbox.egress.credential_context.unauthorized", {
4362
+ ...sandboxEgressAttributes({
4363
+ egressId: activeEgressId,
4364
+ host: upstreamUrl.hostname,
4365
+ method: request.method,
4366
+ path: upstreamUrl.pathname,
4367
+ provider,
4368
+ status: 403
4369
+ }),
4370
+ ...sandboxEgressRoutingAttributes(request, upstreamUrl)
4371
+ });
4372
+ return jsonError$1("Sandbox egress credential context is not authorized", 403);
4373
+ }
4374
+ return await continueSandboxEgressTrace(request, upstreamUrl.hostname, deps.tracePropagation ?? {}, credentialContext, async () => await executeCredentialedEgressRequest({
4375
+ activeEgressId,
4376
+ credentialContext,
4377
+ deps,
4378
+ provider,
4379
+ request,
4380
+ upstreamUrl
4381
+ }));
4382
+ }
4383
+ // src/chat/sandbox/egress/vercel.ts
4384
+ const OIDC_TOKEN_HEADER = "vercel-sandbox-oidc-token";
4385
+ const FORWARDED_HOST_HEADER = "vercel-forwarded-host";
4386
+ const FORWARDED_SCHEME_HEADER = "vercel-forwarded-scheme";
4387
+ const FORWARDED_PORT_HEADER = "vercel-forwarded-port";
4388
+ const FORWARDED_PATH_HEADER = "vercel-forwarded-path";
4389
+ function jsonError(message, status) {
4390
+ return Response.json({ error: message }, { status });
4391
+ }
4392
+ function forwardUrl(credentialToken) {
4393
+ const baseUrl = resolveBaseUrl();
4394
+ if (!baseUrl) throw new Error("Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)");
4395
+ return new URL(`${SANDBOX_EGRESS_PROXY_PATH}/${credentialToken}`, baseUrl).toString();
4396
+ }
4397
+ /**
4398
+ * Turn Junior's network policy into Vercel Sandbox network rules.
4399
+ *
4400
+ * Every other domain stays open through the `*` rule. Credentialed domains
4401
+ * forward to Junior's egress route, and trace domains get a header transform.
4402
+ */
4403
+ function toVercelNetworkPolicy(policy) {
4404
+ if (policy === "allow-all") return policy;
4405
+ const rules = /* @__PURE__ */ new Map();
4406
+ const credentialEgress = policy.credentialEgress;
4407
+ if (credentialEgress && credentialEgress.domains.length > 0) {
4408
+ const forwardURL = forwardUrl(credentialEgress.credentialToken);
4409
+ for (const domain of credentialEgress.domains) rules.set(domain, [{ forwardURL }]);
4410
+ }
4411
+ const traceHeaders = policy.traceHeaders;
4412
+ if (traceHeaders) for (const domain of traceHeaders.domains) rules.set(domain, [{ transform: [{ headers: traceHeaders.headers }] }]);
4413
+ return { allow: {
4414
+ "*": [],
4415
+ ...Object.fromEntries([...rules.entries()].sort(([left], [right]) => left.localeCompare(right)))
4416
+ } };
4417
+ }
4418
+ /** Read the signed credential token from the forward URL path. */
4419
+ function credentialTokenFromRequest(request) {
4420
+ const pathname = new URL(request.url).pathname;
4421
+ const prefix = `${SANDBOX_EGRESS_PROXY_PATH}/`;
4422
+ if (!pathname.startsWith(prefix)) return;
4423
+ const token = pathname.slice(prefix.length).split("/")[0];
4424
+ if (!token) return;
4425
+ try {
4426
+ return decodeURIComponent(token);
4427
+ } catch {
4428
+ return;
4429
+ }
4430
+ }
4431
+ function normalizeHost(value) {
4432
+ const trimmed = value.trim().toLowerCase();
4433
+ if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes(":")) return;
4434
+ return trimmed.replace(/\.$/, "");
4435
+ }
4436
+ function normalizeScheme(value) {
4437
+ return value.trim().toLowerCase() === "https" ? "https" : void 0;
4438
+ }
4439
+ function normalizePort(value) {
4440
+ if (!value) return;
4441
+ const trimmed = value.trim();
4442
+ if (!/^\d{1,5}$/.test(trimmed)) return;
4443
+ const port = Number.parseInt(trimmed, 10);
4444
+ return port >= 1 && port <= 65535 ? trimmed : void 0;
4445
+ }
4446
+ function sandboxIdFromPayload(payload) {
4447
+ return typeof payload.sandbox_id === "string" ? payload.sandbox_id : void 0;
4448
+ }
4449
+ function normalizedForwardedPath(path) {
4450
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("#") || /[\r\n]/.test(path)) return {
4451
+ ok: false,
4452
+ error: "Invalid forwarded path"
4453
+ };
4454
+ try {
4455
+ const url = new URL(path, "https://sandbox-forwarded.local");
4456
+ return {
4457
+ ok: true,
4458
+ path: `${url.pathname}${url.search}`
4459
+ };
4460
+ } catch {
4461
+ return {
4462
+ ok: false,
4463
+ error: "Invalid forwarded path"
4464
+ };
4465
+ }
4466
+ }
4467
+ function upstreamPath(request) {
4468
+ const forwardedPath = request.headers.get(FORWARDED_PATH_HEADER);
4469
+ if (!forwardedPath?.trim()) return {
4470
+ ok: false,
4471
+ error: "Missing forwarded path"
4472
+ };
4473
+ return normalizedForwardedPath(forwardedPath.trim());
4474
+ }
4475
+ /**
4476
+ * Rebuild the HTTPS upstream URL from Vercel forwarding headers.
4477
+ *
4478
+ * This is the proxy's wire-format boundary for forwarded requests: host, path,
4479
+ * scheme, and port must normalize before the request can reach provider egress.
4480
+ */
4481
+ function buildUpstreamUrl(request) {
4482
+ const forwardedHost = request.headers.get(FORWARDED_HOST_HEADER);
4483
+ if (!forwardedHost?.trim()) return {
4484
+ ok: false,
4485
+ error: "Missing forwarded host"
4486
+ };
4487
+ const host = normalizeHost(forwardedHost);
4488
+ if (!host) return {
4489
+ ok: false,
4490
+ error: "Invalid forwarded host"
4491
+ };
4492
+ const forwardedScheme = request.headers.get(FORWARDED_SCHEME_HEADER);
4493
+ if (!forwardedScheme?.trim()) return {
4494
+ ok: false,
4495
+ error: "Missing forwarded scheme"
4496
+ };
4497
+ const scheme = normalizeScheme(forwardedScheme);
4498
+ if (!scheme) return {
4499
+ ok: false,
4500
+ error: "Forwarded scheme must be https"
4501
+ };
4502
+ const forwardedPort = request.headers.get(FORWARDED_PORT_HEADER);
4503
+ const port = normalizePort(forwardedPort);
4504
+ if (forwardedPort && !port) return {
4505
+ ok: false,
4506
+ error: "Invalid forwarded port"
4507
+ };
4508
+ const path = upstreamPath(request);
4509
+ if (!path.ok) return {
4510
+ ok: false,
4511
+ error: path.error
4512
+ };
4513
+ try {
4514
+ return {
4515
+ ok: true,
4516
+ url: new URL(`${scheme}://${host}${port ? `:${port}` : ""}${path.path}`)
4517
+ };
4518
+ } catch {
4519
+ return {
4520
+ ok: false,
4521
+ error: "Invalid forwarded URL"
4522
+ };
4523
+ }
4524
+ }
4525
+ /**
4526
+ * Return whether a request carries the forwarding headers expected from Vercel.
4527
+ *
4528
+ * This is only a cheap classifier for routing into the proxy handler. Real
4529
+ * authentication happens in `proxyVercelSandboxEgressRequest`, which verifies
4530
+ * the OIDC token, and in `executeSandboxEgressRequest`, which verifies the
4531
+ * signed Junior credential context.
4532
+ */
4533
+ function isVercelSandboxEgressRequest(request) {
4534
+ return Boolean(request.headers.get(OIDC_TOKEN_HEADER)?.trim() && request.headers.get(FORWARDED_HOST_HEADER)?.trim() && request.headers.get(FORWARDED_SCHEME_HEADER)?.trim());
4535
+ }
4536
+ /**
4537
+ * Handle one sandbox egress request that Vercel's network layer forwarded.
4538
+ *
4539
+ * This is the Vercel side of the proxy boundary. It rejects requests without a
4540
+ * valid Vercel Sandbox OIDC token and rejects invalid forwarded URLs. Then it
4541
+ * hands the verified VM id, upstream URL, and credential token to the shared
4542
+ * `executeSandboxEgressRequest`.
4543
+ */
4544
+ async function proxyVercelSandboxEgressRequest(request, deps = {}) {
4545
+ const oidcToken = request.headers.get(OIDC_TOKEN_HEADER)?.trim();
4546
+ if (!oidcToken) return jsonError("Missing Vercel Sandbox OIDC token", 401);
4547
+ let oidcPayload;
4548
+ try {
4549
+ oidcPayload = await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(oidcToken);
4550
+ } catch (error) {
4551
+ logWarn("sandbox.egress.oidc_verification.failed", { "app.sandbox.oidc_error": error instanceof Error ? error.message : String(error) });
4552
+ return jsonError("Invalid Vercel Sandbox OIDC token", 401);
4553
+ }
4554
+ const activeEgressId = sandboxIdFromPayload(oidcPayload);
4555
+ if (!activeEgressId) {
4556
+ logWarn("sandbox.egress.oidc_session.missing", {
4557
+ "http.request.method": request.method,
4558
+ "url.path": redactSandboxEgressProxyPath(new URL(request.url).pathname)
4559
+ });
4560
+ return jsonError("Vercel Sandbox OIDC token did not include sandbox_id", 401);
4561
+ }
4562
+ const upstreamResult = buildUpstreamUrl(request);
4563
+ if (!upstreamResult.ok) {
4564
+ logWarn("sandbox.egress.upstream_url.invalid", {
4565
+ ...sandboxEgressAttributes({
4566
+ egressId: activeEgressId,
4567
+ method: request.method,
4568
+ path: redactSandboxEgressProxyPath(new URL(request.url).pathname),
4569
+ status: 400
4570
+ }),
4571
+ ...sandboxEgressRoutingAttributes(request)
4572
+ });
4573
+ return jsonError(upstreamResult.error, 400);
4574
+ }
4575
+ return await executeSandboxEgressRequest({
4576
+ request,
4577
+ egressId: activeEgressId,
4578
+ upstreamUrl: upstreamResult.url,
4579
+ credentialToken: credentialTokenFromRequest(request)
4580
+ }, deps);
4581
+ }
4582
+ // src/chat/sandbox/http-error-details.ts
4583
+ const DEFAULT_PREVIEW_LIMIT = 512;
4584
+ function toTrimmedString(value, maxChars) {
4585
+ if (typeof value !== "string") return;
4586
+ const normalized = value.trim();
4587
+ if (!normalized) return;
4588
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}…` : normalized;
4589
+ }
4590
+ /** Extract structured HTTP error metadata (status, headers, body preview) for logging. */
4591
+ function extractHttpErrorDetails(error, options = {}) {
4592
+ const prefix = options.attributePrefix ?? "app.http_error";
4593
+ const previewLimit = options.previewLimit ?? DEFAULT_PREVIEW_LIMIT;
4594
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
4595
+ const err = error ?? {};
4596
+ const attributes = {
4597
+ "error.type": normalizedError.name || "Error",
4598
+ "exception.message": toTrimmedString(normalizedError.message, previewLimit) ?? "HTTP error"
4599
+ };
4600
+ const response = err.response;
4601
+ const statusCode = typeof response?.status === "number" ? response.status : void 0;
4602
+ const statusText = toTrimmedString(response?.statusText, previewLimit);
4603
+ const responseUrl = toTrimmedString(response?.url, previewLimit);
4604
+ const responseText = toTrimmedString(err.text, previewLimit);
4605
+ const responseJson = toTrimmedString(err.json && typeof err.json === "object" ? JSON.stringify(err.json) : void 0, previewLimit);
4606
+ const contentType = toTrimmedString(response?.headers?.get?.("content-type"), previewLimit);
4607
+ const requestIdHeader = toTrimmedString(response?.headers?.get?.("x-request-id"), previewLimit);
4608
+ const vercelIdHeader = toTrimmedString(response?.headers?.get?.("x-vercel-id"), previewLimit);
4609
+ const requestId = requestIdHeader ?? vercelIdHeader;
4610
+ if (statusCode !== void 0) attributes["http.response.status_code"] = statusCode;
4611
+ if (statusText) attributes[`${prefix}.status_text`] = statusText;
4612
+ if (responseUrl) attributes["url.full"] = responseUrl;
4613
+ if (contentType) attributes["http.response.header.content-type"] = [contentType];
4614
+ if (requestIdHeader) attributes["http.response.header.x-request-id"] = [requestIdHeader];
4615
+ if (vercelIdHeader) attributes["http.response.header.x-vercel-id"] = [vercelIdHeader];
4616
+ if (responseText) {
4617
+ attributes[`${prefix}.response_text_preview`] = responseText;
4618
+ attributes[`${prefix}.response_text_length`] = responseText.length;
4619
+ }
4620
+ if (responseJson) attributes[`${prefix}.response_json_preview`] = responseJson;
4621
+ const summaryParts = [];
4622
+ if (statusCode !== void 0) summaryParts.push(`status=${statusCode}`);
4623
+ if (statusText) summaryParts.push(`statusText=${statusText}`);
4624
+ if (responseUrl) summaryParts.push(`url=${responseUrl}`);
4625
+ if (requestId) summaryParts.push(`requestId=${requestId}`);
4626
+ for (const field of options.extraFields ?? []) {
4627
+ const value = toTrimmedString(err[field.sourceKey], previewLimit);
4628
+ if (!value) continue;
4629
+ attributes[`${prefix}.${field.attributeKey}`] = value;
4630
+ summaryParts.push(`${field.summaryKey ?? field.attributeKey}=${value}`);
4631
+ }
4632
+ if (responseJson) summaryParts.push(`json=${responseJson}`);
4633
+ else if (responseText) summaryParts.push(`text=${responseText}`);
4634
+ const searchableText = `${normalizedError.message} ${responseText ?? ""} ${responseJson ?? ""}`.toLowerCase();
4635
+ return {
4636
+ attributes,
4637
+ summary: summaryParts.join(", "),
4638
+ searchableText
4639
+ };
4640
+ }
4641
+ // src/chat/sandbox/snapshot/not-ready-error.ts
4642
+ /** Report that a Workspace snapshot is not ready to use. */
4643
+ var WorkspaceSnapshotNotReadyError = class extends Error {
4644
+ workspaceName;
4645
+ constructor(workspaceName) {
4646
+ super(`Workspace ${workspaceName} snapshot is not ready`);
4647
+ this.name = "WorkspaceSnapshotNotReadyError";
4648
+ this.workspaceName = workspaceName;
4649
+ }
4650
+ };
4651
+ /** Return WorkspaceSnapshotNotReadyError from this error or its causes. */
4652
+ function getWorkspaceSnapshotNotReadyError(error) {
4653
+ const seen = /* @__PURE__ */ new Set();
4654
+ let current = error;
4655
+ while (current instanceof Error && !seen.has(current)) {
4656
+ if (current instanceof WorkspaceSnapshotNotReadyError) return current;
4657
+ seen.add(current);
4658
+ current = current.cause;
4659
+ }
4660
+ }
4661
+ /** True when this error or a cause is WorkspaceSnapshotNotReadyError. */
4662
+ function isWorkspaceSnapshotNotReadyError(error) {
4663
+ return getWorkspaceSnapshotNotReadyError(error) !== void 0;
4664
+ }
4665
+ /** User message when a Workspace sandbox is still preparing. */
4666
+ function workspaceSnapshotNotReadyUserMessage(error) {
4667
+ return `The ${error.workspaceName} workspace is still preparing its sandbox. Wait for that preparation to finish, then try again.`;
4668
+ }
4669
+ // src/chat/sandbox/errors.ts
4670
+ const SANDBOX_ERROR_FIELDS = [{
4671
+ sourceKey: "sandboxId",
4672
+ attributeKey: "sandbox_id",
4673
+ summaryKey: "sandboxId"
4674
+ }];
4675
+ /** Extract HTTP error details from a sandbox error for span data and checks. */
4676
+ function getSandboxErrorDetails(error) {
4677
+ return extractHttpErrorDetails(error, {
4678
+ attributePrefix: "app.sandbox.api_error",
4679
+ extraFields: [...SANDBOX_ERROR_FIELDS]
4680
+ });
4681
+ }
4682
+ /** Return true when any error in the `cause` chain matches the predicate. */
4683
+ function findInErrorChain(error, predicate) {
4684
+ const seen = /* @__PURE__ */ new Set();
4685
+ let current = error;
4686
+ while (current && !seen.has(current)) {
4687
+ if (predicate(current)) return true;
4688
+ seen.add(current);
4689
+ current = typeof current === "object" ? current.cause : void 0;
4690
+ }
4691
+ return false;
4692
+ }
4693
+ function getFirstErrorMessage(error) {
4694
+ const seen = /* @__PURE__ */ new Set();
4695
+ let current = error;
4696
+ while (current && !seen.has(current)) {
4697
+ if (current instanceof Error) {
4698
+ const message = current.message.trim();
4699
+ if (message) return message;
4700
+ }
4701
+ seen.add(current);
4702
+ current = typeof current === "object" ? current.cause : void 0;
4703
+ }
4704
+ }
4705
+ /** Detect sandbox filesystem operations that can safely ignore existing directories. */
4706
+ function isAlreadyExistsError(error) {
4707
+ const details = getSandboxErrorDetails(error);
4708
+ return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
4709
+ }
4710
+ /** Detect cancellation so a durable builder stays available for check-in. */
4711
+ function isAbortError(error) {
4712
+ return findInErrorChain(error, (candidate) => {
4713
+ if (typeof DOMException !== "undefined" && candidate instanceof DOMException) return candidate.name === "AbortError";
4714
+ return candidate instanceof Error && candidate.name === "AbortError";
4715
+ });
4716
+ }
4717
+ /** Wrap setup failures. Keep WorkspaceSnapshotNotReadyError as-is. */
4718
+ function wrapSandboxSetupError(error) {
4719
+ const notReady = getWorkspaceSnapshotNotReadyError(error);
4720
+ if (notReady) return notReady;
4721
+ try {
4722
+ const details = getSandboxErrorDetails(error);
4723
+ if (details.summary) return new Error(`sandbox setup failed (${details.summary})`, { cause: error });
4724
+ } catch {}
4725
+ let causeMessage;
4726
+ try {
4727
+ causeMessage = getFirstErrorMessage(error);
4728
+ } catch (cause) {
4729
+ causeMessage = cause instanceof Error ? cause.message : void 0;
4730
+ }
4731
+ if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
4732
+ const oneLine = causeMessage.replace(/\s+/g, " ").trim();
4733
+ return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
4734
+ }
4735
+ return new Error("sandbox setup failed", { cause: error });
4736
+ }
4737
+ /** Record span data and throw one stable sandbox operation error. */
4738
+ function throwSandboxOperationError(action, error, includeMissingPath = false) {
4739
+ const details = getSandboxErrorDetails(error);
4740
+ setSpanAttributes({
4741
+ ...details.attributes,
4742
+ ...includeMissingPath ? { "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent") } : void 0,
4743
+ "app.sandbox.success": false
4744
+ });
4745
+ throw new Error(details.summary ? `${action} failed (${details.summary})` : `${action} failed`, { cause: error });
4746
+ }
4747
+ // src/chat/sandbox/vercel/errors.ts
4748
+ function isRecord$1(value) {
4749
+ return typeof value === "object" && value !== null;
4750
+ }
4751
+ function searchableText(error) {
4752
+ const details = getSandboxErrorDetails(error);
4753
+ return `${details.searchableText} ${details.summary}`.toLowerCase();
4754
+ }
4755
+ function isInvalidSandboxSessionError(error) {
4756
+ if (!isRecord$1(error)) return false;
4757
+ if (isRecord$1(error.json) && error.json.invalidToken === true) return true;
4758
+ const response = error.response;
4759
+ return isRecord$1(response) && response.status === 403 && typeof response.url === "string" && response.url.includes("/sandboxes/sessions");
4760
+ }
4761
+ function isUnavailable(error) {
4762
+ return findInErrorChain(error, (candidate) => {
4763
+ if (isInvalidSandboxSessionError(candidate)) return true;
4764
+ const searchable = searchableText(candidate);
4765
+ return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available") || searchable.includes("stream was closed and is not accepting commands");
4766
+ });
4767
+ }
4768
+ function isMissing(error) {
4769
+ return findInErrorChain(error, (candidate) => {
4770
+ const searchable = searchableText(candidate);
4771
+ return searchable.includes("status=404") || searchable.includes("status code 404") || searchable.includes("snapshot_not_found");
4772
+ });
4773
+ }
4774
+ function isSnapshotMissing(error) {
4775
+ const searchable = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
4776
+ return searchable.includes("snapshot") && (searchable.includes("not found") || searchable.includes("unknown") || searchable.includes("404"));
4777
+ }
4778
+ function isSnapshotting(error) {
4779
+ return findInErrorChain(error, (candidate) => {
4780
+ const searchable = searchableText(candidate);
4781
+ return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
4782
+ });
4783
+ }
4784
+ function isTransient(error) {
4785
+ return findInErrorChain(error, (candidate) => {
4786
+ const searchable = searchableText(candidate);
4787
+ if (searchable.includes("internal_server_error") || searchable.includes("status=500") || searchable.includes("status code 500") || searchable.includes("status=502") || searchable.includes("status code 502") || searchable.includes("status=503") || searchable.includes("status code 503") || searchable.includes("status=504") || searchable.includes("status code 504")) return true;
4788
+ if (!isRecord$1(candidate)) return false;
4789
+ const response = candidate.response;
4790
+ return isRecord$1(response) && typeof response.status === "number" && response.status >= 500 && response.status < 600;
4791
+ });
4792
+ }
4793
+ function isCommandStreamInterrupted(error) {
4794
+ return findInErrorChain(error, (candidate) => {
4795
+ if (!(candidate instanceof Error)) return false;
4796
+ return candidate.name === "StreamError" && candidate.message.toLowerCase().includes("stream ended before command finished");
4797
+ });
4798
+ }
4799
+ /** Map Vercel Sandbox API and SDK errors to the shared sandbox error cases. */
4800
+ const vercelSandboxErrors = {
4801
+ isUnavailable,
4802
+ isMissing,
4803
+ isSnapshotMissing,
4804
+ isSnapshotting,
4805
+ isTransient,
4806
+ isCommandStreamInterrupted
4807
+ };
4808
+ // src/chat/sandbox/vercel/provider.ts
4809
+ function credentialOptions() {
4810
+ return getVercelSandboxCredentials() ?? {};
4811
+ }
4812
+ /** Bind one abort signal to every API request made by a Sandbox client. */
4813
+ function scopedFetchOptions(signal) {
4814
+ if (!signal) return {};
4815
+ return { fetch: (input, init) => {
4816
+ const requestSignal = init?.signal ?? (input instanceof Request ? input.signal : void 0);
4817
+ return globalThis.fetch(input, {
4818
+ ...init,
4819
+ signal: requestSignal ? AbortSignal.any([requestSignal, signal]) : signal
4820
+ });
4821
+ } };
4822
+ }
4823
+ /** Pin Junior's sandbox contract to one Vercel session without SDK replay. */
4824
+ function adaptSession(sandbox, options) {
4825
+ const session = sandbox.currentSession();
4826
+ const fileSystem = new FileSystem(session);
4827
+ const run = async (operation) => {
4828
+ try {
4829
+ return await operation();
4830
+ } catch (error) {
4831
+ if (vercelSandboxErrors.isUnavailable(error)) options?.onUnavailable?.(session.sessionId);
4832
+ throw error;
4833
+ }
4834
+ };
4835
+ return {
4836
+ sandboxId: sandbox.name,
4837
+ sessionId: session.sessionId,
4838
+ fs: {
4839
+ readFile: async (filePath, readOptions) => await run(async () => await fileSystem.readFile(filePath, readOptions)),
4840
+ writeFile: async (filePath, content, writeOptions) => await run(async () => await fileSystem.writeFile(filePath, content, writeOptions)),
4841
+ readdir: async (filePath, readOptions) => await run(async () => await fileSystem.readdir(filePath, readOptions)),
4842
+ stat: async (filePath, statOptions) => await run(async () => await fileSystem.stat(filePath, statOptions))
4843
+ },
4844
+ extendTimeout(duration) {
4845
+ return run(async () => await session.extendTimeout(duration));
4846
+ },
4847
+ mkDir(path) {
4848
+ return run(async () => await session.mkDir(path));
4849
+ },
4850
+ readFileToBuffer(input) {
4851
+ return run(async () => await session.readFileToBuffer(input));
4852
+ },
4853
+ async runCommand(input) {
4854
+ const result = await run(async () => await session.runCommand(input));
4855
+ const [stdout, stderr] = await Promise.all([run(async () => await result.stdout()), run(async () => await result.stderr())]);
4856
+ return {
4857
+ exitCode: result.exitCode,
4858
+ stdout,
4859
+ stderr
4860
+ };
4861
+ },
4862
+ snapshot(snapshotOptions) {
4863
+ return run(async () => {
4864
+ return { snapshotId: (await session.snapshot(snapshotOptions)).snapshotId };
4865
+ });
4866
+ },
4867
+ stop() {
4868
+ return run(async () => await session.stop());
4869
+ },
4870
+ update(params) {
4871
+ return run(async () => await session.update(params.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(params.networkPolicy) } : {}));
4872
+ },
4873
+ writeFiles(files) {
4874
+ return run(async () => await session.writeFiles(files));
4875
+ }
4876
+ };
4877
+ }
4878
+ /** Create a Sandbox and return a session pinned to its first VM session. */
4879
+ async function createSandboxSession(input) {
4880
+ const shared = {
4881
+ timeout: input.timeoutMs,
4882
+ ...input.name ? { name: input.name } : void 0,
4883
+ ...input.persistent !== void 0 ? { persistent: input.persistent } : void 0,
4884
+ ...input.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(input.networkPolicy) } : void 0,
4885
+ ...input.resources ? { resources: input.resources } : void 0,
4886
+ ...input.signal ? { signal: input.signal } : void 0,
4887
+ ...credentialOptions(),
4888
+ ...scopedFetchOptions(input.scopeSignal)
4889
+ };
4890
+ return adaptSession(await Sandbox.create(input.snapshotId ? {
4891
+ ...shared,
4892
+ source: {
4893
+ type: "snapshot",
4894
+ snapshotId: input.snapshotId
4895
+ }
4896
+ } : {
4897
+ ...shared,
4898
+ ...input.runtime ? { runtime: input.runtime } : void 0
4899
+ }), input);
4900
+ }
4901
+ /** Resume a named Sandbox and return a session pinned to its VM session. */
4902
+ async function getSandboxSession(input) {
4903
+ return adaptSession(await Sandbox.get({
4904
+ name: input.name,
4905
+ resume: true,
4906
+ ...credentialOptions(),
4907
+ ...scopedFetchOptions(input.scopeSignal)
4908
+ }), input);
4909
+ }
4910
+ /** Create the named, persistent Sandbox that owns one Workspace snapshot build. */
4911
+ async function createWorkspaceSnapshotBuilder(input) {
4912
+ await Sandbox.create({
4913
+ name: input.name,
4914
+ persistent: true,
4915
+ timeout: input.timeoutMs,
4916
+ runtime: input.runtime,
4917
+ signal: input.signal,
4918
+ ...credentialOptions(),
4919
+ ...input.resources ? { resources: input.resources } : void 0
4920
+ });
4921
+ }
2974
4922
  /** Resume the named Sandbox that owns one Workspace snapshot build. */
2975
4923
  async function getWorkspaceSnapshotBuilder(name, signal) {
2976
- const credentials = getVercelSandboxCredentials();
2977
- return await Sandbox.get({
4924
+ const sandbox = await Sandbox.get({
2978
4925
  name,
2979
4926
  resume: true,
2980
4927
  signal,
2981
- ...credentials ?? {}
4928
+ ...credentialOptions()
2982
4929
  });
4930
+ return {
4931
+ session: () => adaptSession(sandbox),
4932
+ async startDetachedCommand(input) {
4933
+ return (await sandbox.runCommand({
4934
+ ...input,
4935
+ detached: true
4936
+ })).cmdId;
4937
+ },
4938
+ getCommand: (commandId, options) => sandbox.getCommand(commandId, options),
4939
+ async snapshot(options) {
4940
+ const snapshot = await sandbox.snapshot(options);
4941
+ return {
4942
+ snapshotId: snapshot.snapshotId,
4943
+ ...typeof snapshot.sizeBytes === "number" ? { sizeBytes: snapshot.sizeBytes } : void 0
4944
+ };
4945
+ }
4946
+ };
2983
4947
  }
2984
4948
  /** Permanently delete named Workspace snapshot builders without resuming them. */
2985
4949
  async function deleteWorkspaceSnapshotBuilders(names) {
2986
- const credentials = getVercelSandboxCredentials();
2987
4950
  let hasError = false;
2988
4951
  let firstError;
2989
4952
  for (const name of new Set(names)) try {
2990
4953
  await (await Sandbox.get({
2991
4954
  name,
2992
4955
  resume: false,
2993
- ...credentials ?? {}
4956
+ ...credentialOptions()
2994
4957
  })).delete();
2995
4958
  } catch (error) {
2996
- if (!isSandboxMissingError(error)) {
4959
+ if (!vercelSandboxErrors.isMissing(error)) {
2997
4960
  if (!hasError) firstError = error;
2998
4961
  hasError = true;
2999
4962
  }
3000
4963
  }
3001
4964
  if (hasError) throw firstError;
3002
4965
  }
4966
+ /** Vercel Sandbox implementation of the sandbox provider contract. */
4967
+ const vercelSandboxProvider = {
4968
+ errors: vercelSandboxErrors,
4969
+ createSession: createSandboxSession,
4970
+ getSession: getSandboxSession,
4971
+ createWorkspaceSnapshotBuilder,
4972
+ getWorkspaceSnapshotBuilder,
4973
+ deleteWorkspaceSnapshotBuilders
4974
+ };
4975
+ // src/chat/sandbox/provider.ts
4976
+ /** Return the sandbox provider for this deployment. */
4977
+ function getSandboxProvider() {
4978
+ return vercelSandboxProvider;
4979
+ }
4980
+ // src/chat/sandbox/guest-docker.ts
4981
+ /** Docker daemon config for Vercel sandbox guests (nested containers). */
4982
+ const DOCKER_DAEMON_JSON = `{
4983
+ "storage-driver": "overlay2",
4984
+ "live-restore": false
4985
+ }
4986
+ `;
4987
+ /**
4988
+ * Start dockerd when the socket is missing and wait until `docker info` works.
4989
+ * Re-execs through sudo so the sandbox user can call it without root.
4990
+ */
4991
+ const DOCKER_ENSURE_SCRIPT = `#!/bin/sh
4992
+ set -eu
4993
+
4994
+ if [ "$(id -u)" -ne 0 ]; then
4995
+ exec sudo -n "$0" "$@"
4996
+ fi
4997
+
4998
+ SOCK=/var/run/docker.sock
4999
+ PIDFILE=/var/run/docker.pid
5000
+ LOG=/var/log/junior-dockerd.log
5001
+
5002
+ is_ready() {
5003
+ [ -S "$SOCK" ] && /usr/bin/docker info >/dev/null 2>&1
5004
+ }
5005
+
5006
+ if is_ready; then
5007
+ chmod 666 "$SOCK" 2>/dev/null || true
5008
+ exit 0
5009
+ fi
5010
+
5011
+ mkdir -p /etc/docker /var/lib/docker /var/run /var/log
5012
+ if [ ! -f /etc/docker/daemon.json ]; then
5013
+ cat > /etc/docker/daemon.json <<'EOF'
5014
+ {
5015
+ "storage-driver": "overlay2",
5016
+ "live-restore": false
5017
+ }
5018
+ EOF
5019
+ fi
5020
+
5021
+ if [ -f "$PIDFILE" ]; then
5022
+ oldpid="$(cat "$PIDFILE" 2>/dev/null || true)"
5023
+ if [ -n "\${oldpid:-}" ] && ! kill -0 "$oldpid" 2>/dev/null; then
5024
+ rm -f "$PIDFILE"
5025
+ fi
5026
+ fi
5027
+
5028
+ if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
5029
+ # dockerd must outlive this helper; nohup + background is intentional.
5030
+ nohup dockerd --host="unix://$SOCK" --pidfile="$PIDFILE" >"$LOG" 2>&1 &
5031
+ fi
5032
+
5033
+ i=0
5034
+ while [ "$i" -lt 50 ]; do
5035
+ if is_ready; then
5036
+ chmod 666 "$SOCK" 2>/dev/null || true
5037
+ exit 0
5038
+ fi
5039
+ i=$((i + 1))
5040
+ sleep 0.1
5041
+ done
5042
+
5043
+ echo "junior-ensure-docker: dockerd failed to become ready" >&2
5044
+ if [ -f "$LOG" ]; then
5045
+ tail -n 50 "$LOG" >&2 || true
5046
+ fi
5047
+ exit 1
5048
+ `;
5049
+ /** PATH wrapper so plain `docker` starts the daemon on first use. */
5050
+ const DOCKER_CLI_WRAPPER_SCRIPT = `#!/bin/sh
5051
+ set -eu
5052
+ if command -v junior-ensure-docker >/dev/null 2>&1; then
5053
+ junior-ensure-docker
5054
+ fi
5055
+ exec /usr/bin/docker "$@"
5056
+ `;
5057
+ /** PATH wrapper so plain `docker-compose` starts the daemon on first use. */
5058
+ const DOCKER_COMPOSE_CLI_WRAPPER_SCRIPT = `#!/bin/sh
5059
+ set -eu
5060
+ if command -v junior-ensure-docker >/dev/null 2>&1; then
5061
+ junior-ensure-docker
5062
+ fi
5063
+ exec /usr/local/libexec/junior-docker-compose "$@"
5064
+ `;
5065
+ /**
5066
+ * Best-effort dockerd start during sandbox prepare.
5067
+ * Missing helper is a no-op so tests and base sandboxes stay green.
5068
+ */
5069
+ async function ensureDockerDaemon(sandbox, signal) {
5070
+ await runNonInteractiveCommand(sandbox, {
5071
+ cmd: "sh",
5072
+ args: ["-c", "command -v junior-ensure-docker >/dev/null 2>&1 && junior-ensure-docker || true"],
5073
+ signal
5074
+ });
5075
+ }
5076
+ // src/chat/sandbox/runtime-dependencies.ts
5077
+ const GLOBAL_RUNTIME_DEPENDENCIES = [
5078
+ {
5079
+ type: "system",
5080
+ package: "docker"
5081
+ },
5082
+ {
5083
+ type: "system",
5084
+ package: "spal-release"
5085
+ },
5086
+ {
5087
+ type: "system",
5088
+ package: "ripgrep"
5089
+ }
5090
+ ];
5091
+ function writeFileCommand(path, contents, mode = "0644") {
5092
+ return [`printf '%s' '${Buffer.from(contents, "utf8").toString("base64")}' | base64 -d > ${path}`, `chmod ${mode} ${path}`].join("\n");
5093
+ }
5094
+ function writeExecutableCommand(path, contents) {
5095
+ return writeFileCommand(path, contents, "0755");
5096
+ }
5097
+ const GLOBAL_RUNTIME_POSTINSTALL = [{
5098
+ cmd: "sh",
5099
+ args: ["-c", [
5100
+ "set -eu",
5101
+ "case \"$(uname -m)\" in",
5102
+ " x86_64) asset=\"x86_64\"; sha256=\"7af95166a730b87e172d4fc9aefea8725d3c6c7327d59149267b452114ddb7d4\" ;;",
5103
+ " aarch64) asset=\"aarch64\"; sha256=\"49082844b87f03cdcd5f5bbef1ba8c9c897b7a2dfb80cea18d61ec8ca6117e0c\" ;;",
5104
+ " *) echo \"unsupported Docker Compose architecture: $(uname -m)\" >&2; exit 1 ;;",
5105
+ "esac",
5106
+ "tmp=\"$(mktemp)\"",
5107
+ "trap 'rm -f \"$tmp\"' EXIT",
5108
+ "curl --fail --location --silent --show-error \"https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-linux-${asset}\" --output \"$tmp\"",
5109
+ "echo \"${sha256} ${tmp}\" | sha256sum --check --status",
5110
+ "install -D -m 0755 \"$tmp\" /usr/local/libexec/junior-docker-compose",
5111
+ "mkdir -p /usr/local/lib/docker/cli-plugins /usr/local/libexec /etc/docker /usr/local/bin",
5112
+ `mkdir -p ${SANDBOX_WORKSPACE_ROOT}/.junior/bin`,
5113
+ writeFileCommand("/etc/docker/daemon.json", DOCKER_DAEMON_JSON),
5114
+ writeExecutableCommand("/usr/local/bin/junior-ensure-docker", DOCKER_ENSURE_SCRIPT),
5115
+ "ln -sfn /usr/local/libexec/junior-docker-compose /usr/local/lib/docker/cli-plugins/docker-compose",
5116
+ writeExecutableCommand("/usr/local/bin/docker-compose", DOCKER_COMPOSE_CLI_WRAPPER_SCRIPT),
5117
+ writeExecutableCommand(`${SANDBOX_WORKSPACE_ROOT}/.junior/bin/docker`, DOCKER_CLI_WRAPPER_SCRIPT),
5118
+ writeExecutableCommand(`${SANDBOX_WORKSPACE_ROOT}/.junior/bin/docker-compose`, DOCKER_COMPOSE_CLI_WRAPPER_SCRIPT),
5119
+ writeExecutableCommand("/usr/local/bin/docker", DOCKER_CLI_WRAPPER_SCRIPT)
5120
+ ].join("\n")],
5121
+ sudo: true
5122
+ }];
5123
+ // src/chat/sandbox/snapshot/profile.ts
5124
+ const VERSION = 1;
5125
+ const DEFAULT_FLOATING_MAX_AGE_MS = 6048e5;
5126
+ function isExactNpmVersion(version) {
5127
+ return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
5128
+ }
5129
+ function isFloating(dep) {
5130
+ return dep.type === "npm" && !isExactNpmVersion(dep.version);
5131
+ }
5132
+ /**
5133
+ * Merge dependencies for one global install, rejecting competing npm versions.
5134
+ */
5135
+ function mergeDependencies(dependencies) {
5136
+ const seen = /* @__PURE__ */ new Set();
5137
+ const npmVersions = /* @__PURE__ */ new Map();
5138
+ const merged = [];
5139
+ for (const dependency of dependencies) {
5140
+ const key = dependency.type === "npm" ? `${dependency.type}:${dependency.package}:${dependency.version}` : "package" in dependency ? `${dependency.type}:package:${dependency.package}` : `${dependency.type}:url:${dependency.url}:${dependency.sha256}`;
5141
+ if (seen.has(key)) continue;
5142
+ seen.add(key);
5143
+ if (dependency.type === "npm") {
5144
+ const version = npmVersions.get(dependency.package);
5145
+ if (version !== void 0 && version !== dependency.version) throw new Error(`Conflicting runtime dependency versions for ${dependency.package}: ${version} and ${dependency.version}`);
5146
+ npmVersions.set(dependency.package, dependency.version);
5147
+ }
5148
+ merged.push(dependency);
5149
+ }
5150
+ return merged;
5151
+ }
5152
+ function floatingMaxAgeMs() {
5153
+ const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
5154
+ if (!raw?.trim()) return DEFAULT_FLOATING_MAX_AGE_MS;
5155
+ const parsed = Number.parseInt(raw, 10);
5156
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_FLOATING_MAX_AGE_MS;
5157
+ }
5158
+ /** Locale-independent string order for stable profile hashes. */
5159
+ function compareCodePoints(left, right) {
5160
+ if (left < right) return -1;
5161
+ if (left > right) return 1;
5162
+ return 0;
5163
+ }
5164
+ function workspaceRecipe(workspace) {
5165
+ const repos = [...workspace.repos].map(({ provider, repo }) => ({
5166
+ provider,
5167
+ repo
5168
+ })).sort((left, right) => {
5169
+ const provider = compareCodePoints(left.provider, right.provider);
5170
+ if (provider !== 0) return provider;
5171
+ return compareCodePoints(left.repo, right.repo);
5172
+ });
5173
+ return {
5174
+ id: workspace.id,
5175
+ repos,
5176
+ setupScript: workspace.setupScript
5177
+ };
5178
+ }
5179
+ /** Build the complete profile that selects one reusable sandbox snapshot. */
5180
+ function create(runtime, workspace) {
5181
+ const dependencies = mergeDependencies([...GLOBAL_RUNTIME_DEPENDENCIES, ...pluginCatalogRuntime.getRuntimeDependencies()]);
5182
+ const pluginPostinstall = pluginCatalogRuntime.getRuntimePostinstall();
5183
+ const postinstall = [...GLOBAL_RUNTIME_POSTINSTALL, ...pluginPostinstall];
5184
+ if (dependencies.length === 0 && postinstall.length === 0 && !workspace) return null;
5185
+ const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
5186
+ const floating = dependencies.some((dependency) => isFloating(dependency)) || pluginPostinstall.length > 0 || Boolean(workspace);
5187
+ return {
5188
+ hash: createHash("sha256").update(JSON.stringify({
5189
+ version: VERSION,
5190
+ runtime,
5191
+ rebuildEpoch,
5192
+ dependencies,
5193
+ postinstall,
5194
+ ...workspace ? { workspace: workspaceRecipe(workspace) } : void 0
5195
+ })).digest("hex"),
5196
+ dependencyCount: dependencies.length,
5197
+ floating,
5198
+ dependencies,
5199
+ postinstall
5200
+ };
5201
+ }
5202
+ /** Return the current dependency profile hash without building its snapshot. */
5203
+ function hash(runtime, workspace) {
5204
+ return create(runtime, workspace)?.hash;
5205
+ }
5206
+ /** Decide whether a cached snapshot has outlived a floating profile. */
5207
+ function isStale(profile, createdAtMs) {
5208
+ if (!profile.floating) return false;
5209
+ const maxAgeMs = floatingMaxAgeMs();
5210
+ return maxAgeMs === 0 || Date.now() - createdAtMs > maxAgeMs;
5211
+ }
3003
5212
  // src/chat/sandbox/snapshot/runtime.ts
3004
5213
  /** Runtime used to build and start reusable Sandbox snapshots. */
3005
5214
  const SANDBOX_RUNTIME = "node22";
@@ -3244,7 +5453,7 @@ function isUniqueViolation(error) {
3244
5453
  async function cleanupSnapshotBuilders(workspaceId, names) {
3245
5454
  if (names.length === 0) return;
3246
5455
  try {
3247
- await deleteWorkspaceSnapshotBuilders(names);
5456
+ await getSandboxProvider().deleteWorkspaceSnapshotBuilders(names);
3248
5457
  } catch (error) {
3249
5458
  logException(error, "sandbox.workspace_snapshot.builder.delete_failed", {
3250
5459
  "app.workspace.id": workspaceId,
@@ -3497,7 +5706,7 @@ const systemPromptMessageArraySchema = z.array(promptMessageSchema);
3497
5706
  function isRecord(value) {
3498
5707
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
3499
5708
  }
3500
- function basePluginContext(plugin) {
5709
+ function basePluginContext$1(plugin) {
3501
5710
  const name = plugin.manifest.name;
3502
5711
  return {
3503
5712
  plugin: { name },
@@ -3512,7 +5721,7 @@ function pluginToolNamespace(pluginName) {
3512
5721
  return `${first}${rest.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("")}`;
3513
5722
  }
3514
5723
  function systemPromptPluginContext(plugin) {
3515
- return { ...basePluginContext(plugin) };
5724
+ return { ...basePluginContext$1(plugin) };
3516
5725
  }
3517
5726
  /** Bind Source, Destination, and Actor for one plugin invocation. */
3518
5727
  function pluginInvocationContext(context) {
@@ -3557,7 +5766,7 @@ function pluginInvocationContext(context) {
3557
5766
  }
3558
5767
  function invocationPluginContext(plugin, context, turnId) {
3559
5768
  return {
3560
- ...basePluginContext(plugin),
5769
+ ...basePluginContext$1(plugin),
3561
5770
  conversationId: context.conversationId,
3562
5771
  locationId: context.locationId,
3563
5772
  embedder: createPluginEmbedder(plugin.manifest.name),
@@ -3832,7 +6041,7 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
3832
6041
  const resolveActor = context.resolveActorIdentity ?? (async () => void 0);
3833
6042
  const automationUrl = (context.source.kind === "event_automation" || context.source.kind === "scheduled_automation") && context.source.automationId ? getDashboardTaskLink(context.source.automationId) : void 0;
3834
6043
  const common = {
3835
- ...basePluginContext(plugin),
6044
+ ...basePluginContext$1(plugin),
3836
6045
  annotations,
3837
6046
  ...automationUrl ? { automationLink: { url: automationUrl } } : void 0,
3838
6047
  conversationId: context.conversationId,
@@ -3969,13 +6178,14 @@ function getPluginRoutes(options) {
3969
6178
  const hook = plugin.hooks?.routes;
3970
6179
  if (!hook) continue;
3971
6180
  const pluginRoutes = hook({
3972
- ...basePluginContext(plugin),
6181
+ ...basePluginContext$1(plugin),
3973
6182
  annotations: { forConversation: (conversationId) => createPluginAnnotations({
3974
6183
  conversationId,
3975
6184
  db: getDb(),
3976
6185
  plugin: pluginName
3977
6186
  }) },
3978
6187
  codeChanges: createCodeChangePublisher(pluginName),
6188
+ users: { readIdentity: readIdentityById },
3979
6189
  events: {
3980
6190
  async hasMatch(event) {
3981
6191
  if (!options.events.hasMatch) return false;
@@ -4039,7 +6249,7 @@ function getPluginApiRoutes() {
4039
6249
  const hook = plugin.hooks?.apiRoutes;
4040
6250
  if (!hook) continue;
4041
6251
  const app = hook({
4042
- ...basePluginContext(plugin),
6252
+ ...basePluginContext$1(plugin),
4043
6253
  conversationEvents: createPluginConversationEventReader(plugin),
4044
6254
  eventStats: createPluginConversationEventStats(plugin),
4045
6255
  users: { resolve: resolveViewerUser }
@@ -4073,7 +6283,7 @@ function getPluginSlackConversationLink(conversationId) {
4073
6283
  const hook = plugin.hooks?.slackConversationLink;
4074
6284
  if (!hook) continue;
4075
6285
  const url = trustedSlackConversationUrl(pluginName, hook({
4076
- ...basePluginContext(plugin),
6286
+ ...basePluginContext$1(plugin),
4077
6287
  conversationId
4078
6288
  }));
4079
6289
  if (url) return { url };
@@ -4211,7 +6421,7 @@ async function getPluginOperationalReports(nowMs) {
4211
6421
  try {
4212
6422
  const state = createPluginState(pluginName);
4213
6423
  const report = await hook({
4214
- ...basePluginContext(plugin),
6424
+ ...basePluginContext$1(plugin),
4215
6425
  eventStats: createPluginConversationEventStats(plugin, () => nowMs),
4216
6426
  nowMs,
4217
6427
  state: pluginReadState(state)
@@ -4241,7 +6451,7 @@ async function getPluginProfileReports(args) {
4241
6451
  try {
4242
6452
  const state = createPluginState(pluginName);
4243
6453
  const report = await hook({
4244
- ...basePluginContext(plugin),
6454
+ ...basePluginContext$1(plugin),
4245
6455
  nowMs: args.nowMs,
4246
6456
  state: pluginReadState(state),
4247
6457
  subject: args.subject,
@@ -4314,7 +6524,7 @@ function createPluginHookRunner(input = {}) {
4314
6524
  }) : void 0;
4315
6525
  try {
4316
6526
  const result = await hook({
4317
- ...basePluginContext(plugin),
6527
+ ...basePluginContext$1(plugin),
4318
6528
  ...tool.conversationId ? { conversationId: tool.conversationId } : void 0,
4319
6529
  ...annotations ? { annotations } : void 0,
4320
6530
  result: {
@@ -4363,7 +6573,7 @@ function createPluginHookRunner(input = {}) {
4363
6573
  }));
4364
6574
  if (selected.length === 0) continue;
4365
6575
  const finalize = await hook({
4366
- ...basePluginContext(plugin),
6576
+ ...basePluginContext$1(plugin),
4367
6577
  repos: selected,
4368
6578
  sandbox: sandboxCapability
4369
6579
  });
@@ -4381,7 +6591,7 @@ function createPluginHookRunner(input = {}) {
4381
6591
  if (!hook) continue;
4382
6592
  logInfo("agent.plugin.sandbox_preparation.started", { "app.plugin.name": pluginName });
4383
6593
  await hook({
4384
- ...basePluginContext(plugin),
6594
+ ...basePluginContext$1(plugin),
4385
6595
  actor: input.actor,
4386
6596
  sandbox: sandboxCapability
4387
6597
  });
@@ -4398,7 +6608,7 @@ function createPluginHookRunner(input = {}) {
4398
6608
  let replacement;
4399
6609
  let blocked;
4400
6610
  await hook({
4401
- ...basePluginContext(plugin),
6611
+ ...basePluginContext$1(plugin),
4402
6612
  actor: input.actor,
4403
6613
  actors,
4404
6614
  tool: {
@@ -4439,4 +6649,925 @@ function createPluginHookRunner(input = {}) {
4439
6649
  }
4440
6650
  };
4441
6651
  }
4442
- export { bindSlackDirectCredentialSubject as $, createCache as $t, getWorkspaceSnapshotBuilder as A, conversationArchivedForUser as At, STOP_WATCHING_TOOL_NAME as B, getSlackFileId as Bt, WorkspaceValidationError as C, createPluginState as Ct, setWorkspaceSnapshotBuild as D, readConversationAccessFromSql as Dt, setWorkspaceSnapshot as E, createPluginLogger as Et, isSlackMessageTurn as F, viewerConversationMembership as Ft, createWatch as G, saveObjectAnnotations as Gt, cancelWatch as H, readAttachmentVisionSummary as Ht, slackMessageTurnDisabledReason as I, createPluginConversationEvents as It, getWatch as J, listConversationAnnotationsById as Jt, deliverWatch as K, createPluginAnnotations as Kt, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as L, readLatestConversationBrief as Lt, workspaceRepoCheckoutPath as M, conversationIsMemberRoot as Mt, resolveChannelCapabilities as N, conversationNotArchivedForEmail as Nt, SANDBOX_RUNTIME as O, conversationArchiveTimes as Ot, getSlackToolContext as P, conversationNotArchivedForUser as Pt, bindScheduledAutomationCredentialSubject as Q, createPluginCache as Qt, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as R, readConversationEventPage as Rt, updateWorkspace as S, PluginToolDeniedError as St, loadSnapshotsForProfile as T, createPluginModel as Tt, collectEventMatchKeys as U, recordSlackAttachment as Ut, cancelSubscriptions as V, matchSlackAttachments as Vt, createTimerWatch as W, resolveRootVisibility as Wt, takeDueTimerWatches as X, resolveUserTimezone as Xt, listWatches as Y, defaultTimezone as Yt, bindEventAutomationCredentialSubject as Z, lookupSlackUser as Zt, deleteWorkspace as _, getDashboardConversationLink as _t, getPluginRoutes as a, downloadPrivateSlackFile as at, listWorkspaces as b, setDashboardConversationLinkOptions as bt, getPluginTools as c, getSlackClient as ct, setPlugins as d, normalizeSlackConversationId as dt, JUNIOR_NATIVE_EVENT_NAMESPACE as en, verifyEventAutomationCredentialSubject as et, validatePlugins as f, runWithSlackInstallationToken as ft, createWorkspace as g, JUNIOR_THREAD_STATE_TTL_MS as gt, setBriefsConfig as h, runWithWorkspaceTeamId as ht, getPluginApiRoutes as i, conversationNoteEvent as in, SlackActionError as it, tryWorkspaceRepoCheckoutPath as j, conversationHasParticipantUser as jt, deleteWorkspaceSnapshotBuilders as k, conversationArchivedForEmail as kt, getPluginUserPromptContributions as l, isConversationScopedChannel as lt, registration_exports as m, getWorkspaceTeamId as mt, applyPluginFormatMarkdown as n, authenticationLinkedEvent as nn, verifySlackDirectCredentialSubject as nt, getPluginSlackConversationLink as o, getFilePermalink as ot, coreTaskRegistrations as p, withSlackRetries as pt, findMatchingWatches as q, listConversationAnnotations as qt, createPluginHookRunner as r, authenticationUnlinkedEvent as rn, verifyTimerWatchCredentialSubject as rt, getPluginSystemPromptContributions as s, getHeaderString as st, agent_hooks_exports as t, agentsInstructionsUpdatedEvent as tn, verifyScheduledAutomationCredentialSubject as tt, getPlugins as u, isDmChannel as ut, getWorkspace as v, getDashboardObjectIconLink as vt, invalidateReadySnapshot as w, createPluginEmbedder as wt, listWorkspacesByRepository as x, ToolInputError as xt, getWorkspaceByName as y, getDashboardTaskLink as yt, RESOURCE_WATCH_TOOL_SOURCE as z, buildSentryConversationUrl as zt };
6652
+ // src/chat/plugins/credential-hooks.ts
6653
+ function parseSchema(schema, value, message) {
6654
+ const result = schema.safeParse(value);
6655
+ if (!result.success) throw new Error(message);
6656
+ return result.data;
6657
+ }
6658
+ function parseAuthorization(value, pluginName) {
6659
+ if (value === void 0) return;
6660
+ const authorization = parseSchema(pluginAuthorizationSchema, value, `Plugin "${pluginName}" grant authorization is invalid`);
6661
+ if (authorization.provider !== pluginName) throw new Error(`Plugin "${pluginName}" grant authorization provider must match the issuing plugin`);
6662
+ return authorization;
6663
+ }
6664
+ function parseGrant(value, pluginName) {
6665
+ return parseSchema(pluginGrantSchema, value, `Plugin "${pluginName}" grantForEgress returned an invalid grant`);
6666
+ }
6667
+ function pluginFor(provider) {
6668
+ return getPlugins().find((candidate) => candidate.manifest.name === provider);
6669
+ }
6670
+ function basePluginContext(plugin) {
6671
+ const pluginName = plugin.manifest.name;
6672
+ return {
6673
+ plugin: { name: pluginName },
6674
+ cache: createPluginCache(pluginName),
6675
+ log: createPluginLogger(pluginName),
6676
+ db: getDb()
6677
+ };
6678
+ }
6679
+ function parseCredentialResult(value, pluginName) {
6680
+ const result = parseSchema(pluginCredentialResultSchema, value, `Plugin "${pluginName}" issueCredential result is invalid`);
6681
+ if (result.type === "lease") {
6682
+ parseAuthorization(result.lease.authorization, pluginName);
6683
+ return result;
6684
+ }
6685
+ if (result.type === "unavailable") return result;
6686
+ parseAuthorization(result.authorization, pluginName);
6687
+ return result;
6688
+ }
6689
+ /** Ask a plugin which grant an outbound request needs. */
6690
+ async function selectPluginGrant(input) {
6691
+ const plugin = pluginFor(input.provider);
6692
+ const hook = plugin?.hooks?.grantForEgress;
6693
+ if (!plugin || !hook) return;
6694
+ const result = await hook({
6695
+ ...basePluginContext(plugin),
6696
+ request: {
6697
+ ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : void 0,
6698
+ method: input.method,
6699
+ ...input.operation ? { operation: input.operation } : void 0,
6700
+ url: input.upstreamUrl.toString()
6701
+ }
6702
+ });
6703
+ return result === void 0 ? void 0 : parseGrant(result, plugin.manifest.name);
6704
+ }
6705
+ /** Let the owning plugin inspect an upstream response without changing pass-through behavior. */
6706
+ async function onPluginEgressResponse(input) {
6707
+ const plugin = pluginFor(input.provider);
6708
+ const hook = plugin?.hooks?.onEgressResponse;
6709
+ if (!plugin || !hook) return {};
6710
+ let permissionDenied;
6711
+ await hook({
6712
+ ...basePluginContext(plugin),
6713
+ grant: input.grant,
6714
+ permissionDenied(message) {
6715
+ const trimmed = message.trim();
6716
+ if (!trimmed) throw new Error(`Plugin "${plugin.manifest.name}" onEgressResponse permissionDenied message is empty`);
6717
+ permissionDenied = { message: trimmed };
6718
+ },
6719
+ request: {
6720
+ method: input.method,
6721
+ ...input.operation ? { operation: input.operation } : void 0,
6722
+ url: input.upstreamUrl.toString()
6723
+ },
6724
+ response: input.response
6725
+ });
6726
+ return permissionDenied ? { permissionDenied } : {};
6727
+ }
6728
+ /** Return whether a plugin owns credential issuance for egress. */
6729
+ function hasEgressCredentialHooks(provider) {
6730
+ const hooks = pluginFor(provider)?.hooks;
6731
+ return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
6732
+ }
6733
+ /** Return providers that can verify an OAuth account for identity storage. */
6734
+ function getOAuthAccountProviders() {
6735
+ return getPlugins().filter((plugin) => Boolean(plugin.hooks?.resolveOAuthAccount)).map((plugin) => plugin.manifest.name);
6736
+ }
6737
+ /** Ask a plugin which provider account belongs to an OAuth token. */
6738
+ async function resolvePluginOAuthAccount(input) {
6739
+ const plugin = pluginFor(input.provider);
6740
+ const hook = plugin?.hooks?.resolveOAuthAccount;
6741
+ if (!plugin || !hook) return;
6742
+ const account = await hook({
6743
+ ...basePluginContext(plugin),
6744
+ tokenResponse: input.tokenResponse,
6745
+ tokens: input.tokens
6746
+ });
6747
+ return account === void 0 ? void 0 : parseSchema(pluginProviderAccountSchema, account, `Plugin "${plugin.manifest.name}" resolveOAuthAccount returned an invalid account`);
6748
+ }
6749
+ /** Ask a plugin to issue headers or describe why the selected grant is unavailable. */
6750
+ async function issuePluginCredential(input) {
6751
+ const plugin = pluginFor(input.provider);
6752
+ const hook = plugin?.hooks?.issueCredential;
6753
+ if (!plugin || !hook) throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
6754
+ const currentUserId = "type" in input.actor ? input.actor.userId : void 0;
6755
+ const credentialSubjectUserId = input.credentialSubject?.userId;
6756
+ return parseCredentialResult(await hook({
6757
+ ...basePluginContext(plugin),
6758
+ actor: input.actor,
6759
+ grant: input.grant,
6760
+ ...input.credentialSubject ? { credentialSubject: input.credentialSubject } : void 0,
6761
+ tokens: {
6762
+ ...currentUserId ? { currentUser: {
6763
+ userId: currentUserId,
6764
+ get: async () => await input.userTokenStore.get(currentUserId, plugin.manifest.name),
6765
+ set: async (tokens) => {
6766
+ await input.userTokenStore.set(currentUserId, plugin.manifest.name, tokens);
6767
+ },
6768
+ withRefresh: async (callback) => await input.userTokenStore.withRefresh(currentUserId, plugin.manifest.name, callback)
6769
+ } } : void 0,
6770
+ ...credentialSubjectUserId ? { credentialSubject: {
6771
+ userId: credentialSubjectUserId,
6772
+ get: async () => await input.userTokenStore.get(credentialSubjectUserId, plugin.manifest.name),
6773
+ set: async (tokens) => {
6774
+ await input.userTokenStore.set(credentialSubjectUserId, plugin.manifest.name, tokens);
6775
+ },
6776
+ withRefresh: async (callback) => await input.userTokenStore.withRefresh(credentialSubjectUserId, plugin.manifest.name, callback)
6777
+ } } : void 0
6778
+ }
6779
+ }), plugin.manifest.name);
6780
+ }
6781
+ // src/chat/capabilities/router.ts
6782
+ var ProviderCredentialRouter = class {
6783
+ brokersByProvider;
6784
+ constructor(input) {
6785
+ this.brokersByProvider = input.brokersByProvider;
6786
+ }
6787
+ async issue(input) {
6788
+ const broker = this.brokersByProvider[input.provider];
6789
+ if (!broker) throw new Error(`No credential broker registered for provider: ${input.provider}`);
6790
+ return await broker.issue({
6791
+ context: input.context,
6792
+ reason: input.reason
6793
+ });
6794
+ }
6795
+ };
6796
+ // src/chat/credentials/user-token-store.ts
6797
+ const storedTokensSchema = pluginStoredTokensSchema;
6798
+ // src/chat/credentials/state-adapter-token-store.ts
6799
+ const KEY_PREFIX = "oauth-token";
6800
+ const BUFFER_MS = 864e5;
6801
+ const LONG_LIVED_TTL_MS = 31536e6;
6802
+ const REFRESH_LOCK_WAIT_MS = 3e4;
6803
+ const REFRESH_LOCK_RETRY_MS = 100;
6804
+ function tokenKey(userId, provider) {
6805
+ return `${KEY_PREFIX}:${userId}:${provider}`;
6806
+ }
6807
+ function refreshLockKey(userId, provider) {
6808
+ return `${tokenKey(userId, provider)}:refresh`;
6809
+ }
6810
+ var StateAdapterTokenStore = class {
6811
+ state;
6812
+ constructor(stateAdapter) {
6813
+ this.state = stateAdapter;
6814
+ }
6815
+ async get(userId, provider) {
6816
+ const stored = await this.state.get(tokenKey(userId, provider));
6817
+ return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
6818
+ }
6819
+ async set(userId, provider, tokens) {
6820
+ const parsed = storedTokensSchema.parse(tokens);
6821
+ const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
6822
+ const ttlMs = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
6823
+ await this.state.set(tokenKey(userId, provider), parsed, ttlMs);
6824
+ }
6825
+ async delete(userId, provider) {
6826
+ await this.state.delete(tokenKey(userId, provider));
6827
+ }
6828
+ /** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
6829
+ async withRefresh(userId, provider, callback) {
6830
+ const lockKey = refreshLockKey(userId, provider);
6831
+ const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
6832
+ while (true) {
6833
+ const lock = await acquireActiveLock(this.state, lockKey);
6834
+ if (lock) try {
6835
+ return await callback();
6836
+ } finally {
6837
+ await this.state.releaseLock(lock);
6838
+ }
6839
+ if (Date.now() >= deadline) throw new Error(`Could not acquire OAuth token refresh lock`);
6840
+ await sleep$1(REFRESH_LOCK_RETRY_MS);
6841
+ }
6842
+ }
6843
+ };
6844
+ // src/chat/capabilities/factory.ts
6845
+ const sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
6846
+ /** Create the user token store used by OAuth-backed credential brokers. */
6847
+ function createUserTokenStore() {
6848
+ return new StateAdapterTokenStore(getStateAdapter());
6849
+ }
6850
+ function createProviderCredentialRouter(userTokenStore) {
6851
+ const brokersByProvider = {};
6852
+ for (const plugin of pluginCatalogRuntime.getProviders()) {
6853
+ const { name } = plugin.manifest;
6854
+ if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) continue;
6855
+ brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, { userTokenStore });
6856
+ }
6857
+ return new ProviderCredentialRouter({ brokersByProvider });
6858
+ }
6859
+ function getSandboxEgressRouter() {
6860
+ const stateAdapter = getStateAdapter();
6861
+ let router = sandboxEgressRouters.get(stateAdapter);
6862
+ if (!router) {
6863
+ router = createProviderCredentialRouter(new StateAdapterTokenStore(stateAdapter));
6864
+ sandboxEgressRouters.set(stateAdapter, router);
6865
+ }
6866
+ return router;
6867
+ }
6868
+ /** Issue one provider credential lease for host-side sandbox egress proxying. */
6869
+ async function issueProviderCredentialLease(input) {
6870
+ return await getSandboxEgressRouter().issue(input);
6871
+ }
6872
+ // src/chat/sandbox/egress/credentials.ts
6873
+ const HTTP_READ_METHODS = /* @__PURE__ */ new Set([
6874
+ "GET",
6875
+ "HEAD",
6876
+ "OPTIONS"
6877
+ ]);
6878
+ /**
6879
+ * Signals that egress selected a grant but could not issue credential headers.
6880
+ *
6881
+ * Callers convert this into command-level auth-required state so the agent can
6882
+ * request authorization without exposing provider-specific lease internals.
6883
+ */
6884
+ var SandboxEgressCredentialError = class extends Error {
6885
+ authorization;
6886
+ grant;
6887
+ kind;
6888
+ provider;
6889
+ constructor(input) {
6890
+ super(input.message);
6891
+ this.name = "SandboxEgressCredentialError";
6892
+ this.authorization = input.authorization;
6893
+ this.grant = input.grant;
6894
+ this.kind = input.kind;
6895
+ this.provider = input.provider;
6896
+ }
6897
+ };
6898
+ function defaultGrantForProvider(input) {
6899
+ const access = HTTP_READ_METHODS.has(input.method.toUpperCase()) ? "read" : "write";
6900
+ return {
6901
+ source: "broker",
6902
+ grant: {
6903
+ name: "default",
6904
+ access,
6905
+ reason: `sandbox-egress:${input.provider}:${access}`
6906
+ }
6907
+ };
6908
+ }
6909
+ function hasGenericCredentials(provider) {
6910
+ const manifest = pluginCatalogRuntime.getDefinition(provider)?.manifest;
6911
+ return Boolean(manifest?.credentials || manifest?.apiHeaders);
6912
+ }
6913
+ function oauthAuthorizationForProvider(provider) {
6914
+ const oauth = pluginCatalogRuntime.getOAuthConfig(provider);
6915
+ return oauth ? {
6916
+ type: "oauth",
6917
+ provider,
6918
+ ...oauth.scope ? { scope: oauth.scope } : void 0
6919
+ } : void 0;
6920
+ }
6921
+ function credentialSubjectFromContext(context) {
6922
+ return "subject" in context.credentials && context.credentials.subject ? {
6923
+ type: "user",
6924
+ userId: context.credentials.subject.userId
6925
+ } : void 0;
6926
+ }
6927
+ function assertLeaseTransformsOwnedByProvider(provider, lease) {
6928
+ for (const transform of lease.headerTransforms) if (resolveSandboxEgressProviderForHost(transform.domain) !== provider) throw new Error(`Credential lease for ${provider} included header transform for unowned domain ${transform.domain}`);
6929
+ }
6930
+ /**
6931
+ * Select the grant needed for one outbound request.
6932
+ *
6933
+ * GitHub GraphQL and other plugin-owned APIs may need body-aware grant choices;
6934
+ * providers without hooks use a simple read/write default based on HTTP method.
6935
+ * A hook plugin that also declares generic credentials can return no grant to
6936
+ * use that default for one request.
6937
+ */
6938
+ async function selectSandboxEgressGrant(input) {
6939
+ if (!hasEgressCredentialHooks(input.provider)) return defaultGrantForProvider(input);
6940
+ const pluginGrant = await selectPluginGrant({
6941
+ ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : void 0,
6942
+ ...input.operation ? { operation: input.operation } : void 0,
6943
+ provider: input.provider,
6944
+ method: input.method,
6945
+ upstreamUrl: input.upstreamUrl
6946
+ });
6947
+ if (!pluginGrant) {
6948
+ if (hasGenericCredentials(input.provider)) return defaultGrantForProvider(input);
6949
+ throw new Error(`Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`);
6950
+ }
6951
+ return {
6952
+ source: "plugin",
6953
+ grant: pluginGrant
6954
+ };
6955
+ }
6956
+ /**
6957
+ * Resolve the authorization flow attached to a broker-selected egress grant.
6958
+ *
6959
+ * Plugin-selected grants can return their own authorization metadata when
6960
+ * issuing credentials; broker defaults use the provider OAuth config.
6961
+ */
6962
+ function authorizationForSandboxEgressGrant(provider, selection) {
6963
+ return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
6964
+ }
6965
+ /**
6966
+ * Return credential header transforms for a selected grant.
6967
+ *
6968
+ * Only shared installation grants are remembered on the host. User and other
6969
+ * grants always issue from the live credential source so a saved login change
6970
+ * cannot leave stale auth headers in place. The sandbox context only authorizes
6971
+ * the request.
6972
+ */
6973
+ async function sandboxEgressCredentialLease(provider, selection, context) {
6974
+ const { grant } = selection;
6975
+ const cached = await getSandboxEgressCredentialLease(provider, grant, context);
6976
+ if (cached) {
6977
+ if (selection.source === "plugin" && cached.grant.access !== grant.access) throw new Error(`Cached credential lease for ${provider}/${grant.name} has ${cached.grant.access} access, but ${grant.access} was selected`);
6978
+ return {
6979
+ ...cached,
6980
+ grant
6981
+ };
6982
+ }
6983
+ let lease;
6984
+ if (selection.source === "plugin") {
6985
+ const credentialSubject = credentialSubjectFromContext(context);
6986
+ const pluginResult = await issuePluginCredential({
6987
+ provider,
6988
+ grant,
6989
+ actor: context.credentials.actor,
6990
+ ...credentialSubject ? { credentialSubject } : void 0,
6991
+ userTokenStore: createUserTokenStore()
6992
+ });
6993
+ if (pluginResult.type === "needed") throw new SandboxEgressCredentialError({
6994
+ provider,
6995
+ grant,
6996
+ kind: "auth_required",
6997
+ authorization: pluginResult.authorization,
6998
+ message: pluginResult.message
6999
+ });
7000
+ if (pluginResult.type === "unavailable") throw new SandboxEgressCredentialError({
7001
+ provider,
7002
+ grant,
7003
+ kind: "unavailable",
7004
+ message: pluginResult.message
7005
+ });
7006
+ lease = pluginResult.lease;
7007
+ } else try {
7008
+ lease = await issueProviderCredentialLease({
7009
+ context: context.credentials,
7010
+ provider,
7011
+ reason: grant.reason ?? `sandbox-egress:${provider}:default`
7012
+ });
7013
+ } catch (error) {
7014
+ if (error instanceof CredentialUnavailableError) throw new SandboxEgressCredentialError({
7015
+ provider,
7016
+ grant,
7017
+ kind: "auth_required",
7018
+ authorization: authorizationForSandboxEgressGrant(provider, selection),
7019
+ message: error.message
7020
+ });
7021
+ throw error;
7022
+ }
7023
+ const headerTransforms = lease.headerTransforms ?? [];
7024
+ if (headerTransforms.length === 0) throw new Error(`Credential lease for ${provider} did not include header transforms`);
7025
+ const leaseExpiresAtMs = Date.parse(lease.expiresAt);
7026
+ if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) throw new Error(`Credential lease for ${provider} is expired`);
7027
+ const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
7028
+ const issuedLease = {
7029
+ provider,
7030
+ grant,
7031
+ ...lease.account ? { account: lease.account } : void 0,
7032
+ ...authorization ? { authorization } : void 0,
7033
+ expiresAt: lease.expiresAt,
7034
+ headerTransforms
7035
+ };
7036
+ assertLeaseTransformsOwnedByProvider(provider, issuedLease);
7037
+ await setSandboxEgressCredentialLease(context, issuedLease);
7038
+ return issuedLease;
7039
+ }
7040
+ /** Return whether a credential lease can modify requests to the target host. */
7041
+ function hasSandboxEgressLeaseTransformForHost(lease, host) {
7042
+ return lease.headerTransforms.some((transform) => matchesSandboxEgressDomain(host, transform.domain));
7043
+ }
7044
+ // src/chat/egress/credentialed.ts
7045
+ const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
7046
+ "connection",
7047
+ "host",
7048
+ "keep-alive",
7049
+ "proxy-authenticate",
7050
+ "proxy-authorization",
7051
+ "te",
7052
+ "trailer",
7053
+ "transfer-encoding",
7054
+ "upgrade"
7055
+ ]);
7056
+ const PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
7057
+ "vercel-sandbox-oidc-token",
7058
+ "vercel-forwarded-host",
7059
+ "vercel-forwarded-scheme",
7060
+ "vercel-forwarded-port",
7061
+ "vercel-forwarded-path"
7062
+ ]);
7063
+ const TRACE_PROPAGATION_HEADERS = /* @__PURE__ */ new Set([
7064
+ "baggage",
7065
+ "sentry-trace",
7066
+ "traceparent"
7067
+ ]);
7068
+ const DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length"]);
7069
+ const UPSTREAM_TOKEN_REJECTION_STATUS = 401;
7070
+ const UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
7071
+ const GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES = 65536;
7072
+ const RESPONSE_BODY_TEXT_LIMIT_BYTES = 65536;
7073
+ function authRequiredResponse(input) {
7074
+ return new Response(`junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized\n${input.message}`, {
7075
+ status: 401,
7076
+ headers: {
7077
+ "content-type": "text/plain; charset=utf-8",
7078
+ "cache-control": "no-store"
7079
+ }
7080
+ });
7081
+ }
7082
+ function policyDeniedResponse(error) {
7083
+ return Response.json({ error: error.message }, {
7084
+ status: 403,
7085
+ headers: { "cache-control": "no-store" }
7086
+ });
7087
+ }
7088
+ function shouldLogSandboxEgressInfo() {
7089
+ return (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase() !== "production";
7090
+ }
7091
+ function egressAttributes(input) {
7092
+ return {
7093
+ ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : void 0,
7094
+ ...input.provider ? { "app.provider.name": input.provider } : void 0,
7095
+ ...input.grantName ? { "app.grant.name": input.grantName } : void 0,
7096
+ ...input.grantAccess ? { "app.grant.access": input.grantAccess } : void 0,
7097
+ ...input.grantReason ? { "app.grant.reason": input.grantReason } : void 0,
7098
+ ...input.host ? { "server.address": input.host } : void 0,
7099
+ ...input.method ? { "http.request.method": input.method } : void 0,
7100
+ ...input.path ? { "url.path": input.path } : void 0,
7101
+ ...input.status ? { "http.response.status_code": input.status } : void 0
7102
+ };
7103
+ }
7104
+ function redactedProxyPath(pathname) {
7105
+ if (pathname.startsWith(`/api/internal/sandbox-egress/`)) return `${SANDBOX_EGRESS_PROXY_PATH}/<token>`;
7106
+ return pathname;
7107
+ }
7108
+ function routingAttributes(request, upstreamUrl) {
7109
+ const attributes = { "app.sandbox.egress.proxy_path": redactedProxyPath(new URL(request.url).pathname) };
7110
+ if (upstreamUrl) {
7111
+ attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
7112
+ const gitService = upstreamUrl.searchParams.get("service");
7113
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) attributes["app.sandbox.egress.git_service"] = gitService;
7114
+ }
7115
+ return attributes;
7116
+ }
7117
+ function displayedUpstreamPath(upstreamUrl) {
7118
+ const gitService = upstreamUrl.searchParams.get("service");
7119
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) return `${upstreamUrl.pathname}?service=${gitService}`;
7120
+ return upstreamUrl.pathname;
7121
+ }
7122
+ function permissionDeniedMessage(provider, grant) {
7123
+ return `${provider} returned HTTP 403 after the runtime injected the ${grant.name} grant. The request was forwarded; this is not a local runtime block.`;
7124
+ }
7125
+ function isEgressAuthRequired(error) {
7126
+ return error instanceof EgressAuthRequired || error instanceof Error && error.name === "EgressAuthRequired";
7127
+ }
7128
+ function isEgressPolicyDenied(error) {
7129
+ return error instanceof EgressPolicyDenied || error instanceof Error && error.name === "EgressPolicyDenied";
7130
+ }
7131
+ /**
7132
+ * Record one forwarded provider request. Write-grant requests are audit
7133
+ * events, so they reach Sentry in production. Other requests log only outside
7134
+ * production.
7135
+ */
7136
+ function logSandboxEgressUpstreamRequest(input) {
7137
+ const isWrite = input.grantAccess === "write";
7138
+ if (!isWrite && !shouldLogSandboxEgressInfo()) return;
7139
+ (isWrite ? logAudit : logInfo)("sandbox.egress.upstream.requested", {
7140
+ ...egressAttributes({
7141
+ egressId: input.egressId,
7142
+ grantAccess: input.grantAccess,
7143
+ grantName: input.grantName,
7144
+ grantReason: input.grantReason,
7145
+ host: input.upstreamUrl.hostname,
7146
+ method: input.request.method,
7147
+ path: input.upstreamUrl.pathname,
7148
+ provider: input.provider,
7149
+ status: input.upstream.status
7150
+ }),
7151
+ ...routingAttributes(input.request, input.upstreamUrl),
7152
+ "app.sandbox.egress.upstream_ok": input.upstream.ok
7153
+ });
7154
+ }
7155
+ async function requestBodyBytes(request) {
7156
+ if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS" || request.body === null) return;
7157
+ return await request.arrayBuffer();
7158
+ }
7159
+ /**
7160
+ * Explain a request body that the runtime delivered already used.
7161
+ *
7162
+ * The forwarded headers cannot show whether the body was empty, so the proxy
7163
+ * does not send a credentialed write upstream without its payload.
7164
+ */
7165
+ function unreadableBodyResponse(eventId) {
7166
+ return Response.json({
7167
+ source: "junior-egress",
7168
+ error: "The sandbox egress proxy could not read the request body, so it did not send the request upstream.",
7169
+ ...eventId ? { sentryEventId: eventId } : void 0
7170
+ }, {
7171
+ status: 500,
7172
+ headers: { "cache-control": "no-store" }
7173
+ });
7174
+ }
7175
+ /** Identify GitHub writes whose body determines whether a grant is safe. */
7176
+ function githubBodyInspection(input) {
7177
+ if (input.provider !== "github" || input.requestMethod.toUpperCase() !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") return;
7178
+ const pathname = input.upstreamUrl.pathname.toLowerCase();
7179
+ if (pathname.endsWith("/graphql")) return "graphql";
7180
+ return /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+\/events)?$/.test(pathname) ? "pull-request-review" : void 0;
7181
+ }
7182
+ function grantSelectionBodyTooLargeMessage(inspection) {
7183
+ return inspection === "graphql" ? "GitHub GraphQL request body is too large to inspect before issuing credentials." : "GitHub pull request review request body is too large to inspect before issuing credentials.";
7184
+ }
7185
+ function grantSelectionBodyText(input) {
7186
+ if (input.body === void 0) return;
7187
+ if (input.body.byteLength > GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES) {
7188
+ const inspection = githubBodyInspection({
7189
+ provider: input.provider,
7190
+ requestMethod: input.request.method,
7191
+ upstreamUrl: input.upstreamUrl
7192
+ });
7193
+ if (inspection) throw new EgressPolicyDenied(grantSelectionBodyTooLargeMessage(inspection));
7194
+ return;
7195
+ }
7196
+ return new TextDecoder().decode(input.body);
7197
+ }
7198
+ function responseContentLength(upstream) {
7199
+ const raw = upstream.headers.get("content-length");
7200
+ if (!raw) return;
7201
+ const parsed = Number(raw);
7202
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
7203
+ }
7204
+ async function responseTextWithinLimit(upstream, maxBytes) {
7205
+ const limit = Math.min(Math.max(0, Math.floor(maxBytes)), RESPONSE_BODY_TEXT_LIMIT_BYTES);
7206
+ if (limit <= 0) return;
7207
+ const contentLength = responseContentLength(upstream);
7208
+ if (contentLength !== void 0 && contentLength > limit) return;
7209
+ let clone;
7210
+ try {
7211
+ clone = upstream.clone();
7212
+ } catch {
7213
+ return;
7214
+ }
7215
+ const body = clone.body;
7216
+ if (!body) return "";
7217
+ const reader = body.getReader();
7218
+ const chunks = [];
7219
+ let bytes = 0;
7220
+ try {
7221
+ while (true) {
7222
+ const { done, value } = await reader.read();
7223
+ if (done) break;
7224
+ if (!value) continue;
7225
+ bytes += value.byteLength;
7226
+ if (bytes > limit) {
7227
+ await reader.cancel().catch(() => void 0);
7228
+ return;
7229
+ }
7230
+ chunks.push(value);
7231
+ }
7232
+ } catch {
7233
+ await reader.cancel().catch(() => void 0);
7234
+ return;
7235
+ } finally {
7236
+ reader.releaseLock();
7237
+ }
7238
+ const combined = new Uint8Array(bytes);
7239
+ let offset = 0;
7240
+ for (const chunk of chunks) {
7241
+ combined.set(chunk, offset);
7242
+ offset += chunk.byteLength;
7243
+ }
7244
+ return new TextDecoder().decode(combined);
7245
+ }
7246
+ /**
7247
+ * Build the upstream request headers.
7248
+ *
7249
+ * Sandbox/proxy-only headers are stripped so they cannot leak to providers, and
7250
+ * trace headers are preserved only for hosts opted into trace propagation.
7251
+ * Credential lease transforms win last because they are the host-issued
7252
+ * authority for provider auth headers.
7253
+ */
7254
+ function requestHeaders(request, lease, upstreamHost, tracePropagation) {
7255
+ const headers = new Headers();
7256
+ const mayPropagateTrace = shouldPropagateSandboxEgressTrace(upstreamHost, tracePropagation);
7257
+ request.headers.forEach((value, key) => {
7258
+ const normalized = key.toLowerCase();
7259
+ if (HOP_BY_HOP_HEADERS.has(normalized) || PROXY_ONLY_HEADERS.has(normalized)) return;
7260
+ if (TRACE_PROPAGATION_HEADERS.has(normalized) && !mayPropagateTrace) return;
7261
+ headers.append(key, value);
7262
+ });
7263
+ for (const transform of lease.headerTransforms) {
7264
+ if (!matchesSandboxEgressDomain(upstreamHost, transform.domain)) continue;
7265
+ for (const [key, value] of Object.entries(transform.headers)) {
7266
+ if (TRACE_PROPAGATION_HEADERS.has(key.toLowerCase()) && !mayPropagateTrace) continue;
7267
+ headers.set(key, value);
7268
+ }
7269
+ }
7270
+ return headers;
7271
+ }
7272
+ function responseHeaders(upstream) {
7273
+ const headers = new Headers();
7274
+ upstream.headers.forEach((value, key) => {
7275
+ const normalized = key.toLowerCase();
7276
+ if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) headers.append(key, value);
7277
+ });
7278
+ return headers;
7279
+ }
7280
+ function leaseLogAttributes(input) {
7281
+ return {
7282
+ ...egressAttributes({
7283
+ egressId: input.egressId,
7284
+ grantAccess: input.lease.grant.access,
7285
+ grantName: input.lease.grant.name,
7286
+ grantReason: input.lease.grant.reason,
7287
+ host: input.upstreamUrl.hostname,
7288
+ method: input.request.method,
7289
+ path: input.upstreamUrl.pathname,
7290
+ provider: input.provider,
7291
+ status: input.status
7292
+ }),
7293
+ ...routingAttributes(input.request, input.upstreamUrl)
7294
+ };
7295
+ }
7296
+ async function recordSandboxAuthRequired(input) {
7297
+ await setSandboxEgressAuthRequiredSignal(input.credentialContext, {
7298
+ provider: input.provider,
7299
+ grant: input.grant,
7300
+ kind: input.kind ?? "auth_required",
7301
+ ...input.authorization ? { authorization: input.authorization } : void 0,
7302
+ message: input.message
7303
+ });
7304
+ }
7305
+ async function recordSandboxPermissionDenied(input) {
7306
+ await setSandboxEgressPermissionDeniedSignal(input.credentialContext, {
7307
+ provider: input.provider,
7308
+ grant: input.lease.grant,
7309
+ ...input.lease.account ? { account: input.lease.account } : void 0,
7310
+ message: input.message,
7311
+ source: "upstream",
7312
+ status: input.upstream.status,
7313
+ upstreamHost: input.upstreamUrl.hostname,
7314
+ upstreamPath: displayedUpstreamPath(input.upstreamUrl)
7315
+ });
7316
+ }
7317
+ /**
7318
+ * Forward one verified sandbox egress request with host-managed credentials.
7319
+ *
7320
+ * The caller must already have authenticated the sandbox VM, checked the signed
7321
+ * credential context, and resolved the provider for `upstreamUrl`. This function
7322
+ * then selects the read/write grant, issues or reuses a short-lived credential
7323
+ * lease, applies its header transforms, and maps provider auth failures into
7324
+ * command-level signals instead of throwing raw upstream details at the agent.
7325
+ */
7326
+ async function executeCredentialedEgressRequest(input) {
7327
+ const { activeEgressId, credentialContext, deps, operation, provider, request, upstreamUrl } = input;
7328
+ let body;
7329
+ try {
7330
+ body = await requestBodyBytes(request);
7331
+ } catch (error) {
7332
+ if (!(error instanceof TypeError)) throw error;
7333
+ return unreadableBodyResponse(logException(error, "sandbox.egress.request_body.unreadable", {
7334
+ ...egressAttributes({
7335
+ egressId: activeEgressId,
7336
+ host: upstreamUrl.hostname,
7337
+ method: request.method,
7338
+ path: upstreamUrl.pathname,
7339
+ provider,
7340
+ status: 500
7341
+ }),
7342
+ ...routingAttributes(request, upstreamUrl)
7343
+ }));
7344
+ }
7345
+ const bodyForGrantSelection = githubBodyInspection({
7346
+ provider,
7347
+ requestMethod: request.method,
7348
+ upstreamUrl
7349
+ }) ? body : void 0;
7350
+ let grantSelection;
7351
+ try {
7352
+ grantSelection = await selectSandboxEgressGrant({
7353
+ bodyText: grantSelectionBodyText({
7354
+ body: bodyForGrantSelection,
7355
+ provider,
7356
+ request,
7357
+ upstreamUrl
7358
+ }),
7359
+ ...operation ? { operation } : void 0,
7360
+ provider,
7361
+ method: request.method,
7362
+ upstreamUrl
7363
+ });
7364
+ } catch (error) {
7365
+ if (isEgressPolicyDenied(error)) {
7366
+ logWarn("sandbox.egress.policy.denied", {
7367
+ ...egressAttributes({
7368
+ egressId: activeEgressId,
7369
+ host: upstreamUrl.hostname,
7370
+ method: request.method,
7371
+ path: upstreamUrl.pathname,
7372
+ provider,
7373
+ status: 403
7374
+ }),
7375
+ ...routingAttributes(request, upstreamUrl),
7376
+ "app.sandbox.egress.policy.reason": error.message
7377
+ });
7378
+ return policyDeniedResponse(error);
7379
+ }
7380
+ throw error;
7381
+ }
7382
+ const issueCredentialLease = deps.issueCredentialLease ?? sandboxEgressCredentialLease;
7383
+ const clearCredentialLease = deps.clearCredentialLease ?? clearSandboxEgressCredentialLease;
7384
+ const recordAuthRequired = deps.recordAuthRequired ?? recordSandboxAuthRequired;
7385
+ const recordPermissionDenied = deps.recordPermissionDenied ?? recordSandboxPermissionDenied;
7386
+ async function resolveLease() {
7387
+ try {
7388
+ return await issueCredentialLease(provider, grantSelection, credentialContext);
7389
+ } catch (error) {
7390
+ if (!(error instanceof SandboxEgressCredentialError)) throw error;
7391
+ await recordAuthRequired({
7392
+ credentialContext,
7393
+ provider: error.provider,
7394
+ grant: error.grant,
7395
+ kind: error.kind,
7396
+ authorization: error.authorization,
7397
+ message: error.message
7398
+ });
7399
+ const isAuthRequired = error.kind === "auth_required";
7400
+ logWarn(isAuthRequired ? "sandbox.egress.credential.needed" : "sandbox.egress.credential.unavailable", {
7401
+ ...egressAttributes({
7402
+ egressId: activeEgressId,
7403
+ grantAccess: error.grant.access,
7404
+ grantName: error.grant.name,
7405
+ grantReason: error.grant.reason,
7406
+ host: upstreamUrl.hostname,
7407
+ method: request.method,
7408
+ path: upstreamUrl.pathname,
7409
+ provider: error.provider,
7410
+ status: 401
7411
+ }),
7412
+ ...routingAttributes(request, upstreamUrl)
7413
+ });
7414
+ return authRequiredResponse({
7415
+ provider: error.provider,
7416
+ grant: error.grant,
7417
+ message: error.message
7418
+ });
7419
+ }
7420
+ }
7421
+ let leaseOrResponse = await resolveLease();
7422
+ if (leaseOrResponse instanceof Response) return leaseOrResponse;
7423
+ let lease = leaseOrResponse;
7424
+ const attributes = (activeLease, status) => leaseLogAttributes({
7425
+ egressId: activeEgressId,
7426
+ lease: activeLease,
7427
+ provider,
7428
+ request,
7429
+ status,
7430
+ upstreamUrl
7431
+ });
7432
+ if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
7433
+ logWarn("sandbox.egress.transform.missing", {
7434
+ ...attributes(lease, 403),
7435
+ "app.sandbox.egress.transform_domains": lease.headerTransforms.map((transform) => transform.domain)
7436
+ });
7437
+ return Response.json({ error: "Credential lease does not cover forwarded host" }, { status: 403 });
7438
+ }
7439
+ const fetchImpl = deps.fetch ?? fetch;
7440
+ let retriedAfter403 = false;
7441
+ while (true) {
7442
+ const headers = requestHeaders(request, lease, upstreamUrl.hostname, deps.tracePropagation ?? {});
7443
+ const intercepted = await deps.interceptHttp?.({
7444
+ provider,
7445
+ request: new Request(upstreamUrl, {
7446
+ method: request.method,
7447
+ headers,
7448
+ ...body !== void 0 ? { body } : void 0
7449
+ }),
7450
+ upstreamUrl
7451
+ });
7452
+ if (intercepted) return intercepted;
7453
+ const requestBody = body instanceof ArrayBuffer ? body.slice(0) : body;
7454
+ const upstream = await fetchImpl(upstreamUrl, {
7455
+ method: request.method,
7456
+ headers,
7457
+ ...requestBody !== void 0 ? { body: requestBody } : void 0,
7458
+ redirect: "manual"
7459
+ });
7460
+ let pluginPermissionDenied;
7461
+ try {
7462
+ pluginPermissionDenied = (await onPluginEgressResponse({
7463
+ provider,
7464
+ grant: lease.grant,
7465
+ method: request.method,
7466
+ ...operation ? { operation } : void 0,
7467
+ upstreamUrl,
7468
+ response: {
7469
+ headers: new Headers(upstream.headers),
7470
+ readText: async (maxBytes) => await responseTextWithinLimit(upstream, maxBytes),
7471
+ status: upstream.status
7472
+ }
7473
+ })).permissionDenied;
7474
+ } catch (error) {
7475
+ if (!isEgressAuthRequired(error)) throw error;
7476
+ await clearCredentialLease(provider, lease.grant, credentialContext);
7477
+ await recordAuthRequired({
7478
+ credentialContext,
7479
+ provider,
7480
+ grant: lease.grant,
7481
+ authorization: error.authorization ?? lease.authorization,
7482
+ message: error.message
7483
+ });
7484
+ logWarn("sandbox.egress.upstream_auth_requirement.classified", { ...attributes(lease, upstream.status) });
7485
+ await upstream.body?.cancel().catch(() => void 0);
7486
+ return authRequiredResponse({
7487
+ provider,
7488
+ grant: lease.grant,
7489
+ message: error.message
7490
+ });
7491
+ }
7492
+ logSandboxEgressUpstreamRequest({
7493
+ egressId: activeEgressId,
7494
+ grantAccess: lease.grant.access,
7495
+ grantName: lease.grant.name,
7496
+ grantReason: lease.grant.reason,
7497
+ provider,
7498
+ request,
7499
+ upstream,
7500
+ upstreamUrl
7501
+ });
7502
+ if (upstream.status >= 400) logWarn("sandbox.egress.upstream_response.failed", {
7503
+ ...attributes(lease, upstream.status),
7504
+ "error.type": `http_${upstream.status}`
7505
+ });
7506
+ if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
7507
+ logWarn("sandbox.egress.upstream_auth.rejected", {
7508
+ ...attributes(lease, upstream.status),
7509
+ "app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
7510
+ });
7511
+ await clearCredentialLease(provider, lease.grant, credentialContext);
7512
+ await recordAuthRequired({
7513
+ credentialContext,
7514
+ provider,
7515
+ grant: lease.grant,
7516
+ authorization: lease.authorization,
7517
+ message: `Provider rejected the injected ${provider} credential.`
7518
+ });
7519
+ await upstream.body?.cancel().catch(() => void 0);
7520
+ return authRequiredResponse({
7521
+ provider,
7522
+ grant: lease.grant,
7523
+ message: `Provider rejected the injected ${provider} credential.\n`
7524
+ });
7525
+ }
7526
+ if (upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS && !retriedAfter403) {
7527
+ logWarn("sandbox.egress.upstream_auth.rejected", { ...attributes(lease, upstream.status) });
7528
+ await clearCredentialLease(provider, lease.grant, credentialContext);
7529
+ await upstream.body?.cancel().catch(() => void 0);
7530
+ logWarn("sandbox.egress.upstream_auth.retrying", { ...attributes(lease, upstream.status) });
7531
+ leaseOrResponse = await resolveLease();
7532
+ if (leaseOrResponse instanceof Response) return leaseOrResponse;
7533
+ lease = leaseOrResponse;
7534
+ retriedAfter403 = true;
7535
+ continue;
7536
+ }
7537
+ if (upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS) {
7538
+ logWarn("sandbox.egress.upstream_auth.rejected", { ...attributes(lease, upstream.status) });
7539
+ await clearCredentialLease(provider, lease.grant, credentialContext);
7540
+ await recordPermissionDenied({
7541
+ credentialContext,
7542
+ provider,
7543
+ lease,
7544
+ message: pluginPermissionDenied?.message ?? permissionDeniedMessage(provider, lease.grant),
7545
+ upstream,
7546
+ upstreamUrl
7547
+ });
7548
+ if (pluginPermissionDenied) logWarn("sandbox.egress.upstream_permission.classified", { ...attributes(lease, upstream.status) });
7549
+ return new Response(upstream.body, {
7550
+ status: upstream.status,
7551
+ statusText: upstream.statusText,
7552
+ headers: responseHeaders(upstream)
7553
+ });
7554
+ }
7555
+ if (pluginPermissionDenied) {
7556
+ await recordPermissionDenied({
7557
+ credentialContext,
7558
+ provider,
7559
+ lease,
7560
+ message: pluginPermissionDenied.message,
7561
+ upstream,
7562
+ upstreamUrl
7563
+ });
7564
+ logWarn("sandbox.egress.upstream_permission.classified", { ...attributes(lease, upstream.status) });
7565
+ }
7566
+ return new Response(upstream.body, {
7567
+ status: upstream.status,
7568
+ statusText: upstream.statusText,
7569
+ headers: responseHeaders(upstream)
7570
+ });
7571
+ }
7572
+ }
7573
+ export { sandboxEgressSignalsResponseSchema as $, defaultTimezone as $n, getHeaderString as $t, setWorkspaceSnapshotBuild as A, createPluginLogger as An, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as At, wrapSandboxSetupError as B, createPluginConversationEvents as Bn, getWatch as Bt, listWorkspaces as C, SANDBOX_WORKSPACE_ROOT as Cn, tryWorkspaceRepoCheckoutPath as Ct, invalidateReadySnapshot as D, createPluginState as Dn, isSlackMessageTurn as Dt, WorkspaceValidationError as E, sandboxSkillPathResolution as En, getSlackToolContext as Et, ensureDockerDaemon as F, conversationHasParticipantUser as Fn, collectEventMatchKeys as Ft, isVercelSandboxEgressRequest as G, matchSlackAttachments as Gn, bindSlackDirectCredentialSubject as Gt, getWorkspaceSnapshotNotReadyError as H, readConversationEventPage as Hn, takeDueTimerWatches as Ht, getSandboxProvider as I, conversationIsMemberRoot as In, createTimerWatch as It, consumeSandboxEgressAuthRequiredSignal as J, resolveRootVisibility as Jn, verifySlackDirectCredentialSubject as Jt, proxyVercelSandboxEgressRequest as K, readAttachmentVisionSummary as Kn, verifyEventAutomationCredentialSubject as Kt, isAbortError as L, conversationNotArchivedForEmail as Ln, createWatch as Lt, create as M, conversationArchiveTimes as Mn, STOP_WATCHING_TOOL_NAME as Mt, hash as N, conversationArchivedForEmail as Nn, cancelSubscriptions as Nt, loadSnapshotsForProfile as O, createPluginEmbedder as On, slackMessageTurnDisabledReason as Ot, isStale as P, conversationArchivedForUser as Pn, cancelWatch as Pt, parseSandboxEgressAuthRequiredSignal as Q, listConversationAnnotationsById as Qn, getFilePermalink as Qt, isAlreadyExistsError as R, conversationNotArchivedForUser as Rn, deliverWatch as Rt, getWorkspaceByName as S, SANDBOX_SKILLS_ROOT as Sn, slackExternalRefIdSchema as St, updateWorkspace as T, sandboxSkillFile as Tn, resolveChannelCapabilities as Tt, isWorkspaceSnapshotNotReadyError as U, buildSentryConversationUrl as Un, bindEventAutomationCredentialSubject as Ut, WorkspaceSnapshotNotReadyError as V, readLatestConversationBrief as Vn, listWatches as Vt, workspaceSnapshotNotReadyUserMessage as W, getSlackFileId as Wn, bindScheduledAutomationCredentialSubject as Wt, createSandboxEgressCredentialToken as X, createPluginAnnotations as Xn, SlackActionError as Xt, consumeSandboxEgressPermissionDeniedSignal as Y, saveObjectAnnotations as Yn, verifyTimerWatchCredentialSubject as Yt, parseSandboxEgressCredentialToken as Z, listConversationAnnotations as Zn, downloadPrivateSlackFile as Zt, registration_exports as _, buildNonInteractiveShellScript as _n, openSlackDirectMessage as _t, agent_hooks_exports as a, withSlackRetries as an, agentsInstructionsUpdatedEvent as ar, formatProviderLabel as at, deleteWorkspace as b, SANDBOX_DATA_ROOT as bn, uploadFilesToConversation as bt, getPluginRoutes as c, runWithWorkspaceTeamId as cn, conversationNoteEvent as cr, startOAuthFlow as ct, getPluginTools as d, getDashboardObjectIconLink as dn, normalizeSlackReplyMarkdown as dt, getSlackClient as en, resolveUserTimezone as er, buildSandboxEgressNetworkPolicy as et, getPluginUserPromptContributions as f, getDashboardTaskLink as fn, normalizeSlackStatusText as ft, coreTaskRegistrations as g, buildCommandScript as gn, getSlackMessagePermalink as gt, validatePlugins as h, PluginToolDeniedError as hn, addReactionToMessage as ht, resolvePluginOAuthAccount as i, runWithSlackInstallationToken as in, JUNIOR_NATIVE_EVENT_NAMESPACE as ir, deliverOAuthAuthorization as it, SANDBOX_RUNTIME as j, readConversationAccessFromSql as jn, RESOURCE_WATCH_TOOL_SOURCE as jt, setWorkspaceSnapshot as k, createPluginModel as kn, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as kt, getPluginSlackConversationLink as l, JUNIOR_THREAD_STATE_TTL_MS as ln, escapeSlackMrkdwnText as lt, setPlugins as m, ToolInputError as mn, truncateStatusText as mt, createUserTokenStore as n, isDmChannel as nn, createPluginCache as nr, resolveSandboxEgressProviderForHost as nt, createPluginHookRunner as o, sleep$1 as on, authenticationLinkedEvent as or, parseOAuthStatePayload as ot, getPlugins as p, setDashboardConversationLinkOptions as pn, slackMention as pt, clearSandboxEgressSignals as q, recordSlackAttachment as qn, verifyScheduledAutomationCredentialSubject as qt, getOAuthAccountProviders as r, normalizeSlackConversationId as rn, createCache as rr, normalizeSandboxEgressTracePropagationDomains as rt, getPluginApiRoutes as s, getWorkspaceTeamId as sn, authenticationUnlinkedEvent as sr, resolveBaseUrl as st, executeCredentialedEgressRequest as t, isConversationScopedChannel as tn, lookupSlackUser as tr, resolveSandboxCommandEnvironment as tt, getPluginSystemPromptContributions as u, getDashboardConversationLink as un, formatSlackLink as ut, setBriefsConfig as v, runNonInteractiveCommand as vn, postSlackMessage as vt, listWorkspacesByRepository as w, sandboxSkillDir as wn, workspaceRepoCheckoutPath as wt, getWorkspace as x, SANDBOX_REPOS_ROOT as xn, slackEntitySchema as xt, createWorkspace as y, SANDBOX_COMMAND_PATH as yn, removeReactionFromMessage as yt, throwSandboxOperationError as z, viewerConversationMembership as zn, findMatchingWatches as zt };