oficina / docs Public

plan7-r3: Plan 7 catches up with the word-processor basics open

On branch worktree-plan7-r3

Doc-only. Plan 7 sat on main while 250db1c landed the word-processor basics, and four things it describes moved. Nothing is re-decided — three tasks got smaller and one got a precise address.

Verified against origin/main rather than relayed from the session that did the work.

MARKS grew and D1 is still unspent. Eleven point-size marks plus the four named sizes they replaced, all still attribute-free spans with literal classes. So link remains the first valued mark. That was a live risk while that work was in flight: a size mark carrying "14pt" would have spent the property fragment_html's unescaped output rests on — the MARKS doc comment names fontFamily as its own example — for a feature nobody would have filed as a security change. The closed enumeration is what makes it additive.

node_json was restructured to collect attributes into one object instead of writing a complete ,"attrs":{…} from independent per-attribute arms. That is now the shape a valued mark must copy, and the reason is worth reading before touching the JSON path: the old shape would have emitted "attrs" twice in one object the moment a node carried two, which every JSON parser accepts, none agrees on, and nothing would report.

D3 gains the address revision 2 could not give. mark_names (fragment.rs:520) returns names; text_json emits each as a bare {"type":…}. That function is the single point where the JSON consumers and the HTML renderer agree on what marks a run carries, so that is where the scheme allowlist belongs — which is what makes "the check runs once, in the shared path" true rather than aspirational. If it lived in node_html instead, markdown.go would emit [text](javascript:…) and the PDF's /Link annotation would carry the same.

Also recorded: mark_names carries unknown marks through by name rather than dropping them. Not a hole today — escape_json handles them and they contribute no HTML wrapper — but a valued mark makes the question live, so the allowlist must reject an unknown mark's value rather than assume only link has one.

Tasks 4 and 5 shrink. document.go's Attrs is already widened (Indent beside Level and Align), so only Mark is left. The fixture generator now runs the real encoding path and already carries cases for values no editor can produce but a raw POST can — the exact shape the adversarial URLs need. And the Insert menu ships Link, Image and Table present-but-disabled, reserving insert-link, insert-image and insert-table, so implementing against those names needs no menu change.

Still with Paul

  • The sealing key. Nothing in the app calls SealSym; blobs cannot be built without it. Recommendation on #23: a separate $DOCS_BLOB_KEY with InstanceKey's fail-closed shape, one key instance-wide, and a spec line moving blobs to the per-document DEK when §2 lands.
  • Who builds links. Not built — the word-processor session deliberately left it, on the grounds that it is the one item where half-done is a vulnerability rather than a missing feature. Agreed.
  • The D6 fetch test, which needs one Google Doc containing an image and decides the shape of the image work.

The browser-drive header was wrong, and it was where I got it

5c9a2ac on this branch, rebased onto 68273a4.

scripts/browser-drive.sh's header said Chromium cannot start inside the agent sandbox. That is true and incomplete in the way that matters: it never said the sandbox can be turned off. Verified here both ways — inside, Chromium dies before opening a page; outside, --dump-dom about:blank returns and exits 0.

The header was also stale on the symptom. It quotes a refused process-singleton socket ("socket() failed: Operation not permitted"); today's failure is "Failed to create a unique user data directory for headless". Quoting one symptom as though it were the diagnosis is part of why the paragraph read as final.

I am the one who relayed it. I read that header, believed it, and told two other sessions "Chromium cannot start in this sandbox" as though I had checked. I had not. Three sessions then repeated it across a commit message, a discussion, a handoff and two cross-session messages before anybody tried it — and one shipped a merge that took the drive from 44/44 to 39/44, including a real WCAG SC 1.4.10 failure at 320px, precisely because it believed it could not check.

The paragraph now states the limitation as "inside the sandbox" and nothing more, in a sentence that cannot be skimmed, with that history as the reason it exists.

And I ran it rather than asserting it, since "this script works" in a comment about not trusting unverified claims would have been the same mistake in a new hat:

44/44 checks passed [exited with code 0]

That is 68273a4 plus this branch's doc and comment changes, from an independent session — so it also second-sources the word-processor session's own fix.

What this adds to Plan 7

A browser drive before merge is now in D3 and in the risk table rather than left to judgement. Its UI is the exact shape a green gate cannot see: a disabled menu item that must become enabled, a NodeView that shows a preview and swaps it, an upload that can fail in front of a person.

With the trap that comes with it: enabling insert-link and insert-image gives each command a second home, and menu items precede the toolbar in document order — so a bare [data-docs-command="insert-link"] selects the menu item, which sits inside a closed <details> with a zero-size rect, and every click lands in the page corner. Drive selectors scope to [docs-toolbar] … or to the dialog's own root.

Not restating what is now in the code

ca9641b put the mark_names allowlist warning into fragment.rs itself, and it is better there than in a plan — it is in front of whoever edits that function, which a plan is not. This revision stays complementary rather than repeating it.

Still with Paul

Unchanged: the sealing key, who builds links (the word-processor session has formally declined them, with a reason I would rather have than a silent pass), and one Google Doc containing an image for the D6 fetch test.

Rebased onto 1859534; the header fix is dropped, not merged

We both fixed scripts/browser-drive.sh's header independently and theirs landed first, as 1859534. I read it before dropping mine, and it is the better paragraph on two counts:

  • Mine quoted today's symptom and called the older one stale. Theirs names both and says explicitly that neither is the diagnosis, because quoting one symptom as though it were the reason is part of what made the old text read as final. That is the general form of the point; mine was the special case of it.
  • Mine left the CI question where the old text had it. Theirs narrows it to "may a gate step run unsandboxed" rather than "can this run here" — which is the actual open question, and is somebody's decision rather than a fact about the machine.

Reconciling two paragraphs we agree about would have been worse than either, so this branch takes theirs whole. scripts/browser-drive.sh is now byte-identical to main here, and this branch is doc-only: one file, 79 insertions.

What survives is the requirement, not the comment

c1ac560. Plan 7's UI is the exact shape a green gate cannot see — a disabled menu item that must become enabled, a NodeView that shows a preview and then swaps it, an upload that can fail in front of a person. 250db1c's merge went 44/44 to 39/44 on that drive with the gate green throughout, including a real WCAG 2.2 SC 1.4.10 failure at 320px.

So D3 and the risk table now require a browser drive before merge rather than leaving it to judgement.

With the trap that comes with it, in the specific form it was given: enabling insert-link and insert-image gives each command a second home, and menu items precede the toolbar in document order — so a bare [data-docs-command="insert-link"] returns the menu item, inside a closed <details>, zero-size rect, every click landing at the page corner, and the failure surfacing thirty seconds later as "timed out waiting for the link". Nothing in that message points at the selector. Scope to [docs-toolbar] … or the dialog's own root; clickSelector now refuses a zero-size target and refuses to pick the first of several matches, but only for code that goes through it.

Verified rather than asserted: the drive runs from here at 44/44, exit 0, on 68273a4 plus this branch.

The shape, five times in one night

Worth recording once, because it is the same defect wearing five hats and every instance was found by accident rather than by design:

  1. A palette check reporting 44/44 green against a deliberately broken binding, because npm had failed silently and the drive tested a stale committed bundle.
  2. A gate read through | tail, reporting tail's exit code.
  3. amadan ci status … | head, same.
  4. This file's header: a "cannot" nobody had tried, costing every check that then never ran.
  5. Two deliberate throws taking every later group() down with them, so a run reports 5/6 and thirty-eight checks are absent rather than failed — the worst of the five, because the number goes down and still looks like an ordinary failure.

One shape: a check that reported on something it never examined. The general remedy is the one the triage session used — before trusting a green run, make it fail on purpose — with the refinement that seeing it fail is not sufficient either, unless you also know the artifact under test was the one you broke.

Still with Paul

Unchanged: the sealing key; who builds links (both other sessions have now formally declined, the second with reasons worth reading); approval or objection on this branch and on #28; and one Google Doc containing an image for the D6 fetch test.