feat(ce-prototype): add live annotation wait loop - #1623
Conversation
Isolated web prototype previews can pin a comment on a live element and wake the agent through a blocking helper wait, then morph the current screen in place. Default helper start stays event-inert so brainstorm visual probes stay display-only. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
innerHTML replacement left interactive screens inert after the first revise. Re-insert script tags so the judged page returns to its initial bindings, and pin that KTD8 queued extras stay until the next wait. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 909af56f1c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Stale comment
Platform pattern check: morph
innerHTML+ script re-execVerdict: OMIT (not a medium+ security/privacy finding)
Verified against PR head
909af56f:
- Unauthenticated callers cannot attach to
/eventsor push morph HTML into another browser (requireAnnotateToken→ 401; tested).- Morph payload is
newestScreenInnerHtml()from localscreens/files;POST /annotationonly queues JSON{comment,selector,textSnippet,rect}— it does not write HTML.- Script clone/re-exec after
root.innerHTML = data.htmlrestores body-script interactivity that initialinjectAnnotate/ full reload already ran from the same agent-authored screens.- No CSP / Trusted Types (defense-in-depth gap only; not an exploitable unauthenticated sink here).
No inline comments for this pattern.
Sent by Cursor Security Agent: Security Reviewer
Morph SSE now carries newest-screen head HTML so CSS and linked styles update with the body. Overlay paths stop at the prototype root, Stop only ends after a 2xx, and pending pins stay queued across morphs. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f3e73fa47
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
… the screen Serve-time overlay no longer wraps body children, so full-document selectors and layout stay intact. Morph replaces body nodes except the overlay, reapplies head resources including the initially served set, re-runs screen scripts in a fresh block scope, and advances one queued pin per revision. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Annotate-mode auth map (read-only, HEAD)
Scope:
skills/ce-prototype/scripts/light-webserver.js(byte-identical toskills/ce-brainstorm/scripts/light-webserver.js). Routes below assume--annotate/options.annotate === true.Token-gated vs open
# Route Token required? Citation 1 GET /Yes requireAnnotateTokenbefore render:722:727:skills/ce-prototype/scripts/light-webserver.js2 GET /versionNo Handled before the annotate block with no token check: 616:622:skills/ce-prototype/scripts/light-webserver.js3 GET /annotate.js,GET /annotate.cssNo Overlay map 18:20; served with norequireAnnotateToken:717:719:skills/ce-prototype/scripts/light-webserver.js. Boot inject also links them tokenless:246:2494 Other GETviasafeFileResponse(options.screensDir, …)No Fallthrough after annotate-specific routes: 737:740:skills/ce-prototype/scripts/light-webserver.js5 POST /annotation,GET /wait,GET /events,POST /session/endYes (each) 655:656,626:627,687:688,680:681respectivelyToken sources for gated routes: query
token, headerx-session-token, orAuthorization: Bearer …(322:330). Session secret israndomUUID()when annotate is on (539).6. Pre-existing screensDir static serving?
Present before this PR. On
origin/main, any non-// non-/versionGETalready calledsafeFileResponse(options, …)which resolved underoptions.screensDir(main~394:396, def ~246/260). This PR keeps that unauthenticated fallthrough (737:740). What is new under annotate is the unauthenticated overlay asset routes (717:719), not screensDir file serving itself.7.
requireAnnotateTokencomparisonExact string
===(607:608):if (requestToken(req) === sessionToken) return trueNot
crypto.timingSafeEqual. Token israndomUUID()(539) → UUID v4, 122 bits random.===on JS strings is not constant-time; UUID entropy does not change that property of the comparison.8. Referrer-Policy vs query token
No
Referrer-Policy/referrerpolicyanywhere in the webserver, injected wrap HTML (252:291), orskills/ce-prototype/assets/annotate.js. Query-token usage: server advertises?token=(764); client re-attachestokenon annotate API URLs (annotate.js2,145:148).Sent by Cursor Security Agent: Security Reviewer
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a7b06d6d1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
Stale comment
Platform-pattern security verification (annotate mode)
Read-only check of
skills/ce-prototype/scripts/light-webserver.js(duplicateskills/ce-brainstorm/scripts/light-webserver.js) andskills/ce-prototype/assets/annotate.jsfor items A–E. No medium+ findings.{ "A": { "verdict": "DISCARD", "severity": null, "evidence_lines": [ "light-webserver.js:11 DEFAULT_HOST=127.0.0.1", "light-webserver.js:18-20 OVERLAY_FILES (/annotate.js,/annotate.css)", "light-webserver.js:159-169 containedRealPath", "light-webserver.js:188-194 /version payload = basename+mtimeMs only", "light-webserver.js:616-622 GET /version ungated (pre-existing on main)", "light-webserver.js:717-719 overlay served without token", "light-webserver.js:722-723 GET / token-gated in annotate mode", "light-webserver.js:737-739 nested screens via safeFileResponse (pre-existing local file serve)" ], "attack_path": "Unauthenticated client hits /version, /annotate.js|/annotate.css, or /<screen-asset> while / requires ?token=. Overlay is static chrome (no secrets). Screen files remain the same localhost prototype tree already served without auth on main; relative assets must stay reachable after gating /. Not a new cross-tenant IDOR.", "rationale": "Token gate was added for annotate control plane + document shell; ungated paths are intentional local static serving / polling, not a new auth gap over secrets." }, "B": { "verdict": "DISCARD", "severity": null, "evidence_lines": [ "light-webserver.js:589-597 broadcastMorph from newestScreenInnerHtml/HeadHtml", "light-webserver.js:687-688 GET /events requires requireAnnotateToken", "annotate.js:61-68 activateScripts", "annotate.js:81,97,102 innerHTML sinks in applyHead/applyScreenHtml", "annotate.js:308-318 EventSource(tokenUrl('/events')) then applyScreenHtml/applyHead" ], "attack_path": "Unauthenticated attacker cannot open /events (401). Morph HTML/scripts only reach browsers that already hold the session token; content is agent-written screen HTML under screens/ — intended prototype execution, not an open HTML/script sink.", "rationale": "Sink exists but is capability-gated and matches product intent for live prototype morph." }, "C": { "verdict": "DISCARD", "severity": null, "evidence_lines": [ "light-webserver.js:539 sessionToken = randomUUID()", "light-webserver.js:607-608 requireAnnotateToken uses requestToken(req) === sessionToken" ], "attack_path": "Remote timing attack on === vs timingSafeEqual against a random UUID over localhost HTTP.", "rationale": "Not a realistic medium+ exploit path for an ephemeral local UUID token on 127.0.0.1; no network multi-tenant verifier." }, "D": { "verdict": "DISCARD", "severity": null, "evidence_lines": [ "light-webserver.js:79 infoFile = state/display-info.json", "light-webserver.js:764,769 token embedded in url/info object", "light-webserver.js:772 writeFileSync(infoFile)", "light-webserver.js:773 console.log(JSON.stringify(info))" ], "attack_path": "Local process stdout / run-state JSON exposes annotate token to the same OS user running the agent.", "rationale": "Designed local IPC for wait/agent discovery of the tokenized URL, not sensitive cloud debug logging of user secrets. Same principal already owns the server process." }, "E": { "verdict": "DISCARD", "severity": null, "evidence_lines": [ "light-webserver.js:755 listen(http) on options.host default 127.0.0.1", "no TLS/rejectUnauthorized/CSP/auth-context APIs in these files", "authorization is only sessionToken equality for annotate routes" ], "attack_path": "none identified", "rationale": "No TLS bypass, CSP exception, auth-context mutation, or unvalidated identity used for authorization." } }Bottom line: Annotate mode adds a local session token around
/,/wait,/annotation,/session/end, and/events. Remaining ungated GETs are pre-existing localhost static/version behavior plus secret-free overlay assets. HTML/script morph is token-gated and intentional.Sent by Cursor Security Agent: Security Reviewer
There was a problem hiding this comment.
Stale comment
Security verification — annotate overlay (HEAD
9a7b06d6)Read-only review of
skills/ce-prototype/assets/annotate.jsandskills/ce-prototype/scripts/light-webserver.js. Verdict on the critical morph/annotation question: annotation POST bodies do not reachinnerHTML/activateScriptswithout an agent rewriting screen files on disk.1. HTML / script sinks
annotate.js— present
Site Kind Input L12–16 chrome.innerHTMLStatic UI literal Fixed markup L26–33 composer.innerHTMLStatic UI literal Fixed markup L81 tmp.innerHTML = headHtmlDynamic Morph SSE data.headviaapplyHead(L71–91)L97 root.innerHTML = htmlDynamic Morph SSE data.htmlviaapplyScreenHtml(L94–98)L102 tmp.innerHTML = htmlDynamic Same screen-body path when no #ce-prototype-root(L101–112)L61–68 activateScriptsScript recreation Scripts discovered under the HTML trees above; L66 copies textContentinto a new<script>Absent in both files:
insertAdjacentHTML,document.write,eval,new Function.
light-webserver.js: no DOM sinks. Server-side string HTML assembly only (wrapFragmentL267,wrapAnnotateFragmentL288).Pin comments use
marker.title = pin.comment(L177) — DOM property, not an HTML parser sink.2. Morph SSE vs annotation POST
Morph source (disk only):
function broadcastMorph() { const payload = JSON.stringify({ html: newestScreenInnerHtml(options), head: newestScreenHeadHtml(options), }) // ... client.write(`event: morph data: ${payload} `)
newestScreenInnerHtml/newestScreenHeadHtml(L201–218)readFileSyncthe newestscreens/*.htmland extract body/head.Client apply (L317–318):
applyScreenHtml(data.html)→innerHTML+activateScripts;applyHead(data.head)→ same for head.Annotation POST path:
- L655–676: token gate →
parseAnnotation(raw)→annotationQueue.push(record)→fulfillWaiters()- Does not call
broadcastMorph/ write screen files- Morph poll is separate: L786–787
setInterval→maybeBroadcastMorph()on screen version change
parseAnnotation(L356–376) keepscomment,selector,textSnippet,rectas JSON for/waitconsumers only.Conclusion: morph HTML is local agent-authored (or otherwise on-disk) screen HTML. Annotation POST cannot feed those sinks unless something outside this helper rewrites the screen files the agent is meant to edit.
3.
wrapAnnotateFragment${content}function wrapAnnotateFragment(content) { return `<!doctype html> ... <main>${content}</main>Callers (L313, L317): static waiting markup, or full screen file contents from
fs.readFileSyncwhen the screen is a fragment. Initial document render only — not the morph SSE path. Trust model matches agent-written screen HTML.4. TLS / CSP / Trusted Types
http.createServeronly (L746); listen onoptions.host(L755); defaultDEFAULT_HOST = "127.0.0.1"(L11). No TLS stack, so no TLS verification bypass.- No
Content-Security-Policyon anywriteHead.- No Trusted Types API usage.
5. Debug / token logging
- L764, L769, L772–773: session
tokenembedded inurl, written toinfoFile, andconsole.log(JSON.stringify(info)).- L513: wait CLI builds a tokenized URL (used for
fetch, not a comment log).- Annotation
comment/ body fields are not logged by these two files.- Paths (
root,screen_dir,state_dir) appear in the same info object.6. Auth context
- L539:
sessionToken = randomUUID()when annotate is on.- L322–330
requestToken: querytoken,x-session-token, orAuthorization: Bearer ….- L607–608
requireAnnotateToken: exact equality tosessionToken— no identity object, no role/user mutation, no unvalidated principal accessors beyond “holds the session UUID.”
Residual (informational, local-prototype trust model): screen HTML is intentionally executable in the browser (
innerHTML+activateScripts). That is XSS-shaped if a non-agent party can writescreens/or if--hostis moved off loopback while the query token is shared. Within the stated agent-on-loopback design, annotation POST alone does not open that path.Sent by Cursor Security Agent: Security Reviewer
Helper: first SSE connect no longer replays the rendered screen (which re-ran prototype scripts on load), idle/owner shutdown ends the session and closes open streams instead of hanging, token compare is timing-safe, and annotate-mode GET / sends Referrer-Policy: no-referrer so the URL token cannot leak via Referer. Overlay: pins follow scroll/resize and place from the target rect, reattach no longer overwrites working/pending state, head applies before body, only classic scripts are block-wrapped on re-run (modules and data scripts stay verbatim), and a closed stream marks the session ended. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad6b9e2ea7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…d of re-running scripts Morph in place only when the revised screen needs no fresh script context; any non-overlay script or base element persists the pins to sessionStorage and reloads the document, so top-level bindings, shared inline scripts, and head state all come from a fresh realm. The morph payload is now the same rendered document the page serves, so fragment shells and body attributes survive an update. Authenticated GET / sets a port-scoped HttpOnly cookie so prototype navigation like href="/?variant=a" stays authorized, and a page load during the stream grace restarts it. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d401be8f25
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
Stale comment
Security verification: annotate-mode HTTP auth
Verdict: NOT_EXPLOITABLE
Checked whether new annotate routes allow unauthenticated access to enqueue annotations, end the session, receive morph HTML, or obtain the session token.
Sensitive routes — token required
requireAnnotateTokengates/wait,/annotation,/session/end,/events, and annotate-modeGET /. Failed auth returns 401 and returns (no fall-through to the later openGET /).Token check uses query /
x-session-token/ Bearer / cookie, with timing-safe compare. Cookie is set only after a successful authenticatedGET /(HttpOnly; SameSite=Strict).cookieNamestartsnulland is assigned in the listen callback; that does not create an unauthenticated bypass.Intentional unauthenticated surfaces — not the sensitive ops
Route Auth Why OK for this question GET /versionnone version metadata only GET /annotate.js,/annotate.cssnone static overlay; client reads token from URL other GETscreen filesnone prototype assets, not morph SSE / queue / session end / token Default bind is
127.0.0.1.No fix proposed — no unauthenticated path to the listed sensitive operations.
Sent by Cursor Security Agent: Security Reviewer
…in place The in-place morph kept accreting reconciliation cases (body and html attributes, head equality, linked-asset caching) and its reason for existing, keeping pins that a reload would drop, no longer applies now that pins survive a reload. The stream now emits screen-changed and the client closes the stream, persists pins and tool state, and reloads. The change key covers every file under screens/ so an asset-only edit is delivered, and annotate-mode responses are Cache-Control: no-store so the reload fetches revised CSS and JS. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b2806325b8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…ep drafts across reloads The helper now tracks each annotation as queued, working, or done (queued on POST, working when wait serves it, done when the agent next enters wait or the session ends) and broadcasts that map over the stream, including once on connect. The overlay mirrors it instead of advancing pins on every reload, so an edit spanning several file writes no longer completes queued pins early. An open composer draft is persisted with the pins, and a screen change that arrives mid-POST waits for the request to settle before reloading. Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Security verification result
exploit_possible: true (within the stated same-machine, malicious-localhost-origin scenario)severity: omitfalse_positive_risk: low for the browser mechanics; high if treated as a production-facing vulnerabilityaffected_changed_lines:
skills/ce-prototype/scripts/light-webserver.js: 349-358, 391-412, 675-679, 702-790, 798-817, 852-859skills/ce-brainstorm/scripts/light-webserver.js: same lines (the two files are byte-identical at the head SHA)
GET /?token=…is authenticated throughrequestTokenand then setsce-light-web-${port}=<sessionToken>; HttpOnly; SameSite=Strict; Path=/. The cookie name contains the port, but cookie matching does not: a host-onlylocalhostcookie is sent to all ports on that host.requestTokenaccepts that cookie (lines 349-358), and every listed annotation route relies only onrequireAnnotateToken(675-679), with noOriginorReferervalidation.A page served from
http://localhost:<other-port>is cross-origin but same-site withhttp://localhost:<preview-port>. It can issue a credentialedfetch(..., { mode: "no-cors", credentials: "include" }); aPOSTwithContent-Type: text/plainis CORS-simple, so no preflight is required. The server reads and JSON-parses the raw body without checking its content type (391-412, 732-755), allowing a valid annotation to enter the queue. CORS only prevents the attacker from reading the response; it does not prevent the request or cookie from being sent. The same mechanism can issueGET /waitto consume an annotation (702-729),POST /session/endto end the session (759-763), or open/events(766-790).127.0.0.1binding blocks network-origin access to the listener, but does not prevent a browser running on the local machine from making loopback requests.This is a real new ambient-authority/localhost-CSRF behavior compared with token-only routes, but the demonstrated attacker must already control a local web origin on the developer's machine. It affects an ephemeral, loopback-only developer preview and can inject into or stop that local agent session; it neither exposes a production service nor crosses the local-machine boundary. I therefore recommend omitting a security finding at the requested severity threshold.
Sent by Cursor Security Agent: Security Reviewer
There was a problem hiding this comment.
Stale comment
Security verification: no claimed MEDIUM+ platform-pattern issue is introduced by this PR.
Claim exploit_possible false_positive_risk affected lines Rationale 1. Overlay assets unauthenticated No material exploit High skills/ce-prototype/scripts/light-webserver.js:792-795GET /annotate.jsand/annotate.cssdo bypassrequireAnnotateToken, but they serve fixed, public overlay assets and disclose neither the per-run token nor annotation data. The overlay obtains the token only from an already-authorized page URL. This is not a MEDIUM+ exposure.2. screens/files unauthenticated while/is gatedNo newly introduced exploit High skills/ce-prototype/scripts/light-webserver.js:798-819,828-833/is token-gated in annotate mode, while other GET paths continue to serve files fromscreens/. The latter behavior predates this PR (the base helper served all GET paths fromscreens/), so it is not introduced here. It remains relevant only to the existing, explicit remote-host trust boundary.3. Token persisted and logged No material exploit High skills/ce-prototype/scripts/light-webserver.js:854-868The token is deliberately included in display-info.jsonand the startup JSON needed by the localwaitCLI. This is local run-state persistence rather than a new external disclosure; it carries no platform credential or secret beyond the short-lived local annotation capability.4. innerHTML/querySelectorXSSNo High skills/ce-prototype/assets/annotate.js:11-16,24-33,99-124,187-197,210-214Both innerHTMLuses are static literals. User-derived comments usetextContentortitle; they do not reach an HTML sink. A stored selector reachesquerySelector, which parses CSS selectors rather than JavaScript and is guarded bytry/catch; no execution path was found.5. TLS, identity validation, or CSP/Trusted Types bypass No High skills/ce-prototype/scripts/light-webserver.js:674-678,850-865;skills/ce-prototype/assets/annotate.js:1,160-165This is an HTTP local-preview helper bound to 127.0.0.1by default; there is no TLS client or certificate-validation path to bypass. The explicit remote-host mode is documented as a trusted-network choice, and the token comparison is constant-time. No CSP or Trusted Types policy is relaxed or bypassed by this change; their absence is not an introduced MEDIUM+ issue for this local prototype surface.I also compared the pre-PR helper: unauthenticated
screens/serving existed before this change. The PR newly gates the annotation mutation/wait/SSE routes and/in annotate mode.Sent by Cursor Security Agent: Security Reviewer
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0e8e60296
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
Stale comment
XSS/CSP/Trusted Types verification (annotate overlay)
Independent read-only check of PR head overlay + inject paths.
{ "exploitable": false, "severity": "omit", "rationale": "chrome.innerHTML/composer.innerHTML use only static UI markup (no comment/selector interpolation). pin.comment is assigned only to marker.title and restored via textarea.value — neither parses HTML. Status/error/pin labels use textContent. querySelector(pin.selector) from cssPath/sessionStorage cannot execute script. POST /annotation queues JSON for the agent wait path; SSE annotations events carry only id→state maps (no comment text) and screen-changed triggers reload, not morph HTML. injectAnnotate injects a fixed boot fragment; wrapAnnotateFragment interpolates WAITING_HTML or on-disk screen HTML (agent-authored prototype pages served as text/html — pre-existing intentional surface, not fed by annotation POST). No Content-Security-Policy headers and no Trusted Types policies are added or relaxed.", "changed_lines_if_finding": "" }Verified against
skills/ce-prototype/assets/annotate.jsand byte-identicallight-webserver.jscopies (injectAnnotate/wrapAnnotateFragment). Comment text is not reflected into an HTML/script sink in the browser.Sent by Cursor Security Agent: Security Reviewer


Isolated web
ce-prototypepreviews can now pin a comment on a live element and have the agent revise that screen in place, using a napkin-style wait loop on the existing local helper.What changed
light-webserver.js(both skill copies, still byte-identical) gains opt-in--annotate: per-run token,GET /wait,POST /annotation,POST /session/end, and an SSE stream carryingscreen-changed,annotations, andsession-endedevents.start --annotateon a root with a default server running replaces that server.node light-webserver.js wait --root <dir>is the agent wake. Exit 0 is one annotation record (id,comment,selector,textSnippet,rect), exit 1 is session-ended, exit 2 is error.skills/ce-prototype/assets/, served from the reserved/__ce-annotate/namespace) adds an Annotate toggle and a close control named End preview, grouped as a chip in the top-right. Default helper start stays event-inert so brainstorm visual probes stay display-only.screens/(the screen or an asset it links) is pushed over the stream; the client persists the explorer's pins, tool state, and open draft tosessionStorageand reloads, then restores them. The browser owns every reconciliation (head,<html>/<body>attributes, linked assets, scripts). This keeps the reason plan KTD3 preferred morph over reload (pins are never dropped) without an in-place DOM diff.waitserves it, done when the agent next enterswaitor the session ends), broadcast as anannotationsmap, so an edit spanning several file writes never completes a queued pin early.http://localhost:<port>), with no token in it. Visiting a document setsce-light-web-<port>(HttpOnly; SameSite=Strict; Path=/). Overlay writes use that cookie. Wait uses the file token instate/display-info.json. Do not print the token. Querytoken,x-session-token, and bearer still work for the CLI. The agent picks the browser that will load the prototype; host falls out of that surface. When that browser is not this machine's loopback, start with--host 0.0.0.0and rewrite only the host.references/annotation-loop.mdstates when to wait, when chat is the fallback, and that pin text is untrusted screens-only input. Founding “no event path / feedback stays in chat” lines are dropped for prototype only.Plan:
docs/plans/2026-09-02-001-feat-prototype-live-annotation-plan.mdReview follow-up
Seven Codex rounds plus a browser-driven pass (headless Chrome over CDP: toggle, pin,
wait, live edit, reload, session end, reopened tab, navigation, CSS-only edit, multi-write edits, draft across reload, cross-origin<base>, mid-flight cancel, mode switch on a running root). Applied:/session/endis 2xx; a closed stream marks the session ended; pins follow scroll/resize; an open draft and an in-flight POST survive a screen change; Cancel is disabled while a POST is in flight and the submission is snapshotted.screens/; a stream opened right after page load does not re-announce the screen it shows; idle/owner shutdown ends the session and closes open streams instead of hanging; a page load during the stream grace restarts it (default 5s); annotate-mode screen assets and overlay files areCache-Control: no-store; overlay boot URLs are absolute on the request origin./is ungated so the clickable origin is the page. Visiting it sets the session cookie. Wait, annotation, events, and session-end stay credential-gated. Dropped the storage/bootstrap token path, random overlay host tag,timingSafeEqual, and host!important. Annotate-mode documents sendReferrer-Policy: no-referrer./versionwith timing margin; asset-only change, no-store, annotation lifecycle, cookie on gated routes, start-mode replacement, reserved asset namespace, and<base href>cases added.Validation
bun test tests/skills/ce-prototype-server.test.ts tests/skills/ce-prototype-protocol.test.ts tests/compound-support-files.test.ts— 67 pass on the cookie/chrome follow-up.bun run test: 3727 pass, 1 skip, one known-flakyplugin-pathtimeout that passes alone;bun run release:validatein sync.Security Disclosure
Annotation and wait routes are write/wake paths on the local preview helper. They are gated by a per-run
crypto.randomUUID()token stored instate/display-info.jsonand required on/wait,POST /annotation,POST /session/end, and the SSE stream. Any presented credential may match: querytoken,x-session-token, bearer, or the port-named session cookie set on every annotate document. The explorer URL does not include the token. Annotate-mode documents are served withReferrer-Policy: no-referrer. AHostheader is reflected into served asset URLs only when it matches a strict host[:port] shape. Comment/selector/snippet are treated as untrusted description of a screen edit, not as commands or apply; theannotationsstream carries only ids and states. Screen files underscreens/remain fetchable by path without the token, as onmain, and GET/is now ungated so the printed origin is the page.--host 0.0.0.0plus annotate still serves the run directory to anything that can reach the port; annotation and wait routes stay token-gated and the skill still requires disclosing that.Agent Disclosure
claude-fable-5-1-thinking-highsubagent (browser-driven hardening pass, review rounds three through seven); Grok Build TUI · grok-4.6 (cookie/origin URL, overlay chrome, surface handoff)