diff --git a/apps/parent-control/app/_components/memory.tsx b/apps/parent-control/app/_components/memory.tsx index 8d84408..67d46f3 100644 --- a/apps/parent-control/app/_components/memory.tsx +++ b/apps/parent-control/app/_components/memory.tsx @@ -44,7 +44,7 @@ export function MemoryPage({ / memory} - desc="Your portable memory namespace — the spine agents read from and write to. It follows you across every vendor device. Stored encrypted; agents see only what their scope grants." + desc="Your portable memory namespace — the spine agents read from and write to. It follows you across every vendor device. Stored encrypted; agents see only the namespaces their scope grants (memory:), and the configured engine ranks what's injected per query — never widening past the gate." /> {!hasMemory && !planting && ( @@ -92,7 +92,7 @@ export function MemoryPage({ ✓ planted Prepared memory is live. The plant action is hidden — re-planting is a server-side no-op - (content-hash match). Agents read this per their granted scope. + (content-hash match). Agents read this per their granted memory:<ns> scope, query-ranked by the configured engine. diff --git a/apps/parent-control/lib/client/core.ts b/apps/parent-control/lib/client/core.ts index 808ba1a..830890e 100644 --- a/apps/parent-control/lib/client/core.ts +++ b/apps/parent-control/lib/client/core.ts @@ -74,6 +74,12 @@ export class CoreBackend extends EmptyBackend { // ── Broker calls in the browser (X1). Beyond the AgentKeysClient interface; // the onboarding/pairing slices call these directly via the CoreBackend. + // `req.service` for memory MUST be namespace-qualified — `memory:` + // (use `memoryService(ns)` from lib/constants); a bare `memory` fails + // cap-mint with `service_not_in_scope` (arch.md §896). The agent's *read* + // is query-aware: the worker stores per-namespace (`memory:.enc`) and + // the configured engine (OpenViking / deterministic) ranks the + // gate-bounded lines per turn, never widening past scope (#177). async capMemoryPut(bearer: string, req: unknown): Promise { return (await loadCore(this.brokerUrl)).capMemoryPut(bearer, req); } diff --git a/apps/parent-control/lib/client/types.ts b/apps/parent-control/lib/client/types.ts index 7fbdde3..a538fc8 100644 --- a/apps/parent-control/lib/client/types.ts +++ b/apps/parent-control/lib/client/types.ts @@ -64,6 +64,10 @@ export interface RevokeIntent { } export interface MasterMemoryEntry { + /** Namespace (e.g. `travel`). An agent's cap/scope to read this namespace is + * the namespace-qualified signed service `memory:` — build it with + * `memoryService(ns)` (lib/constants.ts); a bare `memory` fails cap-mint + * (arch.md §896, #177). The configured engine ranks injected lines per query. */ ns: string; key: string; title: string; @@ -102,7 +106,9 @@ export interface AgentKeysClient { enrollK11Begin(input: { userName: string; userDisplayName: string }): Promise>; enrollK11Finish(input: K11EnrollFinishInput): Promise>; - // §2 — master memory (real list + idempotent plant; server dedups by content-hash) + // §2 — master memory (real list + idempotent plant; server dedups by content-hash). + // Per namespace; an agent reads a namespace only with a `memory:` scope + // (memoryService(ns)), and the configured engine ranks what's injected (#177). listMasterMemory(): Promise>; plantMemory(entries: MasterMemoryEntry[]): Promise>; } diff --git a/apps/parent-control/lib/constants.ts b/apps/parent-control/lib/constants.ts index 4c16ab9..21756e4 100644 --- a/apps/parent-control/lib/constants.ts +++ b/apps/parent-control/lib/constants.ts @@ -2,6 +2,17 @@ import type { ChipKind, Namespace } from '@/app/_components/types'; export const NAMESPACES: Namespace[] = ['personal', 'family', 'work', 'travel']; +// The cap/scope "service" string for a memory namespace is namespace-qualified +// (arch.md §896, issue #147): `memory:`. It is a SIGNED cap field — the +// broker hashes it (`keccak`) for `isServiceInScope`, the worker keys storage +// off it (`bots//memory/memory:.enc`), and the grant must match +// exactly. A bare `memory` never matches a `memory:` grant → +// `service_not_in_scope`. Use this wherever a memory cap/scope service is built +// (pairing claim `requested_scope`, scope grant, cap-mint `req.service`). +export function memoryService(ns: string): string { + return `memory:${ns}`; +} + export const CHIP_STYLES: Record = { default: 'chip', ok: 'chip ok', diff --git a/crates/agentkeys-web-core/src/broker.rs b/crates/agentkeys-web-core/src/broker.rs index 6d85b77..624e0d4 100644 --- a/crates/agentkeys-web-core/src/broker.rs +++ b/crates/agentkeys-web-core/src/broker.rs @@ -190,6 +190,12 @@ impl BrokerClient { pub struct CapRequest { pub operator_omni: String, pub actor_omni: String, + /// Signed capability service. For memory it is **namespace-qualified** — + /// `memory:` (e.g. `memory:travel`), arch.md §896 — because the broker + /// hashes it (`keccak(service)`) for `isServiceInScope` and the worker keys + /// storage off it (`bots//memory/memory:.enc`). A bare `memory` + /// never matches a `memory:` grant → `service_not_in_scope`; the web + /// client builds it with `memoryService(ns)`, never a bare `memory`. pub service: String, pub device_key_hash: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/docs/plan/web-flow/README.md b/docs/plan/web-flow/README.md index 1de3c0b..0d1b38e 100644 --- a/docs/plan/web-flow/README.md +++ b/docs/plan/web-flow/README.md @@ -27,6 +27,10 @@ This directory contains the design. Implementation lands as separate PRs that re | [`input-discipline.md`](input-discipline.md) | Which inputs the operator types vs the system derives vs the system auto-generates. Resolves the operator-login-email vs agent-inbox-address distinction explicitly. | | [`data-model.md`](data-model.md) | The HTTP surface the daemon must expose for the UI to drive these flows. Concrete request/response shapes, persistence boundaries, what's local vs chain-anchored. | | [`deferred-and-followups.md`](deferred-and-followups.md) | What stays shell-only forever (operator power-user paths). Open questions for review. Implementation sequencing if approved. | +| [`wire-real-paths.md`](wire-real-paths.md) | The "wire to real backends" implementation plan (W/X phases): replace stubs with real broker/worker/chain calls; the phone-first WASM-core path. | +| [`wire-real-paths-security-review.md`](wire-real-paths-security-review.md) | Security review of the wire-real-paths plan (trust boundaries, bearer handling, CORS). | +| [`issue-9step-flow.md`](issue-9step-flow.md) | The 9-step operator flow mapped to issues #149/#137/#138. | +| [`web-wire-test-runbook.md`](web-wire-test-runbook.md) | **Test runbook**: how to exercise the web app against the live broker/workers/Heima, mirroring [`harness/phase1-wire-demo.sh`](../../../harness/phase1-wire-demo.sh). Honest map of what's UI-wired today (daemon mode: onboarding K11, memory plant/list, revoke, audit reads) vs the task-2 gaps (pairing / cap-mint / scope-grant / SIWE screens), with the on-chain/S3/CLI cross-checks. | ## How to read diff --git a/docs/plan/web-flow/data-model.md b/docs/plan/web-flow/data-model.md index d2828cb..44bdad1 100644 --- a/docs/plan/web-flow/data-model.md +++ b/docs/plan/web-flow/data-model.md @@ -206,15 +206,15 @@ POST /v1/agents/pair/bind — P.2: master binds the sandbox-gen → 4xx { "error": "pop-sig-invalid" } # the agent's proof-of-possession didn't verify POST /v1/agents/pair/approve-scope/begin — P.3: build the K11 challenge for the scope grant - body: { "pair_id": "...", "services": ["travel"] } + body: { "pair_id": "...", "services": ["memory:travel"] } # namespace-qualified memory: (arch.md §896, #177); bare "travel"/"memory" fails cap-mint → 200 { "challenge": "...", "assertion_id": "..." } # reuses the /v1/k11/assert pattern POST /v1/agents/pair/approve-scope/submit — P.3: submit Touch ID assertion → heima-scope-set --webauthn body: { "assertion_id": "...", "authenticatorData": "...", "clientDataJSON": "...", "signature": "..." } - → 200 { "tx_hash": "0x...", "granted": ["travel"] } + → 200 { "tx_hash": "0x...", "granted": ["memory:travel"] } POST /v1/agents/:id/seed-memory — step 1.5: seed a fresh actor's empty namespace - body: { "namespace": "travel", "content": "..." } # operator-supplied; optional - → 200 { "ok": true, "s3_key": "bots//memory/..." } + body: { "namespace": "travel", "content": "..." } # operator-supplied; optional (bare ns here; the worker signs it as service memory:) + → 200 { "ok": true, "s3_key": "bots//memory/memory:travel.enc" } # per-namespace object (arch.md §896) ``` **Wire (Phase 2).** The hook scripts install into the *runtime's* config, which for a remote runtime lives in the sandbox — so the daemon drives `agentkeys wire` over the runtime's exec channel and reports the per-step `ok/skip/fail`. @@ -236,12 +236,12 @@ POST /v1/agents/:id/unwire — remove the managed hooks block fr ``` POST /v1/agents/:id/verify/memory-inject — runs `hermes hooks test pre_llm_call` via the runtime's dispatcher - → 200 { "injected": true, "context": "## Memory: travel\nChengdu trip — …" } # the authoritative Act-1 signal + → 200 { "injected": true, "context": "## Memory: travel\nChengdu trip — …" } # the authoritative Act-1 signal; gate-bounded lines, engine-ranked per query (#177) → 200 { "injected": false, "reason": "mcp-unreachable" | "scope-missing" | "session-bad" } GET /v1/agents/:id/guarantee-health — the §2.2 health panel → 200 { "wired": "hermes 3/3", "mcp_reachable": true, "fail_closed_armed": true, - "last_check": {...}, "last_block": {...}, "last_memory_inject": {...}, "scope_on_chain": ["travel"] } + "last_check": {...}, "last_block": {...}, "last_memory_inject": {...}, "scope_on_chain": ["memory:travel"] } GET /v1/audit/stream?hook=check|audit|memory-inject — the existing SSE feed (PR-C), now hook-tagged # each event carries { hook: "pre_tool_call|post_tool_call|pre_llm_call", action: "check|audit|memory-inject", diff --git a/docs/plan/web-flow/stage3-agent-usage.md b/docs/plan/web-flow/stage3-agent-usage.md index 9848b69..5ff5acc 100644 --- a/docs/plan/web-flow/stage3-agent-usage.md +++ b/docs/plan/web-flow/stage3-agent-usage.md @@ -114,13 +114,13 @@ The web UI shows a three-step progress card mirroring harness Phase P: **P.2 — master binds the device on-chain.** The UI calls the daemon to run `heima-agent-create --from-pubkey --agent-address --actor-omni --device-key-hash --pop-sig ` → `registerAgentDevice`. The master signs with its own key; it bound a device whose private key it has never seen, attested by the agent's `pop_sig`. *(harness `P.2`.)* -**P.3 — master approves the scope (Touch ID).** The UI runs the K11 ceremony: `heima-scope-set --webauthn --agent travel-bot --services travel` (or whatever §1.1 selected). The operator's real Touch ID prompt fires. This is the on-chain scope grant — the moment the agent *earns* its permission. *(harness `P.3`.)* +**P.3 — master approves the scope (Touch ID).** The UI runs the K11 ceremony: `heima-scope-set --webauthn --agent travel-bot --services memory:travel` (or whatever §1.1 selected — the memory service is **namespace-qualified `memory:`** per arch.md §896 / #177; a bare `travel` or `memory` never matches the cap-mint's `memory:travel` hash → `service_not_in_scope`). The operator's real Touch ID prompt fires. This is the on-chain scope grant — the moment the agent *earns* its permission. *(harness `P.3`.)* > 🔐 *Approve travel-bot's permissions* > *travel-bot is asking to read your **travel** memory. Approve with Touch ID to grant it on-chain.* > `[ Approve with Touch ID → ]` -After P.3 the fresh actor exists on-chain with an empty memory and a `travel`-scoped grant. Because the identity is brand-new, the UI offers to **seed** a first memory so the agent has something to recall (harness step `1.5`) — operator-supplied content, or skip. +After P.3 the fresh actor exists on-chain with an empty memory and a `memory:travel`-scoped grant. Because the identity is brand-new, the UI offers to **seed** a first memory so the agent has something to recall (harness step `1.5`) — operator-supplied content, or skip. **Why "born in the sandbox" matters in the UI copy:** the prior design generated the agent key on the laptop and shipped it out — a key-custody smell. The redesign's headline guarantee is that the master holds *no* agent private keys. The pairing card says so explicitly: *"its key is generated on its own device, never on yours."* @@ -138,7 +138,7 @@ Now the agent has an identity and a scope. Wiring is what makes that scope *unby > agentkeys hook check pre_tool_call blocks over-cap / out-of-scope actions > (pay|order|spend…) — fails CLOSED if AgentKeys unreachable > agentkeys hook audit post_tool_call appends an audit row — never blocks -> agentkeys hook memory-inject pre_llm_call injects only your granted namespaces +> agentkeys hook memory-inject pre_llm_call injects only granted memory:, engine-ranked per query > ``` > > `[ Wire travel-bot → ]` `[ Preview what gets written ]` diff --git a/docs/plan/web-flow/web-wire-test-runbook.md b/docs/plan/web-flow/web-wire-test-runbook.md new file mode 100644 index 0000000..dbb77ee --- /dev/null +++ b/docs/plan/web-flow/web-wire-test-runbook.md @@ -0,0 +1,233 @@ +# Web wire test runbook — the parent-control mirror of `phase1-wire-demo.sh` + +This runbook tests the **parent-control web app** (`apps/parent-control/`) against the **same live backends** the agent-side harness uses — `broker.litentry.org` + the service workers + Heima mainnet. It is the master-control-plane counterpart to [`harness/phase1-wire-demo.sh`](../../../harness/phase1-wire-demo.sh) (spec: [`phase1-wire-harness-test-plan.md`](../phase1-wire-harness-test-plan.md)). + +It follows the same `ok / skip / fail` discipline and the same idempotent, fail-loud posture. Where the web UI can't yet exercise a phase1 capability, the step is marked **⛔ not-wired** with the exact cross-check to use instead and the follow-up that unblocks it. Per the repo's "never gloss over a partial implementation in a runbook" rule, the gap is stated up front, not buried. + +--- + +## 0. Roles, and what actually differs from `phase1-wire-demo` + +`phase1-wire-demo.sh` drives **two** hosts: the **agent** (a Hermes runtime inside an aiosandbox container — Phases 1–4) and the **master** (your laptop, via the `agentkeys` CLI — the Phase-0 session mint + Phase-P pairing claim/ack). This runbook replaces the **master CLI half** with the **web app**, talks to the identical broker/workers/chain, and (optionally) keeps the **agent half** exactly as-is (the same `phase1-wire-demo.sh --real` run, or the CLI) so the web app has a real agent to pair with and observe. + +| Concern | `phase1-wire-demo.sh` (CLI master) | This runbook (web master) | +|---|---|---| +| Master identity / session | `wallet_sig_init_session` SIWE-signs → J1 (0.7) | Web onboarding (email → WebAuthn K11). **SIWE/J1 mint is not in the web client yet** — see §6 | +| Backend | `agentkeys` CLI → broker directly | Browser → **daemon** (`--ui-bridge`, :3114) → broker/chain, **or** browser → **WASM core** → broker directly | +| Broker / workers / chain | `broker.litentry.org`, `memory.litentry.org`, Heima mainnet | **identical** | +| Agent counterpart | the sandbox (same process) | a separate `phase1-wire-demo.sh --real` run, or `agentkeys agent` CLI | + +### Honest capability map (read this before running) + +For each phase1 master-side capability, what the web UI can do **today**: + +| phase1 capability | Web **daemon** mode | Web **core** mode | How this runbook tests it | +|---|---|---|---| +| **K11 WebAuthn enroll** (master passkey) | ✅ real (`/v1/k11/enroll/{begin,finish}`) | ⛔ inherits stub | §3 Step B — real Touch ID / virtual authenticator | +| **Operator session (SIWE → J1)** | ⛔ narrated only in UI | ⛔ narrated only | §6 — mint out-of-band (CLI) or treat as task-2 gap | +| **Register master device on-chain** | ⛔ narrated only in UI | ⛔ narrated only | §3 Step B note — cross-check on-chain via CLI | +| **Memory list / plant** (master preserved memory) | ✅ real (`/v1/master/memory[/plant]`, content-hash dedup) | ⛔ inherits stub | §3 Step C — plant + list in UI; cross-check real worker/S3 per §5 | +| **Cap-mint → worker → S3** (the `1.5 seed memory` path) | ⛔ not UI-wired | ⚠️ `capMemoryPut/Get` exist on the WASM core but **no screen calls them** | §3 Step C note + §4 console-drive | +| **Agent pairing — claim / pending / ack** (Phase P) | ⛔ not UI-wired | ⚠️ `pairingClaim/pendingBindings/ackBinding` exist on the WASM core but **no screen calls them** | §3 Step D — §4 console-drive, or CLI | +| **Scope grant** (`updateScope`) | ⚠️ daemon endpoint exists; UI edits **local state only**, never POSTs | ⛔ inherits stub | §3 Step E — task-2 gap; CLI cross-check | +| **Device revoke** | ✅ real (`/v1/actors/:id/revoke`) | ⛔ inherits stub | §3 Step F | +| **Actor list / audit feed** (observe the agent) | ✅ real (`/v1/actors`, `/v1/audit/recent`, SSE `/v1/audit/stream`) | ⛔ inherits stub | §3 Step G | + +**Bottom line:** in **daemon** mode the web app genuinely exercises onboarding (K11), memory plant/list, device revoke, and the read/audit views. **Pairing, cap-mint, scope-grant POST, SIWE, and on-chain master-register are not yet wired into a UI screen** — the daemon/WASM methods exist, but no component invokes them. Those are the [`wire-real-paths.md`](./wire-real-paths.md) task-2 (W-phase) deliverables. This runbook tests what ships today and gives a console/CLI path for the rest so the broker endpoints can still be smoke-tested from the browser. + +> **Memory model (#177 — OpenViking engine behind the gate).** Memory is namespace-partitioned and the cap/scope service is the **signed `memory:`** string (e.g. `memory:travel`, arch.md §896): the worker keys storage per-namespace (`memory:.enc`), the broker hashes it for `isServiceInScope`, and a bare `memory` fails cap-mint. **Reading is query-aware** — the agent's `pre_llm_call` read is ranked by the configured engine (OpenViking, else a deterministic fallback) over the gate-bounded lines; ranking reorders but can never widen past the granted namespaces. The web client builds the service with `memoryService(ns)` (`lib/constants.ts`). + +> **Which mode to run.** Use **daemon** mode for the fullest UI coverage today. Use **core** mode to validate the phone-first browser→broker path (§4) — today that means `status()` reachability plus the console-driven cap/pairing calls. + +--- + +## 1. Prerequisites (mirror of phase1 Phase 0) + +Run these from the repo root on your laptop. Each is the web analog of a phase1 `0.x` check. + +| id | check | command | pass | +|---|---|---|---| +| W0.1 | Heima contracts live | `AGENTKEYS_CHAIN=heima bash scripts/verify-heima-contracts.sh` | exits 0 (all 5 groups) | +| W0.2 | Broker reachable | `curl -fsS https://broker.litentry.org/healthz` | 200 | +| W0.3 | Account exists (master `alice`, agent `demo-agent`) | `bash scripts/setup-heima.sh` (idempotent; skips if done) | `ok`/`skip` per step | +| W0.4 | Operator session on disk (for the daemon) | `test -f ~/.agentkeys/alice/session.json` | present + unexpired | +| W0.5 | `wasm-pack` installed (core mode only) | `command -v wasm-pack` | path printed | + +**W0.4 detail** — the daemon serves the web app but still authenticates to the broker as the operator, exactly like the CLI. If the session is missing/stale, mint it the same way phase1 does (`wallet_sig_init_session`, SIWE-signing with the master key) before launching: + +```bash +# Same mechanism as phase1-wire-demo.sh 0.7; mints ~/.agentkeys/alice/session.json +AGENTKEYS_CHAIN=heima agentkeys session login --session-id alice --broker-url https://broker.litentry.org +# (or run `bash harness/phase1-wire-demo.sh --real --skip-1 --skip-2 --skip-3 --skip-4` once — +# its Phase 0 mints the operator session as a side effect, then exits.) +``` + +--- + +## 2. Launch the web stack + +```bash +# Daemon mode (fullest UI coverage today): +export NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon +export NEXT_PUBLIC_AGENTKEYS_DAEMON_URL=http://localhost:3114 +AGENTKEYS_CHAIN=heima bash dev.sh + +# — or — Core mode (phone-first browser→broker; pairing/cap via console only today): +export NEXT_PUBLIC_AGENTKEYS_BACKEND=core +export NEXT_PUBLIC_AGENTKEYS_BROKER_URL=https://broker.litentry.org +bash dev.sh +``` + +`dev.sh` brings up three processes and (when `wasm-pack` is present) builds the WASM core into `apps/parent-control/public/wasm/`: + +| process | port | role | +|---|---|---| +| `[daemon]` `agentkeys-daemon --ui-bridge` | 3114 | the web app's backend in daemon mode (talks broker + Heima) | +| `[mcp]` `agentkeys-mcp-server` | 18088 | agent-side MCP (only relevant if you run the agent half here too) | +| `[ui]` `next dev` | 3113 | the parent-control web app → **http://localhost:3113** | + +`ok` W1: `http://localhost:3113` loads; in core mode the dashboard's connection status probes `https://broker.litentry.org/healthz` (see [`core.ts`](../../../apps/parent-control/lib/client/core.ts) `status()` — it honestly reports `disconnected` until the read endpoints wire in task 2, with `detail` distinguishing *broker reachable* from *unreachable*). + +--- + +## 3. The flow (mirror of phase1 Phases P → 1.5 → 2 → 3) + +Drive the browser manually, or with the [`/browse`](../../../) skill / a chrome-devtools MCP session. Each step lists the **UI action**, the **expected result**, and the **cross-check** (the authoritative on-chain / S3 / CLI assertion, since UI green alone is not proof). + +### Step A — open + connection status +- **Action:** load `http://localhost:3113`. +- **Expect:** dashboard renders; empty states (no fakes) until onboarded. +- **Cross-check:** daemon mode → status `connected via daemon`; core mode → `disconnected` with `detail: broker … reachable` (by design today). + +### Step B — onboarding: email → WebAuthn K11 ✅ (daemon) +- **Action:** onboarding screen ([`ceremony.tsx`](../../../apps/parent-control/app/_components/ceremony.tsx)) → enter the master email → run the ceremony. At the **"bind passkey"** stage the browser invokes `navigator.credentials.create()`. + - **Real Touch ID:** approve on the Mac. + - **Headless / CI:** attach a CDP **virtual authenticator** first — see §4. (This is the web analog of the harness's software passkey `harness/scripts/erc4337-webauthn-sign.py`.) +- **Expect:** ceremony completes; `enrollK11Begin` → `enrollK11Finish` hit the daemon (`/v1/k11/enroll/{begin,finish}`); `ak_onboarded` set. +- **⛔ not-wired note:** the ceremony's *email→SIWE→J1* and *register-master-on-chain* stages are **narrated only** in the UI (see §6). They do not mint a session or submit a tx. +- **Cross-check (the real proof):** the K11 credential is registered on-chain for the master. + ```bash + # K11 enrollment is master-only; confirm the registry recorded it (CLI / read RPC): + agentkeys k11 status --operator-omni 0x # or the verify-heima read for the master device + ``` + +### Step C — memory: plant + list ✅ list/plant (daemon) · ⛔ cap-mint path +- **Action:** Memory page ([`memory.tsx`](../../../apps/parent-control/app/_components/memory.tsx)) → **Plant** the prepared archive → the list reloads (`listMasterMemory()` / `plantMemory()`, App wiring in [`app/page` App](../../../apps/parent-control/app/_components)). +- **Expect:** the planted namespaces appear; re-planting is **idempotent** (server dedups by content-hash → `skipped` count rises, `planted` stays 0). This mirrors phase1's idempotent `1.5 seed memory`. +- **⛔ not-wired note:** the UI "plant" writes via the daemon's **master preserved-memory** endpoint (`/v1/master/memory/plant`). The **cap-mint → memory worker → S3** path that phase1 `1.5` exercises (`CoreBackend.capMemoryPut`) is **not invoked by any screen**. So a green UI plant does **not** by itself prove the real worker/S3 write. +- **Namespace = signed service (#177, arch.md §896):** the agent's memory cap/scope is **`memory:`** (e.g. `memory:travel`) — a *signed* cap field; the worker keys storage per-namespace (`bots//memory/memory:.enc`). A bare `memory` fails cap-mint (`service_not_in_scope`). Build it with `memoryService(ns)` (`lib/constants.ts`). +- **Reading is query-aware (#177):** the agent's `pre_llm_call` read is ranked by the configured engine (OpenViking, or the deterministic fallback) over the **gate-bounded** lines — ranking can reorder but never widen past the granted namespaces. The engine is chosen at `wire`-time: `agentkeys wire hermes --memory-engine openviking --openviking-endpoint [--openviking-api-key ]` (default = deterministic; arch.md §15.2 + `docs/operator-runbook-openviking.md`). +- **Cross-check (the real worker, per CLAUDE.md "REAL memory only" rule):** + ```bash + # Authoritative real-memory assertion — same as the agent-side demo: + agentkeys hook memory-inject --namespaces travel /bots//memory/memory:travel.enc + ``` + To smoke-test the **cap-mint** broker route from the browser today, use the §4 console drive (with `service: "memory:travel"`). + +### Step D — agent pairing: claim → pending → ack ⛔ not UI-wired +This is phase1 **Phase P** (the master's half: `P.1 claim`, `P.1c pending`, `P.2 ack`). The web Pairing page ([`pairing.tsx`](../../../apps/parent-control/app/_components/pairing.tsx)) is **UI-only today** (renders from demo data; no screen calls `pairingClaim/pendingBindings/ackBinding`). + +- **Agent side (produce a real pairing code):** run the agent half so there's a live code to claim: + ```bash + bash harness/phase1-wire-demo.sh --real --skip-2 --skip-3 --skip-4 # runs Phase P; prints the agent's pairing_code + ``` + …or `agentkeys agent` from a second sandbox. +- **Master side — two ways to exercise it today:** + 1. **Console drive (browser, core mode)** — the WASM core *does* expose the methods; call them from DevTools (§4): + ```js + const core = await window.__agentkeysCore; // see §4 to expose it + await core.pairingClaim(bearer, { pairing_code: "ABCD-1234", label: "demo-agent", requested_scope: "memory:travel" }); + await core.pendingBindings(bearer); // master sees it awaiting approval + await core.ackBinding(bearer, ""); // clears the rendezvous + ``` + 2. **CLI cross-check** (exactly what phase1 P.1/P.1c/P.2 do): `agentkeys agent claim … / agent pending … / pending-bindings/ack`. +- **Unblocks:** a real Pairing screen calling these three CoreBackend methods (a task-2 / W-phase deliverable in [`wire-real-paths.md`](./wire-real-paths.md)). + +### Step E — scope grant ⚠️ partial +- **Action:** Permissions screen ([`permissions.tsx`](../../../apps/parent-control/app/_components/permissions.tsx)) → edit an agent's namespace scope. +- **⛔ not-wired note:** the edit updates **local React state only** — `updateScope()` exists on the daemon backend but the UI never POSTs it. So the on-chain scope is **not** changed by the UI. +- **Cross-check / do-it-for-real:** grant via the CLI (real Touch ID), same as phase1 `P.3`: + ```bash + bash scripts/heima-scope-set.sh --webauthn --agent