feat(drive): expose authorized typed Workspace Drive API
T-722: reuse T-721 storage/CAS/receipts with clone-bound transaction authorization against attached Server authority. Add migration 84 audited Worker read_only/read_write grants and removal/deletion fencing, member-facing ordinary access, owner-controlled grant settings, Workspace-bound decimal entry references, bounded text/binary chunks, streamed HTTP receive/download with integrity and safe headers, typed errors and generated Rust/OpenAPI/TypeScript contracts. Validation: root cargo check; server-api 83 default / 91 typescript tests; FeatureStorage 16; Drive 31 unit + 12 integration; Workspace Server 691 library + 11 binary tests (--test-threads=4), including 9 real DB/LocalFileSystem HTTP tests for two Workers/non-owner usage, revoke during reception, concurrent CAS, response-loss receipts, chunk expiry and safe binary media. cargo fmt --all -- --check and git diff --check HEAD passed. Existing OpenAPI schemas/routes semantically unchanged; dependent TS digest projections regenerated.
This commit is contained in:
@@ -5,8 +5,9 @@
|
||||
Drive is a Workspace-owned **latest-version document tree**, not a mounted or
|
||||
POSIX-compatible filesystem. `workspace-drive` owns storage, hierarchy, revision
|
||||
conflicts, mutation receipts, bounded reads/search, and garbage collection.
|
||||
`WorkspaceApi::drive` is the trusted Server adapter. Authorization and HTTP DTOs
|
||||
belong to T-722; Worker/Web clients and end-to-end routing belong to T-723–725.
|
||||
`WorkspaceApi::drive` is the trusted Server adapter. The authorized HTTP API and
|
||||
Backend-managed grants are documented below. Worker Tools/WIP, Web UI, and full
|
||||
end-to-end routing belong to T-723–725.
|
||||
Session attachments remain Session-owned and are not Drive authority. Worker,
|
||||
Session, and Workdir cleanup do not delete Drive.
|
||||
|
||||
@@ -262,3 +263,106 @@ The hermetic backup test stops writers, snapshots actual FeatureStorage SQLite,
|
||||
copies actual LocalFileSystem blobs, restores into absent roots, and checks stable
|
||||
IDs/latest content/receipts and non-reused ID allocation. It does not validate a
|
||||
production backup scheduler, volume snapshot, or physical power cut.
|
||||
|
||||
## Authorized typed HTTP API
|
||||
|
||||
The Rust `server-api` contract owns `/api/w/{workspace_id}/drive/` operations:
|
||||
|
||||
- `GET root`, `metadata`, `list`, `search`, `read-text`, `read-chunk`, `download`;
|
||||
- `POST mutate` (create folder/bounded UTF-8 text, replace text, relocate, delete);
|
||||
- `PUT upload` (binary body with flat query metadata);
|
||||
- `GET requests/{request_id}` for the committed receipt snapshot;
|
||||
- owner-controlled `POST/GET grants` and `DELETE grants/{grant_id}`.
|
||||
|
||||
Ordinary usage follows the existing member-facing authenticated Server-user
|
||||
policy. Non-ownership alone does not deny use. Grant management follows the
|
||||
existing Workspace-owner account boundary. No new general membership ACL is
|
||||
introduced. Browser/API-token identity is supplied by authentication middleware;
|
||||
user/account existence and active Workspace state are checked again in the
|
||||
operation transaction. Cookie mutations require the configured application
|
||||
Origin through the existing middleware. No ordinary DTO accepts an actor.
|
||||
|
||||
Runtime-forwarded requests require the existing signed source proof and live
|
||||
Runtime/Worker catalog membership. Workers receive an explicit whole-Drive
|
||||
`read_only` or `read_write` grant, independently of Profile, Subject, Ticket,
|
||||
Workdir or commands. Incomplete/mismatched Runtime or Worker identity cannot fall
|
||||
back to a browser actor. Grant creation rechecks owner, active Workspace, and
|
||||
existing live Worker in the Server DB transaction; active access changes require
|
||||
revoking the previous grant first (no silent downgrade). Migration 84 stores
|
||||
creation/revocation actor and time. Removal fences and catalog deletion revoke
|
||||
access and retain audit; grant rows cascade with Workspace deletion. No alternate
|
||||
Worker registry or Drive Workdir attachment is created.
|
||||
|
||||
A clone-bound Drive authorizer queries the attached Server authority under every
|
||||
client-operation `BEGIN IMMEDIATE`, including receipt replay/result queries.
|
||||
The lock spans validation, blob persistence and DB publication. Grant revocation
|
||||
and Worker removal use the same Server DB write authority: whichever transaction
|
||||
wins first is ordered first. A mutation committing before revocation is valid;
|
||||
a revoked cached handle cannot commit later. Upload admission also checks access
|
||||
before consuming the body, but that admission is not a lease: publication always
|
||||
rechecks current access. Download checks authorization separately for every
|
||||
chunk, and stops on revocation, deletion, revision expiry or storage failure.
|
||||
|
||||
References contain **both** Workspace ID and stable decimal node ID. JSON uses
|
||||
`DriveEntryRef`; flat GET/binary queries require `entry_workspace_id` matching the
|
||||
route Workspace before resolving `id`/`parent_id`. IDs/revisions are canonical
|
||||
positive decimal strings within signed 64-bit range; file sizes/offsets are
|
||||
bounded `u32`. List/search cursors carry Workspace identity and the last node ID;
|
||||
they are continuation hints, not capabilities or snapshots. Returned `latest_url`
|
||||
is a relative authenticated URL bound to Workspace and ID, independent of logical
|
||||
name/parent. It confers neither public sharing nor approval. File URLs download;
|
||||
folder URLs return metadata. Deletion/recreation never reuses the old reference.
|
||||
|
||||
Web upload consumes binary HTTP frames with an observed **16 MiB** ceiling and
|
||||
checks declared size and lowercase SHA-256 before publication. Oversized announced
|
||||
or received data, mismatches, aborted bodies and I/O errors never return success.
|
||||
The storage boundary currently requires a whole-file Vec, so uploads retain at
|
||||
most one bounded binary file in memory before calling T-721; they are not
|
||||
constant-memory object-store multipart uploads and are never expanded to
|
||||
JSON/base64. Runtime source-proof verification separately buffers a bounded body
|
||||
before granting trusted ingress, as in other signed Server operations. Web
|
||||
downloads stream **64 KiB** revision-fixed chunks; they do not materialize the
|
||||
whole file. Generated Rust clients retain their explicit bounded-response policy
|
||||
and return exact `BinaryBody` bytes. Worker JSON-facing adapters can request
|
||||
bounded text or binary chunks through this same contract, but Tools/WIP and
|
||||
model-context/image presentation belong to T-723; this API does not emit large
|
||||
base64 JSON blobs.
|
||||
|
||||
Downloads validate bare MIME types and always use `Content-Disposition:
|
||||
attachment` with percent-encoded UTF-8 names, `nosniff`, a sandbox/default-none
|
||||
CSP, `private, no-store`, and a Workspace/node/revision ETag. Arbitrary HTML/SVG
|
||||
bytes are storable, not safe app-origin inline previews. Content length is exact;
|
||||
if a later chunk loses authority or expires, the response stream fails rather
|
||||
than returning another generation or a successful truncated file. Even an empty
|
||||
file validates its referenced blob before HTTP success. Conditional 304/range
|
||||
HTTP semantics are not added; revision-selected chunk reads are explicit.
|
||||
|
||||
Errors use fixed, path-free typed codes: `denied`, `not_found`, `conflict`,
|
||||
`invalid`, `limit`, `storage_unavailable`, `outcome_unknown`. SQL/commit/task
|
||||
uncertainty at a mutation boundary is `outcome_unknown`/`unknown`; known rejection
|
||||
or blob persistence failure is `not_committed`. A successful DB commit alone
|
||||
returns a mutation response. After a lost response, query the same request ID;
|
||||
`uncommitted` only means no committed receipt in that snapshot and must not be
|
||||
interpreted as proof that an in-flight request can never commit. Exact replay uses
|
||||
the same identity/payload/transport-bound actor and current permissions. Never
|
||||
blindly create a fresh request ID to resolve an unknown outcome. Physical paths,
|
||||
blob keys and raw provider errors are not public.
|
||||
|
||||
Contract regeneration/freshness:
|
||||
|
||||
```sh
|
||||
cargo run -q -p server-api --example export_openapi -- openapi/server-api.json
|
||||
cargo run -q -p server-api --example export_openapi -- --check openapi/server-api.json
|
||||
cargo run -q -p server-api --features typescript --example generate_drive_api_types > web/workspace/src/lib/generated/drive-api.ts
|
||||
deno fmt web/workspace/src/lib/generated/drive-api.ts
|
||||
cargo test -p server-api --features typescript generated_drive_api_contract_is_current
|
||||
# Existing OpenAPI-derived projections embed the whole-contract digest:
|
||||
for example in generate_repository_openapi_types generate_runtime_api_types generate_ticket_api_types generate_companion_api_types generate_worker_launch_api_types; do
|
||||
cargo run -q -p server-api --example "$example"
|
||||
cargo run -q -p server-api --example "$example" -- --check
|
||||
done
|
||||
```
|
||||
|
||||
The existing BinaryBody contract also supports binary **success responses**,
|
||||
including declared response headers, without changing binary request bytes or
|
||||
JSON/error behavior. It exports inline OpenAPI binary bodies, not JSON byte arrays.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# T-722: SubWorker cwd and logical path friction
|
||||
|
||||
During delegation from the assigned repository Workdir, a child started with
|
||||
`cwd: crates/server-api` and readable policy paths under repository-root
|
||||
`docs/development`. Its Read calls using repository-relative logical paths could
|
||||
not read the required testing strategy; absolute paths were rejected as invalid
|
||||
logical paths too. The child correctly stopped without edits. The parent
|
||||
inspected the committed child session, stopped that idle child, and recreated it
|
||||
with repository-root cwd and explicit read-only repository scope plus write scope
|
||||
limited to server-api. Policy reads and implementation then succeeded.
|
||||
|
||||
The current observable behavior appears to root child filesystem tools at the
|
||||
selected cwd, rather than making cwd only the command default as the spawn-tool
|
||||
description states. This makes repository-root logical paths surprising. Consider
|
||||
keeping the attachment root invariant and applying cwd only to command execution,
|
||||
or documenting/returning a distinct filesystem base explicitly. Do not encourage
|
||||
absolute paths, parent traversal, or broader write grants as a workaround. This
|
||||
report records a tool usability issue, not additional Ticket requirements.
|
||||
Reference in New Issue
Block a user