Skip to content

events listen authentication returns HTTP 400 after approved DENY_ALL personal_sign #45

Description

@shawnwollenberg

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):

acp events listen --json

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

  1. Return a stable, non-secret error code/body for /auth/agent failures and
    preserve it in SDK errors.
  2. Confirm whether a Privy smart/embedded wallet requires a different
    verification identity or signature envelope than ordinary EIP-191 recovery.
  3. Confirm whether walletAddress, agent identity, chain ID, or signer public
    key must be supplied differently for P256/Privy agents.
  4. Wire the existing owner-OAuth approval result transport into authentication
    bootstrap so DENY_ALL does not depend on an already-authenticated wallet
    stream.
  5. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions