diff --git a/crates/panday-cli/src/lib.rs b/crates/panday-cli/src/lib.rs index 7bc44a2..3333159 100644 --- a/crates/panday-cli/src/lib.rs +++ b/crates/panday-cli/src/lib.rs @@ -175,9 +175,11 @@ pub enum Command { prompt: String, }, /// docs/21 §The replay tool. Specified as `panday replay `, - /// which needs a store to look the session up in; Postgres is M3.5 and - /// SQLite M18.1, so v1 takes the log file directly (`JsonlStore`). The - /// spec is amended to match rather than the divergence buried here. + /// which needs a store to look the session up in. There was none when this + /// landed; SQLite (M18.1) and the PG `session_events` sink (M18.6) have + /// since shipped, so the session-id form is unblocked and merely unwritten. + /// v1 takes the log file directly (`JsonlStore`). The spec is amended to + /// match rather than the divergence buried here. Replay { log: String, at_seq: Option, diff --git a/crates/panday-gateway/src/cache.rs b/crates/panday-gateway/src/cache.rs index f7bbe46..2510c91 100644 --- a/crates/panday-gateway/src/cache.rs +++ b/crates/panday-gateway/src/cache.rs @@ -11,8 +11,9 @@ //! This one exists because an eval suite replays identical requests hundreds of //! times, and paying for that twice is silly. //! -//! It is **not Postgres**. The spec names a PG unlogged table, and PG is M3.5 — -//! so this is the trait plus an in-memory implementation, and the binaries wire +//! It is **not Postgres**. The spec names a PG unlogged table and no milestone +//! owned building one until M11.10 — so this is the trait plus an in-memory +//! implementation, and the binaries wire //! whichever they have (docs/01: libraries take traits). The eligibility rules, //! the key normalization and the tenant scoping are the parts with the bugs in //! them, and they live here either way. diff --git a/crates/panday-harness/src/jsonl.rs b/crates/panday-harness/src/jsonl.rs index 75fd417..b399b5d 100644 --- a/crates/panday-harness/src/jsonl.rs +++ b/crates/panday-harness/src/jsonl.rs @@ -1,8 +1,10 @@ //! An append-only file event store — the on-disk form of the log (M21.3). //! //! `panday replay` is specified as taking a `` and looking the log -//! up in the store (docs/21 §The replay tool). Postgres is M3.5 and SQLite is -//! M18.1, so in Phase 2 there is nowhere to look a session up *from*. Rather +//! up in the store (docs/21 §The replay tool). When this landed there was +//! nowhere to look a session up *from*; SQLite (M18.1) and the PG +//! `session_events` sink (M18.6) have since shipped, so that form is unblocked +//! and merely unwritten. Rather //! than ship a replay tool with no reachable input, this is the smallest store //! that makes the fold observable: one JSON envelope per line, in `seq` order, //! which is already the shape of the golden fixtures. diff --git a/crates/panday-platform/src/tenancy.rs b/crates/panday-platform/src/tenancy.rs index 6f065ca..a3b2335 100644 --- a/crates/panday-platform/src/tenancy.rs +++ b/crates/panday-platform/src/tenancy.rs @@ -6,8 +6,9 @@ //! //! ## Why this exists before the first query does //! -//! Postgres is M3.5, so there is no SQL to lint yet — which is exactly when a lint is -//! worth writing. The first unscoped query is the one that gets written while someone +//! This was armed before there was any SQL to lint — which is exactly when a lint is +//! worth writing. There are now 49 `sqlx::query` sites and 8 migrations here, so it +//! guards something real. The first unscoped query is the one that gets written while someone //! is debugging something else, and by the time there are fifty queries a lint becomes //! a migration project instead of a guardrail. Armed now, it costs one test run and //! the first violation fails a build. diff --git a/docs/11-gateway.md b/docs/11-gateway.md index c7a4ae3..9a61f7a 100644 --- a/docs/11-gateway.md +++ b/docs/11-gateway.md @@ -361,7 +361,9 @@ in-process, not a product surface. The gateway is stateless apart from cache that must be run deliberately. **The exact cache is not Postgres yet.** The spec names a PG unlogged table and - PG is M3.5, so what shipped is the `ExactCache` trait plus an in-memory + **no milestone owns building one** — this previously cited "M3.5", which is + ledger-rebuild-from-log and never owned the PG lane. Tracked now as M11.10. + What shipped is the `ExactCache` trait plus an in-memory implementation (bounded, TTL, expire-on-read), and the binaries wire whichever they have. The parts with the bugs in them — eligibility, key normalization, tenant scoping — are the same either way. @@ -528,3 +530,20 @@ in-process, not a product surface. The gateway is stateless apart from cache `ModelNotFound` is not retryable. `model_not_found` joins the `ErrorResponse` type enum in the OpenAPI document, and `/v1/chat/completions` now declares the 404 it could always have returned. + +- **M11.10** Postgres-backed exact cache, replacing the in-memory one where a + deployment has a database. + + Carved out because `docs/11` §Exact cache has always specified "hash(normalized + request) → response, **PG unlogged table**" while only `MemoryExactCache` + exists, and the deferral was attributed to "M3.5" — a milestone that is + ledger-rebuild-from-log and never owned the Postgres lane. Two closed + milestones were being cited for work nobody had started, so a reader chasing + the pointer concluded it had shipped. + + The seam is already right: `ExactCache` is a trait and the binaries wire + whichever implementation they have, so this is an additional impl plus a + migration, not a refactor. The parts with the bugs in them — eligibility, key + normalization, tenant scoping — are shared and already tested. Not started, and + not urgent: the in-memory cache is correct, it just does not survive a restart + or span replicas. diff --git a/docs/14-sandbox.md b/docs/14-sandbox.md index 8ba8f36..69d083c 100644 --- a/docs/14-sandbox.md +++ b/docs/14-sandbox.md @@ -302,3 +302,17 @@ type error. The sink is a trait with a discarding default, for the same reason `UsageSink` is: `panday local` has no account, and a loop that needed a billing backend to run a tool would make the offline tier impossible. + +- **M14.8** Egress proxy, so a non-empty `net` allowlist means something. + + Carved out of M14.2's note, which records that the proxy "is not built" and + that "a per-domain allowlist needs the proxy component and is deferred with + it" — true, and invisible, because the deferral carried no number. + + The gap it leaves is worth stating plainly: `docs/16` §plugin.toml lets a + plugin declare `net: [api.github.com]`, and today the sandbox cannot enforce + that. Under `--unshare-net` there is no network at all, so default-deny holds + and nothing is *less* safe than it claims — but a manifest field that reads + like an allowlist and is in fact an all-or-nothing switch is the kind of thing + someone will one day rely on. Until this ships, a non-empty `net` list means + "this plugin wants network", not "this plugin may reach exactly these hosts". diff --git a/docs/16-plugins.md b/docs/16-plugins.md index 6facc7d..1147e20 100644 --- a/docs/16-plugins.md +++ b/docs/16-plugins.md @@ -23,7 +23,7 @@ my-plugin/ `hooks: [pre_tool]`. Install-time consent; the sandbox tiers enforce (T1 WIT world for WASM, MCP servers run as T2 children with that policy). Distribution: `.plugin` archive, ed25519-signed; registry tiers -`verified | community | unlisted`, with the marketplace being a phase-4 +`verified | community | unlisted`, with the marketplace being a phase-6 storefront over the same registry API. ## Skills @@ -239,7 +239,7 @@ of one adapter — the best distribution-per-line-of-code in the plan. `panday acp` defaults to the `dev` profile rather than `unleashed`: an editor session has a human in it, and the point of the gate is that they see the question. The jail's environment is five toolchain variables, never the parent environment (docs/20 T4). -- **M16.6** Registry service (publish/fetch/verify) + `panday plugin install`; marketplace UI deferred to phase 4. ✅ *(shipped: `panday_plugins::archive`, `panday_platform::registry` (+ its `http` router), `panday plugin install`; suites in `crates/panday-plugins/tests/archive.rs`, `crates/panday-platform/tests/registry.rs`, `crates/panday-cli/tests/plugin_install.rs`.)* +- **M16.6** Registry service (publish/fetch/verify) + `panday plugin install`; marketplace UI deferred to phase 6. ✅ *(shipped: `panday_plugins::archive`, `panday_platform::registry` (+ its `http` router), `panday plugin install`; suites in `crates/panday-plugins/tests/archive.rs`, `crates/panday-platform/tests/registry.rs`, `crates/panday-cli/tests/plugin_install.rs`.)* **The order is the security property**: fetch → verify → consent → extract. diff --git a/docs/20-security.md b/docs/20-security.md index 796af32..1d8dbfa 100644 --- a/docs/20-security.md +++ b/docs/20-security.md @@ -225,8 +225,10 @@ run belong inside a T2 jail (docs/14), where the worst case is a lost temp direc false positive costs a redaction rather than a broken turn). - **M20.3** Tenant-scoping CI lint; cache key audit; trace scrubbing defaults. ✅ *(shipped in three places: `panday_platform::tenancy` + `crates/panday-platform/tests/tenant_scoping.rs` (the lint), `CacheKey` at M11.6 (the cache-key audit's finding, built in), `crates/panday-sdk/tests/scrub_audit.rs` at M21.5 (scrubbing defaults).)* - **The lint is armed before the first query exists.** Postgres is M3.5, so today the - scan finds no SQL — which is exactly when this is worth writing. The first unscoped + **The lint was armed before the first query existed.** When this was written the + scan found no SQL at all — which is exactly when it is worth writing. It now + walks 49 `sqlx::query` sites and 8 migrations in `panday-platform`, so the + guard is live rather than theoretical. The first unscoped query is the one written while someone is debugging something else, and by the time there are fifty queries a lint becomes a migration project instead of a guardrail. diff --git a/docs/21-observability.md b/docs/21-observability.md index 5732e6f..823ac19 100644 --- a/docs/21-observability.md +++ b/docs/21-observability.md @@ -70,8 +70,11 @@ week. This tool is why state-must-fold-from-log is an invariant and not a preference (ADR-002). **Amended at M21.3: ``, not ``.** A session id needs a store -to resolve it against, and there is none yet — Postgres is M3.5, SQLite is -M18.1. Rather than ship a replay tool whose only argument is unreachable, v1 +to resolve it against, and at M21.3 there was none. That is no longer true — +SQLite shipped at M18.1 and the Postgres `session_events` sink at M18.6 — so +replay-by-session-id is unblocked and simply unwritten. (It was previously +attributed here to "M3.5"; M3.5 is ledger-rebuild-from-log and never owned a +store.) Rather than ship a replay tool whose only argument is unreachable, v1 takes an append-only JSONL log (`panday_harness::JsonlStore`, one envelope per line in `seq` order — already the shape of the golden fixtures). The `` form is a lookup in front of the same renderer and lands with the store; nothing diff --git a/docs/23-roadmap.md b/docs/23-roadmap.md index 0fb3a2b..834a97a 100644 --- a/docs/23-roadmap.md +++ b/docs/23-roadmap.md @@ -8,18 +8,33 @@ one week of evenings. **The one rule: do not start phase N+1 to avoid finishing phase N.** -**Where numbered work stands (2026-08-20):** 89 milestones, 83 shipped, 3 partial -(M22.3 host, M22.4 KVM timing, M22.5 air-gapped install), 3 not started (M19.3 / -M19.5 / M19.7 training). Laptop-provable leftovers are closed: subscription -OAuth, live M13.2, json-bench 200/200 on `xai/grok-4.6`, that model's catalog -row `provenance: measured`, M20.1 canaries in agent-bench (41 tasks). What -remains is named in each phase's status, not guessed. - -**Operator track (docs/25), started 2026-08-21:** twelve milestones (M25.1–25.12) -to pool upstream API keys and Grok/Claude/Codex subscriptions behind the gateway. -Additive — not Phase 5/6, does not skip training or Stripe. M25.1 vault, M25.2 -`panday creds`, M25.3 transport headers, and live pools + `GET /accounts` -(M25.4, partial M25.9) are shipped. +**Where numbered work stands (2026-08-23):** **107 milestones, 97 shipped, 10 +open** — M0.2 (this phase-2 exit tracker), M11.10 (PG exact cache), M14.8 +(egress proxy), M19.3 / M19.5 / M19.7 (training, not started), M22.3 / M22.4 / +M22.5 (partial: host, KVM timing, air-gapped install), and M25.11 (hosted +Postgres ciphertext). + +The total rose from 104 on the same day. **No work was added** — three things +that had always existed were finally given numbers: M0.2 was being *cited* here +without ever being defined, and the PG cache and egress proxy were deferrals +recorded in prose that no number owned. An unnumbered deferral is invisible to +anyone reading the milestone list, which is the failure CLAUDE.md §4 describes. +Recount rather than trusting this line; it has been stale twice. +Laptop-provable leftovers are closed: subscription OAuth, live M13.2, json-bench +200/200 on `xai/grok-4.6`, that model's catalog row `provenance: measured`, +M20.1 canaries in agent-bench (41 tasks). What remains is named in each phase's +status, not guessed. + +**Operator track (docs/25), 2026-08-21 to 2026-08-23:** twelve milestones +(M25.1–25.12) to pool upstream API keys and Grok/Claude/Codex subscriptions +behind the gateway. Additive — not Phase 5/6, does not skip training or Stripe. +**Eleven of twelve are shipped**: the vault, `panday creds`, transport headers, +the pooled adapter, per-credential breakers and sticky sessions, operator +ceilings and remaining %, the provider-header overlay, the `most_remaining` +selector and its opt-in funnel, vault-as-boot-source, the Codex importer, and +the Keychain-wrapped KEK. **M25.11** (hosted Postgres ciphertext) is the only +one left, and it is not blocked — `sqlx`/`postgres` is already a workspace +dependency and the integration lane already runs Postgres. ## Phase 0 — Spine (~weeks 1–4) @@ -56,7 +71,13 @@ The product wedge. Harness state machine on fake client (M13.1), native tools + T2 sandbox Linux (M14.1–14.2), reducer generic + cargo/git/test compressors (M15.1–15.2), real-model loop (M13.2), permissions + Ask flow (M13.3), cache-aligned assembly + compaction (M13.4), crash-resume (M13.5), -event store PG + WS resume (M3.3), macOS T2 (M14.3). +event store + WS resume (M3.3), macOS T2 (M14.3). + +*(This list previously said "event store **PG**". It is not Postgres: the +implementors of `panday_harness::EventStore` are `MemoryStore`, `JsonlStore` and +`SqliteStore`, and `panday-harnessd` holds an `Arc`. The Postgres +`session_events` table is M18.6's sync **sink**, not an `EventStore`. M3.3's WS +resume is real and shipped; the PG backing is not, and no milestone owns it.)* **Exit:** the agent fixes a real failing test in one of *your* repos, unattended, under `dev` profile — and you reach for it by preference the @@ -96,9 +117,18 @@ observability spine (M21.1–21.2). **Exit:** a stranger installs the CLI, connects their editor via ACP, ports an existing SKILL.md unmodified, and completes a task offline on a laptop. -**Where that stands (M0.2).** Like Phase 0's exit, this criterion has work in it -that no component milestone owns — so it is written down here rather than left -invisible: +- **M0.2** **Phase 2's exit clauses, which no component milestone owns.** + *(Partial: three of four hold as far as CI can hold them. Two things remain + that only a human can do — run it in Zed, and run it against a real local + model — plus a package-manager recipe.)* + + Same reason M0.1 exists: the criterion has work in it that no numbered + milestone covers, so it is written down rather than left invisible. This + number was cited here for some time without ever being defined as a bullet, + which made Phase 2's exit tracker uncountable and unmarkable — the precise + failure CLAUDE.md §4 warns about, hiding behind a number that looked real. + +**Where that stands (M0.2):** | Clause | State | |---|---| diff --git a/docs/25-credentials.md b/docs/25-credentials.md index 7cb798a..5dcffa8 100644 --- a/docs/25-credentials.md +++ b/docs/25-credentials.md @@ -137,7 +137,11 @@ is below threshold, omit that provider from this request's chain. Fixture secrets are `sk-test-aaaa` / `sk-test-bbbb`. `MockTransport` scripts status + headers + SSE. No `~/.grok`, no Keychain, no real tokens in CI. -Property: Σ tokens per `credential_id` == Σ usage frames that named it. +Property: Σ tokens per `credential_id` == Σ usage frames that named it — +**pending, and not currently enforceable**: `UsageRecord` carries no +`credential_id` (see §How a call is served), so there is nothing for this to +range over. It is stated here as the contract the field must satisfy when it +lands, not as a guard that exists. A one-credential gateway must keep today's failover behaviour. ## Milestones diff --git a/docs/README.md b/docs/README.md index 8441701..72b2677 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,8 +4,8 @@ > Rust is what iron does on its own; a panday is what someone does with it > deliberately. Every crate is prefixed `panday-`. -This repository is the blueprint and the seed of a full AI infrastructure -platform: an agent harness, an LLM gateway with a model router, a tiered +This repository is the specification *and* the implementation of a full AI +infrastructure platform: an agent harness, an LLM gateway with a model router, a tiered sandbox, a plugin system (skills + MCP + ACP), a token-economy layer, a subscription platform with metered billing, an offline/local tier, and a path to training your own task models. @@ -15,7 +15,7 @@ path to training your own task models. | Path | What it is | |---|---| | `docs/` | The documentation set — 21 specs, ADRs, threat model, roadmap. The source of truth. Renders with `mdbook serve docs`. | -| `crates/` | A compiling cargo workspace seeded with the core types and traits the specs define. `cargo check` is green. | +| `crates/` | The implementation. `cargo nextest run --workspace` and `cargo clippy --workspace --all-targets -- -D warnings` are green on every commit; `docs/` is updated in the same commit whenever the code diverges from a spec. | ## How to use this repo diff --git a/handover.md b/handover.md index efde66e..42d63af 100644 --- a/handover.md +++ b/handover.md @@ -84,6 +84,26 @@ session's TCC grants, which made the repository unreadable for hours. retargeted — the work has to be rebased and re-filed under a new number. Delete base branches by hand once their children have landed. (This is how PR #13 was lost: it had to be rebased onto the squashed `main` and re-filed as #16.) +- **Check who `gh` is with `gh api user -q .login`, not `gh auth status`.** The + active account drifts back to `hexuria`, which has read but no write here, and + every `gh pr create` then fails with `must be a collaborator`. Parsing local + status output gave the wrong answer twice on 2026-08-23; asking GitHub who you + are does not. +- **`cfg`-gated code cannot be verified locally, so plan a CI round trip.** + `x86_64-unknown-linux-gnu` is installed, but cross-compiling still fails — + `ring` wants `x86_64-linux-gnu-gcc` and there is no C cross-toolchain here. + M25.12 was written entirely under `cfg(target_os = "macos")`, the local gate + only ever compiled the macOS half, and the Linux branch reached CI having never + been built. Five of six jobs passed; the one that failed was the only one that + had looked at it. +- **The integration lane is not in the local gate.** `panday-platform`'s smoke + suite runs only in CI's `integration` job, and a status-code change nearly went + red there right after a local "all green". Run it by hand when touching + anything an HTTP status assertion could observe: + `docker compose -f deploy/integration-compose.yml up -d --wait`, then + `PANDAY_TEST_DATABASE_URL=… cargo nextest run --run-ignored all -E 'package(panday-platform)'`. +- **Never `pkill -f "cargo test"`.** It is not scoped to this repo and will reach + another project's run on the same machine. Kill by recorded PID. - **If `git fetch` fails, start the ssh-agent — do not reach for HTTPS** (§4.2). A failed fetch leaves `origin/main` stale, and a stale `origin/main` will happily let you rebase onto the wrong base; that near-miss is what made this look like a remote problem rather than a missing agent. @@ -96,7 +116,9 @@ session's TCC grants, which made the repository unreadable for hours. **Remote:** `git@github.com:codeitlikemiley/panday.git` (public, user `codeitlikemiley`) **CI:** green on that commit. -**104 milestones total: 97 shipped ✅, 7 remaining.** Recount it rather than trusting this line — +**107 milestones total: 97 shipped ✅, 10 remaining.** The total rose from 104 without any work being added: M0.2, M11.10 and M14.8 +were always-real items that carried no number, so they were invisible to this +count. Recount it rather than trusting this line — `docs/25` added twelve milestones after the "89" figure was written, and CLAUDE.md §4 quotes the count too. @@ -236,8 +258,9 @@ a model this deployment cannot serve became **M11.9**: 503 and 404 respectively. The T1 hook 10ms flake on `c300fb1` (run `32330143170`) was a **timing flake, not a regression**: docs/16 gives a wasm hook a 10ms wall-clock budget, and asserting behaviour against that while a shared runner executes 900 other tests measures the runner. Fixed in `5fdf3f7` by splitting -behaviour (2s) from budget tests, with retries on the budget pair. Current HEAD (`32fbbf2`) is -green on `ci` / `release` / `deploy`. +behaviour (2s) from budget tests, with retries on the budget pair. `main` has been green on `ci` +since (see §2 for the current HEAD; this paragraph used to name `32fbbf2` and went stale within a +day, so it no longer names one). ### 4.2 SSH works. Start the agent first. @@ -364,17 +387,26 @@ just bench # json-bench against a running gateway (Grok CLI OAuth is enough) ## 6. Suggested next steps, in order -**Nothing is laptop-buildable any more.** All eight remaining milestones need -hardware, a third party, training data, a dependency decision, or a judgement -only the builder can make. That is a real state, not a stalling one — the last -thing that could be built here was built. Do not invent rates or p95s to make -the list look shorter. - -0. **The two skip-unless-asked credentials milestones**, if you want them: - **M25.11** hosted Postgres ciphertext (same envelope, not tenant BYOK) needs a - Postgres to put it in; **M25.12** Keychain-wrapped KEK needs the `keyring` - crate, so §1.2's ask-first dependency rule applies before a line is written. - Neither is on the critical path — file+env KEK already works. +**A previous version of this section said "nothing is laptop-buildable any +more". That was wrong**, and it was wrong because it repeated the blocked-list +framing below instead of checking the tree. Two surveys on 2026-08-23 found the +opposite: several "blocked" milestones have substantial laptop-buildable slices +that had never been separated out. Do not invent rates or p95s to make the list +look shorter — but do not assume a milestone is blocked because this file once +said so. + +0. **M25.11 — hosted Postgres ciphertext. Not blocked; simply unstarted.** + `sqlx` with `postgres` is already in `[workspace.dependencies]`, `just dev` + runs Postgres on 5442, `deploy/integration-compose.yml` on 5433, and twelve + test files already gate on `PANDAY_TEST_DATABASE_URL`. It is roughly + `crates/panday-sdk/src/vault/sqlite.rs` with `$1` placeholders and `bytea`, + plus a migration. Rows are **operator-global**, not account-scoped — + "not tenant BYOK", decided 2026-08-23. Start with a `CredentialStore` + conformance suite (there is none; `MemoryStore` and `SqliteStore` are tested + separately and may already have diverged), then the impl falls out of it. + + *(M25.12 shipped 2026-08-23 — `keyring` approved, macOS-only, opt-in behind + `PANDAY_VAULT_KEYCHAIN=1`.)* 1. **Phase 1 dogfood (the builder, not an agent).** Use the agent on a real repo under `dev` and decide whether you reach for it the next day. The fixture loop already passed live.