Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: "createApp"

> **createApp**(`options?`): `Promise`\<`Hono`\<`BlankEnv`, `BlankSchema`, `"/"`\>\>

Defined in: [junior/src/app.ts:316](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L316)
Defined in: [junior/src/app.ts:329](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L329)

Create a Hono app with all Junior routes.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ prev: false
title: "JuniorAppOptions"
---

Defined in: [junior/src/app.ts:54](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L54)
Defined in: [junior/src/app.ts:58](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L58)

## Properties

### configDefaults?

> `optional` **configDefaults?**: `Record`\<`string`, `unknown`\>

Defined in: [junior/src/app.ts:63](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L63)
Defined in: [junior/src/app.ts:67](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L67)

Install-wide provider defaults (`provider.key` format). Channel overrides take precedence.

Expand All @@ -23,7 +23,7 @@ Install-wide provider defaults (`provider.key` format). Channel overrides take p

> `optional` **conversationWork?**: `VercelConversationWorkCallbackOptions`

Defined in: [junior/src/app.ts:65](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L65)
Defined in: [junior/src/app.ts:69](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L69)

Queue consumer wiring for the durable conversation worker.

Expand All @@ -33,17 +33,35 @@ Queue consumer wiring for the durable conversation worker.

> `optional` **plugins?**: [`JuniorPluginSet`](/reference/api/interfaces/juniorpluginset/)

Defined in: [junior/src/app.ts:67](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L67)
Defined in: [junior/src/app.ts:71](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L71)

Direct plugin set override. Usually omitted when `juniorNitro()` uses a plugin module.

---

### sandbox?

> `optional` **sandbox?**: `object`

Defined in: [junior/src/app.ts:73](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L73)

Sandbox execution options.

#### egressTracePropagationDomains?

> `optional` **egressTracePropagationDomains?**: `string`[]

Egress domains allowed to carry Sentry trace propagation headers.
Entries may be exact domains or leading wildcard domains such as
`*.sentry.io`; wildcard entries match subdomains, not the apex domain.

---

### slack?

> `optional` **slack?**: `object`

Defined in: [junior/src/app.ts:56](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L56)
Defined in: [junior/src/app.ts:60](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L60)

Slack-specific overrides applied after env parsing.

Expand All @@ -65,4 +83,4 @@ Slack emoji shown while Junior is processing. Defaults to `eyes`.

> `optional` **waitUntil?**: `WaitUntilFn`

Defined in: [junior/src/app.ts:68](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L68)
Defined in: [junior/src/app.ts:81](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L81)
18 changes: 18 additions & 0 deletions packages/docs/src/content/docs/reference/config-and-env.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ const app = await createApp({

Keys must be registered plugin config keys. Channel-scoped overrides (`jr-rpc config set`) take precedence.

## Sandbox egress trace propagation

Pass `sandbox.egressTracePropagationDomains` to `createApp()` when sandboxed commands should keep Sentry trace context across sandbox network egress:

```ts
import { createApp } from "@sentry/junior";

const app = await createApp({
sandbox: {
egressTracePropagationDomains: ["sentry.io", "*.sentry.io"],
},
});
```

Configured non-provider domains receive trace-header transforms without requiring credential proxying.

Entries may be exact domains or leading wildcard domains. The wildcard form matches subdomains, not the apex domain, so include both forms when needed.

## Verification

- Validate required variables exist in deployment environment.
Expand Down
66 changes: 57 additions & 9 deletions packages/junior/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
} from "@/chat/configuration/defaults";
import { getSlackReactionConfig, setSlackReactionConfig } from "@/chat/config";
import { logException } from "@/chat/logging";
import { generateAssistantReply } from "@/chat/respond";
import { normalizeSandboxEgressTracePropagationDomains } from "@/chat/sandbox/egress-tracing";
import {
getPluginCatalogSignature,
getPluginProviders,
Expand Down Expand Up @@ -40,9 +42,13 @@ import {
createVercelConversationWorkCallback,
registerVercelConversationWorkDevConsumer,
type VercelConversationWorkCallbackOptions,
} from "./chat/task-execution/vercel-callback";
import { getProductionConversationWorkOptions } from "@/chat/app/production";
import type { WaitUntilFn } from "./handlers/types";
} from "@/chat/task-execution/vercel-callback";
import {
createProductionConversationWorkOptions,
createProductionSlackWebhookServices,
} from "@/chat/app/production";
import { withSandboxTracePropagation } from "@/chat/app/services";
import type { WaitUntilFn } from "@/handlers/types";

export { defineJuniorPlugins } from "./plugins";
export type {
Expand All @@ -65,6 +71,15 @@ export interface JuniorAppOptions {
conversationWork?: VercelConversationWorkCallbackOptions;
/** Direct plugin set override. Usually omitted when `juniorNitro()` uses a plugin module. */
plugins?: JuniorPluginSet;
/** Sandbox execution options. */
sandbox?: {
/**
* Egress domains allowed to carry Sentry trace propagation headers.
* Entries may be exact domains or leading wildcard domains such as
* `*.sentry.io`; wildcard entries match subdomains, not the apex domain.
*/
egressTracePropagationDomains?: string[];
};
waitUntil?: WaitUntilFn;
}

Expand Down Expand Up @@ -334,7 +349,12 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
const previousConfigDefaults = getConfigDefaults();
const previousSlackReactionConfig = getSlackReactionConfig();
let agentPluginRoutes: AgentPluginRouteRegistration[] = [];
let sandboxEgressTracePropagationDomains: string[] = [];
try {
sandboxEgressTracePropagationDomains =
normalizeSandboxEgressTracePropagationDomains(
options?.sandbox?.egressTracePropagationDomains,
);
setConfigDefaults(options?.configDefaults);
if (options?.slack) {
setSlackReactionConfig(options.slack);
Expand All @@ -356,6 +376,18 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
}

const waitUntil = options?.waitUntil ?? (await defaultWaitUntil());
const runtimeServiceOverrides = {
sandbox: {
tracePropagation: { domains: sandboxEgressTracePropagationDomains },
},
};
const slackWebhookServices = createProductionSlackWebhookServices({
services: runtimeServiceOverrides,
});
const generateReplyWithTracePropagation = withSandboxTracePropagation(
generateAssistantReply,
runtimeServiceOverrides.sandbox.tracePropagation,
);

const app = new Hono();

Expand All @@ -368,7 +400,9 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
// Vercel Sandbox proxying preserves the original upstream path, so detect
// authenticated proxy traffic before ordinary application routes claim it.
if (isSandboxEgressRequest(c.req.raw)) {
return await sandboxEgressProxyALL(c.req.raw);
return await sandboxEgressProxyALL(c.req.raw, {
tracePropagation: { domains: sandboxEgressTracePropagationDomains },
});
}
await next();
});
Expand All @@ -381,15 +415,21 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
// MCP callback must be registered before the generic OAuth callback
// because Hono matches routes top-down and `:provider` would swallow `mcp/`.
app.get("/api/oauth/callback/mcp/:provider", (c) => {
return mcpOauthCallbackGET(c.req.raw, c.req.param("provider"), waitUntil);
return mcpOauthCallbackGET(c.req.raw, c.req.param("provider"), waitUntil, {
generateReply: generateReplyWithTracePropagation,
});
});

app.get("/api/oauth/callback/:provider", (c) => {
return oauthCallbackGET(c.req.raw, c.req.param("provider"), waitUntil);
return oauthCallbackGET(c.req.raw, c.req.param("provider"), waitUntil, {
generateReply: generateReplyWithTracePropagation,
});
});

app.post("/api/internal/agent-dispatch", (c) => {
return agentDispatchPOST(c.req.raw, waitUntil);
return agentDispatchPOST(c.req.raw, waitUntil, {
tracePropagation: { domains: sandboxEgressTracePropagationDomains },
});
});

let agentContinuePOST:
Expand All @@ -400,7 +440,10 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
| undefined;
const getConversationWorkOptions = () => {
conversationWorkOptions ??=
options?.conversationWork ?? getProductionConversationWorkOptions();
Comment thread
cursor[bot] marked this conversation as resolved.
options?.conversationWork ??
createProductionConversationWorkOptions({
services: runtimeServiceOverrides,
});
Comment thread
cursor[bot] marked this conversation as resolved.
return conversationWorkOptions;
};
if (process.env.NODE_ENV === "development") {
Expand All @@ -418,7 +461,12 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
});

app.post("/api/webhooks/:platform", (c) => {
return webhooksPOST(c.req.raw, c.req.param("platform"), waitUntil);
return webhooksPOST(
c.req.raw,
c.req.param("platform"),
waitUntil,
slackWebhookServices,
);
});

return app;
Expand Down
3 changes: 3 additions & 0 deletions packages/junior/src/chat/agent-dispatch/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
generateAssistantReply as generateAssistantReplyImpl,
type AssistantReply,
} from "@/chat/respond";
import type { SandboxEgressTracePropagationConfig } from "@/chat/sandbox/egress-tracing";
import { logException } from "@/chat/logging";
import {
buildConversationContext,
Expand Down Expand Up @@ -61,6 +62,7 @@ const DISPATCH_SLICE_LEASE_MS = 5 * 60 * 1000;
export interface AgentDispatchRunnerDeps {
generateAssistantReply?: typeof generateAssistantReplyImpl;
scheduleCallback?: typeof scheduleDispatchCallback;
tracePropagation?: SandboxEgressTracePropagationConfig;
}

function getUserMessageId(dispatch: DispatchRecord): string {
Expand Down Expand Up @@ -304,6 +306,7 @@ export async function runAgentDispatchSlice(
sandbox: {
sandboxId,
sandboxDependencyProfileHash,
tracePropagation: deps.tracePropagation,
},
onSandboxAcquired: async (sandbox) => {
sandboxId = sandbox.sandboxId;
Expand Down
36 changes: 33 additions & 3 deletions packages/junior/src/chat/app/production.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SlackAdapter } from "@chat-adapter/slack";
import { createSlackRuntime } from "@/chat/app/factory";
import { withSandboxTracePropagation } from "@/chat/app/services";
import { createUserTokenStore } from "@/chat/capabilities/factory";
import {
getSlackBotToken,
Expand All @@ -14,6 +15,8 @@ import { createSlackConversationWorker } from "@/chat/task-execution/slack-work"
import { getVercelConversationWorkQueue } from "@/chat/task-execution/vercel-queue";
import type { VercelConversationWorkCallbackOptions } from "@/chat/task-execution/vercel-callback";
import { resumeAwaitingSlackContinuation } from "@/chat/runtime/agent-continue-runner";
import type { JuniorRuntimeServiceOverrides } from "@/chat/app/services";
import { generateAssistantReply } from "@/chat/respond";

let productionSlackAdapter: SlackAdapter | undefined;
let productionSlackRuntime: ReturnType<typeof createSlackRuntime> | undefined;
Expand Down Expand Up @@ -53,6 +56,22 @@ export function getProductionSlackRuntime(): ReturnType<
return productionSlackRuntime;
}

/** Create production-backed services for Slack webhook ingress. */
export function createProductionSlackWebhookServices(options?: {
services?: JuniorRuntimeServiceOverrides;
}): SlackWebhookServices {
const runtime = createSlackRuntime({
getSlackAdapter: getProductionSlackAdapter,
services: options?.services,
});
return {
getSlackAdapter: getProductionSlackAdapter,
getUserTokenStore: createUserTokenStore,
queue: getVercelConversationWorkQueue(),
runtime,
};
}

/** Return production services for Slack webhook ingress. */
export function getProductionSlackWebhookServices(): SlackWebhookServices {
return {
Expand All @@ -64,13 +83,24 @@ export function getProductionSlackWebhookServices(): SlackWebhookServices {
}

/** Return the production queue callback options for conversation work. */
export function getProductionConversationWorkOptions(): VercelConversationWorkCallbackOptions {
const runtime = getProductionSlackRuntime();
export function createProductionConversationWorkOptions(options?: {
services?: JuniorRuntimeServiceOverrides;
}): VercelConversationWorkCallbackOptions {
const runtime = createSlackRuntime({
getSlackAdapter: getProductionSlackAdapter,
services: options?.services,
});
return {
queue: getVercelConversationWorkQueue(),
run: createSlackConversationWorker({
getSlackAdapter: getProductionSlackAdapter,
resumeAwaitingContinuation: resumeAwaitingSlackContinuation,
resumeAwaitingContinuation: async (conversationId) =>
await resumeAwaitingSlackContinuation(conversationId, {
generateReply: withSandboxTracePropagation(
generateAssistantReply,
options?.services?.sandbox?.tracePropagation,
),
}),
runtime,
}),
};
Expand Down
30 changes: 28 additions & 2 deletions packages/junior/src/chat/app/services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { completeObject, completeText } from "@/chat/pi/client";
import { generateAssistantReply as generateAssistantReplyImpl } from "@/chat/respond";
import {
generateAssistantReply as generateAssistantReplyImpl,
type ReplyRequestContext,
} from "@/chat/respond";
import type { SandboxEgressTracePropagationConfig } from "@/chat/sandbox/egress-tracing";
import {
getAwaitingAgentContinueRequest,
scheduleAgentContinue,
Expand Down Expand Up @@ -42,9 +46,28 @@ export interface JuniorRuntimeServiceOverrides {
contextCompactor?: Partial<ContextCompactorDeps>;
replyExecutor?: Partial<Omit<ReplyExecutorServices, "generateThreadTitle">>;
subscribedReplyPolicy?: Partial<SubscribedReplyPolicyDeps>;
sandbox?: {
tracePropagation?: SandboxEgressTracePropagationConfig;
};
visionContext?: Partial<VisionContextDeps>;
}

/** Apply app-owned sandbox egress trace config unless a turn overrides it. */
export function withSandboxTracePropagation(
generateReply: typeof generateAssistantReplyImpl,
tracePropagation?: SandboxEgressTracePropagationConfig,
): typeof generateAssistantReplyImpl {
return async (messageText: string, context?: ReplyRequestContext) =>
await generateReply(messageText, {
...context,
sandbox: {
...context?.sandbox,
tracePropagation:
context?.sandbox?.tracePropagation ?? tracePropagation,
},
});
}

export function createJuniorRuntimeServices(
overrides: JuniorRuntimeServiceOverrides = {},
): JuniorRuntimeServices {
Expand Down Expand Up @@ -72,7 +95,10 @@ export function createJuniorRuntimeServices(
overrides.replyExecutor?.contextCompactor ?? contextCompactor,
generateAssistantReply:
overrides.replyExecutor?.generateAssistantReply ??
generateAssistantReplyImpl,
withSandboxTracePropagation(
generateAssistantReplyImpl,
overrides.sandbox?.tracePropagation,
),
getAwaitingAgentContinueRequest:
overrides.replyExecutor?.getAwaitingAgentContinueRequest ??
getAwaitingAgentContinueRequest,
Expand Down
Loading
Loading