Profile Isolation in Agent Harnesses
Routing a turn to the right profile or session is not enough. Every deferred control-plane, secret-resolution, sandbox, settings and delivery phase must re-establish the same durable owner scope or a multiplexed harness can read, mutate or deliver another owner's state.
Page reviewed 4 Oct 2026 Source and review policy
The invariant
Section titled “The invariant”A multiplexed agent harness may run several named profiles, workspaces or sessions inside one long-lived process. Each owner scope can have different configuration, credentials, state databases, provider accounts, MCP servers, terminal policy, sandbox mounts and output files.
The important invariant is therefore stronger than “the request was routed to owner X”:
Every phase that can observe or mutate owner-scoped state must execute under the same durable owner identity, including phases that run after the original turn scope has ended.
This matters because many failures happen after routing was correct. A request can enter under the right profile or session, then later resolve ${VAR}, test an MCP server, load workspace settings, translate a container path, hydrate a resumed session or deliver an attachment after the original context variables have already been restored.
Current Hermes stable remains v2026.9.24 / Agent v0.21.5. A fresh 4 October ancestry check confirms the profile-auth correction at c712f06d is included in that release; the September source snapshots and reported tests below keep their original revision scope.
Audit ownership with two profiles and one deferred action
Section titled “Audit ownership with two profiles and one deferred action”Create two disposable profiles A and B with distinct inert sentinel values, workspaces and transcripts. Exercise one routed turn from A’s transport into B’s runtime, then defer an artifact delivery or background callback until the foreground profile context has unwound. Current Hermes profile documentation defines a profile as a separate persistent home and explicitly warns against multiple independent agent processes writing the same home.
| Phase | Owner to assert |
|---|---|
| Intake authorization | Receiving transport’s policy/credential owner |
| Runtime/config/secrets | Routed destination profile |
| Session and durable memory writes | Canonical session owner |
| Deferred callback or thread hop | Captured durable owner, re-entered explicitly |
| Artifact and reply delivery | Permitted delivery adapter plus owner-scoped artifact |
| Process-global dashboard policy | Launch/process home |
The diagnostic passes only if every phase uses its intended sentinel and no missing B credential silently reads A’s corresponding profile-owned credential. Deliberately shared authentication is a separate authority: record the shared store explicitly and test its allowed consumers instead of requiring every credential to be profile-local.
A correct initial route cannot excuse a wrong deferred reader. Keep the ownership tuple and failure phase in the incident record so routing identity prevention and durable-state repair remediation can be assessed independently. This is a proposed synthetic audit, not a new credential or cross-platform experiment performed here.
Ownership has to survive the whole lifecycle
Section titled “Ownership has to survive the whole lifecycle”| Phase | What must stay owner-scoped | Typical failure if ambient/default state wins |
|---|---|---|
| Ingress and routing | profile/session/workspace identity, home/config root | request reaches the wrong configuration |
| Secret expansion | vault/environment mapping, provider credentials | another owner’s token is used or a valid owner token appears missing |
| MCP/provider control plane | server config, headers, auth workers, connection tests | false-green/false-red health result or credential crossover |
| Settings and permissions | stable workspace root, scope, live policy fan-out | a worktree write lands in the bootstrap project or affects sibling sessions |
| Session and persistence | state DB, transcript, session metadata | another owner’s history or state is read or mutated |
| Tool/runtime execution | terminal policy, sandbox identity, volume map | command or file operation targets the wrong runtime |
| Deferred/background work | owner identity across callbacks, queues and jobs | later work falls back to process-global state |
| Media/artifact delivery | sandbox roots, output mounts, path validation | valid output is dropped or a same-named file from another owner is delivered |
| Caches and memoization | cache key must include the ownership dimensions it depends on | one owner’s resolved client, credential or capability leaks into another |
The safest implementation pattern is to derive the owner from durable request/session identity, then explicitly re-enter that owner’s runtime scope whenever later work needs owner-scoped state. Process-global environment variables, process cwd and a daemon’s bootstrap configuration are not ownership models.
September Hermes case study: control-plane secret scope
Section titled “September Hermes case study: control-plane secret scope”Hermes issue #109901, closed 13 September 2026, reproduced a secondary-profile Desktop MCP health probe that failed when the profile’s Authorization token came from Bitwarden Secrets Manager. A fresh profile-scoped CLI process connected successfully, but the shared Desktop backend entered only the profile home scope before resolving the MCP configuration. Secret expansion then correctly failed closed rather than falling back to another profile’s process environment.
Commit 0388d03f225719fc0df3a239c39bbc103fbf1888 generalizes the correction beyond that one route. The profile-scoped RPC path used by control-plane methods including mcp.servers.*, mcp.catalog, skills.manage, plugins.manage, cron.manage and insights.get now enters the requested profile’s complete runtime scope rather than rebinding only HERMES_HOME. The implementation hydrates that profile’s external secret sources and applies its secret and terminal scope without mutating process os.environ.
The regression is intentionally adversarial: the secondary profile’s .env contains one token while the process environment contains a different default-profile token. The MCP Authorization header must resolve to the secondary profile value. That test shape matters because “connection succeeds” is insufficient evidence when the wrong credential could also be valid.
September Hermes case study: profile-owned credentials are isolated, with an explicit Nous shared-auth exception
Section titled “September Hermes case study: profile-owned credentials are isolated, with an explicit Nous shared-auth exception”Hermes stable v2026.9.14 / v0.21.3 still contains PRs #110024 and #110941, which hardened the then-supported behavior where a named profile without its own Codex/OAuth state could borrow a root-owned rotating grant. #110024 made refresh write-back follow the store that supplied the grant, while #110941 serialized the authoritative root-store re-read, refresh request and write-back so two borrowers could not redeem the same single-use refresh token concurrently. Those fixes remain valid historical evidence for the tagged release.
Current Hermes main deliberately changes the profile-owned authentication model. Commit 93889b770da3dc641c2ed55eecffef90da9c358f (fix(auth): named profiles no longer inherit the root profile's auth.json) removes the implicit root fallback for profile-local provider state and credential pools. A named profile with no local profile-owned provider state is now told to configure its own provider instead of silently acting as the root owner. For the single-use OAuth providers Anthropic, OpenAI Codex and xAI, reads, refreshes and writes stay in the active profile store; the old root write-through, borrowed-row bookkeeping and fallback/heal machinery are removed.
Merged PR #114162 / current-main commit c712f06dcdd24053a4118f38d2090ac53137ecfc makes an important exception explicit after Hermes’ own Bot Mode documentation overgeneralized that change. Nous Portal authentication is intentionally shared across profiles: hermes_cli/auth_nous.py stores the canonical Nous token state in HERMES_SHARED_AUTH_DIR or <root>/shared/nous_auth.json, outside any named profile, and runtime refreshes update that shared store so its refresh token stays current across profiles. The single-use hygiene set in auth_oauth_grants.py contains exactly anthropic, openai-codex and xai-oauth; Nous is not in that set.
The visible creation control is therefore Copy API keys from the main profile, not a promise that every authentication mechanism is isolated. Static API keys may be copied into the new profile. Anthropic, OpenAI Codex and xAI single-use OAuth grants are stripped and each profile signs in separately. A Nous OAuth row can be carried during mirroring because Hermes models Nous as a separate shared authority. Legacy share_auth input is accepted for compatibility but ignored; PR #114162 also corrects a false upstream docs reference to a shared_auth create field, which does not exist. The PR notes one remaining upstream UI-copy mismatch at this cutoff: the Desktop create dialog still lists Nous among OAuth logins that are not copied even though the resolver/shared-store behavior does share it.
The substantive security correction is therefore remove implicit root auth.json fallback for profile-owned authentication, not “all credentials are profile islands.” A service profile cannot act as the root account merely because its profile-local auth store is empty, and its Anthropic/Codex/xAI refresh cannot rotate the root profile’s corresponding token chain. At the same time, a deliberately shared authority such as Nous is allowed to cross profile boundaries because its owner is the shared store, not either profile-local auth.json.
This is post-v0.21.3 behavior on the reviewed development snapshot, not a retroactive claim about the tagged release. The reusable rule is: credentials that define one profile owner should not cross profile boundaries through an implicit “missing local state → use root” fallback. If sharing is intentional, model and document the shared authority explicitly, as Hermes now does for Nous, instead of describing it as profile-local inheritance.
September Hermes case study: process-global dashboard authentication belongs to the launch home
Section titled “September Hermes case study: process-global dashboard authentication belongs to the launch home”Merged Hermes PR #113428 / merge bf28c15de4bcfeacb1e4cd663495385b26cc2668 closes a different ownership gap at the dashboard ingress boundary. A long-lived dashboard process could first register the launch home’s dashboard.basic_auth provider, then later run profile-scoped plugin discovery for a Bot Chat session. Because register_global_provider is process-global and every per-home plugin manager was previously allowed to register, a secondary profile carrying a stale dashboard.basic_auth block could replace the launch home’s provider simply by being discovered later.
The upstream reproduction is security-significant rather than cosmetic: the launch home starts with password P1 and the profile has stale password P0. Before the fix, launch discovery accepts P1 and rejects P0; after one profile-scoped discover_and_load, the provider object is replaced, P1 becomes invalid and P0 is accepted for the process-wide dashboard login. Commit d93f74b7589d0c47d42e00468a1bfb55ab5cd359 binds provider registration to get_process_hermes_home(): a plugin manager whose scope differs from the process launch home is refused with a warning, while the launch-home manager still registers or rotates the global provider normally. A process intentionally launched with HERMES_HOME pointing at a named profile still lets that profile own dashboard authentication because it is then the launch home.
PR #113428 reports 13 passing dashboard-auth plugin-hook tests and a live two-home probe that changes from VERDICT: BUG: profile basic_auth hijacked the dashboard login on the base to VERDICT: OK: main password still accepted, stale profile password rejected after the fix. The important invariant is not “profile config cannot contain dashboard auth”; it is that profile-local discovery cannot mutate a process-global ingress security gate unless that profile is the process owner of the gate.
This is post-v0.21.3 Hermes behavior at the September source cutoff. It changes local dashboard authentication-provider ownership only; it does not change UHP, MCP, ACP or A2A wire semantics, establish native Hermes UHP adoption, or add HarnessRouter conformance evidence.
The reusable rule is: a process-global authentication or authorization gate needs one durable process owner. Do not let “last profile discovered” become the authority for a global login surface; bind registration and rotation to the launch home or another explicit global owner.
September Hermes case study: post-turn media delivery scope
Section titled “September Hermes case study: post-turn media delivery scope”Hermes issue #109024, closed 14 September 2026, documents the complementary delivery-side failure. A multiplexed named profile produced a Docker file successfully, but later MEDIA: processing occurred after the routed turn scope had ended. Path translation then consulted ambient/default profile sandbox and volume state. The result could be a dropped valid file; more seriously, a same-named default-profile file could be selected instead.
Commit 28138f2524335edda86b55da6d71eec60c7e65b0 makes media extraction and validation re-enter the source profile’s home and terminal-policy scope before resolving Docker paths and explicit output mounts. Its regression creates the same filename in default and secondary profile mounts and verifies that the secondary profile’s bytes are the ones delivered.
This is the same ownership problem as the MCP probe, just at a different lifecycle phase: correct ingress routing does not prove correct post-ingress ownership.
September Hermes case study: owner artifacts are wire-contract data, not client overrides
Section titled “September Hermes case study: owner artifacts are wire-contract data, not client overrides”Merged Hermes PR #110747 / commit 49c6d4a9e0dddc64d6333c5b1fcc9d911463a8fd, now included in stable Hermes v0.21.3, adds a complementary boundary at the gateway wire itself. Hermes now declares its TUI/Desktop gateway contract once in Python with Pydantic and generates the TypeScript and OpenRPC representations from that source. The merged PR records contracts for 215 client→server methods, 13 server→client requests and 67 events, with unknown object keys forbidden by default unless a producer-owned open model is explicitly intended.
For profile isolation, the important part is not the generator by itself. The dispatcher now rejects unknown request keys with Hermes gateway error 4000 and the current-head regression explicitly sends forged owner_transport, owner_session_record and owner_token parameters. Those keys are rejected at the wire and no steer reaches the agent. Before this contract enforcement the same unknown keys were silently dropped; the upstream regression says the no-steer ownership invariant was already preserved, so this should be read as stronger fail-closed validation and evidence, not as proof that the previous implementation allowed a successful owner takeover.
The same change removes a second class of ownership risk: hand-maintained frontend/backend wire shapes can drift about which fields exist, are nullable or carry identity. PR #110747 reports that the generated contract exposed three phantom frontend fields, eight nullability mismatches, a SubagentStatus drift and transcript/resume shape mismatches. Staleness/completeness checks now fail when a generated artifact is stale or an emitter has no declared contract, and the generated TypeScript surface makes backend field removal visible to tsc instead of leaving an independently typed client contract behind.
The prerequisite PR #110677 also replaces thirteen hand-rolled *.request / *.respond pairs with real JSON-RPC server→client requests answered by response frames carrying the same id. Pending requests are replayed on resume/activation, one request.cancel event withdraws them, and an unsupported client method fails with -32601 instead of being treated as an ad-hoc event convention. This is useful to isolation because the ownership-bearing session/request path now has one explicit request/response lifecycle rather than thirteen parallel mechanisms.
The reusable rule is: derive ownership server-side, make ownership-bearing wire fields closed and schema-checked, and generate every consumer contract from the same owner of truth. Contract validation does not replace authentication or authorization, but it prevents incidental or misspelled client fields from becoming an ambiguous second ownership channel.
September Hermes case study: routed cron work needs task-local multiplex semantics
Section titled “September Hermes case study: routed cron work needs task-local multiplex semantics”Merged Hermes PR #111187 / merge cedf4a3d78675283fa93e4e6ea2d6212bf414667 closes a deferred-work isolation gap that is easy to miss in a long-lived Desktop backend. A sibling profile’s cron fire can be multiplexed even when the process as a whole was not launched in global multiplexer mode. Hermes now carries that fact through a task-local multiplex context for the routed fire and external-worker handoff, so secret resolution remains fail-closed for that span instead of inheriting the launch profile’s ambient environment.
The same change clarifies three different secret-name sets that cannot safely be collapsed into one:
- a routed profile’s authoritative secret scope includes its profile values plus administrator-managed
.envvalues composed last, preserving managed-policy precedence without reintroducing generic ambient fallback; - the launch-residue scrub set includes dotenv-defined names, terminal configuration and every name an external source supplied, even when an existing process value caused that source value to be skipped; and
- the MCP stdio forward set remains narrower:
secret_source_names()tracks values actually applied from external secret sources, while the newsource_supplied_names()exists for scrubbing. A source merely mentioning a name is not enough reason to forward a process value into an MCP child.
That distinction matters because skipped_existing is still provenance evidence for isolation. If an external source supplied TOKEN but process state already contained launch-profile TOKEN, the routed child must scrub the name; treating the wider supplied-name set as MCP-forwardable would instead risk carrying the process value into the child. Administrator-managed keys are handled separately because they are intentional process-wide policy and are re-applied after the routed profile overlay.
PR #111187 also reuses the same routed-fire secret-scope helper for the external-worker environment build and refreshes an already-installed secret scope when later discovery changes its authoritative values. Upstream reports 334 passing tests across nine cron/env/secret-scope test files, including invariants that a launch-only secret is absent inside a routed ticker fire, a routed no_agent child never receives that launch-only name, managed keys retain precedence and the routed external-worker payload remains multiplexed.
This evidence is post-v0.21.3 behavior on the reviewed development snapshot, not a claim about stable Hermes v0.21.3. It changes Hermes host/runtime isolation only: no UHP, MCP, ACP or A2A wire contract changed, and it is not evidence of native Hermes UHP adoption or a HarnessRouter conformance result.
The reusable rule is: isolation mode belongs to the routed work item, not only to process startup. Keep the set of names that must be scrubbed separate from the set that a child is permitted to receive, and preserve explicit administrator policy precedence without ambient-secret fallback.
September Hermes case study: served-profile MCP connections need owner scope even when the global multiplex flag is off
Section titled “September Hermes case study: served-profile MCP connections need owner scope even when the global multiplex flag is off”Hermes issue #111151, closed 15 September 2026, documents a stronger failure than a missing tool or failed health check. A long-lived hermes dashboard process served multiple profiles that configured the same MCP server name with different valid Authorization credentials. A session for one profile could silently call the already-connected server as another profile and receive that other account’s data.
The root cause was an ownership-model mismatch. Dashboard/Desktop and per-profile cron work can route a turn by overriding HERMES_HOME without globally enabling gateway.multiplex_profiles. Before the fix, _mcp_registry_scope() treated only the process-wide multiplex flag as evidence that a profile scope existed. With the flag off it returned None, the live MCP connection was keyed only by the bare server name, and later served profiles resolved that process-global connection even when their configuration carried different credentials.
Merged PR #111481 / merge 5117e3b3a09b13f5c7c822e8f82c933fb41a7b6b makes the scoping predicate match the actual serving model. agent.secret_scope.serves_routed_profile() returns true when global multiplexing is active or when the current HERMES_HOME override names a different home from the process home. MCP connection keys then use the served profile’s registry scope in either case. A genuine single-profile process with no override, or an override naming its own home, keeps the prior unscoped key behavior.
Review exposed a second copy of the same bug class: the MCP tool availability check_fn cache still used the old process-wide multiplex predicate. A served profile that correctly could not see the launch profile’s connection could cache False under a process-global key and temporarily hide the launch profile’s live tools for the cache TTL. The reviewed head therefore makes the registry scope and the availability-cache scope use the same serves_routed_profile() predicate. Upstream reports 226 passing MCP/registry/secret-scope tests after that follow-up.
This is again post-v0.21.3 behavior on the reviewed development snapshot. It changes Hermes’ local ownership of MCP client connections and capability-cache entries; it does not change MCP wire semantics, MCP authentication standards, UHP, ACP or A2A, and it is not evidence of native Hermes UHP adoption or HarnessRouter conformance.
The reusable rule is: multiplexing is a property of the served work/context, not just a process startup flag. A scoped resource and every cache or ledger that reflects that resource must derive scope from the same owner predicate, or one profile can still borrow another profile’s connection or verdict even after the primary registry is fixed.
September Hermes case study: eviction and startup hooks must re-enter the owning profile
Section titled “September Hermes case study: eviction and startup hooks must re-enter the owning profile”Merged Hermes PR #111878 closes two remaining owner-scope gaps in the multiplex gateway. The first sits in LRU agent-cache eviction. Eviction is initiated inside the requesting turn, but the agent being evicted can belong to another profile. Before the fix, the presence of any active scope was treated as sufficient, so profile A could evict profile B and run B’s end-of-session memory commit under A’s home and credentials. The reviewed-main implementation instead resolves the owner from the evicted session key first, maps agent:main: to the default root even when the gateway was launched with hermes -p <name>, and enters that owner scope whenever the current scope belongs elsewhere.
The second gap happens before any turn exists. The launch profile’s hooks: block is registered during gateway startup. Under multiplexing, secret resolution deliberately fails closed outside a profile scope, so a hooks.outbound[].secret_env target could raise before registration completed and silently drop the launch profile’s outbound webhook block. PR #111878 now registers that block inside the launch profile’s runtime scope; secondary profiles already used their own scope. Registration failure is also raised to WARNING-level visibility instead of disappearing as debug noise.
These are complementary failures. In the eviction path, an active scope can be the wrong owner’s scope. In the startup path, there may be no request scope yet at all. Both therefore require durable owner resolution at the actual side-effect boundary rather than assuming ambient execution context is authoritative.
The PR reports five invariant tests for the two seams and 721 passing gateway/profile/webhook tests, 0 failed across the broader validation set. Current Hermes main at the checked cutoff contains both _run_release_in_profile_scope() owner-first eviction logic and _register_launch_profile_config_hooks() startup scoping.
This is post-v0.21.3 behavior on the reviewed development snapshot, not a stable-release claim. It changes Hermes runtime ownership for memory finalization and local hook registration only; no UHP, MCP, ACP or A2A wire contract changed, and it is not evidence of native Hermes UHP adoption or HarnessRouter conformance.
The reusable rule is: the owner of the object being finalized can differ from the owner of the request that triggered finalization, and startup work that reads owner-scoped secrets still needs an explicit owner even before the first request. Resolve ownership from the durable object/configuration being acted on, then enter that scope before side effects.
September Hermes case study: the launch profile still needs scope after multiplexing starts
Section titled “September Hermes case study: the launch profile still needs scope after multiplexing starts”Merged Hermes PR #112236 / merge 5910de20bc9839fdd36e791a9d72ba2c2e722f66 closes a control-plane ownership gap in Desktop readiness. Once a gateway is multiplexing, Hermes deliberately makes secret reads fail closed outside an explicit profile scope. The launch/default profile was the exception in _readiness_check: named profiles entered _session_profile_runtime_scope, while the launch profile used no profile scope at all. A launch profile using a scoped value such as HERMES_CODEX_BASE_URL could therefore report setup.runtime_check / setup.status as unhealthy and send Desktop back to onboarding even though real sessions built and ran correctly.
The fix routes the launch profile through the same session-profile runtime-scope helper used for named profiles. The helper binds the launch profile’s frozen .env only when multiplexing is active, so ordinary single-profile processes retain their documented os.environ fallthrough. The upstream A→B→A live probe resolves the launch Codex URL, then profile B’s different .env URL, then the launch URL again while leaving process os.environ untouched. Hermes explicitly rejected a proposed workaround that would swallow UnscopedSecretError and read ambient os.getenv: the fail-closed guard was correct, and weakening it could hide the scoping bug while reading residue from another profile.
PR #112236 also repairs probe/execution parity. A readiness call without an explicit provider now uses the same fallback-aware _resolve_agent_model_runtime(None, None) path used by session construction, so a configured working fallback is not misreported as an unhealthy backend merely because the primary provider lacks credentials. An explicitly requested provider remains strict, because onboarding for one just-connected provider must not be masked by another provider’s fallback. The selected model is now reported on both success and credential failure instead of remaining null.
Upstream reports scripts/run_tests.sh tests/tui_gateway across 163 files: 1,962 passed, 0 failed. The three new regression tests fail on the pre-fix base and pass with the correction. This remains post-v0.21.3 development at the 17 September source cutoff Hermes host/runtime behavior: it changes local readiness scoping and provider-resolution parity, not UHP, MCP, ACP or A2A wire semantics, native Hermes UHP adoption or HarnessRouter conformance.
The reusable rule is: the launch/default profile is still an owner once multiplexing begins. Scope health/readiness probes to the same owner and resolution semantics as the execution path they claim to predict; fix missing ownership scope rather than weakening a fail-closed secret boundary.
September Hermes case study: routed child processes and served-profile requests need destination-owned scope
Section titled “September Hermes case study: routed child processes and served-profile requests need destination-owned scope”Merged Hermes PR #111617 / merge 7abe9502eee17c9d8479d2099a6899f697563bba extends owner isolation beyond the main agent turn into child processes and background continuations. The reviewed work routes slash workers, relay helpers, browser children, A2A profile forwards, credential helpers, title-generation threads, session teardown and bridge work through the served profile’s environment or runtime scope instead of inheriting the launch process by accident.
Review then exposed a subtler child-environment authority gap. A routed child could still inherit a launch-profile provider credential injected by systemd, Compose or the shell because that value had no .env or secret-source provenance for the old name-based scrubber to discover. Follow-up commit 3fe8e5e443d1c30843296679e86d90f335aa1269 changes the rule: when the destination is a routed home, every Tier-1/Tier-2 credential is removed from the launch-derived base regardless of provenance before the destination profile’s own allowed values are overlaid. The same change stops keying the decision only on the global multiplex flag; Desktop/dashboard routing by HERMES_HOME override receives the same isolation even when that flag is off. Browser passthrough also resolves from the bound routed scope only, and an unscoped credential-bearing child under multiplexing fails closed.
Merged Hermes PR #111620 / merge b66e7f2149f26280f1a4cdf5326b02a55fa5ea3a closes the matching request-side boundary. Serving the first non-launch profile now activates fail-closed multi-profile hosting even when the process was not started in global multiplexer mode. Follow-up commit 86097433893b392e5e963430032950ce0902d749 fixes three review edges: an installed secret-manager-composed scope remains authoritative instead of being overwritten by raw .env; a launch-profile request binds its credential source at request entry, so a concurrent first-secondary activation cannot flip that request into an unscoped failure or another profile’s credentials mid-flight; and terminal, secret and home scope tokens are released independently so one cleanup failure cannot leave the other scopes installed.
These changes are post-v0.21.3 Hermes behavior at the September source cutoff. They do not alter UHP, MCP, ACP or A2A wire contracts, do not establish native Hermes UHP adoption and are not HarnessRouter conformance evidence.
The reusable rule is: scope authority follows the destination owner and the request/work item that was admitted, not a mutable process-wide multiplex flag. Rebuild routed child environments from the destination’s authority set, and bind request scope at entry so topology changes during execution cannot silently change credential provenance.
September Hermes case study: boot probes and deploy routing overrides must use the same profile authority
Section titled “September Hermes case study: boot probes and deploy routing overrides must use the same profile authority”Merged Hermes PR #112837 / merge c622dd53c445a04e2ec97cd5724e81c38e41122f closes a startup-only variant of the same isolation problem. Under GATEWAY_MULTIPLEX_PROFILES, boot-time turn-machinery warm-up runs every tool check_fn; the vision path can resolve live Nous credentials and routing overrides. That work previously ran on a bare executor thread with no profile runtime scope, so fail-closed secret resolution made a launch-profile HERMES_PORTAL_BASE_URL or NOUS_INFERENCE_BASE_URL appear absent. On non-production hosted deployments the Portal allowlist could then heal routing to production, where the non-production refresh token was rejected and quarantined before the first inbound turn.
The fix enters the launch profile’s runtime scope for those boot probes and carries the bound context across the executor hop with copy_context().run. The same helper now covers the free-tier bootstrap because it mints Portal identity through the same profile-scoped routing authority. Single-profile gateways retain environment semantics, and the scope is unwound after the boot task. Upstream reports 391 passing focused startup/auth tests plus a live probe in which the pre-fix path produced 17 fail-closed check_fn reads and no cached definitions, while the corrected path produced zero fail-closed reads and populated the launch profile’s caches. The PR explicitly does not claim a full hosted-image boot test.
Merged companion PR #112838 / merge 6cd2502629cf17eeac7c8dda466d04cb72096146 closes the routed-turn half for deploy-injected routing overrides. Under multiplexing, Hermes’ profile secret scope is built from each profile’s .env and intentionally does not fall back to process os.environ; a value injected only into the container therefore remained invisible even after request scoping was correct. The stage-2 boot hook now synchronizes HERMES_PORTAL_BASE_URL, its NOUS_PORTAL_BASE_URL alias and NOUS_INFERENCE_BASE_URL into the launch home and every existing profiles/*/.env. Container-set values replace stale assignments; created files are owner-only; symlinked paths are refused; read-only/read-failure paths warn rather than wiping unrelated secrets or aborting startup.
The lifecycle detail is important. Stage-2-written lines carry a # stage2-managed marker, and a later boot where the platform no longer sets the variable removes only those managed lines. Hand-set operator lines survive an unset boot. This prevents the inverse failure—moving an instance back to production while leaving a staging endpoint pinned in every profile. The known gap remains explicit: a profile created while the container is already running receives the synchronized routing values only on the next boot; upstream describes a restored managed-scope composition as the longer-term home for that policy.
These changes are post-v0.21.3 development at the 17 September source cutoff Hermes deployment/runtime behavior. They do not change Nous Portal protocols, UHP, MCP, ACP or A2A wire semantics, do not establish native Hermes UHP adoption and are not HarnessRouter conformance evidence.
The reusable rule is: startup probes, routed requests and deployment-injected routing policy must resolve through the same owner authority. Carry profile context across executor/thread boundaries, and when a multi-profile secret model intentionally rejects ambient process fallback, explicitly materialize deployment-owned routing inputs into each served profile’s scope with a symmetric cleanup rule when that deployment input disappears.
September Hermes case study: default-on multiplexing is a requested policy, not an admitted topology
Section titled “September Hermes case study: default-on multiplexing is a requested policy, not an admitted topology”Merged Hermes PR #112854 changes the default request for gateway.multiplex_profiles from off to on on reviewed main snapshot, but deliberately does not equate that configured default with permission to consolidate every profile onto one gateway. Commit a10bbf95bb416f2b83f60f84832328d4eeec69c3 keeps an unset GatewayConfig.multiplex_profiles distinguishable as None, then settles it once per boot through the same preflight used by hermes gateway migrate --multiplex. The default is admitted only for the default profile on a migratable host with at least two profiles and without a secondary live gateway/installed unit, duplicate credential or port-binding blocker. If the guard refuses, Hermes logs the reason and starts standalone as before.
Explicit operator choices remain authoritative: config/environment values pass through, and --standalone pins the process off. Other readers no longer infer actual topology from the merged default. They consult the live gateway’s served_profiles record first and the explicit flag second, so a host that requested the new default but failed admission is still treated as not yet multiplexed rather than as a multiplexer merely because DEFAULT_CONFIG says true.
Commit 0a1716cb27a3db8eb8b52591b5ad3ce95de61875 makes that fail-safe state observable. A guard refusal is persisted as multiplex_standalone_reason in gateway_state.json; hermes gateway status prints the reason plus both remediation paths — migrate/fold the fleet or explicitly keep per-profile gateways — and a later boot that successfully multiplexes clears the stale reason. PR #112854 reports seven focused mode invariants, a live temporary-home probe, 4,926 passing tests across 250 test files mentioning multiplex, and a separate 98-test status/phase0/new-file set.
This is post-v0.21.3 development at the 17 September source cutoff Hermes host/runtime behavior. It does not change UHP, MCP, ACP or A2A wire semantics, does not establish native Hermes UHP adoption and is not HarnessRouter conformance evidence.
The reusable rule is: configuration defaults express desired policy; live topology is an admitted runtime fact. A default that consolidates owner scopes should cross a fail-safe admission guard, preserve explicit rollback/override semantics, and publish the actual verdict so other processes and operators do not confuse intent with state.
September Hermes case study: MCP teardown must distinguish the launch owner from a global wildcard
Section titled “September Hermes case study: MCP teardown must distinguish the launch owner from a global wildcard”Merged Hermes PR #112885 / merge dd566d52aa009facd38f5847da60f3877e359cce closes a teardown-side ownership gap left after live MCP connections became profile-scoped. When the launch profile removed or disabled MCP server x, reconciliation called shutdown_mcp_servers(scope=None, names={"x"}). Because the launch owner is represented by the unscoped None registry key, the old implementation incorrectly interpreted that None as “every owner” and could close a served profile’s same-named (profile, "x") connection, drop its tools and skip the orphaned-adopter bookkeeping used to restore shared tool overlays.
The fix makes only the completely bare shutdown_mcp_servers() call the process-wide wildcard. A call with names now treats scope=None as the launch/unscoped owner and leaves same-named served-profile connections intact. Process-exit, TUI/Desktop full-reload and other deliberately bare shutdown callers remain process-wide. The regression proves that pruning launch x closes only the launch connection, preserves profile B’s (B, "x"), and records another adopter so the next discovery pass can re-register it. Upstream reports 177 tests green across the 13 files referencing shutdown/orphan-adopter lifecycle state, plus a live selector probe with the same owner-preservation result.
This is post-v0.21.3 Hermes behavior at the September source cutoff. It changes local MCP client lifecycle ownership, not MCP transport, authentication or server semantics, UHP, ACP or A2A; it is not evidence of native Hermes UHP adoption or HarnessRouter conformance.
The reusable rule is: an unset/default owner key and an all-owners selector are different concepts. Model wildcard teardown explicitly; otherwise cleanup for the launch/default owner can accidentally become cross-owner cleanup even when normal lookup keys are correctly isolated.
September Hermes case study: off-turn prompt rebuilds and config inspection must re-enter the session owner
Section titled “September Hermes case study: off-turn prompt rebuilds and config inspection must re-enter the session owner”Merged Hermes PR #113142 / merge 80ab36e4a908851c9e5fe802b130b0b810ffcfa5 closes three owner-scope gaps that occur outside the main model turn. session.context_breakdown, which Desktop refetches after every turn, can rebuild the external-memory system-prompt block. Once a backend served a second profile, that rebuild previously ran without a secret scope: the launch profile could hit UnscopedSecretError, while a secondary profile could resolve the launch profile’s memory-provider credential and home.
The fix runs the breakdown under _session_profile_runtime_scope(session), and _persist_live_session_system_prompt now uses the same full profile runtime scope when model-switch or one-turn-restore logic rebuilds and persists the prompt. config.show also moves from an unscoped RPC to the profile-scoped RPC path, resolves HERMES_BASE_URL through the scoped secret resolver and reports the active profile’s config path instead of the process launch path. This keeps read-only status/config surfaces aligned with the same owner authority as the turn they describe.
Upstream’s A→B→A regressions verify that launch and secondary sessions each rebuild against their own home and memory-provider key, and that config.show returns each profile’s own API key/base URL/config path. The PR reports 1,995 passing TUI-gateway tests plus four timing-sensitive failures in three unrelated files; upstream states all 21 tests in those files pass when run alone. That bounded result is evidence for the changed paths, not a claim that the broad suite was globally zero-failure.
This is post-v0.21.3 Hermes behavior at the September source cutoff. It changes Hermes’ local TUI/Desktop runtime scoping only; it does not change UHP, MCP, ACP or A2A wire semantics, establish native Hermes UHP adoption, or add HarnessRouter conformance evidence.
The reusable rule is: observability, status and derived-state rebuilds are still owner-sensitive execution. A read-only RPC or post-turn prompt recomputation that can resolve secrets, home paths or provider state must re-enter the durable session owner instead of assuming the completed turn’s scope is still active.
September Hermes case study: background thread hops must preserve routed context
Section titled “September Hermes case study: background thread hops must preserve routed context”Merged Hermes PR #113063 / merge 26e9205653802d19906fedf3a4bc6a5ea1f3c1f5 closes a different off-turn failure mode: a routed profile can be correctly scoped on the caller thread and still lose that ownership when work crosses a bare threading.Thread or ThreadPoolExecutor boundary, because Python starts the worker with an empty contextvars.Context rather than inheriting the caller’s profile override.
The fix captures the caller context and runs four background classes inside it: gateway abandoned-turn/disconnect reapers, async-delegation stale-monitor finalization, bounded Nous account/billing fetches and periodic-scheduler callbacks/watchdogs. The affected code now uses copy_context().run at the thread/executor seam, so get_hermes_home() and secret scope stay bound to the routed profile instead of falling back to the launch home. For async delegation this also means a stale completion updates the dispatching profile’s state.db rather than silently writing or failing against the launch profile.
Upstream targeted validation reports 67 passed, 0 failed, 1 skipped across the new regressions and adjacent modules. The broader tests/agent tests/tools tests/gateway run reports 28,441 passed, 6 failed, 168 skipped; the six failures reproduced on the base environment or passed in isolation and are documented as missing optional dependencies, a host-binary assumption or load-sensitive timing, not changed-path failures. A live E2E probe exercises the real periodic scheduler, reaper, account fetch and stale-monitor SQLite path and changes all observed homes/state updates from launch-profile behavior on the base to the routed profile on the merged branch.
This is post-v0.21.3 Hermes behavior at the September source cutoff. It changes local background execution context propagation only; it does not change UHP, MCP, ACP or A2A wire semantics, establish native Hermes UHP adoption, or add HarnessRouter conformance evidence.
The reusable rule is: ContextVar-based ownership is only as durable as the boundaries that propagate it. Every raw thread, executor submission, timer or scheduler callback that can reach owner-scoped state must capture the caller’s context or reconstruct the owner from durable task/session identity before it runs.
September Hermes case study: restored chats may reclaim only their own detached lease
Section titled “September Hermes case study: restored chats may reclaim only their own detached lease”Merged Hermes PR #113947 / merge 251e5f7c0a067239655f3421812ab7492a206065 closes a session-ownership failure that appears after a Desktop renderer crash or reload. A chat restored under a new runtime in the same backend process could still be refused with SESSION_NOT_OWNED because Hermes lease identity includes both process id and live_session_id. The old runtime had lost its client but still held the durable session lease, while the restored runtime had a new live_session_id; upstream reports that the old owner could otherwise survive until the orphan/reaper path released it roughly 11 minutes later in the worst case described by the PR.
The correction allows a refusal to become a lease transfer only under a narrow recovery predicate. Hermes must find an in-process sibling runtime with the same session key, the same profile home, an unreleased lease and a dead transport. transfer_active_session then rewrites the registry metadata to the new live_session_id, moves the lease to the restored runtime and requests interruption of the detached old turn. A live foreign process, an attached sibling such as a genuine second window, or a same-key runtime from another profile remains refused.
The profile guard is not incidental. Review follow-up 8039bb1e619e81646c3c1a522e93b9140418a033 records that timestamp-derived session ids can collide across profile stores; matching the session key alone could therefore let profile B reclaim a detached lease belonging to profile A. The final implementation uses _live_profile_matches in the sibling selector, preserving cross-profile exclusivity even while same-profile crash recovery becomes faster.
The old runtime also loses finalization authority once the lease moves. It is marked _lease_taken_over, so later automatic cleanup cannot end the durable row now driven by the new runtime, interrupt its key-wide delegations or unregister the approval callback the new owner relies on. The final refactor clears that mark if a runtime later legitimately re-owns the lease, preventing the handoff state from becoming sticky.
PR #113947 reports the new same-process takeover regression changing from refusal on the pre-fix base to ownership by the restored runtime, while foreign-process and attached-sibling cases remain refused. Its touched-path run reports 715 passed and 1 unrelated failure that reproduced on the untouched base. The merged follow-ups add the same-profile guard and finalization ownership protections described above.
This is post-v0.21.3 development at the 17 September source cutoff Hermes host/runtime behavior, not a stable-release claim. It does not change UHP, MCP, ACP or A2A wire semantics, does not establish native Hermes UHP adoption and is not HarnessRouter conformance evidence.
The reusable rule is: liveness failure is not identity equivalence. A system may reclaim a dead writer’s lease only after matching every durable owner coordinate relevant to that resource, and once ownership transfers the old writer’s finalizer must no longer be able to destroy the new owner’s state.
September Qwen Code case study: ACP session/worktree ownership
Section titled “September Qwen Code case study: ACP session/worktree ownership”Qwen Code issue #8138 documents the same class of ownership error with a different identity model. A Git worktree session could save a workspace-scoped setting into the daemon’s bootstrap/project-root .qwen/settings.json because the ACP settings handler fell back to process cwd rather than the requesting session’s durable worktree coordinate.
Merged PR #11647 / commit 32a25efa6e1a02f6cc61a5820982fd76e8f0c4e3 hardens that local ACP control plane in several related places:
- core settings, memory and permissions resolve from the requesting session’s stable storage/workspace root rather than the daemon bootstrap directory or a live subdirectory reached through
session/cd; - an explicit
cwdstill wins, while malformed or unknownsessionIdvalues fail explicitly instead of silently retargeting the bootstrap workspace; - request-scoped settings loads set
skipLoadEnvironment, preventing a foreign workspace’s.envfrom being injected into process-global environment state; - a session-scoped read does not repoint the daemon-global settings cache;
- workspace-scoped permission changes fan out only to live sessions bound to that canonical workspace, including equivalent symlink spellings resolved through the real path; and
- MCP, hook and extension writes deliberately remain workspace-global because their status/reload/apply routes are workspace-global. Making only the write side session-scoped would acknowledge a value as “saved” at a coordinate that no corresponding reader or apply path uses.
That last point is important: isolation does not mean “make everything session-scoped.” The ownership coordinate of write, read-back, status and apply paths must agree. A narrower write scope paired with a broader apply scope can create split-brain configuration just as surely as an ambient default can.
The evidence boundary is also explicit. PR #11647 reports focused Linux ACP settings tests, while macOS and Windows were not locally exercised and the reviewer notes say the real enter_worktree end-to-end producer-to-consumer flow was not demonstrated. Issue #8138 remains open. This is therefore merged current-main source evidence of stronger ownership routing, not a claim that every worktree path is fully verified or that stable Qwen Code v0.23.3 contains the fix.
This extends the stable Hermes isolation campaign
Section titled “This extends the stable Hermes isolation campaign”Hermes stable v2026.9.14 / v0.21.3 includes the earlier broad multi-profile isolation campaign plus the same-day source-ownership and generated-contract fixes documented above. Its release notes explicitly retain multiplexed-profile isolation among the patch-window themes. For profile-owned credential storage specifically, stable #110024/#110941 still implement provenance-aware rotation of an implicitly borrowed root grant; reviewed main snapshot supersedes that fallback model with 93889b77, so named profiles no longer borrow the root profile’s profile-owned single-use OAuth grants. PR #114162 / c712f06d clarifies the non-universal boundary: static keys can be copied, Anthropic/Codex/xAI single-use grants require profile sign-in, and Nous Portal remains intentionally backed by a shared root-level auth store.
PRs #111187, #111481, #111617, #111620, #111878, #112236, #112837, #112838, #112854, #112885, #113142, #113063, #113428, #113947 and #114162 are post-release current-main continuations of that campaign, and commit 93889b77 adds the profile-owned credential-store boundary. PR #111187 adds routed-cron/external-worker task-local multiplex semantics, launch-residue scrubbing and managed-secret precedence. PR #111481 extends the same ownership model to live MCP connection keys and the capability cache on Desktop/dashboard and per-profile cron surfaces even when the host process was not globally started as a multiplexer. PR #111617 extends it across routed child processes and background continuations, with current-main hardening that scrubs launch credentials regardless of provenance before applying the destination profile. PR #111620 makes multi-profile serving itself activate fail-closed secret policy and fixes request-entry authority, secret-source precedence and scope teardown. PR #111878 extends the model to cross-profile LRU eviction/finalization and launch-profile webhook startup registration. PR #112236 closes the launch/default-profile readiness gap once multiplexing is active and makes default readiness provider resolution agree with actual session fallback behavior while keeping explicit-provider probes strict. PR #112837 binds boot warm-up and free-tier bootstrap work to the launch profile across executor hops. PR #112838 materializes deploy-owned Nous routing overrides into every existing served profile’s secret scope and removes only stage2-managed lines when the platform later unsets them. PR #112854 changes multiplexing from opt-in default to default-requested behavior while keeping topology admission behind the migration-grade boot guard and surfacing guard refusal in runtime status. PR #112885 makes named MCP teardown owner-selective so launch-profile pruning cannot close another served profile’s same-named connection. PR #113142 extends the same scope discipline to off-turn system-prompt rebuilds and profile config/status inspection. PR #113063 propagates routed ownership across raw thread, executor and scheduler boundaries used by reapers, stale delegation finalization, account fetches and periodic callbacks. PR #113428 binds the process-global dashboard authentication provider to the process launch home so later profile-scoped plugin discovery cannot replace the login gate. PR #113947 makes same-process restored-chat lease recovery require the same durable profile owner and strips finalization authority from the detached runtime after transfer. PR #114162 corrects the credential-isolation documentation boundary so the explicit shared Nous authority is not misreported as profile-local. Checked current head c712f06d includes those ownership changes.
The Hermes stable and current-main fixes plus the Qwen current-main ACP settings work expose the same durable architectural lesson across different harnesses: isolation has to cover control-plane RPCs, configuration ownership, credential provenance and explicit credential ownership, deliberately shared authority, process-global ingress authentication ownership, child-process environments, background threads and executor hops, boot probes, deployment-injected routing policy, topology admission and observability, live connection identity, capability caches, owner-specific teardown selectors, readiness probes, off-turn prompt/config introspection, closed wire contracts, deferred work execution, cross-owner finalization, crash/reload lease recovery, startup registration and delivery continuations, not only the main agent turn.
Engineering rules that generalize
Section titled “Engineering rules that generalize”- Use an explicit owner identity. A profile name, tenant id, workspace id, session id or principal id must travel with the work item; do not reconstruct ownership from whichever global context happens to be active later.
- Re-enter scope at deferred boundaries. Queues, callbacks, RPC handlers, background jobs, delivery workers and resume paths are common places where request-local context has already been reset.
- Fail closed for secrets and invalid owners. If multiplexing is enabled and no valid owner scope is active, refusing resolution is safer than falling back to process environment state, bootstrap workspace state or a default profile.
- Keep ambient state read-only where possible. Avoid mutating
os.environ, process-wide settings caches or globalcwdto emulate owner switching inside a shared process; scoped resolvers are easier to reason about and test. - Key caches by every ownership dimension they depend on. Provider/model alone is insufficient when credentials, endpoints, tool policy, workspace or sandbox identity vary by owner.
- Validate the delivery path, not only execution. A tool can create the correct file and still leak or lose it during later path translation, artifact collection or message delivery.
- Keep scope consistent across the control plane. A write, its status/read-back path and the code that applies it must resolve the same ownership coordinate. Session-scoping only one side can create state that is acknowledged but never applied.
- Make credential ownership explicit. “No local credential, so use root/default” is not an isolation policy. A named owner without local profile-owned authentication should fail closed or require an explicit mirror/copy/sign-in operation. If a system intentionally models one shared credential authority, represent that owner explicitly—as Hermes does for Nous—instead of inferring it from missing state.
- Serialize irreversible rotations at the actual owner boundary. For single-use credentials or other externally rotated capabilities that are intentionally shared, transaction scope must include the authoritative re-read, external rotation request and durable write-back. Locking only the final persistence step still permits duplicate side effects. Current Hermes no longer implicitly shares root
auth.jsongrants for Anthropic, OpenAI Codex or xAI named profiles; its Nous authority is separately and deliberately shared. - Close ownership-bearing wire objects. Unknown client keys should fail explicitly instead of being silently ignored, and owner/session transport artifacts should be derived by the trusted runtime rather than accepted as a parallel client override channel.
- Generate cross-language wire types from one contract source. If backend and frontend independently own the same request/event shape, drift can erase or invent identity-bearing fields without either side noticing locally.
- Test with deliberately conflicting valid state. Give two owners different valid tokens, settings and same-named files. A test that only checks for success can miss cross-owner substitution.
- Exercise scope exit explicitly. Regression tests should run the deferred phase after the original owner context has ended; otherwise they may accidentally prove only the easy in-scope path.
- Canonicalize identity-bearing paths without inventing equivalence. Resolve symlink spellings when they refer to the same workspace, but do not lowercase paths on a case-sensitive filesystem and accidentally merge distinct owners.
- Carry isolation mode with deferred work. A routed job can require multiplex protections even when the host process was not globally started as a multiplexer; scope that mode to the work item and its handoffs.
- Separate scrub provenance from forwarding authority. Names that must be removed from inherited launch state can be broader than names a child is allowed to receive. Keep those sets distinct and layer administrator-managed policy deliberately.
- Use one owner predicate for the resource and its caches. If a connection registry is profile-scoped but its availability, status or memoization cache is process-scoped, the cache can reintroduce cross-owner behavior or hide another owner’s resource.
- Resolve the owner of the finalized object, not the trigger. A request in profile A can evict, close or finalize an object owned by profile B. The trigger’s active scope is not proof of ownership; derive owner identity from the durable object/session key before committing memory or other state.
- Scope startup initialization explicitly. Registration, reconciliation or secret-dependent startup work runs before normal request context exists. Enter the launch/configuration owner’s scope instead of borrowing process environment state or silently dropping work when fail-closed secret resolution fires.
- Treat the launch/default profile as an explicit owner under multiplexing. Once fail-closed multiplex policy is active, a launch-profile probe cannot safely use null/ambient scope. Enter that profile’s runtime scope, and make a default readiness check use the same provider/fallback semantics as the execution path it claims to predict while keeping explicit-provider checks intentionally strict.
- Rebuild routed child environments from destination authority. A child for profile B must not begin with launch-profile credentials simply because those values came from systemd, Compose or the shell and escaped provenance tracking. Scrub credential-bearing launch state first, then overlay only B’s authorized values; use the routed destination, not only a global multiplex flag, as the authority predicate.
- Bind owner scope at request entry and unwind every layer independently. If the host becomes multi-profile while a request is already running, that request’s credential source must not change mid-flight. Scope teardown should release terminal, secret and home layers even if one reset fails, then surface the failure after cleanup.
- Carry launch-profile scope into boot-time executor work. A startup health/check pass that runs owner-sensitive code on another thread must propagate the launch owner’s runtime context rather than treating “before the first request” as permission to use ambient state.
- Materialize deployment-owned routing policy into the authority model that runtime reads. If multiplexed secret resolution intentionally refuses
os.environ, a container/platform routing override must be synchronized into each served profile’s scoped configuration; mark deploy-managed state so an unset deployment value can be removed without deleting operator-owned lines. - Separate desired multiplex policy from admitted live topology. A default-on setting should not make other processes assume consolidation succeeded. Run topology/credential/port admission first, preserve explicit operator overrides, then publish the live served-owner set and any refusal reason as the authoritative runtime fact.
- Model wildcard teardown separately from the default owner. If the launch/default owner is represented by
None, null, or an unscoped key, do not reuse that sentinel to mean “all owners.” Require an explicit wildcard mode so owner-specific pruning cannot become fleet-wide teardown. - Scope observability and derived-state rebuilds. Status, context-breakdown and config-inspection paths can resolve secrets, home paths or cached prompt state even when they do not mutate user data. Re-enter the durable session/profile owner before those reads and rebuilds instead of assuming the completed turn left a usable scope behind.
- Propagate owner context across raw concurrency boundaries. A scoped caller does not imply a scoped
threading.Thread, executor job, timer or scheduler callback. Capture the caller’sContextor carry durable owner identity explicitly at each hop before background work touches owner-scoped state. - Give process-global security gates one durable owner. A per-profile plugin or configuration discovery pass must not replace a process-wide authentication/authorization provider merely because it runs later. Bind global registration and rotation to the process launch home or another explicit global owner.
- Reclaim dead ownership only within the same durable owner coordinate. A dead transport or stale runtime proves liveness failure, not identity equivalence. Require the same session key plus the same profile, tenant or workspace owner before transferring a lease, and after transfer prevent the old runtime’s cleanup from ending the new owner’s row, delegations or callbacks.
A useful negative-test matrix
Section titled “A useful negative-test matrix”A multiplexed harness should be able to demonstrate at least these cases:
- secondary profile secret exists, default process secret is different → secondary value wins;
- secondary secret is absent, default process secret exists → operation fails closed rather than borrowing it;
- named profile has no local profile-owned provider/auth state for Anthropic, OpenAI Codex or xAI while root is fully authenticated → the named profile gets an explicit no-provider/configuration result and never reads or rotates the root profile’s corresponding
auth.jsongrant; - static API keys are explicitly mirrored/copied into a named profile → copied key material is present in that profile, while Anthropic/Codex/xAI single-use OAuth rows are absent and require profile sign-in;
- Nous Portal is authenticated in the shared store → a named profile may resolve that shared Nous authority by design, and a Nous refresh updates the explicit shared store rather than being misclassified as a profile-local refresh;
- root/default profile refreshes its own profile-owned rotating credential → root still refreshes normally without requiring or mutating any named profile store;
- launch home dashboard auth accepts password P1 while secondary profile B contains stale password P0 → profile-scoped plugin discovery for B cannot replace the global provider, P1 remains accepted and P0 remains rejected; launching the process directly into B may legitimately make B the dashboard-auth owner;
- launch process has an ambient-only provider credential that is absent from routed profile B → B’s child and browser helper do not receive it even when the global multiplex flag is off, while a child for the launch profile may retain its own credential;
- a launch-profile request enters while the host is still single-profile, then profile B activates multi-profile hosting concurrently → the launch request keeps its entry-time launch credential source and never resolves B’s secrets;
- one runtime-scope reset fails during teardown → the other profile home/secret/terminal scopes are still released before the failure is surfaced;
- multiplexed startup warm-up or free-tier bootstrap resolves a launch-profile routing override inside an executor thread → the launch value is visible there, no ambient/default fallback is used, and the scope is absent again after the boot task completes;
- container/platform sets a non-production Portal/inference routing override while runtime reads only profile scopes → launch and existing secondary profiles resolve that value; a later boot with the deployment value unset removes only stage2-managed assignments and leaves hand-set lines intact;
gateway.multiplex_profilesis unset on a quiet migratable host with at least two profiles → the default request is admitted and the live gateway records the actual served-profile set;- the same unset/default-on host has a secondary live gateway, installed unit, duplicate credential or port blocker → the boot guard leaves it standalone, records a human-readable refusal reason, and other processes do not infer multiplexing from the default alone;
- operator explicitly pins multiplex true/false or uses
--standalone→ the explicit choice is not silently rewritten by the default-on migration path; - both profiles contain the same relative filename with different bytes → owner profile bytes are delivered;
- only default profile contains the requested file → secondary delivery rejects it;
- background/deferred handler runs after ingress context exits → owner identity is still recoverable from durable task/session metadata;
- non-global-multiplex process fires a sibling-profile cron job → routed work receives multiplex secret semantics and does not inherit launch-only secrets;
- global multiplex flag is off, but one long-lived process serves two profile homes with the same MCP server name and different valid credentials → each profile resolves its own connection and account data;
- served profile B correctly cannot see launch profile A’s live MCP connection → B’s cached negative availability verdict cannot hide A’s tool when execution returns to A;
- launch profile removes MCP server
xwhile served profile B has its own same-named connection → only the launch connection closes, B’s(B, "x")survives, and shared adopters are retained for re-registration; - after multiplex activation, launch and secondary sessions run post-turn context-breakdown/prompt rebuild plus
config.showin A→B→A order → each resolves its own home, scoped credentials and config path, with no ambient-owner substitution; - a routed profile spawns an abandoned-turn reaper, bounded account fetch, stale-delegation finalizer or periodic callback through a raw thread/executor seam → the worker still sees the routed home/scope and never falls back to launch-profile state;
- profile A’s turn evicts a cached agent owned by profile B → B’s end-of-session memory commit runs under B’s home and credentials rather than A’s active scope;
- gateway launched under a named profile evicts an
agent:main:session → the default/root profile remains the owner instead of the launch profile; - multiplexed gateway startup registers launch-profile signed and unsigned outbound webhooks → registration runs under the launch profile and
secret_envresolves there without dropping the block; - multiplexed launch profile uses a scoped Codex base URL → readiness succeeds under the launch owner; probing profile B returns B’s URL; returning to launch restores the launch URL without mutating process
os.environ; - default readiness has an unavailable primary plus a complete configured fallback → readiness reports the same fallback provider/model that session creation would use, while an explicit primary-provider probe remains strict and fails;
- managed-only key exists and a user/profile value collides with it → managed policy remains present and wins by the documented precedence rule;
- external source supplies a name that existing process state caused it to skip → the name is still scrubbed from routed child inheritance but is not forwarded to MCP stdio merely because the source supplied it;
- cache warmed by owner A → owner B resolves its own client/credential/policy rather than reusing A’s owner-sensitive object;
- unknown or malformed session id plus a valid bootstrap workspace → explicit error, no fallback read/write;
- session
cwdmoves into a subdirectory → workspace-scoped settings still resolve to the stable admitted workspace root; - one workspace is addressed through canonical and symlink spellings → permission fan-out reaches the same owner scope without crossing into a genuinely different path;
- request-scoped read touches a foreign workspace containing
.env→ process-global environment remains unchanged; - resource status/apply surface is workspace-global → write path cannot claim session-local success at an unreachable coordinate;
- forged or misspelled ownership-bearing RPC key → explicit schema error and no fallback/steer side effect;
- backend removes or changes an identity-bearing field → generated client types or staleness/completeness validation fails before publication;
- restored runtime has the same session key and profile as a detached in-process holder with a dead transport → the lease moves to the restored runtime and the old turn is interrupted; the same key in another profile, a live foreign process or an attached second window remains refused;
- restart/resume under a shared gateway → persisted owner mapping remains authoritative; and
- invalid or deleted profile → work fails explicitly rather than falling back to the default profile.
Relation to UHP, MCP, A2A and ACP
Section titled “Relation to UHP, MCP, A2A and ACP”This page describes a harness-runtime isolation pattern, not a new interoperability protocol.
- UHP: Hermes profiles and Qwen worktrees are not UHP objects. UHP standardizes a client↔server↔selected-harness execution contract; an implementation that multiplexes several internal owner scopes still has to enforce its own ownership and data-isolation rules.
- MCP: MCP can be one of the resources whose configuration, credentials, live client connections, teardown selectors and local availability caches are owner-scoped. MCP wire semantics do not determine which local profile’s vault, environment, connection registry or process state a host implementation should consult. Hermes PR #111187 changes local child-environment construction and secret provenance; PR #111481 changes local MCP connection/cache ownership; PR #111617 extends the same destination-owner rule to routed child environments; PR #112885 changes local MCP teardown ownership. None changes MCP transport, authentication or server semantics.
- ACP: an ACP
sessionIdcan be an input to local ownership resolution, but ACP does not define Qwen’s.qwen/settings.json, worktree storage roots, daemon process environment or which local settings surfaces are session-scoped versus workspace-global. PR #11647 changes Qwen’s ACP host implementation, not ACP wire semantics. - A2A: A2A may carry work to or from an agent runtime, but it does not make Hermes-style profile homes, Qwen worktrees, Docker mounts or local credential scopes interchangeable. Hermes PR #111617 hardens Hermes’ local environment around A2A profile forwarding; it does not change the A2A protocol.
Hermes PRs #110677/#110747 concern Hermes’ own TUI/Desktop JSON-RPC gateway. The generated OpenRPC artifact describes that gateway; it is not ACP, MCP, A2A or UHP and does not create a standardized UHP↔Hermes gateway binding. Hermes PRs #110024/#110941 concern the stable v0.21.3 borrowed-root provider-auth rotation model; current-main commit 93889b77 supersedes that implicit cross-profile fallback for profile-owned auth by making named profiles stop borrowing root auth.json. PR #114162 / c712f06d clarifies that Nous Portal is intentionally outside that blanket statement because its canonical credential owner is the shared root-level Nous store, while Anthropic, OpenAI Codex and xAI remain the single-use OAuth providers that are not copied between profiles. None changes OpenAI OAuth, ACP, MCP, A2A or UHP wire contracts. Hermes PR #111187 concerns routed cron/external-worker secret scope and environment construction. Hermes PR #111481 concerns owner-scoped MCP client connections and capability-cache keys on served-profile surfaces. Hermes PR #111617 concerns routed child-process and deferred/background scope. Hermes PR #111620 concerns fail-closed multi-profile hosting, request-entry secret authority and scope teardown. Hermes PR #111878 concerns cross-profile memory finalization and launch-profile hook registration. Hermes PR #112236 concerns launch-profile readiness scope and fallback-aware provider/model probing. Hermes PR #112837 concerns boot-time launch-profile scope for warm-up/free-tier probes; PR #112838 concerns deployment-injected Nous routing overrides reaching each profile’s secret scope with lifecycle cleanup. PR #112854 concerns Hermes’ local gateway-topology default, boot-time admission and status observability. PR #112885 concerns local MCP connection teardown selection and adopter bookkeeping. PR #113142 concerns off-turn prompt/context rebuilds and profile-scoped config/status inspection. PR #113063 concerns local background thread/executor context propagation for routed-profile work. PR #113428 concerns process-global dashboard-auth provider ownership and prevents profile-local plugin discovery from replacing the launch home’s login gate. PR #113947 concerns local same-process session-lease recovery, profile-bound writer identity and post-transfer finalization ownership. None alters those protocol contracts.
Nothing in the cited fixes establishes native Hermes or Qwen UHP adoption, changes MCP/A2A/ACP wire semantics, or constitutes new HarnessRouter conformance evidence. HarnessRouter’s Hermes and Qwen backends remain separate adapter/reference-implementation boundaries.
Related reading
Section titled “Related reading”Follow routing identity for ownership resolution, durable state repair for historical contamination, and session recovery for safe continuation.
Primary sources
Section titled “Primary sources”- Hermes current repository
- Hermes
v2026.9.14/v0.21.3release - Hermes v0.21.3 tagged commit —
345cd2b0 - Issue #109901 — Desktop MCP health probe lacks secondary-profile secret scope
- Commit
0388d03f— profile-scoped RPC binds home, secrets and terminal policy - Hermes issue #87503 — profile refresh leaves the root Codex grant stale
- Hermes PR #110024 — stable borrowed-root Codex rotation writes back to root
- Hermes merge
66ddd5f8 - Hermes PR #110941 — stable borrowed-root refresh serialization
- Hermes merge
ca6b189a - Issue #109024 — named-profile Docker MEDIA resolves against ambient default profile
- Commit
28138f25— bind post-turn media delivery to the source profile - Hermes PR #110677 — real server→client JSON-RPC request lifecycle
- Hermes PR #110747 — generated gateway contracts and OpenRPC
- Hermes PR #111187 — routed cron fires use task-local multiplex secret semantics
- Hermes merge
cedf4a3d - Hermes issue #111151 — served profile can call MCP as another profile
- Hermes PR #111481 — served profiles keep their own MCP connections and credentials
- Hermes merge
5117e3b3 - Hermes PR #111617 — served-profile child/background ownership
- Hermes merge
7abe9502 - Hermes follow-up
3fe8e5e4— routed children scrub launch credentials regardless of provenance - Hermes PR #111620 — fail-closed multi-profile serving
- Hermes merge
b66e7f21 - Hermes follow-up
86097433— request-entry authority and independent scope teardown - Hermes PR #111878 — owner-scoped cache eviction and launch-profile hook registration
- Hermes PR #112236 — launch-profile readiness scope and fallback parity
- Hermes merge
5910de20 - Hermes PR #112837 — launch-scope boot warm-up and free-tier bootstrap
- Hermes merge
c622dd53 - Hermes PR #112838 — sync deploy routing overrides into every existing profile scope
- Hermes PR #112854 — default-on multiplex request with guarded admission and visible standalone reason
- Hermes commit
a10bbf95— default-on multiplex request and boot-time serve guard - Hermes commit
0a1716cb— persist and surface guarded-standalone reason - Hermes PR #112885 — launch-profile MCP pruning preserves served-profile connections
- Hermes merge
dd566d52 - Hermes PR #113142 — off-turn prompt/config paths re-enter the requested profile
- Hermes merge
80ab36e4 - Hermes PR #113063 — background thread hops preserve routed profile context
- Hermes merge
26e92056 - Hermes commit
93889b77— named profiles no longer inherit rootauth.json - Hermes PR #113428 — profile plugin discovery cannot replace process-global dashboard auth
- Hermes commit
d93f74b7— bind dashboard-auth provider registration to launch home - Hermes PR #113947 — restored chat takes over only its own detached same-process lease
- Hermes takeover implementation
3ca367ad - Hermes profile/finalization hardening
8039bb1e - Hermes PR #113947 merge/refactor
251e5f7c - Hermes PR #114162 — scope named-profile credential isolation and Nous sharing
- Hermes PR #114162 merge/current-main cutoff
c712f06d - Hermes shared Nous auth store at
c712f06d - Hermes single-use OAuth grant hygiene at
c712f06d - Qwen Code issue #8138 — worktree settings write to project root
- Qwen Code PR #11647 — resolve ACP settings against the owner workspace
- Qwen Code merge
32a25efa - Checked Qwen Code source cutoff
85631a3d - UHP / HarnessRouter source
This is an independent technical guide. The isolation rules above are engineering conclusions from the cited implementation evidence, not vendor certification or a new UHP requirement.