docs: close W4 locator and access review gaps

This commit is contained in:
Rocko committed 2026-10-22 18:36:18 -05:00
1 parent 15e91538f2
commit 019a0ea5ba
1 file changed
+445 -112
+445 -112
View File
@@ -52,6 +52,21 @@ live authorization. Removed history, missing blobs or missing extractor
revisions refuse; there is no worktree, export or engine-memory fallback.
The commit/blob field is a locator, not an authority token.
The project's sole publication ref is `refs/heads/writing-published`; only
the human writer advances it, atomically and without force updates. Each
commit contains the book manifest. For a document, its publication commits
are the first-parent commits on that ref that add or change its DOCX blob or
its canonical manifest entry (including deletion/reinstatement). The first
commit is a publication; a sibling-only save is not. `currentCommit` is the
latest such commit for this document at the captured ref tip. Version latest
selects that commit; a locator commit must be one of those publication commits,
not an arbitrary intervening project commit. Deleted document entries refuse
current reads, including history, until explicitly reinstated and granted.
Published history is exactly that document's reachable publication sequence.
A sibling save does not change its currentCommit, stale flag or cursor.
Staleness is still exact commit inequality within that document, even if a
human deliberately saves an earlier byte-identical blob as a new publication.
The writer's successful commit publishes atomically before Saved. A latest
lookup begun after that receipt resolves that published commit, not an old
cache entry. A lookup already in progress remains internally on its captured
@@ -66,7 +81,7 @@ commits/blobs are lowercase 40-hex Git object IDs in the repository's current
SHA-1 object format. A future object format requires a new schema revision.
Digest values are lowercase 64-hex SHA-256. Integers are safe nonnegative
JSON integers. No floats, coercion, duplicate JSON keys or implicit defaults
except the reading view described below. These are proposed O2-only records.
except the explicitly listed request defaults below. These are proposed O2-only records.
A locator is exactly:
@@ -90,23 +105,68 @@ when absent. A textView is always recorded in every locator; there is no
resolver default. quote is a nonempty valid Unicode string bounded to 16,384
code points and 64 KiB UTF-8. Unknown enum values or unexpected types refuse.
The ordinal-primary rule below deliberately refines D25's wording ("IDs
when present, otherwise ordinal"): a possibly duplicated paraId is a required
cross-check, never the lookup key. This is an O2-only design choice for Sage's
implementation brief, not an amendment to Case 2.
`paragraph.ordinal` is the zero-based paragraph index within the selected
story, in deterministic XML document order, including empty paragraphs and
table-cell paragraphs. `xmlId` is null or the literal eight-hex `w14:paraId`,
table-cell paragraphs. `xmlId` is null exactly when the selected node has no w14:paraId, otherwise the literal eight-hex `w14:paraId`,
canonicalized uppercase. Ordinal is always present: paraId alone is not
unique or stable enough. If provided, xmlId must equal the selected node's
attribute; missing, duplicated or mismatched IDs do not trigger a search.
unique or stable enough. The key is mandatory: passing null for a node that has the attribute refuses.
The non-null value must equal that node's attribute; duplicated IDs do not
trigger a search, since ordinal selects the node. Mismatches refuse.
Thus identical paragraphs remain distinct. `start < end <= codePointLength`
selects one paragraph, end exclusive. A multi-paragraph selection is an
ordered nonempty list of these ranges, with no cross-paragraph offsets.
No auto-merging or silent clipping. Empty paragraph references may be shown
in a reading page but cannot manufacture a nonempty quote locator.
selects one paragraph, end exclusive. A multi-paragraph selection has the exact wrapper below, not a bare array:
```json
{
"kind":"docx-selection", "version":1,
"business":"acme", "project":"book", "document":"chapter_1",
"commit":"<40 lowercase hex>", "blob":"<40 lowercase hex>",
"extractor":{"id":"mosaic-docx-text-v1","revision":"<64 lowercase hex>"},
"part":{"class":"body","name":"word/document.xml","story":"main"},
"textView":"final",
"ranges":[
{"paragraph":{"ordinal":0,"xmlId":null},"start":1,"end":5,"quote":"ello","quoteDigest":"<64 lowercase hex>"},
{"paragraph":{"ordinal":1,"xmlId":null},"start":0,"end":5,"quote":"World","quoteDigest":"<64 lowercase hex>"}
]
}
```
It has 2..32 ranges, all sharing the wrapper's exact document, publication,
blob, extractor, view and part. Ordinals are consecutive and increasing.
First range ends at its paragraph length, last starts at zero; intermediate
ranges cover whole paragraphs. Each range verifies its own quote/digest;
no inserted newline is included in any digest. Empty intermediate paragraphs
are represented by start=end=0, quote="" and SHA-256 of empty UTF-8; the first
and last are nonempty. Total quote budget is 16,384 code points and 64 KiB
UTF-8 for the entire selection. A one-paragraph selection uses docx-range
only: there is one canonical representation. No auto-merging, cross-story
selection or clipping. Resolver's locator union and a range chip accept
either exact kind. An atomic selection fails entirely if any range fails;
a response never returns only the valid fragments.
`extractor.revision` is the digest of a canonical build manifest containing
extractor source blob IDs, projection rules revision and locked parser/ZIP
library versions. The release pins that manifest and independently verifies
its digest. It is not caller-supplied executable code or a mutable version
label. Resolver requires that exact available revision; changed extraction
label. Its canonical manifest is exactly `{manifestVersion:1,id,rulesRevision,
sources,dependencies,formattingProperties}`. id is mosaic-docx-text-v1,
rulesRevision is integer 1; sources is a path-sorted array of `{path,blob}`
for every extractor/transitive source input; dependencies is a name-sorted
array of `{name,version,integrity}` from the lock, including ZIP/XML parsers;
formattingProperties is a sorted unique array of expanded XML names.
Duplicate keys/entries refuse. Canonical bytes are compact JSON, UTF-8,
no BOM/newline, recursively lexicographic ASCII object-key order; strings
escape only quote/backslash and U+0000..001F (lowercase four-hex \u escapes),
with other scalar values literal. Arrays preserve the specified sorted order,
integers are decimal without leading zeroes. revision is SHA-256 of those
bytes. Independent tooling must reproduce it. Server readiness is derived
from the verified release artifact containing that manifest and its reviewed
formatting list, not any human-authored config field.
Resolver requires that exact available revision; changed extraction
rules create a new revision, never silently reinterpret an old locator.
A paragraph extraction record is exactly:
@@ -123,10 +183,20 @@ A paragraph extraction record is exactly:
Anchors have the exact shape `{kind,story,offset}`; allowed
kinds are `footnote-reference`, `endnote-reference`, `comment-start`,
`comment-end`, `comment-reference`; story is a canonical decimal signed XML
ID string, offset a code-point boundary in this record. They never grant
access to their target. wikiTokens are exact source slices matching the
literal syntax in section 5, not instructions or external links. All text
`comment-end`, `comment-reference`; story is a canonical decimal nonnegative XML
ID string (`0` or `[1-9][0-9]*`, the same as note/comment locator stories),
offset a code-point boundary in this record. They never grant
access to their target. wikiTokens have exactly `{start,end,literal,form,target,label,heading}`.
form is link/label/heading/embed; label and heading are null except in their
corresponding form. target is 1..256 code points without `[`, `]`, `|`, `#`,
control characters or newline; a label/heading is 1..256 code points without
brackets, `|`, `#`, controls or newline. literal is respectively `[[target]]`,
`[[target|label]]`, `[[target#heading]]` or `![[target]]`. start/end are exact
paragraph code-point bounds, including the exclamation for embed. No nested
forms or combined label+heading/embed variants are interpreted in v1; they
remain text. Tokens are maximal nonoverlapping complete forms in left-to-right
order, with their literal equal to the text slice. They are not instructions
or external links. All text
and metadata are untrusted document data. Part/story IDs, excerpts and
comment content never enter system prompts as instructions.
@@ -184,7 +254,7 @@ retain a full document without it becoming readable to agents.
Parse XML without DTDs, external entities, XInclude or network/file resolution.
Reject DOCTYPE/entity declarations. Bound before allocation: compressed blob
32 MiB, total expanded bytes 128 MiB, each entry 16 MiB, 2048 entries,
expansion ratio 100:1, XML depth 128, 1,000,000 nodes, 100,000 paragraphs,
expansion ratio 100:1 both per entry and for the total (nonempty expanded data with zero compressed bytes refuses), XML depth 128, 1,000,000 nodes, 100,000 paragraphs,
1,000,000 code points per paragraph and 10,000,000 per document. Extraction
has a 5 s host deadline and bounded worker memory of 256 MiB. Exceeding any
bound yields `editor-document-unsupported`, no truncated success. These are
@@ -194,20 +264,56 @@ changing them requires reviewed parser evidence. CRC/size mismatches refuse.
## 5. Deterministic reading views
`textView` enum is `final`, `original`, `markup`. Default requests use final.
Final is the accepted-reading projection: include w:t in normal runs and
w:ins/w:moveTo; exclude w:del/w:moveFrom, including w:delText. Original does
the reverse. This does not accept/reject changes or modify the DOCX. Markup
includes both in source order and returns explicit revision spans with kind,
ID and code-point offsets; deletion/source text is data, not an instruction.
Original/markup and comments require separately enabled scopes; final body
permission alone cannot reveal hidden deletions or comment text. Revision
author/date metadata is omitted from agent responses in v1.
Final is the accepted-reading projection, not an accept-changes operation.
A text run is visible in final iff no ancestor is w:del or w:moveFrom; in
original iff no ancestor is w:ins or w:moveTo. This ancestor veto applies to
nested revisions, so an insertion inside a deletion is absent in both final
and original. Markup includes both in source order with revision spans for
every revision container (nested spans may overlap). w:t and w:delText use
the same ancestor rule, never special substring searches. Revision IDs are
canonical nonnegative decimals; malformed or unmatched move ranges refuse.
Paragraph-mark revisions under pPr/rPr are not inline text containers. An
inserted paragraph mark makes that paragraph's projected text empty in
original; a deleted paragraph mark makes it empty in final. If both apply,
both views are empty. Markup preserves its text and adds the corresponding
whole-paragraph revision spans. Paragraphs are never merged, split or
renumbered by a view; even an empty projection keeps its ordinal. Neither
view edits or accepts/rejects changes. Original/markup and comments require
separate scopes, so final body access never reveals hidden deletion/comment
text. Revision author/date metadata is omitted from agent responses in v1.
Closed story/run leaf table (all names in w namespace unless stated):
| Leaves | Text/anchor contribution |
| --- | --- |
| t, delText | decoded text subject to view/ancestor rule |
| tab | U+0009 |
| br, cr | U+000A, regardless of page/column type; layout kind does not affect offsets |
| noBreakHyphen | U+2011 |
| softHyphen | U+00AD |
| annotationRef | zero text; only allowed in comment story |
| footnoteRef, endnoteRef | zero text; only in matching note story |
| footnoteReference, endnoteReference | zero text; matching anchor at current offset with canonical story ID |
| commentRangeStart, commentRangeEnd, commentReference | zero text; matching comment-start/end/reference anchor |
| moveFromRangeStart, moveFromRangeEnd, moveToRangeStart, moveToRangeEnd | zero text; validate paired IDs, do not duplicate revision text |
| bookmarkStart, bookmarkEnd, permStart, permEnd, proofErr, lastRenderedPageBreak | zero text; structural only, never access grants |
| sym, ptab | refuse requested story (font/position dependent) |
| fldChar, instrText, delInstrText, fldSimple | refuse requested story, including field results |
| drawing, pict, object, txbxContent, subDoc, altChunk | refuse requested story |
The table is exhaustive for story/run leaves; unknown leaves refuse, including
nontext leaves not explicitly named. Formatting properties belong only to
the separately closed manifest list. Anchors/range marks under invisible
revision ancestors are omitted in that view but still structurally validated.
Comment author data and ACL-like Word permission markers never supply Mosaic
authority. Unknown extension/AlternateContent text is unsupported, not ignored.
Walk direct story paragraphs and table cells in XML order, including nested
tables in their containing cell order. Paragraph ordinals do not change with
view. Concatenate visible text across adjacent runs with no invented spaces.
Preserve decoded Unicode as stored: no NFC, case folding or whitespace
collapse. w:tab yields U+0009; w:br/w:cr yields U+000A; xml:space text remains
collapse. xml:space text remains
literal. Offsets count Unicode scalar values, not UTF-16 units or graphemes;
unpaired surrogates/invalid XML characters refuse. Field instructions,
hidden/vanished text and text boxes are not silently omitted: their presence
@@ -233,7 +339,8 @@ permissive descendant-text scraper.
Reference marks contribute anchors at the current text boundary, not invented
note numbers or inline comment bodies. Each note/comment is separately paged.
A quote cannot straddle a view, paragraph or story. Extract literal
A single range cannot straddle a view, paragraph or story; the selection wrapper
may cover contiguous paragraphs only under section 3. Extract literal
`[[target]]`, `[[target|label]]`, `[[target#heading]]` and `![[target]]`
after concatenating visible runs within one paragraph, so split-run tokens
survive formatting changes. Preserve complete literal bytes as UTF-8 after
@@ -268,28 +375,33 @@ raw package attributes. `nextCursor` is null at end; `currentCommit` is the
current published document commit; stale is exactly commit != currentCommit.
A resolver response is exactly `{version:1,locator,currentCommit,stale}`:
the verified quote is the locator's quote, with no separate unverified text.
There is no default "first part" or automatic descent into notes/comments.
There is no default "first part" or automatic descent into notes/comments. The broker-bound business must equal
locator.business; a mismatch is editor-access-refused before object lookup.
Markup additionally requires `revisionSpans` on each
row: array of `{kind,id,start,end}`, kind insert/delete/move-from/move-to,
with null-free canonical decimal IDs and valid boundaries; other views omit
that key. Limits cap output at 64 KiB UTF-8 JSON per response. Never split a
paragraph silently: if one row exceeds the page budget, return
`editor-range-too-large`; a caller may request a checked subrange through a
separately bounded citation, not receive a truncated paragraph masquerading
as complete. Response overflow never returns partial data with success.
that key. Limits cap output at 64 KiB UTF-8 JSON per response, including envelope and cursor. End a page before the first complete
row that would overflow, and return a cursor for that row. This is normal
paging, not partial-row success. If that first row cannot fit even on an
otherwise empty page, return editor-range-too-large with no text. Such a
paragraph is unsupported for agent reading in v1; no subrange read exists.
A human-visible "paragraph exceeds agent reading limit" reason appears only
after authorization. The human editor/save is unaffected. All empty paragraphs
still produce rows and advance the cursor. No page can return the same cursor
without progress.
Cursor is a host-authenticated opaque token binding principal, session,
project/document, commit/blob, extractor, view, part, next ordinal, grant
revision and expiration (5 minutes). It carries no plaintext content.
project/document, commit/blob, extractor, view, part, next ordinal, the complete
authority revision vector defined in section 7 and expiration (5 minutes). It carries no plaintext content.
Tampering, reuse by another session, wrong request parameters or a revoked
scope refuses. A latest cursor continues its captured snapshot with stale
metadata if a save intervenes; starting again with null cursor returns the
new publication. Cursor is not a capability and grants are rechecked every
page. Repeated ordinal ranges are stable within one committed projection.
Resolver steps: authorize current principal and historical-view scope; load
Resolver steps: authorize the current principal tuple, live scopes and exact/historical share (there is no separate undefined historical-view scope); load
that commit's manifest/blob; validate package; run the exact pinned extractor;
select exact part/story/ordinal and optional XML ID; select code-point range;
select exact part/story/ordinal and mandatory XML-ID value; select code-point range;
require literal quote equality and SHA-256(UTF-8 quote) equality. Require both
blob and commit membership, not digest alone. Any mismatch refuses
`editor-citation-invalid` without guessed text. After newer publication the
@@ -304,91 +416,286 @@ for absent ownership/grant/share (same response for nonexistent or foreign IDs);
authorization; `editor-version-unavailable` for authorized missing history or
extractor; `editor-citation-invalid` for locator/quote mismatch;
`editor-cursor-invalid` for invalid paging; `editor-range-too-large` for size.
Return no source text, XML, path or untrusted parser error in refusals.
The exact refusal envelope is `{ok:false,error:{code,message}}`; code is one
of those enum strings or editor-policy-conflict/editor-sharing-unsupported
(both 409, defined in section 7), and message is fixed server text for that code (no user
input). Both missing/foreign and ungranted IDs use the same code/message/status
403. Syntax errors before identity lookup use editor-citation-invalid, 400;
context unavailable is 503, cursor-invalid 400, version unavailable 409,
unsupported/range-too-large 422. Successful tools return `{ok:true,result}`
where result is the defined response; errors never include result/partial
rows. Return no source text, XML, path or untrusted parser error in refusals.
## 7. Grants, sharing and all read surfaces
## 7. Authority data, dedicated sessions and sharing
Access is the intersection of bound role tool grant, human-configured
business/project/document scope, principal's live project/universe rights,
requested part/view scope, server readiness and a valid share context for
this conversation. No wildcard across businesses/universes. Human/session
identity comes from existing trusted transport; a request cannot name a human.
The six creative role tool declarations grant the *operation*, never every
chapter. An agent's own supporting-document writer grants no story access.
### 7.1 Scope and principal records
Human-only sharing policy defaults `visible-chip-per-send`; the chip is
visible and removable and binds document, committed version and selected
verified locators. Before readiness it says editor context unavailable and
sends no document ID, title, path, selection, quote or source metadata.
The server resolves selection against the saved projection; if it cannot
map exactly (including a live edited selection), refuse context attachment.
Never send unsaved text to try to find a match. Free chat can still send with
no editor context if the human removes the unavailable chip.
O2 context is permitted only in newly created **O2-dedicated conversations
and execution sessions**. Existing C2 threads/sessions cannot be converted,
attached to O2 context or resumed/forked into O2 mode; new IDs and a fresh
empty context are mandatory. There is no mixed-thread turn. The UI offers
"Open dedicated writing conversation", never silently changes an existing
chat's permissions. Existing C2-02 supporting-doc writes and C2-06 replies
retain their semantics. The narrowing below belongs only to this explicitly
opted-in O2 mode, closing review M1 without a Case 2 amendment. It changes
neither the Case 2 schema nor the meaning of its replies/line citations.
A successful send creates a server-owned immutable share record scoped to
that message/conversation, human principal, document and exact committed
ranges/views. It narrows the live grant; it does not widen it or authorize a
later version. Every subsequent lookup supplies the server-resolved share
context and intersects current grants. A future send requires another visible
chip under the default policy. A human may explicitly change sharing to
`off` or `conversation-current`: off revokes future reads; conversation-current
permits the named conversation's authorized latest lookups until human
revocation/session end, with a persistent visible indicator. Such an explicit
policy change cannot add document scopes or hide the indicator. Pins only
organize human UI; they never create a share record or turn on sharing.
A range share permits only its ranges, not an entire paragraph/page or comments.
Latest whole-document reads require a whole-document share, never a range chip.
Authorization principal is a tuple, not a seat's display name:
`{business,human,agentInstance,conversation,executionSession}`. human is the
stable authenticated identity that owns the O2 conversation; agentInstance
and executionSession come from its bound broker run. The request cannot
supply/replace them. All human readers and all agent lookups require live
R5 project/universe access plus the O2 grant below; role files grant the
operation only. The business owner is not an implicit reader of someone
else's private conversation. A viewer can read only their authorized human
view, never write a share/settings change or invoke an agent by sharing.
An owner/collaborator with current project write access may share within
their own readable O2 scope. Agent scope and sharing human scope intersect;
a human cannot share a part or view they lack.
Historical exact shares resolve their old citations while live authorization
holds. A successful save makes the next authorized *latest* lookup use its
new commit, but never silently upgrades an exact earlier share. The UI says
"Saved newer version; share again" where only an old range is shared. No
future edit is disclosed on the strength of an earlier send. Share records
are resolved by broker session/context, not caller-minted IDs.
O2 scopes live in the new append-only bus table writing_access_events, not
access.json or role-file wildcard changes. Proposed human-only verb
`editor.access.set({project,document,subject,scope,expectedRevision})` takes
subject exactly `{kind:"human"|"agent",id}` and scope exactly
`{enabled,parts,views,history,mayShare}`. parts is a sorted unique nonempty
subset of body/footnote/endnote/comment (all stories within the selected
class); views is a sorted unique nonempty subset final/original/markup;
history and mayShare are booleans. mayShare must be false for agent subjects.
enabled=false revokes all access; even its otherwise populated scope cannot
authorize. No wildcards or implicit note/comment inheritance. A comment
requires parts including comment and the requested view. history=true permits
older publications subject to a matching share; history=false permits only
the current publication. The document is always explicit.
Before returning raw conversation/session history, replaying a prompt,
resuming/forking a worker or exporting an agent transcript, reauthorize each
O2 attachment/content block. Keep source text out of generic raw session
stores and logs: persist opaque checked references, not rich prompt text.
Host prompt assembly resolves content in memory for the authorized turn.
Agent harness transcripts containing expanded content must stay host-owned,
outside worker/generic roots, encrypted or withheld from raw session tools;
W4b must prove the selected harness does not persist readable O2 text into a
worker-readable store. Otherwise that harness's editor tools remain disabled.
Revocation cannot erase an agent's memory or already delivered output; it
prevents subsequent fetch/replay, and the UI must not promise retrospective
secrecy. Copying an authorized quote into free agent prose is not a new grant:
O2-derived stored outputs retain the same provenance/access envelope.
This is conservative turn-level provenance, not content recognition: every
output of a turn receiving O2 context, and any resumed/forked descendant,
inherits the union of those document scopes. It can be emitted only into a
host-controlled destination enforcing that envelope. Generic supporting-doc
writes, git writes, exports and arbitrary outgoing tools from that turn
refuse unless their destination has the identical enforcement; normal C2
turns without O2 context keep their existing tool semantics. W4b must enforce
this broker-side before dispatch, not ask an agent to self-label a quote.
If the runner cannot isolate that tool set for an O2-context turn, it cannot
receive O2 context. The separate story-write ban always applies.
Only the existing business-owner human proof may write these policy grants,
through the existing trusted human CLI or authenticated write-enabled Console
transport, with the existing origin/session/proof checks. No new proof file,
caller or exemption. Not tester, agent, viewer or collaborator policy writes.
Sharing and scope are different: an authorized collaborator may send context
but cannot change grants. expectedRevision is the latest sequence for that
business/project/document/subject, zero when none. Compare, append and audit
are atomic; conflict returns editor-policy-conflict (409, fixed message).
Receipt is `{ok:true,result:{id,revision,previousRevision,scopeDigest}}`, where
revision is the appended seq, id is host-generated, and scopeDigest is SHA-256
of canonical scope JSON using section 3 serialization. Missing grant denies.
All O2-derived exports, caches, extraction indexes, previews, temporary files,
source copies and git history are classified with their document, outside
generic read/grep roots and outside Case 2 library mappings. No raw O2 git
clone/read grant; no source copy under a worker's project root. Generic git
or shell tools cannot reach it through shared parent mounts, symlinks,
hardlinks, alternate worktrees, object alternates or exports. Read-only is
insufficient: no store mount in workers. Validate canonical roots, reject
symlink components, and enforce ownership/mode and sandbox mount closure.
A new import/copy needs fresh document grants and sharing. Don't expose
manifest names/document lists through metadata routes before authorization.
Native engine routes remain human-only and cannot be proxied by agent tools.
Each access decision records an authority vector exactly
`{humanAccess,agentAccess,projectAccess,threadAccess,sharingPolicy,share}`.
humanAccess and agentAccess are the respective latest writing_access_events
seq values; projectAccess is the existing R5 access revision; threadAccess
is the latest O2 conversation membership-event seq; sharingPolicy is the
latest writing_policy_events seq; share is the exact writing_shares seq.
Missing inputs deny rather than zero-default except initial policy explicitly
materialized by the human writer as the default. A cursor binds every value;
any change invalidates it. Each new lookup reevaluates rights, including a
revocation between asynchronous extraction and response emission.
W4b is blocked if any raw session or generic surface cannot enforce these
rules. Do not compensate by UI hiding, retroactively classifying all Case 2
Markdown as secret or widening its schema. Store and bus migrations are
reviewed together; revoke/invalidate caches by grant revision and verify live
scope again before emission. Cache keys include document, commit/blob,
extractor, part and view; shared extraction cache is never an authorization
cache. Host audit events contain IDs/digests/status only, never content.
### 7.2 One human per context; settings and chip requests
An O2 conversation has exactly one owner human and one agent instance. No
second human joins it in v1: a thread-share attempt refuses
editor-sharing-unsupported (409), rather than borrowing the sender's grants
for someone else. A second human starts a separate O2 conversation, supplies
their own grant/share and gets no previous content. This is a new O2 mode,
not a restriction on existing C2 sharing. Human and agent must still have
matching business/project/universe rights. Read-only previews cannot send.
Human-only `editor.sharing.set({conversation,mode,expectedRevision})` is
available only to that authenticated owner with current write access over
the same trusted write transports. mode is off/visible-chip-per-send/
conversation-current. It atomically appends writing_policy_events and returns
`{ok:true,result:{id,revision,previousRevision,mode}}`. Default is the explicit
visible-chip-per-send policy on new conversation creation. Selecting off
immediately invalidates **all** prior shares, reads, resolves and cursors for
that conversation, not only new sends. Selecting another mode later does
not revive shares from the earlier policy revision. No pin changes a mode.
In visible-chip-per-send the human sends exactly
`{conversation,message,expectedPolicyRevision,chip}` via the human send path.
message is human-authored free-chat text; W4b's bounded human API handles it,
never an agent-supplied impersonation. chip is null or one of:
- `{kind:"range",document,baseCommit,selection}` where selection is one
docx-range/docx-selection locator, with its business/project/document/
commit equal to the server-owned conversation/document and baseCommit.
- `{kind:"document",document,baseCommit,parts,views,history}` selecting a
whole-document **exact publication**, with explicit part/view lists and
history boolean constrained by both grants.
Both chips require a server-issued human editor selection token as an
additional required `selectionToken` key. It binds authenticated login,
conversation, document, baseCommit, saved editor model revision, and either
exact locator or whole-document scope digest, expires in 2 minutes and is
single-use. The human editor adapter creates it only after exact mapping to
the committed projection. The request supplies no draft text or engine path
for mapping; mismatched/edited selection fails locally and server-side.
A stale base or document change before send refuses, requiring a fresh
visible chip; an old citation remains resolvable through its already-sent
share while authorized. Null chip sends no editor context. Before readiness,
the chip says unavailable and sends no document ID, title, path or selection.
A range share permits only editor_resolve_citation for that exact locator
(or a range wholly contained within it with newly verified quote/digest).
It always refuses editor_read_committed, since rows would expose surrounding
text. A document exact share allows paging only its named publication,
parts/views; it never grants future saves. No implicit body-to-note/comment
or final-to-markup expansion occurs.
conversation-current is an explicit human policy with a persistent visible
indicator and a required whole-document chip. The same chip fields bind
baseCommit and scope, but the share record has versionMode=latest: baseCommit
is the acknowledged publication at issuance, not a future-version ceiling.
The next authorized latest lookup after Saved follows currentCommit within
the granted part/view/history bounds. This exception to exact-version sharing
exists only in this explicit mode. Range chips are refused in this mode,
not silently upgraded. Pins and earlier exact shares never become latest.
Latest shares expire on explicit policy revocation, owner grant loss, O2
conversation close or **execution-session end/clear**. This means the new
O2 execution session, not the Console login, bus connection or a C2 thread
session. Relaunch/clear requires a fresh chip; a dropped socket/login does
not silently widen or prolong one. Exact shares may remain verifiable across
execution sessions through their same human conversation while live policy,
history permission and grants hold, but never supply replay context to a
new agent session without a new share. Their scope is citation verification
only after the issuing execution session ends. No cross-human context reuse.
### 7.3 Stored share schema
The immutable writing_shares payload is exactly
`{version,id,business,project,document,conversation,executionSession,message,
human,agentInstance,mode,versionMode,baseCommit,blob,extractor,scope,selection,
authority,policyRevision,issuedAt}`. version=1; IDs use section 3 grammar;
mode is the policy at issuance; versionMode is exact/latest; baseCommit/blob
are the acknowledged snapshot and extractor is its exact pinned revision.
scope is `{parts,views,history}` as above; selection is the verified locator
for a range, otherwise null. authority is the complete revision vector at
issuance; its share field equals this row's seq (assigned atomically).
issuedAt is canonical UTC millisecond ISO timestamp. policyRevision equals
authority.sharingPolicy. message names a new writing_messages row, not a C2
message. A unique `(conversation,message,document)` constraint and operation
idempotency key prevent duplicate grants on retry. No user-provided human,
agentInstance, authority, issuedAt, blob or share id is accepted.
A scope decrease/revocation denies even historical shares; an unrelated new
policy revision also invalidates prior policy-bound shares. A human who
needs them again explicitly reshares. Missing session/share is no context.
### 7.4 Dedicated storage and every output surface
Every turn receiving O2 context runs with the new O2-only allowlist of
read/resolve/flag plus its dedicated conversation reply. No generic file,
shell, git, supporting-doc writer or arbitrary outgoing tool is available.
This narrows only new O2 sessions, never C2 turns. All agent output from such
a turn is conservatively O2-derived; it stays in the dedicated conversation
with the union of the document provenance/scopes. No semantic quote detector
or agent self-labelling supplies this boundary. Human text and titles in that
conversation use the same dedicated storage, so a human pasting an O2 passage
cannot accidentally route it into an old renderer. O2 replies never enter
C2-06 processing. An O2 flag is metadata stored by W5, not a story edit.
| Existing surface | Required O2 behavior |
| --- | --- |
| threads/<business>/<thread>/session and claude-projects | no O2 content, prompt, model transcript, title or derived reply |
| threads/<business>/<thread>/sessions/<session>/session and claude-projects | same; O2 mode never binds these native stores |
| session-workspaces/<business>/<instance>/<session> | no O2 text or exports; no store mount |
| launches/<business>/<run> including bundle, turns, runner.log and session | opaque IDs/status only, never O2 prompts/output; host transient O2 adapter instead |
| bus events, messages, message_threads and existing sessions rows | no O2 text/titles/quotes; only fixed readiness/status audit with opaque IDs if needed |
| Console legacy thread view | no O2 rows; dedicated writing view reauthorizes every read |
| CLI thread, talk, follow and raw-session tools | no O2 content; unsupported mode refuses, not generic rendering |
| Discord journal.mjs/context.mjs and control-board Discord delivery | no O2 conversation subscriptions/context/output; fixed unavailable notice only |
| <dataRoot>/runs write-once evidence | never O2 text, prompts, titles, excerpts, stdout/stderr or derived output; only opaque run status |
| bus-host/sessions.json, launch logs and activity projections | IDs/liveness only, no document or conversation title |
Host-only `<dataRoot>/writing-context/` (0700 owner, no generic mount/remote)
holds transient O2 adapter memory and any unavoidable encrypted native
transcript. It is keyed to O2 session, not an alias/symlink into seat stores.
Release readiness requires an independently tested adapter that prevents
plaintext native persistence/logging outside this boundary, including crash
and timeout paths. If either Pi or Claude cannot meet it, that harness's O2
tools stay off; do not expose the session then attempt post-hoc redaction of
write-once run records. Readiness is per adapter. No O2 text enters immutable
legacy evidence, even briefly. Stored dedicated replies are authorized afresh
at read time; live grant loss hides cached UI content and prevents prompt
replay/fork. Revocation cannot erase already delivered knowledge and the UI
must not promise that it can.
O2-derived exports, extraction indexes, caches, previews, source copies and
git history retain document classification outside generic roots and C2
library mappings. O2 repositories have **no git remotes**. No push, mirror,
fork, plaintext backup or remote replication of an O2 repository is allowed,
including through human credentials; no seat Gitea account can discover,
list, fork or clone one. Backup transport is out of scope and disabled, not
an implied exception. Stack-copy/release copies source code and extractor
artifacts only, never writing/, writing-context/, an O2 database snapshot,
exports or repository object directories. No raw O2 Gitea grant may be minted.
Validate absence of remotes at store creation, before save/read and activation;
a new remote or object alternate disables O2, rather than trusting a read-only
mount. Symlinks, hardlinks, alternate worktrees and parent mounts must not
expose it to generic tools. Native engine APIs remain human-only.
### 7.5 Named migration and rollback contract
W4b adds the following new STRICT append-only bus tables (seq INTEGER primary
key, at TEXT canonical timestamp, id TEXT unique, all other fields NOT NULL
unless stated). Every update/delete/replace refuses; latest seq is state.
JSON columns contain the exact canonical schema named above, not arbitrary
unvalidated JSON. No existing C2 messages/events/sessions column changes.
| Table | Additional columns and key |
| --- | --- |
| writing_access_events | business TEXT, project TEXT, document TEXT, subject_kind TEXT (human/agent), subject_id TEXT, by_human TEXT, previous_revision INTEGER, scope_json TEXT, operation_id TEXT UNIQUE; latest per document/subject |
| writing_conversations | business TEXT, project TEXT, owner_human TEXT, agent_instance TEXT; one creation row per id, private O2 identity only |
| writing_conversation_events | conversation TEXT FK, op TEXT (close), by_human TEXT; latest membership/close revision, creation seq is initial threadAccess |
| writing_execution_sessions | conversation TEXT FK, execution_session TEXT UNIQUE, adapter_revision TEXT; no native transcript path |
| writing_session_events | execution_session TEXT FK, op TEXT (end/clear), by_principal TEXT; terminal event invalidates latest shares |
| writing_policy_events | conversation TEXT FK, mode TEXT, by_human TEXT, previous_revision INTEGER, operation_id TEXT UNIQUE |
| writing_messages | conversation TEXT FK, execution_session TEXT FK nullable for human-only messages, from_kind TEXT (human/agent), from_id TEXT, body TEXT, provenance_json TEXT, operation_id TEXT UNIQUE |
| writing_shares | conversation TEXT FK, message TEXT FK, document TEXT, payload_json TEXT, operation_id TEXT UNIQUE; UNIQUE(conversation,message,document) |
Each table also has business/project columns if not otherwise listed; FK and
application checks require agreement, not just ID existence. O2 message
provenance_json is exactly an array of `{document,share,commit,blob,extractor}`,
sorted by document/share; every entry requires live scopes. Human messages
with no O2 attachment use [] but remain private dedicated messages. No
existing renderer queries these new tables; generic bus recordEvent and
recordTask cannot write their rows. Access/policy/shares use only the trusted
handlers and atomic transactions. Grant seq values are table-specific, with
kind-qualified revision vector fields; never compare unrelated seq counters.
Sage allocates the schema edge; migration creates these empty tables and
triggers, with no backfill/conversion of C2 citations, messages or raw sessions.
Production adoption stays refused. The writer registers new documents without
creating grants; the owner explicitly sets both human/agent document scopes.
O2 session creation and its default policy are one atomic operation behind
owner write proof; receipt `{id,executionSession,policyRevision}` contains no
content. No legacy thread/session can be attached by guessed ID.
Old release rollback must not render O2 data. The new store schema version
normally makes old binaries refuse opening it, but security does not depend
on that: even a restored pre-W4b database reader can see only its unchanged
legacy tables with no O2 content. O2 messages/share/provenance remain in new
tables, never a tagged body in existing messages. Activating a pre-W4b copy
disables O2 context, never converts dedicated conversations to C2 or exports
transcripts for compatibility. O2 repositories/stores remain outside copies.
A rollback probe uses recognizable secret fixture text in O2-only messages,
then invokes old Console/CLI/Discord/raw-session renderers: none may emit it,
its title, quote or prompt. No live rollback is part of acceptance testing.
The XML/ZIP parser safety ceilings and absence-of-remote checks apply to
human store ingestion too where needed to prevent path escape and remote
replication; a projection-unsupported rich feature alone does not reject a
valid human save. Parser refusal codes have a fixed human-readable reason
category, never raw XML or a misleading empty context. Fields/hyperlinks/
text boxes may make real chapters agent-unreadable in v1; the UI explicitly
says this and never claims an empty successful projection.
Every extractor revision referenced by retained publication history or shares
is retained as a verified immutable host artifact. An activation missing one
refuses O2 readiness (no silent citation downgrade), including rollback to a
W4b-era release that drops a revision. A pre-W4b release can run with O2 wholly
off and cannot read those records. Retiring revisions requires a separately
reviewed retention/prune operation for all dependent O2 history/shares; not
an automatic stack-copy cleanup. Artifact caches are not authorization caches.
## 8. Acceptance matrix and release evidence
@@ -421,6 +728,15 @@ no fixture-only readiness flag may exist in production APIs.
| A20 | Adoption gate before/after readiness and Q36 disposition | absent/revoked server readiness and required notice readiness refuse |
| A21 | Human full-feature save remains separate | unsupported projection yields no agent context, never lossy human save |
| A22 | Scope revocation during extraction | final pre-emission check prevents async stale-authority leak |
| A23 | Sibling chapter save | currentCommit/stale/cursor for untouched document remain unchanged |
| A24 | Full page and oversized paragraph | end before overflow; first oversized row refuses without text; no unreachable subrange claim |
| A25 | No O2 git remote or release copy | seat Gitea list/fork/clone fails; configured remote/object alternate disables O2; copies exclude stores |
| A26 | Every section 7.4 store/renderer | secret marker absent in each named legacy store, raw session, CLI/Discord/Console view and runs/ |
| A27 | Pre-W4b rollback reader | new O2 rows never rendered through legacy tables; missing retained extractor disables O2 |
| A28 | Dedicated mode vs C2 | mixed attachment/resume/fork denied; unchanged C2 supporting writes/replies fixtures pass |
| A29 | Two humans/viewer/grant revisions | viewer cannot share; second human cannot join or reuse context; all cursor authority fields checked |
| A30 | Canonical schemas and complete selections | unknown keys, inconsistent ranges, xmlId null mismatch and noncanonical story IDs refuse |
| A31 | Closed leaf/revision table | independent projection covers every leaf, nested revisions and paragraph-mark empties in all views |
Implementation acceptance includes writing, bus, harness, business, WebUI,
seat mount/import closure and stack-copy/release fixtures; explicit production
@@ -435,6 +751,23 @@ Megavolt reviews this separate document and records a verdict in
commit/blob handoff to Megavolt and Sage. Q36 remains with Jason through Mos;
no adoption, C2 reader/reply notice change or schema reinterpretation ships
on this document. W4b's brief must allocate ownership for every surface in
A16/A17 before code: an unowned bypass is a blocker, not a deferred security
A16/A17 and section 7.4 before code: an unowned enforcement path is a blocker, not a deferred security
note. No live files, editor source imports, parser execution or tests were
used to write this design.
## Round 2 changes against review 08fcb8c1c
B1: exact selection wrapper, canonical paragraph identity, wiki/anchor schema,
chip/share and refusal records; ordinal-primary D25 refinement explicit.
B2: closed leaves and nested/paragraph-mark revision rules. B3: per-document
publication sequence and sibling-save acceptance. B4/B5: concrete append-only
authority, human proof, exact/latest share policy, revision vector and
single-human dedicated session lifetime. B6: complete-row paging; oversized
paragraph unsupported, no circular subrange API. B7: no remotes/backup push,
seat token and copy probes. B8: named native stores, renderers and immutable
run evidence, with O2-only storage/adapter readiness. B9: tables/columns,
rollback separation and extractor retention. M1: dedicated O2 conversations
only; mixed C2 context refused, no C2 requirement change. N1-N7 addressed in
canonical manifest, bound-business check, ratio/default wording, UI unavailable
reason, release-derived readiness and new A23-A31 acceptance rows. These are
proposed definitions, not passing implementation tests. Q36/adoption unchanged.