From 9edcd6330a300f66af3369e3742e660555cfd967 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 29 Jul 2026 23:44:51 +0800 Subject: [PATCH] docs: add profile isolation design and checklist --- agent/prompt_builder.py | 11 +++ agent/shared_knowledge.py | 3 + docs/profile-isolation/README.md | 52 +++++++++++ docs/profile-isolation/checklist.md | 68 ++++++++++++++ docs/profile-isolation/technical-design.md | 104 +++++++++++++++++++++ gateway/principal_profiles.py | 6 ++ hermes_cli/subcommands/memory.py | 4 + 7 files changed, 248 insertions(+) create mode 100644 docs/profile-isolation/README.md create mode 100644 docs/profile-isolation/checklist.md create mode 100644 docs/profile-isolation/technical-design.md diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 18579c9b0..a2282f62d 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -148,6 +148,12 @@ HERMES_AGENT_HELP_GUIDANCE = ( "of truth when the two differ." ) +# Memory layering contract: +# - Global/system rules and shared knowledge are documented in +# docs/profile-isolation/technical-design.md and docs/profile-isolation/README.md. +# - Principal-scoped preferences belong in gateway/principal_profiles.py. +# - Keep the distinction explicit here so the prompt builder never treats all +# user-provided facts as one shared bucket again. MEMORY_GUIDANCE = ( "You have persistent memory across sessions. Save durable facts using the memory " "tool: user preferences, environment details, tool quirks, and stable conventions. " @@ -1855,6 +1861,11 @@ def load_soul_md(context_length: Optional[int] = None) -> Optional[str]: Used as the agent identity (slot #1 in the system prompt). When this returns content, ``build_context_files_prompt`` should be called with ``skip_soul=True`` so SOUL.md isn't injected twice. + + For the full profile-layering contract, read: + - docs/profile-isolation/README.md + - docs/profile-isolation/technical-design.md + - docs/profile-isolation/checklist.md """ try: from hermes_cli.config import ensure_hermes_home diff --git a/agent/shared_knowledge.py b/agent/shared_knowledge.py index fec26df92..f794f6821 100644 --- a/agent/shared_knowledge.py +++ b/agent/shared_knowledge.py @@ -1,5 +1,8 @@ """Shared knowledge / FAQ snapshot for system-prompt injection. +Read docs/profile-isolation/README.md and docs/profile-isolation/technical-design.md +before changing this module. + This layer is intentionally GLOBAL across principal profiles: it lives under the Hermes root (``~/.hermes/shared_knowledge`` by default), not under an active profile home. That lets different principals keep isolated private memories diff --git a/docs/profile-isolation/README.md b/docs/profile-isolation/README.md new file mode 100644 index 000000000..18b451af9 --- /dev/null +++ b/docs/profile-isolation/README.md @@ -0,0 +1,52 @@ +# Profile Isolation & Memory Layering + +This folder is the readable map for the **system profile / principal profile / session context** split. + +Read order: +1. `technical-design.md` — architecture and precedence rules +2. `checklist.md` — implementation and acceptance checklist +3. `README.md` — quick file map and why these files exist + +## What belongs where + +- **System profile** + - Global rules shared by every user + - Admin-only edits from the local machine + - Examples: product policy, security rules, output style, platform-wide behavior + +- **Principal profile** + - One profile per canonical principal + - Bound to the verified email / platform identities for that person + - Examples: personal name preference, delivery preference, verified email, local habits + +- **Session context** + - Temporary turn-by-turn facts for the current conversation + - Must not be written back as permanent profile state unless it is a durable preference + +## Code paths to inspect when changing this flow + +- `agent/prompt_builder.py` + - prompt assembly order + - memory injection boundaries + - identity-only prompt blocks + +- `agent/shared_knowledge.py` + - global reusable knowledge layer + - content sanitization for shared FAQ-style notes + +- `gateway/principal_profiles.py` + - principal-id to profile-path mapping + - creation / resolution of per-principal profile directories + +- `hermes_cli/subcommands/memory.py` + - built-in memory reset commands + - what is and is not affected by memory provider commands + +## Practical rule + +If a fact answers "is this about everyone, or just this person?" then: +- everyone -> system profile / shared knowledge +- just this person -> principal profile +- just this turn -> session context + +If you cannot tell, fail closed and keep it out of the permanent layer. \ No newline at end of file diff --git a/docs/profile-isolation/checklist.md b/docs/profile-isolation/checklist.md new file mode 100644 index 000000000..337b8a4d8 --- /dev/null +++ b/docs/profile-isolation/checklist.md @@ -0,0 +1,68 @@ +# Profile Isolation Acceptance Checklist + +## Goal +Verify that global system rules, principal-specific profiles, and session-only context do not leak into each other. + +## Must-pass checks + +### Identity / principal resolution +- [ ] Every gateway request resolves to a canonical principal when possible +- [ ] Ambiguous identity fails closed +- [ ] A missing verified identity does not fall back to a random operator profile +- [ ] Email is treated as an attribute of the principal, not the primary key + +### System profile protection +- [ ] Only local admin workflows can modify the system profile +- [ ] Telegram / LINE / email / OpenWebUI users cannot edit the system profile +- [ ] System profile changes are audit-logged +- [ ] System profile remains stable across user sessions + +### Principal profile isolation +- [ ] Each canonical principal has its own profile bucket +- [ ] One principal cannot read or write another principal's profile +- [ ] The resolved principal always selects the matching profile +- [ ] Profile writes are restricted to allowed fields + +### Session context safety +- [ ] Session-only facts are not persisted unless explicitly promoted +- [ ] Temporary task notes do not end up in the durable profile +- [ ] A failed or ambiguous identity does not create a permanent profile entry + +### Shared knowledge layer +- [ ] Shared knowledge is clearly global and non-private +- [ ] Shared knowledge is sanitized before injection +- [ ] Shared knowledge does not absorb one user's personal facts +- [ ] Shared knowledge lives outside principal homes + +### Prompt assembly order +- [ ] System rules load before principal personalization +- [ ] Principal personalization loads before session context +- [ ] Lower-priority layers cannot override higher-priority rules +- [ ] Shared knowledge does not replace the system profile + +### Conversation isolation +- [ ] A cannot see B's conversation summary +- [ ] B cannot see A's conversation summary +- [ ] A cannot ask for B's memory or profile notes +- [ ] B cannot ask for A's memory or profile notes +- [ ] Existence leaks are rejected just like full content leaks + +### OpenWebUI / gateway / email +- [ ] OpenWebUI resolves the live caller, not a cached operator default +- [ ] Telegram and LINE use the bound principal, not chat labels alone +- [ ] Email handoff re-checks the live recipient principal +- [ ] Shared surfaces fail closed when caller identity is ambiguous + +## Regression tests to run +- [ ] `tests/agent/test_system_prompt_principal_precedence.py` +- [ ] `tests/agent/test_system_prompt_shared_knowledge.py` +- [ ] `tests/gateway/test_principal_profile_routing_phase1.py` +- [ ] `tests/gateway/test_principal_profile_runtime_phase2.py` +- [ ] `tests/gateway/test_api_server_session_ownership.py` +- [ ] `tests/hermes_cli/test_web_server_session_access.py` +- [ ] `tests/gateway/test_verified_email_handoff_guard_helpers.py` + +## Done means +- [ ] The docs in this folder describe the current behavior accurately +- [ ] Code comments point back here and to `technical-design.md` +- [ ] No code path can silently merge private data into the shared system profile \ No newline at end of file diff --git a/docs/profile-isolation/technical-design.md b/docs/profile-isolation/technical-design.md new file mode 100644 index 000000000..7777fefe4 --- /dev/null +++ b/docs/profile-isolation/technical-design.md @@ -0,0 +1,104 @@ +# Technical Design: System Profile + Principal Profile Isolation + +## Goal + +Stop one user's private facts from being treated as shared state while still keeping a reusable system-wide policy layer. + +## Design summary + +Use a **layered prompt model**: + +1. **System / global profile** + - One shared policy layer for the whole Hermes deployment + - Admin-only edits from the local machine + - Holds stable behavior, safety policy, formatting rules, and product-wide defaults + +2. **Principal profile** + - One durable profile per canonical principal + - Bound to the verified email and external identities for that person + - Holds personal preferences and durable user-specific facts + +3. **Session context** + - Temporary, turn-local context only + - Used for the current conversation and discarded unless it becomes a durable preference + +## Precedence rules + +Highest priority first: + +1. system / safety / governance rules +2. developer or platform constraints +3. principal profile +4. session context + +A lower layer may tailor the response, but it may not override a higher layer. + +## Data model + +### Canonical identity +- `principal_id` +- `verified_email` +- `external_identities` +- `profile_path` +- `last_verified_at` + +### System profile +- Stored separately from principal data +- Editable only by local admin workflows +- Contains reusable rules that are not private to any one person + +### Principal profile +- Stored under the canonical principal namespace +- Contains only data that belongs to that principal +- Must never be auto-populated from another person's conversation + +### Shared knowledge +- Global, non-private FAQ / reusable notes +- Safe only when content is sanitized and clearly non-personal +- Lives outside principal homes + +## Runtime flow + +1. Resolve the live caller to a canonical principal. +2. Load the global system profile. +3. Load the matching principal profile. +4. Add session-only context. +5. Build the prompt. +6. Refuse to build cross-principal context if the caller cannot be resolved. + +## Write rules + +### Allowed +- Admin local editing of the system profile +- Verified principal editing their own principal profile within allowed fields +- Session-only facts for the current conversation + +### Not allowed +- Gateway users editing the global system profile +- One principal writing into another principal's profile +- Treating session-only facts as durable cross-user state +- Using `email` as the sole identity key when `principal_id` is available + +## Security requirements + +- Fail closed when caller identity is ambiguous +- Do not fall back to operator defaults on shared surfaces +- Do not treat summaries or snippets as public +- Sanitize shared knowledge before injection +- Keep audit trails for every profile write + +## Files to inspect during implementation + +- `agent/prompt_builder.py` +- `agent/shared_knowledge.py` +- `gateway/principal_profiles.py` +- `gateway/user_verification.py` +- `hermes_cli/subcommands/memory.py` +- `tests/agent/test_system_prompt_principal_precedence.py` +- `tests/agent/test_system_prompt_shared_knowledge.py` +- `tests/gateway/test_principal_profile_routing_phase1.py` +- `tests/gateway/test_principal_profile_runtime_phase2.py` + +## Rollout note + +When this design changes, update this file first, then update the code comments that point here so future edits still have a clear starting point. \ No newline at end of file diff --git a/gateway/principal_profiles.py b/gateway/principal_profiles.py index a6fb4e24d..99e382d64 100644 --- a/gateway/principal_profiles.py +++ b/gateway/principal_profiles.py @@ -1,3 +1,9 @@ +"""Principal profile mapping helpers. + +Read docs/profile-isolation/README.md and docs/profile-isolation/checklist.md +before changing how principals map to profile directories. +""" + from __future__ import annotations import json diff --git a/hermes_cli/subcommands/memory.py b/hermes_cli/subcommands/memory.py index 23fe0b857..a46e7e97e 100644 --- a/hermes_cli/subcommands/memory.py +++ b/hermes_cli/subcommands/memory.py @@ -38,6 +38,10 @@ def build_memory_parser(subparsers, *, cmd_memory: Callable) -> None: "reset", help="Erase all built-in memory (MEMORY.md and USER.md)", ) + # This command only touches built-in MEMORY/USER stores. It does not + # modify principal-scoped profiles or shared knowledge; see + # docs/profile-isolation/README.md and docs/profile-isolation/checklist.md + # for the separation model and the acceptance rules. _reset_parser.add_argument( "--yes", "-y",