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
42 changes: 42 additions & 0 deletions .github/workflows/harness-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ on:
- ".github/workflows/harness-ci.yml"
- "Cargo.toml"
- "Cargo.lock"
# B2 (#215 re-land): the frontend imports ts-rs-generated wire types and
# is typechecked in rust-checks — a frontend-only PR must run that gate
# too, or a drifted daemon.ts import goes green by skipping the job.
- "apps/parent-control/**"
workflow_dispatch:
inputs:
stage:
Expand Down Expand Up @@ -230,6 +234,44 @@ jobs:
- name: Frontend↔harness plant contract has not drifted (issue #203 / #206)
run: bash scripts/check-web-api-drift.sh

# B1 (#215 re-land / #275): the browser host (agentkeys-web-core — a
# cdylib+rlib that compiles to wasm32) shares the cap-mint wire shape with
# the native client through the pure-serde `agentkeys-protocol` crate.
# This gate fails if that shared crate (or web-core) ever pulls a
# native-only dep (tokio / native reqwest / aws-sdk-sts, the last via the
# provisioner) that breaks the browser build — exactly the regression that
# would occur if web-core depended on the native backend-client directly.
# Default + `wasm` bindings. The wasm32 target itself is pinned in
# rust-toolchain.toml (installed by the `rustup toolchain install` above).
- name: web-core compiles to wasm32 (no native transport leaked into the browser)
run: |
cargo check --target wasm32-unknown-unknown -p agentkeys-web-core
cargo check --target wasm32-unknown-unknown -p agentkeys-web-core --features wasm

# B2 (#215 re-land, #203 parity ladder rung 3): the frontend's Api* wire
# types are GENERATED from the Rust structs via ts-rs into
# apps/parent-control/lib/generated/. `cargo test --workspace` above
# re-ran the #[ts(export)] tests, rewriting those .ts in place; if a
# Rust-side struct changed without committing the regenerated bindings,
# the working tree now differs from HEAD → fail (same discipline as the
# backend-protocol fixtures).
- name: ts-rs frontend bindings are up to date (#215 re-land B2)
run: git diff --exit-code -- apps/parent-control/lib/generated/

# The enforcement half of B2: the React frontend actually COMPILES against
# the generated bindings (a daemon-side rename is a tsc error here, not a
# silent runtime break). Builds the web-core wasm pkg first — core.ts (and
# from #275, daemon.ts's plant path) import its .d.ts, so typecheck needs
# it; this also makes `npm run typecheck` fully green in CI for the first
# time (it used to fail on the missing wasm artifacts in fresh checkouts).
- name: Frontend typechecks against the generated bindings (B2 enforcement)
run: |
curl -sSf https://rustwasm.github.io/wasm-pack/installer/init.sh | sh
wasm-pack build crates/agentkeys-web-core --dev --target web \
--out-dir "$PWD/apps/parent-control/lib/wasm/agentkeys-web-core" -- --features wasm
npm ci --prefix apps/parent-control --no-audit --no-fund
npm run typecheck --prefix apps/parent-control

detect-changes:
# Issue #101: path-conditional triggers for auto-deploy of the test broker.
# Computes `broker_changed` so deploy-test-broker can skip when a PR only
Expand Down
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,13 @@ Cap-tokens are **data-class-explicit**: six storage endpoints (`/v1/cap/{cred,me

## Broker/worker request shapes have ONE owner (issue #203)

The broker/worker client protocol — the six `/v1/cap/*` mint endpoints, the STS relay, worker put/get body types, audit append, the `memory:<ns>` service builder, the `0x`-omni normalizer — is owned by [`agentkeys-backend-client`](crates/agentkeys-backend-client/) (field types co-owned with [`agentkeys-types`](crates/agentkeys-types/)). **Never re-type a cap/worker body in a second Rust path or in bash** (the #200 drift-bug class). All Rust callers (MCP `HttpBackend`, daemon `ui_bridge`) delegate to `BackendClient`, so a drifted shape is a compile error; bash and the web app are fixture-gated in CI.
The broker/worker client protocol — the six `/v1/cap/*` mint endpoints, the STS relay, worker put/get body types, audit append, the `memory:<ns>` service builder, the `0x`-omni normalizer — has ONE definition, split across two crates by transport-safety: the wire **types** live in [`agentkeys-protocol`](crates/agentkeys-protocol/) — pure serde, transport-free, compiles to `wasm32` — and the native **client** (cap-mint → STS → worker) in [`agentkeys-backend-client`](crates/agentkeys-backend-client/), which re-exports the types as `agentkeys_backend_client::protocol` (field types co-owned with [`agentkeys-types`](crates/agentkeys-types/)). The browser host [`agentkeys-web-core`](crates/agentkeys-web-core/) (wasm) depends on the SAME `agentkeys-protocol`, so the cap-mint body cannot drift across native vs browser — it used to (`ttl_seconds` required-`u64` vs `Option<u64>`, and web-core's copy was missing the #76 K10 PoP fields). web-core must NOT depend on `agentkeys-backend-client` directly: that crate pulls `aws-sdk-sts` + `tokio` + native `reqwest` (via the provisioner) and breaks the wasm build — the `wasm32` CI gate in `harness-ci.yml` enforces this. **Never re-type a cap/worker body in a second Rust path or in bash** (the #200 drift-bug class). All Rust callers (the MCP server's `BackendClient`, daemon `ui_bridge`, web-core) compile against the shared types, so a drifted shape is a compile error; bash and the web app are fixture-gated in CI.

**Rules when you touch this surface:**
- Wire-field change → edit the serde type in `agentkeys-backend-client::protocol`, regenerate the committed fixtures (`cargo run -p agentkeys-backend-client --bin dump-protocol-fixtures`), update the frozen key-set test in `fixtures.rs`.
- Wire-field change → edit the serde type in [`agentkeys-protocol`](crates/agentkeys-protocol/) (the single definition; re-exported as `agentkeys_backend_client::protocol`), regenerate the committed fixtures (`cargo run -p agentkeys-backend-client --bin dump-protocol-fixtures`), update the frozen key-set test in `fixtures.rs`. The native callers AND the browser host recompile against the new shape automatically; the `wasm32` gate proves the browser still builds.
- Harness steps drive the `agentkeys` CLI, not hand-rolled curls. Raw curls only for negative / HTTP-status tests; a body that mirrors a canonical shape carries `# @backend-fixture: <shape>` ([`scripts/check-backend-fixture-drift.sh`](scripts/check-backend-fixture-drift.sh) diffs it against [`harness/fixtures/backend-protocol/`](harness/fixtures/backend-protocol/) in CI); deliberately-malformed negative payloads are NOT annotated.
- The daemon's web-API plant contract is pinned the same way: route + `ApiMemoryEntry` body SoT in [`ui_bridge.rs`](crates/agentkeys-daemon/src/ui_bridge.rs), fixture [`harness/fixtures/web-api/master_memory_plant.json`](harness/fixtures/web-api/master_memory_plant.json), both non-Rust consumers annotated `@web-fixture: master_memory_plant` and gated by [`scripts/check-web-api-drift.sh`](scripts/check-web-api-drift.sh).
- **The frontend's wire types are GENERATED, never hand-mirrored (#215 re-land B2, rung 3).** `ts-rs` derives on the `ui_bridge.rs` `Api*` structs, the catalog `Sensitivity`, and the protocol UserOp build/submit responses emit [`apps/parent-control/lib/generated/*.ts`](apps/parent-control/lib/generated/); [`daemon.ts`](apps/parent-control/lib/client/daemon.ts) imports them instead of re-declaring interfaces, so a Rust-side rename is a frontend **compile error**. After changing one of those structs: `cargo test export_bindings` (any `cargo test` triggers it), commit the regenerated `.ts`. CI (`harness-ci.yml` rust-checks) `git diff --exit-code`s the generated dir AND runs `npm run typecheck` against a fresh wasm-pack build, so both the bindings and their consumers are gated. `u64` fields carry `#[ts(type = "number")]`; skip-serialize `Option`s carry `#[ts(optional)]`. Never edit `lib/generated/` by hand, and never add a hand-declared wire interface next to a generated one.
- The daemon's web-API plant contract lives one rung lower still (#275 tier-3): route + `ApiMemoryEntry` + plant request/response bodies are owned by [`agentkeys-protocol::web_api`](crates/agentkeys-protocol/) (re-exported by `ui_bridge.rs`); the React frontend consumes them via the `agentkeys-web-core` **wasm builder** (`masterMemoryPlantRoute()` + `buildMasterMemoryPlantBody()` — one code path, drift is a compile error), and the one remaining hand-built consumer (`harness/web-parity-demo.sh`) stays `@web-fixture`-annotated against [`harness/fixtures/web-api/master_memory_plant.json`](harness/fixtures/web-api/master_memory_plant.json), gated by [`scripts/check-web-api-drift.sh`](scripts/check-web-api-drift.sh) (the fixture itself is pinned to the shared types by a `ui_bridge` unit test).
- Field names are the arch.md canonical spellings — never invent a synonym in a new body.

## Harness rules → [`harness/AGENTS.md`](harness/AGENTS.md)
Expand Down
54 changes: 54 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"crates/agentkeys-mcp-server",
"crates/agentkeys-provisioner",
"crates/agentkeys-backend-client",
"crates/agentkeys-protocol",
"crates/agentkeys-broker-server",
"crates/agentkeys-worker-creds",
"crates/agentkeys-worker-memory",
Expand All @@ -31,6 +32,7 @@ agentkeys-memory-engine = { path = "crates/agentkeys-memory-engine" }
agentkeys-memory-openviking = { path = "crates/agentkeys-memory-openviking" }
agentkeys-web-core = { path = "crates/agentkeys-web-core" }
agentkeys-backend-client = { path = "crates/agentkeys-backend-client" }
agentkeys-protocol = { path = "crates/agentkeys-protocol" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
Expand Down
20 changes: 9 additions & 11 deletions apps/parent-control/lib/client/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@
import { EmptyBackend } from './empty';
import type { ConnectionStatus } from './types';

// Lazy, client-only load of the WASM master-plane core (agentkeys-web-core),
// memoized per broker URL. The dynamic import keeps the wasm glue out of the
// server bundle; init() fetches the .wasm from /wasm/ (served from public/,
// written by dev.sh's build_wasm). Keying by URL means a second CoreBackend with
// a different broker gets its own instance; on failure the entry is evicted so
// the next call retries (a transient load/broker failure must not poison it).
// Lazy, client-only WebCore instances, memoized per broker URL on top of the
// shared module loader (lib/client/wasm-module.ts — also used by the
// DaemonBackend plant path, #275). Keying by URL means a second CoreBackend
// with a different broker gets its own instance; on failure the entry is
// evicted so the next call retries (a transient load/broker failure must not
// poison it).
import { loadWasmModule } from './wasm-module';

type LoadedCore = import('@/lib/wasm/agentkeys-web-core/agentkeys_web_core').WebCore;
const coreByUrl = new Map<string, Promise<LoadedCore>>();
function loadCore(brokerUrl: string): Promise<LoadedCore> {
let p = coreByUrl.get(brokerUrl);
if (!p) {
p = (async () => {
const wasm = await import('@/lib/wasm/agentkeys-web-core/agentkeys_web_core.js');
await wasm.default('/wasm/agentkeys_web_core_bg.wasm');
return new wasm.WebCore(brokerUrl);
})();
p = loadWasmModule().then((wasm) => new wasm.WebCore(brokerUrl));
coreByUrl.set(brokerUrl, p);
void p.catch(() => coreByUrl.delete(brokerUrl));
}
Expand Down
Loading
Loading