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
10 changes: 9 additions & 1 deletion packages/eve/src/harness/authorize-approval-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,22 @@ export async function authorizePendingApprovalResponse(input: {
},
};
const batch = getPendingInputBatch(input.session.state);
const explicitResponse = input.stepInput?.inputResponses?.find((entry) =>
batch?.requests.some(
(request) => request.requestId === entry.requestId && isApprovalRequest(request),
),
);
const activeCandidate = getApprovalAuditState(input.session.state).activeCandidates.find(
(candidate) =>
(candidate.status === "pending" && candidate.pendingEventEmitted === true) ||
(explicitResponse === undefined &&
candidate.status === "pending" &&
candidate.pendingEventEmitted === true) ||
(candidate.status === "authorization-required" &&
candidate.authorizationName !== undefined &&
getAuthorizationResult(candidate.authorizationName) !== undefined),
);
const response =
explicitResponse ??
input.stepInput?.inputResponses?.find((entry) =>
batch?.requests.some(
(request) => request.requestId === entry.requestId && isApprovalRequest(request),
Expand Down
61 changes: 61 additions & 0 deletions packages/eve/src/harness/input-requests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@
const session = setPendingInputBatch({
requests: [
...(
pendingSession().state?.["eve.runtime.pendingInputBatch"] as {

Check warning on line 1056 in packages/eve/src/harness/input-requests.test.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unsafe-optional-chaining)

Unsafe usage of optional chaining
requests: readonly InputRequest[];
}
).requests,
Expand Down Expand Up @@ -1113,6 +1113,67 @@
).toBe("unresolved");
});

it("starts a second responder candidate instead of resuming another user's pending candidate", async () => {
const base = pendingSession();
const session = {
...base,
state: {
...base.state,
"eve.runtime.hitl.approvalState": {
activeCandidates: {
"candidate-original": {
candidateId: "candidate-original",
createdAt: 100,
expiresAt: 700,
pendingEventEmitted: true,
requestId: "approval-1",
responder: {
authenticator: "slack-webhook",
issuer: "slack:T1",
principalId: "U_ORIGINAL",
principalType: "user",
},
status: "pending",
},
},
candidateHistory: [],
settlements: {},
},
},
};
const tools: HarnessToolMap = new Map([
[
"create_issue",
{
approval: { authorizeResponse: () => "allowed", policy: () => "user-approval" },
description: "Create issue",
inputSchema: jsonSchema({ type: "object" }),
name: "create_issue",
},
],
]);

const result = await approvalContext(() =>
authorizePendingApprovalResponse({
now: 200,
session,
stepInput: { inputResponses: [{ optionId: "approve", requestId: "approval-1" }] },
tools,
}),
);

expect(result).toMatchObject({ kind: "candidate-created" });
const active = (
result.session.state?.["eve.runtime.hitl.approvalState"] as {

Check warning on line 1167 in packages/eve/src/harness/input-requests.test.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unsafe-optional-chaining)

Unsafe usage of optional chaining
activeCandidates: Record<string, { responder: { principalId: string } }>;
}
).activeCandidates;
expect(Object.values(active).map((candidate) => candidate.responder.principalId)).toEqual([
"U_ORIGINAL",
"U1",
]);
});

it("restores the persisted candidate responder before resumed policy runs", async () => {
const candidateState = {
"eve.runtime.hitl.approvalState": {
Expand Down
Loading