fix(config): /config reports what is actually in paddock.config.yaml, and null clears instead of zeroing (#722, #723) - #757
Merged
Conversation
…onfig (#722, #723) buildInstanceConfig never read paddock.config.yaml, so a GET could not observe any write — a successful save re-fetched the pre-save values and looked like it had reverted, two tabs clobbered each other with no client able to notice, and restartRequired had to be a hardcoded false. Each field now reports `value` (in force now) alongside `pendingValue` (what the file says, i.e. what a restart would load); the editor binds to the latter and restartRequired falls out of the comparison. Saves carry a configVersion so a stale write gets a 409 instead of silently winning. Also: a null on a nonNegInt field clears the key instead of writing 0 (#723, Number(null) === 0), numeric fields reject booleans/arrays, env-shadowed fields are refused by the API as well as the UI, and numeric/string/list fields have upper bounds. Co-Authored-By: Claude <noreply@anthropic.com>
maxSpawnDepth had #723's hole in its own validator — Number(null) is 0, and depth 0 is valid, so "restore the default" wrote the one value that takes the self-MCP away from every child. It now clears the key like its nonNegInt siblings; an explicit 0 still works. The e2e suite touched /config but never saved anything, which is how a screen that discarded every save on screen shipped. It now saves, asserts the value is still there after the post-save re-fetch and after a reload, and restores the instance's original value so the shared server is left as it was found. Co-Authored-By: Claude <noreply@anthropic.com>
Deploying paddock with
|
| Latest commit: |
79f195b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9c28e351.paddock-7u2.pages.dev |
| Branch Preview URL: | https://fix-instance-config-pending.paddock-7u2.pages.dev |
Merged
edspencer
pushed a commit
that referenced
this pull request
Aug 8, 2026
instance-settings.md described a single always-on restart banner and a write with no concurrency story. #757/#722/#723 replaced both. - three banner states (configFileError / just-saved / restartRequired), and restartRequired is computed now, not hardcoded false - value vs pendingValue vs pendingRestart, per field - configVersion -> expectedVersion -> 409 config_conflict, opt-in - the 400 on an env-shadowed write - a save stamps schemaVersion: 1 when absent, which is what config-file.md promises
edspencer
added a commit
that referenced
this pull request
Aug 9, 2026
* docs(configuration,reference): start 0.66 refresh * docs(configuration): headers/type:sse are carried, and host plugins work herdctl 5.32.0 (Paddock 0.63.0) carries an MCP server's `type` and `headers` verbatim on both the host-inherit and declared paths, and added the `plugins` passthrough that makes host plugin inheritance real. Three pages still told readers neither capability existed. - config-file.md: rewrite the two "cannot be carried" cautions; replace the "inert today" plugins paragraph and the "none of them reach Paddock" block with the instructions x mcpServers truth table from claude-plugins.ts:48-52; add the mcpServers: schema table; note headers in the argv-exposure caution and the env:VAR leaf list; fourteen -> fifteen paddock_manage tools. - environment.md: same for the PADDOCK_CLAUDE_MCP_SERVERS and PADDOCK_CLAUDE_INSTRUCTIONS rows, and the env:VAR leaf list. * docs(configuration): the Config screen's 0.66.2 DTO instance-settings.md described a single always-on restart banner and a write with no concurrency story. #757/#722/#723 replaced both. - three banner states (configFileError / just-saved / restartRequired), and restartRequired is computed now, not hardcoded false - value vs pendingValue vs pendingRestart, per field - configVersion -> expectedVersion -> 409 config_conflict, opt-in - the 400 on an env-shadowed write - a save stamps schemaVersion: 1 when absent, which is what config-file.md promises * docs(reference): websocket.md was three frames and several fields behind - add chat:queued_state, chat:queued_returned and chat:injected to the server->client table (all three exist and are handled by the client) - chat:send and chat:set_queue carry attachments; set_queue's identity is qid, not the legacy ts (#245/#736) -- note added on why - chat:tool_start/tool_call carry subagentType/description/hasSubagent (#429) - chat:queued_flushed carries attachments (#728) - the no-seq list was missing the three hub.broadcast frames - replace the misleading "no chat:queued" note with a section on the four queue frames and why _returned is not a flag on _flushed * docs(configuration,reference): remaining stale claims - self-mcp.md frontmatter + mcp.md scope prose: fourteen -> fifteen paddock_manage tools (ALL_OPERATIONS = 3 read + 8 write + 4 trigger) - api.md: the auth section named /api/health as the sole exemption; there are three groups (health, the compiled bundle, /mcp + its metadata). Also document the /api/root/* mount -- every workspace route is registered once and mounted twice, which is why the root workspace's key is "" - hooks.md: add the two trigger routes it was missing (GET .../runtime and POST .../:name/run); schedules.md already listed all six - openapi.md: note /open-api/yaml - environment.md: add the CLAUDE_SECURESTORAGE_CONFIG_DIR row * docs(configuration,reference): cite the filed code-bug issues - websocket.md: a note that the page documents what the server SENDS, citing #772 (chat:injected absent from the ServerMessage union) and #773 (the web client's mirrored types are stale on the #728 queue attachments), so neither is later used to 'correct' the page back - instance-settings.md: the Self MCP projects field's help text says only 'create whole new projects' but the flag also gates promote_project, which clones a caller-supplied URL (#775) -- document the grant, not the string - retire the 'notebook project to repo-backed' phrasing in the two capability rows, matching self-mcp.md's managed/unmanaged vocabulary --------- Co-authored-by: HomeLab Agent <homelab-infra@valfenda.net>
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.
Fixes #722. Fixes #723.
#722 — three symptoms, one root cause
buildInstanceConfigbuilt the GET response out of the boot-frozenPaddockConfigand never readpaddock.config.yaml. So aGET /api/instance-configcould not observe any write — not another tab's, not evenone the same client had made a millisecond earlier. All three reported defects
fall out of that:
save()re-fetches and clearsedits; the re-fetch returned the pre-save values, so setting OVERVIEW.mdmax tokens to
1234wrote1234to disk, showed a green "Saved to disk",and put
2000back in the box.ETag — able to reveal it, because the value a client would have polled for was
never read from the file.
restartRequiredwas hardcodedfalse, because there was nothing tocompare against.
So it is one fix, as the issue suggested. Each field now reports two values:
valuependingValuependingRestartrestartRequiredis nowpendingRestartover all fields rather than anassertion, and the response carries a
configVersion(a fingerprint of the file)and a
configFileErrorfor a file that exists but won't parse — the screen thatexists to fix a broken config should say it is broken rather than report
"nothing pending".
The editor binds to
pendingValue. That one substitution is what makes thescreen honest: it is an editor for the file, so a save round-trips, another
tab's write shows up on the next load, and where the two disagree the field shows
an
In force now:line and the banner says a restart is outstanding.Pending values are computed for editable, non-env-shadowed fields only: an
env-shadowed field resolves to the same env value after a restart, and the
read-only
advancedbindings are normalised at boot (paths canonicalised,portNumber()-ed), so comparing them against raw file text would manufacturedivergence that isn't there. A field the frozen config leaves unset is compared
as its documented default, so e.g.
models— unset, thereforenull— doesn'tread as permanently diverging from the catalog list its absence implies.
Two tabs: visible and refused
Rendering the file makes a concurrent write visible on the next load, but a
tab that already has the page open would still clobber the other one. So a save
is now conditional: the UI echoes the
configVersionit read back asexpectedVersion, and a write composed against a stale snapshot gets a 409with the file left alone. The client keeps the operator's edits, reloads so they
can see what the other writer did, and lets them save again deliberately.
expectedVersionis optional, socurland scripts write unconditionally asbefore.
#723 —
nullon a numeric field cleared, not zeroedThe PUT contract is that a
nulldeletes the key.nonNegIntusedNumber(raw), andNumber(null) === 0is a finite non-negative integer, so{"recovery.maxRetries": null}wrote0— recovery stops retrying entirely.The validator now has the same explicit
null/""/undefinedbranch as itsoptNonNegNumbersibling, and a deliberate0still works.maxSpawnDepthhad the identical hole in its own validator (a "restore thedefault" wrote depth 0, which takes the self-MCP away from every child) and is
fixed the same way.
The same missing type check let
Number()'s other coercions through —overviewMaxTokens: truewrote1,[7]wrote7,debounceMs: falsewrote0. Numeric fields now accept a number or a numeric string and nothing else.Two smaller holes from the same audit
read-only, but
validatePatchdidn't check, so a PUT returned 200 +restartRequired: truefor a write that could never take effect. Now a 400naming the variable. (Tradeoff: you can no longer stage a file value in
preparation for removing the env var — an option the UI never offered anyway.)
brand.nameproduceda 200 KB
paddock.config.yamlfor every boot to parse. Numbers cap at 1e9,plain strings at 1024 characters, list fields at 64 entries;
environmentPromptkeeps its own 32 KiB cap.
Nothing that worked was weakened
The existing patch validation is genuinely strong and is untouched: negative /
zero / fractional budgets,
"abc", bad and wrong-case enums, unknown keys,read-only keys, unknown model ids, an empty model list, non-hex accents, NUL
bytes and oversized prompts are all still rejected,
__proto__is still inert,and the file still round-trips through the
yamlDocumentAPI with operatorcomments and unmanaged keys intact. The client's dirty-tracking still sends only
genuinely-changed keys — it just compares against the file now instead of the
boot config.
Tests
All new tests were confirmed failing on
main(source reverted, tests kept):instance-config.test.ts, the load-bearing one — this filecovered the PUT but never asserted what a subsequent GET reported, which is
exactly why /config: a successful save reverts the form to stale values, two tabs silently last-writer-wins, and restartRequired is hardcoded false #722 shipped): a write is visible to the next GET as
pendingValue/pendingRestart; a second client sees the first's write and astale conditional write is refused with the file intact;
nullclears the threerecovery keys; env-shadowed and oversized writes are refused. 5 fail on main.
computation including the cases that must not report divergence; a malformed
file. 11 fail on main.
banner; the 409 path keeps the edits and re-reads. 4 fail on main.
/confignow actually saves — no test anywhere did — asserts thevalue is still on screen after the re-fetch and after a reload, then restores
the instance's original value so the shared server is left as found. Verified
failing against a
mainbuild.Full suite green locally (server + web),
npm run typecheckclean.🤖 Generated with Claude Code