Skip to content

Client-facing blob contract for CC 3.3.13 / 5.3.2.6: one cross-platform shape to create, read, and enumerate blobs — and the virtual drive over everything a user can reach #615

Description

@emooreatx

Context

CIRISConstitution#104 is ruled on rc5 (@44ae7b2, PDF rc5.17): CC 3.3.13 now carries the multimedia Source struct (IANA format, RFC 6381 codec, required size, captions as a VTT blob ref, IPTC Digital Source Type for AI disclosure, placeholder/name/content_digest/derived_from, no tier bit), and new CC 5.3.2.6 makes the render tier receiver policy computed from verified, sniffed bytes. size was generalised to everything evidence_refs[] cites. SVG is C + optional PNG rendition.

#614 is the node's ingest pipeline. This issue is the other half: the contract the KMP client (Android, iOS, desktop JVM, wasmJs) speaks to create, read, and enumerate blobs. Today the client has none of it — attachments ride inside a chat message as a base64 string in memory, 10 MB cap, images+PDF+DOCX only, wasm picker stubbed, and no /v1/* blob route at all (verified in client/shared/.../FilePicker.kt and CIRISApiClient.kt). We would rather ask for the ideal shape once than bolt five platform-specific ones onto the substrate that already exists (BlobPointer.epoch, BlobPuller, holds_bytes:sha256:* with 24 h TTL, ContentMiss withdrawals, structural invisibility for self|family per CC 5.2).

The client's job under CC 5.3.2.6 is narrow and should stay narrow: canonicalise-then-hash on send, verify-then-sniff-then-policy on receive, render renditions only. Everything below is what the node has to expose for that to be possible on all four platforms with one commonMain implementation.

1. Create (the sender side)

What a client has at hand on every platform is a stream (a Source/InputStream/ReadableStream), never a whole file in memory — iOS and wasm cannot hold a 100 MB video as a ByteArray, and desktop should not.

Ask for a create shape with these properties:

  • Streaming upload with resume. Chunked PUT/PATCH against a session (tus-style or equivalent): the client declares size and format up front (CC 3.3.13 requires both), streams bytes, and can resume from an offset after a network drop — mobile uploads of video are interrupted routinely. The node computes the sha256 as bytes arrive and the session completes only if the client's declared digest matches (the client hashes the same stream as it sends; both sides have the digest at the end, no second pass over the file).
  • Descriptor submission is separate from bytes. The CC 3.3.13 Source struct (with placeholder.thumbhash the client computed from its own decode, width/height/duration_ms, codec, name) is submitted as the attestation payload referencing the digest. The node validates the struct against CC 3.3.13 (RFC 6838 grammar, codec present for MP4, size == bytes received) and refuses on mismatch — server-side validation is the only place the grammar check is authoritative.
  • Sender-side canonicalisation is the client's duty, not the node's (CC 5.3.2.6 rationale: an intermediary that is the hash authority must never re-encode). The client will convert AVIF/HEIC → JPEG/PNG and WebM/MOV → H.264/AAC-LC MP4 via the platform encoder before hashing. Please expose GET /v1/media/policy (or equivalent) returning the node's current Tier-A set and caps, so the client canonicalises to what this node will admit rather than a compiled-in list — the constitution says the table is recommended, not normative, and nodes may narrow it.
  • Renditions the node produces (Media ingest pipeline against CC 3.3.13 / 5.3.2.6: sniff → size → full-SHA → memory-safe decode → canonical re-encode → rendition + descriptor #614) must be discoverable from the original. A derived_from index: given a digest, list the renditions (display, thumb, poster) with their own digests, format, size, dims. The client renders the rendition and never the original.
  • Scope at creation. cohort_scope is set with the attestation, and for self|family the node must not emit holds_bytes (CC 5.2). The client needs to know, per blob, whether it is structurally invisible — that decides whether "share to Neighbours" is a promotion (supersedes → new scope → holds_bytes at promotion time) and the UI must say so.

2. Read (the receiver side)

  • Fetch by digest, with Range. GET /v1/blob/{sha256} honouring HTTP range requests, because a client scrubbing a video (or AVPlayer/Media3 buffering) reads byte ranges, and because CC 5.3.2.5 verification of a 100 MB file on a phone must be streaming — sha256 over the stream as it lands, compare at the end, refuse before handing to the decoder. If the node serves partial content it must also serve the total size (Content-Length on a full GET, Content-Range on a range) so the client can pre-check size before hashing (the OCI rule CC now carries).
  • Descriptor first, bytes second. GET /v1/blob/{sha256}/descriptor returns the CC 3.3.13 struct so the UI can show the thumbhash placeholder and layout at width/height before any byte of the blob arrives — that is the one thing CC 5.3.2.6 lets a client show pre-verification.
  • The node tells the client which holder it fetched from and whether it fell back, so a ContentMiss on a stale holds_bytes (24 h TTL) surfaces as "this file's holder is gone" in the UI rather than a spinner. On an embedded node the puller (Blob puller not spawned on standalone nodes: backend::spawn_blob_puller resolved the process-global Edge, which only the embedded fold publishes (seen on the canonical's first 0.5.211 boot) #604 — must actually spawn on standalone too) does the holder resolution; the client should never speak to holders directly.
  • Verified-rendition streaming for video. For Tier-A video/mp4 the client hands the normalised file (Media ingest pipeline against CC 3.3.13 / 5.3.2.6: sniff → size → full-SHA → memory-safe decode → canonical re-encode → rendition + descriptor #614's mp4san output, a rendition) to MediaCodec/VideoToolbox. Since the platform player reads by range, the node should serve the rendition's digest as the streamable object, and the client verifies the rendition's full sha256 on first complete read and caches the verdict; per-range verification is not possible with SHA-256 (no tree mode — noted in ci: adopt CIRISCache cross-repo cache in the conformance warmer (CIRISServer#99) #104). If the substrate wants per-chunk verification for large media, that is the HLS/CMAF live_stream shape (init ‖ chunks), which CC now records; a recording served as chunks-with-manifest would let the client verify per segment. Ask: is that the intended path for large files, or only for live?
  • Encrypted scopes. For community+ blobs sealed under the per-epoch DEK, the client needs content_digest (plaintext hash) alongside digest (ciphertext as transported), and the node or Edge does the unseal — the client must not receive raw ciphertext to decrypt in Kotlin on four platforms. Confirm whether the client ever sees ciphertext or always receives plaintext from its own node.

3. Enumerate — the virtual drive

The product intent: expose every file this user can reach as one drive, organised by the circles (self → family → community → affiliations → federation) that own them, with the same set visible on every device the user signs in from. This is the Files tab per circle from the explainer, generalised to one tree.

Ask for a listing contract with these properties:

  • GET /v1/drive (or a Files surface query) returning entries, not attestations. Each entry = the CC 3.3.13 descriptor + cohort_scope + owner (attesting key) + subject_key_ids (who it is about — the take-back right) + created/valid_until + derived_from links + the user's relationship to it (mine / shared-with-me / about-me / held-for-a-peer). The client should not have to fold attestations into a file list on four platforms.
  • Paginated and incremental. Cursor pagination plus a since watermark (or a change feed) so a phone with 10,000 entries syncs deltas, not the world, on every open. Include withdrawals/revocations in the delta so a take-back (consent:state:revoked, withdraws) disappears from every device promptly — that is the promise the explainer makes.
  • Filter and sort are server-side and named: by scope, by format family (image/*, video/*, …), by owner, by subject, by content_class, by date; sort by created / size / name. "Sorted by your own rules, two helpers" from the explainer needs the query surface to carry an ordering the user chose.
  • Held-but-not-mine is a first-class state. A node holding bytes for a peer (holds_bytes) is not "the user's file"; the drive should be able to show or hide those, and never show structurally-invisible self|family blobs of other people (CC 5.2 makes that impossible at the wire, but the listing must not leak their descriptors either).
  • Folders are circles, not paths. No filesystem paths in the contract. name is display-only and RFC 6266-sanitised (CC 3.3.13); two blobs may share a name. The client will present a folder-like tree over cohort_scope and let the user pin/label, but the node should not invent a path model.
  • Quota and holder health. Per-scope byte totals and the node's holds_bytes obligations (what it is holding for others, TTLs), so the drive can show "you are using X of Y" and "this node keeps Z for the family".

4. Cross-platform constraints the shape has to respect

  • One commonMain client. Ktor 3 client, streaming bodies (ByteReadChannel), Okio/kotlinx-io for hashing as bytes pass — no platform-specific HTTP semantics (no multipart tricks that wasm's fetch cannot do; no chunked-transfer assumptions that AVPlayer range reads break).
  • wasmJs has no filesystem and no Rust core (Gobley has no wasm target). The browser's own sandboxed decoders are the isolation there; the client will still verify sha256 in Kotlin and still apply the CC 5.3.2.6 policy table, but sender-side canonicalisation on wasm is canvas/MediaRecorder-based and lossy. The contract should not assume the client can transcode.
  • iOS cannot hold large files in memory or run a background upload without URLSession background tasks; the resume semantics in §1 are what make that work.
  • Desktop and Android may run the client against a remote node (FSD/ONE_CLIENT_N_NODES). The drive listing therefore has to work over the network, not by reading the node's home directory — which is also why enumeration must be a query, not a directory walk.

Requested

  1. A design note (here or a doc) for the create / read / enumerate shape above — endpoints or RPC, field names matching CC 3.3.13 verbatim, pagination and delta semantics, and the answers to the two explicit questions (large-file chunk verification path; ciphertext-vs-plaintext at the client boundary).
  2. Confirmation that GET /v1/media/policy (or equivalent) is acceptable as the node's published Tier-A/caps, so the client canonicalises to the node rather than to a compiled-in table.
  3. Sequencing with Media ingest pipeline against CC 3.3.13 / 5.3.2.6: sniff → size → full-SHA → memory-safe decode → canonical re-encode → rendition + descriptor #614: the client can build create/read against a node that stores-and-forwards verbatim before renditions exist (it will render Tier-A originals it verified and sniffed itself), so ingest and this contract can land independently; but the drive listing needs derived_from to be a real index from day one.

Refs: CIRISConstitution#104 (ruled), CC 3.3.13, CC 5.2, CC 5.3.2.1, CC 5.3.2.5, CC 5.3.2.6; #614 (ingest), #604 (blob puller on standalone), #602 (BlobPointer.epoch, BlobPuller); the brief with the five surveys https://claude.ai/artifact/SXABcab5AMUPr13GhSaceJ (private; share on request).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xv1s8qrUoPoFX2MHZUPA15

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:client-kmpThe vendored KMP client (client/, app/)enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions