248 lines
15 KiB
Markdown
248 lines
15 KiB
Markdown
# 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.
|