admin-config-tool's spec.json blocked_on and tasks.md both still described
the OP_upgradeConfig dispatch-gate defect as an open blocker. It was fixed
2026-07-25 (bug-1468, commit 959b0cda) by config-upgrade-snapshot task 8.6,
which independently hit the same wall building a live OP_upgradeConfig
sender -- the fix's own comment in cluster.c already cross-references this
spec. The controller-side path has since been exercised live and
successfully many times via config-upgrade-snapshot's --config-upgrade
soak. All 23 admin-config-tool tasks are already [x]; this was pure
tracking-metadata debt, not a reopened implementation gap.
Also appends a cross-spec gap analysis (admin-config-tool x
config-upgrade-snapshot x clusterconfig-schema-split) to all three specs'
research.md, confirming clean schema integration between the latter two
and flagging the one remaining real gap: a new commit-timeout issue in
config-upgrade-snapshot task 8.6's force-elect step, surfaced now that
bug-1477 is fixed, not yet root-caused.
35 KiB
Research & Design Decisions: admin-config-tool
Summary
- Feature:
admin-config-tool - Discovery Scope: Complex Integration — first Python component in a C/Go codebase, first external client of the controller's raw TLS wire protocol, no in-repo precedent for either.
- Key Findings:
- The controller's
OP_upgradeConfignetwork dispatch path is currently broken for every role (standby rejected byha_dispatch_gate, primary rejected by an invertedha_is_primary()check incluster.c) — a controller-side defect discovered during this design's discovery phase, out of this spec's boundary but blocking real E2E validation ofconfig push. - Redirect (
redirectHost/redirectPort) is wired only forOPCode_join, neverOPCode_upgradeConfig— auto-primary-discovery (Req 3) has no live data to act on for config push until the controller is extended. pycapnphandlescontroller.capnp's$C.mapname(...)annotations without issue (declared, inert metadata under dynamic reflection) — no custom-annotation workaround needed.src/frontend/xcecli/meson.build'sgo_build-wrapper +custom_target()pattern is a directly reusable template forsrc/xceadmin/meson.build, resolving brief.md's open question on Python package location/build integration.
- The controller's
Research Log
TLS setup and certificate validation
- Context:
config pushneeds to establish the same trust model the controller cluster already uses (brief.md Req 2.3-2.4 assumption). - Sources Consulted:
src/lib/netio.c:1208-1243(create_ssl_context),src/controller/myapp.c:94-109,src/controller/main.c:374-375,90,src/controller/join.c:13-16. - Findings:
create_ssl_context(cert_file, key_file)builds a client context (TLS_client_method()) when no cert/key is given, a server context otherwise.myconf->certfile/keyfileresolve to<certdir>/xce.crt/xce.key.- The controller's own outbound client context explicitly sets
SSL_CTX_set_verify(client_ssl_ctx, SSL_VERIFY_NONE, NULL)(myapp.c:109). - No
SSL_CTX_set_verify(..., SSL_VERIFY_PEER, ...),X509_check_host, orSSL_get_peer_certificatecall exists anywhere in the codebase.validate_host()(join.c:13-16) is an unrelated stub (return 1;) used only in the cluster-join path, not TLS. - No minimum TLS version or cipher-suite pinning exists (
SSL_CTX_set_min_proto_version/set_ciphersuitesnot found).
- Implications:
xceadminshould connect withssl.CERT_NONE/check_hostname=Falseto match current behavior exactly — this is encryption-only transport security throughout XCE today, not a weakening introduced by this spec. Captured in design.md Security Considerations.
Wire framing byte order
- Context: brief.md asserted "8-byte uint64 size prefix in host byte order" — needed independent confirmation before committing a Python
struct.packformat string. - Sources Consulted:
src/include/netio.h:30,src/lib/netio.c:111-199,src/lib/request.c:863-938,src/controller/config_repl.c:1925-1968. - Findings:
io_packet_t.sz(uint64_t) is written/read as raw memory (&pkt->sz,sizeof(...)) directly intoSSL_read/SSL_write— nohtole64/htobe64/ntohllanywhere in the path.request.c:863-864states this explicitly in a comment;config_repl.c'sxce_probe_config_tagsend/recv code (:1925-1968) follows the identical pattern. - Implications: Python must use
struct.pack("=Q", size)(native byte order), not network byte order (">Q"). Documented in design.mdxcecommon.wire.
OP_upgradeConfig dispatch path
- Context: Verifying the request would actually reach
do_upgrade_config_with_bytes()on a real cluster before designing around it as a working dependency. - Sources Consulted:
src/controller/ha.c:3794-3960(ha_dispatch_gate),src/controller/cluster.c:238-682(myreq_handler),src/controller/config_repl.c:1591-1810,src/controller/config_repl.h:347-349,src/controller/testsuite/test_config_repl.c:2042,test_commit_barrier_rollout.c:1484,1531. - Findings:
ha_dispatch_gate()'sCTRL_STANDBYallow-list (ha.c:3926-3951) does not includeOPCode_upgradeConfig(onlyupgradeConfigAckis listed) — standbys are rejected with-XCE_ERR_STANDBYbefore reaching the opcode switch.- For
CTRL_PRIMARYwith quorum, the gate falls through (ha.c:3880) — primaries pass the gate. - But
cluster.c:667-679's in-switch handler doesif (ha_is_primary(&c->ha)) { /* reject, -EAGAIN */ } else { handle_upgrade_config(...) }— i.e., it only calls the real handler for a non-primary, the opposite of what passed the gate. - Net: standby → rejected at the gate; primary → passes the gate, rejected by the inverted in-switch check. No role reaches
handle_upgrade_config()/do_upgrade_config_with_bytes()via the network today. That function is exercised only by unit tests calling it directly. config_repl.h:347-349's doc comment ("handle an OP_upgradeConfig request from an admin tool") and the handler's actual logic (WAL marker commit,push_config_to_standbys, restart signal) both imply it should run on the primary — thecluster.c:671guard looks inverted/stale relative to both the dispatch gate and the handler's own semantics.
- Implications: This is a controller-side defect, not an xceadmin gap — out of this spec's boundary (see design.md Boundary Commitments), but it makes real-cluster E2E testing of
config pushcurrently impossible. Testing Strategy in design.md treats the mock-controller test suite as load-bearing, not merely a dev-loop convenience, until this is fixed. Recommend logging this as a bug/blocking item outside this spec's implementation tasks.
Redirect wiring scope
- Context: Requirement 3 (Auto Primary Discovery) assumes
redirectHost/redirectPortget populated on a standby's reply to any request, includingupgradeConfig. - Sources Consulted:
src/schema/controller.capnp:573-595(RequestReply),src/controller/join.c:27-31,64,277. - Findings:
fill_redirect_reply()is defined atjoin.c:27-31and its only production call site isjoin.c:277, inside theOPCode_joinhandling path. No other opcode handler calls it. - Implications:
xceadmin.discovery's redirect-following is implemented generically against theRequestReplyschema shape (correct for any opcode that does populate these fields, e.g.join), but will not actually fire forconfig pushuntil the controller extendsfill_redirect_reply()'s call sites to includeupgradeConfig. Documented as an Out-of-Boundary item and Revalidation Trigger in design.md.
ProbeConfigTag liveness
- Context:
config push --dry-run(Requirement 6) was planned around reusingProbeConfigTag. - Sources Consulted:
src/controller/config_repl.c:1811-1998(xce_probe_config_tag+ doc comment),src/schema/controller.capnp:821-827,ha.c:7409,ha.c:3828,3860,3889,3911,3943(dispatch-gate allow-lists). - Findings:
xce_probe_config_tag()is a direct-struct RPC (noRequest/Replywrapper) with only test callers (test_ha_bootstrap.c:259and a mock intest_ha_bootstrap_support.c). No server-side dispatch case forOPCode_probeConfigTagexists anywhere incluster.c, despiteha_dispatch_gatelisting the opcode as allowed for every role — the allow-list entry is moot because nothing answers it. A stale doc comment atha.c:7409claims it's "already called in myapp_run before ha_start_election_async"; no such call exists inmyapp.cor anywhere in production code. - Implications:
config push --dry-run's wire client can and should still be implemented against the documented contract (Requirement 6 remains valid future-facing design), but it has no live counterpart to call today. Reflected in design.md as an Out-of-Boundary item; Requirement 6 stays Nice-to-Have/Follow-up as already scoped in requirements.md.
No existing external-client precedent
- Sources Consulted:
src/tools/(xce-build.sh,deploy-ha.sh,go_build,appimage/entry.sh),src/controller/testsuite/test.c(built asxce_test),src/frontend/xcecli/common/request/request.go, repo-widefind . -iname "*.py". - Findings:
src/tools/has no socket/capnp client.xce_testis a WAL/snapshot file-inspection CLI with zero network code.xcecli(Go) speaks HTTP/JSON toapiserver, never the raw wire protocol. No Python file exists anywhere in the repository. - Implications:
xceadminis the first client of this wire protocol from outside a C daemon — there is no reference implementation to diff against; correctness rests entirely on independently re-deriving the framing/union contract from the C source (done above), not on copying an existing pattern.
Build tooling precedent for a non-C, non-Meson-native component
- Sources Consulted:
src/tools/xce-build.sh:52,63-131,.claude/skills/xce-build/SKILL.md,.claude/skills/xce-deploy-ha/SKILL.md,src/nixSupport/xce.nix:18-70,src/frontend/xcecli/meson.build,src/meson.build:34-35,84-86,src/meson.options:3-6. - Findings:
xce-build.shdispatches on component name via acasestatement;apiserver/xceclibranches additionally shell out tomake test/go test.xcecli'smeson.buildwrapsgo buildvia acustom_target()calling a generictools/go_buildscript, gated by thecomponentmeson option (combo:all|xcecore|xcecli) and conditionally included viaif component != 'xcecore': subdir('frontend/xcecli').xce.nix'snativeBuildInputshas zero Python packages today — confirmed genuinely absent, not just undocumented. - Implications:
src/xceadmin/meson.buildshould mirrorxcecli's pattern exactly (a newtools/py_buildwrapper +custom_target()+ acomponentchoice or unconditionalsubdir('xceadmin')), resolving brief.md's open question #9 (Python package layout) in favor ofsrc/xceadmin/, consistent withstructure.md's "each major subsystem lives undersrc/<component>/" convention.
pycapnp schema loading and message encoding
- Context: Confirm
controller.capnp's C-specific annotations ($C.mapname(...)) don't break pycapnp's schema compilation, and confirm the right API surface for non-RPC encode/decode. - Sources Consulted: pycapnp GitHub/README, pycapnp CHANGELOG, Quickstart (2.2.4 docs), capnp.pyx source.
- Findings:
$C.mapname(...)is a properly declared annotation (viac.capnp'sannotation mapname @0x...), not an unknown construct — pycapnp'sDynamicStructreflection parses it as inert metadata; the only real failure mode is an unresolved/c.capnpimport path, handled viacapnp.load(path, imports=[schema_dir]).msg.write(f)callsf.fileno()and writes to the raw fd, bypassing any Python-level wrapper — safe foropen(path, 'wb'), unsafe for a socket.msg.to_bytes()/Struct.from_bytes()are pure in-memory and the correct choice for the socket path. - Implications: No custom annotation-stripping preprocessing needed before schema load.
xcecommon.wiremust usefrom_bytes()/to_bytes()exclusively (never.write()/.read()on a live socket) — captured as an explicit constraint in design.md.
pycapnp + Python ssl module composition
- Findings: pycapnp's async/KJ-loop machinery (
AsyncIoStream,capnp.run()) is scoped to its RPC/vat-network layer (TwoPartyClient/Server) and does not intrude on plain synchronousfrom_bytes()/to_bytes()usage.ssl.SSLContext().wrap_socket()gives a normalSSLSocketwhose.recv()/.sendall()perform the framing at the plaintext layer; pycapnp never touches the socket object directly in this design. - Implications: Confirms the
xcecommon.wiredesign (manual framing overssl.SSLSocket, in-memory capnp encode/decode) has no hidden event-loop conflict.
click and pytest conventions
- Findings: Current stable
click(8.3.x/8.4.x) idiom for grouped subcommands:@click.group()→@cli.group()→@config.command(), givingxceadmin config build/xceadmin config pushnaturally.pytest-regressions'file_regressionfixture (binary-capable, deliberate--force-regenupdates) is the more actively maintained golden-file library versuspytest-golden(YAML-only, no binary support) — evaluated but not adopted (see Design Decisions) in favor of a plainassert produced == expected_bytesfixture comparison, since the byte-equality need here doesn't need the extra dependency's ergonomics.
Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| Layered CLI over shared library (selected) | Thin click CLI (xceadmin) over a protocol/schema/mapping library (xcecommon) with no CLI dependency |
Matches project's inward-dependency convention; xcecommon directly reusable by future Python xcecli (brief.md Future Roadmap); each layer independently testable |
None material for this scope | — |
| Single-file script | One xceadmin.py doing everything |
Fastest to write initially | Not reusable by future xcecli; violates project's "one domain per file" convention (structure.md); harder to unit-test wire framing in isolation |
Rejected |
Two separate pip distributions (xceadmin, xcecommon) from day one |
Split packaging immediately | Cleaner dependency graph if xcecommon gets a second consumer soon |
No second consumer exists yet; premature packaging overhead (Simplification lens) | Deferred, not rejected — revisit when Python xcecli work starts |
Design Decisions
Decision: src/xceadmin/ as the package location
- Context: brief.md left this as Open Design Question #9 (candidates:
src/xceadmin/,tools/xceadmin/, top-levelxceadmin/). - Alternatives Considered: 1)
tools/xceadmin/alongsidedeploy-ha.sh. 2) top-levelxceadmin/. 3)src/xceadmin/. - Selected Approach:
src/xceadmin/, mirroringsrc/frontend/'s existing precedent for a non-C, non-Meson-native language living directly undersrc/with its own build wrapper. - Rationale:
structure.md's "Organization Philosophy" states "each major subsystem lives undersrc/<component>/" —xcecli(Go) already establishes that this applies across languages, not just C. Discovery confirmed a directly reusablecustom_target()+ wrapper-script pattern fromxcecli/meson.build. - Trade-offs: None material — this is the path of least resistance given an existing, working precedent.
- Follow-up: Confirm during implementation whether
componentmeson option needs a newxceadminchoice or can use unconditionalsubdir()inclusion (both patterns exist forxceclidepending on option value — seesrc/meson.build:84-86).
Decision: plain byte-equality golden-file tests, not pytest-regressions
- Context: External research surfaced
pytest-regressionsas the more idiomatic 2025/2026-era binary golden-file library versus brief.md's unspecified "compare binary, verify SHA-256." - Alternatives Considered: 1)
pytest-regressions(file_regression.check(..., binary=True)). 2)pytest-golden(YAML-only, ruled out — no binary support). 3) Plainassert produced_bytes == open(expected_path, 'rb').read()in a parametrized fixture. - Selected Approach: Option 3 — plain assertion.
- Rationale:
pytest-regressionsis not yet in this project's nix-pinned dependency list (brief.md's Nix Environment section) and would need independent nixpkgs-availability verification before adoption; the actual requirement (9.1-9.3, byte-for-byte determinism against a committed fixture) doesn't needfile_regression's deliberate-update ergonomics for a small, stable fixture set (Simplification lens). - Trade-offs: Loses
--force-regen's one-flag golden-file update convenience; gains zero new dependencies to verify/pin. - Follow-up: Revisit if the fixture set grows large enough that manual golden-file regeneration becomes a recurring pain point.
Decision: generic (opcode-agnostic) redirect-following and wire exchange
- Context: Discovery found
fill_redirect_reply()only wired forOPCode_join, and two distinct message shapes (Request/Reply-wrapped vs.ProbeConfigTagRequest/Replydirect-struct) both need send/receive. - Alternatives Considered: 1) An
upgradeConfig-specific redirect function, built now and generalized later ifdry-run/other opcodes need it. 2) One genericfollow_redirects()against theRequestReplyshape, and one genericsend_request()/framing pair reused by both message shapes. - Selected Approach: Option 2, generic from the start.
- Rationale: Generalization lens — both consumers (upgradeConfig today, join-shaped mocks in tests, potentially dry-run's direct-struct variant) are instances of the same "read
RequestReply-shaped fields, frame-and-exchange bytes" problem; building the opcode-specific version first and generalizing later would mean deleting code, not adding it (the redirect logic and framing logic are trivial regardless — no meaningful cost to being generic from day one). - Trade-offs: None material.
- Follow-up: None.
Decision: push_config()/probe_config() split into two functions, not one boolean-flagged function (Design Review correction, 2026-07-21, second pass)
- Context: A second
/kiro-validate-designpass found the first draft'spush_config(..., dry_run: bool) -> PushResultimplied a dry-run reply hasacked_count, whichProbeConfigTagReply(only aconfigTagfield) does not have — the shared return type would misrepresent one of the two call shapes. - Alternatives Considered: 1) Keep one function, make
PushResultfieldsOptionalfor the dry-run case. 2) Two functions, two result types. - Selected Approach: Option 2 —
push_config() -> PushResultandprobe_config() -> DryRunResult. - Rationale: The two wire messages and replies share no fields; a boolean flag silently switching which fields are populated is a Type Safety anti-pattern this project's
design-principles.mdimplicitly warns against ("discriminated unions for error handling" — same spirit applies to result shapes). - Trade-offs: None material — strictly a clarity improvement over the first draft.
- Follow-up: None.
Decision: schema files are packaged as package_data, not referenced from src/schema/ at runtime
- Context: User question after design approval — does
pip install .(Req 8.2, non-editable) actually work? It doesn't as originally drafted: non-editable installs copy files intosite-packages, severing any fixed relative path back tosrc/schema/controller.capnp. A__file__-relative walk (e.g.Path(__file__).parents[3] / "schema") only survives editable installs, where__file__still resolves into the checked-out repo tree. - Alternatives Considered: 1) Require
--schema-pathon every invocation (no default) — rejected, bad ergonomics for the common case. 2)__file__-relative resolution — rejected, breaks forpip install .. 3) Package a synced copy of the schema aspackage_data, resolved viaimportlib.resources(packaging-aware, correct for both install modes). - Selected Approach: Option 3.
src/schema/{controller,c}.capnpsynced (cp) intosrc/xceadmin/src/xcecommon/schema_files/byxce-build.sh'sxceadmin)case on every build, packaged viapyproject.tomlpackage_data, resolved at runtime viaimportlib.resources.files("xcecommon") / "schema_files" / "controller.capnp". - Rationale:
importlib.resourcesis the standard-library-correct way to locate packaged non-code files regardless of install mode (editable, wheel-installed, zipped) — it doesn't care wheresite-packagesphysically is.src/schema/remains the single source of truth (this spec doesn't own it, doesn't hand-edit the copy); the sync step is mechanical and runs every build, so it can't drift stale. - Trade-offs: One more build-step to keep in sync (mitigated: it's a single
cp, not a generation/compilation step); the packaged copy is momentarily stale between acontroller.capnpedit and the nextxce-build xceadminrun — acceptable, matches how any other build artifact in this repo already works (nothing is watched/hot-reloaded). - Follow-up: None.
Risks & Mitigations
- Controller
OP_upgradeConfigdispatch defect (cluster.c:667-679vsha.c:3926-3951) — no role can process a push today. Mitigation: documented as an explicit Out-of-Boundary/Revalidation-Trigger item in design.md;config pushimplementation and testing proceed against the documented contract via a mock controller; real-cluster E2E is blocked until a separate fix (outside this spec) lands. Recommend raising this as its own tracked defect. - Redirect not wired for
upgradeConfig— auto-discovery (Requirement 3) is untestable end-to-end against a real cluster for this opcode specifically. Mitigation: generic implementation is correct and forward-compatible; mock-controller tests exercise the logic using syntheticredirectHost/redirectPortreplies. ProbeConfigTaghas no server handler —config push --dry-runcannot be validated live. Mitigation: already Nice-to-Have/Follow-up in requirements.md; client implemented against the schema contract, tested via mock only.- First Python component in the repo — nix/build/CI integration has no precedent to copy exactly (only an analogous one,
xcecli's Go integration). Mitigation:xcecli'smeson.build/custom_target()pattern is close enough structurally to de-risk this; flagged for extra scrutiny during task review. pycapnpC-extension build inside nix —pip install pycapnpnormally compiles a C extension againstcapnproto; getting this wrong would breaknix develop. Mitigation: usepython3Packages.pycapnpfrom nixpkgs (pre-built against nix-providedcapnproto), not a barepip install pycapnp, exactly as brief.md's Nix Environment section already specifies.
Decision: xceadmin is not wired into meson (Design Review correction, 2026-07-21)
- Context:
/kiro-validate-designreview flagged that the initial design draft'ssrc/xceadmin/meson.build(mirroringxcecli'scustom_target()) was architecturally unsound — Go's pattern gates on a single static-binary output;pip install -e .mutates a venv/site-packages tree with no single trackable output for meson to gate on. - Alternatives Considered: 1) Force it through meson anyway via a stamp-file
custom_target(). 2) Skip meson entirely, dispatch purely fromxce-build.sh's case statement. - Selected Approach: Option 2.
- Rationale:
brief.md's own harness sketch (xce-build.sh'sxceadmin)case) already specified directpip install/pytestdispatch with nomeson compilestep — the meson integration was an unjustified addition during design drafting, not something the original harness plan called for. - Trade-offs:
xceadmindoesn't participate inmeson compile -C build <targets>the way C/Go components do; acceptable since it has no C-extension compile step of its own that meson would need to orchestrate (pycapnp's C extension is pip/nix's concern, not this project's meson graph). - Follow-up: None — resolved at design time,
componentmeson option is unchanged.
Decision: bundle the 7 JSON Schema files too, not just controller.capnp (2026-07-21, post-approval user question)
- Context: After design approval, user asked whether the JSON schemas used to validate YAML input should also ship with
xceadmin, the same waycontroller.capnpdoes. Initial answer was "no, they're operator-supplied input per Req 1.1/1.6" — but on reconsideration, that framing didn't hold up: the JSON schemas are the formal spec of whatxcecommon.mapping's hardcoded field-extraction logic (groupname,exclude_members,resname, …) actually expects, not independent operator content. An operator-managed copy that drifts from the shipped mapping code risks a silent failure mode — validation passes on YAML that then maps wrong (or rejects YAML that would map fine) — arguably worse than the capnp case (hard crash) because nothing raises. No requirement or brief text ever described org-custom validation schemas as an actual use case; theconfigs_dir/schema/layout inbrief.md's original sketch was inherited unreflectively fromsrc/configs/'s example layout, not a deliberate feature. - Alternatives Considered: 1) Keep operator-supplied,
configs_dir/schema/required (original design). 2) Bundle aspackage_data, same mechanism ascontroller.capnp,configs_dir/schema/becomes irrelevant. - Selected Approach: Option 2 — confirmed with the user directly (
AskUserQuestion, "Yes, bundle" selected over "Keep operator-supplied"). - Rationale: Consistency with the capnp decision's own rationale (version-lock a tool-internal contract to the release that consumes it); removes an entire operator-facing requirement (maintaining a
schema/folder they didn't author) for zero documented benefit forfeited. - Trade-offs: No mechanism left for an org to enforce custom/stricter YAML validation rules — acceptable, since none was ever a stated requirement; can be revisited as a follow-up feature (e.g. a future
--extra-schemaoverlay) if a real need surfaces. - Process note: this reopened an approved acceptance criterion (Requirement 1.6) — amended requirements.md directly (added 5.5, rewrote 1.6 as "resolved, superseded") rather than looping back through
/kiro-spec-requirements, since the user's direct confirmation in this conversation constitutes the approval for this specific amendment. - Follow-up: None.
References
- pycapnp GitHub — README, quickstart usage patterns
- pycapnp CHANGELOG — confirms synchronous
from_bytes/to_bytes/write/readremain available through 2.2.x after RPC-layer sync support removal in 2.0.0b1 - pycapnp Quickstart (2.2.4 docs)
- capnp.pyx source — verified
.write()'sfileno()behavior directly from source - Click — Commands and Groups
- pytest-regressions / GitHub — evaluated, not adopted (see Design Decisions)
- pytest-golden PyPI — ruled out, YAML-only
src/schema/controller.capnp,src/schema/c.capnp,src/lib/netio.c,src/lib/request.c,src/controller/{ha,cluster,config_repl,join,myapp,main}.c,src/frontend/xcecli/meson.build,src/nixSupport/xce.nix— primary in-repo sources for all findings above
Cross-Spec Gap Analysis: admin-config-tool × config-upgrade-snapshot × clusterconfig-schema-split (2026-07-28)
1. Current State Investigation
All three specs touch the same controller-side config surface (controller.capnp's ClusterConfig, config_repl.c's OP_upgradeConfig handler, cluster.c's dispatch gate) from different angles:
| Spec | Phase | Scope |
|---|---|---|
clusterconfig-schema-split |
implementation-complete |
Remove per-instance/dead fields (port, candidates, perf-tuning) from ClusterConfig |
admin-config-tool |
implementation-complete (blocked_on stale — see §2) |
xceadmin CLI: build/push cluster.bin against the controller |
config-upgrade-snapshot |
tasks-generated, 18/19 actionable tasks [x] |
Snapshot the config alongside cluster state at every upgrade boundary; activation fencing |
Verified current schema state (src/schema/controller.capnp:712-726): ClusterConfig has exactly 13 fields (@0–@12: clusterName … configGeneration), matching clusterconfig-schema-split's Requirement 3.1 target exactly. MasterCandidate/HashZArrayConfig/ResmgrConfig are gone (Requirements 1.6/2.5). config-upgrade-snapshot's own schema additions (HostAdminState, QueueAdminState, lines 769/774) are separate message types, not fields on ClusterConfig itself — no ordinal collision or renumbering conflict between the two specs' schema work.
Verified dispatch-gate state (src/controller/cluster.c:669-694): the OPCode_upgradeConfig polarity-inversion bug that admin-config-tool's spec.json.blocked_on and tasks.md (line 155) both cite as blocking real-cluster config push E2E validation is already fixed — commit 959b0cda (bug-1468, landed 2026-07-25 as part of config-upgrade-snapshot task 8.6). The fix's own comment in cluster.c explicitly cross-references admin-config-tool: "Independently diagnosed and documented as a blocking risk by the admin-config-tool spec (design.md 'Boundary Commitments' / 'Risks') before config-upgrade-snapshot task 8.6 needed a live OP_upgradeConfig sender and hit the same wall."
2. Gap Found: admin-config-tool's blocked_on is stale documentation, not a live blocker
admin-config-tool/spec.json's blocked_on array still lists:
"controller OP_upgradeConfig dispatch defect (cluster.c:667-679 vs ha.c:3926-3951) blocks real-cluster config push E2E validation"
This is the exact same defect as bug-1468, fixed 2026-07-25. Additionally, the same controller-side OP_upgradeConfig path this blocker describes has since been exercised live, repeatedly, and successfully by config-upgrade-snapshot's own --config-upgrade soak (.claude/skills/xce-test-ha/run-ha-soak.sh) — dozens of live 3-node runs today alone, all showing correct dispatch, config replication, majority-ack, and WAL_CONFIG_MARKER commit. The only difference is the client: the soak drives the RPC via xce_test's hand-rolled upgradeconfig command (built for config-upgrade-snapshot task 8.6, since no other live OP_upgradeConfig client existed at the time), not xceadmin's Python config push. But the controller-side code path is identical for both clients — the dispatch gate doesn't distinguish caller identity.
- Gap classification: Missing (stale tracking data, not missing capability).
admin-config-tool's actual E2E validation task (referenced in tasks.md line 155) can very likely be marked done now, or re-run directly withxceadmin config pushagainst a live cluster to close it out formally — the blocking condition itself no longer exists. - Recommendation for design/task phase: Clear
admin-config-tool/spec.json'sblocked_onentry (or mark it resolved with a dated note, matching this codebase's own convention — e.g.clusterconfig-schema-split's already-resolved entries andconfig-upgrade-snapshot's own dated Implementation Notes). If the E2E task was left unchecked pending this, runxceadmin config pushagainst a real deployed cluster (/xce-deploy-ha) once to get first-handxceadmin-specific confirmation (not just proof the shared controller path works) — the CLI's own TLS/redirect/framing code (Requirements 2–3) has never been proven against the real handler, only mock-tested (per tasks.md's own note).
3. Gap Found (residual, this session): config-upgrade-snapshot task 8.6 — new commit-timeout issue post-bug-1477
config-upgrade-snapshot's own remaining task (8.6, live chaotic-upgrade soak) was blocked on bug-1472 → then bug-1477 (both now fixed and verified this session — see .wolf/buglog.json). Re-running --config-upgrade after the bug-1477 fix shows the soak's core assertions (checkpoint-identity, config-identity, host-admin-status survival — the things bug-1471/1472/1477 blocked) all PASS for the first time. But the same run then fails later, in the force-elect-rotation step, with repeated commit timeouts (rc=-110) on the primary — a step this soak scenario has never previously reached (every prior attempt failed earlier). This is very likely a fourth pre-existing, newly-exposed defect in the same family as bug-1471/1472/1474/1477 (surfaced by clearing the earlier blockers, not caused by them) — not yet root-caused, flagged for the next investigation pass. Task 8.6 remains open pending this.
- Gap classification: Unknown (root cause not yet investigated — flagged, not analyzed, per this skill's "defer deep research to design phase" principle).
- Ownership: Likely
UNCLEARuntil investigated — could beha-commit-durability/ha-term-safety/raft-log-replication territory (commit-engine admission capacity or write-quorum under chaos), or aconfig-upgrade-snapshot-adjacent interaction. Follow this session's established routing discipline (bug-1471→1472→1474→1477 precedent): root-cause first, only fix in-place if narrowly scoped, otherwise route to the owning spec and keep task 8.6 blocked.
4. No gap found: clusterconfig-schema-split × admin-config-tool integration
admin-config-tool/requirements.md's own Adjacent Expectations section already documents this dependency as "RESOLVED, landed" (commit 519e11c), and the verified current schema state (§1 above) confirms it: port/candidates/perf-tuning fields are genuinely absent from ClusterConfig, so xceadmin's Requirements 1.8–1.10 (never source these fields) have nothing to accidentally violate — the fields don't exist to source. No further action needed here.
5. No gap found: clusterconfig-schema-split × config-upgrade-snapshot schema coexistence
Verified directly (§1): the two specs' schema additions/removals touch disjoint parts of controller.capnp (ClusterConfig's field list vs. new standalone HostAdminState/QueueAdminState structs). No ordinal renumbering conflict, no shared struct mutated by both specs in incompatible ways.
Summary Table
| # | Finding | Classification | Effort | Risk | Action |
|---|---|---|---|---|---|
| 1 | admin-config-tool blocked_on stale (bug-1468 already fixed) |
Missing (tracking, not capability) | S (update spec.json + one live xceadmin config push run) |
Low | Clear blocker, optionally run one live E2E confirmation |
| 2 | config-upgrade-snapshot task 8.6: new commit-timeout past force-elect step |
Unknown (not yet root-caused) | Unknown until investigated | Medium–High (commit/write-quorum path) | Dedicated debug pass before considering 8.6 closeable |
| 3 | clusterconfig-schema-split ↔ admin-config-tool |
No gap | — | — | None |
| 4 | clusterconfig-schema-split ↔ config-upgrade-snapshot schema |
No gap | — | — | None |
Research Needed (carried forward)
- Root cause of the new post-bug-1477 commit-timeout (finding #2) — not investigated in this pass, flagged only.
Resolved during this pass (no longer open): admin-config-tool/tasks.md was audited directly — all 23 tasks are [x], none gated on the stale blocker as an active checklist item. The E2E gap is purely spec.json.blocked_on + an informational tasks.md note (line 155), not an incomplete task. Confirms finding #1 is pure metadata cleanup, not a reopened implementation gap.