Skip to content

v0.2.0 — Wallet façade + policy engine + encrypted local vault

Choose a tag to compare

@riccardoio riccardoio released this 14 Jun 04:03

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çadecreate, 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.decidevault.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.x client ↔ v0.2.x server → ✅ compatible
  • v0.2.x client ↔ v0.1.x server → ❌ version_mismatch
  • v0.2.x client ↔ v0.3.x server → ❌ 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(): Decision split (kills the truthy-object footgun)
  • No wallet.vault / wallet.policy properties 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:


Full changelog: v0.1.0...v0.2.0