You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
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.
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
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).
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.
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).
Context
CIRISConstitution#104 is ruled on
rc5(@44ae7b2, PDF rc5.17): CC 3.3.13 now carries the multimedia Source struct (IANAformat, RFC 6381codec, requiredsize,captionsas 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.sizewas generalised to everythingevidence_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 inclient/shared/.../FilePicker.ktandCIRISApiClient.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,ContentMisswithdrawals, structural invisibility forself|familyper 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
commonMainimplementation.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 aByteArray, and desktop should not.Ask for a create shape with these properties:
PUT/PATCHagainst a session (tus-style or equivalent): the client declaressizeandformatup 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).placeholder.thumbhashthe 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,codecpresent for MP4,size== bytes received) and refuses on mismatch — server-side validation is the only place the grammar check is authoritative.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.derived_fromindex: 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.cohort_scopeis set with the attestation, and forself|familythe node must not emitholds_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_bytesat promotion time) and the UI must say so.2. Read (the receiver side)
Range.GET /v1/blob/{sha256}honouring HTTP range requests, because a client scrubbing a video (orAVPlayer/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 totalsize(Content-Lengthon a full GET,Content-Rangeon a range) so the client can pre-check size before hashing (the OCI rule CC now carries).GET /v1/blob/{sha256}/descriptorreturns the CC 3.3.13 struct so the UI can show the thumbhash placeholder and layout atwidth/heightbefore any byte of the blob arrives — that is the one thing CC 5.3.2.6 lets a client show pre-verification.ContentMisson a staleholds_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.video/mp4the 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'smp4sanoutput, a rendition) toMediaCodec/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/CMAFlive_streamshape (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?community+blobs sealed under the per-epoch DEK, the client needscontent_digest(plaintext hash) alongsidedigest(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 theFilestab per circle from the explainer, generalised to one tree.Ask for a listing contract with these properties:
GET /v1/drive(or aFilessurface 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_fromlinks + 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.sincewatermark (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.formatfamily (image/*,video/*, …), by owner, by subject, bycontent_class, by date; sort bycreated/size/name. "Sorted by your own rules, two helpers" from the explainer needs the query surface to carry an ordering the user chose.holds_bytes) is not "the user's file"; the drive should be able to show or hide those, and never show structurally-invisibleself|familyblobs of other people (CC 5.2 makes that impossible at the wire, but the listing must not leak their descriptors either).nameis display-only and RFC 6266-sanitised (CC 3.3.13); two blobs may share a name. The client will present a folder-like tree overcohort_scopeand let the user pin/label, but the node should not invent a path model.holds_bytesobligations (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
commonMainclient. Ktor 3 client, streaming bodies (ByteReadChannel), Okio/kotlinx-io for hashing as bytes pass — no platform-specific HTTP semantics (no multipart tricks that wasm'sfetchcannot do; no chunked-transfer assumptions thatAVPlayerrange reads break).canvas/MediaRecorder-based and lossy. The contract should not assume the client can transcode.URLSessionbackground tasks; the resume semantics in §1 are what make that work.Requested
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.derived_fromto 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