Skip to content

fix(cockpit): keep local markdown paths inert - #975

Open
matgren wants to merge 1 commit into
mainfrom
cez/eeb6780d
Open

matgren wants to merge 1 commit into
mainfrom
cez/eeb6780d

Conversation

@matgren

@matgren matgren commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

🎯 What changes

Transcript markdown now keeps only http:// and https:// destinations active. Agent-written local paths such as /Users/.../*.md remain visible, but they no longer trigger the “Open link?” confirmation or send the SPA to a bogus localhost route after confirmation.

📋 Scope

This is scoped to the task-thread markdown renderer and the thread markdown styling for Streamdown links that lose their href. Existing browser-openable links still use the link-safety confirmation dialog, and no server/API contract changes.

🧪 Validation

  • Regression test was added and confirmed red before the fix, then green after the URL guard.
  • npm run test -w @open-mercato/cezar-web -- src/routes/task-thread/markdown.test.tsx
  • npm run build:server
  • npm run typecheck -w @open-mercato/cezar-web
  • npm run build -w @open-mercato/cezar-web

Manual browser QA has not been run in this PR; the change is user-facing click behavior, so it is marked needs-qa under the repo gate.

@matgren matgren added bug Something isn't working needs-qa Requires manual QA before merge priority-medium Ordinary bug or feature review Ready for code review risk-medium Ordinary change with tests labels Sep 12, 2026
@matgren

matgren commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

🤖 om-open-pr — 🏷️ label rationale

  • 🔍 review — The implementation is ready for code review after the targeted regression test, server build, web typecheck, and production web build passed.
  • 🐛 bug — This fixes local filesystem paths in transcript markdown being offered as browser links that navigate through localhost.
  • 🧪 needs-qa — The change affects user-facing click behavior in the task transcript and should be exercised in a browser before merge.
  • 🔹 priority-medium — This is an ordinary cockpit bug that breaks an expected link action without blocking startup or risking data loss.
  • 🟡 risk-medium — The change touches shared task-thread markdown link handling, but the diff is small and covered by a regression test.

@github-actions

Copy link
Copy Markdown

📦 npm preview published — 0.10.1-pr975.1455

Try this PR build (exact pinned version — copy-paste as-is):

npx cezar-cli@0.10.1-pr975.1455                                # cockpit at http://localhost:4321
npx cezar-cli@0.10.1-pr975.1455 run "…"                        # headless run
npx cezar-cli@0.10.1-pr975.1455 server-deploy --platform <id>  # roll a server to this exact build

Also tagged: npm install -g cezar-cli@pr-975 (moving tag for this PR).
Packages: cezar-cli@0.10.1-pr975.1455@open-mercato/cezar@0.10.1-pr975.1455@open-mercato/cezar-api-client@0.10.1-pr975.1455 (provenance attested).

@matgren
matgren requested a review from pat-lewczuk September 12, 2026 10:07
@pat-lewczuk pat-lewczuk self-assigned this Sep 12, 2026
@pat-lewczuk pat-lewczuk added the in-progress Cezar agent is actively working this issue label Sep 12, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr started by @pat-lewczuk at 2026-09-12T17:05:47Z. Other auto-skills will skip this PR until the lock is released.

@pat-lewczuk pat-lewczuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review: fix(cockpit): keep local markdown paths inert

Verdict: 🔴 Changes requested — 1 major, 2 minor, 2 nits. No blockers.

The core fix is right and lands cleanly. I verified the before/after in jsdom: on main, [lesson](/Users/me/notes.md) renders as Streamdown's link-safety <button data-streamdown="link"> — click it and you get the "Open link?" dialog and then a bogus localhost navigation, exactly the reported bug. With this patch it renders <a data-streamdown="link">lesson</a> with no href, inert, text intact. https:// links are untouched. Reusing the existing isHttpUrl guard (#431) rather than inventing a second protocol allowlist is the right call, and the CSS rule is correctly placed in the deliberately-UNLAYERED block (index.css:481) so it outranks Streamdown's text-primary underline font-medium utilities. The a[…]:not([href]) selector also can't misfire: a live link renders as a <button>, so only the stripped case is ever an href-less <a>.

One thing needs to change before merge.


🟠 Major

1. urlTransform also strips image src, and a non-http image then disappears completely — alt text and all.
packages/web/src/routes/task-thread/markdown.tsx:101-104

Streamdown applies urlTransform to every URL attribute in html-url-attributes (href, src, poster, cite, …), not just href:

// node_modules/streamdown/dist/chunk-BO2N2NFS.js
for (let n in urlAttributes)
  if ( Object.hasOwn(e.properties, n))
    e.properties[n] = t(String(r || ''), n, e) ?? void 0

markdownUrlTransform ignores its key argument, so image sources go through isHttpUrl too. Verified in jsdom:

markdown on main with this PR
![Screenshot](/home/me/qa/shot.png) <img alt="Screenshot" src="/home/me/qa/shot.png"> nothing — container renders as <div class="… thread-markdown"></div>
![diagram](./a.png) <img alt="diagram" src="/a.png"> nothing
![shot](https://example.com/a.png) renders renders (unaffected)

This contradicts the contract the new comment itself states — "local file paths and other non-http destinations stay visible but inert". For links that is true; for images the content is silently deleted, with no trace that anything was suppressed. It's reachable in the PR's own primary surface (agents routinely write local screenshot paths into transcripts) and in the GitHub tab, which renders issue/PR bodies that commonly use repo-relative image paths.

The key argument already in the signature confines it precisely:

const markdownUrlTransform: UrlTransform = (url, key, node) => {
  const transformed = defaultUrlTransform(url, key, node)
  // Only link destinations navigate; an `img src` cannot turn into a bogus SPA route.
  if (key !== 'href') return transformed
  return isHttpUrl(transformed) ? transformed : undefined
}

If suppressing non-http images is intended, that's defensible — but then say so in the comment and pin it with a test asserting what the reader is left with.


🟡 Minor

2. The behavioural blast radius is wider than the description says, and that's what QA will scope from.

The PR body says "scoped to the task-thread markdown renderer". True of the file, not of the behaviour — Markdown is imported by five modules:

  • packages/web/src/routes/task-thread/thread-items.tsx:24, run-header.tsx:79
  • packages/web/src/routes/github/github.tsx:59 — GitHub issue/PR bodies (:739) and comments (:1396)
  • packages/web/src/routes/compare-variants.tsx:34
  • packages/web/src/components/skill-detail.tsx:9SKILL.md bodies

GitHub bodies and SKILL.md text are full of relative links (references/claim-pr.md, docs/arch.md), which now render as inert text there too. That's arguably correct — those links already went nowhere useful — but this PR is needs-qa, and a tester reading the description will click through the task thread only. Please name the GitHub tab and the skill-detail view so they get covered.

3. The new test asserts only negatives, so it doesn't pin what the fix actually depends on.
packages/web/src/routes/task-thread/markdown.test.tsx:248-257

It checks that no dialog opens and window.open isn't called. It never checks the two things the behaviour rests on: that the anchor has no href (the exact hook a[data-streamdown='link']:not([href]) keys on), and that the path text survives — the "stays visible" half of the contract. A future transform returning '' instead of undefined would leave href="" on the anchor: this test stays green, the CSS rule stops matching, and the path renders as a styled link again. Two lines close it, and both hold today:

expect(rendered.hasAttribute('href')).toBe(false)
expect(rendered.textContent).toBe('lesson')

🔵 Nits

4. CODE_REVIEW.md asks that non-obvious code cite the spec or issue behind it, and this file's neighbours do (#524 at markdown.tsx:56, #431 via isHttpUrl). The new markdownUrlTransform block and the new CSS rule carry no citation.

5. mailto: and tel: destinations are now inert too (verified). Consistent with how isHttpUrl is used across the app, and I found nothing in-tree that emits them — just worth one line in the description.


Validation gate

Run in an isolated worktree at cf0d51b6.

Command Result
npm run typecheck ✅ pass
npm test ⚠️ 6191/6197 — see note
npm run test:unit ✅ pass (36)
npm run build (incl. check:pack) ✅ pass
npm run test:package ✅ pass (16)

The 6 npm test failures are not from this PR — all are in packages/cezar (the PR touches only packages/web) and all assert "outside a git repository" (git.test.ts:70, git-worktree.test.ts, git-changes.test.ts, health-forge.test.ts, projects-api.test.ts, automations-api.test.ts). My review sandbox redirects TMPDIR to …/.ai/cezar/tmp/<runId>, which sits inside the cezar checkout, so mkdtempSync(join(tmpdir(), …)) lands in a git repo and getRepoInfo correctly returns {root: '/home/cezar/cezar', …} instead of null. Re-running those six files with TMPDIR=/tmp: 176/176 pass. PR CI is green on all three required checks.

Targeted: packages/web/src/routes/task-thread/markdown.test.tsx — 23/23 pass, including the new case.

needs-qa is the right call and stays on. Manual QA should cover the task thread, the GitHub tab, and skill detail.

@pat-lewczuk pat-lewczuk added changes-requested Reviewer requested changes and removed review Ready for code review labels Sep 12, 2026
@pat-lewczuk

pat-lewczuk commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr — 🏷️ label rationale

  • 🔴 changes-requested — Code review found one major issue: urlTransform also strips image src, so a non-http image is removed from the DOM entirely rather than staying visible (review).
  • 🐛 bug — unchanged; this fixes local filesystem paths being offered as browser links.
  • 🧪 needs-qa — unchanged and still required; the change alters click behaviour in the task thread, the GitHub tab and skill detail, none of which has been exercised in a browser yet.
  • 🟡 priority-medium — unchanged; an ordinary user-facing bug fix, not release-blocking.
  • 🟡 risk-medium — unchanged; the touched renderer is shared by five call sites, and the fix ships with a regression test.

@pat-lewczuk pat-lewczuk assigned matgren and unassigned pat-lewczuk Sep 12, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

@matgren, the link fix itself is good — I confirmed the before/after in jsdom and [lesson](/Users/…/notes.md) goes from Streamdown's clickable link-safety button to an inert href-less <a>, with https:// links untouched.

One thing to change before this can merge: urlTransform is applied by Streamdown to every URL attribute, not just href, so it strips image src too — ![Screenshot](/home/me/qa/shot.png) now renders nothing at all (alt text included) where main rendered an <img>. That contradicts the "stay visible but inert" contract in your own comment. Gating on the key argument already in the signature (if (key !== 'href') return transformed) confines it to links; if suppressing non-http images is deliberate, document it and pin it with a test instead.

Also worth picking up while you're in there: two assertions on the new test to pin the href-absence and the surviving text (the CSS rule keys on :not([href]), and nothing currently guards that), and a line in the description naming the GitHub tab and skill-detail view — Markdown is shared by five call sites, so QA needs to click through more than the task thread.

Details and the reproduction table are in the review. Push the update and re-request review.

@pat-lewczuk pat-lewczuk removed the in-progress Cezar agent is actively working this issue label Sep 12, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr completed: CHANGES REQUESTED. Lock released.

1 major, 2 minor, 2 nits — the link fix is correct, but urlTransform also strips image src, which deletes non-http images from the DOM entirely. No blockers, no merge conflicts, PR CI green on all three required checks.

Validation gate ran at cf0d51b6: typecheck, test:unit, build (incl. check:pack) and test:package all pass. The 6 npm test failures are a sandbox artifact, not this PR — all six are packages/cezar tests asserting "outside a git repository", and my review environment redirects TMPDIR inside the cezar checkout; they pass 176/176 with TMPDIR=/tmp. The PR touches only packages/web.

autofix: skipped (not my PR — re-run with --autofix to fix it here). Reassigned to @matgren.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working changes-requested Reviewer requested changes needs-qa Requires manual QA before merge priority-medium Ordinary bug or feature risk-medium Ordinary change with tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants