Repository navigation
Give the system actor the seeded system principal ID in external mode - #514
Open
alex-clickhouse wants to merge 9 commits into
Open
alex-clickhouse wants to merge 9 commits into
alex-clickhouse wants to merge 9 commits into
Conversation
In external mode, the environment can name the ID of the agent's system principal. To use that ID for autonomous work, the system actor of a new database must be able to take it once, although v047 makes the ID immutable. Add the reader for NERVE_SYSTEM_PRINCIPAL_ID (canonical lower-case UUID only) and a store method that changes the ID in one transaction: it drops system_actor_id_cannot_change, updates the row and creates the trigger again with the v047 statement. It changes nothing when another actor has the new ID or when a row refers to the system actor. The referencing columns come from the schema (PRAGMA foreign_key_list), so columns that later migrations add are checked too. A read-only variant gives the same answer for CLI commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In external mode, autonomous work must be attributed to the agent's system principal, whose ID the environment names in NERVE_SYSTEM_PRINCIPAL_ID. The identity bootstrap of the server start and of nerve init now reads the variable once and gives the system actor that ID while no row refers to the system actor. A new installation thus takes the ID before it attributes any work. When a row already refers to the local ID, when another actor has the new ID, or when the value is not a canonical lower-case UUID, the local ID stays and the bootstrap logs one warning. Local mode ignores the variable. The dry-run preview of nerve migrate reports the same step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the system actor keeps its local ID, autonomous work is not attributed to the agent's system principal, and only the start log says so. nerve doctor now reads NERVE_SYSTEM_PRINCIPAL_ID in its own shell, as it does for NERVE_AUTH_MODE, and shows whether the system actor has that ID, whether the next start gives it the ID, or why it keeps its local ID. It reads nerve.db read-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two processes that start at the same time with different values could both change the ID. The first process then kept a cached ID that no longer existed, and its next write attributed to the system actor failed the foreign key. A connection now changes the ID only while the stored ID is still the ID it cached. Otherwise it keeps the stored ID, refreshes its cache and logs a warning. The checks compare with COLLATE BINARY, as the foreign key does, so a column declared with a collation that this connection does not register cannot stop the start. A check that cannot run keeps the ID. The read-only inspection and the dry run read one snapshot, so a concurrent change cannot give a mixed answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nerve start runs the identity preview of the config migration before it serves, and with --foreground the server runs in the same process. The preview read NERVE_SYSTEM_PRINCIPAL_ID and logged a warning, and the identity bootstrap then read it again and logged the same warning. The preview of nerve start now leaves out the system actor step, so the start reads the variable once and logs its result once. nerve migrate and nerve upgrade, which do not start the server, still show the step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A process that opened the database after another process changed the system actor ID cached the new ID, and could then change it again for a different value. The first process then used an ID that no longer existed. The change now records the ID it set, in the same transaction, in an instance_secrets row (the existing name/value table for instance state; the row is not secret). While the row exists, every different ID is refused with the usual warning, so a database changes its system actor ID at most once. The result of a change gives the IDs of its own transaction, not of a later read. The read-only inspection applies the same rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nerve start --foreground on a fresh install runs the slim nerve init and then serves in the same process. Both ran the identity bootstrap, so the process read NERVE_SYSTEM_PRINCIPAL_ID twice and warned twice about an invalid value. start now passes a hidden init option, and the slim init then leaves the system actor step to the gateway's bootstrap, which reads the variable and logs the result once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a successful seed, the current system actor ID is a principal ID, not a local one. nerve doctor still said that the system actor keeps its "local" ID for an invalid or a different value, which misstates where autonomous work is attributed. The doctor, the local-mode note and the migrate preview now say "current ID". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A --no-secrets backup emptied instance_secrets, so a restored database lost the record of the system principal ID. A process that opened the restored database later could then change the system actor ID again, and a process that was already running kept an ID that no longer existed. The scrub now deletes every instance_secrets row except that record, which is instance state and not a secret (the same ID is in actor_refs). The signing secret and the setup token are still removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
alex-clickhouse
marked this pull request as ready for review
October 17, 2026 12:06
pufit
approved these changes
Oct 17, 2026
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
In external mode, the environment can give the ID of the agent's system principal in
NERVE_SYSTEM_PRINCIPAL_ID. Nerve's system actor takes that ID. Autonomous work (cron jobs, workflow legs, internal sessions, system and MCP tokens) is then attributed to the same principal that the platform uses for the agent, and a proxy in front of the model API can check that ID.How
nerve initand each server start read the variable once, in the identity bootstrap.system_actor_id_cannot_changetrigger, changes the ID, writes a one-time record of the change, and creates the trigger again with the v047 statement. The record is not secret, so a--no-secretsbackup keeps it.actor_refsfrom the schema (PRAGMA foreign_key_list), so attribution columns added later are also checked.nerve doctorshows whether the ID is in force, is set at the next start, or is kept, and why. Thenerve migrateandnerve upgradepreviews show the same step.Why
A new installation takes the ID before it attributes any work, so no history is rewritten. An installation that already attributed work to its local ID keeps that ID, because a change would orphan or rewrite its attribution.
Docs
docs/accounts.md(the system actor in external mode) anddocs/config.md(system principal ID).Testing
tests/test_system_principal.py: 68 tests (plus one intests/test_backup.py), among them races between two connections, a late connection, invalid values, local mode, the trigger after the change, a backup and restore, and the doctor output.Part of the batch that attributes autonomous and human work in hosted mode. Independent of the other PRs in the batch.
🤖 Generated with Claude Code