Changelog
All notable changes to Anchor are recorded here, following Keep a Changelog and Semantic Versioning.
The build diary at anchor.ryanio.com has the story behind these; this file has the facts.
[Unreleased]
Added
- Local read-only data service: cache-first OpenSea API v2 client, SQLite response cache with explicit freshness, keyring-backed credentials, loopback-only HTTP API.
- Autonomy and spend-control model — bounded authority, value tiers from $100 to $100k+, vendor versus onchain policy enforcement (
docs/autonomy.md). - Working agreement for agents and humans (
AGENTS.md). - Test suite on
node --test, plus CI for typecheck and tests. - Build diary and changelog published to anchor.ryanio.com.
Added (tooling)
- Biome 2.5.12 as the single formatter and linter — never ESLint or Prettier. One config at the root governs every workspace; CI runs
biome ci .and fails on any diagnostic. - A daily build-diary job on the Hermes scheduler that skips the entry when the day produced nothing worth reading, rather than padding.
Added (brand)
- An anchor mark that reads as an A — distinctive rather than a stock anchor glyph, and legible at 16px. Stroke-based with
currentColor, so one file themes everywhere. Favicon and solid-fill variants alongside it. scripts/generate-asset.ts— image assets from xAI's Grok Imagine, key in the OS keyring, zero dependencies (the endpoint is OpenAI-compatible). For illustration and OG cards only; the logo and icons stay hand-authored SVG.
Added (executor)
Executorinterface splittingrequest → simulate → decide → submit, so holding one stage does not grant the next.PolicyDecisionappears only in return types — "here is my own approval, please submit it" is not a sentence the API can express.- Approvals are unforgeable two ways: an
ApprovedActioncarries a property keyed by a non-exportedunique symbol, so constructing one elsewhere fails to compile; and because a cast defeats types, the brand is backed by a module-privateWeakSetthatSigner.submitchecks. The witness has no runtime representation, so a spread,structuredClone, or JSON round-trip fails closed. - Reference
PolicyEngineenforcing per-transaction and rolling caps, contract/action/withdrawal allowlists, and one-way revoke. It signs nothing and submits nothing. setApprovalForAllis blocked three ways, including at the type level — it cannot be configured intoallowedActionsat all.- Caps charge gross outflow (netting would enable wash-trade drains) and budget is reserved at approval rather than settlement (otherwise never settling means unlimited live approvals).
Security
- The site generator escaped
<,>and&but not quotes, so a link URL in a diary entry or changelog line could break out of thehrefattribute;javascript:URLs rendered as live links. Quotes are now escaped and onlyhttp,https,mailtoand site-relative URLs survive — verified against a real HTML parser, not a regex. - The data service now requires a loopback
Hostheader. Binding to127.0.0.1stops the network reaching it but not a browser: a page whose DNS rebinds to127.0.0.1was same-origin and could read the wallet inventory. cloudflare/wrangler-actionis pinned to a commit rather than the mutablev3tag, since it receives the deploy token. (The first attempt at this silently did nothing — the patch pattern did not match and no one checked. The claim was in this file before the change was in the repo.)
Fixed
- A malformed request URL crashed the whole service.
new URL()sat outside the handler'stry, soGET http://[ HTTP/1.1became an unhandled rejection and exited the process — 22 bytes to take down the data service, with nothing to restart it. - Wrapped list items were broken out of their list, and a header-less table separator crashed the build outright. Table rows without a trailing pipe lost their last cell, pipes inside code spans split cells, and markdown inside code spans was rendered rather than left literal.
- Cache staleness used second precision and no clock guard: a backwards clock (NTP, suspend/resume) reported stale entries as fresh for the length of the jump. Now millisecond-based, and a negative age is treated as stale.
/collectionsreturned 200 with per-slug error strings when the API key was missing, so a widget checkingres.okrendered "no collections" instead of prompting for a key. It now returns 401.HEADwas refused with 405, breakingcurl -Iand readiness probes. It is read-only and is now routed likeGET.- Config values were trusted verbatim:
"requestsPerSecond": "fast"producedNaNand silently disabled rate limiting entirely. Config is now validated with actionable errors. - The declared Node floor (
>=22.6.0) could not run the code, which needs unflaggednode:sqliteand TypeScript stripping. Corrected to>=24, and the PKGBUILD now declaresnodejs>=24. Cachecreated the shared data directory even when given an explicit path, so every test touched the developer's real~/.local/share/anchor.- The API key prompt echoed the secret to the terminal; shutdown could hang forever on a keep-alive connection, leaving the cache unclosed until SIGKILL.
- Cache staleness was off by one: a TTL of N kept an entry fresh for N+1 seconds, and a TTL of 0 never expired. Found by a test written against the intended semantics rather than the code.
[Unreleased]: https://github.com/ryanio/anchor/compare/main...HEAD