Skip to content

Give every installation setting one home in config.json, applied with parsar - #151

Merged
SaladDay merged 24 commits into
mainfrom
codex/install-config
Sep 26, 2026
Merged

SaladDay merged 24 commits into
mainfrom
codex/install-config

Conversation

@SaladDay

@SaladDay SaladDay commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Phase 1b of the new-user install work: every process setting of an installation gets one visible home, <install>/config.json, applied with a parsar management command.

  • config.json is the only file an operator edits.
    • It has a JSON Schema (deploy/install/config.schema.json), and the settings table in docs/configuration.md is generated from it.
    • The fields are: public URL, ports, log settings, execution concurrency, harnesses and default harness, write-audit retention, OAuth trusted origins, database pool, and the runtime-history export.
    • mode and native_core are fixed after install.
  • Layout:
    • secrets/ holds one copy each of the Core key, the credential key and the database password (Postgres POSTGRES_PASSWORD_FILE, Core AGENTS_API_DATABASE_PASSWORD_FILE). The Core key digest file is derived from the key.
    • state.json holds install facts that only tools write.
    • generated/ holds compose.json, core.env, the systemd unit, the digest file and a non-secret settings snapshot (AGENTS_API_SETTINGS_FILE, served by GET /core/v1/installation).
    • A hand edit to a generated file is detected and refused unless you pass --discard-edits.
  • parsar is copied into the install directory and works without the bundle: status, start, stop, apply [--dry-run] and rotate-core-key.
    • apply decides what to restart by comparing the inputs rendered from config.json with what is actually running (Compose service labels, or native Core's running environment). It restarts only what differs, and never recreates the database unless its own inputs changed.
    • An interrupted apply, rotation or rollback is converged by the next apply.
    • A public URL change lists what is bound to the old address and requires typed confirmation.
    • Key rotation cuts over immediately.
  • Re-running install.sh reads config.json, rejects flags other than the documented resume cases, and repairs.
  • install.sh --convert turns installs made by the two previous installers into this layout: before Derive every Core address from one public URL and report installation facts #138 and Derive every Core address from one public URL and report installation facts #138 itself, in all, core-only, web-only and native modes.
  • Public URLs are validated with exactly Core's origin rule (valid_core_origin).
  • Docs: CONTRIBUTING now allows config.json as the single operator file. The install, configuration and operations guides are updated.

The Web key hint moves to secrets/core.key in the paired frontend PR #150, which merges right after this one.

Review

  1. Round 1: 2 P1s and 4 P2s, all fixed.
  2. Round 2: 2 P1s, both in interruption bookkeeping. Repeated failures in the same area led to a simpler design: apply compares against what is actually running.
  3. Round 3: 2 narrow P1s (local-only Derive every Core address from one public URL and report installation facts #138 conversion, false edits after repeated rollbacks) and 2 P2s, fixed in 3392eb50, 019583ec and de3dd6ec.
  4. Focused review of those fixes: no P0 or P1. Its P2 (a live install missing config.json was told to delete its directory) and its P3s were fixed in 7e2abc13, e9fc7481 and 563ceeb7. These were verified by tests and the gate, under the small-fix rule.

Verification

Upgrading

  • Existing installs move with install.sh --convert from the new bundle, and Core is upgraded as part of that.
  • Operators who used AGENTS_API_EXECUTION_OPTIONS_FILE set deployment default models in Web (System) afterwards.
  • The live demo cluster and the rehearsal install were not touched.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

A new installation writes <install>/config.json, the only file an operator
edits, with every setting that applies to its mode. The installer flags only
seed it. deploy/install/config.schema.json describes it; config_model.py
validates the small keyword subset the schema uses and a test keeps the
schema inside that subset.

The installation directory now holds secrets/ (one copy of each secret),
state.json (identity and install facts) and generated/, which parsar apply
derives from config.json: compose.json and core.env without secrets, the
Core key digest file, the native unit, the settings snapshot Core serves at
GET /core/v1/installation and runtime-history.json when set. Core reads the
new AGENTS_API_PUBLIC_URL, AGENTS_API_DATABASE_PASSWORD_FILE and
AGENTS_API_SETTINGS_FILE; the retired AGENTS_API_DAEMON_WS_URL and
AGENTS_API_CONFIG_FILE are no longer generated.

The parsar zipapp in the installation directory runs status, start, stop,
apply and rotate-core-key without the bundle. apply refuses hand-edited
generated files, changed fixed fields and changed secrets, confirms a public
URL change that strands bound nodes, recreates only the services whose
inputs changed (a Compose label carries each service's input digest) and
restores the previous files when Core rejects the new ones.
rotate-core-key cuts over at once. install.sh reruns read config.json,
reject flags and repair; --status and --stop name their replacements.

install.sh --convert moves an installation made before config.json to this
layout and release. Its preflight changes nothing and stops on anything it
can't convert; secrets are renamed, not copied; an interrupted conversion
resumes. An AGENTS_API_EXECUTION_OPTIONS_FILE stays as it is until the model
settings import, which hooks into parsar apply.

scripts/config-reference.py renders the settings table in
docs/configuration.md from the schema; make check-distribution checks it.
…ons docs

CONTRIBUTING now names config.json as the single operator file instead of
forbidding a new configuration format. The install guide covers changing
settings after installation, repair reruns and converting an earlier
installation; the operations guide uses parsar status, start, stop and
rotate-core-key. Key paths move from admin/ to secrets/.
status and start report why config.json can't be used and fall back to the
last applied settings, which the running services use. A missing retained
execution options file names itself instead of failing generically.
Phase 0's installer output moves into the new summary: the public Console and
API base lines, local-only labels, Core's loopback /v1 when the public URL is
Web's loopback port, the core-only /core/v1 line and the Projects and keys and
Nodes wording, now pointing at secrets/core.key and config.json. The local
Docker node keeps its 2 CPU / 2048 MiB default, and the docs keep phase 0's
page names.
Review round 1 on 8158ef0.

- apply recomputes the files to write after stamping the snapshot, so
  settings.json is written whenever its digest is recorded. Rotation and
  applies that restore a file no longer report settings.json as edited.
- An apply records the digests it is about to write before writing, then
  commits state.json. A run interrupted in between is finished by the next
  apply instead of being reported as hand edits; status says so.
- Native Core restarts only when its inputs changed, including on the
  install.sh repair path where enable --now leaves an active unit alone.
- A failed first start leaves the apply unfinished, and --convert finishes a
  converted installation whose first start failed.
- Web-only conversion before its Core notes the older Core instead of
  failing; the next apply records the pairing.
- The conversion preflight refuses secret files that are links or readable by
  others, starts an old native unit by path, lowercases an old public URL
  when case is the only problem and records no core.key digest.
- A public URL change needs confirmation when the applied value can't be
  read; state.json records the applied public URL.
- Rotation restores the old key only before the new apply is recorded;
  status reports a Core that rejects secrets/core.key and a mismatched digest
  file, and config.json fields that disagree with state.json.
- The origin check refuses IPv6 literals, underscores, trailing dots,
  non-ASCII hosts and leading-zero ports.
- rotate-core-key reads state under the lock, the Compose marker escapes $,
  and unused imports are gone.
The public URL and web.core_url check now uses the rule of PR 1a's installer
valid_core_origin, which matches Core's ValidateSandboxCoreURL: bracketed IP
literals are accepted like Core accepts them, while underscores, a trailing
dot, non-ASCII hosts and leading-zero ports stay refused. After 1a merges,
config_model calls that one shared function.
The installer keeps the config.json structure. 1a's generator edits are
resolved into it: the new Core env names were already generated, the local
node no longer sends core_url, and valid_core_origin in configuration.py is
the one origin rule. config.json's public_url and web.core_url check calls
it, and --public-url and --core-url normalize case and a trailing slash
before applying it. 1a's rule tests run against both. The docs keep 1a's
public URL semantics and point upgrades at install.sh --convert.
Found in the mx1 real check. A first apply says "Services to start" rather
than restart, and only a new installation says it has no execution node; a
repaired or converted installation keeps its nodes.
Review rounds 1 and 2 both found P1s in interruption bookkeeping, so apply
no longer decides restarts from state.json. Every Compose service carries
the digest of its inputs in the io.parsar.inputs label, and the native unit
carries it as PARSAR_INPUTS, which apply reads back from the Core process.
apply renders config.json, compares with the running services and recreates
or restarts exactly those that differ, Core first. A Core that rejects
secrets/core.key is restarted, then Web. An interrupted apply, rotation or
rollback is finished by the next apply, and status reports services that run
with other inputs than config.json renders.

state.json keeps identity and install facts and the last digests written for
each generated file, used only to detect hand edits: a file is edited only if
it matches none of them and differs from the current render; a missing file
is written again. Rollback happens only when every service ran with the
previous files; rotation never rolls back. The applied and pending records
are gone.

--convert now accepts installs made by the #138 installer (public URL,
password file, no retired variables) as well as earlier ones; retired
variables may be missing and a hand-set AGENTS_API_PUBLIC_URL becomes
public_url. The test fixtures come from both earlier generators, kept
verbatim in deploy/install/testdata/.

Also: rotation stops Web inside its error handling and a stale core.key.new
is removed; failed first starts name the right rerun command, and a local
node requested at install is enrolled by the repair; parsar refuses a
symlinked or non-private installation, secrets/ or generated/ directory; a
resumed conversion checks its bundle and may repeat --public-url; the old
Runtime history file is removed once config.json holds it; the public URL
confirmation lists only nodes on the old address; and a Web-only dry run
sends no key to a changed web.core_url.
Found in the mx1 recovery check: the message had an empty reason.
Phase 3 retires AGENTS_API_EXECUTION_OPTIONS_FILE at Core startup, so
--convert no longer carries it into the generated core.env: it reports the
variable, points to the deployment model providers and leaves the old file
in place, since it may hold model keys. The generator's pass-through and the
parsar apply import hook are gone. model_provider_sessions.py also finds an
installation's generated/compose.json and no longer names install.sh
--status. The bundle file list keeps both branches' additions.
…ive hosts first

Review round 3. At #138 the sandbox deployment's core_url is derived from
AGENTS_API_PUBLIC_URL, including the loopback fallback, so conversion no
longer adopts it when core.env names the address: the #138 value is used as
is and the loopback fallback becomes public_url null, keeping Web's origin on
its own port. Both --convert branches run the native preflight before
anything changes. A fresh install writes state.json before config.json, and
a directory left with only state.json gets a clear message.
Review round 3. A rollback records the digests of the files it restores, so
repeated rejected applies no longer end in a false hand-edit report. The
public URL confirmation counts bound nodes from /core/v1/installation's
address_bindings, so a failed node list read still requires confirmation.
The running-state read skips one-off Compose containers, parsar stop checks
the directories, a failed command names its program, and a rollback that
could not start the previous files says so.
Review round 3. valid_core_origin treats an IPv4-mapped loopback address as
loopback, as Go does. format must be the integer 1, not true or 1.0.
ports.core restarts Web too with native Core; the schema's native_restarts
annotation carries that into the settings snapshot and the generated table.
…start over

Focused review. Only a directory that never applied anything (no recorded
generated files, empty generated/) counts as an interrupted fresh install,
including one stopped before state.json was written; an installation that
lost config.json is told to restore it from a backup, with
generated/settings.json for the last applied values, since deleting it would
destroy the credential key and orphan the database. An unreadable
installation.json gets conversion's own message.
Focused review. A rollback records only restored files that parsar wrote and
that were not edited by hand, so a hand edit it puts back is still reported.
…to none

Focused review. A hand-set loopback AGENTS_API_PUBLIC_URL other than Core's
own fallback is reported, and in all mode a hand-set address that differs from
installation.json's public_url stops conversion until --public-url names the
one to keep, instead of silently moving Web's origin. A pre-#138 deployment
core_url equal to Core's loopback address means no public URL. The core.env
value is not canonicalized twice, and its message points at config/core.env.
The docs say the deployment is read only for pre-#138 installs. Tests: the
deployment-read assertion now watches HTTP requests, the native core-only
ports case is back, and the hand-set loopback and resume preflight paths are
covered.
@SaladDay
SaladDay merged commit 9e4898f into main Sep 26, 2026
4 checks passed
@SaladDay
SaladDay deleted the codex/install-config branch October 7, 2026 06:37
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.

1 participant