ACP events listen authentication fails after approved DENY_ALL signature
Upstream issue summary
@virtuals-protocol/acp-cli 1.0.34, using its transitive
@virtuals-protocol/acp-node-v2 0.1.x events transport, cannot establish an
agent session for a Base/Privy agent whose P256 signer uses DENY_ALL.
The initial SDK bootstrap deadlock was isolated separately: the SDK waits for
a personal_sign result over the authenticated wallet stream, but that stream
cannot open until the signature has authenticated the agent. A compatibility
bridge retrieved the owner-approved result through the official OAuth-backed
GET /wallets/approvals/:approvalId endpoint and delivered it to the SDK's
existing approval gate.
After that delivery, the signature passed all local integrity checks but the
official POST /auth/agent request returned HTTP 400. The event streams never
opened.
Environment
- ACP CLI: exactly
1.0.34
- ACP Node SDK:
0.1.12 (resolved transitively from ^0.1.12)
- Network/chain ID: Base mainnet,
8453
- Agent wallet type: Virtuals-provisioned Privy EVM wallet
- Signer: P256 signer with
DENY_ALL
- Node: project requirement
>=20.19.0
Agent identifiers, wallet addresses, OAuth credentials, approval results, and
the real signature are omitted because they are unnecessary for reproduction.
Exact sanitized request contract
The request is constructed by
@virtuals-protocol/acp-node-v2/dist/events/acpHttpClient.js:
POST https://api.acp.virtuals.io/auth/agent
Content-Type: application/json
{
"walletAddress": "0x<registered 20-byte provider wallet>",
"signature": "0x<130 lowercase-or-uppercase hex characters; 65 bytes>",
"message": "acp-auth:<13-digit Unix timestamp in milliseconds>",
"chainId": 8453
}
Observed response contract:
HTTP/1.1 400 Bad Request
Content-Type: application/json
<body unavailable from the failed run>
The exact body is intentionally reported as unavailable. The SDK throws only
Agent auth failed: 400 Bad Request and discards the response body. The
compatibility diagnostic used an asynchronous clone read; the CLI exited before
that read emitted the body. It has now been changed to await the sanitized clone
read, but no further live owner-signature attempt was made.
Facts verified before the request
- Approval kind was exactly
sign-message.
- Encoding was exactly UTF-8.
- Message matched
^acp-auth:\\d{13}$ with no additional text.
- Approval result was bound to the original approval ID, wallet, and message.
- Signature was a 65-byte
0x-prefixed ECDSA signature.
- EIP-191 recovery of the exact message and signature equaled the registered
provider wallet supplied in walletAddress.
chainId was the numeric value 8453.
- The challenge was fresh (well inside the approximately five-minute backend
validity window established with non-signing probes).
- No transaction, contract call, allowance, value transfer, or typed-data
signature was involved.
Reproduction
The non-live contract test requires no credentials and performs no network
request:
Credential-free contract test
node --test tests-js/acp_auth_contract.test.mjs
It imports the official events client, fixes the timestamp, supplies a mock
signer, intercepts fetch, and asserts the exact method, headers, URL, and JSON
serialization shown above. It also demonstrates that the SDK discards a JSON
error body and exposes only status/status text.
Live reproduction (upstream maintainers only, with an owner prepared to approve
one exact auth message):
With a DENY_ALL signer, approve only the request whose decoded operation is
personal_sign("acp-auth:<current timestamp milliseconds>"). Observe that the
approval completes, then /auth/agent returns 400 and neither /chats/stream
nor /wallets/stream opens.
Expected behavior
The backend accepts a fresh EIP-191 signature that recovers the registered agent
wallet, returns { "data": { "token": "<redacted>" } }, and the CLI opens the
chat and wallet event streams.
Actual behavior
The backend returns HTTP 400 after successful owner approval and signature
recovery. No agent token is returned, so the listener exits before connecting.
Requested upstream investigation
- Return a stable, non-secret error code/body for
/auth/agent failures and
preserve it in SDK errors.
- Confirm whether a Privy smart/embedded wallet requires a different
verification identity or signature envelope than ordinary EIP-191 recovery.
- Confirm whether
walletAddress, agent identity, chain ID, or signer public
key must be supplied differently for P256/Privy agents.
- Wire the existing owner-OAuth approval result transport into authentication
bootstrap so DENY_ALL does not depend on an already-authenticated wallet
stream.
- Add an integration test covering
events listen cold start with a
DENY_ALL signer on Base.
Safety and current blocker
No more live authentication attempts should be made until upstream explains the
400 response contract or provides a corrected client/backend flow. The buyer's
funded state is unrelated and must remain unchanged. No job was created.
ACP
events listenauthentication fails after approved DENY_ALL signatureUpstream issue summary
@virtuals-protocol/acp-cli1.0.34, using its transitive@virtuals-protocol/acp-node-v20.1.x events transport, cannot establish anagent session for a Base/Privy agent whose P256 signer uses
DENY_ALL.The initial SDK bootstrap deadlock was isolated separately: the SDK waits for
a
personal_signresult over the authenticated wallet stream, but that streamcannot open until the signature has authenticated the agent. A compatibility
bridge retrieved the owner-approved result through the official OAuth-backed
GET /wallets/approvals/:approvalIdendpoint and delivered it to the SDK'sexisting approval gate.
After that delivery, the signature passed all local integrity checks but the
official
POST /auth/agentrequest returned HTTP 400. The event streams neveropened.
Environment
1.0.340.1.12(resolved transitively from^0.1.12)8453DENY_ALL>=20.19.0Agent identifiers, wallet addresses, OAuth credentials, approval results, and
the real signature are omitted because they are unnecessary for reproduction.
Exact sanitized request contract
The request is constructed by
@virtuals-protocol/acp-node-v2/dist/events/acpHttpClient.js:Observed response contract:
The exact body is intentionally reported as unavailable. The SDK throws only
Agent auth failed: 400 Bad Requestand discards the response body. Thecompatibility diagnostic used an asynchronous clone read; the CLI exited before
that read emitted the body. It has now been changed to await the sanitized clone
read, but no further live owner-signature attempt was made.
Facts verified before the request
sign-message.^acp-auth:\\d{13}$with no additional text.0x-prefixed ECDSA signature.provider wallet supplied in
walletAddress.chainIdwas the numeric value8453.validity window established with non-signing probes).
signature was involved.
Reproduction
The non-live contract test requires no credentials and performs no network
request:
Credential-free contract test
It imports the official events client, fixes the timestamp, supplies a mock
signer, intercepts
fetch, and asserts the exact method, headers, URL, and JSONserialization shown above. It also demonstrates that the SDK discards a JSON
error body and exposes only status/status text.
Live reproduction (upstream maintainers only, with an owner prepared to approve
one exact auth message):
With a
DENY_ALLsigner, approve only the request whose decoded operation ispersonal_sign("acp-auth:<current timestamp milliseconds>"). Observe that theapproval completes, then
/auth/agentreturns 400 and neither/chats/streamnor
/wallets/streamopens.Expected behavior
The backend accepts a fresh EIP-191 signature that recovers the registered agent
wallet, returns
{ "data": { "token": "<redacted>" } }, and the CLI opens thechat and wallet event streams.
Actual behavior
The backend returns HTTP 400 after successful owner approval and signature
recovery. No agent token is returned, so the listener exits before connecting.
Requested upstream investigation
/auth/agentfailures andpreserve it in SDK errors.
verification identity or signature envelope than ordinary EIP-191 recovery.
walletAddress, agent identity, chain ID, or signer publickey must be supplied differently for P256/Privy agents.
bootstrap so
DENY_ALLdoes not depend on an already-authenticated walletstream.
events listencold start with aDENY_ALLsigner on Base.Safety and current blocker
No more live authentication attempts should be made until upstream explains the
400 response contract or provides a corrected client/backend flow. The buyer's
funded state is unrelated and must remain unchanged. No job was created.