Skip to content

Security: kyle-vbc/resolver

Security

SECURITY.md

Security Policy

Posture

Resolver holds a private key and signs with it. That is the product, and it makes the threat model wider than a read-only tool's, so it is worth being precise about what actually bounds the damage.

Four properties do the work, and all four are enforced in code rather than by convention:

  1. The key is reachable from exactly one file. src/wallet/keypair.ts reads it once at construction and never again. It is never logged, never serialised, never returned by any accessor, and toJSON is overridden to emit [redacted]. The class exposes one signing method and it takes bytes — nothing in it knows what a transaction is.

  2. The model cannot express a transaction. Every state change is an Intent, a closed union in src/kernel/domain.ts. There is no method anywhere that accepts a pre-built transaction from a model, so "drain the wallet" has no representation in the type a completion can produce.

  3. Every intent passes a pure, deny-by-default gate. src/wallet/policy.ts takes no network, no ambient clock and no model. An intent that matches no rule is denied. Budgets are denominated in lamports over a window, not in call counts, and the renewal reserve is a floor nothing may spend into.

  4. Every action is journalled before it is submitted. A crash between signing and confirming leaves a submitted row, and the next pass reconciles it against the chain rather than repeating it.

A pull request that adds a path from a model output to a signer without passing through Intent and Policy will be closed on that basis alone, independent of quality. The same applies to anything that makes a burn autonomous.

Beyond custody, the realistic threat model is about credential exposure, resolution spoofing and misleading output.

Class Example Severity
Credential leak An API key reaching a log file, an error message, a cache entry, or a crash report High
Prompt injection Desk or DAS response text steering the operator loop into calling a tool with chosen arguments High
Tool-surface escape A tool handler reaching the filesystem, the shell, or an arbitrary host High
Key exposure The signing key reaching a log line, an error message, a crash report or a cache entry Critical
Policy bypass A code path that reaches a signer without forming an Intent and passing the gate Critical
Resolution spoofing An unverified sol-record steering a payment to an address the name's owner does not control High
Data poisoning A protocol API returning a fabricated APY that the ranker presents without discounting Medium
Denial of service A desk response that hangs the discovery fan-out or exhausts memory Medium
Output integrity A rendering or rounding bug that materially misstates a rate or a risk score Medium

Reporting

Open a private advisory on github.com/kyle-vbc/resolver, or contact @kyle-vbc. Include the affected version, a description, and a reproduction. Do not open a public issue for anything in the High rows above.

Expect an acknowledgement within 72 hours and a fix or a documented mitigation within 14 days. You will be credited in the release notes unless you ask not to be.

Why there is no signing path

A tool that reads the chain and a tool that moves funds have different failure modes. A bug in this one produces a wrong number. A bug in the other produces a wrong transfer. Keeping the two apart means the worst outcome of a compromised dependency, a hostile upstream API, or a prompt injection through a protocol's own response body is bad analysis — never a signature.

Concretely:

  • Keypair, Transaction, VersionedTransaction, signTransaction and sendTransaction appear nowhere in src/. Grep for them; the result is empty, and keeping it empty is the review bar.
  • The chain layer (src/venues/) issues JSON-RPC reads only: getSlot, getBalance, getTokenAccounts, getStakeAccounts, account data reads, and DAS asset lookups. That list is the whole ChainReader interface in src/kernel/domain.ts, and it is the only door to the chain.
  • The operator's tool registry in src/ops/tools.ts is a fixed list of read and analysis tools. There is no shell tool, no filesystem write tool, and no generic fetch tool.

Every command's output is a description of what you could do. Executing it is your job, in your wallet, on a venue you chose.

Key handling

Keys are read from the environment only: HELIUS_API_KEY (or RESOLVER_HELIUS_KEY) and ANTHROPIC_API_KEY. Nothing that can be committed holds a secret — config.json and .resolver.json are for wallet, mandate, constraints and endpoints.

config set refuses credentials. src/cli/commands/config.ts runs a guard before any write. It rejects on two independent grounds:

  • The key name matches api-key, secret, token, password, private, seed or mnemonic, or the value has a recognisable provider shape (sk-…, hf_…, ghp_…, xox[baprs]-…, or a bare UUID).
  • The value is a URL that embeds a credential. This matters because rpcUrl is a legitimate setting and every RPC provider ships its key inside the URL, so a name-only check would let the key through the front door. urlCredential() rejects userinfo (https://user:pass@host), a credential-bearing query parameter (api-key, key, token, access_token, auth), and an opaque path segment of 24 or more URL-safe characters — the shape QuickNode and Alchemy use.

A bare endpoint and a base58 wallet address remain settable. The intended way to use your own node with a keyed URL is the environment:

export RESOLVER_RPC_URL='https://your-host/?api-key=…'

Writes are validated before they land: the candidate config is written to a temporary home, re-parsed through loadConfig, and only then renamed into place, so an invalid file cannot be left behind.

Redaction

src/common/journal.ts is the only logger. Every log record's fields pass through redact() before serialisation:

  • Any key matching ^(x[-_])?(api[-_]?key|apikey|authorization|bearer|token|access[-_]?token|secret|private[-_]?key|seed|mnemonic|passphrase|password)$ is replaced with [redacted]. The optional x- prefix is deliberate: x-api-key is the header the Anthropic client actually sends, and an anchored ^api-key$ would miss the single name most likely to be logged by mistake.
  • Every string value is scrubbed for ?api-key=…, &key=…, &token=…, &access_token=… and &auth=…, and for scheme://user:pass@ authority credentials.
  • The walk is recursive over nested objects and arrays, with cycle detection and a depth cap. A flat pass looked correct while leaving { req: { url } } and { urls: [...] } untouched, which is exactly the shape a debug log takes.

Redaction covers logs, not your shell. RESOLVER_RPC_URL printed by env is your own business.

Other hardening in place

  • Closed tool set. The operator's tools are a fixed registry. Inputs are schema-validated before a handler runs, and handler failures are wrapped as ResolverError rather than surfaced raw to the model.
  • Untrusted upstream data. Desk and DAS responses are parsed with zod schemas via fetchJson(), not trusted structurally. A shape mismatch is a desk/unavailable fault, not a silently coerced value. Token names and symbols from DAS are treated as display strings: trimmed on ingest, truncated at render, and never interpolated into the operator's system prompt, which is a static directive plus a fixed session header.
  • Errors are redacted twice. src/venues/transport.ts scrubs URLs, response bodies and cause text before an ResolverError is constructed — including a literal replacement of the configured Helius key — so a stack trace printed by a wrapper script cannot carry a credential out of the process.
  • Bounded fan-out. Discovery runs settleAll at maxConcurrentDesks with a 12-second per-desk timeout, under a shared rate limiter. A hanging or hostile endpoint costs that one desk's opportunities and nothing else; the desk reports itself down in resolver desks and the run continues.
  • No dynamic code. No eval, no new Function, no dynamic import() of a path derived from input, no plugin loader.
  • Single failure path. Errors exit through one function with one shape and a stable exit code per class, so a supervising script can distinguish "your key is wrong" (3, do not retry) from "the upstream is down" (4, retry later).

What is not claimed

  • Resolver does not verify that a protocol's reported APY is true. confidence records what was measured versus what was reported, and the ranker discounts accordingly, but a protocol that lies to its own public API produces a wrong ranking.
  • Resolver does not audit the protocols its desks describe. ExposureProfile.auditedBy reports what the protocol claims. An audit is not a guarantee.
  • The simulator is a model. Its distribution is a shape, not a forecast.
  • Nothing here is financial advice. Every position a desk describes can lose money.

Supported versions

The current minor line receives fixes. Older lines do not.

There aren't any published security advisories