Repository navigation
[admin] add the runtime environment policy admin API and space-admin-cli commands - #985
Open
the-gigi-pplx wants to merge 1 commit into
Open
the-gigi-pplx wants to merge 1 commit into
the-gigi-pplx wants to merge 1 commit into
Conversation
the-gigi-pplx
force-pushed
the
gigi/pla-6904-runtime-policy-admin-api
branch
16 times, most recently
from
October 27, 2026 05:16
30ba8f9 to
4ef2d38
Compare
the-gigi-pplx
force-pushed
the
gigi/pla-6904-runtime-policy-admin-api
branch
from
October 27, 2026 05:26
4ef2d38 to
1dcd0bf
Compare
the-gigi-pplx
marked this pull request as ready for review
October 27, 2026 05:35
kumarpallav-pplx
requested review from
johnlouie-pplx and
nikhilbirmiwal-pplx
October 27, 2026 05:35
johnlouie-pplx
approved these changes
Oct 28, 2026
johnlouie-pplx
left a comment
Contributor
There was a problem hiding this comment.
Approving. I read the handlers, the population query, and the store changes, and checked that the CI Rust tests job ran the Postgres-backed write and auth tests rather than skipping them.
One non-blocking follow-up:
- Non-blocking: policy writes have no generation precondition.
PUT /runtime-environments/{ref}/stateandPUT /runtime-environments/defaulttake the policy row lock but never compare against the generation the operator last read. So if one operator blocks an environment while another runsset-state <ref> allowedfrom an olderlist, the second write reopens the environment and overwrites the block reason. Re-allowing skips the strand check, so nothing stops it.update_templatealready handles this for network policy: an optionalIf-Match: <generation>header, checked in the write, with a 412 on mismatch. Could both PUTs take the same header, compared afterlock_policy, by movingparse_if_match_generationintosupport.rs? The CLI could then send the generation it just read, or take--if-generation.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the first production writer of the cluster's execution environment policy: admin API routes under
/admin/api/v1/runtime-environments, and matchingspace-admin-cli runtime envcommands. Until now the policy store had no caller that writes it.GET /runtime-environmentsruntime env listPUT /runtime-environments/defaultruntime env default <ref> --reason [--strand]allowedif it isn't; the old default staysallowed.PUT /runtime-environments/{ref}/stateruntime env state <ref> <state> --reason [--strand]allowed,deprecatedorblocked.GET /runtime-environments/nodesruntime env nodes [--after ID | --all]GET /runtime-environments/populationruntime env populationBoth writes go through
pplx-space-runtime-environment-policy-store, which locks the policy row, enforces its rules and increments the generation in the same transaction. A write it refuses is a 409 that names the rule: blocking or deprecating the default, blocking with no default set, or making a blocked environment the default. A required reason (1 to 1024 characters) and the caller are recorded on the rows the write touches, and each write emits aspace.admin.auditevent as well as the existing request audit row.The API adds one check of its own, for a write that stops an environment executing. Blocking stops the blocked environment, and the cluster's first default stops every environment with no state, since nodes then narrow to the environments that have one. Such a write needs
strand: truewhile sandboxes, or template releases still in use, are bound to an environment it stops, or while any sandbox's environment can't be told: no binding, an unreadable one, or no binding record. Unbound legacy sandboxes count because they execute as the environment they match, which only a node's compiled catalog can resolve, and a sandbox with no observation from its node may be bound to one that stops. A release is in use while a tag points to it or an enabled pool config pins it. An in-use release counts when it's bound to an environment that stops or its environment can't be told; a superseded release is reported but doesn't count, since it strands work only through the sandboxes made from it, which do count. Sandboxes that can't run again (cleanup_requiredordeleting, or failed with no live evidence and nothing the repair loop could revert to, by the sharedfailed_repair_revert_eligibilityrule) are reported but don't count either. Later defaults only add themselves asallowed, and blocking an environment that already doesn't execute (blocked, or with no state under a default) stops nothing new, so neither is checked. A concurrent write between that read and the store's lock is caught by the generation the write returns, and the check then runs under the lock. The counts are read just before the write's transaction, so they're an acknowledgement, not a guarantee.Node status reports the generation each node applied and whether it's current, and judges conformance by content, so a node behind on generation still conforms when nothing it would change has changed. A node conforms when its default matches, it executes nothing the cluster doesn't, and it doesn't allow legacy work the cluster revoked. A node that narrows further than the cluster, or refuses legacy work the cluster permits, still conforms.
Population attributes a suspended or restoring sandbox to the snapshot its substrate's restore would pick. On Direct that's the newest full snapshot in any upload state, since restore runs the source node's artifact. On Kubernetes it's the newest ready full snapshot with a live storage location: while a node drain evacuates the sandbox, the one holding its current local artifact or, with no artifact marker, the newest full snapshot; otherwise, for a suspended sandbox, the one holding its current local artifact or, with no marker, one taken since the last placement, and for a restoring sandbox, the newest from before the restore began. The query interpolates the shared
snapshot_is_current_local_artifact,restore_retry_source_snapshot_is_currentandno_newer_full_snapshotpredicates rather than re-spelling them. A failed sandbox with evidence the repair loop reads as live (a non-failed status, a pid, a node assignment, or an active Kubernetes execution) is attributed by its observation, or counted as unrecorded without one; otherwise a failed Kubernetes sandbox the repair loop can revert is attributed to the snapshot it would then restore from, and any other failed sandbox has no record. Every other sandbox is attributed to the execution metadata its node last published. Deletion tombstones aren't counted. Work with no binding record and work with no environment in its record are reported in their own buckets.Supporting changes:
get_record, which reads the policy with each row's last change.getreads through it, so there's one read path.RuntimeEnvironmentState::ALLandPolicyChangeReason::MAX_CHARS, which the API errors and the OpenAPI document use.AdminNodeRow::execution_policydecodes a node's stored policy once, for the node detail route and node status alike.support::read_db_erroranswers a read's database failure without the write-race 409.AdminClient::sendis the CLI's shared send-and-interpret helper.Validation
cargo fmt --all -- --check, andcargo clippy --all-targets --all-features -D warningson the three changed crates.cargo arch: no findings in the changed files.strandgate on blocks and on the first default, including the store's refusal taking precedence, releases that are superseded, tagged, or pinned by an enabled or disabled pool config, failed sandboxes that are live, repairable, unrepairable, or on Direct, authorization, node paging at exactly 100 and 101 nodes, re-blocking an already-blocked environment, including one another operator allows again while the block waits for the policy lock, and a population fixture covering each binding source on both substrates, the artifact marker, restore retries, orphaned and missing storage locations, snapshots from a previous placement, and tombstones.--allpaging, and argument parsing.cargo mutantson the new store, API and CLI code: every viable mutant is caught except the one that drops thespace.admin.auditevent, which only a log assertion could catch, and the test policy rules those out. cargo-mutants doesn't mutate SQL, so each arm of the population query's snapshot filter was removed by hand: the population test fails for every one.ci/scripts/run-rust-checks.sh tests-unprivilegedon a Linux devbox at this head: 10,535 passed, 55 privileged tests skipped, no dependency skips. The privileged tier passed except three fs-verity tests that need a kernel with fs-verity, which the devbox lacks; this change doesn't touch them.Severity and risk
Severity: does not apply; this is new operator surface.
Risk: nodes apply the cluster policy the cluster API serves them, and it serves the policy only while
SPACE_INTERNAL_API_RUNTIME_DESIRED_STATE_ENABLEDis on, which is off by default. Where it's on, a mistaken block stops new work on that environment, which is what thestrandcheck guards. The fix is anotherPUT .../state, which is audited. The only existing behavior touched is the node detail route's policy decode, which moves intoAdminNodeRow::execution_policyunchanged.Not covered
AI assistance: Claude Code (Claude Opus) drafted the code, the tests and this description.