Files
qbit-record-files/docs/INGESTION-CONTRACT.md
james.maesandClaude Sonnet 5.5 3f08c584d0
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 3d75c629c2706be133505268b6696fe9b1ae420c] (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Failing after 7s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 6s
feat: allow host-controlled admission for captured files
Keep the default quarantine policy for ordinary uploads and captures without a provider-resolved device identity. Audit the selected policy without fabricating a release or review event.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 08:23:14 -06:00

9.9 KiB
Raw Permalink Blame History

Record Files ingestion contract

The native record-files semantic extension is implemented and registered when the host configures uploads. Version1.0.0 is the wire contract version, not a production release claim. Generated contract: record-files.yaml. Four real HTTP tests and the deterministic schema test passed in target/upload-api-http-verification.log; broader integration and provider/device qualification remain in progress.

Ownership

Record Files owns staging, immutable originals, ordered associations, review and file history. Originals go through standard Collaboration AttachmentsService; there is no second canonical byte store. Android owns capture/gallery UX and temporary local files. Mobile Scanners supplies verified server-side device attribution through api.CaptureIdentityProvider. Neither scanner nor printing is a runtime dependency of Record Files. Mobile v1 is online-only with resumable interrupted requests.

Capture authorization through commit

The revised Java SPI adds <T> T executeAuthorized(Optional<CaptureIdentity> identity, Operation<T> operation) throws QException; nested Operation<T>.run() returns T and throws QException. The provider revalidates the exact persisted identity under its authorization guard and invokes the operation synchronously while retaining that guard through all commits. The same authority guard must cover device revocation and session termination through their commits. Returning a Future, merely rechecking once, or releasing before the callback's actual persistence completes does not satisfy this contract.

Present identities default to refusal until a device adapter implements the guard. The compatibility default permits only verified empty desktop attribution and rechecks that capture() is still empty. An invalid device-bearing request must throw rather than return empty. Guarded work repeats capture/revalidation, so the provider must support same-thread reentrant read validation. Providers document their exclusion scope: a single-JVM lock does not establish cross-node revocation protection.

Record Files acquires capture authority before opening its transaction or calling the attachment owner. Creation covers quota/session commits; subsequent guards cover expiry, chunk object writes and metadata commit, cancellation, finalizer claims/renewals, retained intake, collection/checkpoint commits, completion receipts and failed-attempt transitions. Caller-owned staging cleanup is also guarded through deletion and its commits. Privileged maintenance retains its independent permission and does not impersonate the original device. Existing caller-owned transactions are refused so a later external commit cannot outlive the guard. Bounded chunk-body reads happen before acquiring the guard; authority is checked again before writing. Deployment storage timeouts and provider lock fairness must bound the remaining storage work inside the scope.

Frozen0238 retains the previous SPI and is historical integration evidence. New device adapters must use a separately verified artifact containing this contract; do not silently repin a running consumer. Root verification and Mobile's real revoke-versus-commit/HTTP acceptance remain distinct gates.

Registered operations

All paths below start with /qqq/v1/qbit/record-files. Native operation IDs are the method plus extension-relative path, for example POST /qbit/record-files/uploads.

Method Path Contract
GET /capabilities?tableName=...&recordId=... Both query fields required exactly once. Returns {version,canUpload,resumable,offline,limits} under current source rights.
POST /uploads Strict immutable JSON manifest; creates or recovers the same actor/request intent.
GET /uploads/{uploadId} Authorized persisted status and final receipt.
PUT /uploads/{uploadId}/items/{itemId}/chunks/{chunkIndex} Exact binary chunk; identical retry is accepted.
POST /uploads/{uploadId}/finalize No body; Idempotency-Key must equal the original manifest requestId.
POST /uploads/{uploadId}/cancel No body; cannot delete committed retained originals.

Every mutation requires X-Record-Files-Request: 1. This is a non-simple browser protocol header, not authentication. Browser Origin must match the request origin or an exact deployment-approved origin; no wildcards. Native machine callers may omit Origin but still need the protocol header and normal authentication. Current actor, record visibility, verified device/session, client-channel write restrictions and scoped-credential ceilings are checked before effects. The API never manufactures a user session or stores browser credentials.

Chunk requests require Content-Type: application/octet-stream, exact Content-Length, and X-Chunk-SHA256 containing64 lowercase hex characters. The manifest uses application/json; encoded request bodies are refused. Unknown fields, duplicate JSON keys, scalar coercions and trailing data are rejected. Malformed input400, missing/invisible upload404, conflicting immutable intent/state409, exhausted quota429, unavailable deployment503. Errors use the native {error,code,message,traceId} body and omit storage/database details.

Manifest and progress

Create body: {requestId,parent:{tableName,recordId},title?,items:[{itemId,position,fileName,declaredMediaType,sizeBytes,sha256,origin,caption?,categoryId?,capturedAt?}]}. Request/item IDs are stable lowercase UUIDs. Positions are contiguous from zero. Category IDs are optional positive native integers. Parent IDs stay opaque strings until the owner resolves their canonical key. Origin is UPLOAD, CAMERA, GALLERY or SCANNER_IMPORT; capturedAt is untrusted supplementary provenance, never device authority.

The server scopes request identity to the authenticated actor within its QQQ instance. It reauthorizes the canonical parent and compares the full immutable manifest on every replay. Mutable session permission/security collections are deliberately excluded from identity; they are authorization inputs, not idempotency keys. Reusing an actor/request with a different authorized manifest returns409.

Status: {uploadId,state,expiresAt,limits,items:[{itemId,state,receivedChunks}],receipt}. Upload states: OPEN, FINALIZING, COMPLETE, FAILED, CANCELLED, EXPIRED. Item states: PENDING, RECEIVING, RECEIVED. receivedChunks is a list of zero-based durably recorded indices. Receipt is null until COMPLETE; a disconnected finalizer can be polled/retried using the same identity.

Final receipt: {uploadId,collectionId,state,completedAt,items:[{itemId,fileId,versionId,attachmentId,sizeBytes,sha256,position,lifecycleAtCommit}]}. fileId is the logical file-key string; versionId is the immutable version-row integer; attachmentId/collectionId are standard native integer IDs. COMPLETE means exact originals, associations and outcomes committed. It does not imply content release, download, printer acceptance or physical output. New files normally remain QUARANTINED and collection OPEN until review/release; clients may show Saved only after COMPLETE, and must obtain current download permission separately.

Deployment policy and lifecycle

UploadConfiguration requires a dedicated staging bucket alias, immutable limits, per-actor quota policy and trusted Java CaptureIdentityProvider. It rejects sharing the canonical attachment alias. A host may additionally configure exact approved browser origins. Uploads have no implicit desktop adapter: the host must distinguish genuine desktop requests from device-bearing requests and reject missing device attribution. Capability support is advertised only for explicit configuration and supported durable metadata storage; actual object-store/device acceptance still requires deployment qualification.

Hard ceilings:20MiB per original,200MiB total,100items,64KiB–4MiB fixed chunk size,60–86400second staging lifetime,256KiB manifest JSON. Deployment limits can be lower and are frozen per upload. The original20MiB bound reflects the conditional single-PUT owner path, not a throughput benchmark. Accepted media types are declarations subject to quarantine/scanning, not verified file contents.

Initial lifecycle: finalized items are QUARANTINED unless the host sets the Java-only quarantineCapturedFiles=false, which makes only CAMERA/GALLERY items (origin CAPTURE) with the provider-resolved, revalidated device identity AVAILABLE. The manifest carries no policy, device or actor field and strict parsing rejects them. This is a host trust decision (approved devices, bounded media types), not a scan or review result; it emits no release event and leaves review status unchanged. See README "Not a 1.0".

Durable quota defaults:10 active uploads,512MiB reserved staging bytes and100 new uploads/hour per actor. Cancel/expiry does not immediately release capacity: bounded exact-key cleanup must finish and commit its cursor/outcome/quota release. UploadMaintenance.cleanupOwn permits currently authorized terminal cleanup. A trusted host scheduler can invoke bounded cleanupBatch under record-files.maintain; this does not grant content access. Expired finalization fences commit before irreversible staging deletion. Failed deletion/outcome retains cursor and quota for safe retry. Canonical originals are outside cleanup.

Remaining integration and release evidence

A persistent mobile HTTP demo and real verified-device adapter acceptance remain pending. Camera transport must stay disabled until discovered capabilities and that integration succeed. Real object-store conditional creation, indefinite retention policy, malware/type policy, restore, supported production-database qualification, background maintenance scheduling, capacity measurements, browser/native UX and final security review are still release gates. Tests use actual native HTTP and file-H2 transactions with synthetic storage/auth adapters; they do not qualify a production storage provider or hostile execution runtime.