Skip to content

feat(ce-prototype): add live annotation wait loop - #1623

Open
kieranklaassen wants to merge 44 commits into
mainfrom
cursor/prototype-live-annotation-c54f
Open

feat(ce-prototype): add live annotation wait loop#1623
kieranklaassen wants to merge 44 commits into
mainfrom
cursor/prototype-live-annotation-c54f

Conversation

@kieranklaassen

@kieranklaassen kieranklaassen commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Isolated web ce-prototype previews 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

  • Shared 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 carrying screen-changed, annotations, and session-ended events. start --annotate on 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.
  • Serve-time overlay (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.
  • Update model: any change under 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 to sessionStorage and 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.
  • Pin status follows the helper's annotation lifecycle (queued on POST, working when wait serves it, done when the agent next enters wait or the session ends), broadcast as an annotations map, so an edit spanning several file writes never completes a queued pin early.
  • Auth model: the printed URL is the origin (http://localhost:<port>), with no token in it. Visiting a document sets ce-light-web-<port> (HttpOnly; SameSite=Strict; Path=/). Overlay writes use that cookie. Wait uses the file token in state/display-info.json. Do not print the token. Query token, 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.0 and rewrite only the host.
  • Protocol: references/annotation-loop.md states 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.md

Review 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:

  • End preview stays retryable until /session/end is 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.
  • The in-place morph was removed after it kept accreting reconciliation cases; reload with preserved pins replaces it.
  • Helper: change key hashes every file under 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 are Cache-Control: no-store; overlay boot URLs are absolute on the request origin.
  • Auth: GET / 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 send Referrer-Policy: no-referrer.
  • Overlay chrome: Annotate names the mode; a close icon named End preview ends the session. Grouped chip, top-right, so it does not look like a page button.
  • Tests: SSE disconnect aborts the fetch; idle-timeout liveness is checked via /version with 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

  • Targeted: 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.
  • Earlier full bun run test: 3727 pass, 1 skip, one known-flaky plugin-path timeout that passes alone; bun run release:validate in 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 in state/display-info.json and required on /wait, POST /annotation, POST /session/end, and the SSE stream. Any presented credential may match: query token, 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 with Referrer-Policy: no-referrer. A Host header 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; the annotations stream carries only ids and states. Screen files under screens/ remain fetchable by path without the token, as on main, and GET / is now ungated so the printed origin is the page. --host 0.0.0.0 plus 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

  • Model: Cursor Grok 4.6 (plan, implementation, first two review rounds); Cursor · claude-fable-5-1-thinking-high subagent (browser-driven hardening pass, review rounds three through seven); Grok Build TUI · grok-4.6 (cookie/origin URL, overlay chrome, surface handoff)
Open in Web Open in Cursor 

cursoragent and others added 2 commits September 2, 2026 17:22
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>
@kieranklaassen
kieranklaassen marked this pull request as ready for review September 2, 2026 17:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T07:11:57.396550Z e094737 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread skills/ce-prototype/scripts/light-webserver.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Platform pattern check: morph innerHTML + script re-exec

Verdict: OMIT (not a medium+ security/privacy finding)

Verified against PR head 909af56f:

  1. Unauthenticated callers cannot attach to /events or push morph HTML into another browser (requireAnnotateToken → 401; tested).
  2. Morph payload is newestScreenInnerHtml() from local screens/ files; POST /annotation only queues JSON {comment,selector,textSnippet,rect} — it does not write HTML.
  3. Script clone/re-exec after root.innerHTML = data.html restores body-script interactivity that initial injectAnnotate / full reload already ran from the same agent-authored screens.
  4. No CSP / Trusted Types (defense-in-depth gap only; not an exploitable unauthenticated sink here).

No inline comments for this pattern.

Open in Web View Automation 

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread skills/ce-prototype/scripts/light-webserver.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread tests/skills/ce-prototype-server.test.ts Outdated
… 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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Annotate-mode auth map (read-only, HEAD)

Scope: skills/ce-prototype/scripts/light-webserver.js (byte-identical to skills/ce-brainstorm/scripts/light-webserver.js). Routes below assume --annotate / options.annotate === true.

Token-gated vs open

# Route Token required? Citation
1 GET / Yes requireAnnotateToken before render: 722:727:skills/ce-prototype/scripts/light-webserver.js
2 GET /version No Handled before the annotate block with no token check: 616:622:skills/ce-prototype/scripts/light-webserver.js
3 GET /annotate.js, GET /annotate.css No Overlay map 18:20; served with no requireAnnotateToken: 717:719:skills/ce-prototype/scripts/light-webserver.js. Boot inject also links them tokenless: 246:249
4 Other GET via safeFileResponse(options.screensDir, …) No Fallthrough after annotate-specific routes: 737:740:skills/ce-prototype/scripts/light-webserver.js
5 POST /annotation, GET /wait, GET /events, POST /session/end Yes (each) 655:656, 626:627, 687:688, 680:681 respectively

Token sources for gated routes: query token, header x-session-token, or Authorization: Bearer … (322:330). Session secret is randomUUID() when annotate is on (539).

6. Pre-existing screensDir static serving?

Present before this PR. On origin/main, any non-/ / non-/version GET already called safeFileResponse(options, …) which resolved under options.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. requireAnnotateToken comparison

Exact string === (607:608):

if (requestToken(req) === sessionToken) return true

Not crypto.timingSafeEqual. Token is randomUUID() (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 / referrerpolicy anywhere in the webserver, injected wrap HTML (252:291), or skills/ce-prototype/assets/annotate.js. Query-token usage: server advertises ?token= (764); client re-attaches token on annotate API URLs (annotate.js 2, 145:148).

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/scripts/light-webserver.js
Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/scripts/light-webserver.js Outdated
Comment thread tests/skills/ce-prototype-server.test.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Platform-pattern security verification (annotate mode)

Read-only check of skills/ce-prototype/scripts/light-webserver.js (duplicate skills/ce-brainstorm/scripts/light-webserver.js) and skills/ce-prototype/assets/annotate.js for 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.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Security verification — annotate overlay (HEAD 9a7b06d6)

Read-only review of skills/ce-prototype/assets/annotate.js and skills/ce-prototype/scripts/light-webserver.js. Verdict on the critical morph/annotation question: annotation POST bodies do not reach innerHTML / activateScripts without an agent rewriting screen files on disk.

1. HTML / script sinks

annotate.js — present

Site Kind Input
L12–16 chrome.innerHTML Static UI literal Fixed markup
L26–33 composer.innerHTML Static UI literal Fixed markup
L81 tmp.innerHTML = headHtml Dynamic Morph SSE data.head via applyHead (L71–91)
L97 root.innerHTML = html Dynamic Morph SSE data.html via applyScreenHtml (L94–98)
L102 tmp.innerHTML = html Dynamic Same screen-body path when no #ce-prototype-root (L101–112)
L61–68 activateScripts Script recreation Scripts discovered under the HTML trees above; L66 copies textContent into 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 (wrapFragment L267, wrapAnnotateFragment L288).

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) readFileSync the newest screens/*.html and 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 setIntervalmaybeBroadcastMorph() on screen version change

parseAnnotation (L356–376) keeps comment, selector, textSnippet, rect as JSON for /wait consumers 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.readFileSync when 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.createServer only (L746); listen on options.host (L755); default DEFAULT_HOST = "127.0.0.1" (L11). No TLS stack, so no TLS verification bypass.
  • No Content-Security-Policy on any writeHead.
  • No Trusted Types API usage.

5. Debug / token logging

  • L764, L769, L772–773: session token embedded in url, written to infoFile, and console.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: query token, x-session-token, or Authorization: Bearer ….
  • L607–608 requireAnnotateToken: exact equality to sessionToken — 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 write screens/ or if --host is moved off loopback while the query token is shared. Within the stated agent-on-loopback design, annotation POST alone does not open that path.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/scripts/light-webserver.js
Comment thread skills/ce-prototype/scripts/light-webserver.js
Comment thread skills/ce-prototype/scripts/light-webserver.js
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

requireAnnotateToken gates /wait, /annotation, /session/end, /events, and annotate-mode GET /. Failed auth returns 401 and returns (no fall-through to the later open GET /).

Token check uses query / x-session-token / Bearer / cookie, with timing-safe compare. Cookie is set only after a successful authenticated GET / (HttpOnly; SameSite=Strict). cookieName starts null and 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 /version none version metadata only
GET /annotate.js, /annotate.css none static overlay; client reads token from URL
other GET screen files none 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.

Open in Web View Automation 

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread skills/ce-prototype/assets/annotate.js Outdated
Comment thread skills/ce-prototype/assets/annotate.js Outdated
cursoragent and others added 2 commits September 2, 2026 19:24
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Security verification result

  • exploit_possible: true (within the stated same-machine, malicious-localhost-origin scenario)
  • severity: omit
  • false_positive_risk: low for the browser mechanics; high if treated as a production-facing vulnerability
  • affected_changed_lines:
    • skills/ce-prototype/scripts/light-webserver.js: 349-358, 391-412, 675-679, 702-790, 798-817, 852-859
    • skills/ce-brainstorm/scripts/light-webserver.js: same lines (the two files are byte-identical at the head SHA)

GET /?token=… is authenticated through requestToken and then sets ce-light-web-${port}=<sessionToken>; HttpOnly; SameSite=Strict; Path=/. The cookie name contains the port, but cookie matching does not: a host-only localhost cookie is sent to all ports on that host. requestToken accepts that cookie (lines 349-358), and every listed annotation route relies only on requireAnnotateToken (675-679), with no Origin or Referer validation.

A page served from http://localhost:<other-port> is cross-origin but same-site with http://localhost:<preview-port>. It can issue a credentialed fetch(..., { mode: "no-cors", credentials: "include" }); a POST with Content-Type: text/plain is 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 issue GET /wait to consume an annotation (702-729), POST /session/end to end the session (759-763), or open /events (766-790). 127.0.0.1 binding 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.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-795 GET /annotate.js and /annotate.css do bypass requireAnnotateToken, 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 gated No 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 from screens/. The latter behavior predates this PR (the base helper served all GET paths from screens/), 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-868 The token is deliberately included in display-info.json and the startup JSON needed by the local wait CLI. 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 / querySelector XSS No High skills/ce-prototype/assets/annotate.js:11-16, 24-33, 99-124, 187-197, 210-214 Both innerHTML uses are static literals. User-derived comments use textContent or title; they do not reach an HTML sink. A stored selector reaches querySelector, which parses CSS selectors rather than JavaScript and is guarded by try/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-165 This is an HTTP local-preview helper bound to 127.0.0.1 by 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.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread skills/ce-prototype/scripts/light-webserver.js Outdated
Comment thread skills/ce-prototype/scripts/light-webserver.js

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.js and byte-identical light-webserver.js copies (injectAnnotate / wrapAnnotateFragment). Comment text is not reflected into an HTML/script sink in the browser.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer