Skip to content

data-plane: serve Native files:stat, files:list and files:read - #1372

Merged
NancyKwongPerplexity merged 1 commit into
mainfrom
data-plane-native-files-read
Nov 3, 2026
Merged

NancyKwongPerplexity merged 1 commit into
mainfrom
data-plane-native-files-read

Conversation

@NancyKwongPerplexity

@NancyKwongPerplexity NancyKwongPerplexity commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Lets the data plane answer Native files:stat, files:list and files:read, under /v1 and /api/v1 as space-api mounts them. A new setting, SPACE_DATA_PLANE_NATIVE_FILES=off|serve, controls it. It defaults to off, which leaves the routes unmounted, so merging this changes nothing until a deployment sets serve and routes traffic to the data plane.

space-api keeps every decision; the data plane carries the bytes. For each request the data plane:

  1. Asks space-api's activation listener (SPACE_INGRESS_ACTIVATION_ENDPOINT, Cluster bearer) to authorize it with AuthorizeNativeFileRequest, from its headers alone, before reading the body (api: serve native file access RPCs on the activation listener #1365).
  2. Reads and checks the body with the shared pplx-space-native-files rules: the same extractor rejections, field checks and messages as space-api's handlers.
  3. Asks PrepareNativeFileCall for the sandbox's route. space-api runs its drain gate, admission charge and wake, and grants the runtime endpoint and route token.
  4. Calls WorkerApi on that runtime with the grant's route token, and maps a failure with the shared files mapping.

A refusal from either RPC is answered byte for byte as space-api rendered it. The data plane also matches space-api in these cases:

  • Wrong method: answered with 405 and Allow: POST, after authorization, in the Native envelope.
  • Draining node-api: a unary call reaches the runtime again once, through a fresh PrepareNativeFileCall, as space-api's spaced client reconnects. A stream open (list, read) is also retried after a closed connection.
  • Streaming reads: files:read streams the chunks, and a mid-stream failure aborts the body.
  • API unreachable: an API that cannot be reached, or that predates the RPCs (UNIMPLEMENTED), gets a retryable Native 503 service_unavailable with Retry-After: 1. space-api has no such case, because it is the API.

Two small supporting changes:

  • normalize_public_error: pplx-space-public-api-http gains this function, which normalizes a single response. The data plane uses it for framework rejections, leaving forwarded refusals as rendered. The middleware calls it, unchanged in behaviour.
  • Shared transport-close check: the stream-open check moves from space-api's spaced client into pplx-space-node-api-client (should_reconnect_stream_open), so both servers retry on the same statuses. space-api's existing stream_open_retry_* tests cover the move.

Validation

At this head (5d6bfac4), on main at 0271415a:

  • Rebased onto auth: let the data-plane identity call the native file access RPCs #1371's new head. Two resolutions against main; git range-diff shows only these:
    • data/plane/server/src/lib.rs: main (data-plane: record request latency by route #1356) added a route_layer that times each routed request. .merge(native_files) now comes before it, so the Native files routes are timed like the other data-plane routes when they are served.
    • data/plane/server/Cargo.toml: main already adds tonic-prost to the dev-dependencies, so this PR no longer adds it. Its Cargo.lock line goes with it.
  • cargo fmt --all -- --check, scripts/generate-env-contract.sh --check, scripts/check-rust-error-policy.sh and scripts/source_guards/env_test_hook_boundary.py pass.
  • cargo clippy --locked --all-targets --all-features -p pplx-space-data-plane-server -p pplx-space-data-plane-svc -p pplx-space-node-api-client -p pplx-space-public-api-http -p pplx-space-api -- -D warnings passes.
  • cargo check --workspace --all-targets --locked passes.
  • cargo arch against main's baseline, renames and ARCHITECTURE.toml: 0 required findings and 112 advisory, the same findings as main.
  • cargo nextest run --config 'profile.test.package.pplx-space-api.opt-level=0' --all-features --all-targets, one package at a time, with Postgres and Redis from the nextest setup scripts and SPACE_CI_REQUIRE_TEST_DEPS=1: pplx-space-api 2189 passed, 1 skipped; pplx-space-data-plane-server 402 passed; pplx-space-data-plane-svc 7 passed; pplx-space-node-api-client 28 passed; pplx-space-public-api-http 9 passed; pplx-space-native-files 2 passed.

The new tests run the real data-plane router against a scripted NativeFileAccessApi and a fake WorkerApi runtime, over TCP. Each was mutation-checked: I applied the change, rebuilt, saw the test fail, and reverted.

Test What it proves Mutation that makes it fail
reads_answer_through_the_granted_route_in_the_apis_shapes stat, list and read answer in space-api's shapes. Worker calls carry the grant's route token and the grant's canonical sandbox id. Each request is authorized, then prepared, with the client's headers and the request id it is answered with no route token on worker calls; an empty request id
a_refused_request_is_answered_as_the_api_rendered_it A 401 from authorization and a 409 from preparation are answered with space-api's status, headers and body. The refused request's body is never read and the sandbox is not called. A wrong method without credentials is still a 401 read the body before authorizing; drop the forwarded headers; check the method before authorizing
each_failure_answers_as_the_api_answers_it Parity table, 29 rows: each class of failure answers with the status, code, message, retryable, details, Retry-After and Allow space-api's route gives. The expected values come from files.rs, normalize_public_error_response and the mapping files/tests.rs pins. Classes: framework rejections (400/413/415/422/405), field checks, every gRPC code, storage full, refused-before-start hints, guest missing path, list entry cap skip normalizing rejections; skip normalizing the 405; check the list cap after the entry
a_draining_runtime_is_reached_again_through_a_fresh_preparation A unary call is reconnected once on a node-api drain and not on a closed connection. A stream open is reconnected after either. Only once. A refused re-preparation is the answer never reconnect; reconnect stream opens only on drain; reconnect unary calls on a closed connection
a_read_that_fails_mid_stream_aborts_its_body A mid-stream failure ends the 200 body with an error, not a clean end map stream errors to empty chunks
an_api_that_cannot_answer_is_a_retryable_503 Unavailable, Unimplemented and DeadlineExceeded from the RPCs give a retryable Native 503 with Retry-After: 1. InvalidArgument gives a 500 treat UNIMPLEMENTED as a 500
the_routes_are_mounted_only_when_served With the setting off, the files paths reach the data plane's other routes mount the routes unconditionally
svc native_files_are_off_unless_served, native_files_are_served_from_the_environment The flag and SPACE_DATA_PLANE_NATIVE_FILES parse off and serve, default off, and reject other values default to serve
api stream_open_retry_* (existing) The moved transport-close check still matches drop the transport-close half of should_reconnect_stream_open

Not validated here: a real space-api and runtime behind the data plane. The Kind run at PR 11's head (smoke-native-files-data-plane) covers that.

Severity and risk

Severity: does not apply (new, off by default).

Risk:

  • With the setting off: only the two shared changes run. Both are moves covered by existing tests.
  • With serve: the files paths stop reaching the data plane's ECW fallback.
  • Rollback: set the setting to off, or revert this PR.

Rollout

  • serve needs SPACE_INGRESS_ACTIVATION_ENDPOINT set to space-api's activation listener. Without it the data plane refuses to start.
  • The data plane's service account must be in that listener's SPACE_CLUSTER_API_AUTH_ALLOWED_SERVICE_ACCOUNTS (auth: let the data-plane identity call the native file access RPCs #1371 lets it call the two RPCs).
  • The data plane must be able to reach the runtimes space-api grants.
  • An older space-api answers UNIMPLEMENTED, which the data plane turns into a retryable 503. Keep traffic on space-api until both are upgraded.
  • Until PRs 7 and 8 land, serve answers only stat, list and read. Route no other files paths to the data plane.

Stack

This is PR 6 of 12 and is based on data-plane-native-file-access-authz (#1371). Merge in order:

  1. native-files: share the file-request fingerprints and guest key #1362 native-files-shared-request
  2. native-files: share Native file request handling #1363 native-files-shared-handling
  3. public-api-http: make Native error rendering usable outside space-api #1364 native-error-rendering-shared
  4. api: serve native file access RPCs on the activation listener #1365 native-file-access-rpcs
  5. auth: let the data-plane identity call the native file access RPCs #1371 data-plane-native-file-access-authz
  6. this PR
  7. data-plane-native-files-mutations
  8. data-plane-native-files-write
  9. data-plane-native-files-drain
  10. kind-native-files-data-plane
  11. smoke-native-files-data-plane
  12. docs-native-files-data-plane

Not covered

  • Docs: docs/configuration.md and docs/architecture.md describe the setting in PR 12.
  • Reconnect charging: a reconnect prepares again, so it runs the bind (a renew for the same fingerprint) and the dormant-entrypoint charge. space-api's own reconnect skips that charge; the charge does nothing for a live sandbox.
  • Connection reset scope: the data plane resets its pooled channel to a runtime on a drain or a closed connection, not on a timeout as space-api does. The pool is shared across sandboxes on one node, so a slow call does not reset everyone's connection.
  • Metrics: these routes record public_api.response from the data plane, not sandbox.api.*.

AI assistance: Claude Code drafted the code, tests, and this description; the author reviewed them.

@nikhilbirmiwal-pplx nikhilbirmiwal-pplx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I checked the request order (authorize from headers first, then body and field checks, then prepare, then the worker call), the reconnect rules against space-api's spaced_client_call! and spaced_stream_open_call!, the read streaming and mid-stream abort, refusal pass-through, the RPC failure mapping, and the should_reconnect_stream_open move. They match space-api, and I found nothing blocking.

Two non-blocking notes:

  • native_files/call.rs:167: on a failure that isn't retried, the pooled channel to the endpoint is kept. That covers a budget timeout, and on stat a transport close. space-api retires its kept connection in both cases (retire_after_call_failure), and the ECW paths call forget_endpoint on transport failures. Keepalive on the node-api endpoint limits how long a half-open connection lingers, but calling forget_endpoint on TimedOut and on is_transport_close_status would match the API.
  • native_files.rs:110 / read.rs:82: owner scope runs before the body is read, so a malformed body or a bad depth sent to a missing or foreign sandbox gets a 404. space-api returns 400/415/422 there, because its Json extractor and list_depth run before ensure_sandbox_owner. This follows from the authorize-before-body design and only affects requests that fail either way, but it is a parity difference the parity table doesn't cover.

@NancyKwongPerplexity
NancyKwongPerplexity force-pushed the data-plane-native-file-access-authz branch from c38e35f to fe9375c Compare November 3, 2026 19:40
@NancyKwongPerplexity
NancyKwongPerplexity force-pushed the data-plane-native-files-read branch from 52a2c5e to 5d6bfac Compare November 3, 2026 19:40
@NancyKwongPerplexity
NancyKwongPerplexity force-pushed the data-plane-native-file-access-authz branch from fe9375c to d078cb5 Compare November 3, 2026 21:53
Base automatically changed from data-plane-native-file-access-authz to main November 3, 2026 22:25
Add the Native read file routes to the data plane behind
SPACE_DATA_PLANE_NATIVE_FILES=off|serve (default off, which leaves
them unmounted). They are mounted under /v1 and /api/v1, as on space-api.

space-api keeps every decision. Each request is authorized from its
headers alone through AuthorizeNativeFileRequest on space-api's
activation listener (SPACE_INGRESS_ACTIVATION_ENDPOINT, Cluster bearer)
before its body is read. Its body is then checked with the shared
pplx-space-native-files rules. PrepareNativeFileCall then wakes the
sandbox and grants its route, and the worker call goes straight to the
granted runtime with the grant's route token. A refusal from either RPC
is answered exactly as space-api rendered it.

The data plane answers the rest as space-api does:
- framework rejections (415/400/422/413, 405 after authorization) in
  the Native envelope;
- sandbox failures through the shared files mapping;
- the spaced client's reconnect rules: a unary call reaches the runtime
  again once, through a fresh preparation, when the node-api drains,
  and a stream open also after a closed connection;
- files:read streams and aborts its body on a mid-stream failure.

An API that cannot answer is a retryable Native 503.

Supporting changes:
- pplx-space-public-api-http exposes normalize_public_error, which
  normalizes one response, so forwarded refusals stay untouched.
- The stream-open transport-close check moves from space-api's spaced
  client into pplx-space-node-api-client so both servers share it.
@NancyKwongPerplexity
NancyKwongPerplexity force-pushed the data-plane-native-files-read branch from 5d6bfac to 7af744f Compare November 3, 2026 22:27
@NancyKwongPerplexity
NancyKwongPerplexity added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 6cbd02c Nov 3, 2026
37 checks passed
@NancyKwongPerplexity
NancyKwongPerplexity deleted the data-plane-native-files-read branch November 3, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants