Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/junior/src/chat/agent/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/junior/src/chat/agent/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/junior/src/chat/runtime/agent-run-outcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type AgentRunOutcome =
| {
status: "awaiting_auth";
providerDisplayName: string;
requestText?: string;
usage?: AgentTurnUsage;
};

Expand Down
7 changes: 6 additions & 1 deletion packages/junior/src/chat/runtime/reply-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,13 +577,15 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
};
const postAuthPauseNotice = async (
providerDisplayName: string,
requestText?: string,
): Promise<void> => {
if (!actor) {
throw new Error("Slack auth pause notice requires an actor");
}
const text = buildAuthPauseResponse(
actor.userId,
providerDisplayName,
requestText,
);
try {
await beforeFirstResponsePost();
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion packages/junior/src/chat/runtime/slack-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>) | undefined;
let deferredFailureHandler: (() => Promise<void>) | undefined;
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -793,6 +798,7 @@ export async function resumeSlackTurn(
buildAuthPauseResponse(
deferredAuthInfo.actorId,
deferredAuthInfo.providerDisplayName,
deferredAuthInfo.requestText,
),
runArgs.conversationId,
);
Expand Down
24 changes: 23 additions & 1 deletion packages/junior/src/chat/services/auth-pause-response.ts
Original file line number Diff line number Diff line change
@@ -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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}

/** 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.`;
}
3 changes: 3 additions & 0 deletions packages/junior/src/chat/services/auth-pause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -31,6 +33,7 @@ export class AuthorizationPauseError extends Error {
this.kind = kind;
this.provider = provider;
this.providerDisplayName = providerDisplayName;
this.requestText = requestText;
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/junior/src/chat/services/mcp-auth-orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -232,6 +233,7 @@ export function createMcpAuthOrchestration(
provider,
providerLabel,
reusingPendingLink ? "link_already_sent" : "link_sent",
input.userMessage,
);
input.abortAgent();
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -56,6 +57,7 @@ export interface PluginAuthOrchestrationInput {
destination?: Destination;
source?: Source;
threadTs?: string;
userMessage?: string;
pendingAuth?: ConversationPendingAuthState;
recordPendingAuth?: (
pendingAuth: ConversationPendingAuthState,
Expand Down Expand Up @@ -224,6 +226,7 @@ export function createPluginAuthOrchestration(
provider,
providerLabel,
reusingPendingLink ? "link_already_sent" : "link_sent",
input.userMessage,
);
input.abortAgent();
throw pendingPause;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
}),
}),
]);
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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.",
}),
}),
]);
Expand Down Expand Up @@ -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({
Expand Down
22 changes: 22 additions & 0 deletions packages/junior/tests/unit/services/auth-pause-response.test.ts
Original file line number Diff line number Diff line change
@@ -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 <roadmap> & notify the team ",
),
).toBe(
"<@U123> I need access to GitHub to continue.\n\n*Why:* Update &lt;roadmap&gt; &amp; 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.",
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading