docs: add profile isolation design and checklist
parent
df09dfd304
commit
9edcd6330a
|
|
@ -148,6 +148,12 @@ HERMES_AGENT_HELP_GUIDANCE = (
|
||||||
"of truth when the two differ."
|
"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 = (
|
MEMORY_GUIDANCE = (
|
||||||
"You have persistent memory across sessions. Save durable facts using the memory "
|
"You have persistent memory across sessions. Save durable facts using the memory "
|
||||||
"tool: user preferences, environment details, tool quirks, and stable conventions. "
|
"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
|
Used as the agent identity (slot #1 in the system prompt). When this
|
||||||
returns content, ``build_context_files_prompt`` should be called with
|
returns content, ``build_context_files_prompt`` should be called with
|
||||||
``skip_soul=True`` so SOUL.md isn't injected twice.
|
``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:
|
try:
|
||||||
from hermes_cli.config import ensure_hermes_home
|
from hermes_cli.config import ensure_hermes_home
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
"""Shared knowledge / FAQ snapshot for system-prompt injection.
|
"""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
|
This layer is intentionally GLOBAL across principal profiles: it lives under the
|
||||||
Hermes root (``~/.hermes/shared_knowledge`` by default), not under an active
|
Hermes root (``~/.hermes/shared_knowledge`` by default), not under an active
|
||||||
profile home. That lets different principals keep isolated private memories
|
profile home. That lets different principals keep isolated private memories
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ def build_memory_parser(subparsers, *, cmd_memory: Callable) -> None:
|
||||||
"reset",
|
"reset",
|
||||||
help="Erase all built-in memory (MEMORY.md and USER.md)",
|
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(
|
_reset_parser.add_argument(
|
||||||
"--yes",
|
"--yes",
|
||||||
"-y",
|
"-y",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue