feat: browser-proxy wallet signing relay - #2
Merged
Conversation
…used checks - package-lock.json had a corrupted empty version string for the axios override that broke `npm ci` (Invalid Version error). Regenerated from package.json — now resolves cleanly with 0 vulnerabilities. - Removed unused `IncomingMessage` import from src/agent/server.ts. - Enabled `noUnusedLocals` and `noUnusedParameters` in tsconfig.server.json to prevent future dead imports from being committed. The codebase already passes with these flags after the import fix.
Enable wallet transaction and message signing from inside workspace VMs
without private keys ever entering the guest. The relay chain is:
guest process → bloom-workspace CLI → guest-control Unix socket
→ agent HTTP (signing.request/signing.status/signing.resolve)
→ control plane HTTP (/api/workspaces/:id/signing/pending|resolve)
→ browser 3s poll → wallet provider (personal_sign, eth_sendTransaction,
eth_signTypedData_v4)
Changes across 13 files:
- Python guest controller: signing.request/status/resolve operations,
in-memory pending queue with TTL expiry, capability flags now true
- Guest CLI (bloom-workspace): 'sign' subcommand with polling + timeout
- TypeScript protocol: SigningRequest/SigningResolve envelope types in
guest-protocol.ts, z.boolean() for walletSigning/transactions in results
- Agent server: 3 new HTTP endpoints, randomUUID import
- Agent client: signingPending/signingResolve/signingRequest methods
- Control plane: WorkspaceService.signingPending/signingResolve,
2 new authenticated HTTP routes
- Browser: 3s signing poll, resolveSigningRequest dispatches to wallet,
starts on workspace running, cleaned up on close
- SIWE statement updated to disclose signing relay
- WalletConnect methods expanded to include transaction methods
- Bootstrap comment updated to document relay-based signing
- Browser now shows a preview dialog before forwarding signing requests to the wallet. Users see method, from, to, value, and data before approving. This prevents blind signing of malicious transactions. - formatSigningRequest() renders human-readable summaries for personal_sign, eth_sendTransaction, and eth_signTypedData_v4. - seen-set pruned to prevent unbounded memory growth during long sessions. - 5 integration tests covering: full request→pending→resolve lifecycle, method/param validation, double-resolve prevention, error rejection, and unknown request ID handling. - Signing dialog HTML + CSS.
BREAKING CHANGE: replaces raw EIP-1193 passthrough with Bloom outbox bridge
The previous implementation forwarded raw personal_sign / eth_sendTransaction
calls from the guest to the browser wallet, bypassing all of Bloom's safety
infrastructure. This rewrite goes through Bloom's native pipeline:
guest process writes intent to /bloom/wallets/<w>/chains/<c>/outbox/new.tx
→ bloom serve stages it, simulates it, generates plan.md
→ guest controller reads pending entries via IPC
→ agent HTTP → control plane → browser
→ browser shows Bloom's plan.md
→ user approves → confirm written back via IPC
This reuses Bloom's:
- Intent parser (shell/TOML/JSON → typed RawIntent)
- TxEngine (stage → simulate → confirm → broadcast)
- Policy engine (spending limits, allowed contracts)
- MEV/sandwich detection
- Nonce management and conflict resolution
- Outbox audit trail (intent.json, plan.md, policy_check.json)
Changes (13 files, +173/-310):
- Guest controller: replaced signing.request/status/pending/resolve with
outbox.pending/confirm/cancel using bloom IPC (JSON-RPC over Unix socket)
- Guest CLI: removed 'sign' subcommand (use bloom CLI's native outbox)
- Agent server: replaced signing endpoints with outbox endpoints
- Control plane: replaced signing routes with outbox routes
- Browser: shows Bloom's plan.md instead of raw hex
- Protocol: outbox.confirm/cancel schemas use txId to avoid envelope id clash
- Tests: removed signing tests (outbox requires running bloom serve daemon)
- Updated SIWE statement, WalletConnect methods, bootstrap comment, README
The 128 MiB default was hardcoded in three places (db.ts schema, db.ts migration, workspaces.ts constant) with no env var or config path. This made the quota impossible to tune without code changes. - Add BLOOM_STORAGE_QUOTA_MIB env var (16–5120 MiB, default 512) - Wire config.storageQuotaBytes through WorkspaceService - Remove hardcoded DEFAULT_STORAGE_QUOTA_BYTES constant - Update DB schema and migration defaults to 512 MiB (536870912) - Update Python guest controller default to match - Remove hardcoded '128 MiB' text from web UI (actual quota is already shown dynamically via formatBytes) - Add config test for the new option - Update workspace-data-api test expectation - Add __pycache__/ to .gitignore
…irectory walk
Security: outbox confirm/cancel endpoints accepted arbitrary strings for
txId, chain, and wallet fields — only length-validated. These values are
interpolated into Bloom VFS paths (e.g. /wallets/{wallet}/chains/{chain}/
outbox/pending/{txId}/confirm). A crafted value like '../../' could
traverse the VFS tree. Now enforced at all four layers: guest protocol
Zod schema, control server, agent server, and Python guest controller.
Performance: file write handler walked the entire workspace directory tree
twice per write — once before writing (quota check) and once after (usage
report). The post-write walk was redundant since we already know the delta
from the pre-write walk + file stat. Replaced with arithmetic.
Tests: added path traversal rejection tests to guest-protocol.test.ts.
…endpoint Reliability: ensureExt4Volume now checks available disk space via statfs before allocating a workspace volume. Returns HTTP 507 if insufficient. Observability: every HTTP request to both control and agent servers is now logged as structured JSON (timestamp, level, component, requestId, method, url, statusCode, durationMs). Request IDs are generated per-request and exposed via x-request-id response header for client-side correlation. Health-check 200s are suppressed to avoid log noise. Metrics: GET /metricsz returns workspace counts by state (running, pending, stopped, failed, total), process uptime, and memory usage. Intended for operator dashboards and monitoring tools.
…TP endpoints security.test.ts (31 tests): safeEqual timing-safe comparison, cookie parsing edge cases, clientIp trusted-proxy chain extraction, origin validation, token hashing determinism. data-volume.test.ts (6 tests): volume ID validation (path traversal rejection), quota bounds enforcement, disk space pre-flight check, idempotent creation. http-security.test.ts (7 tests): path traversal payloads rejected at HTTP layer for outbox confirm/cancel (id, chain, wallet fields), unauthenticated requests rejected, cross-wallet access denied, metricsz and healthz endpoints validated.
Previously, guest-bootstrap.sh hardcoded preinstalled = [] to prevent network-fetched code from entering guest VMs. This also blocked operator-curated Petals — a core workflow for Bloom workspaces. Changes: - Add BLOOM_PREINSTALLED_PETALS env var (comma-separated, max 32, alphanumeric/dash/underscore, validated by Zod) - guest-bootstrap.sh now writes the operator-approved list into config.toml instead of hardcoding [] - Re-bootstrap validates configured Petals against the approved list - Petal list passed through kernel cmdline (bloom_petals=) for QEMU and Firecracker runtimes, env var for process runtime - bloom-init parses bloom_petals from /proc/cmdline and exports BLOOM_PREINSTALLED_PETALS before bootstrap Users can still bloom install explicitly from the terminal (GitHub is in the egress allowlist). The operator-approved list controls what gets auto-installed during bootstrap.
Replace the 1,157-line Python guest-control daemon and shell wrapper with a modular Rust crate — a single binary serving as both the guest-side control server (AF_VSOCK, Unix socket, stdio) and the guest-local CLI client. Rebrand the outbox transaction relay as Bloom's Sealed Approval ceremony: - Remove outbox.confirm/outbox.cancel endpoints and protocol operations - Add read-only ceremony.pending that returns a ceremony URL - Reduce WalletConnect scope to personal_sign only (drop eth_sendTransaction and eth_signTypedData_v4) - Update SIWE auth statement to reference the ceremony flow Web UI: ceremony polling replaces outbox polling, lease countdown gains warning/critical visual states and an expiry message. Build: demo image and guest-bootstrap updated for the Rust binary. Excludes debug-env.test.ts (scratch file).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Enables wallet transaction and message signing from inside workspace VMs. Private keys never enter the guest — signing is relayed to the user's browser wallet.
How it works
The user runs
bloom-workspace sign personal_sign '["0x..."]'inside the workspace. The request flows through the existing guest→agent→control chain to the browser, which forwards it to the connected wallet (injected or WalletConnect). The wallet prompts the user for approval. The result flows back through the same chain.Design decisions
personal_sign,eth_sendTransaction,eth_signTypedData_v4only.reject_signer_inputsguard remains unchanged. Keys never enter the VM.Changes (13 files, +253/-16)
bloom-guest-control.pybloom-workspaceCLIsignsubcommand with polling + timeoutguest-protocol.tsSigningRequest/SigningResolveenvelope schemasresults.tsz.literal(false)→z.boolean()server.tsrandomUUIDimportagent-client.tssigningPending/signingResolve/signingRequestworkspaces.tssigningPending/signingResolveservice methodsserver.tsauth.tsmain.tsguest-bootstrap.shREADME.mdVerification
npm run typecheck✅npm test✅ (140/141; 1 pre-existing uid/gid env failure unrelated to signing)npm run build✅python3 -c "import py_compile; ..."✅ (both Python files)