Rolling kerbside-proxy dev releases¶
Prompt¶
Before responding to questions or discussion points in this
document, explore the kerbside codebase thoroughly. Read
relevant source files, understand existing patterns (the
Rust SPICE proxy in rust/kerbside-proxy/, the gRPC
control contract in kerbside/rpc/, the proxy launch path
in kerbside/proxy_supervisor.py, the release machinery in
.github/workflows/release.yml,
tools/stamp-proxy-version.sh, and
tools/build-proxy-wheel.sh). Ground your answers in what
the code actually does today. Do not speculate about the
codebase when you could read it instead. Where a question
touches on external concepts (PEP 440 version semantics,
pip pre-release resolution, maturin version handling, PyPI
trusted publishing, Kolla image builds), research as needed
to give a confident answer. Flag any uncertainty explicitly
rather than guessing.
All planning documents should go into docs/plans/.
Consult ARCHITECTURE.md for the overall proxy
architecture and AGENTS.md for build commands, project
conventions, and code organisation. Key cross-repo
references for this plan:
shakenfist/kerbside-patches— carries_patches/patch175-kolla-master-install-proxy-wheel.patch, the downstream image-build workaround this plan partially obsoletesopenstack/kolla—kolla/common/sources.pypinskerbside-baseto this repo's develop branch tip and the merged kerbside-base Dockerfile doespip install /kerbside; the whole point of this plan is that upstream kolla needs NO further changesopenstack/kolla-ansible— Gerrit changes 988189, 988913 and 989614 run the (currently non-voting, currently red) kerbside scenario jobs that will prove this plan worked
When we get to detailed planning, I prefer a separate plan
file per detailed phase. These separate files should be
named for the master plan, in the same directory as the
master plan, and simply have -phase-NN-descriptive
appended before the .md file extension. Tracking of these
sub-phases should be done via a table like this in this
master plan under the Execution section.
I prefer one commit per logical change, and at minimum one commit per phase. Do not batch unrelated changes into a single commit. Each commit should be self-contained: it should build, pass tests, and have a clear commit message explaining what changed and why.
Situation¶
The rust-proxy master plan (complete) split the SPICE data
path into a Rust binary (rust/kerbside-proxy/) shipped as
a separate maturin bin wheel, kerbside-proxy, released in
lockstep with the pure-Python kerbside package from the
same v* tag. The lockstep pairing is enforced at release
time only: tools/stamp-proxy-version.sh inserts an exact
kerbside-proxy==X.Y.Z pin into pyproject.toml
immediately before the # KERBSIDE_PROXY_PIN marker, and
stamps the same version into the crate's Cargo.toml. By
deliberate policy the committed tree carries NO pin at all
(the marker line is a comment), so that dev and CI installs
do not require the sibling package to exist on PyPI; a dev
checkout is expected to resolve the binary from the Rust
build tree or KERBSIDE_PROXY_BIN via
proxy_supervisor.py::find_proxy_bin().
This leaves a gap between the two supported install modes.
A consumer that installs kerbside from git but does not
build the Rust tree gets a daemon that cannot start:
find_proxy_bin() exhausts its search paths and raises
RuntimeError. The most important such consumer is the
upstream Kolla image build: kolla/common/sources.py (on
kolla master) fetches this repo at the develop branch tip
and the kerbside-base Dockerfile runs pip install
/kerbside. Since "Phase 5: supervise the Rust proxy from
the daemon" merged on 2026-07-07, every kerbside scenario
job on the upstream kolla-ansible Gerrit series (988189,
988913, 989614) has failed the same way: the
kerbside_proxy container exits ~2 seconds after start and
kolla-ansible check reports the container missing. The
jobs are non-voting, so this went unnoticed until
2026-08-13.
The downstream shakenfist/kerbside-patches repo has a
partial workaround
(patch175-kolla-master-install-proxy-wheel.patch: install
a local wheel from /kerbside/proxy-wheels/ if present,
else pip install kerbside-proxy), but that patch only
affects downstream image builds — upstream Zuul builds
images from upstream kolla plus the Gerrit Depends-On
chain and never see _patches/. Landing an equivalent
change in upstream kolla takes weeks per patch, and its
unpinned PyPI fallback would reintroduce version skew
(latest released binary against develop-tip Python).
Also note pip will not select dev releases for a bare
kerbside-proxy requirement, so that fallback can never
serve unreleased contract changes.
The fix that requires no new upstream kolla code: publish
rolling PEP 440 dev releases of the kerbside-proxy wheel
to PyPI, and commit a dev-inclusive version specifier to
pyproject.toml so that any pip install <checkout>
transitively resolves the newest proxy wheel. Because a
specifier that itself names a dev version opts that one
requirement into pre-releases, no consumer needs --pre
and the already-merged upstream Dockerfile works unchanged.
Two refinements agreed during design discussion:
- Publish only when the binary's inputs change. Most
develop merges touch only Python; republishing an
identical binary is waste. The publish workflow is
path-filtered. Critically, the proto that defines the
gRPC contract lives in the Python tree
(
kerbside/rpc/kerbside.proto) and the Rust stubs are generated at build time bybuild.rsintoOUT_DIR— nothing committed underrust/changes when the contract changes — so the filter must include the proto explicitly..github/workflows/rust.ymlalready models the correct path set. - A contract handshake as backstop. With sparse
publishing, version inequality between the Python
package and the installed binary is the normal,
correct state, so the guard must check contract
compatibility, not version equality. Both sides can
embed a hash of
kerbside.protoat build/generation time; the supervisor compares them before launching the binary. This also catches any future input the path filter fails to anticipate.
Relevant release machinery facts, verified 2026-08-14:
kerbsideversions come from setuptools_scm (git tags,write_to = kerbside/_version.py). On develop between tags it derivesX.Y.Z.devN+g<sha>where N counts commits since the last tag (monotonic per branch).- The proxy wheel's version comes from Cargo.toml
[package] versionvia maturindynamic = ["version"]; the release stamps it from the tag. The committed value is a placeholder (0.1.0). Cargo versions must be semver, so a PEP 440 dev version cannot be stamped there directly; the dev workflow needs either maturin's semver→PEP 440 pre-release translation (e.g.X.Y.Z-dev.N) or a staticversion =in the crate's pyproject.toml. Phase 1 must verify which path maturin supports cleanly. tools/build-proxy-wheel.shbuilds manylinux_2_28 wheels for x86_64 and aarch64 (zig cross) and is reusable outside the release workflow. Bin wheels arepy3-tagged, so one wheel covers all Python versions.- Release publishing uses PyPI trusted publishers gated by
the
releaseGitHub environment (.github/workflows/release.yml, RELEASE-SETUP.md). A new workflow file publishing dev wheels needs its own trusted-publisher registration on thekerbside-proxyPyPI project, and must NOT be gated by the approval-requiredreleaseenvironment or the automation is defeated. tools/stamp-proxy-version.shcurrently inserts the pin when only the marker exists, or replaces an existingkerbside-proxy==pin. Once a committed>=specifier exists it must learn to replace that line with the exact pin at release time. Its "COMMITTED-PIN POLICY" comment block (and the matching comment in pyproject.toml) documents the old policy and must be rewritten.- The
kerbsidewheel does not currently ship the.protofile ([tool.setuptools] packagesonly), so the handshake should compare hashes embedded at generation time (tools/gen-protos.shwriting a committed constant) rather than hashing the proto file at runtime.
Mission and problem statement¶
Make pip install of an unreleased kerbside checkout
produce a working deployment with zero manual intervention
and zero further upstream kolla changes, by:
- publishing rolling PEP 440 dev releases of the
kerbside-proxywheel to PyPI whenever (and only when) the binary's inputs change; - committing a dev-inclusive
kerbside-proxyversion specifier so plain pip installs resolve those wheels, while the release process continues to stamp exact lockstep pins into released artifacts; and - adding a proto-hash contract handshake between the daemon and the binary so that any skew the sparse publishing scheme lets through is detected loudly at startup instead of failing subtly at runtime.
Success is externally observable: the upstream kolla-ansible kerbside scenario jobs (non-voting, red since 2026-07-07) turn green without any new kolla or kolla-ansible code beyond what is already in flight.
Open questions¶
All resolved by the operator on 2026-08-14:
- Refuse or warn on contract-hash mismatch?
Decision: refuse to launch, with a helpful debug
message — name both hashes, the binary path and
version, and the remediation options (upgrade the
wheel; rebuild the local Rust tree; set
KERBSIDE_PROXY_BIN; or set theKERBSIDE_SKIP_CONTRACT_CHECKescape hatch for debugging). Fail-fast matches how a missing binary fails today, and a clear refusal message helps deployers generally, not just this CI path. - Dev version scheme. Decision: as recommended —
derive from setuptools_scm and strip the local segment
(
0.4.1.dev62+g<sha>→0.4.1.dev62), so dev versions are monotonic within a release cycle and sort below the next release. Confirm in phase 1 that maturin emits exactly this version for the wheel (via Cargo semver pre-release translation or a static pyproject version — whichever proves clean). - Committed specifier form. Decision:
kerbside-proxy>=0.4.0.dev0— floored at the current lockstep release (v0.4.0 at planning time) rather than the fully-open>=0.0.0.dev0. Note the.dev0component is functionally required, not decorative: pip only considers pre-releases for a requirement whose specifier itself names one, so a plain>=0.4.0would silently ignore every dev wheel.X.Y.Z.dev0sorts immediately belowX.Y.Z, so the floor still admits the 0.4.0 release itself. The floor may be bumped opportunistically at later releases but requires no maintenance to keep working (the stamp script replaces the whole line at release time anyway). - Unattended publishing acceptable? Decision: yes,
with trust boundaries made explicit. Dev wheels publish
through a separate GitHub environment (
dev-release) with no required reviewers, scoping its own PyPI trusted-publisher registration — the approval-gatedreleaseenvironment remains exclusive to real releases. Dev wheels still get build provenance attestations (actions/attest-build-provenanceis automatic and needs no approval), so consumers can verify origin; what they do not get is the Sigstore tag-signing step, which is meaningless here since no tag exists. Their lower trust level is carried by the version scheme itself (PEP 440 dev releases are explicitly pre-release artifacts). - Both architectures? Decision: yes — Kolla CI needs both x86_64 and aarch64, so both matrix legs are required, not optional.
- Pruning old dev releases. Decision: automate it as a phase of this plan (phase 5) rather than deferring — an unautomated pruning chore would be forgotten until something breaks.
Execution¶
| Phase | Plan | Status |
|---|---|---|
| 1. Dev wheel publish workflow | PLAN-proxy-dev-releases-phase-01-publish-workflow.md | Implemented (on branch; merge deferred to plan completion) |
| 2. Committed dev specifier and release stamping | PLAN-proxy-dev-releases-phase-02-dev-specifier.md | Implemented (on branch; merge deferred to plan completion) |
| 3. Contract handshake | PLAN-proxy-dev-releases-phase-03-contract-handshake.md | Implemented (on branch; merge deferred to plan completion) |
| 4. Docs, downstream cleanup and verification | PLAN-proxy-dev-releases-phase-04-docs-and-downstream.md | Planned |
| 5. Automated dev release pruning | PLAN-proxy-dev-releases-phase-05-pypi-prune.md | Not started |
Phase sketches (to be expanded into per-phase plans):
Phase 1 — dev wheel publish workflow. New
.github/workflows/dev-proxy-wheel.yml: on push to
develop, path-filtered to rust/**,
kerbside/rpc/kerbside.proto,
tools/build-proxy-wheel.sh and the workflow file itself,
plus workflow_dispatch as a force-publish escape hatch
(which also serves the bootstrap publish). Stamp a dev
version (open question 2), build both arches with
tools/build-proxy-wheel.sh, publish with
pypa/gh-action-pypi-publish and skip-existing: true.
One-time setup documented in RELEASE-SETUP.md: create the
dev-release GitHub environment (no required reviewers)
and register the workflow + environment as a trusted
publisher on the kerbside-proxy PyPI project. Dev wheels
get build provenance attestations
(actions/attest-build-provenance, automatic); the
Sigstore tag-signing step is NOT replicated (no tag
exists for a dev build) — note both decisions in the
workflow header comment.
Phase 2 — committed dev specifier and release
stamping. Replace the comment-only # KERBSIDE_PROXY_PIN
policy: commit "kerbside-proxy>=0.4.0.dev0", (open
question 3) above the marker in pyproject.toml, rewrite
the policy comment blocks in pyproject.toml and
tools/stamp-proxy-version.sh, and teach the stamp script
to replace the >= specifier with the exact ==X.Y.Z pin
at release time (it already handles the replace-existing-
pin case; the insert-at-marker branch becomes a fallback).
Verify the pin-indirect-dependencies workflow is
indifferent to the new line. Once this phase merges, a
plain pip install of a checkout resolves the newest
proxy wheel from PyPI — this is the phase that turns the
upstream scenario jobs green. (Sequencing note, corrected
2026-08-14 during phase 2 planning: the phases all land in
ONE PR when the master plan completes, per the operator's
CI-cost policy — not one PR per phase as originally
sketched. The bootstrap dispatch runs immediately after
that merge; in the window before it completes, fresh git
installs resolve the 0.4.0 release wheel, and the phase 3
contract handshake is what makes any resulting skew a loud
startup refusal rather than a subtle failure.)
Phase 3 — contract handshake. tools/gen-protos.sh
additionally writes the sha256 of kerbside.proto to a
committed constant (e.g. kerbside/rpc/contract.py);
build.rs embeds the same hash into the binary
(env!-style); the binary gains a --contract-hash
print-and-exit flag; proxy_supervisor.launch_rust_proxy()
invokes it before launch and refuses/warns (open question
1) on mismatch. A CI or gen-protos check asserts the
committed constant matches the proto so the hash cannot go
stale. Unit tests on both sides.
Phase 4 — docs, downstream cleanup and verification.
Update AGENTS.md / ARCHITECTURE.md / docs for the new
release semantics ("how do unreleased installs get a
proxy binary"). In shakenfist/kerbside-patches, simplify
patch175: the PyPI fallback branch becomes redundant once
phase 2 MERGES and the bootstrap wheel is published (the
plain install resolves the dev wheel) — simplifying earlier
would leave master images with no proxy at all, so that
step is gated on merge + bootstrap (timing correction,
2026-08-15 during phase 4 planning) — while the
/kerbside/proxy-wheels/ local-override branch remains
useful and keeps its skew-safety role. Confirm upstream
kolla needs no change.
Verification: after the next develop merge that triggers a
publish, recheck one of the Gerrit changes and confirm the
kerbside scenario jobs go green (the ubuntu-noble-upgrade
cirros flake, issue #293, is unrelated and may still need
a recheck).
Phase 5 — automated dev release pruning. A scheduled
workflow that keeps the dev release set bounded: retain
the newest K dev releases (and never touch final
releases), remove the rest. Honest caveat for the phase
plan to resolve: PyPI's Warehouse has NO official API for
deleting or yanking releases — upload tokens cannot do it.
Known options, in rough order of preference: (a) the
pypi-cleanup tool, which automates a form login with a
bot/machine account and TOTP secret stored as repository
secrets; (b) if automation proves too fragile or
policy-risky, degrade gracefully to a monitoring workflow
that checks the project's remaining PyPI quota / dev
release count and files a GitHub issue when a threshold is
crossed, making the manual chore impossible to forget.
The phase plan should research (a) properly — including
whether a second PyPI account with maintainer rights on
kerbside-proxy is acceptable — and pick. Quota headroom is
years even without pruning (two ~single-digit-MB wheels
per triggering merge, 10 GB default quota), so this phase
is about hygiene, not urgency, and lands last.
Agent guidance¶
Execution model¶
All implementation work is done by sub-agents, never in the management session. The management session (this conversation) is reserved for planning, review, and decision-making. This keeps the management context lean and avoids drowning it in implementation diffs.
The workflow is:
- Plan at high effort in the management session.
- Spawn a sub-agent for each implementation step with the brief from the plan, at the recommended effort level and model.
- Review the sub-agent's output in the management session. Check the actual files — the sub-agent's summary describes what it intended, not necessarily what it did.
- Fix or retry if the output is wrong. Diagnose whether the brief was insufficient (improve it) or the model was too light (upgrade it), then re-run.
- Commit once the management session is satisfied with the result.
This applies to all steps, including high-effort ones. If a sub-agent can't succeed even with a detailed brief and the right model, that's a signal the brief needs improving, not that the management session should do the implementation itself.
Use isolation: "worktree" for sub-agents when the change
is risky or experimental. The worktree is discarded if the
output is unsatisfactory. For safe, well-understood
changes, sub-agents can work directly in the main tree.
Planning effort¶
The master plan itself was created at high effort. Phase 1 should be planned at high effort (PyPI trusted publishing, maturin version semantics, and workflow security posture all involve judgment calls and external research). Phase 2 is mechanical once phase 1 settles the version scheme and can be planned at medium effort. Phase 3 should be planned at high effort (it touches the gRPC contract convention, build.rs, clap args, and the supervisor launch path across two languages). Phase 4 is medium effort and partly cross-repo. Phase 5 should be planned at medium effort but with its research step (PyPI deletion automation options and their account/security implications) done carefully before any implementation.
Step-level guidance¶
Each phase plan should include the step table described in PLAN-TEMPLATE.md (step, effort, model, isolation, brief), with briefs written so a colleague who has never seen the codebase could execute them. Front-load the research from this master plan into the briefs — e.g. phase 1's brief should state outright that Cargo versions are semver-only and that the maturin translation must be verified, rather than leaving the implementing agent to rediscover it.
Management session review checklist¶
After a sub-agent completes, the management session should verify:
- The files that were supposed to change actually changed (read them, don't trust the summary).
- No unrelated files were modified.
- The code passes
tox -eflake8andtox -epy3; for Rust changes,cargo fmt --check,cargo clippyandcargo testinrust/kerbside-proxy/. - Workflow changes pass actionlint.
- The changes match the intent of the brief — not just syntactically correct but semantically right.
- Commit message follows project conventions
(including the
Co-Authored-Byline with model, context window, effort level, and other settings).
Administration and logistics¶
Success criteria¶
We will know when this plan has been successfully implemented because the following statements will be true:
- A merge to develop that touches
rust/**orkerbside/rpc/kerbside.protopublishes akerbside-proxydev wheel to PyPI within one workflow run, and a Python-only merge publishes nothing. pip install .from a clean develop checkout on a machine with no Rust toolchain yields akerbside daemon runthat launches the proxy binary successfully.tools/stamp-proxy-version.sh X.Y.Zstill produces a pyproject.toml whose only kerbside-proxy requirement iskerbside-proxy==X.Y.Z, and the release workflow is otherwise unchanged.- The daemon refuses to launch the proxy when the
binary's embedded contract hash does not match the
Python side's committed constant, with a debug message
naming both hashes, the binary path and version, and
the remediation options (including
KERBSIDE_SKIP_CONTRACT_CHECK). - The upstream kolla-ansible kerbside scenario jobs pass on a recheck without new kolla/kolla-ansible code.
- The code passes
tox -eflake8andtox -epy3; the crate passes fmt/clippy/test; lines wrap at 120 characters; Python strings use single quotes except docstrings. README.md,ARCHITECTURE.md,AGENTS.mdanddocs/reflect the new dev-release semantics, and RELEASE-SETUP.md documents the one-time trusted publisher registration.- Dev releases on PyPI are automatically bounded (or, if automation proves unsafe, a monitoring workflow files an issue before quota becomes a problem) — no unautomated recurring chore remains.
docs/plans/index.mdhas a row for this plan, updated as phases complete.
Future work¶
- Consider whether the downstream kerbside-patches
/kerbside/proxy-wheels/override should grow a SHA-match assertion against the source tree it is installed beside (the "fail loudly on stale wheel" refinement from the design discussion). - The tree-SHA publish guard (
git rev-parse HEAD:rust/kerbside-proxycompared against the last published wheel) as a more robust alternative to path filters, if the path list ever bites us. - A
sys_platform == 'linux'environment marker on the committed kerbside-proxy floor would restorepip install kerbsideon macOS/musl hosts (no proxy wheels exist there), at the cost of teaching both stamp scripts to preserve the marker when rewriting the line. Raised by review on PR #314; declined there because phase 2 accepted the platform trade deliberately and no contributor currently needs it. - Upstream kolla: consider eventually switching
kerbside-basefrom git-develop to released tarballs, which would make image builds reproducible and reduce the dev-wheel dependency to CI-of-kerbside only.
Bugs fixed during this work¶
(none yet)
Known related issues at planning time:
- kerbside#293 — merge CI tempest cirros download flake; unrelated to this plan but co-occurs on the same Gerrit changes and confuses verification (a red ubuntu-noble-upgrade job is NOT a kerbside failure).
- No existing kerbside issue tracks the missing-binary image-build failure itself; this plan is its resolution. Consider filing one for cross-reference when the first phase PR opens.
Documentation index maintenance¶
When creating a new master plan from this template, update
docs/plans/index.md — done for this plan (see the Master
plans table). When all phases of a plan are complete,
update the status column in index.md to Complete.
Back brief¶
Before executing any step of this plan, please back brief the operator as to your understanding of the plan and how the work you intend to do aligns with that plan.