# Profile Isolation Implementation Status

## Purpose

This file is the changelog-style bridge between the design docs in this folder and the code that already landed.
Use it when you need to answer three questions quickly:

1. What isolation work is already implemented?
2. Which code paths enforce it?
3. Which follow-up gaps still need explicit review?

## Completed phases

### Phase 1 — Prompt and profile layering

Implemented:
- layered prompt assembly for system / principal / session ordering
- shared knowledge prompt block with separate budget and doc pointers
- principal-aware profile resolution helpers

Primary files:
- `agent/prompt_builder.py`
- `agent/system_prompt.py`
- `agent/shared_knowledge.py`
- `agent/agent_init.py`
- `gateway/principal_profiles.py`
- `hermes_cli/config.py`

### Phase 2 — Session, dashboard, and memory ownership gates

Implemented:
- principal-aware session visibility in dashboard routes
- owner-gated session detail / export / descendant access
- principal-scoped remote durable memory writes
- principal-aware `session_search` profile access filtering

Primary files:
- `hermes_cli/web_server.py`
- `tools/memory_tool.py`
- `tools/session_search_tool.py`
- `gateway/platforms/api_server.py`

### Phase 3 — Verified-identity routing and managed-download ownership

Implemented:
- verified identity carried into API-server session binding
- principal-aware managed-download owner metadata
- owner-bound managed downloads rejected outside the matching request scope

Primary files:
- `gateway/platforms/api_server.py`
- `hermes_cli/managed_downloads.py`
- `hermes_cli/web_server.py`

### Phase 4 — API response-chain and session CRUD isolation

Implemented:
- `api_server` session CRUD / message / fork / chat endpoints now resolve the verified caller first and then operate inside the matching principal profile scope
- response snapshots used by `previous_response_id`, conversation-name chaining, and `GET/DELETE /v1/responses/{id}` now live in a per-profile `response_store.db`
- streaming and non-streaming responses share the same principal-scoped response-store behavior
- `/v1/runs` now resolves `previous_response_id` from the active principal profile instead of a process-global store
- a lazy compatibility proxy preserves `adapter._response_store` call sites in tests without opening SQLite on every adapter construction

Primary files:
- `gateway/platforms/api_server.py`

### Phase 5 — Download-link mode control

Implemented:
- managed downloads now support explicit `link_mode` selection
- `private` mode keeps `owner_metadata` bound to the link
- `public` mode strips `owner_metadata` while preserving the existing expiry policy
- `api_server` request bodies now accept `download_link_mode` and return machine-readable `hermes.download_link_mode*` metadata so callers can prompt for private vs public link behavior
- `/v1/responses` non-streaming payloads now keep `response.output[*]` aligned with the rewritten managed-download URL instead of exposing a host-local artifact path in the assistant message item
- `/v1/responses` streaming terminal events now carry the same managed-download rewrite and `hermes.download_link_mode` metadata as the non-streaming path
- live streaming delta paths now hold back split local file-path prefixes and rewrite them before delivery instead of briefly exposing `/Users/...` or `C:\...` during SSE streaming
- the OpenWebUI runs proxy now accepts `download_link_mode`, forwards it upstream, and rewrites local artifact paths using owner-aware private/public managed-download selection instead of always minting public links

Primary files:
- `hermes_cli/managed_downloads.py`
- `tests/hermes_cli/test_managed_downloads.py`
- `gateway/platforms/api_server.py`
- `tests/gateway/test_api_server.py`
- `tests/gateway/test_session_api.py`
- `/Users/hermes/.hermes/scripts/openwebui_runs_proxy.py`
- `tests/gateway/test_openwebui_runs_proxy_handoff.py`

## Verified tests already exercised for this workstream

- `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_api_server.py -k 'ResponsesEndpoint or ResponsesStreaming or Truncation or ConversationParameter'`
- `tests/gateway/test_multiplex_api_server_routing.py`
- `tests/hermes_cli/test_managed_downloads.py`

## Remaining follow-up review items

These are not known regressions, but they remain explicit review targets whenever this isolation layer changes again:

- artifact-rewrite call sites outside `api_server` that may still default to public links without a caller-selected mode
- gateway UX hooks that should ask the user whether a link should stay owner-bound or become public
- any future response-chain storage outside `response_store.db`
- non-API file-delivery paths that may need the same principal-aware mode selection contract

## Operational finding from live runtime inspection

- the current macOS LaunchAgent `~/Library/LaunchAgents/ai.hermes.gateway.plist` was found running `gateway run --replace`
- `launchctl print gui/$(id -u)/ai.hermes.gateway` showed repeated restarts (`runs=41`, `forks=2400`, last terminating signal `Killed: 9`) while the service was still healthy
- for a long-lived supervised LaunchAgent, `--replace` should be removed and reserved for explicit operator takeover flows instead of steady-state service startup
- see `restart-recovery-runbook.md` for the safe reload / health-check sequence before applying that launchd change

## Current invariant

If a request is authenticated as principal **A**, then:
- it must read and write only principal A's session store and response store,
- it must not resolve principal B's response chain,
- and any owner-bound download minted from that request must remain inaccessible to principal B.