v0.2.0 — Wallet façade + policy engine + encrypted local vault
Local-first wallet for autonomous agent buying. Four-method surface for the junior path; full policy + vault primitives underneath for power users.
The 10-line junior path
import { Wallet } from "@steelyard/buyer";
const wallet = await Wallet.open();
if (await wallet.isAllowed(intent)) {
const payment = await wallet.pay(intent);
// v0.3 will charge then call payment.complete()
// v0.2 stops here:
await payment.cancel();
}That's it. No YAML to write, no keystore to pick, no UUID to manage.
What ships in this release
| Package | Purpose |
|---|---|
@steelyard/buyer (root) |
Wallet façade — create, open, isAllowed, decide, pay, plus 12 maintenance methods (cards, limits, billing, recovery, rotatePassword). |
@steelyard/buyer/policy |
YAML policy engine. Deny-by-default. can / cannot rules with glob domains, ISO-4217 currency-explicit limits, project-vs-global overlay (global cannot always wins). |
@steelyard/buyer/vault |
Local encrypted card vault. XSalsa20-Poly1305 via @noble/ciphers. Master key in OS keychain (@napi-rs/keyring) by default; passwordKeystore (Argon2id-derived) for headless / Alpine / NixOS / containers. |
@steelyard/buyer/client |
Unchanged from v0.1 (buyer SDK for MCP/ACP/UCP). |
@steelyard/agent |
Unchanged from v0.1 (LLM CLI). |
@steelyard/protocol |
Unchanged from v0.1 (MCP/ACP/UCP adapters). |
Cross-platform CI matrix (all green)
| Lane | What it proves |
|---|---|
macos-latest / keychain |
@napi-rs/keyring works on hosted macOS without Touch ID prompts |
ubuntu-latest / keychain |
libsecret + gnome-keyring-daemon spun up in a single CI step works for the Secret Service path |
ubuntu-latest / password |
The no-system-deps passwordKeystore path works (Argon2id-derived master key) |
Advisory, not enforced
v0.2 ships advisory primitives. Policy and vault enforce nothing on a misbehaving agent that imports the subpaths directly. A prompt-injected agent could call vault.revealCard() and ignore the policy entirely. v0.3's purchase() will wire policy.decide → vault.pay into a single enforced flow that closes that bypass.
This is documented in the Wallet docs page — juniors using the Wallet API are safe by default; the bypass requires intentional subpath imports.
Test coverage
| Package | Tests | Coverage |
|---|---|---|
@steelyard/buyer |
62 | 96.46% |
@steelyard/core |
11 | 100% |
@steelyard/protocol |
27 | 98.16% |
@steelyard/agent |
8 | 90.06% |
Compatibility
Pre-1.0 minor-match (per the rule we established in v0.1.0).
v0.2.xclient ↔v0.2.xserver → ✅ compatiblev0.2.xclient ↔v0.1.xserver → ❌version_mismatchv0.2.xclient ↔v0.3.xserver → ❌version_mismatch
The read-side capability version on @steelyard/protocol/mcp servers stays at 0.1 until v0.3 ships payment-capable adapters; only the buyer-side package versions bump.
Settled design decisions
The full v0.2 design went through two rounds of adversarial review with Codex. 38 numbered decisions are baked into the implementation, including:
- XSalsa20-Poly1305 over AES-256-GCM (pure JS, no native crypto)
- UUID-bound vault headers (vault survives file moves and machine recovery)
isAllowed(): boolean+decide(): Decisionsplit (kills the truthy-object footgun)- No
wallet.vault/wallet.policyproperties on the root Wallet (forces juniors through the safe path) - Currency-explicit
SimpleLimits(JPY/KRW convert correctly) - Spending ledger uses
proper-lockfile+O_APPEND(atomic across processes) - Recovery via
exportRecovery()is part of setup, not buried in engine docs
Docs
The public mkdocs site is updated:
- Wallet (junior surface) — start here
- Policy (engine)
- Vault (engine)
- Existing protocol pages updated to reflect the v0.1 namespace migration (
@steelyard/protocol/mcp, etc.)
Full changelog: v0.1.0...v0.2.0