Skip to content
Closed
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
21 changes: 17 additions & 4 deletions packages/eve/src/execution/workflow-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,19 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
// the `emit` function is created below.
const pendingAuth = getPendingAuthorization(durableSession.state);
let completedAuths:
| Array<{ name: string; authorization: ConnectionAuthorizationChallenge }>
| Array<{
candidateId?: string;
name: string;
authorization: ConnectionAuthorizationChallenge;
}>
| undefined;
if (pendingAuth && input.input?.kind === "deliver") {
const authResults: Array<{ name: string } & AuthorizationResult> = [];
const completed: Array<{ name: string; authorization: ConnectionAuthorizationChallenge }> = [];
const completed: Array<{
candidateId?: string;
name: string;
authorization: ConnectionAuthorizationChallenge;
}> = [];
const remainingPayloads: DeliverPayload[] = [];
for (const payload of input.input.payloads) {
const cb = payload["authorizationCallback"] as
Expand All @@ -180,7 +188,11 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
callback: cb.callback,
hookUrl: challenge.hookUrl,
});
completed.push({ name: challenge.name, authorization: challenge.challenge });
completed.push({
candidateId: challenge.candidateId,
name: challenge.name,
authorization: challenge.challenge,
});
}
} else {
remainingPayloads.push(payload);
Expand Down Expand Up @@ -353,10 +365,11 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
});
if (completedAuths) {
const emissionState = getHarnessEmissionState(schemaSession.state);
for (const { name, authorization } of completedAuths) {
for (const { candidateId, name, authorization } of completedAuths) {
await handleEvent(
createAuthorizationCompletedEvent({
authorization,
candidateId,
name,
outcome: "authorized",
sequence: emissionState.sequence,
Expand Down
2 changes: 2 additions & 0 deletions packages/eve/src/harness/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const AUTHORIZATION_PENDING_BRAND = "__eveAuthorizationPending" as const;
// ---------------------------------------------------------------------------

export interface AuthorizationChallenge {
readonly candidateId?: string;
readonly name: string;
readonly challenge: ConnectionAuthorizationChallenge;
readonly hookUrl: string;
Expand Down Expand Up @@ -104,6 +105,7 @@ export function requestAuthorization(
export function redactSignalResume(signal: AuthorizationSignal): AuthorizationSignal {
return requestAuthorization(
signal.challenges.map((entry) => ({
candidateId: entry.candidateId,
name: entry.name,
challenge: entry.challenge,
hookUrl: entry.hookUrl,
Expand Down
8 changes: 7 additions & 1 deletion packages/eve/src/harness/input-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,13 @@ async function authorizePendingApprovalResponseInternal(input: {
}),
};
return {
authorization,
authorization: {
...authorization,
challenges: authorization.challenges.map((challenge) => ({
...challenge,
candidateId,
})),
},
candidateId,
kind: "authorization-required",
requestId: response.requestId,
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/harness/tool-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ export function createToolLoopHarness(config: ToolLoopHarnessConfig): StepFn {
await emit(
createAuthorizationRequiredEvent({
authorization: challenge.challenge,
candidateId: authorized.candidateId,
description:
challenge.challenge.instructions ?? `Authorization required for ${challenge.name}`,
name: challenge.name,
Expand Down
10 changes: 10 additions & 0 deletions packages/eve/src/protocol/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ export interface CompactionCompletedStreamEvent {
export interface AuthorizationRequiredStreamEvent {
data: {
authorization?: ConnectionAuthorizationChallenge;
candidateId?: string;
description: string;
name: string;
sequence: number;
Expand Down Expand Up @@ -560,6 +561,7 @@ export type ConnectionAuthorizationOutcome = AuthorizationOutcome;
*/
export interface AuthorizationCompletedStreamEvent {
data: {
candidateId?: string;
/**
* The challenge from the matching `authorization.required` event,
* journaled across the park. Lets channels keep rendering the
Expand Down Expand Up @@ -970,6 +972,7 @@ export function createActionsRequestedEvent(input: {
*/
export function createAuthorizationRequiredEvent(input: {
readonly authorization?: ConnectionAuthorizationChallenge;
readonly candidateId?: string;
readonly description: string;
readonly name: string;
readonly sequence: number;
Expand All @@ -987,6 +990,9 @@ export function createAuthorizationRequiredEvent(input: {
if (input.authorization !== undefined) {
data.authorization = input.authorization;
}
if (input.candidateId !== undefined) {
data.candidateId = input.candidateId;
}
if (input.webhookUrl !== undefined) {
data.webhookUrl = input.webhookUrl;
}
Expand All @@ -1003,6 +1009,7 @@ export function createAuthorizationRequiredEvent(input: {
*/
export function createAuthorizationCompletedEvent(input: {
readonly authorization?: ConnectionAuthorizationChallenge;
readonly candidateId?: string;
readonly name: string;
readonly outcome: AuthorizationOutcome;
readonly reason?: string;
Expand All @@ -1020,6 +1027,9 @@ export function createAuthorizationCompletedEvent(input: {
if (input.authorization !== undefined) {
data.authorization = input.authorization;
}
if (input.candidateId !== undefined) {
data.candidateId = input.candidateId;
}
if (input.reason !== undefined) {
data.reason = input.reason;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/eve/src/public/channels/slack/defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ describe("defaultEvents authorization.required", () => {
expect(channel.state.pendingAuthMessageTs).toEqual({ notion: "ts1" });
});

it("does not post a public status for candidate-scoped authorization", async () => {
const { channel, post, postEphemeral } = buildChannelStub({ triggeringUserId: "U777" });

await defaultEvents["authorization.required"]!(
{ ...authRequiredEvent(), candidateId: "candidate-1" },
channel,
sessionCtx,
);

expect(post).not.toHaveBeenCalled();
expect(postEphemeral).toHaveBeenCalledTimes(1);
});

it("targets the current Slack caller instead of stale channel state", async () => {
const { channel, postEphemeral } = buildChannelStub({ triggeringUserId: "U_FIRST" });
const currentCaller = sessionContext({
Expand Down
4 changes: 2 additions & 2 deletions packages/eve/src/public/channels/slack/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ export const defaultEvents: SlackChannelInternalEvents = {
// the session is blocked and later see it complete. The challenge
// itself remains private.
const pending = channel.state.pendingAuthMessageTs ?? {};
if (pending[event.name] === undefined) {
if (event.candidateId === undefined && pending[event.name] === undefined) {
const publicText = buildAuthRequiredPublicText({
displayName,
hasUser: triggeringUserId !== null,
Expand Down Expand Up @@ -350,7 +350,7 @@ export const defaultEvents: SlackChannelInternalEvents = {

async "authorization.completed"(event, channel, _ctx) {
const displayName = event.authorization?.displayName ?? formatConnectionDisplayName(event.name);
if (event.outcome === "authorized") {
if (event.outcome === "authorized" && event.candidateId === undefined) {
await channel.thread.startTyping(`Connected to ${displayName}. Resuming...`);
}

Expand Down