From 798f19b83aeddbcfa876ea5c8935ed65af01246a Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 22 Jul 2026 03:57:43 +0000 Subject: [PATCH] feat(admin-config-tool): task 1.3 - nix and xce-build.sh integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add python3 + pycapnp/pyyaml/jsonschema/click/pytest/ruff (+pip) to the nix dev shell, wire a new `xceadmin` case into xce-build.sh (schema sync from src/schema/ and src/configs/schema/, pip install by profile, pytest), package-data config so bundled schemas survive a non-editable install, xce-build SKILL.md docs, and ruff format/check hooks matching this repo's stdin-JSON hook convention (not brief.md's env-var sketch, which would have silently no-op'd). Verified live end-to-end inside `nix develop ./src` for both the default (editable) and release (pinned) profiles; found and fixed 3 real bugs along the way (missing pip, PEP 668 externally-managed- environment, missing package-data for release-profile bundling) — logged as bug-1458/1459/1460. Requirements: 5.5, 8.1, 8.2, 8.6 --- .claude/settings.json | 10 +++++++ .claude/skills/xce-build/SKILL.md | 7 +++-- .kiro/specs/admin-config-tool/tasks.md | 8 +++++- src/nixSupport/xce.nix | 12 ++++++++ src/tools/xce-build.sh | 39 +++++++++++++++++++++++++- src/xceadmin/.gitignore | 12 ++++++++ src/xceadmin/pyproject.toml | 9 ++++++ 7 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 src/xceadmin/.gitignore diff --git a/.claude/settings.json b/.claude/settings.json index af43a4d6..c9c88a80 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -22,6 +22,16 @@ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js\"", "timeout": 10 + }, + { + "type": "command", + "command": "FILE_PATH=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path') or d.get('tool_input',{}).get('path') or '')\" 2>/dev/null); [[ \"$FILE_PATH\" == *.py ]] && ruff format --quiet \"$FILE_PATH\" 2>/dev/null; true", + "timeout": 10 + }, + { + "type": "command", + "command": "FILE_PATH=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path') or d.get('tool_input',{}).get('path') or '')\" 2>/dev/null); [[ \"$FILE_PATH\" == *.py ]] && ruff check --quiet \"$FILE_PATH\" 2>/dev/null; true", + "timeout": 10 } ] } diff --git a/.claude/skills/xce-build/SKILL.md b/.claude/skills/xce-build/SKILL.md index 42520755..ee32c1db 100644 --- a/.claude/skills/xce-build/SKILL.md +++ b/.claude/skills/xce-build/SKILL.md @@ -1,6 +1,6 @@ --- name: xce-build -description: "Build XCE components with tests. Usage: /xce-build [component] [profile] [build_dir]. Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli. Profiles: asan (default), debug (non-ASAN -O0 -g3 for gdb/cores), release." +description: "Build XCE components with tests. Usage: /xce-build [component] [profile] [build_dir]. Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli, xceadmin. Profiles: asan (default), debug (non-ASAN -O0 -g3 for gdb/cores), release." disable-model-invocation: false allowed-tools: Bash(*) --- @@ -10,12 +10,13 @@ allowed-tools: Bash(*) Args: `[component] [profile|build_dir] [build_dir]` — passed as free text after `/xce-build`, e.g. `/xce-build controller debug`. Parse the ARGUMENTS text yourself and run the command below with the parsed values inline. - **No `context: fork`, intentionally.** It was tried (twice) and reproducibly breaks this skill: whenever ARGUMENTS is non-empty (e.g. `controller`), the forked subagent receives no usable task — it responds as if handed only ambient project context, never runs the build, and reports nothing back. Bare `/xce-build` (no args) happens to still "succeed" under `context: fork` only because the empty-arg case is indistinguishable from no task at all failing open into some default — not because forking works. Do not re-add `context: fork` here without first confirming the harness actually threads a non-empty ARGUMENTS payload into the forked subagent's prompt. -- **component** (1st word, default `all`) — `all`, `lib`, `controller`, `scheduler`, `worker`, `apiserver`, `xcecli` +- **component** (1st word, default `all`) — `all`, `lib`, `controller`, `scheduler`, `worker`, `apiserver`, `xcecli`, `xceadmin` - **profile** (2nd word, default `asan`) — `asan` (debug + AddressSanitizer → `xce.build`), `debug` (non-ASAN `-O0 -g3 -fno-inline -fno-omit-frame-pointer` → `build.dbg`), `release` (`-O3`, no sanitizer → `build.release`). Each profile uses its own default build dir so the variants never clobber each other. If the 2nd word isn't one of these three keywords, treat it as a legacy explicit build_dir instead (3rd word, if present, always overrides the build dir). - Use the **`debug`** profile to root-cause heisenbug crashes that ASAN perturbs away and that gdb-on-release turns into a hang/timeout — the non-ASAN `-O0 -g3` build reproduces them and gives a fully symbolized backtrace under gdb (how bug-1282/bug-1286 were cracked). - **`controller` always builds `xce_test` too** (`meson compile … xce_controller controller_unit_test controller/xce_test`), in every profile — not just `asan`. `xce_test` is the driver `/xce-test-ha` needs; you never have to build it as a separate step after `/xce-build controller`, and the two binaries in a build dir are always the same commit/profile in lockstep. +- **`xceadmin` is Python, not meson, and ignores the ASAN/debug/release sanitizer semantics entirely** — it is dispatched by a plain shell case branch in `xce-build.sh`, deliberately not wired into `src/meson.build`/`meson.options` (see `.kiro/specs/admin-config-tool/design.md`). The `profile` arg still matters for it, but only to pick the pip install mode: default/`asan`/`debug` → `pip install -e .` (editable, for dev); `release` → `pip install .` (pinned, non-editable, for production). Every invocation — either install mode — syncs the bundled `controller.capnp`/`c.capnp` and the 7 JSON validation schemas from `src/schema/` and `src/configs/schema/` into `xcecommon`'s packaged `schema_files/`, then runs `pytest` against whichever install just happened. Only the `release` run actually exercises the packaged (non-editable) schema resolution path — an editable-only test run can't catch a `package_data` regression there. Requires the nix dev shell (`pycapnp`, `pyyaml`, `jsonschema`, `click`, `pytest`, `ruff` — all provided by `src/nixSupport/xce.nix`). -Examples: `/xce-build controller` (ASAN), `/xce-build controller debug` (gdb/core build), `/xce-build all release`. +Examples: `/xce-build controller` (ASAN), `/xce-build controller debug` (gdb/core build), `/xce-build all release`, `/xce-build xceadmin` (editable install + tests), `/xce-build xceadmin release` (pinned install + tests). Run (substituting the parsed component/profile/build_dir; omit trailing empty ones): diff --git a/.kiro/specs/admin-config-tool/tasks.md b/.kiro/specs/admin-config-tool/tasks.md index c0e0584b..a4116ca0 100644 --- a/.kiro/specs/admin-config-tool/tasks.md +++ b/.kiro/specs/admin-config-tool/tasks.md @@ -13,7 +13,7 @@ - Observable: raising any of these subclasses and catching the common base type recovers the message - _Requirements: 7.1, 7.2, 7.3, 7.4_ -- [ ] 1.3 Wire xceadmin into the project's build and dev-shell tooling +- [x] 1.3 Wire xceadmin into the project's build and dev-shell tooling - Nix dev shell provides the Python interpreter and all dependencies needed to build and test xceadmin - Running the build command for xceadmin performs an editable install by default and a pinned non-editable install under the release profile, running the test suite after whichever install just happened - The build step keeps the packaged copies of the wire-protocol schema and the validation schemas in sync with their source locations elsewhere in the repo, and packages both correctly under either install mode @@ -140,3 +140,9 @@ - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 3.1, 3.2, 3.3, 3.4, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3_ - _Boundary: xceadmin.push, xceadmin.discovery_ - _Depends: 3.3_ + +## Implementation Notes + +- **1.3**: nix's bare `python3` does not put `pip` on PATH — `python3Packages.pip` had to be added to `nativeBuildInputs` alongside the packages design.md's Technology Stack table lists; not a spec gap, just an operational dependency the table didn't need to spell out. +- **1.3**: Claude Code `PostToolUse` hooks receive tool input via stdin JSON (`tool_input.file_path`), not a `$FILE_PATH` env var — brief.md's ruff-hook sketch used the env-var form, which would have silently no-op'd. The actual hooks added to `.claude/settings.json` follow this repo's existing `.wolf/hooks/post-write.js` stdin-JSON convention instead. Any future hook work in this repo should follow the same pattern, not brief.md's original sketch. +- **1.3**: `src/xceadmin/src/xcecommon/schema_files/` (synced capnp + JSON schema copies) is a build artifact, regenerated every `xce-build.sh xceadmin` run — never hand-edit it, never commit it (`src/xceadmin/.gitignore` excludes it). diff --git a/src/nixSupport/xce.nix b/src/nixSupport/xce.nix index 2203fe19..65c47ede 100644 --- a/src/nixSupport/xce.nix +++ b/src/nixSupport/xce.nix @@ -61,6 +61,18 @@ stdenv.mkDerivation { })) packcc cacert + # Python — for xceadmin (admin config tool, src/xceadmin/). pycapnp links + # against the capnproto C++ runtime above; nix's python3Packages.pycapnp + # handles that link correctly, unlike a plain `pip install pycapnp`. + # See .kiro/specs/admin-config-tool/brief.md "Nix Environment". + python3 + python3Packages.pip + python3Packages.pycapnp + python3Packages.pyyaml + python3Packages.jsonschema + python3Packages.click + python3Packages.pytest + python3Packages.ruff ] ++ (if withDebug then [ (librtti_debug.overrideAttrs (oldattrs: { separateDebugInfo = true; diff --git a/src/tools/xce-build.sh b/src/tools/xce-build.sh index f0688cb8..c97ac3fc 100755 --- a/src/tools/xce-build.sh +++ b/src/tools/xce-build.sh @@ -49,7 +49,7 @@ cd "$SRC_DIR" usage() { echo "Usage: $0 [component] [profile|build_dir] [build_dir]" - echo "Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli" + echo "Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli, xceadmin" echo "Profiles: asan (default), debug (non-ASAN -O0 -g3, for gdb/cores), release" exit 1 } @@ -112,6 +112,43 @@ case "$COMPONENT" in cd frontend/xcecli && CGO_ENABLED=0 go test ./... -count=1 fi ;; + xceadmin) + # Python — no meson, dispatched by this case statement alone (design + # review correction: xceadmin is deliberately NOT wired into + # src/meson.build/meson.options, see .kiro/specs/admin-config-tool/ + # design.md, File Structure Plan). Ignores $BUILD_DIR/$MESON_OPTS; + # $PROFILE only selects editable vs. pinned pip install below. + XCEADMIN_DIR="$SRC_DIR/xceadmin" + SCHEMA_DST="$XCEADMIN_DIR/src/xcecommon/schema_files" + echo "=== Syncing xceadmin bundled schemas ===" + mkdir -p "$SCHEMA_DST/json" + cp "$SRC_DIR/schema/controller.capnp" "$SCHEMA_DST/controller.capnp" + cp "$SRC_DIR/schema/c.capnp" "$SCHEMA_DST/c.capnp" + cp "$SRC_DIR/configs/schema/"*.schema.json "$SCHEMA_DST/json/" + cd "$XCEADMIN_DIR" + # nix's python3 site-packages lives under the read-only /nix/store, + # so `pip install` (editable or pinned) can't target it directly — + # it refuses with "externally-managed-environment" (PEP 668). Use a + # throwaway venv that inherits the nix-provided pycapnp/pyyaml/ + # jsonschema/click/pytest (--system-site-packages) so pip only ever + # has to install xceadmin itself, never rebuild those from PyPI. + # Recreated on every invocation for a clean, reproducible install — + # same philosophy as setup_build's `rm -rf "$BUILD_DIR"` above. + VENV_DIR="$XCEADMIN_DIR/.venv" + rm -rf "$VENV_DIR" + python3 -m venv --system-site-packages "$VENV_DIR" + if [ "$PROFILE" = "release" ]; then + echo "=== Building xceadmin Python package (pinned, non-editable) ===" + "$VENV_DIR/bin/pip" install . --quiet + else + echo "=== Building xceadmin Python package (editable) ===" + "$VENV_DIR/bin/pip" install -e . --quiet + fi + echo "=== Running xceadmin tests ===" + "$VENV_DIR/bin/python" -m pytest tests/ -v + echo "=== Done: xceadmin (profile=$PROFILE) ===" + exit 0 + ;; all) setup_build echo "=== Building all components ===" diff --git a/src/xceadmin/.gitignore b/src/xceadmin/.gitignore new file mode 100644 index 00000000..1351ddf5 --- /dev/null +++ b/src/xceadmin/.gitignore @@ -0,0 +1,12 @@ +# Build artifacts +build/ +*.egg-info/ +__pycache__/ +*.pyc +.pytest_cache/ +.venv/ +.ruff_cache/ + +# Synced package data — regenerated from src/schema/ and src/configs/schema/ +# by `xce-build.sh xceadmin`'s schema-sync step; never hand-edited, never committed. +src/xcecommon/schema_files/ diff --git a/src/xceadmin/pyproject.toml b/src/xceadmin/pyproject.toml index c4bdf5ad..985036c7 100644 --- a/src/xceadmin/pyproject.toml +++ b/src/xceadmin/pyproject.toml @@ -27,3 +27,12 @@ include = ["xceadmin*", "xcecommon*"] [tool.setuptools.package-dir] "" = "src" + +# Bundled copies of controller.capnp/c.capnp and the 7 JSON validation +# schemas (synced from src/schema/ and src/configs/schema/ by +# src/tools/xce-build.sh's `xceadmin)` case, see Requirement 5.5). Without +# this, a non-editable `pip install .` builds a wheel containing only *.py +# files and silently drops schema_files/ — an editable install would never +# catch this since it resolves straight into the checked-out source tree. +[tool.setuptools.package-data] +xcecommon = ["schema_files/*.capnp", "schema_files/json/*.schema.json"]