Skip to content

Web-first setup wizard, external-auth support, workspace templates and best-practice guides - #500

Merged
pufit merged 21 commits into
mainfrom
pufit/web-setup-wizard
Oct 12, 2026
Merged

pufit merged 21 commits into
mainfrom
pufit/web-setup-wizard

Conversation

@pufit

@pufit pufit commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Setup moves into the browser. nerve init now asks only what the terminal has to know — server or Docker, workspace, port, bind — writes the machine layer, starts the daemon and hands over to a setup page. Everything else (mode, model provider, profile, memory, channels, sources, automation, agents) is a server-authoritative checklist in the web UI that works for local accounts and for instances with external authentication. The workspace brain-file templates are rewritten from two new best-practice guides, and the worker guide is injected in full into the worker's first-boot onboarding session.

What changed

Setup backend and API

  • One shared writer (nerve/setup_writer.py) produces identical files for the browser flow, nerve init --terminal and nerve init --non-interactive; golden fixtures pin the output.
  • Server-derived checklist (nerve/setup_checklist.py): each step's status comes from config, workspace and DB facts; only skips, answers and restart debts are stored.
  • GET /api/setup, PUT /api/setup/{mode|provider|profile|memory|channels|sources|automation|agents} with PATCH semantics, skip/unskip, provider test, model catalog, channel token verification, Telegram pairing, POST /api/setup/finish, POST /api/system/restart with a boot generation in /health.
  • External authentication: no claim step, profile from the trusted actor header, wizard errors as 400/403/409 (a 401 means "agent unavailable" to the hosted frontend), restart semantics for systemd, containers and detached daemons.
  • Lockdown: unfinished config steps are blocked, never nagged.
  • Model defaults live in one constants block at the top of nerve/config.py (DEFAULT_MODEL is Opus 5.5; a test fails if a model id is hardcoded elsewhere); the picker lists what the key's catalog advertises.
  • Telegram sync sign-in from the wizard (phone → code → two-step password) writing the same session file nerve setup-telegram does.
  • Connection provenance: every secret reports its source (file, environment, config-repo, gateway), so pre-configured connections render read-only with an explanation and nerve doctor prints the source.

CLI and installer

  • Slim nerve init (flags --terminal, --deployment, --workspace, --port, --bind localhost|lan, --no-start, --no-browser, --no-clipboard, --yes); re-running on a configured install prints status and exits 0.
  • install.sh no longer starts the daemon twice (status probe captured before it is searched).

Web

  • Setup wizard: steps, store, "finish setting up" banner, restart overlay, ready screen, pairing after restart; plain-English copy throughout.
  • Quiet hours are optional (a toggle; off by default for workers); the Automation step shows mode defaults.

Workspace templates, guides and onboarding

  • docs/personal-best-practices.md and docs/worker-best-practices.md: practices with the reasoning and an adoptable example; the worker guide is built around the task/cron state machine. No memory backend is named.
  • Templates for both modes rewritten from the guides (SOUL, AGENTS, TOOLS, MEMORY; new worker TASK.md skeleton with an onboarding marker). Byte budgets keep the injected prompt small.
  • Worker onboarding: TASK.md is rendered from the template with the task description; the onboarding prompt carries the full worker guide, decides which built-in jobs to enable, and ends with a walkthrough of everything it did plus next steps for the operator.
  • Workers: task-planner and quiet hours are off by default; onboarding enables the planner when the worker should plan through the approval loop.
  • Docs: docs/setup.md, docs/api.md, docs/cron.md, docs/worker-guide.md, docs/plans.md, docs/config.md; several statements aligned with the code (skill-plan approval spawns an implementation session; plan_revise returns to the proposing session; lock semantics; quiet hours).

Deliberate deviations

  • nerve init --non-interactive output for personal mode is byte-for-byte unchanged. For worker mode it now writes quiet hours off and task-planner disabled (the goldens were regenerated for that reason).
  • Interactive nerve init --terminal in worker mode has no cron step, so it writes the worker's optional jobs disabled; documented in docs/cron.md and docs/worker-guide.md.

Testing

  • pytest: full suite green; vitest: green; npm run build; lint at the existing baseline.
  • Live end-to-end: slim init → wizard → restart → ready, on an isolated instance (own config dir, state dir, workspace and port); the same with NERVE_AUTH_MODE=external driven by the actor header; installer checklist in a scratch HOME.
  • Every claim in the two guides about platform behaviour was checked against the code; the stale statements found in older docs were corrected in the same change.

Follow-ups (not in this PR)

  • Telegram sync sign-in is refused under lockdown although the session file is runtime state.
  • nerve/tasks/escalation.py::check_escalations has no caller, so quiet hours gate nothing at runtime; the docs now say what the setting does today.
  • docs/cron.md documents a target field and session_mode: main the code does not read.
image

Generated by Nerve

pufit added 19 commits October 10, 2026 16:41
…, connection provenance

- Default Claude models live in one block at the top of nerve/config.py
  (agent.model is now claude-opus-5-5; a test fails on any other literal).
- The Sources step can sign the Telegram message sync in from the browser
  (phone, code, two-step password); nerve setup-telegram stays as fallback.
- Every credential the setup page reports carries its source (file,
  environment, config repository, channel gateway); managed values are
  read-only and documented for operators in docs/setup.md.
… the worker TASK.md template

SOUL, AGENTS, TOOLS and MEMORY for both modes are now short operating rules
distilled from docs/personal-best-practices.md and docs/worker-best-practices.md,
each file one job, no slogans; the guides stay linked from AGENTS.md. The worker
gets a TASK.md template with the guide's anatomy, an onboarding-pending marker
and a {{TASK_DESCRIPTION}} slot. Sizes: personal AGENTS 11.9 → 7.0 KB,
worker AGENTS 8.6 → 6.9 KB, MEMORY and SOUL roughly halved.
…orker guide in the onboarding prompt

apply_mode renders TASK.md from the worker template (browser, --terminal and
--non-interactive share the writer); onboarding-pending is keyed on the marker,
legacy raw files still recognised; the first-boot onboarding session receives
docs/worker-best-practices.md in full (read from the checkout, packaged copy as
fallback, identity-tested) and is told to fill every section in place and delete
the marker last. Goldens and docs follow.
…sk/cron state machine; no memory backend named
… workers; onboarding ends with a walkthrough and next steps
…ocker host view on Linux); port-probe test models the gateway's SO_REUSEADDR listener
…rkspace path; test models the Linux permission case
@alex-clickhouse

Copy link
Copy Markdown
Collaborator

Environment-only credential: the checks see it, the API clients do not

This PR makes credential_available() and resolve_anthropic_api_key() accept ANTHROPIC_API_KEY from the environment (config.py#L3299-L3327). With only that variable set, the Model step shows the provider as done, memU warm-up and recall run, and title generation and model discovery pass their key checks.

The two client factories still pass api_key=self.effective_api_key (L3422, L3451). effective_api_key reads only the config files, so here it is "". In anthropic 0.123.0, any explicit api_key, "" included, stops the SDK from reading ANTHROPIC_API_KEY (anthropic/_client.py: "Explicit ctor args are total"). The request goes out with an empty X-Api-Key and gets a 401.

Reproduction on f36703a:

$ ANTHROPIC_API_KEY=sk-ant-envkey123 uv run python -c '
from nerve.config import NerveConfig
c = NerveConfig.from_dict({})
print(c.credential_available(), c.resolve_anthropic_api_key())
cl = c.create_anthropic_client()
print(repr(cl.api_key), cl.base_url)'
True sk-ant-envkey123
'' https://api.anthropic.com

These callers pass the new check and then build a client with no key:

Prompt rewrite (prompt_rewrite.py#L101), source condensing (registry.py#L41) and the nerve doctor connectivity check (cli.py#L1456) use the same factories and fail the same way.

Suggested fix: in both factories, pass api_key=self.resolve_anthropic_api_key() or None. With None, the SDK resolves ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN itself. A test that sets only the environment variable and checks create_anthropic_client().api_key would catch a regression.

The endpoint is the other half. anthropic_api_base_url returns https://api.anthropic.com/v1/ unless the proxy is on (config.py#L3282-L3288), and the factories pass it explicitly, so ANTHROPIC_BASE_URL has no effect on Nerve's own calls. On an instance behind an Anthropic-compatible gateway, the Claude CLI follows ANTHROPIC_BASE_URL, but discovery, titles and memU send the gateway's key to api.anthropic.com. #484 adds the anthropic_base_url key for this. It does not read ANTHROPIC_BASE_URL from Nerve's environment, on purpose (see its description). With the key fix here and #484, all of Nerve's own calls use the configured credential and endpoint.

@alex-clickhouse

alex-clickhouse commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up for a later PR: worker starter templates

Not a blocker for this PR.

The worker Mode step shows three pills: "Monitor a build", "Review pull requests" and "Watch logs" (copy.ts#L79-L92, ModeStep.tsx#L105-L111). Each pill replaces the full task description with one fixed paragraph that names example/repo or /var/log/example. So the operator can use only one template, must edit the placeholders by hand, and gets no cron jobs from the choice. The jobs come only from what the onboarding session infers from the text.

Proposal:

  • Make the templates a list where the operator can select more than one item. A worker often does more than one of these jobs, for example monitoring CI and reviewing PRs on the same repository.
  • Each selected item sets up two things:
    • Its part of the broad task description. The parts merge into one description. The targets (repository, log path, schedule) are form fields, not placeholders in the text.
    • Its cron jobs in config/cron/jobs.yaml, with a schedule, a run_if gate, and lock and catchup as docs/worker-best-practices.md recommends. For example, PR review gets a github_pr_activity gate.
  • The Review step lists the jobs that each item creates, so the operator sees what will run before Finish.
  • The onboarding prompt (step 4, "Configure cron jobs") must know that these jobs exist, so that it refines them and does not add duplicates.

Maybe this should even be combined with the Automation page?

Comment thread nerve/setup_writer.py Outdated
"max_turns": 50,
"max_concurrent": 32,
"thinking": "max",
"effort": "max",

@alex-clickhouse alex-clickhouse Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The default effort should be xhigh imo. max is a bad default on the 5.5 models, expensive and slow for no real gains.

The same default is also in AgentConfig.effort = "max" (config.py#L952), which applies to every install that has no agent.effort. A DEFAULT_EFFORT = "xhigh" in the default-model constants block at the top of config.py could serve both. The four settings.yaml goldens change with it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 829c4ca: DEFAULT_EFFORT = "xhigh" sits in the constants block at the top of nerve/config.py and is used by AgentConfig.effort (declared and parsed alike) and by the setup writer; the four settings.yaml goldens, the API doc examples and the constants tests follow it.

Generated by Nerve

const configured = detail.openai_api_key.configured;
const managed = secretManaged(detail.openai_api_key) !== null;

const [choice, setChoice] = useState<Choice>(() => (configured ? 'embeddings' : 'without'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"Use embeddings" should be the default choice here, not "No embeddings".

It should also work with no input on a hosted instance. There the person has no OpenAI key to paste, so Nerve must get the embedding endpoint and credential from the platform:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in daace27: the step opens on "Use embeddings" (a step that was answered without a key still shows that answer). The hosted case — an OpenAI-compatible embedding base URL and credential supplied by the platform, shown as managed — is a follow-up alongside the base-URL change in #484.

Generated by Nerve

@alex-clickhouse

Copy link
Copy Markdown
Collaborator

Went through the wizard and the restart process and it all works very smoothly, it's great. Just need to lock down certain sections for the hosted mode and it's great.

Also maybe in the future an MCP step would make sense as well?

@pufit

pufit commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Fixed in 829c4ca: both client factories now pass resolve_anthropic_api_key() — the config key, else the environment's, and "" when neither exists, so construction still never raises — with tests for the environment-only, config-wins and no-key cases. The endpoint half stays with #484; the multi-select worker starter templates are a follow-up as well.

Generated by Nerve

@pufit
pufit merged commit 2b1b534 into main Oct 12, 2026
4 checks passed
@pufit
pufit deleted the pufit/web-setup-wizard branch October 12, 2026 14:17
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