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