docs: specify O2 projection marks and restore revocation fence

This commit is contained in:
Rocko committed 2026-10-22 18:42:16 -05:00
1 parent 019a0ea5ba
commit 54246cb32b
1 file changed
+164 -30
+164 -30
View File
@@ -273,15 +273,20 @@ 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.
Paragraph-mark revisions under pPr/rPr mark only the paragraph boundary,
not the runs' text. A mark-only w:ins or w:del changes **no projected text**
in final or original: run content follows the ancestor rule alone. Although
accepting a mark deletion in Word joins paragraphs, this locator projection
never merges, splits or renumbers XML paragraphs in any view. It preserves
the text on both sides as separate ordinal records. In markup a paragraph
mark contributes a zero-width span at that paragraph's code-point length,
kind paragraph-mark-insert or paragraph-mark-delete with its revision ID;
it never becomes a whole-paragraph deletion span. A mark-only insertion's
text stays in original; a mark-only deletion's text stays in final. Inline
run revisions remain independent. No 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):
@@ -293,6 +298,7 @@ Closed story/run leaf table (all names in w namespace unless stated):
| noBreakHyphen | U+2011 |
| softHyphen | U+00AD |
| annotationRef | zero text; only allowed in comment story |
| separator, continuationSeparator, continuationNotice | structural only, allowed only inside reserved footnote/endnote stories; never projected |
| 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 |
@@ -302,6 +308,15 @@ Closed story/run leaf table (all names in w namespace unless stated):
| fldChar, instrText, delInstrText, fldSimple | refuse requested story, including field results |
| drawing, pict, object, txbxContent, subDoc, altChunk | refuse requested story |
Leaf validation covers **the entire selected XML part**, including all its
normal and reserved stories, before any requested story is projected. Reserved
separator/continuation notes are structurally validated against this same
closed vocabulary, with separator/continuationSeparator/continuationNotice
permitted only there, never a readable locator story. Each reserved ID/type
must be internally consistent; duplicate reserved IDs refuse. A separator
leaf in a normal note/body/comment refuses. A valid separator in a reserved
note does not make readable footnotes/endnotes unsupported.
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
@@ -327,9 +342,9 @@ properties) and sdtContent only. Hyperlink containers are unsupported even
without a relationship. Unknown text-bearing containers refuse. Nontext
subtrees pPr, rPr, tblPr, tblGrid, trPr, tcPr and sdtPr may be ignored only
if they contain no story text elements, field codes, external references,
objects or unknown namespaces. Structural leaves bookmarkStart/bookmarkEnd,
proofErr, lastRenderedPageBreak and the reference marks below contribute no
text. w:sectPr is layout-only, with relationship targets validated normally.
objects or unknown namespaces. Structural-leaf behavior is defined only by
the closed table above. w:sectPr is layout-only, with relationship targets
validated normally.
Formatting-property descendants are a closed W4b release artifact: its full
expanded-name list is included in the extractor manifest and independently
reviewed before enablement; any name absent from that list refuses. This
@@ -378,7 +393,8 @@ 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. 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,
row: array of `{kind,id,start,end}`, kind insert/delete/move-from/move-to/
paragraph-mark-insert/paragraph-mark-delete (mark spans have start=end),
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, 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
@@ -456,7 +472,9 @@ a human cannot share a part or view they lack.
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
subject exactly `{kind:"human"|"agent",id}`; agent id is the business's
bus agent **instance ID**, equal to principal.agentInstance, never a role
definition ID or a seat display name. scope is 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;
@@ -480,11 +498,12 @@ 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.
Each access decision records an authority vector exactly
`{humanAccess,agentAccess,projectAccess,threadAccess,sharingPolicy,share}`.
`{restoreEpoch,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.
restoreEpoch is the independent restore-fence epoch in section 7.5.
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
@@ -563,7 +582,7 @@ 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,
`{version,id,restoreEpoch,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
@@ -571,7 +590,8 @@ 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
restoreEpoch must equal authority.restoreEpoch and the live independent
fence. 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,
@@ -607,8 +627,12 @@ C2-06 processing. An O2 flag is metadata stored by W5, not a story edit.
| 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.
holds the immutable dedicated message/share objects defined below, transient
O2 adapter material and any unavoidable encrypted native transcript. Object
files are regular 0600 owner files, no symlink/hardlink aliases. This is an
O2-classified content store; it is never included in the bus release backup,
post-attempt store, stack-copy, rehearse input or generic backup. Revocation
changes permission at read time, never rewrites a write-once run record. 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
@@ -626,8 +650,10 @@ 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.
artifacts only, never writing/, writing-context/, writing-control/, O2 content
objects, exports or repository object directories. The separate release bus
backup retains metadata rows only, under the private-store rules below;
it is not a source copy or permission to read context. 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
@@ -637,7 +663,9 @@ expose it to generic tools. Native engine APIs remain human-only.
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.
unless stated). Each row also has restore_epoch TEXT NOT NULL, equal to the
independent current fence epoch at write time. Every update/delete/replace
refuses; latest seq **within that epoch** is state.
JSON columns contain the exact canonical schema named above, not arbitrary
unvalidated JSON. No existing C2 messages/events/sessions column changes.
@@ -649,14 +677,25 @@ unvalidated JSON. No existing C2 messages/events/sessions column changes.
| 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) |
| writing_messages | conversation TEXT FK, execution_session TEXT FK nullable for human-only messages, from_kind TEXT (human/agent), from_id TEXT, content_ref TEXT, content_digest TEXT, operation_id TEXT UNIQUE |
| writing_shares | conversation TEXT FK, message TEXT FK, document TEXT, payload_ref TEXT, payload_digest 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
application checks require agreement, not just ID existence. The canonical message content object is exactly `{body,provenance}`: body is
the complete human/agent message string; provenance is an array of `{document,share,commit,blob,extractor}`,
sorted by document/share; every entry requires live scopes. The share content
object is exactly the full section 7.3 payload, including any quoted selection.
Neither message bodies nor share payloads/quotes enter bus.sqlite, WAL,
events, SQL error logs or backup manifests. Human messages with no O2
attachment have provenance=[] but use the same private content store.
content_ref/payload_ref are opaque 64-hex object names, not paths; their
corresponding digest equals SHA-256 of the exact canonical UTF-8 object.
Objects reside at writing-context/objects/<digest>.json, with no remote or
release-backup copy. Write/flush the immutable object before committing its
bus reference. A failed transaction leaves only an inaccessible orphan,
never a visible partial message/share; a missing or mismatched object refuses.
Object reads always require current authority, not knowledge of a digest.
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
@@ -677,10 +716,90 @@ 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,
A rollback probe uses recognizable secret fixture text in O2-only content objects,
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.
#### Release backup, restore counting and independent revocation fence
The existing release behavior must be explicitly accounted for: at this base
`scripts/stack-release.sh:474-493` copies bus.sqlite into
`<dataRoot>/backups/stack/<ACT>/bus/`; restore_store at :496ff moves current
bus/ to `<dataRoot>/bus.post-attempt-<stamp><suffix>/`. rehearse reads backup
stores, and release never prunes them. These paths remain **O2-classified
metadata stores** even though content/quotes are outside SQLite. Directories
are 0700 owner and files (including SQLite/WAL/SHM if present) 0600 owner,
with ownership, symlink/hardlink and generic mount checks. No remote, mirror,
seat token access or generic export. rehearse may read metadata for validation
only and cannot load content objects, emit quotes or restore readiness.
Release does not silently delete evidence. A separate explicit human/ops
retention action may prune a stopped backup/post-attempt tree only after
checking it is not the selected recovery set or needed restore evidence,
recording its IDs/digests and a prune receipt. No automatic age-based prune,
no editing of retained metadata, no pruning runs/ under this policy. Until
that separately reviewed action exists, keep these private local metadata
trees indefinitely; it grants no content access. writing-context objects
have a distinct reviewed retention process and are never backed up by release.
W4b owns the release guard changes, not a follow-up: all eight writing_*
tables above join COUNTED in scripts/stack-release.sh:420 and the T46 table
classification in packages/cli/tests/stack-release.test.mjs:913. They are
non-event writers, not classified as ignorable tables or double-counted as
generic events. Jason's existing exact --events N confirmation remains
required when the combined post-backup row count is nonzero. A restore plan
also compares writing row identities with the proposed source store; equal
aggregate counts cannot conceal a dropped revocation replaced by another row.
A schema with any unclassified writing_* table refuses restore readiness.
An **independent host-private fence** lives outside bus/ and every backup:
`<dataRoot>/writing-control/restore-fence.json`, directory 0700, regular file
0600, no links/mounts/remote/source-copy/rehearse access. Its exact schema is
`{version:1,epoch,restoreOperation,droppedRowsDigest,confirmedBusinesses}`.
epoch and restoreOperation are host-generated opaque IDs; droppedRowsDigest
is SHA-256 of the canonical sorted dropped `(table,id)` list (empty allowed);
confirmedBusinesses is a sorted unique array of authenticated business IDs.
Only the trusted release controller and existing owner-proof handler write
it by durable atomic replacement; agents/testers/request paths cannot.
It is not a human-editable readiness setting: verified release readiness is
still required, and this file is an additional independent deny fence.
Before **any** store swap/restore (not just one with nonzero counts), the
current compatible controller durably creates a fresh epoch, records dropped
writing rows and clears confirmedBusinesses. If fencing fails, restore
refuses before changing bus/. A crash anywhere keeps O2 denied. Old snapshot
rows have old restore_epoch; they cannot authorize even if their grant,
policy or share says enabled. Automatic failure recovery and rehearse-driven
restore use the same fence. Normal code rollback without store restore
keeps the fence and leaves O2 off on incompatible code. A pre-W4b controller
is not allowed to restore an O2-enabled data root: the active controller's
capability preflight refuses it; operations must use the compatible restore
controller even when the target runtime is old.
After restore, the verified W4b runtime remains unavailable for O2 in every
unconfirmed business. New human-only
`editor.restore.confirm({expectedEpoch})`, under the business owner's existing
trusted human proof, acknowledges that business in the independent fence
and returns `{ok:true,result:{business,epoch,confirmed:true}}`. It restores
**no old grant/share/policy/conversation**. Fresh current-epoch grants,
a fresh dedicated conversation/default policy and fresh human chips must
be created explicitly. Past rows may be inspected by an authorized recovery
operator as metadata, but never used as current authority or prompt replay.
Every read/resolve/cursor/share checks both business confirmation and matching
restore_epoch; old object bytes remaining on disk are not capabilities.
Thus even owner confirmation cannot revive a previously revoked old share.
Missing/malformed/rolled-back fence state denies O2; --events N approves a
store operation only and is never a substitute for this recovery protocol.
Acceptance seeds a secret in a content object and a share, takes a backup,
revokes access/sharing, then restores the backup with explicit row-loss
approval: the old share stays refused before and after owner confirmation;
a new grant/conversation/chip is needed. Sweep bus.sqlite, WAL/SHM, every
backups/stack and bus.post-attempt-* tree, rehearse output and release logs:
no secret/title/quote/body marker may appear. Only the explicitly named
private writing-context content object may contain it. Verify COUNTED/T46
covers each table and simulate failure before/after each fence/store step.
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
@@ -736,7 +855,8 @@ no fixture-only readiness flag may exist in production APIs.
| 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 |
| A31 | Closed leaf/revision table | every leaf/nested rule; mark-only deletion keeps text in final, mark-only insertion keeps text in original; markup mark spans zero-width; reserved note leaves validated but never projected |
| A32 | Revocation after backup, then restore | all writing_* counted/classified; old share refuses before/after owner confirmation; fence survives crash and no secret marker in backup/post-attempt/rehearse paths |
Implementation acceptance includes writing, bus, harness, business, WebUI,
seat mount/import closure and stack-copy/release fixtures; explicit production
@@ -771,3 +891,17 @@ 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.
## Round 3 changes against review 0d627a2b3
R2-B1: paragraph-mark revisions preserve all run text; markup has a zero-width
mark span, never a fabricated whole-paragraph deletion. Reserved note leaves
are allowed only in reserved stories; whole-part structural validation is
explicit. A31 covers both mark-only cases and reserved notes.
R2-B2: bodies/quoted share payloads move out of SQLite into private content
objects; backup/post-attempt metadata paths are named with access/retention
rules. Every writing_* table is COUNTED/T46-classified. A non-restored deny
fence changes epoch before any store swap; owner confirmation cannot revive
old-epoch grants/shares. A32 tests revocation/restore and secret-marker sweeps.
R2-N1: agent grant subject means agentInstance. R2-N2: structural leaves have
one table. This remains design only; no code, restore, parser run or test.