docs: publish reviewed CHAT-01C private readback contracts (#1507)

This commit is contained in:
2026-09-13 00:48:26 -05:00
parent 28d4e98ad8
commit b023841c8a
4 changed files with 1512 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
# CHAT-01C private readback and recovery contracts
R2 draft candidate for independent review, not an implemented API.
R1 request 26137 received REQUEST CHANGES from Filbert and Dewey via agent-send.
Both complete verdicts were collected before revision; R1 remains archived at
`/tmp/chat-01c-r1-frozen-yvsfowd1`.
Jason approved the contract-only proposal in #1507 comment 26131 in the
control-board conversation and required performing-agent attribution. Darkwing
authors these four files. Filbert independently reviews; Dewey reviews consumer
semantics. No earlier CHAT-01 approval applies to this candidate.
Baseline: reviewed CHAT-01 commit `28d4e98ad8b406ca84b30170558bee22402f1e55`.
Those files remain unchanged. This companion adds private requests/projections;
it does not widen the base command parser, grants or native capabilities.
## Boundary and attribution
Only this directory's README, schema, fixtures and checker are authored, plus
scoped shared tracking and append-only logs. No runtime endpoints, engine
adapters, native sessions, live access, policy changes or deployments. CHAT-02
and CHAT-03I/03D retain their separate charters and unresolved gates.
Darkwing's seat credentials were verified to authenticate as Gitea `darkwing`,
account 104. Use that identity for issue actions. Do not use the legacy API
helper's default Jason credentials. Future commits explicitly use the performing
agent as author and committer, without changing shared Git defaults. Existing
published history is not rewritten. No credential value belongs in these files.
## Closed messages and authority
The draft 2020-12 schema defines six closed top-level message variants and five
closed private item projections. Every property is required. Requests alone use
`privateClientRequest`; the other variants are server responses, not client input.
Validate that request definition, not the union of requests and responses, at
untrusted ingress. Version 1 identifies this new proposal, not a live migration.
IDs are opaque bounded references, never paths or authority. The server resolves
connection, actor, current grant, approved scope and storage epoch. A client cannot
post an actor, grant, snapshot contents or private storage reference. Native
controller ownership is not required for reading one's own data or explicitly
recovering one's own refused input. Separate read-private/recover-private grants
are proposed names only; no role file or actual grant is changed here.
Every operation requires connected authenticated ingress, active/unexpired current
actor grant, approved conversation/scope and the relevant capability. The model's
scope digest binds the full inherited host/seat/project/workspace/conversation
scope. These are supplied synthetic facts, not an authentication implementation.
Browser origin/CSRF, private channel identity, storage provenance and actual grant
producers remain prerequisite runtime work. Current authority is checked on every
page, range, preview and recovery, including an idempotent retry.
| Command | Behavior |
|---|---|
| read-private-state | Bounded actor-private snapshot page; optional exact opaque cursor |
| read-upload-range | Exact owned immutable upload revision and verified byte range |
| preview-upload-text | Bounded complete UTF-8 preview as literal text, never active content |
| recover-refused-draft | Explicit CAS recovery of own never-dispatched refused input into a private unsent draft |
Refusals use the closed `privateRefusal` reason set. The reference functions return
symbolic `refused:reason` strings internally; `response()` demonstrates the typed
wire projection. Refusal is not permission to change source or retry native input.
There are no hidden writes, engine commands or automatic resume operations.
## Safe private-state restoration
The page includes authenticated actor, conversation, opaque snapshot ID, source
revision, items and next cursor. Items contain only explicit allowlisted fields:
* Draft: original branch, text, upload IDs, revision, state and recovery lineage.
* Upload: display filename/media type, total and committed received byte counts,
full digest, revision and state.
* Queue: revision, actual state, causal admission request, original draft, exact
target and frozen payload digest. Bounded frozen input includes original branch,
draft revision, text and immutable upload IDs/revisions/filenames/sizes/digests.
* Receipt: causal request, monotonic source revision, actual/uncertain state/reason.
* Confirmation: exact target, operation, stop context, expiry and display state.
No privateBlobRef, source path, credential, grant or arbitrary internal property is
serialized. Filenames and media labels remain untrusted display data. This is
actor-private restoration, not a shared transcript or permission to adopt another
actor's drafts. Reading any earlier branch never changes the active branch.
Queue and receipt request fields reference the same causal admission. Queue draft
and target fields preserve the original lineage and branch; never join pending
input to a newer mutable draft's text. Frozen input resolves by immutable digest,
not the current mutable queue/draft contents. Missing/corrupt or mismatched retained
content yields frozenInput null and integrity-unavailable, without inventing text
or changing actual queue status. No private blob reference is projected. Snapshot
retention must preserve referenced immutable payloads for valid cursors.
For every snapshot page, resolve each confirmation's CURRENT authoritative record
by scoped actor/conversation/ID. Overlay current consumed/cancelled/expired outcomes.
Do not promote an old snapshot to more permissive authority. Missing or changed
current revision, operation, connection/incarnation, target, expiry or stop context
makes the pending/confirmed snapshot reconfirm-required. A changed current stop
invalidates it even when connection and execution target did not change.
An old connection/incarnation or changed target makes a pending/confirmed
confirmation display reconfirm-required. Expiry is explicit. Consumed/cancelled/
expired terminal outcomes are preserved rather than relabelled by reconnect.
Reading a confirmation never renews it or creates a replacement. The base control
protocol still validates exact current target/stop context and single use.
The miniature clock and projected confirmation expiry use UTC epoch milliseconds.
Production normalization must convert authoritative timestamps without guessing.
### Pagination and snapshot rules
Capture the authorized private records at one atomic cut. Sort by kind and opaque
ID; the cursor retains the exact immutable snapshot and next offset. A page has
at most 100 items and 8 MiB serialized UTF-8, whichever is reached first. Reduce
the number of items to fit; do not truncate text or silently omit records. An
individual over-budget item refuses projection-size. Field limits keep valid
individual draft projections below the page ceiling.
A cursor binds actor, full scope digest, conversation, private-state purpose,
source epoch/revision, expiry, snapshot ID and offset. Only its opaque ID crosses
the client boundary. Expiry equality is expired. Unknown/foreign/stale cursors
refuse; never follow an alternate file or substitute a newer snapshot. Validate
snapshot record ownership again before projection. Expiry affects the cursor,
not the retained draft or upload. Cursor retention needs bounded storage/quota
and cleanup in the eventual implementation; none is provided by this model.
Here sourceRevision means replacement of the private store/source identity, not
every ordinary item edit. Per-item revisions track mutations. An existing snapshot
retains its old content while mutable source items advance. Current authorization
and confirmation actionability are rechecked at read time and can become stricter
than the snapshot; they never inherit an old grant's authority.
After browser/server restart, reconstruct drafts, queue/receipts and confirmations
from the durable private index. Preserve delivery-unknown; absent native evidence
never becomes unsent. The JSON round-trip fixture tests reference-state readback,
not disk durability, transactions or authenticated reconnection. Full persistence,
source-epoch management, grant revocation and race injection remain runtime gates.
## Attachment access and preview
Resolve only an owned upload in the approved recipient scope. Require exact
revision and a completed staged/delivered/sent record. Refuse receiving, rejected,
discarded, foreign or missing uploads. Check stored bytes against the complete
length and SHA-256 before returning any range. A posted filename, size, media type
or blob reference is not evidence. Missing/corrupt bytes refuse integrity.
Ranges are explicit offset/length, at most 256 KiB, wholly within a file of at most
20 MiB. This contract refuses overrun instead of silently clamping. Responses give
actual offset/length/total, full digest, range digest and canonical base64 bytes.
They specify attachment disposition and application/octet-stream. Transport must
use authenticated JSON/binary responses with nosniff; never reflect an uploaded
media label into active HTML or an executable navigation response. Complete byte
assembly must match the advertised file digest before being called a download.
Text preview accepts only a complete file of at most 256 KiB that decodes as strict
UTF-8. It escapes non-display controls, escape characters and directional controls
into visible Unicode escape notation. HTML/Markdown remains literal text. The
response explicitly requires textContent-only rendering. It does not authorize
innerHTML, Markdown execution, terminal escapes, SVG documents or embedded scripts.
The original bytes/digest remain unchanged; preview is a separate display value.
Invalid UTF-8 or over-budget content refuses preview without truncating it. Other
formats remain downloadable; image/document rendering needs the separately reviewed
CHAT-05 decoder/renderer behavior. The reference checks do not prove browser safety.
### Receiving upload restoration
Upload projections retain authoritative receivedBytes and revision even while
receiving; ranges/preview still refuse incomplete uploads. After a lost append
acknowledgment or restart, obtain a FRESH private-state snapshot, not an old cursor's
progress, before constructing another append. Reselect the original local file
explicitly and match full byte count and digest, never filename alone. Do not
persist or guess a browser-local file path. The reference resumeOffset check gives
the restored revision/offset only after that match. No file, wrong bytes or invalid
progress means no append; preserve the staging record and let the actor explicitly
select matching bytes or discard/start a new upload through the base lifecycle.
Construct the existing CHAT-01 append command using that revision and committed
offset. Its CAS must reject progress that advanced again after readback. If all
bytes are received but state is still receiving, use explicit completion rather
than append duplicates; full server digest verification still gates staged state.
The lost-ack fixture starts with a GIVEN authoritative committed append result,
then tests readback and local-file matching. It does not prove the append producer,
disk durability, native delivery or a live browser's file picker.
No extraction, execution, native delivery, workspace staging or remote forwarding
occurs from readback or preview. These operations expose only already authorized
private content to its actor, not to the agent engine before Send.
## Recovering refused input without replay
This closes the private recovery path tracked as R3-2 in #1507 comment 26128.
Eligibility is deliberately narrow: owned item, state dispatch-refused, exact item
revision and payload digest, and null nativeRequest. Dispatched, acknowledged,
working, delivery-unknown, finished and recovery-failed items are refused. The
trusted queue journal must establish that dispatch-refused means no native attempt;
this model assumes that upstream fact and does not reconstruct it from a timeout.
Verify the retained CHAT-01 frozen payload with its existing canonical digest,
including complete upload revisions/digests and original branch. Verify current
owned upload revisions, recipient scopes and actual retained bytes too. A missing,
changed or corrupt payload/upload leaves the queue and original data intact with
an integrity refusal. Never fabricate a draft from partial metadata or returned
native text. The fixture copies a synthetic frozen example from the published
baseline and computes the same sorted-key, UTF-8 digest excluding its digest field.
One idealized transaction creates a NEW private unsent draft, preserves actor,
branch, exact text and upload references, records recoveredFrom, advances the queue
revision and marks recovered-as-draft. Original immutable payloads remain intact.
No native outbox entry is emitted. The draft still needs explicit Send and normal
current controller/branch/grant checks under CHAT-01.
Recovery dedup is scoped to authenticated actor, conversation and client request ID,
encoded as a canonical JSON tuple, with canonical command fingerprint. Delimiter
concatenation is forbidden: opaque IDs may themselves contain colons. The shared
ledger fixture tests colliding colon spellings in two independently authorized
conversations, legitimate distinct recoveries and conflicting reuse. Reconnect/restart retry returns the original
result without creating another draft. Conflicting reuse refuses. A fresh request
ID cannot recover an already recovered item. The model persists the idealized
receipt in its synthetic state; actual multi-record crash atomicity is not proved.
R3-1, reconciliation of dispatched-but-unconsumed native input after fencing,
belongs to CHAT-02 adapter evidence. It is NOT made recoverable by this companion.
## Evidence and completion gates
Run without installs or network:
```sh
node --check docs/plans/chat-01c/check.mjs
node docs/plans/chat-01c/check.mjs
node docs/plans/chat-01/check.mjs
node docs/plans/chat-00/check.mjs
```
The checker uses Node and installed Python jsonschema. Missing validation fails.
R2 validates eleven closed request/response examples and 187 omission/extra-field
refusals, plus the reference scenarios. New regressions cover terminal/stop changes
between confirmation pages and after restart, colon-ID cross-conversation dedup,
two immutable queue inputs with distinct branches and receipt/draft joins despite
mutable draft edits, and receiving-offset restoration after a lost append reply.
It checks closed request/response examples, required-field/extra-field refusals,
private filtering, cursor actor/scope/purpose/source/expiry, immutable pagination,
synthetic restart, confirmation restoration, byte ranges/digests, literal preview,
corrupt upload/payload refusal, immutable recovery, repeated request identity and
no native replay. It does not implement HTTP, real authentication, storage quotas,
durable transactions, every record mutation, native adapters or UI rendering.
Independent exact-candidate approval by Filbert and Dewey is required before Q21
scoped publication. Existing B1-B6, CHAT-02/03I/03D, Q22, live-cutover and all-seat
acceptance gates remain. #1507 stays open. No new queue item starts because this
draft exists.
+324
View File
@@ -0,0 +1,324 @@
// Offline CHAT-01C draft model. No authentication or durable storage producer.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
const sorted = x => Array.isArray(x) ? x.map(sorted) : x && typeof x === 'object' ? Object.fromEntries(Object.keys(x).sort().map(k => [k, sorted(x[k])])) : x;
const canonical = x => JSON.stringify(sorted(x));
const frozenDigest = p => sha(Buffer.from(canonical(Object.fromEntries(Object.entries(p).filter(([k]) => k !== 'digest')))));
const fixtureURL = new URL('./fixtures.json', import.meta.url);
const schemaURL = new URL('./contracts.schema.json', import.meta.url);
const f = JSON.parse(readFileSync(fixtureURL, 'utf8'));
const examples = [f.request];
assert.equal(f.evidence, 'synthetic-contract-only');
const py = spawnSync('python3', ['-c', `
import json,sys
from jsonschema import Draft202012Validator
s=json.load(open(sys.argv[1])); f=json.load(open(sys.argv[2]))
Draft202012Validator.check_schema(s);v=Draft202012Validator(s)
r=f['request'];assert v.is_valid(r)
for key in r:
x=dict(r);del x[key];assert not v.is_valid(x),key
assert not v.is_valid(dict(r,actor='forged'))
print('PASS initial request shape, required fields and forged actor refusal')
`, schemaURL.pathname, fixtureURL.pathname], { encoding: 'utf8', timeout: 30000 });
assert.equal(py.status, 0, py.stdout + py.stderr); process.stdout.write(py.stdout);
function project(x, w, now) {
const base = { id: x.id, kind: x.kind, revision: x.revision };
switch (x.kind) {
case 'draft': return { ...base, branch: x.branch, text: x.text, state: x.state, attachments: [...(x.attachments ?? [])], recoveredFrom: x.recoveredFrom ?? null };
case 'upload': return { ...base, filename: x.filename, mediaType: x.mimeType ?? x.mediaType, size: x.size, receivedBytes: x.receivedBytes, digest: x.digest, state: x.state };
case 'queue': {
const p = w.payloads?.[x.payloadDigest];
const valid = p && p.actor === x.actor && p.conversation === x.conversation && p.branch === x.target?.branch && x.target?.conversation === x.conversation && p.draft === x.draft && p.digest === x.payloadDigest && frozenDigest(p) === x.payloadDigest;
return { ...base, state: x.state, payloadDigest: x.payloadDigest, reason: x.failureReason ?? null, request: x.request, draft: x.draft, target: structuredClone(x.target), frozenInputState: valid ? 'available' : 'integrity-unavailable', frozenInput: valid ? { branch: p.branch, draftRevision: p.draftRevision, text: p.text, uploads: p.uploads.map(u => ({ id: u.id, revision: u.revision, filename: u.filename, size: u.size, digest: u.digest })) } : null };
}
case 'receipt': return { ...base, request: x.request, state: x.state, reason: x.reason ?? null };
case 'confirmation': {
const current = w.confirmations?.find(c => c.id === x.id && c.actor === x.actor && c.conversation === x.conversation);
let state = 'reconfirm-required';
if (['consumed', 'cancelled', 'expired'].includes(x.state)) state = x.state;
else if (current && ['consumed', 'cancelled', 'expired'].includes(current.state)) state = current.state;
else if (x.expiresAt <= now || current?.expiresAt <= now) state = 'expired';
else if (current && current.revision === x.revision && current.state === x.state && current.operation === x.operation && current.expiresAt === x.expiresAt && current.connection === x.connection && current.connectionGeneration === x.connectionGeneration && current.stop === x.stop && current.stop === w.stop && canonical(current.target) === canonical(x.target) && canonical(x.target) === canonical(w.target) && current.connection === w.connection.id && current.connectionGeneration === w.connection.generation) state = x.state;
return { ...base, operation: x.operation, target: structuredClone(x.target), state, expiresAt: x.expiresAt, stop: x.stop };
}
default: throw new Error('unsupported private projection');
}
}
function privateItems(w) {
return [...w.items, ...(w.uploads ?? []).map(x => ({ ...x, kind: 'upload' })), ...(w.queue ?? []).map(x => ({ ...x, kind: 'queue' })), ...(w.receipts ?? []).map(x => ({ ...x, kind: 'receipt' })), ...(w.confirmations ?? []).map(x => ({ ...x, kind: 'confirmation' }))];
}
function readState(w, r, now) {
const c = w.connection;
const refused = authorize(w, r, 'read-private', now); if (refused) return refused;
if (r.command.operation !== 'read-private-state') return 'refused:operation';
let snapshot, snapshotId, offset = 0;
if (r.command.cursor) {
const cursor = w.cursors[r.command.cursor];
if (!cursor || !Array.isArray(cursor.snapshot) || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0 || cursor.offset > cursor.snapshot.length || cursor.actor !== c.actor || cursor.conversation !== r.conversation || cursor.purpose !== 'private-state' || cursor.scopeDigest !== w.grant.scopeDigest || !Number.isSafeInteger(cursor.expiresAt) || cursor.expiresAt <= now || cursor.sourceRevision !== w.sourceRevision) return 'refused:cursor';
snapshot = cursor.snapshot; snapshotId = cursor.snapshotId; offset = cursor.offset;
} else {
snapshot = structuredClone(privateItems(w).filter(x => x.actor === c.actor && x.conversation === r.conversation).sort((a, b) => (a.kind + ':' + a.id).localeCompare(b.kind + ':' + b.id, 'en')));
snapshotId = `snapshot-${w.snapshotSerial = (w.snapshotSerial ?? 0) + 1}`;
}
if (snapshot.some(x => x.actor !== c.actor || x.conversation !== r.conversation)) return 'refused:cursor';
if (snapshot.some(x => x.kind === 'upload' && (!Number.isSafeInteger(x.receivedBytes) || x.receivedBytes < 0 || x.receivedBytes > x.size))) return 'refused:integrity';
const items = []; let byteCount = 4096;
for (const x of snapshot.slice(offset, offset + r.command.limit)) {
const bytes = Buffer.byteLength(JSON.stringify(project(x, w, now)), 'utf8') + 1;
if (byteCount + bytes > 8 * 1024 * 1024) { if (!items.length) return 'refused:projection-size'; break; }
items.push(x); byteCount += bytes;
}
let nextCursor = null;
if (offset + items.length < snapshot.length) {
nextCursor = `cursor-${Object.keys(w.cursors).length + 1}`;
w.cursors[nextCursor] = { actor: c.actor, conversation: r.conversation, purpose: 'private-state', scopeDigest: w.grant.scopeDigest, expiresAt: now + 100, sourceRevision: w.sourceRevision, snapshot, snapshotId, offset: offset + items.length };
}
// Explicit projection, not arbitrary internal object serialization.
return { version: 1, kind: 'privateStatePage', actor: c.actor, conversation: r.conversation, snapshot: snapshotId, sourceRevision: w.sourceRevision, items: items.map(x => project(x, w, now)), nextCursor };
}
{
const w = structuredClone(f.world), result = readState(w, f.request, f.now);
assert.equal(result.items.length, 1); assert.equal(result.items[0].id, 'draft-1');
assert.equal('actor' in result.items[0], false); assert.deepEqual(w.nativeOutbox, []);
for (const [field, value] of [['state', 'revoked'], ['expiresAt', f.now], ['actor', 'actor-2']]) {
const bad = structuredClone(w); bad.grant[field] = value;
assert.equal(readState(bad, f.request, f.now), 'refused:grant');
}
}
{
const w = structuredClone(f.world);
w.items.push({ ...w.items[0], id: 'draft-3' });
const r = structuredClone(f.request); r.command.limit = 1;
const first = readState(w, r, f.now); assert.ok(first.nextCursor);
r.command.cursor = first.nextCursor;
w.items[2].text = 'newer mutable text';
assert.equal(readState(w, r, f.now).items[0].text, 'private unsent text');
for (const [field, value] of [['actor','actor-2'], ['conversation','foreign'], ['purpose','history'], ['scopeDigest','foreign-scope'], ['expiresAt',f.now], ['sourceRevision',2]]) {
const bad = structuredClone(w); bad.cursors[first.nextCursor][field] = value;
assert.equal(readState(bad, r, f.now), 'refused:cursor');
}
const restored = JSON.parse(JSON.stringify(w));
assert.deepEqual(readState(restored, r, f.now), readState(w, r, f.now));
assert.deepEqual(restored.nativeOutbox, []);
}
function authorize(w, r, capability, now) {
if (r.kind !== 'privateClientRequest' || r.version !== 1) return 'refused:request';
const c = w.connection, g = w.grant;
if (r.connection !== c.id || c.state !== 'connected') return 'refused:channel';
if (g.actor !== c.actor || g.state !== 'active' || !Number.isSafeInteger(g.expiresAt) || g.expiresAt <= now || !g.capabilities.includes(capability)) return 'refused:grant';
if (g.conversation !== r.conversation || !w.mappingVerified || g.scopeDigest !== sha(Buffer.from(canonical(w.scope)))) return 'refused:scope';
return null;
}
function readRange(w, r, now) {
const refused = authorize(w, r, 'read-private', now); if (refused) return refused;
const cmd = r.command;
if (cmd.operation !== 'read-upload-range') return 'refused:operation';
const u = w.uploads.find(u => u.id === cmd.upload);
if (!u || u.actor !== w.connection.actor || u.conversation !== r.conversation || u.revision !== cmd.revision || !['staged', 'delivered', 'sent'].includes(u.state) || canonical(u.recipientScope) !== canonical(w.scope)) return 'refused:upload';
const bytes = w.blobs[u.id];
if (!Buffer.isBuffer(bytes) || bytes.length !== u.size || sha(bytes) !== u.digest) return 'refused:integrity';
if (!Number.isSafeInteger(cmd.offset) || !Number.isSafeInteger(cmd.length) || cmd.offset < 0 || cmd.length < 1 || cmd.length > 262144 || cmd.offset >= bytes.length || cmd.offset + cmd.length > bytes.length) return 'refused:range';
const part = bytes.subarray(cmd.offset, cmd.offset + cmd.length);
return { version: 1, kind: 'uploadRange', upload: u.id, revision: u.revision, offset: cmd.offset, length: part.length, total: bytes.length, digest: u.digest, rangeDigest: sha(part), dataBase64: part.toString('base64'), disposition: 'attachment', mediaType: 'application/octet-stream' };
}
function previewText(w, r, now) {
const refused = authorize(w, r, 'read-private', now); if (refused) return refused;
if (r.command.operation !== 'preview-upload-text') return 'refused:operation';
const u = w.uploads.find(u => u.id === r.command.upload);
if (!u || u.actor !== w.connection.actor || u.conversation !== r.conversation || u.revision !== r.command.revision || canonical(u.recipientScope) !== canonical(w.scope)) return 'refused:upload';
if (u.size > 262144) return 'refused:preview';
const range = readRange(w, { ...r, command: { operation: 'read-upload-range', upload: u.id, revision: r.command.revision, offset: 0, length: u.size } }, now);
if (typeof range === 'string') return range;
let text;
try { text = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(range.dataBase64, 'base64')); }
catch { return 'refused:preview'; }
text = text.replace(/[\u0000-\u0008\u000b-\u001f\u007f\u202a-\u202e\u2066-\u2069]/gu, c => '\\u' + c.codePointAt(0).toString(16).padStart(4, '0'));
return { version: 1, kind: 'privateTextPreview', upload: u.id, revision: u.revision, digest: u.digest, text, renderMode: 'textContent-only' };
}
function recoverRefused(w, r, now) {
const refused = authorize(w, r, 'recover-private', now); if (refused) return refused;
const cmd = r.command;
if (cmd.operation !== 'recover-refused-draft') return 'refused:operation';
const q = w.queue.find(q => q.id === cmd.item);
if (!q || q.actor !== w.connection.actor || q.conversation !== r.conversation) return 'refused:item';
const key = canonical([w.connection.actor, r.conversation, r.id]);
const fingerprint = canonical(cmd);
if (w.recoveries[key]) return w.recoveries[key].fingerprint === fingerprint ? structuredClone(w.recoveries[key].response) : 'refused:conflicting-request';
if (q.state !== 'dispatch-refused' || q.nativeRequest !== null || q.revision !== cmd.revision || q.payloadDigest !== cmd.payloadDigest) return 'refused:state';
const p = w.payloads[q.id];
if (!p || p.digest !== q.payloadDigest || frozenDigest(p) !== q.payloadDigest || p.actor !== q.actor || p.conversation !== q.conversation) return 'refused:integrity';
for (const frozen of p.uploads) {
const id = frozen.id, u = w.uploads.find(u => u.id === id);
if (!u || u.actor !== q.actor || u.conversation !== q.conversation || u.revision !== frozen.revision || u.digest !== frozen.digest || canonical(u.recipientScope) !== canonical(w.scope) || canonical(frozen.recipientScope) !== canonical(w.scope) || !Buffer.isBuffer(w.blobs[id]) || w.blobs[id].length !== frozen.size || sha(w.blobs[id]) !== frozen.digest) return 'refused:integrity';
}
const draft = { id: `recovered-${q.id}-${q.revision}`, kind: 'draft', actor: q.actor, conversation: q.conversation, revision: 1, branch: p.branch, text: p.text, attachments: p.uploads.map(u => u.id), state: 'unsent', recoveredFrom: q.id };
// Idealized one-transaction result; no actual durable write is implemented.
w.items.push(draft); q.state = 'recovered-as-draft'; q.revision++;
const response = { version: 1, kind: 'privateRecovery', item: q.id, revision: q.revision, draft: draft.id, state: q.state };
w.recoveries[key] = { fingerprint, response: structuredClone(response) }; return response;
}
function storageWorld() {
const w = structuredClone(f.world), bytes = Buffer.from('synthetic attachment');
const p = structuredClone(f.frozenExample);
p.text = 'retained text'; Object.assign(p.uploads[0], { filename: 'synthetic.txt', mimeType: 'text/plain', size: bytes.length, receivedBytes: bytes.length, digest: sha(bytes), privateBlobRef: 'not-public' });
p.digest = frozenDigest(p);
w.uploads = structuredClone(p.uploads); w.blobs = { 'upload-1': bytes };
w.payloads = { 'queue-1': p, [p.digest]: p }; w.recoveries = {};
w.target = { conversation: 'conversation-1', branch: 'branch-1', execution: 'execution-1', controllerGeneration: 2 }; w.stop = null;
w.queue = [{ id: 'queue-1', actor: 'actor-1', conversation: 'conversation-1', revision: 1, state: 'dispatch-refused', nativeRequest: null, request: 'request-1', draft: p.draft, target: structuredClone(w.target), payloadDigest: p.digest }];
return w;
}
{
const w = storageWorld(), r = { ...f.request, command: { operation: 'read-upload-range', upload: 'upload-1', revision: 1, offset: 0, length: 9 } };
examples.push(r);
const part = readRange(w, r, f.now); examples.push(part); assert.equal(Buffer.from(part.dataBase64, 'base64').toString(), 'synthetic');
assert.equal('privateBlobRef' in part, false); assert.equal(part.disposition, 'attachment');
for (const change of [{ offset: -1 }, { length: 262145 }, { offset: 20, length: 1 }, { length: 0 }]) assert.equal(readRange(w, { ...r, command: { ...r.command, ...change } }, f.now), 'refused:range');
w.uploads[0].actor = 'other'; assert.equal(readRange(w, r, f.now), 'refused:upload');
w.uploads[0].actor = 'actor-1'; w.blobs['upload-1'][0] ^= 1; assert.equal(readRange(w, r, f.now), 'refused:integrity');
assert.deepEqual(w.nativeOutbox, []);
}
{
const w = storageWorld(), r = { ...f.request, command: { operation: 'recover-refused-draft', item: 'queue-1', revision: 1, payloadDigest: w.queue[0].payloadDigest } };
for (const state of ['dispatched', 'acknowledged', 'delivery-unknown', 'working', 'finished', 'recovery-failed']) {
w.queue[0].state = state; assert.equal(recoverRefused(w, r, f.now), 'refused:state');
}
examples.push(r);
w.queue[0].state = 'dispatch-refused'; w.queue[0].nativeRequest = 'native-attempt';
assert.equal(recoverRefused(w, r, f.now), 'refused:state'); w.queue[0].nativeRequest = null;
const recovered = recoverRefused(w, r, f.now); examples.push(recovered);
assert.equal(recovered.state, 'recovered-as-draft'); assert.equal(w.items.at(-1).text, 'retained text');
assert.equal(w.items.at(-1).state, 'unsent'); const count = w.items.length;
assert.deepEqual(recoverRefused(w, r, f.now), recovered); assert.equal(w.items.length, count);
const restart = JSON.parse(JSON.stringify(w)); restart.connection.id = 'reconnected';
assert.deepEqual(recoverRefused(restart, { ...r, connection: 'reconnected' }, f.now), recovered);
assert.equal(restart.items.length, count);
assert.equal(recoverRefused(restart, { ...r, id: 'new-attempt', connection: 'reconnected' }, f.now), 'refused:state');
assert.deepEqual(w.nativeOutbox, []);
const corrupt = storageWorld(); corrupt.payloads['queue-1'].text = 'changed';
assert.equal(recoverRefused(corrupt, r, f.now), 'refused:integrity'); assert.equal(corrupt.queue[0].state, 'dispatch-refused');
}
{
const w = storageWorld(); w.connection.generation = 2;
w.receipts = [{ id: 'receipt-1', actor: 'actor-1', conversation: 'conversation-1', revision: 1, request: 'request-1', state: 'delivery-unknown', reason: 'lost-ack' }];
w.confirmations = [{ id: 'confirmation-1', actor: 'actor-1', conversation: 'conversation-1', revision: 1, operation: 'force-stop', target: structuredClone(w.target), connection: 'old-connection', connectionGeneration: 1, state: 'confirmed', expiresAt: 2000, stop: null }];
const page = readState(w, f.request, f.now); examples.push(page);
assert.deepEqual(page.items.map(x => x.kind).sort(), ['confirmation', 'draft', 'queue', 'receipt', 'upload']);
assert.equal(page.items.find(x => x.kind === 'confirmation').state, 'reconfirm-required');
assert.equal(page.items.find(x => x.kind === 'receipt').state, 'delivery-unknown');
w.confirmations[0].state = 'consumed'; w.confirmations[0].expiresAt = 0;
assert.equal(readState(w, f.request, f.now).items.find(x => x.kind === 'confirmation').state, 'consumed');
assert.equal(JSON.stringify(page).includes('not-public'), false);
assert.deepEqual(w.nativeOutbox, []);
}
{
const w = storageWorld(), r = { ...f.request, command: { operation: 'recover-refused-draft', item: 'queue-1', revision: 1, payloadDigest: w.queue[0].payloadDigest } };
w.queue[0].actor = 'actor-2'; assert.equal(recoverRefused(w, r, f.now), 'refused:item');
w.queue[0].actor = 'actor-1'; w.uploads[0].revision++; assert.equal(recoverRefused(w, r, f.now), 'refused:integrity');
w.mappingVerified = false; assert.equal(readState(w, f.request, f.now), 'refused:scope');
}
function response(r, value) {
return typeof value === 'string' && value.startsWith('refused:') ? { version: 1, kind: 'privateRefusal', request: r.id, reason: value.slice(8) } : value;
}
examples.push(response(f.request, 'refused:cursor'));
{
const w = storageWorld(), r = { ...f.request, command: { operation: 'preview-upload-text', upload: 'upload-1', revision: 1 } };
examples.push(r); const preview = previewText(w, r, f.now); examples.push(preview);
assert.equal(preview.text, 'synthetic attachment'); assert.equal(preview.renderMode, 'textContent-only');
const bytes = Buffer.from('<script>not executed</script>\u001b'); w.blobs['upload-1'] = bytes;
Object.assign(w.uploads[0], { size: bytes.length, digest: sha(bytes) });
assert.equal(previewText(w, r, f.now).text, '<script>not executed</script>\\u001b');
w.blobs['upload-1'] = Buffer.from([255]); Object.assign(w.uploads[0], { size: 1, digest: sha(w.blobs['upload-1']) });
assert.equal(previewText(w, r, f.now), 'refused:preview');
}
// R2: current confirmation overlays must never revive stale snapshot authority.
{
function confirmationWorld() {
const w = storageWorld(); w.connection.generation = 1; w.stop = 'stop-old';
w.confirmations = ['a', 'b'].map(id => ({ id, actor: 'actor-1', conversation: 'conversation-1', revision: 1, operation: 'force-stop', target: structuredClone(w.target), connection: w.connection.id, connectionGeneration: 1, state: 'confirmed', expiresAt: 2000, stop: 'stop-old' })); return w;
}
for (const change of ['consumed', 'cancelled', 'stop', 'missing', 'restart']) {
let w = confirmationWorld(); const r = { ...f.request, command: { operation: 'read-private-state', cursor: null, limit: 1 } };
r.command.cursor = readState(w, r, f.now).nextCursor;
if (change === 'stop') w.stop = 'stop-new';
else if (change === 'missing') w.confirmations.pop();
else w.confirmations[1].state = change === 'restart' ? 'consumed' : change;
if (change === 'restart') w = JSON.parse(JSON.stringify(w));
const item = readState(w, r, f.now).items[0];
assert.equal(item.id, 'b'); assert.equal(item.state, ['stop', 'missing'].includes(change) ? 'reconfirm-required' : change === 'restart' ? 'consumed' : change);
}
}
// Valid colon-bearing IDs must not alias another scope's recovery receipt.
{
const first = storageWorld(), r1 = { ...f.request, id: 'x:y', command: { operation: 'recover-refused-draft', item: 'queue-1', revision: 1, payloadDigest: first.queue[0].payloadDigest } };
assert.equal(recoverRefused(first, r1, f.now).state, 'recovered-as-draft');
const second = storageWorld(); second.recoveries = first.recoveries;
const conversation = 'conversation-1:x'; second.scope.conversation = conversation; second.grant.conversation = conversation;
second.grant.scopeDigest = sha(Buffer.from(canonical(second.scope))); second.target.conversation = conversation;
for (const u of second.uploads) { u.conversation = conversation; u.recipientScope = structuredClone(second.scope); }
const p = second.payloads['queue-1']; p.conversation = conversation; p.uploads = structuredClone(second.uploads); p.digest = frozenDigest(p);
second.payloads = { 'queue-1': p, [p.digest]: p };
Object.assign(second.queue[0], { conversation, target: structuredClone(second.target), payloadDigest: p.digest });
const r2 = { ...r1, id: 'y', conversation };
assert.equal(recoverRefused(second, r2, f.now), 'refused:state');
assert.equal(second.queue[0].state, 'dispatch-refused');
r2.command = { ...r2.command, payloadDigest: p.digest }; examples.push(r2);
assert.equal(recoverRefused(second, r2, f.now).state, 'recovered-as-draft');
assert.equal(second.items.at(-1).conversation, conversation);
assert.equal(recoverRefused(second, { ...r2, command: { ...r2.command, revision: 2 } }, f.now), 'refused:conflicting-request');
}
// Queue readback uses frozen admissions and causal joins, never current drafts.
{
const w = storageWorld(); w.queue[0].state = 'queued';
const p = structuredClone(w.payloads['queue-1']); p.id = 'payload-2'; p.branch = 'branch-2'; p.draft = 'draft-branch-2'; p.text = 'second frozen input'; p.digest = frozenDigest(p);
w.payloads['queue-2'] = p; w.payloads[p.digest] = p;
w.queue.push({ ...structuredClone(w.queue[0]), id: 'queue-2', request: 'request-2', draft: p.draft, target: { ...w.target, branch: p.branch }, payloadDigest: p.digest, state: 'dispatch-refused' });
w.items[0].text = 'edited mutable draft'; w.items[0].revision++;
w.items.push({ ...w.items[0], id: p.draft, branch: p.branch, text: 'another later edit' });
w.receipts = w.queue.map((q, i) => ({ id: `receipt-${i}`, actor: q.actor, conversation: q.conversation, revision: 1, request: q.request, state: q.state }));
const page = readState(w, f.request, f.now); examples.push(page);
const queues = page.items.filter(x => x.kind === 'queue');
assert.deepEqual(queues.map(q => q.frozenInput.text), ['retained text', 'second frozen input']);
assert.deepEqual(queues.map(q => q.target.branch), ['branch-1', 'branch-2']);
for (const q of queues) {
assert.ok(page.items.some(x => x.kind === 'receipt' && x.request === q.request));
assert.ok(page.items.some(x => x.kind === 'draft' && x.id === q.draft));
assert.equal(q.frozenInputState, 'available');
}
assert.equal(JSON.stringify(page).includes('not-public'), false);
}
function resumeOffset(view, localBytes) {
if (view.state !== 'receiving' || !Number.isSafeInteger(view.receivedBytes) || view.receivedBytes < 0 || view.receivedBytes > view.size || !Buffer.isBuffer(localBytes) || localBytes.length !== view.size || sha(localBytes) !== view.digest) return 'refused:local-file';
return { revision: view.revision, offset: view.receivedBytes, complete: view.receivedBytes === view.size };
}
{
const w = storageWorld(), local = Buffer.from(w.blobs['upload-1']);
w.uploads.push({ ...structuredClone(w.uploads[0]), id: 'upload-receiving', state: 'receiving', revision: 2, receivedBytes: 7 });
w.blobs['upload-receiving'] = local.subarray(0, 7); // Given committed append; reply lost.
const view = readState(w, f.request, f.now).items.find(x => x.id === 'upload-receiving');
assert.deepEqual(resumeOffset(view, local), { revision: 2, offset: 7, complete: false });
assert.equal(resumeOffset(view, Buffer.from('wrong local selection')), 'refused:local-file');
assert.equal(readRange(w, { ...f.request, command: { operation: 'read-upload-range', upload: view.id, revision: view.revision, offset: 0, length: 7 } }, f.now), 'refused:upload');
assert.deepEqual(w.nativeOutbox, []);
}
console.log('PASS R2 live confirmation overlays, tuple-scoped recovery dedup, frozen queue joins and receiving-upload resume readback');
const shapes = spawnSync('python3', ['-c', `
import json,sys,copy
from jsonschema import Draft202012Validator
p=json.load(sys.stdin);v=Draft202012Validator(p['schema']);count=0
for e in p['examples']:
assert v.is_valid(e),list(v.iter_errors(e))
for key in e:
x=copy.deepcopy(e);del x[key];assert not v.is_valid(x),(e['kind'],key);count+=1
x=dict(e,privateBlobRef='forged');assert not v.is_valid(x);count+=1
if e['kind']=='privateStatePage':
for i,item in enumerate(e['items']):
for key in item:
x=copy.deepcopy(e);del x['items'][i][key];assert not v.is_valid(x),(item['kind'],key);count+=1
print('PASS',len(p['examples']),'closed request/response examples and',count,'omission/extra-field refusals')
`], { input: JSON.stringify({ schema: JSON.parse(readFileSync(schemaURL, 'utf8')), examples }), encoding: 'utf8', timeout: 30000 });
assert.equal(shapes.status, 0, shapes.stdout + shapes.stderr); process.stdout.write(shapes.stdout);
console.log('PASS private projections/cursors, synthetic restart, bounded attachment ranges and refused-input recovery. Offline models only.');
+835
View File
@@ -0,0 +1,835 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.invalid/mosaic/chat-01c/draft-1",
"title": "CHAT-01C private request and safe projection drafts",
"oneOf": [
{
"$ref": "#/$defs/privateClientRequest"
},
{
"$ref": "#/$defs/privateStatePage"
},
{
"$ref": "#/$defs/uploadRange"
},
{
"$ref": "#/$defs/privateRecovery"
},
{
"$ref": "#/$defs/privateRefusal"
},
{
"$ref": "#/$defs/privateTextPreview"
}
],
"$defs": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"revision": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"digest": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"target": {
"type": "object",
"additionalProperties": false,
"properties": {
"conversation": {
"$ref": "#/$defs/id"
},
"branch": {
"$ref": "#/$defs/id"
},
"execution": {
"$ref": "#/$defs/id"
},
"controllerGeneration": {
"$ref": "#/$defs/revision"
}
},
"required": [
"conversation",
"branch",
"execution",
"controllerGeneration"
]
},
"privateClientRequest": {
"type": "object",
"additionalProperties": false,
"required": [
"version",
"kind",
"id",
"connection",
"conversation",
"command"
],
"properties": {
"version": {
"const": 1
},
"kind": {
"const": "privateClientRequest"
},
"id": {
"$ref": "#/$defs/id"
},
"connection": {
"$ref": "#/$defs/id"
},
"conversation": {
"$ref": "#/$defs/id"
},
"command": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": [
"operation",
"cursor",
"limit"
],
"properties": {
"operation": {
"const": "read-private-state"
},
"cursor": {
"anyOf": [
{
"$ref": "#/$defs/id"
},
{
"type": "null"
}
]
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": [
"operation",
"upload",
"revision",
"offset",
"length"
],
"properties": {
"operation": {
"const": "read-upload-range"
},
"upload": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"offset": {
"type": "integer",
"minimum": 0,
"maximum": 20971520
},
"length": {
"type": "integer",
"minimum": 1,
"maximum": 262144
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": [
"operation",
"item",
"revision",
"payloadDigest"
],
"properties": {
"operation": {
"const": "recover-refused-draft"
},
"item": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"payloadDigest": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
}
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"operation": {
"const": "preview-upload-text"
},
"upload": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
}
},
"required": [
"operation",
"upload",
"revision"
]
}
]
}
}
},
"draftView": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"const": "draft"
},
"id": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"text": {
"type": "string",
"maxLength": 262144
},
"state": {
"enum": [
"unsent",
"submitted",
"discarded"
]
},
"attachments": {
"type": "array",
"maxItems": 10,
"uniqueItems": true,
"items": {
"$ref": "#/$defs/id"
}
},
"recoveredFrom": {
"anyOf": [
{
"$ref": "#/$defs/id"
},
{
"type": "null"
}
]
},
"branch": {
"$ref": "#/$defs/id"
}
},
"required": [
"kind",
"id",
"revision",
"text",
"state",
"attachments",
"recoveredFrom",
"branch"
]
},
"uploadView": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"const": "upload"
},
"id": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"filename": {
"type": "string",
"maxLength": 255
},
"mediaType": {
"type": "string",
"maxLength": 127
},
"size": {
"type": "integer",
"minimum": 1,
"maximum": 20971520
},
"digest": {
"$ref": "#/$defs/digest"
},
"state": {
"enum": [
"receiving",
"staged",
"transferring",
"delivered",
"sent",
"rejected",
"discarded"
]
},
"receivedBytes": {
"type": "integer",
"minimum": 0,
"maximum": 20971520
}
},
"required": [
"kind",
"id",
"revision",
"filename",
"mediaType",
"size",
"digest",
"state",
"receivedBytes"
]
},
"queueView": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"const": "queue"
},
"id": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"state": {
"enum": [
"admitted",
"queued",
"dispatched",
"acknowledged",
"working",
"finished",
"failed",
"delivery-unknown",
"recovered-as-draft",
"recovery-failed",
"dispatch-refused",
"rejected"
]
},
"payloadDigest": {
"$ref": "#/$defs/digest"
},
"reason": {
"anyOf": [
{
"$ref": "#/$defs/id"
},
{
"type": "null"
}
]
},
"request": {
"$ref": "#/$defs/id"
},
"draft": {
"$ref": "#/$defs/id"
},
"target": {
"$ref": "#/$defs/target"
},
"frozenInputState": {
"enum": [
"available",
"integrity-unavailable"
]
},
"frozenInput": {
"anyOf": [
{
"$ref": "#/$defs/frozenInputView"
},
{
"type": "null"
}
]
}
},
"required": [
"kind",
"id",
"revision",
"state",
"payloadDigest",
"reason",
"request",
"draft",
"target",
"frozenInputState",
"frozenInput"
],
"allOf": [
{
"if": {
"properties": {
"frozenInputState": {
"const": "available"
}
}
},
"then": {
"properties": {
"frozenInput": {
"$ref": "#/$defs/frozenInputView"
}
}
},
"else": {
"properties": {
"frozenInput": {
"type": "null"
}
}
}
}
]
},
"receiptView": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"const": "receipt"
},
"id": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"request": {
"$ref": "#/$defs/id"
},
"state": {
"enum": [
"admitted",
"queued",
"dispatched",
"acknowledged",
"working",
"finished",
"failed",
"delivery-unknown",
"recovered-as-draft",
"recovery-failed",
"dispatch-refused",
"rejected"
]
},
"reason": {
"anyOf": [
{
"$ref": "#/$defs/id"
},
{
"type": "null"
}
]
}
},
"required": [
"kind",
"id",
"revision",
"request",
"state",
"reason"
]
},
"confirmationView": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"const": "confirmation"
},
"id": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"operation": {
"enum": [
"force-stop",
"recover",
"acquire-recovery-control"
]
},
"target": {
"$ref": "#/$defs/target"
},
"state": {
"enum": [
"pending",
"confirmed",
"consumed",
"expired",
"cancelled",
"reconfirm-required"
]
},
"expiresAt": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"stop": {
"anyOf": [
{
"$ref": "#/$defs/id"
},
{
"type": "null"
}
]
}
},
"required": [
"kind",
"id",
"revision",
"operation",
"target",
"state",
"expiresAt",
"stop"
]
},
"privateStatePage": {
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"const": 1
},
"kind": {
"const": "privateStatePage"
},
"actor": {
"$ref": "#/$defs/id"
},
"conversation": {
"$ref": "#/$defs/id"
},
"snapshot": {
"$ref": "#/$defs/id"
},
"sourceRevision": {
"$ref": "#/$defs/revision"
},
"items": {
"type": "array",
"maxItems": 100,
"items": {
"oneOf": [
{
"$ref": "#/$defs/draftView"
},
{
"$ref": "#/$defs/uploadView"
},
{
"$ref": "#/$defs/queueView"
},
{
"$ref": "#/$defs/receiptView"
},
{
"$ref": "#/$defs/confirmationView"
}
]
}
},
"nextCursor": {
"anyOf": [
{
"$ref": "#/$defs/id"
},
{
"type": "null"
}
]
}
},
"required": [
"version",
"kind",
"actor",
"conversation",
"snapshot",
"sourceRevision",
"items",
"nextCursor"
]
},
"uploadRange": {
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"const": 1
},
"kind": {
"const": "uploadRange"
},
"upload": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"offset": {
"type": "integer",
"minimum": 0,
"maximum": 20971520
},
"length": {
"type": "integer",
"minimum": 1,
"maximum": 262144
},
"total": {
"type": "integer",
"minimum": 1,
"maximum": 20971520
},
"digest": {
"$ref": "#/$defs/digest"
},
"rangeDigest": {
"$ref": "#/$defs/digest"
},
"dataBase64": {
"type": "string",
"maxLength": 349528,
"pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
},
"disposition": {
"const": "attachment"
},
"mediaType": {
"const": "application/octet-stream"
}
},
"required": [
"version",
"kind",
"upload",
"revision",
"offset",
"length",
"total",
"digest",
"rangeDigest",
"dataBase64",
"disposition",
"mediaType"
]
},
"privateRecovery": {
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"const": 1
},
"kind": {
"const": "privateRecovery"
},
"item": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"draft": {
"$ref": "#/$defs/id"
},
"state": {
"const": "recovered-as-draft"
}
},
"required": [
"version",
"kind",
"item",
"revision",
"draft",
"state"
]
},
"privateRefusal": {
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"const": 1
},
"kind": {
"const": "privateRefusal"
},
"request": {
"$ref": "#/$defs/id"
},
"reason": {
"enum": [
"request",
"operation",
"channel",
"grant",
"scope",
"cursor",
"projection-size",
"upload",
"integrity",
"range",
"item",
"conflicting-request",
"state",
"preview"
]
}
},
"required": [
"version",
"kind",
"request",
"reason"
]
},
"privateTextPreview": {
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"const": 1
},
"kind": {
"const": "privateTextPreview"
},
"upload": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"digest": {
"$ref": "#/$defs/digest"
},
"text": {
"type": "string",
"maxLength": 1572864
},
"renderMode": {
"const": "textContent-only"
}
},
"required": [
"version",
"kind",
"upload",
"revision",
"digest",
"text",
"renderMode"
]
},
"frozenInputView": {
"type": "object",
"additionalProperties": false,
"properties": {
"branch": {
"$ref": "#/$defs/id"
},
"draftRevision": {
"$ref": "#/$defs/revision"
},
"text": {
"type": "string",
"maxLength": 262144
},
"uploads": {
"type": "array",
"maxItems": 10,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"$ref": "#/$defs/id"
},
"revision": {
"$ref": "#/$defs/revision"
},
"filename": {
"type": "string",
"maxLength": 255
},
"size": {
"type": "integer",
"minimum": 1,
"maximum": 20971520
},
"digest": {
"$ref": "#/$defs/digest"
}
},
"required": [
"id",
"revision",
"filename",
"size",
"digest"
]
}
}
},
"required": [
"branch",
"draftRevision",
"text",
"uploads"
]
}
}
}
+106
View File
@@ -0,0 +1,106 @@
{
"evidence": "synthetic-contract-only",
"now": 1000,
"request": {
"version": 1,
"kind": "privateClientRequest",
"id": "request-1",
"connection": "connection-1",
"conversation": "conversation-1",
"command": {
"operation": "read-private-state",
"cursor": null,
"limit": 10
}
},
"world": {
"connection": {
"id": "connection-1",
"actor": "actor-1",
"state": "connected"
},
"grant": {
"actor": "actor-1",
"conversation": "conversation-1",
"state": "active",
"expiresAt": 2000,
"capabilities": [
"read-private",
"recover-private"
],
"scopeDigest": "2e4d08329c39869151bb7dff3df6542dfe3f7a746d635b069fabbfc4eb40f680"
},
"sourceRevision": 1,
"items": [
{
"id": "draft-1",
"kind": "draft",
"actor": "actor-1",
"conversation": "conversation-1",
"revision": 1,
"text": "private unsent text",
"state": "unsent",
"branch": "branch-1"
},
{
"id": "draft-2",
"kind": "draft",
"actor": "actor-2",
"conversation": "conversation-1",
"revision": 1,
"text": "other actor text",
"state": "unsent",
"branch": "branch-1"
}
],
"cursors": {},
"nativeOutbox": [],
"scope": {
"host": "host-1",
"seat": "seat-1",
"project": "project-1",
"workspace": "workspace-1",
"conversation": "conversation-1"
},
"mappingVerified": true
},
"frozenExample": {
"version": 2,
"kind": "frozenPayload",
"id": "payload-1",
"actor": "actor-1",
"conversation": "conversation-1",
"branch": "branch-1",
"draft": "draft-1",
"draftRevision": 1,
"text": "hello",
"uploads": [
{
"version": 2,
"kind": "upload",
"id": "upload-1",
"actor": "actor-1",
"conversation": "conversation-1",
"revision": 1,
"filename": "example.png",
"mimeType": "image/png",
"size": 20,
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"state": "staged",
"privateBlobRef": "blob-1",
"recipientScope": {
"host": "host-1",
"seat": "seat-1",
"project": "project-1",
"workspace": "workspace-1",
"conversation": "conversation-1"
},
"transferAck": null,
"createdAt": "2026-09-13T12:00:00Z",
"receivedBytes": 20
}
],
"digest": "4e06080f131b6e7d87e692ffebe8ccd1f78e25aedc24a10aaf68e7c3191f9f48",
"createdAt": "2026-09-13T12:00:00Z"
}
}