# Plan

Work tracked in this repository. Update it in the same change as the work it tracks.

## Next

1. **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.toml` to 1.100.x, and the `Dockerfile`'s `RUST_IMAGE` tag and digest to match (`scripts/ci/docker-toolchain.sh` fails until they agree);
   - add `.cargo/config.toml` with `[registry]` `global-min-publish-age = "7 days"`;
   - remove `scripts/ci/crate-age.sh` and its line in `scripts/ci/check.sh`, and drop "dependency age" from the `check` recipe comment in the justfile (Cargo enforces it at resolve time instead);
   - confirm `cargo update` then respects the age and `just check` and CI still pass.

## Backlog

- **Instruction table: enforce mode (high):** report mode is built (`instruction_table = { path, sha256, mode = "report" }`, findings `instructions`, `accounts` and `profile` in 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](https://ttdonovan.github.io/sa-forge-signer/dev/hardening.html), 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-redeploy` audit entry and, with `on_program_redeploy = "hold"`, refuse `auto` signs per instruction family until `programs accept` or a passing `canary` run over `canary_dir` (payloads with expected outcomes, where an unexpected success fails and an RPC error releases nothing) ([Hardening proposals](https://ttdonovan.github.io/sa-forge-signer/dev/hardening.html), 8).
- **Reserved headroom for recovery (optional, after the instruction table):** `reserved_per_minute` and `reserved_lamports` kept for messages whose game instructions are all in `reserved_families`, classified from the table, never from `summary` ([Hardening proposals](https://ttdonovan.github.io/sa-forge-signer/dev/hardening.html), 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 verify` and the console's start-up and minutely check still read it all, and its size is unbounded. Revisit when a real `audit.jsonl` passes 100 MB (roughly 100,000 signs) or `audit verify` takes more than 10 s. Decide between rotation (daily segments, the hash chain carried across files, `audit verify` and the console reading all segments) and a store with an index, keeping the chain's guarantees and the repair procedure in `SECURITY.md`.
- **RPC latency (optional, when a trace shows the need):** a sign through `serve` takes about 1.7 s against `rpc1.z.ink`, almost all RPC round trips. Candidates, each with the trace symptom that would justify it: a keep-alive call while `serve` is 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](https://ttdonovan.github.io/sa-forge-signer/dev/rpc-latency.html)).
- **Approval outside the terminal:** `confirm` reads `/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 which `serve` in 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), so `confirm` keys also work under `serve`.
- **`serve` thread cap and RPC budgets:** one thread per request is unbounded today; cap concurrent requests and answer the rest with 503. `check` has no rate limit either: add `[serve] check_per_minute` per token (429) and keep polling of already-sent transactions ahead of `check` and preflight RPC calls; the genesis hash is already cached per process ([Hardening proposals](https://ttdonovan.github.io/sa-forge-signer/dev/hardening.html), 10).
- **MCP: drive `/mcp` with 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](https://ttdonovan.github.io/sa-forge-signer/dev/mcp-conformance.html)).
- **MCP: progress during a long `sign` (optional):** the `2026-07-28` revision 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](https://ttdonovan.github.io/sa-forge-signer/dev/design.html)).
- **SPL Token authority check:** refuse top-level SPL Token `Transfer` / `Approve` (and their `Checked` forms) 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 `serve` start) 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](https://ttdonovan.github.io/sa-forge-signer/dev/hardening.html), 11).
- **Read-after-write (optional):** document the report's `slot` as the minimum context slot for the caller's next reads, and pass the key's last landed slot as `minContextSlot` on the signer's own pre-sign reads ([Hardening proposals](https://ttdonovan.github.io/sa-forge-signer/dev/hardening.html), 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 with `systemd-analyze security` ([Hardening proposals](https://ttdonovan.github.io/sa-forge-signer/dev/hardening.html), 13).
- **Secrets in memory:** `Keypair` already zeroes itself on drop (`solana-ed25519`'s default `zeroize` feature), and key files, payload text and partial signers are read into `Zeroizing` buffers. What is left: buffers that grow while being filled (`serde_json` parsing a key file into a `Vec<u8>`, `read_to_string` from 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 a `serde_json::Value` that holds `partial_signers` and is dropped without wiping; and moves of a `Keypair` leave copies on the stack. Ahead of those, guard against copies outside the process: turn off core dumps (`RLIMIT_CORE` 0, and `PR_SET_DUMPABLE` 0 on Linux) and consider `mlock` for 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 on `BlockhashNotFound`.
- **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.
