Skip to content

feat: handles manual transaction approval#12

Open
andrew-virtuals wants to merge 4 commits into
mainfrom
feat/policy-guardrail
Open

feat: handles manual transaction approval#12
andrew-virtuals wants to merge 4 commits into
mainfrom
feat/policy-guardrail

Conversation

@andrew-virtuals
Copy link
Copy Markdown
Contributor

@andrew-virtuals andrew-virtuals commented May 12, 2026

Note

High Risk
High risk because it changes authentication signing from message-signing to EIP-712 typed data and modifies the Privy/Alchemy transaction submission path to support manual approval and new user-operation signing, impacting wallet security and transaction execution flow.

Overview
Switches agent/provider authentication from signMessage to EIP-712 typed-data signatures via new core/agentAuth.ts and updates all auth clients/context to send issuedAt + typed data instead of a raw message.

Adds a manual approval gate (core/approvalGate.ts) and a new SSE wallet stream alongside chat; SseTransport.connect() and AcpAgent.start() now accept configurable SupportedStreams (defaulting to chat+wallet) and the wallet stream resolves pending approvals.

Updates PrivyAlchemyEvmProviderAdapter to detect server-side APPROVAL_REQUIRED responses, wait for approval events before continuing, and changes call submission to a prepare/sign-user-operation/send-prepared flow using a configured ALCHEMY_SIGNING_CONTRACT.

Reviewed by Cursor Bugbot for commit 1ec514b. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread src/providers/evm/privyAlchemyEvmProviderAdapter.ts
const payload = data?.code ? data : data?.message;
if (res.status === 403 && payload?.code === "APPROVAL_REQUIRED") {
const approvalId = payload.details?.approvalId ?? "";
const approvalUrl = payload.details?.approvalUrl ?? "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty approvalId fallback creates broken approval flow

Medium Severity

When payload.details?.approvalId is missing from the server's 403 response, approvalId falls back to "". This empty string is passed to awaitApproval(""), registering a pending entry keyed by "". The subsequent SSE approval event will almost certainly carry a real ID that won't match "", so the promise silently times out after 5 minutes instead of failing fast. Multiple requests hitting this case would also collide on the "" key in the global pending map, causing "already being awaited" errors.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0096d06. Configure here.


throw new BaseError(msg, {
details: data?.details,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-string details passed to BaseError constructor

Low Severity

data?.details is passed as the details option to viem's BaseError, which expects string | undefined. Since data is any from res.json(), and server error responses may include details as an object (e.g., { approvalId, approvalUrl }), the error message would render Details: [object Object] — producing an unhelpful error for debugging.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0096d06. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 559f5a9. Configure here.

chatStream?.close();
walletStream?.close();
throw err;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.all cleanup leaks successfully opened EventSource streams

Medium Severity

When Promise.all rejects (one stream fails to open), the await throws before the destructuring assignment [chatStream, walletStream] = ... executes, so both variables remain null. The catch block's chatStream?.close() and walletStream?.close() calls are therefore no-ops. If one openStream resolved successfully before the other rejected, the successfully opened EventSource is leaked with no reference to close it.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 559f5a9. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants