diff --git a/packages/junior/src/chat/agent/resume.ts b/packages/junior/src/chat/agent/resume.ts index 9d9c5fc5a..027e06236 100644 --- a/packages/junior/src/chat/agent/resume.ts +++ b/packages/junior/src/chat/agent/resume.ts @@ -216,6 +216,7 @@ export function createResumeState(args: ResumeStateArgs) { return { status: "awaiting_auth", providerDisplayName: pause.providerDisplayName, + ...(pause.requestText ? { requestText: pause.requestText } : {}), ...(usage ? { usage } : {}), }; } catch (error) { diff --git a/packages/junior/src/chat/agent/tools.ts b/packages/junior/src/chat/agent/tools.ts index f8c3f0c3a..944c6f122 100644 --- a/packages/junior/src/chat/agent/tools.ts +++ b/packages/junior/src/chat/agent/tools.ts @@ -174,6 +174,7 @@ export async function wireAgentTools( args.routing.source.platform === "slack" ? args.routing.source.threadTs : undefined, + userMessage: args.userInput, pendingAuth: args.state.pendingAuth, recordPendingAuth: args.durability.recordPendingAuth, authorizationFlowMode: args.policy.authorizationFlowMode, diff --git a/packages/junior/src/chat/runtime/agent-run-outcome.ts b/packages/junior/src/chat/runtime/agent-run-outcome.ts index 8bae90b88..2325b57e7 100644 --- a/packages/junior/src/chat/runtime/agent-run-outcome.ts +++ b/packages/junior/src/chat/runtime/agent-run-outcome.ts @@ -19,6 +19,7 @@ export type AgentRunOutcome = | { status: "awaiting_auth"; providerDisplayName: string; + requestText?: string; usage?: AgentTurnUsage; }; diff --git a/packages/junior/src/chat/runtime/reply-executor.ts b/packages/junior/src/chat/runtime/reply-executor.ts index 3fcbcdd97..f88aa4699 100644 --- a/packages/junior/src/chat/runtime/reply-executor.ts +++ b/packages/junior/src/chat/runtime/reply-executor.ts @@ -577,6 +577,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) { }; const postAuthPauseNotice = async ( providerDisplayName: string, + requestText?: string, ): Promise => { if (!actor) { throw new Error("Slack auth pause notice requires an actor"); @@ -584,6 +585,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) { const text = buildAuthPauseResponse( actor.userId, providerDisplayName, + requestText, ); try { await beforeFirstResponsePost(); @@ -1386,7 +1388,10 @@ export function createReplyToThread(deps: ReplyExecutorDeps) { shouldPersistFailureState = false; return; } - await postAuthPauseNotice(outcome.providerDisplayName); + await postAuthPauseNotice( + outcome.providerDisplayName, + outcome.requestText, + ); completeAuthPauseTurn({ conversation: preparedState.conversation, sessionId: turnId, diff --git a/packages/junior/src/chat/runtime/slack-resume.ts b/packages/junior/src/chat/runtime/slack-resume.ts index 4a5db4e17..f295682e1 100644 --- a/packages/junior/src/chat/runtime/slack-resume.ts +++ b/packages/junior/src/chat/runtime/slack-resume.ts @@ -426,7 +426,11 @@ export async function resumeSlackTurn( }); let processingReaction: ProcessingReactionSession | undefined; let deferredAuthInfo: - | { providerDisplayName: string; actorId: string | undefined } + | { + providerDisplayName: string; + actorId: string | undefined; + requestText?: string; + } | undefined; let deferredPauseHandler: (() => Promise) | undefined; let deferredFailureHandler: (() => Promise) | undefined; @@ -631,6 +635,7 @@ export async function resumeSlackTurn( deferredAuthInfo = { providerDisplayName: outcome.providerDisplayName, actorId: isUserActor(resumeActor) ? resumeActor.userId : undefined, + ...(outcome.requestText ? { requestText: outcome.requestText } : {}), }; deferredPauseHandler = async () => { await onAuthPause({ @@ -793,6 +798,7 @@ export async function resumeSlackTurn( buildAuthPauseResponse( deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, + deferredAuthInfo.requestText, ), runArgs.conversationId, ); diff --git a/packages/junior/src/chat/services/auth-pause-response.ts b/packages/junior/src/chat/services/auth-pause-response.ts index 434101436..5243177ed 100644 --- a/packages/junior/src/chat/services/auth-pause-response.ts +++ b/packages/junior/src/chat/services/auth-pause-response.ts @@ -1,8 +1,30 @@ +const MAX_AUTH_REQUEST_LENGTH = 200; + +function formatAuthRequest(requestText: string): string | undefined { + const normalized = requestText.replace(/\s+/g, " ").trim(); + if (!normalized) { + return undefined; + } + const bounded = + normalized.length > MAX_AUTH_REQUEST_LENGTH + ? `${normalized.slice(0, MAX_AUTH_REQUEST_LENGTH - 1).trimEnd()}…` + : normalized; + return bounded + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); +} + /** Build the visible Slack thread note for an auth-paused turn. */ export function buildAuthPauseResponse( slackUserId: string | undefined, providerDisplayName: string, + requestText?: string, ): string { const mention = slackUserId ? `<@${slackUserId}> ` : ""; - return `${mention}I'll need you to authorize ${providerDisplayName}. I sent you a link.`; + const request = requestText ? formatAuthRequest(requestText) : undefined; + if (!request) { + return `${mention}I'll need you to authorize ${providerDisplayName}. I sent you a link.`; + } + return `${mention}I need access to ${providerDisplayName} to continue.\n\n*Why:* ${request}\n\nI sent you a link.`; } diff --git a/packages/junior/src/chat/services/auth-pause.ts b/packages/junior/src/chat/services/auth-pause.ts index c8d8d6039..808b09434 100644 --- a/packages/junior/src/chat/services/auth-pause.ts +++ b/packages/junior/src/chat/services/auth-pause.ts @@ -11,12 +11,14 @@ export class AuthorizationPauseError extends Error { readonly kind: AuthorizationPauseKind; readonly provider: string; readonly providerDisplayName: string; + readonly requestText?: string; constructor( kind: AuthorizationPauseKind, provider: string, providerDisplayName: string, disposition: AuthorizationPauseDisposition, + requestText?: string, ) { super( kind === "mcp" @@ -31,6 +33,7 @@ export class AuthorizationPauseError extends Error { this.kind = kind; this.provider = provider; this.providerDisplayName = providerDisplayName; + this.requestText = requestText; } } diff --git a/packages/junior/src/chat/services/mcp-auth-orchestration.ts b/packages/junior/src/chat/services/mcp-auth-orchestration.ts index 14d50ba0e..0334e79f2 100644 --- a/packages/junior/src/chat/services/mcp-auth-orchestration.ts +++ b/packages/junior/src/chat/services/mcp-auth-orchestration.ts @@ -35,8 +35,9 @@ export class McpAuthorizationPauseError extends AuthorizationPauseError { provider: string, providerDisplayName: string, disposition: "link_already_sent" | "link_sent", + requestText?: string, ) { - super("mcp", provider, providerDisplayName, disposition); + super("mcp", provider, providerDisplayName, disposition, requestText); } } @@ -232,6 +233,7 @@ export function createMcpAuthOrchestration( provider, providerLabel, reusingPendingLink ? "link_already_sent" : "link_sent", + input.userMessage, ); input.abortAgent(); return true; diff --git a/packages/junior/src/chat/services/plugin-auth-orchestration.ts b/packages/junior/src/chat/services/plugin-auth-orchestration.ts index c12c4fd72..c5b80ce6b 100644 --- a/packages/junior/src/chat/services/plugin-auth-orchestration.ts +++ b/packages/junior/src/chat/services/plugin-auth-orchestration.ts @@ -32,8 +32,9 @@ export class PluginAuthorizationPauseError extends AuthorizationPauseError { provider: string, providerDisplayName: string, disposition: "link_already_sent" | "link_sent", + requestText?: string, ) { - super("plugin", provider, providerDisplayName, disposition); + super("plugin", provider, providerDisplayName, disposition, requestText); } } @@ -56,6 +57,7 @@ export interface PluginAuthOrchestrationInput { destination?: Destination; source?: Source; threadTs?: string; + userMessage?: string; pendingAuth?: ConversationPendingAuthState; recordPendingAuth?: ( pendingAuth: ConversationPendingAuthState, @@ -224,6 +226,7 @@ export function createPluginAuthOrchestration( provider, providerLabel, reusingPendingLink ? "link_already_sent" : "link_sent", + input.userMessage, ); input.abortAgent(); throw pendingPause; diff --git a/packages/junior/tests/integration/mcp-auth-runtime-slack.test.ts b/packages/junior/tests/integration/mcp-auth-runtime-slack.test.ts index 92a147cef..0e8e09714 100644 --- a/packages/junior/tests/integration/mcp-auth-runtime-slack.test.ts +++ b/packages/junior/tests/integration/mcp-auth-runtime-slack.test.ts @@ -488,7 +488,7 @@ describe("mcp auth runtime slack integration", () => { params: expect.objectContaining({ channel: "C123", thread_ts: "1700000000.001", - text: "<@U123> I'll need you to authorize Eval Auth. I sent you a link.", + text: "<@U123> I need access to Eval Auth to continue.\n\n*Why:* what did i say about the budget?\n\nI sent you a link.", }), }), ]); @@ -643,7 +643,7 @@ describe("mcp auth runtime slack integration", () => { params: expect.objectContaining({ channel: "C123", thread_ts: "1700000000.001", - text: "<@U123> I'll need you to authorize Eval Auth. I sent you a link.", + text: "<@U123> I need access to Eval Auth to continue.\n\n*Why:* what did i say about the budget?\n\nI sent you a link.", }), }), expect.objectContaining({ @@ -741,7 +741,7 @@ describe("mcp auth runtime slack integration", () => { params: expect.objectContaining({ channel: "C124", thread_ts: "1700000000.002", - text: "<@U123> I'll need you to authorize Eval Auth. I sent you a link.", + text: "<@U123> I need access to Eval Auth to continue.\n\n*Why:* what did i say about the budget?\n\nI sent you a link.", }), }), ]); @@ -1062,7 +1062,7 @@ describe("mcp auth runtime slack integration", () => { params: expect.objectContaining({ channel: "C125", thread_ts: "1700000000.003", - text: "<@U123> I'll need you to authorize Eval Auth. I sent you a link.", + text: "<@U123> I need access to Eval Auth to continue.\n\n*Why:* use eval-auth directly for the budget answer\n\nI sent you a link.", }), }), expect.objectContaining({ diff --git a/packages/junior/tests/unit/services/auth-pause-response.test.ts b/packages/junior/tests/unit/services/auth-pause-response.test.ts new file mode 100644 index 000000000..4c4694c98 --- /dev/null +++ b/packages/junior/tests/unit/services/auth-pause-response.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { buildAuthPauseResponse } from "@/chat/services/auth-pause-response"; + +describe("buildAuthPauseResponse", () => { + it("shows the escaped user request in the public notice", () => { + expect( + buildAuthPauseResponse( + "U123", + "GitHub", + " Update & notify the team ", + ), + ).toBe( + "<@U123> I need access to GitHub to continue.\n\n*Why:* Update <roadmap> & notify the team\n\nI sent you a link.", + ); + }); + + it("falls back to the generic notice without request text", () => { + expect(buildAuthPauseResponse("U123", "GitHub")).toBe( + "<@U123> I'll need you to authorize GitHub. I sent you a link.", + ); + }); +}); diff --git a/packages/junior/tests/unit/services/mcp-auth-orchestration.test.ts b/packages/junior/tests/unit/services/mcp-auth-orchestration.test.ts index f4a94e0a1..f885918cf 100644 --- a/packages/junior/tests/unit/services/mcp-auth-orchestration.test.ts +++ b/packages/junior/tests/unit/services/mcp-auth-orchestration.test.ts @@ -177,6 +177,9 @@ describe("createMcpAuthOrchestration", () => { await expect(orchestration.onAuthorizationRequired("github")).resolves.toBe( true, ); + expect(orchestration.getPendingPause()).toMatchObject({ + requestText: "use MCP", + }); expect(deliverPrivateMessage).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/junior/tests/unit/services/plugin-auth-orchestration.test.ts b/packages/junior/tests/unit/services/plugin-auth-orchestration.test.ts index f853313a1..f5d96a571 100644 --- a/packages/junior/tests/unit/services/plugin-auth-orchestration.test.ts +++ b/packages/junior/tests/unit/services/plugin-auth-orchestration.test.ts @@ -253,6 +253,32 @@ describe("createPluginAuthOrchestration", () => { expect(unlinkProvider).not.toHaveBeenCalled(); }); + it("preserves the user request on the auth pause", async () => { + startOAuthFlow.mockResolvedValue({ + ok: true, + delivery: { channelId: "D123" }, + }); + + const orchestration = createPluginAuthOrchestration({ + abortAgent: vi.fn(), + actorId: "U123", + userMessage: "Create the requested issue", + userTokenStore: tokenStore(), + }); + + let caught: unknown; + try { + await orchestration.maybeHandleAuthSignal({ + auth_required: githubWriteSignal, + }); + } catch (error) { + caught = error; + } + + expect(caught).toBeInstanceOf(PluginAuthorizationPauseError); + expect(caught).toMatchObject({ requestText: "Create the requested issue" }); + }); + it("starts oauth for GitHub write grant signal", async () => { startOAuthFlow.mockResolvedValue({ ok: true,