feat: add bounded filesystem projection server

This commit is contained in:
2026-09-30 08:53:44 +09:00
parent fdfaa3a685
commit df51da3d03
10 changed files with 2529 additions and 3 deletions
+136
View File
@@ -0,0 +1,136 @@
# 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-https` routes, JSON codecs,
envelopes, batching, and HTTP status mapping.
## Security model
- The effective root is opened once as a directory capability. 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.
- 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 listener is cleartext and loopback-only. Production deployments
must pass `--behind-tls-proxy` and terminate HTTPS at a trusted reverse proxy.
Plain development HTTP requires the explicit `--development-http` flag.
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
Read-only behind a TLS-terminating reverse proxy:
```console
cargo run -p wip-fs-server -- \
--root /srv/public-files \
--listen 127.0.0.1:8443 \
--base-path /wip \
--behind-tls-proxy
```
Local development 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 \
--development-http \
--read-write
```
The server reports the effective root, `ReadOnly`/`ReadWrite` mode, transport
mode, endpoint, and actual listen address. It does not enumerate the root.
## Standard interactions
The following examples use development HTTP for readability. The JSON is the
standard `wip-https` profile; validators and operation `Bytes` are base64 body
fields, never `ETag` or `If-Match` headers.
Fetch root:
```console
curl -sS -X POST http://127.0.0.1:8080/wip/v1/fetch \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{"path":"/"}'
```
Fetch a bounded tree (the root counts toward `limit`):
```console
curl -sS -X POST http://127.0.0.1:8080/wip/v1/fetch_tree \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{"path":"/","depth":2,"limit":100}'
```
A regular-file fetch 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-fetch>"}'
```
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=="}
}'
```
`fetch`, `fetch_tree`, and `fetch_interface` can also be combined through the
standard `POST /wip/v1/batch` route. `call_operation` is never batchable.
## Resource and failure behavior
`ResourceLimits` bounds request/response bytes, JSON nesting, batch items, file
read/write bytes, tree depth/node count, pinned handles, concurrent requests, and
a cooperative deadline. Tree traversal is breadth-first and opens only requested
entries. Once a limit or depth boundary is reached it reads only far enough to
prove whether an additional visible child exists, so `truncated` is true only
when something was actually omitted. I/O failure and cancellation remain errors,
not clean exhaustion.
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.