Skip to content
Closed
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
4 changes: 2 additions & 2 deletions apps/parent-control/app/_components/memory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function MemoryPage({
<PageHead
crumb="memory · per-namespace · agentmemory-compatible"
title={<><span className="muted serif">/</span> 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:<ns>), and the configured engine ranks what's injected per query — never widening past the gate."
/>

{!hasMemory && !planting && (
Expand Down Expand Up @@ -92,7 +92,7 @@ export function MemoryPage({
<span className="lbl">✓ planted</span>
<span>
Prepared memory is live. The <strong>plant</strong> 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 <code>memory:&lt;ns&gt;</code> scope, query-ranked by the configured engine.
</span>
</div>

Expand Down
6 changes: 6 additions & 0 deletions apps/parent-control/lib/client/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<ns>`
// (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:<ns>.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<unknown> {
return (await loadCore(this.brokerUrl)).capMemoryPut(bearer, req);
}
Expand Down
8 changes: 7 additions & 1 deletion apps/parent-control/lib/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<ns>` — 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;
Expand Down Expand Up @@ -102,7 +106,9 @@ export interface AgentKeysClient {
enrollK11Begin(input: { userName: string; userDisplayName: string }): Promise<Result<K11EnrollBegin>>;
enrollK11Finish(input: K11EnrollFinishInput): Promise<Result<K11EnrollResult>>;

// §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:<ns>` scope
// (memoryService(ns)), and the configured engine ranks what's injected (#177).
listMasterMemory(): Promise<Result<MasterMemoryEntry[]>>;
plantMemory(entries: MasterMemoryEntry[]): Promise<Result<PlantResult>>;
}
11 changes: 11 additions & 0 deletions apps/parent-control/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<ns>`. It is a SIGNED cap field — the
// broker hashes it (`keccak`) for `isServiceInScope`, the worker keys storage
// off it (`bots/<actor>/memory/memory:<ns>.enc`), and the grant must match
// exactly. A bare `memory` never matches a `memory:<ns>` 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<ChipKind, string> = {
default: 'chip',
ok: 'chip ok',
Expand Down
6 changes: 6 additions & 0 deletions crates/agentkeys-web-core/src/broker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:<ns>` (e.g. `memory:travel`), arch.md §896 — because the broker
/// hashes it (`keccak(service)`) for `isServiceInScope` and the worker keys
/// storage off it (`bots/<actor>/memory/memory:<ns>.enc`). A bare `memory`
/// never matches a `memory:<ns>` 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")]
Expand Down
4 changes: 4 additions & 0 deletions docs/plan/web-flow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 6 additions & 6 deletions docs/plan/web-flow/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<ns> (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/<actor>/memory/..." }
body: { "namespace": "travel", "content": "..." } # operator-supplied; optional (bare ns here; the worker signs it as service memory:<ns>)
→ 200 { "ok": true, "s3_key": "bots/<actor>/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`.
Expand All @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions docs/plan/web-flow/stage3-agent-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <addr> --actor-omni <omni> --device-key-hash <hash> --pop-sig <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:<ns>`** 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."*

Expand All @@ -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:<ns>, engine-ranked per query
> ```
>
> `[ Wire travel-bot → ]` `[ Preview what gets written ]`
Expand Down
Loading
Loading