Skip to content

api: serve native file access RPCs on the activation listener - #1365

Merged
NancyKwongPerplexity merged 1 commit into
mainfrom
native-file-access-rpcs
Nov 3, 2026
Merged

NancyKwongPerplexity merged 1 commit into
mainfrom
native-file-access-rpcs

Conversation

@NancyKwongPerplexity

@NancyKwongPerplexity NancyKwongPerplexity commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds NativeFileAccessApi (proto/node/api/v1/native_file_access.proto) to space-api's existing activation listener, which uses Cluster bearer auth. The service is for a data-plane edge that carries a Native files:* request's bytes and leaves every decision to space-api. Nothing calls it yet. The data-plane identity's allowlist entry is the next PR.

  • AuthorizeNativeFileRequest takes the operation, the {sandbox_id} path segment as sent, the client's headers and the edge's request id. It answers with the effective org and owner, or with the exact response the public route renders for the refusal: status, headers and Native envelope, carrying the edge's request id.
  • PrepareNativeFileCall authorizes again, with no cache, so revocation stays immediate. For write, write_batch, mkdir, remove and rename it binds the Idempotency-Key to the request's fingerprint. It then runs the drain gate, the admission charge and the inline wake. It answers with a route grant, or with the rendered refusal. The grant carries:
    • the runtime endpoint the API connected to;
    • the bridge route token over the sandbox's authoritative route identity;
    • for write, write_batch and rename, the guest key.

How the decisions stay the public route's own. Both RPCs run a body-less copy of the request through the public route's own layers: request id, error normalization, require_native_auth, the hidden-operation gate, owner scope, and the native data-plane failover scope. v1_router is split into native_router, and native_file_access_router mounts the 11 files operations behind it with a capture handler (routes/public/files/access.rs). The handler stops where each route's handler would call the sandbox, and keeps that handler's order:

  1. the key check;
  2. the bind;
  3. the guest key;
  4. the runtime grant, with the route's own metric name and the route's own error mapping (unprotected_mutation_error for mkdir, remove and change tracking).

This reuses everything unchanged: session tokens, the injected external console authenticator, federated owners and service-grant upserts, the owner-less key allowlist, owner scope, and federated key namespacing.

Supporting changes:

  • Runtime grant: the spaced client's connect, authoritative-route reload and token mint become SpacedRuntimeGrant (controllers/sandbox/spaced/runtime_grant.rs). The spaced client, its mid-stream reconnect and the RPC all use it.
  • Ingress path: connect_spaced_runtime_client itself is unchanged, because the ingress caller needs no token and would otherwise mint one on every preparation.
  • Endpoint: RuntimeClient records the URI it dialed.
  • Operations: FileOperation in pplx-space-native-files names each route and what it does with its key.
  • Message limits: the listener decodes at most 256 KiB per native file access message (a request's headers). The ingress activation RPCs keep their 1 KiB limit.

Validation

At this head (1640ffba), on main at 0271415a:

  • Rebased onto public-api-http: make Native error rendering usable outside space-api #1364's new head with no conflicts; the commit is unchanged (git range-diff reports it identical).
  • 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-native-files -p pplx-space-api -p pplx-space-node-api-client -- -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-native-files 2 passed; pplx-space-node-api-client 28 passed.
  • cargo check builds the whole workspace against the new proto. No checked-in generated code comes from node/api/v1 protos (the Go shim vendors only the sandbox-manager, gateway, policy, execution and operations protos), so there is nothing to regenerate.

New tests. Each was mutation-checked: the named change was applied, the test failed, and the change was reverted.

Test What it proves Mutation that makes it fail
native_file_access::tests::a_request_without_valid_credentials_is_refused_as_its_route_refuses_it With no credential or a wrong one, the RPC answers 401 with the same body the public files:stat route returns, carrying the edge's request id in the envelope and x-request-id mount the capture handler without native_router's layers
…::a_credential_without_an_owner_is_authorized_for_reads_only An owner-less service key is authorized for stat, list and read with no owner, and gets 403 on every mutation and files:modified. A key acting for a user reports that owner drop the service_credential_may_call refusal in require_native_auth
…::a_federated_owners_key_is_bound_in_its_own_namespace A federated owner's key is bound under native-owner-<sha256(subject, key)>, scoped to the sandbox, and nothing is bound under the raw key remove namespace_idempotency_key from owner scope
…::a_changed_request_is_refused_before_the_runtime_is_reached Port of a_refused_write_never_acquires_the_runtime: with snapshot_storage_locations locked, a changed fingerprint still gets 409 idempotency_conflict while the bound request waits on runtime acquisition grant before bind
…::a_request_is_bound_before_any_grant A prepare whose runtime cannot be reached still holds its key (a changed request is refused), no grant is issued, and a bound operation without a fingerprint is INVALID_ARGUMENT grant before bind; separately, no bind
ingress_activation::tests::remote_activation_authenticates_and_preserves_terminal_status (extended) The listener serves NativeFileAccessApi behind the same bearer check: no token or a wrong one is UNAUTHENTICATED, and the right one reaches the service do not add the service to the listener
pplx-space-native-files::tests::every_files_route_is_one_operation Every manifest files: route is exactly one FileOperation, named by its action map Modified to LIST_FILES

Not validated here: a successful grant against a live runtime. The tests' sandboxes are stopped, so preparation ends at the wake. The Kind run at the top of this stack shows only that Native behaviour is unchanged; it does not exercise the RPCs.

Severity and risk

Severity: does not apply (new internal RPC, no caller).

Risk:

  • Shared code paths. The public files routes keep their handlers. The changes they share with this RPC are native_router (the same layers in the same order, which every_control_plane_operation_is_mounted covers), SpacedRuntimeGrant in the spaced client and its reconnect (the same three steps), and the new RuntimeClient field.
  • New listener surface. It is reachable only with the Cluster bearer or an allowlisted Kubernetes identity, and the data plane's identity is not allowlisted until the next PR.

To roll back, revert this PR.

Rollout

The listener is configured only where SPACE_INGRESS_ACTIVATION_LISTEN is set, as before. A data plane calling an older space-api gets UNIMPLEMENTED. The edge must then keep traffic on space-api.

Stack

This is PR 4 of 5 and depends on #1364 (native-error-rendering-shared). 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. this PR
  5. auth: let the data-plane identity call the native file access RPCs #1371 (data-plane-native-file-access-authz)

Not covered

  • Kubernetes grant endpoint. On Kubernetes the grant's endpoint is the sandbox pod's worker address that space-api dialed, not the node address that data-plane ECW routes use. Whether the data plane may reach it is a network-policy question for the PR that first uses the grant.
  • Header forwarding. The RPC takes the client's headers less body framing, not an allowlist. The external console authenticator decides from headers this repository cannot enumerate, so an allowlist would break parity.
  • Error precedence. Today a missing Idempotency-Key is reported after JSON extraction and before field checks. A caller that validates the body between the two RPCs must check the key first to keep the same message precedence.

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.

Checked authorization and refusal parity: both RPCs run a body-less request through the same native_router layers as the public routes (request id, error normalization, require_native_auth incl. external console/federated owner, hidden-op gate, owner_scope::authorize with key namespacing, data-plane failover scope), decide is reachable only with the RPC-set FileAccessCall extension, and prepare mirrors each handler's key check → bind → guest key → runtime grant with matching metric names, Access activity and unprotected_mutation_error vs controller_error mapping. The SpacedRuntimeGrant extraction and RuntimeClient endpoint plumbing are behavior-preserving (initial vs mid-stream charge flag kept). Non-blocking perf note: every file op now pays two full credential/owner-scope resolutions (Authorize + Prepare) plus RPC round trips; fine given the stated no-cache revocation goal, but worth watching on the hot path.

@NancyKwongPerplexity
NancyKwongPerplexity force-pushed the native-error-rendering-shared branch from 0c7cba6 to 1be4b2a Compare November 3, 2026 19:39
@NancyKwongPerplexity
NancyKwongPerplexity force-pushed the native-error-rendering-shared branch from 1be4b2a to 41953a9 Compare November 3, 2026 20:43
Base automatically changed from native-error-rendering-shared to main November 3, 2026 21:21
Add NativeFileAccessApi beside IngressActivationApi on space-api's
activation listener (Cluster bearer auth), for a data-plane edge that
carries a Native files:* request's bytes and leaves every decision to
space-api. Nothing calls it yet.

- AuthorizeNativeFileRequest takes the operation, the {sandbox_id} path
  segment, the client's headers and the edge's request id, and answers
  with the effective org and owner, or with the response the public
  route renders for the refusal (status, headers, Native envelope).
- PrepareNativeFileCall authorizes again, binds the Idempotency-Key to the
  request's fingerprint for write, write_batch, mkdir, remove and rename,
  and runs the drain gate, admission charge and inline wake. It answers
  with a route grant (the runtime endpoint the API connected to, the
  bridge route token over the sandbox's authoritative route identity, and
  for write, write_batch and rename the guest key), or the rendered
  refusal.

Both run the request through the public route's own layers: v1_router is
split into native_router, and native_file_access_router mounts the files
operations behind it with a capture handler that stops where each
handler would call the sandbox. Credentials, the external console
authenticator, federated owners and grants, the owner-less key
allowlist, owner scope and key namespacing are therefore the public
route's, unchanged and uncached.

The spaced client's connect, authoritative route reload and token mint
become SpacedRuntimeGrant, which the spaced client, its reconnect and
the RPC share; RuntimeClient records the endpoint it dialed. The
ingress path keeps calling connect_spaced_runtime_client alone, since it
needs no token. FileOperation in pplx-space-native-files names the
routes and what each does with its key.
@NancyKwongPerplexity
NancyKwongPerplexity added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit f9f9147 Nov 3, 2026
41 checks passed
@NancyKwongPerplexity
NancyKwongPerplexity deleted the native-file-access-rpcs branch November 3, 2026 21:52
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