Resolve one connected lineage and retain its root-to-parent capabilities. Backends validate ancestor identity/membership and refresh all policy metadata inside the same boundary as target validators and typed dispatch; hidden/replaced ancestors reject as NotFound without invocation or effects. Preserve pinned leaf and configured-root capabilities. Covers MR ANCESTOR_VISIBILITY_TOCTOU with HTTP memory and Linux replacement/budget/guard regressions; all T-19 workspace acceptance commands pass (Clippy via Nix shell).
256 lines
13 KiB
Markdown
256 lines
13 KiB
Markdown
# 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`](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:
|
|
|
|
```console
|
|
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:
|
|
|
|
```console
|
|
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:
|
|
|
|
```console
|
|
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:
|
|
|
|
```console
|
|
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 the opaque read interface reference and an object
|
|
validator. Retrieve that descriptor with:
|
|
|
|
```console
|
|
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":"<read-interface-from-observation>"}'
|
|
```
|
|
|
|
Then call a bounded range read:
|
|
|
|
```console
|
|
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":"<read-interface>"},
|
|
"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:
|
|
|
|
```console
|
|
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":"<write-interface>"},
|
|
"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.
|