Skip to content

Self-modification via PR (propose_config_change + nerve-workspace skill) (11/13) - #214

Open
alex-clickhouse wants to merge 2 commits into
config-refactor/10-lockdownfrom
config-refactor/11-self-modify
Open

Self-modification via PR (propose_config_change + nerve-workspace skill) (11/13)#214
alex-clickhouse wants to merge 2 commits into
config-refactor/10-lockdownfrom
config-refactor/11-self-modify

Conversation

@alex-clickhouse

@alex-clickhouse alex-clickhouse commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

propose_config_change, plus a nerve-workspace skill that tells the agent how to use it. Under lockdown the agent cannot write tracked config directly, so it opens a pull request against the workspace repo instead: branch, commit, push, PR. Unlocked installs keep editing directly.

Proposals are confined to the reviewed-config surface and classified by effect rather than by file extension, and the destination path is resolved before containment is checked.

Why

Lockdown removes the agent's ability to change its own configuration, which is the intent — but with nothing in its place, the agent's only recourse is to ask a human to make the edit, and the useful half of self-modification is lost along with the dangerous half. A pull request keeps the capability and puts a review in front of it.

Classifying by effect rather than extension is the security-relevant part. A gate plugin is executable code regardless of what it is called, so a proposal touching one is a different kind of change from a schedule edit, and deciding that from the filename would be trivially avoidable.

Resolving the destination before checking containment is the ordering that makes traversal detectable: checking a path that still contains .. tests the string rather than the location it names.

🤖 Generated with Claude Code

@alex-clickhouse alex-clickhouse changed the title Self-modification via PR (propose_config_change + nerve-workspace skill) Self-modification via PR (propose_config_change + nerve-workspace skill) (11/13) Jul 28, 2026
@alex-clickhouse
alex-clickhouse requested a review from Copilot July 28, 2026 10:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a “self-modification via PR” path so the agent can propose reviewed workspace-config changes (skills/cron/settings/instruction files) by staging them in an isolated git worktree, validating, pushing a branch, and opening a PR via gh. This supports lockdown mode where direct edits to tracked config are blocked.

Changes:

  • Introduces nerve.config_pr.propose_config_change with path-surface allowlisting, traversal/symlink containment guards, validation-before-PR, and executable-effect announcements.
  • Adds an agent tool (propose_config_change) with schema + handler, and registers it in the default tool registry.
  • Adds extensive unit + integration tests and a new nerve-workspace skill documenting the workflow (plus a pointer from nerve-dev).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_config_pr.py Comprehensive tests for PR-based config proposals, including real-git integration cases and safety/effect announcements.
nerve/config_pr.py Implements PR proposal workflow: path/scope guards, worktree staging, validation, push, and PR creation via gh.
nerve/agent/tools/handlers/config_pr.py Tool handler + spec for propose_config_change, running the workflow off-thread and formatting agent-facing output.
nerve/agent/tools/schemas.py Adds JSON schema for propose_config_change tool inputs.
nerve/agent/tools/handlers/__init__.py Registers config PR tool specs into the default tool registry.
nerve/templates/skills/nerve-workspace/SKILL.md New skill instructing the agent to always propose workspace config via PR (esp. under lockdown) and clarifying scope.
nerve/templates/skills/nerve-dev/SKILL.md Directs “config changes” requests to the nerve-workspace skill instead of app-code development.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nerve/config_pr.py Outdated
Comment thread nerve/config_pr.py Outdated
Comment thread nerve/config_pr.py
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from ffc39b8 to f85e621 Compare July 28, 2026 12:11
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from f85e621 to abfd8e9 Compare July 28, 2026 12:36
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from abfd8e9 to f60b5a3 Compare July 28, 2026 13:15
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from f60b5a3 to fa47c06 Compare July 28, 2026 14:23
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from fa47c06 to f8ace59 Compare July 28, 2026 14:54
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from f8ace59 to da545bb Compare July 28, 2026 15:34
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch 2 times, most recently from a505bc8 to 86739f0 Compare July 28, 2026 16:20
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from 86739f0 to 7af2ebc Compare July 28, 2026 18:40
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from 7af2ebc to a6da8ad Compare July 29, 2026 08:29
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from a6da8ad to ee9a90e Compare July 29, 2026 09:18
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from ee9a90e to 486db4c Compare July 29, 2026 09:31
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from 486db4c to c3335b4 Compare July 29, 2026 10:31
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from c3335b4 to 87672ab Compare July 29, 2026 13:28
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch 2 times, most recently from d90e6cc to 83ecee6 Compare July 30, 2026 08:25
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch 2 times, most recently from ba04b55 to 737e878 Compare July 30, 2026 09:04
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from 737e878 to 1a8b5c1 Compare July 30, 2026 09:48
Under lockdown the agent cannot edit tracked config directly, so it proposes:
branch, commit, push, and open a PR against the workspace repo where a human
reviews it. Unlocked instances keep editing directly. This ships as its own skill
so the existing app-codebase workflow is untouched, with a one-line pointer added
to it.

Proposals are restricted to the reviewed-config surface and classified by effect,
not by file extension — the question is what a file causes the daemon to run, not
what it is named. Path traversal is refused and the branch ref is not leaked. A
proposal is based on the remote's default branch rather than local HEAD, so it
cannot carry unreviewed local commits into the very PR meant to get them
reviewed, and it can commit on a box with no git identity configured.

The reviewer notice flags a change to lockdown by diffing the proposal against the
current tracked file rather than by presence: presence would flag every proposal
from a locked instance for its own unchanged flag, and a notice that always fires
teaches reviewers to skip it. Keys that are dangerous whenever they appear stay on
presence; keys that are dangerous when they change are diffed. The split is
commented so it is not unified back into a bug.

Co-Authored-By: Claude <noreply@anthropic.com>
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/11-self-modify branch from 1a8b5c1 to 7ebd10e Compare July 30, 2026 10:06
…leniently

Three from the review, and the case the first one made visible.

Require an `origin` remote up front rather than "a remote". Everything downstream
names origin — the ls-remote, the fetch, the branch the worktree comes from, the
push — as does the sync that pulls a merged change back, which has no remote
setting either. So a workspace whose only remote is `upstream` passed the check
and was then told origin was unreachable, and prescribed a `git remote set-head
origin -a` that fails for the same reason. Following that name here instead would
be worse: the proposal has to target the branch sync pulls from, so the PR would
be one that can never reach the instance.

Stage through a helper that falls back to $TMPDIR. The mkdtemp sat outside the
try, so an unwritable workspace parent raised PermissionError out of a function
documented "Never raises". Beside the workspace is still the preference — an
untracked directory inside it would show up in the status ff-only sync reads — but
the parent being writable is an assumption nothing else makes, since sync needs
the workspace itself and no more. A workspace provisioned into a root-owned
directory with the daemon unprivileged is an ordinary locked-fleet layout.

Decode gh's output as UTF-8 with errors="replace" and catch broad Exception, as
sync_service._git already does. errors="strict" is the default under every codec,
so this was not a C-locale edge case: one byte gh relayed from elsewhere raised.
It raised after the push, which replaced "branch pushed but 'gh pr create' failed
… Open the PR manually" with an internal error naming a codec, leaving a pushed
branch with nothing pointing at it.

The origin check then made the local case visible. The nerve-workspace skill
offered the PR route and only that route — "always propose", "don't edit tracked
config files directly", "don't try to bypass it by editing files directly" —
while being installed by every `nerve init`, which never git-inits the workspace.
An unlocked instance with a local workspace was told to always propose, refused
by the tool, and told not to edit: it would report that its config could not be
changed on a box where writing the file is both allowed and correct. The refusal
now says which situation it is, split by lockdown in the handler because that is
the layer told whether the instance is locked, and the skill establishes whether
the workspace is shared before choosing a route.

Co-Authored-By: Claude <noreply@anthropic.com>
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review July 30, 2026 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants