Files
Hare 8ecc307fc9 feat: migrate protocol and HTTP to structured interface references
T-20: identify interfaces by exact scope/name pairs; add optional opaque scope_ref metadata and separate shape validation from path-dependent scope compatibility. Preserve call error precedence, strict JSON, validators and transport boundaries. Mechanically migrate consumers and document API/wire compatibility and deferred lifecycle semantics.

Validation: cargo fmt --all --check; git diff --check; cargo check --workspace --all-targets; cargo test -p wip-protocol -p wip-http; cargo test --workspace (221 passing tests including doctests); cargo test --workspace --all-targets; nix develop --command cargo clippy --workspace --all-targets -- -D warnings; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps. All pass. cargo/rustc 1.95.0; development-shell Clippy/rustc 1.97.1.
2026-10-07 18:29:51 +09:00
..

wip-fs-server

wip-fs-server projects one configured Linux filesystem directory onto WIP Worldspace / at request time. It does not register the filesystem contents or keep one object instance per entry. It uses wip-host for publication and typed operation dispatch and uses the standard wip-http routes, JSON codecs, envelopes, and HTTP status mapping.

Library assembly and migration

The existing ServerConfig, WipFsServer::new, handle_http, CLI flags and wire representation remain compatible. The formerly private projection has become a public, generic library engine. No package version or publication policy changes are required.

Use default_projection(root, access, limits) to obtain the default Linux engine without starting a listener. projection.view(&principal) is the effective view; it owns canonical observe and fetch_interface and implements the standard wip_host::ObjectProvider typed call_operation API. DefaultPolicy has the unit principal (); default engines also offer unit-principal convenience methods.

For an external backend/composition:

  1. Implement BackendEntry on a local, cloneable, root-confined stable capability. Its WipObject metadata is public-only; its validator binds identity and the protected state. Implement FilesystemBackend to resolve one connected ResolvedEntry<Entry> (root-to-parent ancestry plus pinned target), enumerate bounded children, and execute against that already-resolved lineage.
  2. Register generated #[wip_interface::interface] definitions using the existing wip_host::InterfaceRegistry. DefaultInterfaces::register optionally supplies reusable built-in tokens; using those tokens does not force their publication.
  3. Implement Composition<Entry> on a local type. Its local Object wraps the entry and implements both WipObject and the chosen generated traits, avoiding orphan rules. PublishedObject::expose proves each implementation at compile time. Publication can add, replace, reorder or omit built-ins based on entry metadata and actual backend capabilities, not just entry kind. LinuxEntry implements bounded FileRead, FileWrite and SymlinkInspect for safe delegation; never advertise write in read-only mode or link operations on a non-link capability.
  4. Assemble Projection::new(backend, registry, composition, policy, limits). ProjectionPolicy supplies an opaque associated Principal, hides Objects and entire hidden subtrees, selects an ordered subset of implemented interfaces, controls descriptor/operation visibility, and permits or denies execution.
  5. An authenticated deployment chooses the principal and ExecutionContext. To use HTTP, construct HttpAdapter::new(&view, &endpoint, limits) and invoke handle_http_with_context. This uses the standard wip-http binding, not a filesystem wire profile. Enforce body collection, header, concurrency and deadline bounds in the deployment's listener; the adapter owns none of these intake resources. The bundled loopback listener remains a thin default user.

A complete external-style memory backend, local Object with multiple custom Operations, typed registration, principal policy and HTTP flow is exercised in tests/extensions.rs. It uses no Linux filesystem for its backend. The public library APIs do not require private types, a fork, an HTTP listener, a global object registry, or dynamic plugin loading.

Extension security contract

Backend implementations and Operation authors are trusted code, not sandboxed plugins. FilesystemBackend::resolve returns ResolvedEntry::new(ancestors, target) from one connected backend traversal/snapshot, not independent path-prefix resolutions. A root target has no ancestors; other targets retain the root first, then intermediate parent containers. This is backend-private context, never wire metadata. Backend resolve and execute signatures both carry this type; compositions still receive only the exact scoped target Entry.

FilesystemBackend::execute must validate ancestor membership/identity and refresh all lineage state/validators inside its transaction or lock before calling the engine callback, retaining the same target and connected ancestry through validation and dispatch. Replaced/detached ancestors must return NotFound without callback invocation, even when the replacement is visible. The engine evaluates Object visibility for every refreshed ancestor and target inside that same boundary; a hidden ancestor rejects the entire subtree before publication/typed dispatch. Never fix a changed lineage by resolving the raw path again and substituting capabilities. Participating policy-relevant backend state changes must be serialized by this boundary, not just target-content writes. Return the committed post-operation validator. Retained ancestry is subject to handle/work ceilings; resource exhaustion is an error, not missing ancestry. Confirm rollback only after rollback succeeds. Pre-dispatch failures must never commit; failed started operations without a non-commit guarantee remain OperationOutcomeUnknown. Object implementations must delegate validators to that entry and declare requirements per (InterfaceRef, operation), rather than classify all operations by a read/write name. Operations use the scoped entry and propagated context, not a fresh raw-path resolution; they must check deadlines, cancellation and resource budgets throughout backend work and report effects truthfully. POSIX symlinks, identity, atomic mutations and locking cannot be assumed for other backends.

The default Linux backend acquires advisory exclusive locks in root-to-parent order and then on the lockable target (directory or regular file). It compares retained ancestor identity to no-follow descriptor-relative lookup results, refreshes lineage snapshots after acquiring the locks, and holds the guards through post-dispatch state observation. The configured root remains its pinned capability even if its textual path is renamed; replaced intermediate ancestors are rejected. Leaf path replacement never redirects execution to a new inode. This serializes cooperating library calls; it does not promise transactional isolation from unrelated processes ignoring flock. Linux capabilities only expose bounded methods, not raw descriptors or backend paths.

A policy must be stable/deterministic within one effective Worldspace view. Interface/operation visibility is uniform per registered reference within that view, so observation, fetch and dispatch use the identical filtered descriptor. For entry-specific operation sets, register distinct interface definitions and select them by entry. Different principals may have different effective views. Changing a policy must establish a new view/reference lifetime (e.g. rebuild the registry/engine); do not keep validator-free references while changing their meaning. Execution permission may deny a visible Operation without hiding it. A published interface is always fetchable by that principal; hidden operations are omitted (along with their unreachable named types and interface-wide docs) and guessed calls return OperationNotFound, whereas visible denied operations return PermissionDenied. Hidden Objects and descendants return NotFound and are absent from parent observations. Work spent scanning hidden entries still counts against traversal bounds; resource limits are not a constant-time/side-channel guarantee.

Never put absolute paths, hidden metadata, credentials or policy internals in public WipObject metadata, descriptions, validators or normal logs. Validators must be opaque public tokens, not confidential content. Engine/HTTP failures use bounded generic diagnostics; arbitrary trusted implementation metadata/logging cannot be sanitized automatically. Do not log credentials or sensitive panic payloads from extensions.

Security model

  • The effective root is opened once as a directory capability. Root path components must be real directories (symlink components are rejected), and the displayed effective root is read back from that same pinned descriptor. Every request is resolved segment-by-segment with directory-relative, O_NOFOLLOW handles.
  • Symlinks are projected as links and are never followed. Their stored target is available only through the SymlinkInspect.read_link application operation.
  • Regular files, directories, symlinks, and unsupported entry types have distinct public descriptions and capability sets.
  • WIP names are Unicode. Linux names that are not valid UTF-8 are deliberately not published; valid UTF-8 names map byte-for-byte and therefore cannot collide. Unicode paths containing backend-invalid NUL bytes are likewise treated as unpublished rather than as backend failures.
  • No object ref is issued. This avoids claiming representation identity for hard links or path-specific names. Regular files and symlinks receive an opaque validator containing filesystem identity plus protected metadata; replacing a path therefore does not preserve its validator.
  • Protocol metadata and errors never contain the backend absolute path. Startup prints only the explicitly configured effective root and security-sensitive modes.
  • The built-in HTTP listener is loopback-only. TLS termination, certificates, authentication, an externally visible URL scheme, and proxy routing are deployment concerns rather than separate WIP transport profiles.

The implementation currently targets Linux because it uses O_PATH, directory-relative descriptor traversal, and /proc/self/fd to reopen the same pinned backend object without resolving the untrusted Worldspace path twice.

Run

Run the loopback listener in read-only mode:

cargo run -p wip-fs-server -- \
  --root /srv/public-files \
  --listen 127.0.0.1:8080 \
  --base-path /wip

Run with the write capability deliberately enabled:

cargo run -p wip-fs-server -- \
  --root ./example-files \
  --listen 127.0.0.1:8080 \
  --base-path /wip \
  --read-write

The server reports the effective root, ReadOnly/ReadWrite mode, endpoint, and actual listen address. It does not enumerate the root.

Standard interactions

The following examples use the built-in HTTP listener. The JSON follows the standard WIP over HTTP binding; validators and operation Bytes are base64 body fields, never ETag or If-Match headers.

Observe only the root Object:

curl -sS -X POST http://127.0.0.1:8080/wip/v1/observe \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  --data '{"path":"/","depth":0}'

Observe the root and two levels of indexable children:

curl -sS -X POST http://127.0.0.1:8080/wip/v1/observe \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  --data '{"path":"/","depth":2}'

A regular-file observation returns a structured read interface reference and an object validator. Built-in legacy registrations use scope / and copy the opaque registration string verbatim into name; they do not emit scope_ref. Copy the complete reference from the observation to retrieve that descriptor:

curl -sS -X POST http://127.0.0.1:8080/wip/v1/fetch_interface \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  --data '{"interface":{"scope":"/","name":"<read-interface-name-from-observation>"}}'

Then call a bounded range read:

curl -sS -X POST http://127.0.0.1:8080/wip/v1/call_operation \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  --data '{
    "target":{"path":"/notes.txt","validator":"<base64-validator>"},
    "interface":{"reference":{"scope":"/","name":"<read-interface-name>"}},
    "operation":"read",
    "arguments":{"offset":0,"length":4096}
  }'

In --read-write mode a second regular-file interface declares replace. Writes require the current object validator and return the post-write validator:

curl -sS -X POST http://127.0.0.1:8080/wip/v1/call_operation \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  --data '{
    "target":{"path":"/notes.txt","validator":"<base64-validator>"},
    "interface":{"reference":{"scope":"/","name":"<write-interface-name>"}},
    "operation":"replace",
    "arguments":{"contents":"dXBkYXRlZA=="}
  }'

Resource and failure behavior

ResourceLimits bounds request/response bytes, HTTP request-head bytes, JSON nesting, file read/write bytes, tree depth/node count, pinned handles, and active connections/requests. One absolute request deadline covers header and body intake, dispatch, cooperative filesystem work, and response transmission; stalled clients are closed and cannot occupy a worker beyond that bound. Workers accept connections directly, so the listener never moves more than the configured concurrency into active userspace service. Advisory file-lock acquisition uses a nonblocking loop and checks the same deadline/cancellation rather than allowing a contended lock to stall a worker. Object observation recursively opens only the requested depth. A successful response is complete through that depth; if the configured projected-node budget would be exceeded, the interaction fails with ResourceLimitExceeded instead of returning a partial observation. I/O failure and cancellation remain errors.

Whole-file replacement compares the request validator against the identity and state of the same pinned file handle used for the write. Failures before mutation carry a NotCommitted guarantee and map to Internal or ResourceLimitExceeded; a failure after truncation/write begins maps to OperationOutcomeUnknown. The server and Clients do not automatically retry side-effecting operations.