Repository navigation
Add external auth mode that trusts the gateway actor header - #482
Open
alex-clickhouse wants to merge 2 commits into
Open
alex-clickhouse wants to merge 2 commits into
alex-clickhouse wants to merge 2 commits into
Conversation
alex-clickhouse
added this pull request to stack #407
October 5, 2026 07:30
alex-clickhouse
marked this pull request as ready for review
October 5, 2026 09:21
A hosted Nerve runs behind a gateway that signs people in and names the person behind each request in the X-Nerve-Actor-Context header. Local accounts, login and session tokens do not apply there. NERVE_AUTH_MODE selects the mode: local (default) or external. create_app() reads it once and pins it, so a configuration reload cannot change it. An unknown value stops the server and `nerve start`. In external mode: - require_auth and the WebSocket upgrade decode the header payload in one function, decode_actor_context(), and act as the person it names. The signature is not checked. A later change can replace that one function and keep its callers. - The actor_refs row is added on first sight, and the display name is written when it changes. An in-process map skips the write when the name is the same, so a normal request takes no write lock. A header that names the system actor gives 401. - Session tokens are refused on REST, /ws, the MCP endpoint and the worker-token route. System and MCP tokens give the system actor. - The account and setup routes are not registered, login answers 404, and GET /api/auth/status reports the mode without login state. - Startup creates no account, keeps the signing secret and deletes a setup token from local mode. nerve doctor does not warn about a missing local account. Local mode keeps its behavior. Its status responses add the mode field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the external mode found these gaps: - A display name with a lone UTF-16 surrogate is valid JSON, but SQLite cannot store it. The upsert raised UnicodeEncodeError, so REST gave 500 and /ws did not get its 4001 close. decode_actor_context() now refuses such a name, so both paths give the normal refusal. - A request with the actor context header twice used the first value. REST and the WebSocket upgrade now read every value and refuse a request that has more than one. - nerve restart stopped the running daemon before the new daemon read NERVE_AUTH_MODE. It now checks the value first, as nerve start does. - nerve doctor showed the mode only in external mode. It now always shows the mode, and says that it reads NERVE_AUTH_MODE in its own shell, not from the running server. The setup-token startup test seeds an unclaimed local account, so that it fails without the external-mode guard. New tests cover the surrogate name and repeated headers on REST and /ws, the header alone on /ws in local mode, and nerve restart with an unknown mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alex-clickhouse
force-pushed
the
alex/external-mode-01-auth
branch
from
October 5, 2026 09:31
548e7db to
2309c11
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
This adds an external authentication mode. With
NERVE_AUTH_MODE=external, a gateway in front of Nerve signs people in. The gateway names the person behind each request in theX-Nerve-Actor-Contextheader, and Nerve acts as that person. Local mode (the default) keeps its behavior.Stacked on #467. The hosted channel in #465 already verifies gateway-signed tokens (
nerve/channels/hosted/auth.py). A later signature check for the actor header can reuse that key handling. This PR also changes files that the stack changes:server.py, thenerve doctorchecks, the MCP HTTP endpoint anddocs/config.md.How
Mode
create_app()readsNERVE_AUTH_MODEonce and pins it. A configuration reload does not change the mode. The values arelocal(also used when the variable is not set) andexternal. Any other value stops the server andnerve startwith an error.config.yamlkeys and a reload replaces the configuration object, so the environment is the only source.Requests
decode_actor_context()splits the compact JWS and decodes its payload. It readsprincipal_id(a UUID, stored as a lower-case string) andprofile.display_name. The signature is not checked. REST requests and the WebSocket upgrade both use this one function, so a later verifier replaces only this function.Authorization, thenerve_tokencookie and?token=. A header that Nerve cannot read gives 401./ws, the MCP endpoint and the worker-token route. System and MCP tokens still give the system actor.Data
AccountStore.upsert_external_actor()adds theactor_refsrow on first sight and writes the display name when it changes. This happens before the request writes rows that refer to the actor through foreign keys. An in-process map skips the statement when the name is the same, so a normal request does not take the SQLite write lock.system_actor_cannot_be_replacedtrigger abort the insert. Nerve gives 401 for this, not 500.Routes
POST /api/auth/logingives 404.GET /api/auth/statusaddsmode. In external mode it does not read login state, andloginkeeps the fail-closed value.Startup
nerve doctordoes not warn about a missing local account.Why
Hosted agents sign people in at the gateway, so local accounts and session tokens do not apply. Nerve must know which person sends each message so that it can attribute the message.
Limits
Do not deploy this mode yet. Nerve trusts the header without a signature check, so any caller that can reach Nerve can act as any person. Before external mode is used outside a local stack, either only the gateway must be able to reach Nerve, or Nerve must verify the signature.
docs/config.mdstates this.Testing
.venv/bin/pytest tests/ -q: 4800 passed, 37 skipped, 0 failed.tests/test_external_auth.pycovers:sessions.created_by_actor_idandmessages.actor_id;/ws, the MCP endpoint and worker-token, while system and MCP tokens work;modefield;nerve init,nerve startandnerve doctorin external mode.GET /api/auth/statuschanged, to add"mode": "local".🤖 Generated with Claude Code