Repository navigation
[api] trigger a stopped sandbox's runtime upgrade from a test hook, and smoke it on Kind - #1322
Conversation
3cefdc9 to
6c20854
Compare
ac-pplx
left a comment
There was a problem hiding this comment.
One design problem worth settling before the rebase. A fuller review will follow on the rebased head. Line references are at 6c2085473.
On Kubernetes, the upgrade's boot can't run from the Wake, and the pod boots the old environment instead.
- The pod starts the sandbox. The operation places the restore pod a start places, with checkpoint
stopped(crates/libs/common/core/src/k8s.rs:769-772). For a sandbox that is stopped on the node, the shim clears that checkpoint and calls an ordinaryStartat container create (shim/space.go:536-557,:1065-1066). ThatStartcarries no upgrade (crates/libs/sbx/mgr/server/src/service/grpc_core.rs:570), so the sandbox boots under the environment it's moving off. #826 adds astopped-materializationcheckpoint for the same reason. - The Wake can't launch. It names the pod's route identity (
kubernetes/support.rs:27-41) and sendsnetns_path: None(stopped_runtime_upgrade.rs:453). Sandbox-manager refuses a launch for a Kubernetes Wake without a network namespace, withMISSING_RUNTIME_CONTEXT_MARKER(crates/libs/sbx/mgr/server/src/service/activation_support.rs:806-838). Main pins this indirect_wake_resources_preserve_manager_owned_network_context.wake_stopped_runtime_upgradebuilds its boot withboot.start_request(...)?(service/runtime_upgrade.rs:1084), so that refusal comes back before the upgrade runs, and beforemark_unstartedcan see it. - What follows. The api treats the missing-namespace refusal as a readiness poll and re-sends the Wake (
kubernetes/support.rs:853-870). If the shim has booted the sandbox by then, the re-send is refused as "not started ... needs a stopped sandbox" (service/runtime_upgrade.rs:1062-1066). The api reads that as "nothing runs" and deletes the pod under the running guest, without a stop or a backup. If the old environment no longer executes, the shim'sStartis refused instead, and nothing boots under the target either. Either way nothing is upgraded. - Why the tests pass. The api tests run against a scripted node and pod server. The sandbox-manager tests'
upgrade_wakehelper, "shaped as the control plane sends one" (runtime_upgrade/node_effects/tests.rs:713-726), builds a Direct-kind identity with no pod UID. That takes the launch-allowed branch.
Suggested direction: run the upgrade's boot where Kubernetes launches VMs, in the shim at container create, with the pod's network namespace. That means a pod materialization mode that carries the upgrade and never falls back to an ordinary Start, along the lines of #826's checkpoint. Then show one upgrade on Kind, with the test inserting the catalog row and policy it needs.
Question: #1015's start and resume upgrade goes through the same start.start_request(...)? (crates/libs/sbx/mgr/server/src/service/activation.rs:724-760). Is it affected the same way? I haven't run it.
2adf499 to
2ece836
Compare
|
You're right. On Kubernetes the upgrade's boot can't run from the Wake, and this PR as written never upgrades anything there. I traced each step at the PR head. None of this is confirmed on Kind yet: nothing calls the operation, and #1015's path needs Firecracker 1.17.0 approved on the worker.
#1015: yes, it's affected. Its Kubernetes start and resume send Plan: we're splitting this along your suggestion, and the node half lands first as a separate PR on top of #826:
This PR becomes the api half, stacked on it:
A Kind scenario proves it end to end: a stopped upgrade, a #1015 start upgrade, and a negative arm. The scenario seeds the catalog rows and policy it needs, and it uses a test-only caller for the operation. The Wake-based boot comes out of this PR, and I'll ask for the fuller review once it's rebased onto the node half. |
2ece836 to
d230330
Compare
6c20854 to
e896f6b
Compare
79b38a2 to
5a602fe
Compare
2299f73 to
fe7221c
Compare
5a602fe to
200f89a
Compare
Pre-merge checks:
|
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Title check | This assessment has not completed. The check will be retried on the next pre-merge check run. |

Passed checks: 5 passed
-
Check name Status Explanation Description Follows The Pr Template The PR description contains all four required items with real content. 1. "## Summary" section: Present. It states that merging the change lets a test cluster trigger a stopped sandbox's runtime u… Bug Fixes Name A Regression Test The check applies only when the pull request fixes a defect in application behavior under crates/, shim/, or ui/. This pull request is not a bug fix. Its title is "[api] trigger a stopped sandbox's ru… Weakened Safeguards Are Justified The check passes. I compared HEAD (8de562d) against its parent c4964a9, which is the PR's actual base. The diff touches 19 files. Nothing in it removes, disables, or loosens a test, assertion, lint, C… Agent Tooling Runs No New Commands Summary: No agent-tooling content that runs commands or starts servers exists at this PR head. The check passes. Scope of evidence: - The checkout is a single root commit ( 8de562d, no parent…Public-Safe Content Public-safe content check: no failing content found in the PR title, description, or the changed files named in the description. Scope and limits: the checkout is a single root commit with no merge ba… Full details: Description Follows The Pr Template
-
Explanation
The PR description contains all four required items with real content. 1. "## Summary" section: Present. It states that merging the change lets a test cluster trigger a stopped sandbox's runtime upgrade through a test-only admin hook. It also adds a Kind bench and a
space_smokescenario. 2. "## Validation" section: Present. It names the commands and tests run, such ascargo clippy, theroutes::debug::runtime_upgrade_hook::testssuites, andspace_smokepytest (683 passed, 1 skipped). It also reports the Kind run results. 3. "## Severity and risk" section: Present. It states "Severity: low" and gives a risk statement ("Risk: low. The trigger is a test hook..."). 4. "AI assistance:" line: Present. It reads "AI assistance: Claude Code drafted the code, tests, docs and this description." This names the tool and the parts it drafted. The check judges presence only, so I did not inspect the code. No tool calls were needed.
Full details: Bug Fixes Name A Regression Test
-
Explanation
The check applies only when the pull request fixes a defect in application behavior under crates/, shim/, or ui/. This pull request is not a bug fix. Its title is "[api] trigger a stopped sandbox's runtime upgrade from a test hook, and smoke it on Kind". The description says it adds a test-only admin trigger (POST /debug/sandbox/{sandbox_id}/upgrade-stopped-runtime, mounted only when a test hook is on), Kind bench settings that are unset by default, a manual-tier space_smoke scenario, and documentation. It also states "Severity: low. Bench and test support, plus documentation." It does not claim to correct a defect in existing behavior. Per the check, a pull request that is not an application bug fix passes. I did not inspect the diff, because the description states the scope clearly and the check's applicability depends only on whether the change is a bug fix.
Full details: Weakened Safeguards Are Justified
-
Explanation
The check passes. I compared HEAD (8de562d) against its parent c4964a9, which is the PR's actual base. The diff touches 19 files. Nothing in it removes, disables, or loosens a test, assertion, lint, CI step, or repository guard, and it adds no allowlist, baseline, or exception entry. Evidence: - Test and lint suppressions: No added
#[allow],#[expect],#[ignore],pytest.mark.skip/xfail, orcfg(not(test))lines. No changes to.config/nextest.toml,clippy.toml,deny.toml,Cargo.tomllint tables,pyproject.toml,ci/,.github/, orARCHITECTURE.toml/architecture-baseline.toml. Nocontinue-on-errororSPACE_TEST_DEPS_OPTIONALadditions. - Test deletions: None. The one test file is added (runtime_upgrade_hook/tests.rs), and its assertions are new. - Guard insandbox.rs:require_managing_org_adminstill runs first in the new hook. The only change is wideningapi_errorandrequire_managing_org_adminfrom private topub(super). - Removed lines: The-lines are a Tiltfileset=list refactor, adebug/mod.rsrouter signature change (the route is now mounted only when the hook is enabled), anadmin/mod.rsmerge call, and a doc sentence. None of them weakens a guard. - CodeRabbit guidance files: No changes toAGENTS.md,CLAUDE.md,CONTRIBUTING.md,.agents/, ordocs/rust/.docs/execution-environments.mdchanges but is not one of the guidance files, and its single removed line is prose about resume. - Only new suppression:space_smoke/runtime_upgrade.py:235adds# noqa: BLE001 - attached to the body's erroron a new line. It is a lint suppression in new Python code, not a#[allow]in Rust. It carries an inline reason, and the code re-raises the original error with a note attached, so it does not weaken a guard. Themainbranch is not the PR base, sogit diff mainshows 1460 unrelated files. I used the parent commit for all findings.
Full details: Agent Tooling Runs No New Commands
-
Explanation
Summary: No agent-tooling content that runs commands or starts servers exists at this PR head. The check passes. Scope of evidence: - The checkout is a single root commit (
8de562d, no parents).git diff main HEADhas no merge base, and it shows 1,460 files of stacked-base drift (for example, plugin versions0.2.55on main vs0.2.53at HEAD). That drift is not this PR's patch, so I did not use it to decide changed-line causality. - The PR description lists no agent files among its changes. Its files are the debug routeruntime_upgrade_hook.rs, the Tiltfile, Kind scripts and chart, thespace_smokescenario modules, anddocs/execution-environments.md. - Because the check fails on content that is present, I checked the state of HEAD directly. Anyhooks, MCP, LSP, or shell-substitution content the PR added would still be present there. Findings at HEAD: - Inline shell substitutions: none.git grepfor!`...`across.agents,.claude,*.md, anddocs/execution-environments.md,AGENTS.md,CLAUDE.mdfound no matches. - Plugin manifests:.agents/plugins/pplx-space/.claude-plugin/plugin.json,.codex-plugin/plugin.json, andplugin.yamlcontain only name, description, version,skillspath, and interface metadata. None hashooks,mcpServers,lspServers, or any command entry. - SKILL.md frontmatter: everySKILL.md(16 files) has onlynameanddescriptionkeys. Noallowed-tools,hooks,context,agent, ormodelkeys appear in.agentsor.claude. - Marketplace:.agents/plugins/marketplace.jsononly registers a local plugin path and policy. - Hook matches elsewhere: thehooksandmcpmatches in the repo are Cargotest-hooksfeatures,.pre-commit-config.yaml, and.coderabbit.yaml. None is an agent plugin entry. Remaining risk: Scripts under skill directories, such asskills/space-validation/scripts/ensure-docker.sh, are runnable files. The PR description does not touch them. Their presence is pre-existing, and I did not find them in this PR's described changes. Result: No hooks, MCP or LSP servers, frontmatter keys beyondnameanddescription, or inline shell substitutions were added or present in agent tooling files at HEAD.
Full details: Public-Safe Content
-
Explanation
Public-safe content check: no failing content found in the PR title, description, or the changed files named in the description. Scope and limits: the checkout is a single root commit with no merge base against main, so the PR diff could not be isolated mechanically. I inspected the files the description names as changed:
crates/l40_api/api/src/routes/debug/runtime_upgrade_hook.rs,Tiltfile,infra/space/kind/scripts/up.sh,infra/space/kind/README.md,docs/execution-environments.md, andspace_smoke/{scenarios/native_runtime_upgrade_stopped_and_start.py, runtime_upgrade.py, adapters.py, capabilities.py}. Findings checked against the criteria: - Credentials, tokens, keys: the hits are variable names with empty defaults (up.sh~131–167,Tiltfile~843), aprintftemplate that writes values from environment variables, and token-handling code. No literal secret values were found. Thetoken=secrets.token_hex(16)call inadapters.pygenerates a value at runtime. - ARNs and cloud account IDs:arn:aws:iam::000000000000:role/kind-storage-vendininfra/space/kind/README.md(~2734) uses an all-zero account ID, which is an obvious placeholder. The ARN incapabilities.py(~2877) is built from an f-string. - Internal hostnames and URLs:README.mdreferences in-cluster Service DNS names (*.svc.cluster.local), RFC1918 ranges, andlocalhost. These are generic Kind and Kubernetes examples, not internal hosts. The description links only to publicperplexityai/SPACEPRs. - Personal-workstation paths:space_smoke/scenarios/native_runtime_upgrade_stopped_and_start.py:17uses/home/user/data/..., a path inside the sandbox guest. TheREADME.md(~1411)/home/devis the default Kind home directory. Neither is a personal workstation path. - Production logs, measurements, incidents, or unfixed vulnerabilities: the description reports test counts and pass/fail outcomes from a Kind bench. It contains no raw production output and no incident details. - Private trackers, chats, or documents: none found in the description or the inspected files. Values from the criteria were not quoted in this explanation.
-
fe7221c to
5d19b2c
Compare
5d19b2c to
d1a1b41
Compare
200f89a to
6fe90f4
Compare
d1a1b41 to
e498a04
Compare
1bd9d2e to
167936d
Compare
992dff7 to
41434b9
Compare
41434b9 to
472d660
Compare
a1819ed to
8556b10
Compare
472d660 to
b9bb162
Compare
fbcca3b to
8cb022d
Compare
b9bb162 to
9c41402
Compare
8cb022d to
4bc00bb
Compare
9c41402 to
5d311f8
Compare
13e977d to
132ae2d
Compare
5d311f8 to
8b52a56
Compare
132ae2d to
1c43156
Compare
8b52a56 to
c38cb6c
Compare
1c43156 to
5ad7ee2
Compare
…nd smoke it on Kind
Summary
Merging this lets a test cluster upgrade a stopped sandbox's runtime without starting it, through a test-only admin trigger, and adds a Kind bench and a typed
space_smokescenario that drive the stay-stopped and start-time runtime upgrades end to end. It also carries the long-form documentation for the whole stack.Terms
SubstrateAdapter::upgrade_stopped_runtime, the operation the PRs below add. It boots a stopped sandbox's disk under a new execution environment in a runtime-upgrade pod, records the result, and leaves the sandboxstoppedwith its disk backup owed.pplx_space_common_core::env::test_hook_var, which returns nothing unless the binary was built with thetest-hooksfeature. Kind's default images have it; release images never do. Test hooks stay out ofdocs/configuration.md.The problem
The PRs below add the stay-stopped upgrade, its claim, its owed backup, and its handling in the drain and the cleanup loops, but nothing in SPACE calls the operation yet. Unit fixtures exercise each piece against fakes. Nothing proves the pieces work together on a real cluster: a pod that boots the disk under the new environment, a node that records the outcome, an api that settles it and leaves the sandbox stopped and placed, and a later start that runs under the target.
A stock Kind bench can't run that check either. Its workers allow only E0 and the legacy environment and default to E0, and internal-api serves neither the cluster's execution policy nor the provisioning recipe, so no worker ever installs or enforces the target.
The stack's behavior is also spread over its PRs, with no single description of how the pieces fit together.
How this fixes it
crates/l40_api/api/src/routes/debug/runtime_upgrade_hook.rs).POST /debug/sandbox/{sandbox_id}/upgrade-stopped-runtimeon the admin surface runsupgrade_stopped_runtimefor a managing-org admin. It's mounted only while theSPACE_ADMIN_RUNTIME_UPGRADE_TEST_HOOK_ENABLEDtest hook is on. The API reads it throughtest_hook_var, so a binary built without thetest-hooksfeature, as release images are, never mounts the route; off, the route answers 404. The scenario declares the hook, so a release-build bench skips it visibly.Tiltfile,infra/space/kind/scripts/up.sh, thespace-node-manager-localchart). Five settings, unset by default:SPACE_KIND_ALLOWED_EXECUTION_ENVIRONMENTSandSPACE_KIND_DEFAULT_EXECUTION_ENVIRONMENTrender node-manager's startup policy, andSPACE_KIND_RUNTIME_DESIRED_STATE_ENABLED,SPACE_KIND_RUNTIME_PROVISIONING_RECIPE_ENABLEDandSPACE_KIND_RUNTIME_PROVISIONING_PERCENTturn on internal-api's runtime desired state and provisioning recipe.infra/space/kind/README.mdgains a "Runtime-upgrade bench" section.native.runtime_upgrade_stopped_and_start(space_smoke/scenarios/native_runtime_upgrade_stopped_and_start.py,space_smoke/runtime_upgrade.py,space_smoke/adapters.py,space_smoke/capabilities.py). It's amanual-tier scenario, so--alland--category nativelist it asMANUAL; it runs only when named, and on a non-Kind backend it ends with a configuration error before it changes anything. It scopes the settings to the API and controller Deployments, seeds the catalog, sets the policy through the admin API, and restores what it captured. Arms: a stranded stay-stopped upgrade is refused with 412 and places nothing (stranded by a default, E2, that no node certifies an edge to from E0, so the controller's edge rebuild can't undo the setup); a stay-stopped upgrade leaves the sandbox stopped, published under the target, backed up and podless, and a later start runs it under the target with its file intact; a plain start of a stopped sandbox boots it under the target. A bench whose workers don't enforce Firecracker 1.17.0 / Linux 6.1.155 is an infrastructure error, never a pass.docs/execution-environments.mdgains the long form described under Docs below.What this leaves alone: the operation itself, its claim, owed backup, drain and cleanup-loop handling, and the node and wait rules all land in the PRs below. This PR only exposes the operation to a test trigger and adds the bench and scenario that exercise it.
Validation
cargo clippy --workspace --all-features --all-targets -- -D warningsis clean at this head, stacked on its bases.routes::debug::runtime_upgrade_hook::tests(the_trigger_is_mounted_only_when_enabled_and_only_for_operators, which pins the substrate to Direct, andonly_a_truthy_setting_enables_the_trigger) pass, with the api's stay-stopped suites and archcheck's repository test (136 tests).space_smoke's unit tests, run as CI runs them (uv run --group test python -m pytest space_smoke/tests), pass on Linux: 683 passed, 1 skipped; the generated scenario index (python -m space_smoke.inventory --check) lists the scenario;ruff checkandruff format --checkpass on the new modules. The harness itself is validated by the Kind run below, as the repository's test policy asks for smoke definitions.node_drain.stopped_evacuationcounter reported onlyreleasedandawaiting_durable_diskoutcomes, noneupgrade_abandon_refused. Every SPACE image built from the stack was certified clean at that commit; the worker DaemonSets (sandbox-manager, node-api, runtime-manager) and the api, cluster-controller and internal-api rolled without restarts, and every sandbox-manager migration applied with checksums matching their files on all four sandbox-manager nodes. With the test hook off, the trigger answered 404.native.runtime_upgrade_stopped_and_startpassed on its first run. Negative arm: with E0 blocked and a default it has no certified edge to, the trigger answered HTTP 412 with the stranded message and no pod was created. Stopped arm: the trigger answeredupgradedunder a fresh UUIDv7 operation carried by the runtime-upgrade pod, the node verified and retired the upgrade, the disk backup was recorded, and the sandbox stayedstoppedand placed on its worker (active_node_idandtarget_node_idunchanged throughout) until a user start ran it under the new environment. Start arm: starting a stopped sandbox booted it under the new environment in a runtime-upgrade pod targetingstarted. Afterwards the catalog, the cluster policy and the api and cluster-controller env were back to their standing values.stoppedand placed on its worker with nothing published.native.forced_drain_re_drains_re_placed_stopped_sandboxandnative.forced_drain_re_drains_restarted_re_placed_stopped_sandboxpassed. Each starts with a forced drain that re-places the stopped sandbox onto the other worker as a stopped materialization without starting it; then a re-drain released the destination on the re-placement's backup, or on the newer backup a start and stop published, and the next pod stayed off both drained workers.Not covered
Dependencies
This PR is the top of a stacked chain. Merge each PR below first, in order from the bottom, then retarget this PR to
main:main)Severity and risk
Severity: low. Bench and test support, plus documentation.
Risk: low. The trigger is a test hook: release binaries, built without the
test-hooksfeature, never readSPACE_ADMIN_RUNTIME_UPGRADE_TEST_HOOK_ENABLEDand never mount the route, and a test-hook build mounts it only while the hook is on. The Kind settings are unset by default and change nothing for an existing bench. The scenario is explicit-only and refuses to run anywhere but Kind. Roll back by reverting this PR; nothing persists.Docs
docs/execution-environments.mdgains the stack's long form: "Booting an upgrade in its pod" (the node's per-pod records andAbandonRuntimeUpgrade), "Upgrading at resume and start", and "Upgrading a stopped sandbox without starting it" (the claim, the owed backup, the drain's exclusion and the failure paths). Like every test hook, the trigger's setting stays out ofdocs/configuration.md.AI assistance: Claude Code drafted the code, tests, docs and this description.