diff --git a/.github/workflows/harness-ci.yml b/.github/workflows/harness-ci.yml index ab7670a2..2d2cbc8e 100644 --- a/.github/workflows/harness-ci.yml +++ b/.github/workflows/harness-ci.yml @@ -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: @@ -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 diff --git a/AGENTS.md b/AGENTS.md index f5f1b3ab..e8ee1556 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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:` 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:` 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`, 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: ` ([`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) diff --git a/Cargo.lock b/Cargo.lock index bfd3dbe9..96c12f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,7 @@ name = "agentkeys-backend-client" version = "0.1.0" dependencies = [ "agentkeys-core", + "agentkeys-protocol", "agentkeys-provisioner", "async-trait", "reqwest", @@ -124,6 +125,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "thiserror 2.0.18", + "ts-rs", ] [[package]] @@ -238,6 +240,7 @@ dependencies = [ "tower-service", "tracing", "tracing-subscriber", + "ts-rs", "url", "webauthn-rs", ] @@ -340,6 +343,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "agentkeys-protocol" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "ts-rs", +] + [[package]] name = "agentkeys-provisioner" version = "0.1.0" @@ -373,6 +385,7 @@ dependencies = [ name = "agentkeys-web-core" version = "0.1.0" dependencies = [ + "agentkeys-protocol", "axum", "reqwest", "serde", @@ -4596,6 +4609,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termtree" version = "0.5.1" @@ -4981,6 +5003,29 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ts-rs" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" +dependencies = [ + "lazy_static", + "thiserror 2.0.18", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "termcolor", +] + [[package]] name = "tungstenite" version = "0.23.0" @@ -5378,6 +5423,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 4da756b7..50d2ed53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", @@ -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"] } diff --git a/apps/parent-control/lib/client/core.ts b/apps/parent-control/lib/client/core.ts index 830890e7..cb398909 100644 --- a/apps/parent-control/lib/client/core.ts +++ b/apps/parent-control/lib/client/core.ts @@ -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>(); function loadCore(brokerUrl: string): Promise { 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)); } diff --git a/apps/parent-control/lib/client/daemon.ts b/apps/parent-control/lib/client/daemon.ts index 63b5cb7c..9a15618d 100644 --- a/apps/parent-control/lib/client/daemon.ts +++ b/apps/parent-control/lib/client/daemon.ts @@ -41,6 +41,23 @@ import type { StatusKind, Worker, } from '@/app/_components/types'; +// Wire types GENERATED from the Rust structs via ts-rs (#203 B2 / #215 re-land): +// the daemon's ui_bridge Api* structs, the catalog Sensitivity union, and the +// broker UserOp build/submit response shapes (agentkeys-protocol). Do not +// hand-edit @/lib/generated or re-declare these here — a Rust-side field rename +// regenerates the .ts and the mappers below stop compiling (rung-3 drift gate; +// CI also git-diffs the generated dir after `cargo test` regenerates it). +import type { ApiActor } from '@/lib/generated/ApiActor'; +import type { ApiAnchorStatus } from '@/lib/generated/ApiAnchorStatus'; +import type { ApiAuditEvent } from '@/lib/generated/ApiAuditEvent'; +import type { ApiMemoryEntry } from '@/lib/generated/ApiMemoryEntry'; +import type { ApiWorker } from '@/lib/generated/ApiWorker'; +import type { BuildAcceptUserOpResponse } from '@/lib/generated/BuildAcceptUserOpResponse'; +import type { MasterMemoryPlantResponse } from '@/lib/generated/MasterMemoryPlantResponse'; +import type { MemoryCategory as ApiMemoryCategory } from '@/lib/generated/MemoryCategory'; +import type { ProposedScope as ApiProposedScope } from '@/lib/generated/ProposedScope'; +import type { SubmitAcceptUserOpResponse as ApiSubmitResult } from '@/lib/generated/SubmitAcceptUserOpResponse'; +import { loadWasmModule } from './wasm-module'; /** * DaemonBackend — talks to a running agentkeys-daemon over HTTP. @@ -90,6 +107,26 @@ export class DaemonBackend implements AgentKeysClient { } } + // Like postJson, but the body is ALREADY serialized JSON (the wasm plant + // builder returns serde_json's exact bytes — re-stringifying would launder + // them through JS). #275 tier-3. + private async postJsonBody(path: string, jsonBody: string): Promise> { + try { + const resp = await fetch(`${this.baseUrl}${path}`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: jsonBody, + }); + if (!resp.ok) { + const text = await resp.text(); + return { ok: false, status: unreachable(`POST ${path} → ${resp.status}: ${text}`) }; + } + return { ok: true, data: (await resp.json()) as T }; + } catch (e) { + return { ok: false, status: unreachable(`POST ${path}: ${(e as Error).message}`) }; + } + } + private async postJson(path: string, body: unknown): Promise> { try { const resp = await fetch(`${this.baseUrl}${path}`, { @@ -198,11 +235,7 @@ export class DaemonBackend implements AgentKeysClient { } async getAnchorStatus(): Promise> { - const r = await this.getJson<{ - last_anchor_at: number; - next_anchor_in: number; - recent: { ts: string; root: string; count: number; txn: string; conf: number }[]; - }>('/v1/anchor/status'); + const r = await this.getJson('/v1/anchor/status'); if (!r.ok) return r; return { ok: true, @@ -256,9 +289,7 @@ export class DaemonBackend implements AgentKeysClient { // the per-agent unpair; every paired agent = the pre-reset fleet teardown // (ONE Touch ID). The broker skips already-revoked hashes; all-skipped // returns 409 "nothing to revoke". - async revokeBuild(input: { deviceKeyHashes: string[] }): Promise< - Result<{ user_op: Record; user_op_hash: string; entry_point: string; chain_id: number }> - > { + async revokeBuild(input: { deviceKeyHashes: string[] }): Promise> { return this.postJson('/v1/revoke/build', { device_key_hashes: input.deviceKeyHashes }); } @@ -425,18 +456,26 @@ export class DaemonBackend implements AgentKeysClient { } async plantMemory(entries: MasterMemoryEntry[]): Promise> { - const r = await this.postJson<{ planted: number; skipped: number; total: number; taxonomy_status?: string }>( - '/v1/master/memory/plant', - { - // @web-fixture: master_memory_plant — entry shape gated by scripts/check-web-api-drift.sh - // (must match the daemon's ApiMemoryEntry + web-parity-demo.sh; issue #203 / the #206 parity ladder). - entries: entries.map((m) => ({ - ns: m.ns, key: m.key, title: m.title, bytes: m.bytes, - version: m.version, updated: m.updated, preview: m.preview, body: m.body, - content_hash: m.contentHash ?? '', - })), - }, - ); + // #275 tier-3: the plant route + body come from the daemon's OWN wire types + // (agentkeys-protocol::web_api) compiled to wasm — one code path, so this + // client cannot hand-build a drifted body (the old @web-fixture diff gate + // for this file is retired; the ts-rs ApiMemoryEntry type checks the entry + // shape at compile time and the wasm builder validates + serializes it). + let route: string; + let body: string; + try { + const wasm = await loadWasmModule(); + const wireEntries = entries.map((m): ApiMemoryEntry => ({ + ns: m.ns, key: m.key, title: m.title, bytes: m.bytes, + version: m.version, updated: m.updated, preview: m.preview, body: m.body, + content_hash: m.contentHash ?? '', + })); + route = wasm.masterMemoryPlantRoute(); + body = wasm.buildMasterMemoryPlantBody(wireEntries); + } catch (e) { + return { ok: false, status: unreachable(`wasm plant builder failed: ${String(e)}`) }; + } + const r = await this.postJsonBody(route, body); if (!r.ok) return r; return { ok: true, @@ -444,7 +483,7 @@ export class DaemonBackend implements AgentKeysClient { planted: r.data.planted, skipped: r.data.skipped, total: r.data.total, - taxonomyStatus: r.data.taxonomy_status ?? 'ok', + taxonomyStatus: r.data.taxonomy_status, }, }; } @@ -587,9 +626,7 @@ export class DaemonBackend implements AgentKeysClient { maxPerPeriod: string; maxTotal: string; periodSeconds: number; - }): Promise< - Result<{ user_op: Record; user_op_hash: string; entry_point: string; chain_id: number }> - > { + }): Promise> { return this.postJson('/v1/accept/build', { request_id: input.requestId, services: input.services, @@ -614,9 +651,7 @@ export class DaemonBackend implements AgentKeysClient { services: string[]; preserveServiceIds?: string[]; readOnly: boolean; - }): Promise< - Result<{ user_op: Record; user_op_hash: string; entry_point: string; chain_id: number }> - > { + }): Promise> { return this.postJson('/v1/scope/build', { actor_omni: input.actorOmni, services: input.services, @@ -655,71 +690,10 @@ export class DaemonBackend implements AgentKeysClient { } } -interface ApiProposedScope { - data_class: string; - entity: string; - service: string; - category: string; - sensitivity: 'safe' | 'sensitive'; - gating: 'auto' | 'k11'; - confidence: number; -} - -// ─── API wire types (snake_case, mirror ui_bridge.rs ApiActor etc.) ──── - -interface ApiActor { - id: string; - omni: string; - omni_hex: string; - label: string; - role: string; - parent: string | null; - derivation: string; - device: string; - device_pubkey: string; - last_active: string; - status: string; - vendor: string; - k11: boolean; - scope?: Record; - // #248: on-chain scope service ids (keccak hex) that aren't a known memory: - // (e.g. cred:); echoed back on commit so set-replace can't wipe them. - scope_unknown_service_ids?: string[]; - // On-chain SidecarRegistry device key hash — the Touch-ID unpair's target. - device_key_hash?: string; - payment_cap?: { per_tx: number; daily: number; currency: string }; - time_window?: { start: string; end: string; tz: string }; - services?: string[]; - // #225 E7: on-chain account (master → P256Account address; agent → device omni). - account_address?: string | null; - account_type?: string; // "p256account" | "device" | "none" -} - -interface ApiAuditEvent { - id: string; - ts: string; - actor_id: string; - actor: string; - kind: string; - detail: string; - chip: string; - sev: string; - tx_hash?: string; - audit_envelope_hashes?: string[]; -} - -interface ApiWorker { - id: string; - title: string; - host: string; - desc: string; - calls_today: number; - calls_hour: number; - p50: number; - p95: number; - cap: string; - by_actor: { actor: string; count: number; share: number }[]; -} +// ─── Wire types are imported from @/lib/generated (ts-rs, generated from the +// Rust structs — #203 B2). The mappers below convert the snake_case wire +// types to the camelCase UI domain types; a Rust-side field rename +// regenerates the .ts and breaks these mappers (the drift gate). ───────── function apiToActor(a: ApiActor): Actor { return { @@ -749,19 +723,9 @@ function apiToActor(a: ApiActor): Actor { }; } -/** #97: broker submit response shape (relayed verbatim by the daemon proxies). */ -interface ApiSubmitResult { - ok?: boolean; - tx_hash?: string; - block_number?: string; - user_op_hash?: string; - pending?: boolean; - audit_envelope_hashes?: string[]; -} - function apiToSubmitResult(r: ApiSubmitResult): SubmitResult { return { - ok: r.ok ?? true, + ok: r.ok, txHash: r.tx_hash || undefined, blockNumber: r.block_number || undefined, userOpHash: r.user_op_hash || undefined, @@ -825,23 +789,6 @@ function normalizeChip(c: string): ChipKind { return (allowed as string[]).includes(c) ? (c as ChipKind) : 'default'; } -interface ApiMemoryEntry { - ns: string; - key: string; - title: string; - bytes: number; - version: string; - updated: string; - preview: string; - body: string; - content_hash?: string; -} - -interface ApiMemoryCategory { - ns: string; - label: string; -} - function apiToMemoryEntry(m: ApiMemoryEntry): MasterMemoryEntry { return { ns: m.ns, key: m.key, title: m.title, bytes: m.bytes, diff --git a/apps/parent-control/lib/client/wasm-module.ts b/apps/parent-control/lib/client/wasm-module.ts new file mode 100644 index 00000000..c2bc5db3 --- /dev/null +++ b/apps/parent-control/lib/client/wasm-module.ts @@ -0,0 +1,26 @@ +'use client'; + +// Lazy, client-only, memoized load of the agentkeys-web-core wasm MODULE — +// shared by CoreBackend (which wraps it in a per-broker-URL WebCore) and +// DaemonBackend's plant path (#275 tier-3: module-level route/body builders). +// 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). On failure the memo is evicted so the next call retries (a +// transient load failure must not poison the page). +type WasmModule = typeof import('@/lib/wasm/agentkeys-web-core/agentkeys_web_core.js'); + +let modulePromise: Promise | null = null; + +export function loadWasmModule(): Promise { + if (!modulePromise) { + modulePromise = (async () => { + const wasm = await import('@/lib/wasm/agentkeys-web-core/agentkeys_web_core.js'); + await wasm.default('/wasm/agentkeys_web_core_bg.wasm'); + return wasm; + })(); + void modulePromise.catch(() => { + modulePromise = null; + }); + } + return modulePromise; +} diff --git a/apps/parent-control/lib/generated/ApiActor.ts b/apps/parent-control/lib/generated/ApiActor.ts new file mode 100644 index 00000000..c76631b3 --- /dev/null +++ b/apps/parent-control/lib/generated/ApiActor.ts @@ -0,0 +1,32 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ApiPaymentCap } from "./ApiPaymentCap"; +import type { ApiScopeBits } from "./ApiScopeBits"; +import type { ApiTimeWindow } from "./ApiTimeWindow"; + +export type ApiActor = { id: string, omni: string, omni_hex: string, label: string, role: string, parent: string | null, derivation: string, device: string, device_pubkey: string, last_active: string, status: string, vendor: string, k11: boolean, +/** + * #233/#243: the on-chain `SidecarRegistry` device key hash (`0x` + 64 hex) + * when known — set for chain-reconstructed actors and fresh pairings. Lets + * the master-reset fleet teardown revoke by hash even when the per-label + * `~/.agentkeys/agents/