@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,24 +1,22 @@
1
- import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-DpOwh_zX.js";
2
- import { V as toOptionalString, f as logException, m as logWarn, o as getActiveTraceId, p as logInfo, s as getLogContextAttributes, t as buildTurnFailureResponse, u as logAudit, y as setSpanAttributes, z as isRecord } from "./logging-DNQqsQ57.js";
3
- import { n as captureMessage } from "./sentry-BBUlHih9.js";
4
- import { n as botConfig, v as normalizeSlackEmojiName } from "./config-BxPrFVJP.js";
5
- import { c as toStoredSlackActor, f as parseSlackUserId, g as parseSlackMessageTs } from "./actor-tlxzqaRe.js";
6
- import { C as addAgentTurnUsage, R as parseDestination, c as getConversationEventStore, d as getDb, f as getSqlExecutor, m as withConversationEventLock, p as createSqlConversationEventStore, u as getConversationStore, x as bindProviderConversation, y as startLocationThread } from "./viewer-BuPwzOC9.js";
1
+ import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-B0YZfxNy.js";
2
+ import { n as botConfig } from "./config-C0UwYqDK.js";
3
+ import { V as toOptionalString, f as logException, m as logWarn, o as getActiveTraceId, t as buildTurnFailureResponse, y as setSpanAttributes, z as isRecord } from "./logging-Ce0mXZ8J.js";
4
+ import { c as toStoredSlackActor, g as parseSlackMessageTs } from "./actor-tlxzqaRe.js";
5
+ import { S as bindProviderConversation, b as startLocationThread, d as getConversationStore, f as getDb, h as withConversationEventLock, l as getConversationEventStore, m as createSqlConversationEventStore, p as getSqlExecutor, w as addAgentTurnUsage } from "./viewer-BpE6Z3qf.js";
7
6
  import { d as juniorDestinations } from "./agent-invocations-CxKldAeB.js";
8
- import { a as juniorAutomationRuns, l as juniorLocationConfigurations, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-D3WIS4vl.js";
9
- import { d as contextProvenance, f as instructionActors, n as agentHistoryItemSchema, p as instructionProvenanceFor } from "./history-AY8rs-FR.js";
10
- import { _ as CredentialUnavailableError, a as resolveAuthTokenPlaceholder, f as credentialContextForActor, h as credentialSubjectSchema, o as resolvePluginCommandEnv, p as credentialContextSchema } from "./registry-CfdUEk4N.js";
11
- import { n as pluginCatalogRuntime } from "./catalog-runtime-R53MWcMi.js";
12
- import { Et as createPluginLogger, Qt as createPluginCache, ct as getSlackClient, dt as normalizeSlackConversationId, en as JUNIOR_NATIVE_EVENT_NAMESPACE, it as SlackActionError, n as applyPluginFormatMarkdown, nn as authenticationLinkedEvent, pt as withSlackRetries, rn as authenticationUnlinkedEvent, tn as agentsInstructionsUpdatedEvent, u as getPlugins, ut as isDmChannel, xt as ToolInputError } from "./agent-hooks-D_PbluRX.js";
13
- import { a as acquireActiveLock, i as MUTATION_LOCK_TTL_MS, o as fenceLock, r as getStateAdapter, s as withLock } from "./adapter-BIbMSKlG.js";
14
- import { N as messageCardRef } from "./conversation-B0baEwoA.js";
15
- import { y as sleep } from "./profile-ieODOp4u.js";
16
- import { A as eventAutomationSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-CWX0qL8U.js";
7
+ import { a as juniorAutomationRuns, l as juniorLocationConfigurations, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-TnWbvL09.js";
8
+ import { d as contextProvenance, f as instructionActors, n as agentHistoryItemSchema, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
9
+ import { f as credentialContextForActor, h as credentialSubjectSchema } from "./registry-1BEIPdbT.js";
10
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-DAJjAbSx.js";
11
+ import { An as createPluginLogger, Q as parseSandboxEgressAuthRequiredSignal, ar as agentsInstructionsUpdatedEvent, at as formatProviderLabel, ct as startOAuthFlow, ir as JUNIOR_NATIVE_EVENT_NAMESPACE, mn as ToolInputError, nr as createPluginCache, nt as resolveSandboxEgressProviderForHost, on as sleep, or as authenticationLinkedEvent, p as getPlugins, sr as authenticationUnlinkedEvent, t as executeCredentialedEgressRequest } from "./credentialed-enF5Bf3y.js";
12
+ import { i as MUTATION_LOCK_TTL_MS, o as fenceLock, r as getStateAdapter, s as withLock } from "./adapter-eRFA_naf.js";
13
+ import { N as messageCardRef } from "./conversation-DF4rUcjb.js";
14
+ import { A as eventAutomationSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-BKpY9LPa.js";
17
15
  import { isDeepStrictEqual } from "node:util";
18
16
  import { z } from "zod";
19
- import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
17
+ import { createHash, randomUUID } from "node:crypto";
20
18
  import { and, asc, desc, eq, inArray, ne, or, sql } from "drizzle-orm";
21
- import { EgressAuthRequired, EgressPolicyDenied, actorSchema, eventMatches, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, sourceSchema } from "@sentry/junior-plugin-api";
19
+ import { actorSchema, eventMatches, sourceSchema } from "@sentry/junior-plugin-api";
22
20
  // src/chat/configuration/defaults.ts
23
21
  let installDefaults = {};
24
22
  function cloneDefaults(defaults) {
@@ -54,7 +52,7 @@ function authorizationObservationMessage(data, createdAtMs) {
54
52
  role: "user",
55
53
  content: [{
56
54
  type: "text",
57
- text: `${label} completed for provider "${data.provider}". Continue the request that waited for it and retry the provider operation. If the person dropped that request since, do not continue it; say only that ${data.provider} is connected.`
55
+ text: `${label} completed for provider "${data.provider}". Continue the newest request that waited for it and retry the provider operation. If a later message from the person dropped that request, do not continue it; say only that ${data.provider} is connected.`
58
56
  }],
59
57
  timestamp: createdAtMs
60
58
  });
@@ -397,2065 +395,6 @@ function createDurableLocationConfigurationService(args) {
397
395
  }
398
396
  });
399
397
  }
400
- // src/chat/plugins/credential-hooks.ts
401
- function parseSchema(schema, value, message) {
402
- const result = schema.safeParse(value);
403
- if (!result.success) throw new Error(message);
404
- return result.data;
405
- }
406
- function parseAuthorization(value, pluginName) {
407
- if (value === void 0) return;
408
- const authorization = parseSchema(pluginAuthorizationSchema, value, `Plugin "${pluginName}" grant authorization is invalid`);
409
- if (authorization.provider !== pluginName) throw new Error(`Plugin "${pluginName}" grant authorization provider must match the issuing plugin`);
410
- return authorization;
411
- }
412
- function parseGrant(value, pluginName) {
413
- return parseSchema(pluginGrantSchema, value, `Plugin "${pluginName}" grantForEgress returned an invalid grant`);
414
- }
415
- function pluginFor(provider) {
416
- return getPlugins().find((candidate) => candidate.manifest.name === provider);
417
- }
418
- function basePluginContext(plugin) {
419
- const pluginName = plugin.manifest.name;
420
- return {
421
- plugin: { name: pluginName },
422
- cache: createPluginCache(pluginName),
423
- log: createPluginLogger(pluginName),
424
- db: getDb()
425
- };
426
- }
427
- function parseCredentialResult(value, pluginName) {
428
- const result = parseSchema(pluginCredentialResultSchema, value, `Plugin "${pluginName}" issueCredential result is invalid`);
429
- if (result.type === "lease") {
430
- parseAuthorization(result.lease.authorization, pluginName);
431
- return result;
432
- }
433
- if (result.type === "unavailable") return result;
434
- parseAuthorization(result.authorization, pluginName);
435
- return result;
436
- }
437
- /** Ask a plugin which grant an outbound request needs. */
438
- async function selectPluginGrant(input) {
439
- const plugin = pluginFor(input.provider);
440
- const hook = plugin?.hooks?.grantForEgress;
441
- if (!plugin || !hook) return;
442
- const result = await hook({
443
- ...basePluginContext(plugin),
444
- request: {
445
- ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : void 0,
446
- method: input.method,
447
- ...input.operation ? { operation: input.operation } : void 0,
448
- url: input.upstreamUrl.toString()
449
- }
450
- });
451
- return result === void 0 ? void 0 : parseGrant(result, plugin.manifest.name);
452
- }
453
- /** Let the owning plugin inspect an upstream response without changing pass-through behavior. */
454
- async function onPluginEgressResponse(input) {
455
- const plugin = pluginFor(input.provider);
456
- const hook = plugin?.hooks?.onEgressResponse;
457
- if (!plugin || !hook) return {};
458
- let permissionDenied;
459
- await hook({
460
- ...basePluginContext(plugin),
461
- grant: input.grant,
462
- permissionDenied(message) {
463
- const trimmed = message.trim();
464
- if (!trimmed) throw new Error(`Plugin "${plugin.manifest.name}" onEgressResponse permissionDenied message is empty`);
465
- permissionDenied = { message: trimmed };
466
- },
467
- request: {
468
- method: input.method,
469
- ...input.operation ? { operation: input.operation } : void 0,
470
- url: input.upstreamUrl.toString()
471
- },
472
- response: input.response
473
- });
474
- return permissionDenied ? { permissionDenied } : {};
475
- }
476
- /** Return whether a plugin owns credential issuance for egress. */
477
- function hasEgressCredentialHooks(provider) {
478
- const hooks = pluginFor(provider)?.hooks;
479
- return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
480
- }
481
- /** Return providers that can verify an OAuth account for identity storage. */
482
- function getOAuthAccountProviders() {
483
- return getPlugins().filter((plugin) => Boolean(plugin.hooks?.resolveOAuthAccount)).map((plugin) => plugin.manifest.name);
484
- }
485
- /** Ask a plugin which provider account belongs to an OAuth token. */
486
- async function resolvePluginOAuthAccount(input) {
487
- const plugin = pluginFor(input.provider);
488
- const hook = plugin?.hooks?.resolveOAuthAccount;
489
- if (!plugin || !hook) return;
490
- const account = await hook({
491
- ...basePluginContext(plugin),
492
- tokenResponse: input.tokenResponse,
493
- tokens: input.tokens
494
- });
495
- return account === void 0 ? void 0 : parseSchema(pluginProviderAccountSchema, account, `Plugin "${plugin.manifest.name}" resolveOAuthAccount returned an invalid account`);
496
- }
497
- /** Ask a plugin to issue headers or describe why the selected grant is unavailable. */
498
- async function issuePluginCredential(input) {
499
- const plugin = pluginFor(input.provider);
500
- const hook = plugin?.hooks?.issueCredential;
501
- if (!plugin || !hook) throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
502
- const currentUserId = "type" in input.actor ? input.actor.userId : void 0;
503
- const credentialSubjectUserId = input.credentialSubject?.userId;
504
- return parseCredentialResult(await hook({
505
- ...basePluginContext(plugin),
506
- actor: input.actor,
507
- grant: input.grant,
508
- ...input.credentialSubject ? { credentialSubject: input.credentialSubject } : void 0,
509
- tokens: {
510
- ...currentUserId ? { currentUser: {
511
- userId: currentUserId,
512
- get: async () => await input.userTokenStore.get(currentUserId, plugin.manifest.name),
513
- set: async (tokens) => {
514
- await input.userTokenStore.set(currentUserId, plugin.manifest.name, tokens);
515
- },
516
- withRefresh: async (callback) => await input.userTokenStore.withRefresh(currentUserId, plugin.manifest.name, callback)
517
- } } : void 0,
518
- ...credentialSubjectUserId ? { credentialSubject: {
519
- userId: credentialSubjectUserId,
520
- get: async () => await input.userTokenStore.get(credentialSubjectUserId, plugin.manifest.name),
521
- set: async (tokens) => {
522
- await input.userTokenStore.set(credentialSubjectUserId, plugin.manifest.name, tokens);
523
- },
524
- withRefresh: async (callback) => await input.userTokenStore.withRefresh(credentialSubjectUserId, plugin.manifest.name, callback)
525
- } } : void 0
526
- }
527
- }), plugin.manifest.name);
528
- }
529
- // src/chat/slack/post-warning.ts
530
- const codes = /* @__PURE__ */ new Set([
531
- "invalid_metadata_format",
532
- "invalid_metadata_schema",
533
- "metadata_too_large",
534
- "invalid_arguments",
535
- "invalid_blocks",
536
- "missing_scope",
537
- "missing_charset",
538
- "superfluous_charset",
539
- "message_truncated"
540
- ]);
541
- const pathFields = /* @__PURE__ */ new Set([
542
- "metadata",
543
- "event_type",
544
- "event_payload",
545
- "entities",
546
- "entity_type",
547
- "entity_payload",
548
- "external_ref",
549
- "id",
550
- "type",
551
- "url",
552
- "attributes",
553
- "title",
554
- "text",
555
- "display_id",
556
- "display_type",
557
- "product_name",
558
- "fields",
559
- "status",
560
- "custom_fields",
561
- "key",
562
- "label",
563
- "value",
564
- "long",
565
- "format",
566
- "link",
567
- "display_order",
568
- "app_unfurl_url",
569
- "app_id",
570
- "created_by",
571
- "created_at",
572
- "updated_by",
573
- "updated_at",
574
- "full_size_preview",
575
- "preview_url",
576
- "mime_type"
577
- ]);
578
- const diagnosticWords = new Set(`WARN ERROR Message message metadata was incorrectly formatted The the will be
579
- ignored as a result For for event entity refer to following errors error
580
- missing required field fields property properties additional unexpected unknown
581
- unsupported invalid expected received actual type types must should match matches
582
- matching does not is are has have of at in on and or an only allowed allow
583
- value values string number integer boolean object array null minimum maximum
584
- length min max items item empty nonempty too long short large small size limit
585
- exceeded exceeds less greater than equal valid format schema validation failed
586
- failure cannot contain contains found one any all none enum pattern definition
587
- defined constraint constraints satisfy satisfies with without provided specified
588
- unrecognized unrecognised recognized recognised extraneous disallowed duplicate
589
- unique malformed nested root exactly needs need requires requirement supported
590
- json-pointer input different schema_uri schema_id schemas additionalProperties
591
- minLength maxLength minItems maxItems anyOf oneOf allOf`.split(/\s+/));
592
- function summarizeDiagnostic(value) {
593
- 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) => {
594
- if (token.startsWith("/")) return token.split("/").slice(0, 17).map((part, index) => index === 0 ? "" : pathFields.has(part) ? part : "*").join("/");
595
- return codes.has(token) || pathFields.has(token) || diagnosticWords.has(token) ? token : "[value]";
596
- }).replace(/\s+/g, " ").slice(0, 2e3);
597
- }
598
- /** Report Slack warnings once, on the request span, without retrying the post. */
599
- function captureSlackPostWarning(response, attributes) {
600
- const warnings = response.response_metadata?.warnings ?? [];
601
- const messages = response.response_metadata?.messages ?? [];
602
- const knownCodes = [...new Set(warnings.slice(0, 20).filter((code) => codes.has(code)))].sort();
603
- const diagnostics = {
604
- "app.slack.warning_count": warnings.length,
605
- "app.slack.response_message_count": messages.length,
606
- "app.slack.diagnostic_codes": knownCodes
607
- };
608
- setSpanAttributes(diagnostics);
609
- if (!warnings.length && !messages.length) return;
610
- captureMessage("Slack chat.postMessage returned warnings", {
611
- level: "warning",
612
- fingerprint: ["slack.chat.postMessage.warning", ...knownCodes.length ? knownCodes : ["unknown"]],
613
- tags: { "app.slack.method": "chat.postMessage" },
614
- extra: {
615
- ...getLogContextAttributes(),
616
- ...attributes,
617
- ...diagnostics,
618
- "app.slack.response_diagnostics": {
619
- warnings: warnings.slice(0, 20).map(summarizeDiagnostic),
620
- messages: messages.slice(0, 20).map(summarizeDiagnostic)
621
- }
622
- }
623
- });
624
- }
625
- // src/chat/slack/work-object.ts
626
- const url = z.url({ protocol: /^https?$/ });
627
- const textField = z.strictObject({
628
- type: z.literal("string"),
629
- key: z.string().min(1),
630
- label: z.string().min(1),
631
- value: z.string(),
632
- long: z.boolean().optional(),
633
- format: z.literal("markdown").optional(),
634
- link: url.optional()
635
- }).refine((field) => !field.format || !field.link, { message: "Slack string fields cannot combine format and link" });
636
- const customField = z.discriminatedUnion("type", [textField, z.strictObject({
637
- type: z.literal("slack#/types/timestamp"),
638
- key: z.string().min(1),
639
- label: z.string().min(1),
640
- value: z.number().int(),
641
- link: url.optional()
642
- })]);
643
- const payload = z.strictObject({
644
- attributes: z.strictObject({
645
- title: z.strictObject({ text: z.string().min(1) }),
646
- display_id: z.string().optional(),
647
- display_type: z.string().optional(),
648
- product_name: z.string().optional(),
649
- product_icon: z.strictObject({
650
- url,
651
- alt_text: z.string().min(1)
652
- }).optional()
653
- }),
654
- custom_fields: z.array(customField).optional(),
655
- display_order: z.array(z.string()).optional()
656
- });
657
- /** Limit IDs to Junior's safe subset; Slack silently rejects JSON punctuation. */
658
- const slackExternalRefIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, "Unsupported Slack external reference ID");
659
- const entity = z.strictObject({
660
- external_ref: z.strictObject({
661
- id: slackExternalRefIdSchema,
662
- type: z.string().min(1).optional()
663
- }),
664
- url
665
- });
666
- /** Validate notification and detail metadata before Slack can ignore bad fields. */
667
- const slackEntitySchema = z.discriminatedUnion("entity_type", [entity.extend({
668
- entity_type: z.literal("slack#/entities/item"),
669
- entity_payload: payload.extend({ fields: z.never().optional() })
670
- }), entity.extend({
671
- entity_type: z.literal("slack#/entities/task"),
672
- entity_payload: payload.extend({ fields: z.strictObject({ status: z.strictObject({ value: z.string() }).optional() }).optional() })
673
- })]);
674
- // src/chat/slack/outbound.ts
675
- const MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
676
- function requireSlackConversationId(channelId, action) {
677
- const normalized = normalizeSlackConversationId(channelId);
678
- if (!normalized) throw new Error(`${action} requires a valid channel ID`);
679
- return normalized;
680
- }
681
- function requireSlackThreadTimestamp(threadTs, action) {
682
- const normalized = threadTs.trim();
683
- if (!normalized) throw new Error(`${action} requires a thread timestamp`);
684
- return normalized;
685
- }
686
- function requireSlackMessageTimestamp(timestamp, action) {
687
- const normalized = parseSlackMessageTs(timestamp);
688
- if (!normalized) throw new Error(`${action} requires a target message timestamp`);
689
- return normalized;
690
- }
691
- function requireSlackMessageText(text, action) {
692
- if (text.trim().length === 0) throw new Error(`${action} requires non-empty text`);
693
- if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) throw new Error(`${action} text exceeds Slack's 40000 character truncation limit`);
694
- return text;
695
- }
696
- /** Resolve a Slack message permalink without making read-only workflows fail on lookup errors. */
697
- async function getSlackMessagePermalink(args) {
698
- try {
699
- return (await withSlackRetries(() => getSlackClient().chat.getPermalink({
700
- channel: args.channelId,
701
- message_ts: args.messageTs
702
- }), 3, {
703
- action: "chat.getPermalink",
704
- idempotent: true,
705
- spanAttributes: {
706
- "app.slack.channel_id": args.channelId,
707
- "app.slack.message_ts": args.messageTs
708
- }
709
- })).permalink;
710
- } catch {
711
- return;
712
- }
713
- }
714
- /** Open the bot's direct message with one Slack user and return its channel ID. */
715
- async function openSlackDirectMessage(userId) {
716
- const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
717
- if (!channelId) throw new Error("Slack did not return a direct message channel.");
718
- return channelId;
719
- }
720
- /** Post Slack `mrkdwn` text to a conversation or thread via the shared outbound boundary. */
721
- async function postSlackMessage(input) {
722
- const channelId = requireSlackConversationId(input.channelId, "Slack message posting");
723
- const text = requireSlackMessageText(input.text, "Slack message posting");
724
- const entities = input.entities?.length ? slackEntitySchema.array().parse(input.entities) : void 0;
725
- const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack thread message posting") : void 0;
726
- const attributes = {
727
- "app.slack.channel_id": channelId,
728
- ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0,
729
- "app.slack.work_object.count": entities?.length ?? 0
730
- };
731
- const response = await withSlackRetries(async () => {
732
- const response = await getSlackClient().chat.postMessage({
733
- channel: channelId,
734
- text,
735
- unfurl_links: false,
736
- unfurl_media: false,
737
- ...input.blocks?.length ? { blocks: input.blocks } : void 0,
738
- ...entities ? { metadata: { entities } } : void 0,
739
- ...threadTs ? { thread_ts: threadTs } : void 0
740
- });
741
- const messageId = parseSlackMessageTs(response.ts);
742
- setSpanAttributes({ "messaging.message.id": messageId });
743
- captureSlackPostWarning(response, {
744
- ...attributes,
745
- "messaging.message.id": messageId
746
- });
747
- return response;
748
- }, 3, {
749
- action: "chat.postMessage",
750
- attributes
751
- });
752
- const messageTs = parseSlackMessageTs(response.ts);
753
- if (!messageTs) throw new Error("Slack message posted without ts");
754
- return {
755
- ts: messageTs,
756
- ...input.includePermalink ? { permalink: await getSlackMessagePermalink({
757
- channelId,
758
- messageTs
759
- }) } : void 0
760
- };
761
- }
762
- /**
763
- * Post an ephemeral Slack message. Delivery is best-effort on Slack's side, but
764
- * request validation and Web API behavior are centralized here.
765
- */
766
- async function postSlackEphemeralMessage(input) {
767
- const channelId = requireSlackConversationId(input.channelId, "Slack ephemeral message posting");
768
- const userId = parseSlackUserId(input.userId);
769
- if (!userId) throw new Error("Slack ephemeral message posting requires a user ID");
770
- const text = requireSlackMessageText(input.text, "Slack ephemeral message posting");
771
- const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack ephemeral thread message posting") : void 0;
772
- return { messageTs: (await withSlackRetries(() => getSlackClient().chat.postEphemeral({
773
- channel: channelId,
774
- user: userId,
775
- text,
776
- ...input.blocks?.length ? { blocks: input.blocks } : void 0,
777
- ...threadTs ? { thread_ts: threadTs } : void 0
778
- }), 3, {
779
- action: "chat.postEphemeral",
780
- spanAttributes: {
781
- "app.slack.channel_id": channelId,
782
- "app.slack.user_id": userId,
783
- ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
784
- }
785
- })).message_ts };
786
- }
787
- /** Upload files into a Slack conversation or thread via the shared outbound file boundary. */
788
- async function uploadFilesToConversation(input) {
789
- const channelId = requireSlackConversationId(input.channelId, "Slack file upload");
790
- const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack file upload") : void 0;
791
- if (input.files.length === 0) throw new Error("Slack file upload requires at least one file");
792
- const fileUploads = input.files.map((file) => {
793
- const filename = file.filename.trim();
794
- if (!filename) throw new Error("Slack file upload requires every file to have a filename");
795
- return {
796
- file: file.data,
797
- filename
798
- };
799
- });
800
- return { files: ((await withSlackRetries(() => getSlackClient().filesUploadV2({
801
- channel_id: channelId,
802
- ...threadTs ? { thread_ts: threadTs } : void 0,
803
- file_uploads: fileUploads
804
- }), 3, {
805
- action: "filesUploadV2",
806
- spanAttributes: {
807
- "app.slack.channel_id": channelId,
808
- ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
809
- }
810
- })).files ?? []).flatMap((completion) => (completion.files ?? []).map((file) => file.id ? { id: file.id } : {})) };
811
- }
812
- /** Add a reaction to a Slack message, treating `already_reacted` as idempotent success. */
813
- async function addReactionToMessage(input) {
814
- const channelId = requireSlackConversationId(input.channelId, "Slack reaction");
815
- const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction");
816
- const emoji = normalizeSlackEmojiName(input.emoji);
817
- if (!emoji) throw new Error("Slack reaction requires a valid emoji alias name");
818
- try {
819
- await withSlackRetries(() => getSlackClient(input.timeoutMs === void 0 ? void 0 : { timeoutMs: input.timeoutMs }).reactions.add({
820
- channel: channelId,
821
- timestamp,
822
- name: emoji
823
- }), input.timeoutMs === void 0 ? 3 : 1, {
824
- action: "reactions.add",
825
- idempotent: true,
826
- spanAttributes: {
827
- "app.slack.channel_id": channelId,
828
- "app.slack.message_ts": timestamp,
829
- "app.slack.reaction": emoji
830
- }
831
- });
832
- } catch (error) {
833
- if (error instanceof SlackActionError && error.code === "already_reacted") return { ok: true };
834
- throw error;
835
- }
836
- return { ok: true };
837
- }
838
- /** Remove a reaction from a Slack message, treating `no_reaction` as idempotent success. */
839
- async function removeReactionFromMessage(input) {
840
- const channelId = requireSlackConversationId(input.channelId, "Slack reaction removal");
841
- const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction removal");
842
- const emoji = normalizeSlackEmojiName(input.emoji);
843
- if (!emoji) throw new Error("Slack reaction removal requires a valid emoji alias name");
844
- try {
845
- await withSlackRetries(() => getSlackClient().reactions.remove({
846
- channel: channelId,
847
- timestamp,
848
- name: emoji
849
- }), 3, {
850
- action: "reactions.remove",
851
- idempotent: true,
852
- spanAttributes: {
853
- "app.slack.channel_id": channelId,
854
- "app.slack.message_ts": timestamp,
855
- "app.slack.reaction": emoji
856
- }
857
- });
858
- } catch (error) {
859
- if (error instanceof SlackActionError && error.code === "no_reaction") return { ok: true };
860
- throw error;
861
- }
862
- return { ok: true };
863
- }
864
- // src/chat/slack/status-format.ts
865
- const SLACK_STATUS_MAX_LENGTH = 50;
866
- /** Truncate a status string to fit Slack's 50-char assistant status limit. */
867
- function truncateStatusText(text) {
868
- const trimmed = text.trim();
869
- if (!trimmed) return "";
870
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) return trimmed;
871
- return `${trimmed.slice(0, 47).trimEnd()}...`;
872
- }
873
- // src/chat/slack/mrkdwn.ts
874
- /** Escape dynamic text for Slack mrkdwn without changing intended formatting. */
875
- function escapeSlackMrkdwnText(text) {
876
- return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
877
- }
878
- /** Escape a URL for Slack explicit link syntax while preserving query semantics. */
879
- function escapeSlackLinkUrl(url) {
880
- return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
881
- }
882
- /** Build a Slack explicit link from dynamic URL and label values. */
883
- function formatSlackLink(url, label) {
884
- return `<${escapeSlackLinkUrl(url)}|${escapeSlackMrkdwnText(label)}>`;
885
- }
886
- function readInlineCodeSpan(line, start) {
887
- if (line[start] !== "`") return;
888
- let n = 1;
889
- while (line[start + n] === "`") n++;
890
- const marker = "`".repeat(n);
891
- let search = start + n;
892
- while (search < line.length) {
893
- const close = line.indexOf(marker, search);
894
- if (close === -1) return;
895
- const after = close + n;
896
- if (line[after] !== "`") return {
897
- text: line.slice(start, after),
898
- end: after
899
- };
900
- search = after + 1;
901
- }
902
- }
903
- function readExistingSlackAngleToken(line, start) {
904
- if (line[start] !== "<") return;
905
- const close = line.indexOf(">", start + 1);
906
- if (close === -1) return;
907
- const body = line.slice(start + 1, close);
908
- if (/^(?:https?:\/\/|@|#|!)/.test(body)) return {
909
- text: line.slice(start, close + 1),
910
- end: close + 1
911
- };
912
- }
913
- function readMarkdownLink(line, start) {
914
- if (line[start] !== "[") return;
915
- const labelEnd = line.indexOf("](", start + 1);
916
- if (labelEnd === -1) return;
917
- const destStart = labelEnd + 2;
918
- if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) return;
919
- const closeParens = line.indexOf(")", destStart);
920
- if (closeParens === -1) return;
921
- return {
922
- text: line.slice(start, closeParens + 1),
923
- end: closeParens + 1
924
- };
925
- }
926
- function hasUnmatchedClosingParen(text) {
927
- let balance = 0;
928
- for (const ch of text) if (ch === "(") balance++;
929
- else if (ch === ")") balance--;
930
- return balance < 0;
931
- }
932
- function readBareUrl(line, start) {
933
- let end = start;
934
- while (end < line.length) {
935
- const ch = line.charAt(end);
936
- if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === "\"" || ch === "`" || ch === "|" || ch === "*") break;
937
- end++;
938
- }
939
- if (end === start) return;
940
- let raw = line.slice(start, end);
941
- let suffix = "";
942
- const peel = () => {
943
- suffix = raw.slice(-1) + suffix;
944
- raw = raw.slice(0, -1);
945
- };
946
- const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
947
- while (raw.length > 0 && shouldPeel()) peel();
948
- if (!/^https?:\/\/.+/.test(raw)) return;
949
- return {
950
- url: raw,
951
- suffix,
952
- end
953
- };
954
- }
955
- const BARE_SLACK_USER_ID_RE = /^@([UW](?=[A-Z0-9]*\d)[A-Z0-9]{8,})(?![\w])/;
956
- function readBareSlackUserMention(line, start) {
957
- if (line[start] !== "@" || /\w/.test(line[start - 1] ?? "")) return;
958
- const match = BARE_SLACK_USER_ID_RE.exec(line.slice(start));
959
- if (!match) return;
960
- return {
961
- text: `<@${match[1]}>`,
962
- end: start + match[0].length
963
- };
964
- }
965
- /**
966
- * Wrap bare http(s) URLs and bare `@U123` Slack user IDs on a single line as
967
- * Slack explicit `<url>` links and `<@U123>` mentions.
968
- */
969
- function wrapBareReferencesOnLine(line) {
970
- let result = "";
971
- let i = 0;
972
- while (i < line.length) {
973
- const codeSpan = readInlineCodeSpan(line, i);
974
- if (codeSpan) {
975
- result += codeSpan.text;
976
- i = codeSpan.end;
977
- continue;
978
- }
979
- const angleToken = readExistingSlackAngleToken(line, i);
980
- if (angleToken) {
981
- result += angleToken.text;
982
- i = angleToken.end;
983
- continue;
984
- }
985
- const mdLink = readMarkdownLink(line, i);
986
- if (mdLink) {
987
- result += mdLink.text;
988
- i = mdLink.end;
989
- continue;
990
- }
991
- const userMention = readBareSlackUserMention(line, i);
992
- if (userMention) {
993
- result += userMention.text;
994
- i = userMention.end;
995
- continue;
996
- }
997
- if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
998
- const parsed = readBareUrl(line, i);
999
- if (parsed) {
1000
- result += `<${parsed.url}>${parsed.suffix}`;
1001
- i = parsed.end;
1002
- continue;
1003
- }
1004
- }
1005
- result += line[i];
1006
- i++;
1007
- }
1008
- return result;
1009
- }
1010
- /**
1011
- * Pre-wrap bare http(s) URLs outside fenced code blocks as Slack explicit
1012
- * links, preventing Slack's auto-linker from consuming adjacent formatting
1013
- * markers into the URL. Bare `@U123` user IDs become `<@U123>` mentions,
1014
- * because Slack shows a bare ID as literal text.
1015
- *
1016
- * Uses the same fence-toggle rule as `ensureBlockSpacing` so both passes
1017
- * agree on which lines are code.
1018
- */
1019
- function wrapBareReferences(text) {
1020
- const lines = text.split("\n");
1021
- const out = [];
1022
- let inCodeBlock = false;
1023
- for (const line of lines) {
1024
- if (line.trimStart().startsWith("```")) {
1025
- inCodeBlock = !inCodeBlock;
1026
- out.push(line);
1027
- continue;
1028
- }
1029
- out.push(inCodeBlock ? line : wrapBareReferencesOnLine(line));
1030
- }
1031
- return out.join("\n");
1032
- }
1033
- const LIST_ITEM_PATTERN = /^[-*•]\s|^\d+\.\s/;
1034
- /**
1035
- * Turn a follow-up paragraph inside a list item into a bullet at the same
1036
- * indent.
1037
- *
1038
- * Slack's `markdown` block keeps only one paragraph per list item. It joins
1039
- * the other paragraphs to the top-level item's text with no space and renders
1040
- * nested lists after them. As a bullet, the paragraph keeps its place and
1041
- * nesting.
1042
- */
1043
- function bulletListFollowUpParagraphs(text) {
1044
- const lines = text.split("\n");
1045
- let inCodeBlock = false;
1046
- let inList = false;
1047
- for (const [i, line] of lines.entries()) {
1048
- const trimmed = line.trimStart();
1049
- const isFence = trimmed.startsWith("```");
1050
- if (!inCodeBlock && trimmed && trimmed.length === line.length) inList = LIST_ITEM_PATTERN.test(line);
1051
- 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}`;
1052
- if (isFence) inCodeBlock = !inCodeBlock;
1053
- }
1054
- return lines.join("\n");
1055
- }
1056
- /** Insert blank lines between content blocks so Slack renders them with visual separation. */
1057
- function ensureBlockSpacing(text) {
1058
- const codeBlockPattern = /^```/;
1059
- const lines = text.split("\n");
1060
- const result = [];
1061
- let inCodeBlock = false;
1062
- for (const line of lines) {
1063
- if (codeBlockPattern.test(line.trimStart())) {
1064
- if (!inCodeBlock) {
1065
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
1066
- if (prev !== void 0 && prev.trim() !== "") result.push("");
1067
- }
1068
- inCodeBlock = !inCodeBlock;
1069
- result.push(line);
1070
- continue;
1071
- }
1072
- if (inCodeBlock) {
1073
- result.push(line);
1074
- continue;
1075
- }
1076
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
1077
- const isListItem = LIST_ITEM_PATTERN.test(line.trimStart());
1078
- const isListContinuation = prev !== void 0 && !isListItem && /^\s/.test(line) && (/^\s/.test(prev) || LIST_ITEM_PATTERN.test(prev));
1079
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !isListContinuation && !(LIST_ITEM_PATTERN.test(prev.trimStart()) && isListItem)) result.push("");
1080
- result.push(line);
1081
- }
1082
- return result.join("\n");
1083
- }
1084
- /**
1085
- * Normalize model-authored Slack markdown for delivery via `markdown_text`
1086
- * or `{ type: "markdown" }` blocks.
1087
- *
1088
- * Plugins may first rewrite ordinary Markdown. Core then applies Slack-only
1089
- * delivery shaping: bare URL wrapping, block spacing, and list follow-up
1090
- * paragraph bullets. Slack reply delivery owns chunking and continuation
1091
- * markers separately.
1092
- */
1093
- function normalizeSlackReplyMarkdown(text) {
1094
- let normalized = applyPluginFormatMarkdown(text).replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
1095
- normalized = wrapBareReferences(normalized);
1096
- normalized = ensureBlockSpacing(normalized);
1097
- normalized = bulletListFollowUpParagraphs(normalized);
1098
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
1099
- }
1100
- /** Normalize assistant status text before handing it to Slack. */
1101
- function normalizeSlackStatusText(text) {
1102
- const trimmed = text.trim();
1103
- if (!trimmed) return "";
1104
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
1105
- }
1106
- /** Render a Slack user mention, which Slack shows as the person's name. */
1107
- function slackMention(userId) {
1108
- return `<@${userId}>`;
1109
- }
1110
- // src/chat/slack/oauth-authorization-message.ts
1111
- const ACCOUNT_SCOPE_NOTE = "This connects only your account. Junior sees only what you can see.";
1112
- /** Present private OAuth authorization with a clear action and text fallback. */
1113
- function buildSlackOAuthAuthorizationMessage(args) {
1114
- return {
1115
- text: `${formatSlackLink(args.authorizationUrl, args.label)}. ${ACCOUNT_SCOPE_NOTE} ${args.completionText}`,
1116
- blocks: [{
1117
- type: "section",
1118
- text: {
1119
- type: "mrkdwn",
1120
- text: `*${escapeSlackMrkdwnText(args.label)}*\n${ACCOUNT_SCOPE_NOTE}\n${escapeSlackMrkdwnText(args.completionText)}`
1121
- },
1122
- accessory: {
1123
- type: "button",
1124
- text: {
1125
- type: "plain_text",
1126
- text: "Connect"
1127
- },
1128
- style: "primary",
1129
- url: args.authorizationUrl,
1130
- action_id: "oauth_connect"
1131
- }
1132
- }]
1133
- };
1134
- }
1135
- // src/chat/oauth-flow.ts
1136
- const OAUTH_STATE_TTL_MS = 6e5;
1137
- function optionalString(value) {
1138
- return typeof value === "string" ? value : void 0;
1139
- }
1140
- /** Parse OAuth callback state that was persisted before a provider redirect. */
1141
- function parseOAuthStatePayload(value) {
1142
- if (!isRecord(value)) return;
1143
- if (typeof value.userId !== "string" || typeof value.provider !== "string") return;
1144
- const actor = value.actor === void 0 ? void 0 : actorSchema.safeParse(value.actor);
1145
- if (value.actor !== void 0 && (!actor || !actor.success)) return;
1146
- const destination = parseDestination(value.destination);
1147
- if (value.destination !== void 0 && !destination) return;
1148
- const source = value.source === void 0 ? void 0 : sourceSchema.safeParse(value.source);
1149
- if (value.source !== void 0 && (!source || !source.success)) return;
1150
- return {
1151
- userId: value.userId,
1152
- provider: value.provider,
1153
- ...actor?.success ? { actor: actor.data } : void 0,
1154
- ...optionalString(value.channelId) ? { channelId: optionalString(value.channelId) } : void 0,
1155
- ...destination ? { destination } : void 0,
1156
- ...source?.success ? { source: source.data } : void 0,
1157
- ...optionalString(value.threadTs) ? { threadTs: optionalString(value.threadTs) } : void 0,
1158
- ...optionalString(value.resumeConversationId) ? { resumeConversationId: optionalString(value.resumeConversationId) } : void 0,
1159
- ...optionalString(value.resumeSessionId) ? { resumeSessionId: optionalString(value.resumeSessionId) } : void 0,
1160
- ...optionalString(value.scope) ? { scope: optionalString(value.scope) } : void 0
1161
- };
1162
- }
1163
- /** Return the manifest-owned display label for a provider. */
1164
- function formatProviderLabel(provider) {
1165
- const displayName = pluginCatalogRuntime.getDisplayName(provider);
1166
- if (!displayName) throw new Error(`Unknown plugin provider display name: "${provider}"`);
1167
- return displayName;
1168
- }
1169
- /** Resolve the public base URL from environment variables (JUNIOR_BASE_URL or Vercel). */
1170
- function resolveBaseUrl() {
1171
- const explicit = process.env.JUNIOR_BASE_URL?.trim();
1172
- if (explicit) return explicit;
1173
- const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
1174
- if (vercelProd) return `https://${vercelProd}`;
1175
- const vercelUrl = process.env.VERCEL_URL?.trim();
1176
- if (vercelUrl) return `https://${vercelUrl}`;
1177
- }
1178
- /**
1179
- * Authorization links must only be visible to the requesting user.
1180
- * Try in-context private delivery first, then fall back to a DM.
1181
- */
1182
- async function deliverPrivateMessage(input) {
1183
- let client;
1184
- try {
1185
- client = getSlackClient();
1186
- } catch {
1187
- logWarn("oauth.private_delivery.skipped", { "app.reason": "missing_bot_token" });
1188
- return false;
1189
- }
1190
- if (input.channelId) try {
1191
- if (isDmChannel(input.channelId)) await postSlackMessage({
1192
- channelId: input.channelId,
1193
- text: input.text,
1194
- blocks: input.blocks,
1195
- threadTs: input.threadTs
1196
- });
1197
- else await postSlackEphemeralMessage({
1198
- channelId: input.channelId,
1199
- userId: input.userId,
1200
- text: input.text,
1201
- blocks: input.blocks,
1202
- threadTs: input.threadTs
1203
- });
1204
- return "in_context";
1205
- } catch (error) {
1206
- logWarn("oauth.private_delivery.failed", {
1207
- "app.slack.error": error instanceof Error ? error.message : String(error),
1208
- "app.slack.channel": input.channelId
1209
- });
1210
- }
1211
- try {
1212
- const dmChannelId = (await withSlackRetries(() => client.conversations.open({ users: input.userId }), 3, {
1213
- action: "conversations.open",
1214
- spanAttributes: { "app.slack.user_id": input.userId }
1215
- })).channel?.id;
1216
- if (!dmChannelId) {
1217
- logWarn("oauth.dm.fallback.failed", { "app.reason": "no_dm_channel_id" });
1218
- return false;
1219
- }
1220
- await postSlackMessage({
1221
- channelId: dmChannelId,
1222
- text: input.text,
1223
- blocks: input.blocks
1224
- });
1225
- return "fallback_dm";
1226
- } catch (error) {
1227
- logWarn("oauth.dm.fallback.failed", { "app.slack.error": error instanceof Error ? error.message : String(error) });
1228
- return false;
1229
- }
1230
- }
1231
- /** Present one OAuth request through the active surface's private delivery. */
1232
- async function deliverOAuthAuthorization(request, input) {
1233
- if (input.authorization) {
1234
- await input.authorization.deliver(request);
1235
- return "in_context";
1236
- }
1237
- return await deliverPrivateMessage({
1238
- channelId: input.channelId,
1239
- threadTs: input.threadTs,
1240
- userId: input.userId,
1241
- ...buildSlackOAuthAuthorizationMessage(request)
1242
- });
1243
- }
1244
- /** Initiate an OAuth authorization code flow for a provider and deliver the auth link to the user. */
1245
- async function startOAuthFlow(provider, input) {
1246
- const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
1247
- if (!providerConfig) return {
1248
- ok: false,
1249
- error: `Provider "${provider}" does not support OAuth authorization`
1250
- };
1251
- const clientId = process.env[providerConfig.clientIdEnv]?.trim();
1252
- if (!clientId) return {
1253
- ok: false,
1254
- error: `Missing ${providerConfig.clientIdEnv} environment variable`
1255
- };
1256
- const baseUrl = resolveBaseUrl();
1257
- if (!baseUrl) return {
1258
- ok: false,
1259
- error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
1260
- };
1261
- const state = input.authorization ? await input.authorization.createState() : randomBytes(32).toString("hex");
1262
- const requestedScope = input.scope ?? providerConfig.scope;
1263
- await getStateAdapter().set(`oauth-state:${state}`, {
1264
- userId: input.actorId,
1265
- provider,
1266
- ...input.actor ? { actor: input.actor } : void 0,
1267
- ...input.channelId ? { channelId: input.channelId } : void 0,
1268
- ...input.destination ? { destination: input.destination } : void 0,
1269
- ...input.source ? { source: input.source } : void 0,
1270
- ...input.threadTs ? { threadTs: input.threadTs } : void 0,
1271
- ...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : void 0,
1272
- ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : void 0,
1273
- ...requestedScope ? { scope: requestedScope } : void 0
1274
- }, OAUTH_STATE_TTL_MS);
1275
- const authorizeParams = new URLSearchParams({
1276
- client_id: clientId,
1277
- state,
1278
- redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
1279
- response_type: "code"
1280
- });
1281
- if (requestedScope) authorizeParams.set("scope", requestedScope);
1282
- for (const [key, value] of Object.entries(providerConfig.authorizeParams ?? {})) authorizeParams.set(key, value);
1283
- logInfo("jr_rpc.oauth.started", {
1284
- "app.credential.provider": provider,
1285
- ...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : void 0
1286
- });
1287
- return {
1288
- ok: true,
1289
- delivery: await deliverOAuthAuthorization({
1290
- authorizationUrl: `${providerConfig.authorizeEndpoint}?${authorizeParams.toString()}`,
1291
- label: `Connect your ${formatProviderLabel(provider)} account`,
1292
- completionText: input.resumeSessionId ? "Once you've authorized, Junior will continue automatically." : "Once you've authorized, you'll see a confirmation in Slack."
1293
- }, {
1294
- authorization: input.authorization,
1295
- channelId: input.channelId,
1296
- threadTs: input.threadTs,
1297
- userId: input.actorId
1298
- })
1299
- };
1300
- }
1301
- // src/chat/sandbox/egress/schemas.ts
1302
- const finiteNumberSchema = z.number().refine(Number.isFinite);
1303
- const httpStatusSchema = z.number().int().min(100).max(599);
1304
- const providerNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
1305
- const credentialSignalKindSchema = z.enum(["auth_required", "unavailable"]);
1306
- const sandboxEgressGrantSchema = pluginGrantSchema;
1307
- const sandboxEgressCredentialContextSchema = z.object({
1308
- credentials: credentialContextSchema,
1309
- /** Conversation that the credentials act for. Telemetry only; it grants nothing. */
1310
- conversationId: z.string().min(1).optional(),
1311
- egressId: z.string().min(1),
1312
- expiresAtMs: finiteNumberSchema,
1313
- contextId: z.string().min(1)
1314
- }).strict();
1315
- const sandboxEgressCredentialLeaseSchema = z.object({
1316
- account: pluginProviderAccountSchema.optional(),
1317
- authorization: pluginAuthorizationSchema.optional(),
1318
- grant: sandboxEgressGrantSchema,
1319
- provider: providerNameSchema,
1320
- expiresAt: z.string().min(1),
1321
- headerTransforms: z.array(pluginCredentialHeaderTransformSchema).min(1)
1322
- }).strict();
1323
- const sandboxEgressAuthRequiredSignalSchema = z.object({
1324
- authorization: pluginAuthorizationSchema.optional(),
1325
- grant: sandboxEgressGrantSchema,
1326
- kind: credentialSignalKindSchema.default("auth_required"),
1327
- provider: providerNameSchema,
1328
- message: z.string().optional(),
1329
- createdAtMs: finiteNumberSchema
1330
- }).strict().superRefine((signal, ctx) => {
1331
- if (signal.authorization && signal.authorization.provider !== signal.provider) ctx.addIssue({
1332
- code: z.ZodIssueCode.custom,
1333
- message: "Auth signal authorization provider must match provider",
1334
- path: ["authorization", "provider"]
1335
- });
1336
- });
1337
- const sandboxEgressPermissionDeniedSignalSchema = z.object({
1338
- account: pluginProviderAccountSchema.optional(),
1339
- grant: sandboxEgressGrantSchema,
1340
- message: z.string().min(1),
1341
- provider: providerNameSchema,
1342
- source: z.literal("upstream"),
1343
- status: httpStatusSchema,
1344
- upstreamHost: z.string().min(1),
1345
- upstreamPath: z.string().min(1),
1346
- createdAtMs: finiteNumberSchema
1347
- }).strict();
1348
- const sandboxEgressSignalsResponseSchema = z.object({
1349
- auth_required: sandboxEgressAuthRequiredSignalSchema.optional(),
1350
- permission_denied: sandboxEgressPermissionDeniedSignalSchema.optional()
1351
- }).strict();
1352
- /** Parse a host-owned sandbox egress auth signal from state or tool results. */
1353
- function parseSandboxEgressAuthRequiredSignal(value) {
1354
- const result = sandboxEgressAuthRequiredSignalSchema.safeParse(value);
1355
- return result.success ? result.data : void 0;
1356
- }
1357
- /** Parse a host-owned sandbox egress permission-denied signal from state or tool results. */
1358
- function parseSandboxEgressPermissionDeniedSignal(value) {
1359
- const result = sandboxEgressPermissionDeniedSignalSchema.safeParse(value);
1360
- return result.success ? result.data : void 0;
1361
- }
1362
- // src/chat/sandbox/egress/session.ts
1363
- const SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
1364
- const SANDBOX_EGRESS_TOKEN_VERSION = "v1";
1365
- const SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
1366
- const SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX = "sandbox-egress-auth-required";
1367
- const SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX = "sandbox-egress-permission-denied";
1368
- const SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
1369
- const DEFAULT_SESSION_TTL_MS = 18e5;
1370
- /** Treat a cached lease as expired this long before its provider expiry. */
1371
- const LEASE_REFRESH_BUFFER_MS = 3e5;
1372
- /** True when this grant may keep shared auth headers on the host. */
1373
- function isSharedInstallationGrant(grant) {
1374
- return grant.name.startsWith("installation-");
1375
- }
1376
- /**
1377
- * Build the host key for a shared installation grant.
1378
- *
1379
- * Only installation grants are remembered. User and other grants issue live
1380
- * headers each time, so they never write a host key.
1381
- */
1382
- function leaseKey(provider, grant) {
1383
- return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${grant.name}:shared`;
1384
- }
1385
- /**
1386
- * Build the command signal key for credential issuance failures by access level.
1387
- */
1388
- function authSignalKey(egressId, access) {
1389
- return `${SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX}:${egressId}:${access}`;
1390
- }
1391
- /**
1392
- * Build the command signal key for upstream permission denials by access level.
1393
- */
1394
- function permissionSignalKey(egressId, access) {
1395
- return `${SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX}:${egressId}:${access}`;
1396
- }
1397
- function base64Url(input) {
1398
- return Buffer.from(input, "utf8").toString("base64url");
1399
- }
1400
- function fromBase64Url(input) {
1401
- return Buffer.from(input, "base64url").toString("utf8");
1402
- }
1403
- /**
1404
- * Sign a versioned context payload in the sandbox egress HMAC domain.
1405
- *
1406
- * `JUNIOR_SECRET` is the shared secret so proxy URLs cannot be forged by code
1407
- * running inside the sandbox.
1408
- */
1409
- function signPayload(payload) {
1410
- return createHmac("sha256", getSandboxEgressSecret()).update(`${SANDBOX_EGRESS_HMAC_CONTEXT}:${payload}`).digest("base64url");
1411
- }
1412
- function timingSafeMatch(expected, actual) {
1413
- const expectedBuffer = Buffer.from(expected);
1414
- const actualBuffer = Buffer.from(actual);
1415
- if (expectedBuffer.length !== actualBuffer.length) return false;
1416
- return timingSafeEqual(expectedBuffer, actualBuffer);
1417
- }
1418
- function parseSandboxEgressContext(value) {
1419
- const result = sandboxEgressCredentialContextSchema.safeParse(value);
1420
- if (!result.success) return;
1421
- if (result.data.expiresAtMs <= Date.now()) return;
1422
- return result.data;
1423
- }
1424
- function parseLease(value) {
1425
- const result = sandboxEgressCredentialLeaseSchema.safeParse(value);
1426
- if (!result.success) return;
1427
- const expiresAtMs = Date.parse(result.data.expiresAt);
1428
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now() + LEASE_REFRESH_BUFFER_MS) return;
1429
- return result.data;
1430
- }
1431
- function getSandboxEgressSecret() {
1432
- const secret = process.env.JUNIOR_SECRET?.trim();
1433
- if (secret) return secret;
1434
- throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
1435
- }
1436
- /**
1437
- * Create the signed Junior credential context embedded in the proxy URL.
1438
- *
1439
- * The token binds actor credentials to a specific sandbox egress id and a
1440
- * short expiry. Vercel OIDC proves which VM sent a request; this token proves
1441
- * which Junior actor and credential context that VM is allowed to use.
1442
- */
1443
- function createSandboxEgressCredentialToken(input) {
1444
- const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
1445
- const now = Date.now();
1446
- const context = {
1447
- ...input.conversationId ? { conversationId: input.conversationId } : void 0,
1448
- credentials: input.credentials,
1449
- egressId: input.egressId,
1450
- expiresAtMs: now + ttlMs,
1451
- contextId: randomUUID()
1452
- };
1453
- const payload = `${SANDBOX_EGRESS_TOKEN_VERSION}.${base64Url(JSON.stringify(context))}`;
1454
- return `${payload}.${signPayload(payload)}`;
1455
- }
1456
- /**
1457
- * Verify the signed Junior credential context from the proxy URL.
1458
- *
1459
- * Returning `undefined` means the token is missing, expired, malformed, or not
1460
- * signed with the current `JUNIOR_SECRET`.
1461
- */
1462
- function parseSandboxEgressCredentialToken(token) {
1463
- if (!token) return;
1464
- const parts = token.split(".");
1465
- if (parts.length !== 3 || parts[0] !== SANDBOX_EGRESS_TOKEN_VERSION) return;
1466
- const encodedSession = parts[1];
1467
- const signature = parts[2];
1468
- if (!encodedSession || !signature) return;
1469
- if (!timingSafeMatch(signPayload(`${parts[0]}.${encodedSession}`), signature)) return;
1470
- try {
1471
- return parseSandboxEgressContext(JSON.parse(fromBase64Url(encodedSession)));
1472
- } catch {
1473
- return;
1474
- }
1475
- }
1476
- /**
1477
- * Remember auth headers for a shared installation grant.
1478
- *
1479
- * User and other grants are ignored here. TTL follows the provider expiry.
1480
- * Sandbox context expiry still decides whether a request may use the headers;
1481
- * it does not shorten a shared installation grant for other sandboxes.
1482
- */
1483
- async function setSandboxEgressCredentialLease(_context, lease) {
1484
- if (!isSharedInstallationGrant(lease.grant)) return;
1485
- const leaseExpiresAtMs = Date.parse(lease.expiresAt);
1486
- if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) return;
1487
- const ttlMs = Math.max(1, leaseExpiresAtMs - Date.now());
1488
- const state = getStateAdapter();
1489
- await state.connect();
1490
- await state.set(leaseKey(lease.provider, lease.grant), lease, ttlMs);
1491
- }
1492
- /** Load remembered auth headers for a shared installation grant. */
1493
- async function getSandboxEgressCredentialLease(provider, grant, _context) {
1494
- if (!isSharedInstallationGrant(grant)) return;
1495
- const state = getStateAdapter();
1496
- await state.connect();
1497
- return parseLease(await state.get(leaseKey(provider, grant)));
1498
- }
1499
- /** Drop remembered installation headers after the upstream rejects them. */
1500
- async function clearSandboxEgressCredentialLease(provider, grant, _context) {
1501
- if (!isSharedInstallationGrant(grant)) return;
1502
- const state = getStateAdapter();
1503
- await state.connect();
1504
- await state.delete(leaseKey(provider, grant));
1505
- }
1506
- /**
1507
- * Record that credential issuance needs user authorization for this command.
1508
- *
1509
- * The command runner consumes this after the sandbox tool finishes so the agent
1510
- * can present a normal authorization-required response instead of a raw proxy
1511
- * failure.
1512
- */
1513
- async function setSandboxEgressAuthRequiredSignal(context, signal) {
1514
- const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
1515
- const state = getStateAdapter();
1516
- await state.connect();
1517
- await state.set(authSignalKey(context.egressId, signal.grant.access), {
1518
- ...signal,
1519
- createdAtMs: Date.now()
1520
- }, ttlMs);
1521
- }
1522
- /**
1523
- * Record that the provider rejected an issued credential with permission denial.
1524
- */
1525
- async function setSandboxEgressPermissionDeniedSignal(context, signal) {
1526
- const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
1527
- const state = getStateAdapter();
1528
- await state.connect();
1529
- await state.set(permissionSignalKey(context.egressId, signal.grant.access), {
1530
- ...signal,
1531
- createdAtMs: Date.now()
1532
- }, ttlMs);
1533
- }
1534
- /** Remove pending auth/permission signals before or after a sandbox command. */
1535
- async function clearSandboxEgressSignals(egressId) {
1536
- if (!egressId) return;
1537
- const state = getStateAdapter();
1538
- await state.connect();
1539
- await Promise.all([
1540
- state.delete(authSignalKey(egressId, "read")),
1541
- state.delete(authSignalKey(egressId, "write")),
1542
- state.delete(permissionSignalKey(egressId, "read")),
1543
- state.delete(permissionSignalKey(egressId, "write"))
1544
- ]);
1545
- }
1546
- /**
1547
- * Consume the auth signal produced during a sandbox command, preferring writes.
1548
- */
1549
- async function consumeSandboxEgressAuthRequiredSignal(egressId) {
1550
- if (!egressId) return;
1551
- const state = getStateAdapter();
1552
- await state.connect();
1553
- const [writeSignal, readSignal] = await Promise.all([state.get(authSignalKey(egressId, "write")), state.get(authSignalKey(egressId, "read"))]);
1554
- const signal = parseSandboxEgressAuthRequiredSignal(writeSignal) ?? parseSandboxEgressAuthRequiredSignal(readSignal);
1555
- await Promise.all([state.delete(authSignalKey(egressId, "read")), state.delete(authSignalKey(egressId, "write"))]);
1556
- return signal;
1557
- }
1558
- /**
1559
- * Consume the permission signal produced during a sandbox command, preferring writes.
1560
- */
1561
- async function consumeSandboxEgressPermissionDeniedSignal(egressId) {
1562
- if (!egressId) return;
1563
- const state = getStateAdapter();
1564
- await state.connect();
1565
- const [writeSignal, readSignal] = await Promise.all([state.get(permissionSignalKey(egressId, "write")), state.get(permissionSignalKey(egressId, "read"))]);
1566
- const signal = parseSandboxEgressPermissionDeniedSignal(writeSignal) ?? parseSandboxEgressPermissionDeniedSignal(readSignal);
1567
- await Promise.all([state.delete(permissionSignalKey(egressId, "read")), state.delete(permissionSignalKey(egressId, "write"))]);
1568
- return signal;
1569
- }
1570
- // src/chat/sandbox/egress/tracing.ts
1571
- function isValidDomainPattern(domain) {
1572
- if (domain.includes("*")) return domain.startsWith("*.") && domain.indexOf("*", 1) === -1;
1573
- return true;
1574
- }
1575
- /** Normalize exact and leading-wildcard sandbox egress trace domains. */
1576
- function normalizeSandboxEgressTracePropagationDomains(domains) {
1577
- if (domains === void 0) return [];
1578
- if (!Array.isArray(domains)) throw new Error("sandbox.egressTracePropagationDomains must be an array");
1579
- return [...new Set(domains.map((domain) => {
1580
- if (typeof domain !== "string") throw new Error("sandbox.egressTracePropagationDomains entries must be strings");
1581
- const normalized = domain.trim().toLowerCase();
1582
- if (!normalized) throw new Error("sandbox.egressTracePropagationDomains entries must be non-empty");
1583
- if (!isValidDomainPattern(normalized)) throw new Error("sandbox.egressTracePropagationDomains entries must be exact domains or leading wildcard domains");
1584
- return normalized;
1585
- }))].sort((left, right) => left.localeCompare(right));
1586
- }
1587
- /** Return whether a host may carry sandbox egress trace propagation headers. */
1588
- function shouldPropagateSandboxEgressTrace(host, config = {}) {
1589
- const normalizedHost = host.trim().toLowerCase();
1590
- return (config.domains ?? []).some((domain) => {
1591
- if (domain.startsWith("*.")) {
1592
- const suffix = domain.slice(1);
1593
- return normalizedHost.endsWith(suffix) && normalizedHost !== domain.slice(2);
1594
- }
1595
- return domain === normalizedHost;
1596
- });
1597
- }
1598
- // src/chat/sandbox/egress/policy.ts
1599
- /** Return whether an outbound host is covered by a provider domain rule. */
1600
- function matchesSandboxEgressDomain(host, domain) {
1601
- return host.toLowerCase() === domain.toLowerCase();
1602
- }
1603
- function manifestDomains(manifest) {
1604
- return [.../* @__PURE__ */ new Set([...manifest.credentials?.domains ?? [], ...manifest.domains ?? []])].sort((left, right) => left.localeCompare(right));
1605
- }
1606
- function providerEntries() {
1607
- return pluginCatalogRuntime.getProviders().map((plugin) => ({
1608
- provider: plugin.manifest.name,
1609
- domains: manifestDomains(plugin.manifest)
1610
- })).filter((entry) => entry.domains.length > 0).sort((left, right) => left.provider.localeCompare(right.provider));
1611
- }
1612
- /**
1613
- * Resolve the plugin provider responsible for an outbound sandbox host.
1614
- *
1615
- * Only plugin-declared provider domains are eligible for credentialed sandbox
1616
- * egress. Hosts without a matching provider are blocked by the proxy even if a
1617
- * malformed or stale network policy tried to forward them.
1618
- */
1619
- function resolveSandboxEgressProviderForHost(host) {
1620
- return providerEntries().find((entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain)))?.provider;
1621
- }
1622
- function sandboxProxyUrl(credentialToken) {
1623
- const baseUrl = resolveBaseUrl();
1624
- if (!baseUrl) throw new Error("Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)");
1625
- const path = credentialToken ? `${SANDBOX_EGRESS_PROXY_PATH}/${credentialToken}` : SANDBOX_EGRESS_PROXY_PATH;
1626
- return new URL(path, baseUrl).toString();
1627
- }
1628
- /**
1629
- * Build the policy that forwards provider domains through the egress proxy.
1630
- *
1631
- * The signed credential token is embedded in the forward URL so the proxy can
1632
- * bind each forwarded request back to the actor and sandbox VM that created the
1633
- * command. The sandbox receives placeholder env vars, not provider credentials.
1634
- */
1635
- function buildSandboxEgressNetworkPolicy(input) {
1636
- const allow = { "*": [] };
1637
- const entries = providerEntries();
1638
- const traceHeaders = Object.fromEntries(Object.entries(input?.traceHeaders ?? {}).filter(([, value]) => typeof value === "string" && value.trim()));
1639
- const hasTraceHeaders = Object.keys(traceHeaders).length > 0;
1640
- if (entries.length === 0 && (!hasTraceHeaders || (input?.traceConfig?.domains ?? []).length === 0)) return { allow };
1641
- const forwardURL = input?.credentialToken ? sandboxProxyUrl(input.credentialToken) : void 0;
1642
- const domains = /* @__PURE__ */ new Map();
1643
- if (forwardURL) for (const entry of entries) for (const domain of entry.domains) domains.set(domain, { forward: true });
1644
- if (hasTraceHeaders) for (const domain of input?.traceConfig?.domains ?? []) domains.set(domain, { forward: domains.get(domain)?.forward ?? false });
1645
- for (const [domain, policy] of [...domains.entries()].sort(([left], [right]) => left.localeCompare(right))) {
1646
- const shouldPropagateTrace = shouldPropagateSandboxEgressTrace(domain, input?.traceConfig);
1647
- if (policy.forward && forwardURL) allow[domain] = [{ forwardURL }];
1648
- else if (shouldPropagateTrace && hasTraceHeaders) allow[domain] = [{ transform: [{ headers: traceHeaders }] }];
1649
- }
1650
- return { allow };
1651
- }
1652
- /**
1653
- * Resolve non-secret command environment values for registered sandbox providers.
1654
- *
1655
- * Credential env vars are placeholders that tell tools which auth variable name
1656
- * exists; real credential headers are issued lazily by host egress.
1657
- */
1658
- async function resolveSandboxCommandEnvironment() {
1659
- const env = {};
1660
- for (const plugin of pluginCatalogRuntime.getProviders().sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))) {
1661
- Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
1662
- const credentials = plugin.manifest.credentials;
1663
- if (credentials?.authTokenEnv) env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
1664
- }
1665
- return env;
1666
- }
1667
- // src/chat/capabilities/router.ts
1668
- var ProviderCredentialRouter = class {
1669
- brokersByProvider;
1670
- constructor(input) {
1671
- this.brokersByProvider = input.brokersByProvider;
1672
- }
1673
- async issue(input) {
1674
- const broker = this.brokersByProvider[input.provider];
1675
- if (!broker) throw new Error(`No credential broker registered for provider: ${input.provider}`);
1676
- return await broker.issue({
1677
- context: input.context,
1678
- reason: input.reason
1679
- });
1680
- }
1681
- };
1682
- // src/chat/credentials/user-token-store.ts
1683
- const storedTokensSchema = pluginStoredTokensSchema;
1684
- // src/chat/credentials/state-adapter-token-store.ts
1685
- const KEY_PREFIX = "oauth-token";
1686
- const BUFFER_MS = 864e5;
1687
- const LONG_LIVED_TTL_MS = 31536e6;
1688
- const REFRESH_LOCK_WAIT_MS = 3e4;
1689
- const REFRESH_LOCK_RETRY_MS = 100;
1690
- function tokenKey(userId, provider) {
1691
- return `${KEY_PREFIX}:${userId}:${provider}`;
1692
- }
1693
- function refreshLockKey(userId, provider) {
1694
- return `${tokenKey(userId, provider)}:refresh`;
1695
- }
1696
- var StateAdapterTokenStore = class {
1697
- state;
1698
- constructor(stateAdapter) {
1699
- this.state = stateAdapter;
1700
- }
1701
- async get(userId, provider) {
1702
- const stored = await this.state.get(tokenKey(userId, provider));
1703
- return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
1704
- }
1705
- async set(userId, provider, tokens) {
1706
- const parsed = storedTokensSchema.parse(tokens);
1707
- const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
1708
- const ttlMs = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
1709
- await this.state.set(tokenKey(userId, provider), parsed, ttlMs);
1710
- }
1711
- async delete(userId, provider) {
1712
- await this.state.delete(tokenKey(userId, provider));
1713
- }
1714
- /** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
1715
- async withRefresh(userId, provider, callback) {
1716
- const lockKey = refreshLockKey(userId, provider);
1717
- const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
1718
- while (true) {
1719
- const lock = await acquireActiveLock(this.state, lockKey);
1720
- if (lock) try {
1721
- return await callback();
1722
- } finally {
1723
- await this.state.releaseLock(lock);
1724
- }
1725
- if (Date.now() >= deadline) throw new Error(`Could not acquire OAuth token refresh lock`);
1726
- await sleep(REFRESH_LOCK_RETRY_MS);
1727
- }
1728
- }
1729
- };
1730
- // src/chat/capabilities/factory.ts
1731
- const sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
1732
- /** Create the user token store used by OAuth-backed credential brokers. */
1733
- function createUserTokenStore() {
1734
- return new StateAdapterTokenStore(getStateAdapter());
1735
- }
1736
- function createProviderCredentialRouter(userTokenStore) {
1737
- const brokersByProvider = {};
1738
- for (const plugin of pluginCatalogRuntime.getProviders()) {
1739
- const { name } = plugin.manifest;
1740
- if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) continue;
1741
- brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, { userTokenStore });
1742
- }
1743
- return new ProviderCredentialRouter({ brokersByProvider });
1744
- }
1745
- function getSandboxEgressRouter() {
1746
- const stateAdapter = getStateAdapter();
1747
- let router = sandboxEgressRouters.get(stateAdapter);
1748
- if (!router) {
1749
- router = createProviderCredentialRouter(new StateAdapterTokenStore(stateAdapter));
1750
- sandboxEgressRouters.set(stateAdapter, router);
1751
- }
1752
- return router;
1753
- }
1754
- /** Issue one provider credential lease for host-side sandbox egress proxying. */
1755
- async function issueProviderCredentialLease(input) {
1756
- return await getSandboxEgressRouter().issue(input);
1757
- }
1758
- // src/chat/sandbox/egress/credentials.ts
1759
- const HTTP_READ_METHODS = /* @__PURE__ */ new Set([
1760
- "GET",
1761
- "HEAD",
1762
- "OPTIONS"
1763
- ]);
1764
- /**
1765
- * Signals that egress selected a grant but could not issue credential headers.
1766
- *
1767
- * Callers convert this into command-level auth-required state so the agent can
1768
- * request authorization without exposing provider-specific lease internals.
1769
- */
1770
- var SandboxEgressCredentialError = class extends Error {
1771
- authorization;
1772
- grant;
1773
- kind;
1774
- provider;
1775
- constructor(input) {
1776
- super(input.message);
1777
- this.name = "SandboxEgressCredentialError";
1778
- this.authorization = input.authorization;
1779
- this.grant = input.grant;
1780
- this.kind = input.kind;
1781
- this.provider = input.provider;
1782
- }
1783
- };
1784
- function defaultGrantForProvider(input) {
1785
- const access = HTTP_READ_METHODS.has(input.method.toUpperCase()) ? "read" : "write";
1786
- return {
1787
- source: "broker",
1788
- grant: {
1789
- name: "default",
1790
- access,
1791
- reason: `sandbox-egress:${input.provider}:${access}`
1792
- }
1793
- };
1794
- }
1795
- function hasGenericCredentials(provider) {
1796
- const manifest = pluginCatalogRuntime.getDefinition(provider)?.manifest;
1797
- return Boolean(manifest?.credentials || manifest?.apiHeaders);
1798
- }
1799
- function oauthAuthorizationForProvider(provider) {
1800
- const oauth = pluginCatalogRuntime.getOAuthConfig(provider);
1801
- return oauth ? {
1802
- type: "oauth",
1803
- provider,
1804
- ...oauth.scope ? { scope: oauth.scope } : void 0
1805
- } : void 0;
1806
- }
1807
- function credentialSubjectFromContext(context) {
1808
- return "subject" in context.credentials && context.credentials.subject ? {
1809
- type: "user",
1810
- userId: context.credentials.subject.userId
1811
- } : void 0;
1812
- }
1813
- function assertLeaseTransformsOwnedByProvider(provider, lease) {
1814
- 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}`);
1815
- }
1816
- /**
1817
- * Select the grant needed for one outbound request.
1818
- *
1819
- * GitHub GraphQL and other plugin-owned APIs may need body-aware grant choices;
1820
- * providers without hooks use a simple read/write default based on HTTP method.
1821
- * A hook plugin that also declares generic credentials can return no grant to
1822
- * use that default for one request.
1823
- */
1824
- async function selectSandboxEgressGrant(input) {
1825
- if (!hasEgressCredentialHooks(input.provider)) return defaultGrantForProvider(input);
1826
- const pluginGrant = await selectPluginGrant({
1827
- ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : void 0,
1828
- ...input.operation ? { operation: input.operation } : void 0,
1829
- provider: input.provider,
1830
- method: input.method,
1831
- upstreamUrl: input.upstreamUrl
1832
- });
1833
- if (!pluginGrant) {
1834
- if (hasGenericCredentials(input.provider)) return defaultGrantForProvider(input);
1835
- throw new Error(`Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`);
1836
- }
1837
- return {
1838
- source: "plugin",
1839
- grant: pluginGrant
1840
- };
1841
- }
1842
- /**
1843
- * Resolve the authorization flow attached to a broker-selected egress grant.
1844
- *
1845
- * Plugin-selected grants can return their own authorization metadata when
1846
- * issuing credentials; broker defaults use the provider OAuth config.
1847
- */
1848
- function authorizationForSandboxEgressGrant(provider, selection) {
1849
- return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
1850
- }
1851
- /**
1852
- * Return credential header transforms for a selected grant.
1853
- *
1854
- * Only shared installation grants are remembered on the host. User and other
1855
- * grants always issue from the live credential source so a saved login change
1856
- * cannot leave stale auth headers in place. The sandbox context only authorizes
1857
- * the request.
1858
- */
1859
- async function sandboxEgressCredentialLease(provider, selection, context) {
1860
- const { grant } = selection;
1861
- const cached = await getSandboxEgressCredentialLease(provider, grant, context);
1862
- if (cached) {
1863
- 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`);
1864
- return {
1865
- ...cached,
1866
- grant
1867
- };
1868
- }
1869
- let lease;
1870
- if (selection.source === "plugin") {
1871
- const credentialSubject = credentialSubjectFromContext(context);
1872
- const pluginResult = await issuePluginCredential({
1873
- provider,
1874
- grant,
1875
- actor: context.credentials.actor,
1876
- ...credentialSubject ? { credentialSubject } : void 0,
1877
- userTokenStore: createUserTokenStore()
1878
- });
1879
- if (pluginResult.type === "needed") throw new SandboxEgressCredentialError({
1880
- provider,
1881
- grant,
1882
- kind: "auth_required",
1883
- authorization: pluginResult.authorization,
1884
- message: pluginResult.message
1885
- });
1886
- if (pluginResult.type === "unavailable") throw new SandboxEgressCredentialError({
1887
- provider,
1888
- grant,
1889
- kind: "unavailable",
1890
- message: pluginResult.message
1891
- });
1892
- lease = pluginResult.lease;
1893
- } else try {
1894
- lease = await issueProviderCredentialLease({
1895
- context: context.credentials,
1896
- provider,
1897
- reason: grant.reason ?? `sandbox-egress:${provider}:default`
1898
- });
1899
- } catch (error) {
1900
- if (error instanceof CredentialUnavailableError) throw new SandboxEgressCredentialError({
1901
- provider,
1902
- grant,
1903
- kind: "auth_required",
1904
- authorization: authorizationForSandboxEgressGrant(provider, selection),
1905
- message: error.message
1906
- });
1907
- throw error;
1908
- }
1909
- const headerTransforms = lease.headerTransforms ?? [];
1910
- if (headerTransforms.length === 0) throw new Error(`Credential lease for ${provider} did not include header transforms`);
1911
- const leaseExpiresAtMs = Date.parse(lease.expiresAt);
1912
- if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) throw new Error(`Credential lease for ${provider} is expired`);
1913
- const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
1914
- const issuedLease = {
1915
- provider,
1916
- grant,
1917
- ...lease.account ? { account: lease.account } : void 0,
1918
- ...authorization ? { authorization } : void 0,
1919
- expiresAt: lease.expiresAt,
1920
- headerTransforms
1921
- };
1922
- assertLeaseTransformsOwnedByProvider(provider, issuedLease);
1923
- await setSandboxEgressCredentialLease(context, issuedLease);
1924
- return issuedLease;
1925
- }
1926
- /** Return whether a credential lease can modify requests to the target host. */
1927
- function hasSandboxEgressLeaseTransformForHost(lease, host) {
1928
- return lease.headerTransforms.some((transform) => matchesSandboxEgressDomain(host, transform.domain));
1929
- }
1930
- // src/chat/egress/credentialed.ts
1931
- const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
1932
- "connection",
1933
- "host",
1934
- "keep-alive",
1935
- "proxy-authenticate",
1936
- "proxy-authorization",
1937
- "te",
1938
- "trailer",
1939
- "transfer-encoding",
1940
- "upgrade"
1941
- ]);
1942
- const PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
1943
- "vercel-sandbox-oidc-token",
1944
- "vercel-forwarded-host",
1945
- "vercel-forwarded-scheme",
1946
- "vercel-forwarded-port",
1947
- "vercel-forwarded-path"
1948
- ]);
1949
- const TRACE_PROPAGATION_HEADERS = /* @__PURE__ */ new Set([
1950
- "baggage",
1951
- "sentry-trace",
1952
- "traceparent"
1953
- ]);
1954
- const DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length"]);
1955
- const UPSTREAM_TOKEN_REJECTION_STATUS = 401;
1956
- const UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
1957
- const GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES = 65536;
1958
- const RESPONSE_BODY_TEXT_LIMIT_BYTES = 65536;
1959
- function authRequiredResponse(input) {
1960
- return new Response(`junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized\n${input.message}`, {
1961
- status: 401,
1962
- headers: {
1963
- "content-type": "text/plain; charset=utf-8",
1964
- "cache-control": "no-store"
1965
- }
1966
- });
1967
- }
1968
- function policyDeniedResponse(error) {
1969
- return Response.json({ error: error.message }, {
1970
- status: 403,
1971
- headers: { "cache-control": "no-store" }
1972
- });
1973
- }
1974
- function shouldLogSandboxEgressInfo() {
1975
- return (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase() !== "production";
1976
- }
1977
- function egressAttributes(input) {
1978
- return {
1979
- ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : void 0,
1980
- ...input.provider ? { "app.provider.name": input.provider } : void 0,
1981
- ...input.grantName ? { "app.grant.name": input.grantName } : void 0,
1982
- ...input.grantAccess ? { "app.grant.access": input.grantAccess } : void 0,
1983
- ...input.grantReason ? { "app.grant.reason": input.grantReason } : void 0,
1984
- ...input.host ? { "server.address": input.host } : void 0,
1985
- ...input.method ? { "http.request.method": input.method } : void 0,
1986
- ...input.path ? { "url.path": input.path } : void 0,
1987
- ...input.status ? { "http.response.status_code": input.status } : void 0
1988
- };
1989
- }
1990
- function redactedProxyPath(pathname) {
1991
- if (pathname.startsWith(`/api/internal/sandbox-egress/`)) return `${SANDBOX_EGRESS_PROXY_PATH}/<token>`;
1992
- return pathname;
1993
- }
1994
- function routingAttributes(request, upstreamUrl) {
1995
- const attributes = { "app.sandbox.egress.proxy_path": redactedProxyPath(new URL(request.url).pathname) };
1996
- if (upstreamUrl) {
1997
- attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
1998
- const gitService = upstreamUrl.searchParams.get("service");
1999
- if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) attributes["app.sandbox.egress.git_service"] = gitService;
2000
- }
2001
- return attributes;
2002
- }
2003
- function displayedUpstreamPath(upstreamUrl) {
2004
- const gitService = upstreamUrl.searchParams.get("service");
2005
- if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) return `${upstreamUrl.pathname}?service=${gitService}`;
2006
- return upstreamUrl.pathname;
2007
- }
2008
- function permissionDeniedMessage(provider, grant) {
2009
- return `${provider} returned HTTP 403 after the runtime injected the ${grant.name} grant. The request was forwarded; this is not a local runtime block.`;
2010
- }
2011
- function isEgressAuthRequired(error) {
2012
- return error instanceof EgressAuthRequired || error instanceof Error && error.name === "EgressAuthRequired";
2013
- }
2014
- function isEgressPolicyDenied(error) {
2015
- return error instanceof EgressPolicyDenied || error instanceof Error && error.name === "EgressPolicyDenied";
2016
- }
2017
- /**
2018
- * Record one forwarded provider request. Write-grant requests are audit
2019
- * events, so they reach Sentry in production. Other requests log only outside
2020
- * production.
2021
- */
2022
- function logSandboxEgressUpstreamRequest(input) {
2023
- const isWrite = input.grantAccess === "write";
2024
- if (!isWrite && !shouldLogSandboxEgressInfo()) return;
2025
- (isWrite ? logAudit : logInfo)("sandbox.egress.upstream.requested", {
2026
- ...egressAttributes({
2027
- egressId: input.egressId,
2028
- grantAccess: input.grantAccess,
2029
- grantName: input.grantName,
2030
- grantReason: input.grantReason,
2031
- host: input.upstreamUrl.hostname,
2032
- method: input.request.method,
2033
- path: input.upstreamUrl.pathname,
2034
- provider: input.provider,
2035
- status: input.upstream.status
2036
- }),
2037
- ...routingAttributes(input.request, input.upstreamUrl),
2038
- "app.sandbox.egress.upstream_ok": input.upstream.ok
2039
- });
2040
- }
2041
- async function requestBodyBytes(request) {
2042
- if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS" || request.body === null) return;
2043
- return await request.arrayBuffer();
2044
- }
2045
- /**
2046
- * Explain a request body that the runtime delivered already used.
2047
- *
2048
- * The forwarded headers cannot show whether the body was empty, so the proxy
2049
- * does not send a credentialed write upstream without its payload.
2050
- */
2051
- function unreadableBodyResponse(eventId) {
2052
- return Response.json({
2053
- source: "junior-egress",
2054
- error: "Junior's sandbox egress proxy could not read the request body, so it did not send the request upstream.",
2055
- ...eventId ? { sentryEventId: eventId } : void 0
2056
- }, {
2057
- status: 500,
2058
- headers: { "cache-control": "no-store" }
2059
- });
2060
- }
2061
- /** Identify GitHub writes whose body determines whether a grant is safe. */
2062
- function githubBodyInspection(input) {
2063
- if (input.provider !== "github" || input.requestMethod.toUpperCase() !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") return;
2064
- const pathname = input.upstreamUrl.pathname.toLowerCase();
2065
- if (pathname.endsWith("/graphql")) return "graphql";
2066
- return /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+\/events)?$/.test(pathname) ? "pull-request-review" : void 0;
2067
- }
2068
- function grantSelectionBodyTooLargeMessage(inspection) {
2069
- return inspection === "graphql" ? "GitHub GraphQL request body is too large for Junior to inspect before issuing credentials." : "GitHub pull request review request body is too large for Junior to inspect before issuing credentials.";
2070
- }
2071
- function grantSelectionBodyText(input) {
2072
- if (input.body === void 0) return;
2073
- if (input.body.byteLength > GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES) {
2074
- const inspection = githubBodyInspection({
2075
- provider: input.provider,
2076
- requestMethod: input.request.method,
2077
- upstreamUrl: input.upstreamUrl
2078
- });
2079
- if (inspection) throw new EgressPolicyDenied(grantSelectionBodyTooLargeMessage(inspection));
2080
- return;
2081
- }
2082
- return new TextDecoder().decode(input.body);
2083
- }
2084
- function responseContentLength(upstream) {
2085
- const raw = upstream.headers.get("content-length");
2086
- if (!raw) return;
2087
- const parsed = Number(raw);
2088
- return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
2089
- }
2090
- async function responseTextWithinLimit(upstream, maxBytes) {
2091
- const limit = Math.min(Math.max(0, Math.floor(maxBytes)), RESPONSE_BODY_TEXT_LIMIT_BYTES);
2092
- if (limit <= 0) return;
2093
- const contentLength = responseContentLength(upstream);
2094
- if (contentLength !== void 0 && contentLength > limit) return;
2095
- let clone;
2096
- try {
2097
- clone = upstream.clone();
2098
- } catch {
2099
- return;
2100
- }
2101
- const body = clone.body;
2102
- if (!body) return "";
2103
- const reader = body.getReader();
2104
- const chunks = [];
2105
- let bytes = 0;
2106
- try {
2107
- while (true) {
2108
- const { done, value } = await reader.read();
2109
- if (done) break;
2110
- if (!value) continue;
2111
- bytes += value.byteLength;
2112
- if (bytes > limit) {
2113
- await reader.cancel().catch(() => void 0);
2114
- return;
2115
- }
2116
- chunks.push(value);
2117
- }
2118
- } catch {
2119
- await reader.cancel().catch(() => void 0);
2120
- return;
2121
- } finally {
2122
- reader.releaseLock();
2123
- }
2124
- const combined = new Uint8Array(bytes);
2125
- let offset = 0;
2126
- for (const chunk of chunks) {
2127
- combined.set(chunk, offset);
2128
- offset += chunk.byteLength;
2129
- }
2130
- return new TextDecoder().decode(combined);
2131
- }
2132
- /**
2133
- * Build the upstream request headers.
2134
- *
2135
- * Sandbox/proxy-only headers are stripped so they cannot leak to providers, and
2136
- * trace headers are preserved only for hosts opted into trace propagation.
2137
- * Credential lease transforms win last because they are the host-issued
2138
- * authority for provider auth headers.
2139
- */
2140
- function requestHeaders(request, lease, upstreamHost, tracePropagation) {
2141
- const headers = new Headers();
2142
- const mayPropagateTrace = shouldPropagateSandboxEgressTrace(upstreamHost, tracePropagation);
2143
- request.headers.forEach((value, key) => {
2144
- const normalized = key.toLowerCase();
2145
- if (HOP_BY_HOP_HEADERS.has(normalized) || PROXY_ONLY_HEADERS.has(normalized)) return;
2146
- if (TRACE_PROPAGATION_HEADERS.has(normalized) && !mayPropagateTrace) return;
2147
- headers.append(key, value);
2148
- });
2149
- for (const transform of lease.headerTransforms) {
2150
- if (!matchesSandboxEgressDomain(upstreamHost, transform.domain)) continue;
2151
- for (const [key, value] of Object.entries(transform.headers)) {
2152
- if (TRACE_PROPAGATION_HEADERS.has(key.toLowerCase()) && !mayPropagateTrace) continue;
2153
- headers.set(key, value);
2154
- }
2155
- }
2156
- return headers;
2157
- }
2158
- function responseHeaders(upstream) {
2159
- const headers = new Headers();
2160
- upstream.headers.forEach((value, key) => {
2161
- const normalized = key.toLowerCase();
2162
- if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) headers.append(key, value);
2163
- });
2164
- return headers;
2165
- }
2166
- function leaseLogAttributes(input) {
2167
- return {
2168
- ...egressAttributes({
2169
- egressId: input.egressId,
2170
- grantAccess: input.lease.grant.access,
2171
- grantName: input.lease.grant.name,
2172
- grantReason: input.lease.grant.reason,
2173
- host: input.upstreamUrl.hostname,
2174
- method: input.request.method,
2175
- path: input.upstreamUrl.pathname,
2176
- provider: input.provider,
2177
- status: input.status
2178
- }),
2179
- ...routingAttributes(input.request, input.upstreamUrl)
2180
- };
2181
- }
2182
- async function recordSandboxAuthRequired(input) {
2183
- await setSandboxEgressAuthRequiredSignal(input.credentialContext, {
2184
- provider: input.provider,
2185
- grant: input.grant,
2186
- kind: input.kind ?? "auth_required",
2187
- ...input.authorization ? { authorization: input.authorization } : void 0,
2188
- message: input.message
2189
- });
2190
- }
2191
- async function recordSandboxPermissionDenied(input) {
2192
- await setSandboxEgressPermissionDeniedSignal(input.credentialContext, {
2193
- provider: input.provider,
2194
- grant: input.lease.grant,
2195
- ...input.lease.account ? { account: input.lease.account } : void 0,
2196
- message: input.message,
2197
- source: "upstream",
2198
- status: input.upstream.status,
2199
- upstreamHost: input.upstreamUrl.hostname,
2200
- upstreamPath: displayedUpstreamPath(input.upstreamUrl)
2201
- });
2202
- }
2203
- /**
2204
- * Forward one verified sandbox egress request with host-managed credentials.
2205
- *
2206
- * The caller must already have authenticated the sandbox VM, checked the signed
2207
- * credential context, and resolved the provider for `upstreamUrl`. This function
2208
- * then selects the read/write grant, issues or reuses a short-lived credential
2209
- * lease, applies its header transforms, and maps provider auth failures into
2210
- * command-level signals instead of throwing raw upstream details at the agent.
2211
- */
2212
- async function executeCredentialedEgressRequest(input) {
2213
- const { activeEgressId, credentialContext, deps, operation, provider, request, upstreamUrl } = input;
2214
- let body;
2215
- try {
2216
- body = await requestBodyBytes(request);
2217
- } catch (error) {
2218
- if (!(error instanceof TypeError)) throw error;
2219
- return unreadableBodyResponse(logException(error, "sandbox.egress.request_body.unreadable", {
2220
- ...egressAttributes({
2221
- egressId: activeEgressId,
2222
- host: upstreamUrl.hostname,
2223
- method: request.method,
2224
- path: upstreamUrl.pathname,
2225
- provider,
2226
- status: 500
2227
- }),
2228
- ...routingAttributes(request, upstreamUrl)
2229
- }));
2230
- }
2231
- const bodyForGrantSelection = githubBodyInspection({
2232
- provider,
2233
- requestMethod: request.method,
2234
- upstreamUrl
2235
- }) ? body : void 0;
2236
- let grantSelection;
2237
- try {
2238
- grantSelection = await selectSandboxEgressGrant({
2239
- bodyText: grantSelectionBodyText({
2240
- body: bodyForGrantSelection,
2241
- provider,
2242
- request,
2243
- upstreamUrl
2244
- }),
2245
- ...operation ? { operation } : void 0,
2246
- provider,
2247
- method: request.method,
2248
- upstreamUrl
2249
- });
2250
- } catch (error) {
2251
- if (isEgressPolicyDenied(error)) {
2252
- logWarn("sandbox.egress.policy.denied", {
2253
- ...egressAttributes({
2254
- egressId: activeEgressId,
2255
- host: upstreamUrl.hostname,
2256
- method: request.method,
2257
- path: upstreamUrl.pathname,
2258
- provider,
2259
- status: 403
2260
- }),
2261
- ...routingAttributes(request, upstreamUrl),
2262
- "app.sandbox.egress.policy.reason": error.message
2263
- });
2264
- return policyDeniedResponse(error);
2265
- }
2266
- throw error;
2267
- }
2268
- const issueCredentialLease = deps.issueCredentialLease ?? sandboxEgressCredentialLease;
2269
- const clearCredentialLease = deps.clearCredentialLease ?? clearSandboxEgressCredentialLease;
2270
- const recordAuthRequired = deps.recordAuthRequired ?? recordSandboxAuthRequired;
2271
- const recordPermissionDenied = deps.recordPermissionDenied ?? recordSandboxPermissionDenied;
2272
- async function resolveLease() {
2273
- try {
2274
- return await issueCredentialLease(provider, grantSelection, credentialContext);
2275
- } catch (error) {
2276
- if (!(error instanceof SandboxEgressCredentialError)) throw error;
2277
- await recordAuthRequired({
2278
- credentialContext,
2279
- provider: error.provider,
2280
- grant: error.grant,
2281
- kind: error.kind,
2282
- authorization: error.authorization,
2283
- message: error.message
2284
- });
2285
- const isAuthRequired = error.kind === "auth_required";
2286
- logWarn(isAuthRequired ? "sandbox.egress.credential.needed" : "sandbox.egress.credential.unavailable", {
2287
- ...egressAttributes({
2288
- egressId: activeEgressId,
2289
- grantAccess: error.grant.access,
2290
- grantName: error.grant.name,
2291
- grantReason: error.grant.reason,
2292
- host: upstreamUrl.hostname,
2293
- method: request.method,
2294
- path: upstreamUrl.pathname,
2295
- provider: error.provider,
2296
- status: 401
2297
- }),
2298
- ...routingAttributes(request, upstreamUrl)
2299
- });
2300
- return authRequiredResponse({
2301
- provider: error.provider,
2302
- grant: error.grant,
2303
- message: error.message
2304
- });
2305
- }
2306
- }
2307
- let leaseOrResponse = await resolveLease();
2308
- if (leaseOrResponse instanceof Response) return leaseOrResponse;
2309
- let lease = leaseOrResponse;
2310
- const attributes = (activeLease, status) => leaseLogAttributes({
2311
- egressId: activeEgressId,
2312
- lease: activeLease,
2313
- provider,
2314
- request,
2315
- status,
2316
- upstreamUrl
2317
- });
2318
- if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
2319
- logWarn("sandbox.egress.transform.missing", {
2320
- ...attributes(lease, 403),
2321
- "app.sandbox.egress.transform_domains": lease.headerTransforms.map((transform) => transform.domain)
2322
- });
2323
- return Response.json({ error: "Credential lease does not cover forwarded host" }, { status: 403 });
2324
- }
2325
- const fetchImpl = deps.fetch ?? fetch;
2326
- let retriedAfter403 = false;
2327
- while (true) {
2328
- const headers = requestHeaders(request, lease, upstreamUrl.hostname, deps.tracePropagation ?? {});
2329
- const intercepted = await deps.interceptHttp?.({
2330
- provider,
2331
- request: new Request(upstreamUrl, {
2332
- method: request.method,
2333
- headers,
2334
- ...body !== void 0 ? { body } : void 0
2335
- }),
2336
- upstreamUrl
2337
- });
2338
- if (intercepted) return intercepted;
2339
- const requestBody = body instanceof ArrayBuffer ? body.slice(0) : body;
2340
- const upstream = await fetchImpl(upstreamUrl, {
2341
- method: request.method,
2342
- headers,
2343
- ...requestBody !== void 0 ? { body: requestBody } : void 0,
2344
- redirect: "manual"
2345
- });
2346
- let pluginPermissionDenied;
2347
- try {
2348
- pluginPermissionDenied = (await onPluginEgressResponse({
2349
- provider,
2350
- grant: lease.grant,
2351
- method: request.method,
2352
- ...operation ? { operation } : void 0,
2353
- upstreamUrl,
2354
- response: {
2355
- headers: new Headers(upstream.headers),
2356
- readText: async (maxBytes) => await responseTextWithinLimit(upstream, maxBytes),
2357
- status: upstream.status
2358
- }
2359
- })).permissionDenied;
2360
- } catch (error) {
2361
- if (!isEgressAuthRequired(error)) throw error;
2362
- await clearCredentialLease(provider, lease.grant, credentialContext);
2363
- await recordAuthRequired({
2364
- credentialContext,
2365
- provider,
2366
- grant: lease.grant,
2367
- authorization: error.authorization ?? lease.authorization,
2368
- message: error.message
2369
- });
2370
- logWarn("sandbox.egress.upstream_auth_requirement.classified", { ...attributes(lease, upstream.status) });
2371
- await upstream.body?.cancel().catch(() => void 0);
2372
- return authRequiredResponse({
2373
- provider,
2374
- grant: lease.grant,
2375
- message: error.message
2376
- });
2377
- }
2378
- logSandboxEgressUpstreamRequest({
2379
- egressId: activeEgressId,
2380
- grantAccess: lease.grant.access,
2381
- grantName: lease.grant.name,
2382
- grantReason: lease.grant.reason,
2383
- provider,
2384
- request,
2385
- upstream,
2386
- upstreamUrl
2387
- });
2388
- if (upstream.status >= 400) logWarn("sandbox.egress.upstream_response.failed", {
2389
- ...attributes(lease, upstream.status),
2390
- "error.type": `http_${upstream.status}`
2391
- });
2392
- if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
2393
- logWarn("sandbox.egress.upstream_auth.rejected", {
2394
- ...attributes(lease, upstream.status),
2395
- "app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
2396
- });
2397
- await clearCredentialLease(provider, lease.grant, credentialContext);
2398
- await recordAuthRequired({
2399
- credentialContext,
2400
- provider,
2401
- grant: lease.grant,
2402
- authorization: lease.authorization,
2403
- message: `Provider rejected the injected ${provider} credential.`
2404
- });
2405
- await upstream.body?.cancel().catch(() => void 0);
2406
- return authRequiredResponse({
2407
- provider,
2408
- grant: lease.grant,
2409
- message: `Provider rejected the injected ${provider} credential.\n`
2410
- });
2411
- }
2412
- if (upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS && !retriedAfter403) {
2413
- logWarn("sandbox.egress.upstream_auth.rejected", { ...attributes(lease, upstream.status) });
2414
- await clearCredentialLease(provider, lease.grant, credentialContext);
2415
- await upstream.body?.cancel().catch(() => void 0);
2416
- logWarn("sandbox.egress.upstream_auth.retrying", { ...attributes(lease, upstream.status) });
2417
- leaseOrResponse = await resolveLease();
2418
- if (leaseOrResponse instanceof Response) return leaseOrResponse;
2419
- lease = leaseOrResponse;
2420
- retriedAfter403 = true;
2421
- continue;
2422
- }
2423
- if (upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS) {
2424
- logWarn("sandbox.egress.upstream_auth.rejected", { ...attributes(lease, upstream.status) });
2425
- await clearCredentialLease(provider, lease.grant, credentialContext);
2426
- await recordPermissionDenied({
2427
- credentialContext,
2428
- provider,
2429
- lease,
2430
- message: pluginPermissionDenied?.message ?? permissionDeniedMessage(provider, lease.grant),
2431
- upstream,
2432
- upstreamUrl
2433
- });
2434
- if (pluginPermissionDenied) logWarn("sandbox.egress.upstream_permission.classified", { ...attributes(lease, upstream.status) });
2435
- return new Response(upstream.body, {
2436
- status: upstream.status,
2437
- statusText: upstream.statusText,
2438
- headers: responseHeaders(upstream)
2439
- });
2440
- }
2441
- if (pluginPermissionDenied) {
2442
- await recordPermissionDenied({
2443
- credentialContext,
2444
- provider,
2445
- lease,
2446
- message: pluginPermissionDenied.message,
2447
- upstream,
2448
- upstreamUrl
2449
- });
2450
- logWarn("sandbox.egress.upstream_permission.classified", { ...attributes(lease, upstream.status) });
2451
- }
2452
- return new Response(upstream.body, {
2453
- status: upstream.status,
2454
- statusText: upstream.statusText,
2455
- headers: responseHeaders(upstream)
2456
- });
2457
- }
2458
- }
2459
398
  // src/chat/experimental.ts
2460
399
  /**
2461
400
  * Known unstable product features that must be opted into explicitly.
@@ -5814,4 +3753,4 @@ async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
5814
3753
  async function saveScheduledAutomationInLock(db, task, current) {
5815
3754
  await writeScheduledAutomation(db, requireStoredTask(task), current);
5816
3755
  }
5817
- export { persistConversationMessageSummaries as $, resolvePluginOAuthAccount as $n, isToolResultMessage as $t, commitAcceptedReply as A, createSandboxEgressCredentialToken as An, abandonTurnRecord as At, recordAttachmentsDelivered as B, normalizeSlackReplyMarkdown as Bn, buildTurnErrorResponse as Bt, automationVersionOf as C, resolveSandboxEgressProviderForHost as Cn, canReusePendingAuthLink as Ct, AutomationConflictError as D, clearSandboxEgressSignals as Dn, continuableMessages as Dt, sameDefinitionValue as E, SANDBOX_EGRESS_PROXY_PATH as En, wasPendingAuthStopped as Et, loadProjection as F, parseOAuthStatePayload as Fn, recordTurnSummary as Ft, recordMcpProviderConnected as G, getSlackMessagePermalink as Gn, getPiMessageRole as Gt, recordAuthorizationCompleted as H, slackMention as Hn, isTurnExecutionLimitExceededError as Ht, loadTurnProjection as I, resolveBaseUrl as In, TURN_LIMIT_DISABLED_REASON as It, recordTurnRoute as J, removeReactionFromMessage as Jn, hasRuntimeTurnContext as Jt, recordSubscribedReplyRoute as K, openSlackDirectMessage as Kn, getTerminalAssistantMessages as Kt, loadTurnRoute as L, startOAuthFlow as Ln, TurnBudgetExceededError as Lt, loadConnectedMcpProviders as M, sandboxEgressSignalsResponseSchema as Mn, failTurnRecord as Mt, loadConversationProjection as N, deliverOAuthAuthorization as Nn, getTurnRecord as Nt, automationRevision as O, consumeSandboxEgressAuthRequiredSignal as On, loadTurnCheckpoint as Ot, loadLastModelProfile as P, formatProviderLabel as Pn, listTurnSummaries as Pt, projectConversationMessages as Q, getOAuthAccountProviders as Qn, isToolResultError as Qt, openConversationProjection as R, escapeSlackMrkdwnText as Rn, TurnSliceLimitExceededError as Rt, automationDefinition as S, resolveSandboxCommandEnvironment as Sn, abandonReplacedPendingAuth as St, readAutomationVersion as T, shouldPropagateSandboxEgressTrace as Tn, getConversationPendingAuth as Tt, recordAuthorizationRequested as U, truncateStatusText as Un, persistWithRetry as Ut, recordAuthenticationLinked as V, normalizeSlackStatusText as Vn, createTurnLimitGate as Vt, recordGuardianActionReviewed as W, addReactionToMessage as Wn, extractAssistantText as Wt, hydrateConversationMessages as X, slackEntitySchema as Xn, isAssistantMessage as Xt, appendConversationMessages as Y, uploadFilesToConversation as Yn, instructionTextForProjection as Yt, persistConversationMessages as Z, slackExternalRefIdSchema as Zn, isContinuablePiBoundary as Zt, parseEventAutomationRow as _, isExperimentalFeatureEnabled as _n, createPluginAuthOrchestration as _t, saveScheduledAutomation as a, SESSION_CONTEXT_SECTION_TAG as an, setConfigDefaults as ar, buildConversationContext as at, admitEventAutomationRun as b, createUserTokenStore as bn, AuthorizationFlowDisabledError as bt, withScheduledAutomationLock as c, buildDeterministicTurnId as cn, getConversationMessageSlackTs as ct, createEventAutomation as d, getSubscribedReplyPreflightDecision as dn, normalizeConversationText as dt, normalizeToolNameFromResult as en, createDurableLocationConfigurationService as er, resolveConversationTitle as et, deleteEventAutomation as f, getThreadStopDecision as fn, recordDeliveredAssistantMessage as ft, listEventAutomationsForTeam as g, getExperimentalFeatures as gn, PluginCredentialFailureError as gt, listDeletedEventAutomationsCreatedBy as h, getAgentContextInputLimitTokens as hn, createPluginEgress as ht, readScheduledAutomation as i, REQUEST_CONTEXT_SECTION_TAG as in, getConfigDefaults as ir, appendThreadContextMessages as it, commitMessages as j, parseSandboxEgressCredentialToken as jn, completeTurnRecord as jt, requireAutomationRevision as k, consumeSandboxEgressPermissionDeniedSignal as kn, saveTurnCheckpoint as kt, blockEventAutomation as l, SubscribedReplyReason as ln, isHumanConversationMessage as lt, getEventAutomation as m, getAgentContextCompactionTriggerTokens as mn, upsertConversationMessage as mt, listScheduledAutomationsForTeam as n, stripRuntimeTurnContext as nn, historyItemFromPiMessage as nr, fallbackShortTitle as nt, saveScheduledAutomationInLock as o, TURN_CONTEXT_TAG as on, createConversationMemoryService as ot, findMatchingEventAutomations as p, estimateTextTokens as pn, turnHasReply as pt, recordToolExecutionStarted as q, postSlackMessage as qn, getUserMessageInstructionText as qt, parseScheduledAutomationRow as r, trimTrailingAssistantMessages as rn, piMessageFromHistoryItem as rr, resolveTaskTitle as rt, scheduledRunId as s, buildDeterministicAssistantMessageId as sn, estimateConversationContextTokens as st, createScheduledAutomation as t, retainRuntimeTurnContext as tn, configValueSchema as tr, settleConversationTitleWork as tt, collectEventAutomationMatchKeys as u, decideSubscribedThreadReply as un, markConversationMessage as ut, saveEventAutomation as v, setExperimentalFeatures as vn, authorizationId as vt, listAutomationVersions as w, normalizeSandboxEgressTracePropagationDomains as wn, clearPendingAuth as wt, eventAutomationRunLimitReason as x, buildSandboxEgressNetworkPolicy as xn, AuthorizationPauseError as xt, setEventAutomationStatus as y, executeCredentialedEgressRequest as yn, AuthPausePersistenceError as yt, recordAgentsInstructionsUpdated as z, formatSlackLink as zn, TurnToolCallLimitExceededError as zt };
3756
+ export { persistConversationMessageSummaries as $, isToolResultMessage as $t, commitAcceptedReply as A, abandonTurnRecord as At, recordAttachmentsDelivered as B, buildTurnErrorResponse as Bt, automationVersionOf as C, getConfigDefaults as Cn, canReusePendingAuthLink as Ct, AutomationConflictError as D, continuableMessages as Dt, sameDefinitionValue as E, wasPendingAuthStopped as Et, loadProjection as F, recordTurnSummary as Ft, recordMcpProviderConnected as G, getPiMessageRole as Gt, recordAuthorizationCompleted as H, isTurnExecutionLimitExceededError as Ht, loadTurnProjection as I, TURN_LIMIT_DISABLED_REASON as It, recordTurnRoute as J, hasRuntimeTurnContext as Jt, recordSubscribedReplyRoute as K, getTerminalAssistantMessages as Kt, loadTurnRoute as L, TurnBudgetExceededError as Lt, loadConnectedMcpProviders as M, failTurnRecord as Mt, loadConversationProjection as N, getTurnRecord as Nt, automationRevision as O, loadTurnCheckpoint as Ot, loadLastModelProfile as P, listTurnSummaries as Pt, projectConversationMessages as Q, isToolResultError as Qt, openConversationProjection as R, TurnSliceLimitExceededError as Rt, automationDefinition as S, piMessageFromHistoryItem as Sn, abandonReplacedPendingAuth as St, readAutomationVersion as T, getConversationPendingAuth as Tt, recordAuthorizationRequested as U, persistWithRetry as Ut, recordAuthenticationLinked as V, createTurnLimitGate as Vt, recordGuardianActionReviewed as W, extractAssistantText as Wt, hydrateConversationMessages as X, isAssistantMessage as Xt, appendConversationMessages as Y, instructionTextForProjection as Yt, persistConversationMessages as Z, isContinuablePiBoundary as Zt, parseEventAutomationRow as _, isExperimentalFeatureEnabled as _n, createPluginAuthOrchestration as _t, saveScheduledAutomation as a, SESSION_CONTEXT_SECTION_TAG as an, buildConversationContext as at, admitEventAutomationRun as b, configValueSchema as bn, AuthorizationFlowDisabledError as bt, withScheduledAutomationLock as c, buildDeterministicTurnId as cn, getConversationMessageSlackTs as ct, createEventAutomation as d, getSubscribedReplyPreflightDecision as dn, normalizeConversationText as dt, normalizeToolNameFromResult as en, resolveConversationTitle as et, deleteEventAutomation as f, getThreadStopDecision as fn, recordDeliveredAssistantMessage as ft, listEventAutomationsForTeam as g, getExperimentalFeatures as gn, PluginCredentialFailureError as gt, listDeletedEventAutomationsCreatedBy as h, getAgentContextInputLimitTokens as hn, createPluginEgress as ht, readScheduledAutomation as i, REQUEST_CONTEXT_SECTION_TAG as in, appendThreadContextMessages as it, commitMessages as j, completeTurnRecord as jt, requireAutomationRevision as k, saveTurnCheckpoint as kt, blockEventAutomation as l, SubscribedReplyReason as ln, isHumanConversationMessage as lt, getEventAutomation as m, getAgentContextCompactionTriggerTokens as mn, upsertConversationMessage as mt, listScheduledAutomationsForTeam as n, stripRuntimeTurnContext as nn, fallbackShortTitle as nt, saveScheduledAutomationInLock as o, TURN_CONTEXT_TAG as on, createConversationMemoryService as ot, findMatchingEventAutomations as p, estimateTextTokens as pn, turnHasReply as pt, recordToolExecutionStarted as q, getUserMessageInstructionText as qt, parseScheduledAutomationRow as r, trimTrailingAssistantMessages as rn, resolveTaskTitle as rt, scheduledRunId as s, buildDeterministicAssistantMessageId as sn, estimateConversationContextTokens as st, createScheduledAutomation as t, retainRuntimeTurnContext as tn, settleConversationTitleWork as tt, collectEventAutomationMatchKeys as u, decideSubscribedThreadReply as un, markConversationMessage as ut, saveEventAutomation as v, setExperimentalFeatures as vn, authorizationId as vt, listAutomationVersions as w, setConfigDefaults as wn, clearPendingAuth as wt, eventAutomationRunLimitReason as x, historyItemFromPiMessage as xn, AuthorizationPauseError as xt, setEventAutomationStatus as y, createDurableLocationConfigurationService as yn, AuthPausePersistenceError as yt, recordAgentsInstructionsUpdated as z, TurnToolCallLimitExceededError as zt };