docs: add profile isolation design and checklist

fix/verification-admin-route-recovery
Hermes Agent 2026-07-29 23:44:51 +08:00
parent df09dfd304
commit 9edcd6330a
7 changed files with 248 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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",