Skip to content

Help panel uses hand-rolled Markdown subset; vetted library would reduce fragility (agent-os-vscode extension.ts) #2199

@finnoybu

Description

@finnoybu

Problem

renderMarkdownHtml plus its helpers inlineMdFormat (lines 1094-1099) and escHtml (line 1092) implement a small Markdown-to-HTML converter to render help-panel content:

function renderMarkdownHtml(md: string): string {
    const out: string[] = [];
    let inList = false, inTable = false;
    function close(): void { ... }
    for (const line of md.split('\n')) {
        const t = line.trim();
        if (t.startsWith('### ')) { close(); out.push(`<h3>${escHtml(t.slice(4))}</h3>`); }
        else if (t.startsWith('## ')) { ... }
        ...
        else if (t.startsWith('| ')) { /* tables */ }
        else if (t.startsWith('- ')) { /* unordered list */ }
        ...
    }
    ...
}

function inlineMdFormat(s: string): string {
    let out = escHtml(s);
    out = out.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
    return out.replace(/`(.+?)`/g, '<code>$1</code>');
}

The subset supports:

  • # / ## / ### headers (line-prefix match only; no setext form, no ATX-close suffix).
  • - unordered lists (line-prefix match only).
  • | ... | ... | tables (every |-line opens or extends a table; the separator row is detected by every cell matching /^[\s-:]+$/).
  • Inline **bold** and `code` via greedy capture groups.
  • Plain paragraphs for everything else.

Known places where the rendered output drifts from CommonMark and from how the source .md looks on github.com:

  • Ordered lists are not supported — 1. item renders as a paragraph.
  • Code blocks (```fenced``` or indented) render as paragraphs whose contents are HTML-escaped but unstyled; inline backticks inside the fence content land back inside <code>...</code> via inlineMdFormat.
  • Blockquotes (> ...) render as paragraphs.
  • Links ([label](url)) render as raw text — only escaped, never linkified.
  • Nested lists flatten to a single <ul> with no indentation.
  • Table alignment (| :--- | ---: |) is ignored — alignment markers are stripped from the cell-grid build but never applied as text-align.
  • Bold-inside-code / code-inside-bold parses incorrectly because the inline replace runs greedily on already-escaped HTML, so `**not bold**` produces <code><strong>not bold</strong></code> instead of <code>**not bold**</code>.
  • The line-by-line architecture can't represent any construct that spans lines without a continuation marker (multi-line paragraphs collapse to per-line <p>).

Each of these is the kind of regression that surfaces the moment someone authors documentation in a real Markdown editor (or copies an existing .md from the docs site) and ships it through the help panel.

Why this isn't an obvious unilateral fix

The natural fix is "replace with a vetted Markdown library," but that's a non-trivial trade-off rather than a clean win:

  • Supply-chain risk. The extension currently pulls Markdown rendering from its own source and ships no Markdown dependency. Adding e.g. marked (~30 KB, ~200 KB with deps) or markdown-it (~80 KB, more transitive deps) widens the supply-chain surface and adds a CVE-watch surface for the extension. marked has had several historical XSS regressions; markdown-it has had fewer but a larger plugin ecosystem to audit. Both options need to be evaluated against the project's vendor policy.
  • VS Code's built-in option. VS Code ships a vscode.markdown Markdown-It-based renderer accessible via vscode.commands.executeCommand('markdown.api.render', md). This is the lowest-supply-chain option (the dep is already trusted because it's bundled with the editor itself) but the API is unofficial / undocumented and its exact return shape has shifted between VS Code versions.
  • DOM-based approach. Since this is a webview, the rendered HTML eventually lands in a browser context. A targeted alternative is to keep the .md source, ship it to the webview, and render with marked + DOMPurify on the webview side — which sidesteps the "trust the host renderer" question entirely but adds two webview-side deps and shifts the XSS audit to DOMPurify.
  • Output surface is small. The help-panel content is project-controlled (not user-provided), so the fragility is a UX/maintenance bug rather than a security bug. The benefit of a library scales with how much help content the project ends up shipping.
  • CSP impact. The current renderer composes a CSP with 'unsafe-inline' for styles and no script source. A library-based renderer may want to emit syntax-highlighted <code> blocks via a script, which would require relaxing the CSP. Worth factoring into the choice.

Suggested next step

I'd like the maintainers to make the trade-off call. Three options I see, in increasing intrusiveness:

  1. Keep hand-rolled, fix the named gaps. Document the subset in a comment, add ordered-list and link support, fix the bold-inside-code parse order. No new deps; the renderer remains fragile by design but is now closer to what an author would expect.
  2. Adopt vscode.commands.executeCommand('markdown.api.render', ...). Removes the hand-rolled renderer entirely with zero new deps in package.json. Risk is the API contract, which has changed across VS Code versions in the past. Would want a fallback path if the call fails.
  3. Add marked + a pinned DOMPurify. Full CommonMark support and a clear audit story. New supply-chain surface and CSP review needed.

Happy to follow up with a PR once a direction is chosen — I haven't shipped a change to this file because the right answer depends on the project's vendor policy, not on the code itself.


Surfaced during independent audit conducted by @finnoybu (Ken Tannenbaum, AEGIS Initiative); [LOW, TypeScript].

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions