Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/panday-cli/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,9 +175,11 @@ pub enum Command {
prompt: String,
},
/// docs/21 §The replay tool. Specified as `panday replay <session_id>`,
/// 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<u64>,
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-gateway/src/cache.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions crates/panday-harness/src/jsonl.rs
Original file line numberDiff line numberDiff line change
@@ -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 `<session_id>` 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.
Expand Down
5 changes: 3 additions & 2 deletions crates/panday-platform/src/tenancy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
21 changes: 20 additions & 1 deletion docs/11-gateway.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand DownExpand Up@@ -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.
14 changes: 14 additions & 0 deletions docs/14-sandbox.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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".
4 changes: 2 additions & 2 deletions docs/16-plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/20-security.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
7 changes: 5 additions & 2 deletions docs/21-observability.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `<log>`, not `<session_id>`.** 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 `<session_id>`
form is a lookup in front of the same renderer and lands with the store; nothing
Expand Down
62 changes: 46 additions & 16 deletions docs/23-roadmap.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)

Expand DownExpand Up@@ -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<MemoryStore>`. 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
Expand DownExpand Up@@ -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 |
|---|---|
Expand Down
6 changes: 5 additions & 1 deletion docs/25-credentials.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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

Expand Down
Loading