Plan
Work tracked in this repository. Update it in the same change as the work it tracks.
Next
- Cargo’s native minimum publish age (not before 2026-11-19). Rust 1.100 (due 2026-11-12) stabilises
registry.global-min-publish-age; on 1.98 the key only warns. Wait until the release is itself at least 7 days old (later if a point release follows), then:- bump
rust-toolchain.tomlto 1.100.x, and theDockerfile’sRUST_IMAGEtag and digest to match (scripts/ci/docker-toolchain.shfails until they agree); - add
.cargo/config.tomlwith[registry]global-min-publish-age = "7 days"; - remove
scripts/ci/crate-age.shand its line inscripts/ci/check.sh, and drop “dependency age” from thecheckrecipe comment in the justfile (Cargo enforces it at resolve time instead); - confirm
cargo updatethen respects the age andjust checkand CI still pass.
- bump
Backlog
- Instruction table: enforce mode (high): report mode is built (
instruction_table = { path, sha256, mode = "report" }, findingsinstructions,accountsandprofilein reports and audit entries). Remaining:mode = "enforce", which refuses on any finding (feat!:, since it refuses what was signed before), after report mode has run against real play long enough to show the table and forge-mcp’s payloads agree (Hardening proposals, 4). - Redeploy watch and holds (low; high before mainnet): read each game program’s last-deploy slot from its program-data account; on a change, write a
program-redeployaudit entry and, withon_program_redeploy = "hold", refuseautosigns per instruction family untilprograms acceptor a passingcanaryrun overcanary_dir(payloads with expected outcomes, where an unexpected success fails and an RPC error releases nothing) (Hardening proposals, 8). - Reserved headroom for recovery (optional, after the instruction table):
reserved_per_minuteandreserved_lamportskept for messages whose game instructions are all inreserved_families, classified from the table, never fromsummary(Hardening proposals, 9). - Audit store: the log is one JSONL file that only grows. Signs now read only its end and the console only what was appended (with a full read at most once a minute), but
audit verifyand the console’s start-up and minutely check still read it all, and its size is unbounded. Revisit when a realaudit.jsonlpasses 100 MB (roughly 100,000 signs) oraudit verifytakes more than 10 s. Decide between rotation (daily segments, the hash chain carried across files,audit verifyand the console reading all segments) and a store with an index, keeping the chain’s guarantees and the repair procedure inSECURITY.md. - RPC latency (optional, when a trace shows the need): a sign through
servetakes about 1.7 s againstrpc1.z.ink, almost all RPC round trips. Candidates, each with the trace symptom that would justify it: a keep-alive call whileserveis idle if the server drops connections sooner than 2 minutes; one retry of a read after a transport error on a reused connection; more idle connections per host for parallel signs; JSON-RPC batches for the five reads before sending, if the RPC accepts them; poll-schedule tuning under congestion (RPC calls and latency). - Approval outside the terminal:
confirmreads/dev/tty, which an agent running as the terminal’s user can type into through a multiplexer or a terminal app’s scripting API, and whichservein a container usually lacks. Move approval to a channel that checks who answers, such as a Unix socket that accepts only a configured approver uid (peer credentials), soconfirmkeys also work underserve. servethread cap and RPC budgets: one thread per request is unbounded today; cap concurrent requests and answer the rest with 503.checkhas no rate limit either: add[serve] check_per_minuteper token (429) and keep polling of already-sent transactions ahead ofcheckand preflight RPC calls; the genesis hash is already cached per process (Hardening proposals, 10).- MCP: drive
/mcpwith a real client in tests (optional): the handler’s messages are checked against the Rust MCP SDK’s types (rmcp, a dev-only dependency), but no client has been run against a live service. The SDK’s own client would do it at the cost of an HTTP stack in the test build (MCP conformance). - MCP: progress during a long
sign(optional): the2026-07-28revision lets a server answer a request with an event stream and send progress on it. The service answers with one JSON object, so a client sees nothing until the report. Needs streaming, which the hand-written handler does not do (Design decisions). - SPL Token authority check: refuse top-level SPL Token
Transfer/Approve(and theirCheckedforms) whose authority is the signing key, using the public SPL Token instruction layout. Today such instructions pass on program id alone, so a hand-written payload could move tokens from a token account the key owns. - Key stores: an encrypted-file backend (age, passphrase at
servestart) and macOS Keychain / Secret Service backends, for running outside a container. Check first whether a Keychain item can be isolated from other processes of the same user. For age: write files at a scrypt work factor of at least 2^18 and refuse to load one below it; use age rather than an own Argon2id envelope; take the passphrase from the terminal or systemd credentials, never an environment variable or argument (Hardening proposals, 11). - Read-after-write (optional): document the report’s
slotas the minimum context slot for the caller’s next reads, and pass the key’s last landed slot asminContextSloton the signer’s own pre-sign reads (Hardening proposals, 12). - systemd unit (low): a chapter with an example hardened unit (dedicated user,
ProtectSystem=strict, empty capabilities,SystemCallFilter=@system-service,LimitCORE=0,MemorySwapMax=0,TimeoutStopSec=230,LoadCredentialEncrypted=for the key passphrase) checked withsystemd-analyze security(Hardening proposals, 13). - Secrets in memory:
Keypairalready zeroes itself on drop (solana-ed25519’s defaultzeroizefeature), and key files, payload text and partial signers are read intoZeroizingbuffers. What is left: buffers that grow while being filled (serde_jsonparsing a key file into aVec<u8>,read_to_stringfrom stdin or an HTTP body) free copies that are never wiped, so reserve the full size up front;serve’s MCP route parses the request into aserde_json::Valuethat holdspartial_signersand is dropped without wiping; and moves of aKeypairleave copies on the stack. Ahead of those, guard against copies outside the process: turn off core dumps (RLIMIT_CORE0, andPR_SET_DUMPABLE0 on Linux) and considermlockfor key pages so they are not swapped out. Low priority: all of this assumes a memory disclosure in a process the agent’s OS user cannot read. - Local-validator integration tests: prove the outcomes that only fake-chain unit tests cover today:
failed(exit 12, landed with a program error), an expired blockhash with no status found (unknown, exit 14; exit 13 is retired), and the one-time re-sign onBlockhashNotFound. - Releases: reproducible builds (verify two builds match byte for byte), an SBOM attested next to the provenance, and an external review before any mainnet use. Checksums and build provenance attestations ship with each release since v0.4.0.