Skip to content

feat(cli): Cursor-only display_links MCP for unmangled URLs - #1564

Open
heavygee wants to merge 20 commits into
tiann:mainfrom
heavygee:feat/display-links
Open

feat(cli): Cursor-only display_links MCP for unmangled URLs#1564
heavygee wants to merge 20 commits into
tiann:mainfrom
heavygee:feat/display-links

Conversation

@heavygee

Copy link
Copy Markdown
Collaborator

Why this exists (please read before calling it MCP bloat)

I did not want another MCP tool. HappyServer tool lists are real context, and "just add an MCP" is a loaded gun in this project for good reason.

Cursor ACP forced the issue. Cursor-routed agents drop doubled letters and digits when they recall identifiers in visible assistant texttiann becomes tian; MagicDNS labels lose a doubled digit, *.mmd becomes *.md. Headset and phone operators then tap a 404. HAPI is not stripping the character: the assistant payload is already wrong when the hub stores it.

This is not "LLMs cannot emit doubled letters." In the same Cursor turn, thinking traces and MCP JSON can still contain tiann while the visible markdown contains tian/hapi. Native Claude Code and Codex CLI, same strings, do not do this in our controls. I reported it to Cursor; I am not asking this repo to fix Cursor.

The only HAPI-side fix that does not shoot remote operators is the same contract as display_image: do not let the click target be reconstructed from chat markdown. Paint href bytes that were constructed outside that prose path.

If you run Cursor ACP and have never seen this: please comment. I am assuming everyone on that path hits it and this is the least-bad escape hatch. If it is just me, the Cursor-only gate still keeps the tool out of everyone else's context.

What this PR does

  • MCP display_links on the session HappyServer, Cursor sessions only (Claude / Codex / OpenCode / Grok do not register it).
  • CLI / script fallback: hapi display-links and scripts/tooling/hapi-display-links.mjs (same idea as hapi-display-image.mjs).
  • Web Links card: <a href> from stored bytes. http(s) only; javascript: / data: / vbscript: / file: denied.
  • Callers that know landmine hosts should concatenate in the tool/script arguments ("tia"+"nn"), never type the host in assistant prose.
  • Plumbing: Cursor MCP overlay follows a relocated ~/.cursor symlink so the tool is actually visible (Cursor ACP ignores session/new mcpServers; overlay install used to refuse the symlink).

Not in this PR: rewriting historical mangled assistant text at render time.

Test plan

  • Shared parse/validate: concatenated landmine href round-trips as stored bytes; deny-schemes rejected
  • MCP registration tests: display_links on Cursor, absent for other flavors
  • Overlay follows a symlinked Cursor config dir and writes mcp.json on the real target
  • Web card renders href from the payload (not from markdown)
  • Cursor ACP session: call display_links with a concatenated landmine href; tap the card; address bar has tiann, not tian
  • Same session: a URL typed in assistant markdown may still mangle; that is the bug this tool bypasses, not a regression

Issues

Fixes #1516

@github-actions github-actions 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.

Findings

  • [Major] Explicit session targeting dereferences the target runner's loopback MCP URL on the caller host — hapiMcpUrl is published as http://127.0.0.1:<port> by the CLI that owns the session, but the new command can resolve any session in the hub namespace and then opens that URL locally. Cross-runner use therefore fails or can reach an unrelated local MCP server on a coincident port. Evidence cli/src/commands/displayLinks.ts:176; same new path scripts/tooling/hapi-display-links.mjs:157; related context cli/src/claude/utils/startHappyServer.ts:589.
    Suggested fix:
    if (parsed.sessionArg !== null && !SELF_TOKENS.has(parsed.sessionArg)) {
        throw new Error('display-links only supports the current local session; run it on the target runner')
    }
    Longer-term, explicit remote targeting needs a hub-to-owning-CLI RPC instead of exposing/dereferencing hapiMcpUrl.

Summary
Review mode: initial

  • One Major correctness issue found in the explicit-session CLI/script fallback.
  • Residual risk: the PR test plan still lacks a real Cursor ACP invocation/tap check.

Testing

  • GitHub test and integration checks pass.
  • Local test run unavailable because bun is not installed in this review runner.
  • Add a regression test where the selected session belongs to another machine/runner.

HAPI Bot

Comment thread cli/src/commands/displayLinks.ts

@github-actions github-actions 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.

Findings

  • No findings.

Summary
Review mode: follow-up after new commits

  • Reviewed the full latest diff; the prior Major cross-runner loopback issue is addressed by restricting explicit targets to the current local session.
  • Residual risk: the real Cursor ACP invocation/tap test remains unchecked, so the end-to-end workaround is not yet verified in its affected environment.

Testing

  • GitHub integration check: pass.
  • GitHub test check: pending at review time.
  • Not run locally (automation review; PR code was not executed).

HAPI Bot

@heavygee
heavygee force-pushed the feat/display-links branch from bdc2e36 to f1c77b9 Compare August 16, 2026 17:06

@github-actions github-actions 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.

Findings

  • [Minor] Use a unique key for duplicate destinations — the payload accepts repeated hrefs, but both URL-row branches use href alone as the React key. Two cards pointing to the same URL (for example, with different titles) therefore create duplicate keys and can be reused incorrectly during reconciliation. Evidence web/src/components/AssistantChat/messages/ToolMessage.tsx:119
    Suggested fix:
    {props.block.urls.map((url, index) => {
        const rowKey = `${url.href}:${index}`
        // use key={rowKey} in both branches
    })}
  • [Minor] Localize the new card labels — the added headings, copy action, success state, and default accessible label are hard-coded English, so the Chinese UI renders a mixed-language card. This file already uses useTranslation; add matching keys to both locale dictionaries. Evidence web/src/components/AssistantChat/messages/ToolMessage.tsx:97
    Suggested fix:
    const { t } = useTranslation()
    // ...
    {copied ? t('displayLinks.copied') : t('displayLinks.tapToCopy')}

Summary
Review mode: follow-up after new commits

  • Two Minor findings: duplicate React row keys and untranslated user-facing labels.
  • Residual risk: the PR test plan still has no completed real Cursor ACP invocation/tap check, so the affected end-to-end path remains unverified.

Testing

  • GitHub integration check: pass.
  • GitHub test check: in progress at review time.
  • Suggested: render two URL entries with the same href and different titles; add coverage for the localized display-links labels and markdown export.
  • Not run locally (automation review; PR code was not executed).

HAPI Bot

Comment thread web/src/components/AssistantChat/messages/ToolMessage.tsx Outdated
Comment thread web/src/components/AssistantChat/messages/ToolMessage.tsx Outdated

@github-actions github-actions 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.

Findings

  • [Minor] Preserve configured hub headers in the new CLI fallback — hapi display-links calls /api/auth and the session endpoints with hand-built headers, bypassing configuration.extraHeaders. HAPI documents these headers for protected/self-hosted hubs and centralizes them in buildHubRequestHeaders, so this command can fail at the proxy while existing CLI hub operations succeed. Evidence cli/src/commands/displayLinks.ts:139
    Suggested fix:
    import { buildHubRequestHeaders } from "@/api/hubExtraHeaders"
    
    headers: buildHubRequestHeaders({ "Content-Type": "application/json" })
    // After JWT exchange:
    return buildHubRequestHeaders({ Authorization: `Bearer ${body.token}` })

Summary
Review mode: follow-up after new commits

  • The previous duplicate-key and localization findings are resolved by the latest commit.
  • One Minor finding remains: the CLI fallback does not honor configured extra hub headers.
  • Residual risk: the PR test plan still has no completed real Cursor ACP invocation/tap check, so the affected end-to-end path remains unverified.

Testing

  • GitHub integration check: pass.
  • GitHub test check: pending at review time.
  • Suggested: configure configuration.extraHeaders, then assert auth, session-detail, and session-list requests include them.
  • Not run locally (security review; PR code was not executed).

HAPI Bot

Comment thread cli/src/commands/displayLinks.ts Outdated

@github-actions github-actions 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.

Findings

  • [Minor] Preserve configured hub headers in the standalone fallback - the new script reads the configured token/settings but sends auth and session requests with hand-built headers. On hubs protected by HAPI_EXTRA_HEADERS_JSON or settings.json.extraHeaders, this fallback fails at the proxy even though hapi display-links works. Evidence scripts/tooling/hapi-display-links.mjs:114.
    Suggested fix:
    const settings = JSON.parse(readFileSync(SETTINGS, "utf8"))
    const extraHeaders = process.env.HAPI_EXTRA_HEADERS_JSON
        ? JSON.parse(process.env.HAPI_EXTRA_HEADERS_JSON)
        : (settings.extraHeaders ?? {})
    
    headers: { ...extraHeaders, "Content-Type": "application/json" }
    const authHeaders = { ...extraHeaders, Authorization: `Bearer ${jwt}` }

Summary
Review mode: follow-up after new commits

  • The prior CLI extra-header finding is fixed, but the same protected-hub incompatibility remains in the newly added standalone script.
  • Residual risk: the PR test plan still has no completed real Cursor ACP invocation/tap check, so overlay-driven tool availability and exact href navigation remain unverified end to end.

Testing

  • GitHub integration check: pass.
  • GitHub test check: pending at review time.
  • Not run locally (security review; PR code was not executed).
  • Suggested: run the standalone script with settings-backed and environment-backed extra headers and assert both auth and session requests include them.

HAPI Bot

Comment thread scripts/tooling/hapi-display-links.mjs Outdated

@github-actions github-actions 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.

Findings

  • No findings.

Summary
Review mode: follow-up after new commits

  • Reviewed the full latest diff. The prior standalone fallback extra-header finding is resolved on the current head.
  • Residual risks: the standalone environment/settings header merge has no direct automated request test, and the PR test plan still has no completed real Cursor ACP invocation/tap check.

Testing

  • GitHub integration check: pass.
  • GitHub test check: pending at review time.
  • Not run locally (security review; PR code was not executed).
  • Suggested: add standalone-script request tests for environment-backed and settings-backed extra headers; complete one real Cursor ACP display_links invocation and navigation check.

HAPI Bot

@github-actions github-actions 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.

Findings

  • [Major] Starting another Cursor session disables every live sibling HAPI MCP and cleanup never re-enables it. Evidence cli/src/cursor/utils/cursorMcpOverlay.ts:452.
  • [Major] Sibling detection treats any hapi-* id as HAPI-owned, so unrelated user MCP integrations can be persistently disabled. Evidence cli/src/cursor/utils/cursorMcpOverlay.ts:386.
  • [Major] Exact-copy values advertised for secrets/tokens remain in Share Turn capture DOM. Evidence web/src/components/AssistantChat/messages/ToolMessage.tsx:80.

Summary
Review mode: follow-up after new commits

  • Reviewed the full latest diff plus the prior HAPI Bot review and commit delta.
  • The new session-id check fails closed for a misrouted display_links call, but the sibling-disable lifecycle introduces broader MCP regressions and exact-copy content is not excluded from share capture.

Testing

  • GitHub integration check: pass.
  • GitHub test check: pending at review time.
  • Not run locally (security review; PR code was not executed).
  • Suggested: concurrent Cursor A/B lifecycle test (including B cleanup and A reuse), user-owned hapi-docs preservation test, and Share Turn capture test proving exact-copy values are absent.

HAPI Bot

Comment thread cli/src/cursor/utils/cursorMcpOverlay.ts Outdated
Comment thread cli/src/cursor/utils/cursorMcpOverlay.ts Outdated
Comment thread web/src/components/AssistantChat/messages/ToolMessage.tsx Outdated

@github-actions github-actions 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.

Findings

  • [Major] Exact-copy values can still enter session exports through the ACP tool-call record. Evidence cli/src/claude/utils/startHappyServer.ts:135; related flow cli/src/agent/backends/acp/AcpMessageHandler.ts:737, cli/src/agent/messageConverter.ts:90, and web/src/lib/sessionExport/markdown.ts:87.
    Suggested fix:
    const safeMessage = message.type === 'tool_call'
        && isDisplayLinksToolName(message.name)
        ? { ...message, input: redactDisplayLinksTextValues(message.input) }
        : message
    const converted = convertAgentMessage(safeMessage, this.currentBackendModel)
  • [Major] The secret fallback places exact-copy values in argv. Evidence cli/src/commands/displayLinks.ts:48 and scripts/tooling/hapi-display-links.mjs:73.
    Suggested fix:
    const value = args.includes('--text-stdin')
        ? (await Bun.stdin.text()).replace(/\r?\n$/, '')
        : undefined

Summary
Review mode: follow-up after new commits

  • Reviewed the complete current diff plus the prior bot review and commit delta.
  • The prior sibling-overlay and Share Turn findings are resolved at this head.
  • Two secret-handling paths still bypass the intended exact-copy containment.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub integration and test checks were pending at review time.
  • Suggested: an ACP display_links sentinel test proving persisted/exported tool-call input is redacted, plus CLI/script stdin tests proving the value is absent from argv.

HAPI Bot

Comment thread cli/src/claude/utils/startHappyServer.ts
Comment thread cli/src/commands/displayLinks.ts

@github-actions github-actions 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.

Findings

  • No findings at or above the 80% confidence threshold.

Summary

  • Review mode: follow-up after new commits
  • Reviewed the complete current diff, the prior HAPI Bot review, and the commit delta.
  • The prior ACP/export redaction and argv secret-exposure findings are resolved at this head.
  • Residual risk: the real Cursor ACP click/copy path remains manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub integration: passed. GitHub test: in progress at review time.
  • Suggested: complete the PR plan’s Cursor ACP session checks for byte-accurate link navigation and exact-copy behavior.

HAPI Bot

heavygee and others added 12 commits August 20, 2026 13:05
Cursor-routed agents drop doubled letters when recalling URLs; store
href bytes built outside the model (MCP display_links + CLI fallback)
and render a tappable SessionChat card without a fake user turn.

Fixes tiann#1516

Co-authored-by: Cursor <cursoragent@cursor.com>
Doubled-letter URL recall is a Cursor-routed failure mode. Keep the
paint path and CLI fallback, but do not advertise display_links to
Claude/Codex/OpenCode/Grok tool lists or system prompts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Estate hosts bind Cursor state under /var/lib/hapi/cursor via a home
symlink; refusing the symlink directory left overlay install dead and
pushed agents toward stale project mcp.json sidecars. Resolve to the
real directory (and honor HAPI_CURSOR_MCP_CONFIG_DIR); keep refusing
symlinked mcp.json files.

Co-authored-by: Cursor <cursoragent@cursor.com>
hapiMcpUrl is 127.0.0.1 on the owning CLI. Opening it for another hub
session hits the caller host. Restrict the CLI/script fallback to self.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cursor drops doubled letters in secrets and tokens the same way it
drops them in hosts. Extend display_links with texts so agents construct
byte-accurate copy cards in tool args instead of typing them in prose.

Co-authored-by: Cursor <cursoragent@cursor.com>
Duplicate hrefs with different titles collided on React keys. Card
chrome was English-only in a file that already uses i18n.

Co-authored-by: Cursor <cursoragent@cursor.com>
Protected hubs already inject HAPI_EXTRA_HEADERS_JSON via
buildHubRequestHeaders. display-links was skipping them on /api/auth
and session GETs, so the fallback could 403 at the proxy.

Co-authored-by: Cursor <cursoragent@cursor.com>
Same proxy-header hole as the CLI command. Read
HAPI_EXTRA_HEADERS_JSON, else settings.json extraHeaders, and merge
onto auth and session fetches.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cursor routes duplicate bare tool names to one hapi-* server, so
Kinrupt display_links calls painted Sparling. Disable sibling overlays
and refuse paints whose sessionId is not this HappyServer.

Co-authored-by: Cursor <cursoragent@cursor.com>
… share

Exclusive sibling disable stranded live Cursor sessions and could hit
user-owned hapi-* ids. Fail-close misroutes with sessionId instead.
Exclude exact-copy rows from Share Turn and session markdown export.

Co-authored-by: Cursor <cursoragent@cursor.com>
ACP persisted display_links rawInput, so session markdown still leaked
secrets after Share Turn exclusion. Redact texts values on the wire and
add --text-stdin so secrets are not process arguments.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rebase onto upstream/main added an exhaustive ChatBlock projector;
handle the new display-links kind so web typecheck stays green.

Co-authored-by: Cursor <cursoragent@cursor.com>
@heavygee
heavygee force-pushed the feat/display-links branch from 0555671 to e90d7a3 Compare August 20, 2026 13:07

@github-actions github-actions 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.

Findings

  • [Major] Native clients drop the new display-links block; see the inline finding on web/scripts/fixtures/projection.ts:109.

Summary

  • Review mode: follow-up after new commits
  • One Major correctness/contract issue found: the rebased PR adds a new normative web chat block without the required iOS/Android protocol ports or generated conformance fixture/version update.
  • Residual risk: the real Cursor ACP click/copy path remains manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub drift-gate, integration, and test checks: passed.
  • Suggested: add the generated cursor-display-links chat fixture, bump the fixture version for the projection-shape change, and run both native conformance suites after implementing the block.

HAPI Bot

Comment thread web/scripts/fixtures/projection.ts
heavygee and others added 2 commits August 20, 2026 13:23
Bump FIXTURE_VERSION for the new projection shape, add cursor-display-links
golden, and implement normalize/reduce/project/UI on both native ports.

Co-authored-by: Cursor <cursoragent@cursor.com>
Kotlin refuses smart-casts on public API properties from another module;
bind title locals before Text/clipboard use so :app:compileDebugKotlin passes.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions 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.

Findings

  • [Minor] Android link taps can crash when no URL handler is installed; see the inline finding on android/app/src/main/kotlin/app/hapi/companion/feature/chat/blocks/DisplayLinksBlockView.kt:66.

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • The prior Major native-contract issue is resolved: fixture versioning plus Android/iOS normalization, reduction, projection, and rendering are now present.
  • One Minor Android correctness issue remains in the new card interaction.
  • Residual risk: the real Cursor ACP click/copy path remains manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub app-build, build, drift-gate, integration, package-tests, and test checks: passed.
  • Suggested: cover the no-browser ActivityNotFoundException path and complete the manual Cursor ACP click/copy checks from the PR test plan.

HAPI Bot

Catch ActivityNotFoundException via the existing ChatLinkHandler helper so
kiosk/work-profile devices toast instead of crashing the chat screen.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions 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.

Findings

  • [Major] Concurrent Cursor sessions still advertise a colliding display_links tool name; see the inline finding on cli/src/cursor/utils/cursorMcpOverlay.ts:298.

    Suggested fix:

    const displayLinksToolName =
        `hapi_${client.sessionId.replaceAll('-', '_')}_display_links`
    
    mcp.registerTool(displayLinksToolName, options, handler)
    toolNames.push(displayLinksToolName)

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • The prior Android ActivityNotFoundException finding is resolved by reusing the guarded openUrl helper.
  • One Major concurrent-session correctness issue remains: session binding fails closed after a wrong-server dispatch, but does not make the tool callable for every live Cursor session.
  • Residual risk: the real Cursor ACP click/copy path and concurrent-session routing remain manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub app-build, build, drift-gate, integration, and package-tests checks: passed. The main test check was still pending at review time.
  • Suggested: add a two-session Cursor MCP routing test that invokes each session-specific tool, and complete the manual Cursor ACP checks from the PR test plan.

HAPI Bot

Comment thread cli/src/cursor/utils/cursorMcpOverlay.ts Outdated
Register hapi_<sessionId>_display_links so concurrent Cursor hapi-* overlays
do not collide on bare display_links (forum 148059). Bridge/CLI/mjs forward
the same name; redaction still matches *_display_links.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions 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.

Findings

  • [Minor] Android retention accepts non-empty display-links arrays even when every item is rejected by normalization; see the inline finding on android/core/protocol/src/main/kotlin/app/hapi/protocol/window/MessageRetention.kt:158.

    Suggested fix:

    "display-links" ->
        parseDisplayLinksInput(prop(data, "urls")).isNotEmpty() ||
            parseDisplayTextsInput(prop(data, "texts")).isNotEmpty()
  • [Minor] Doctor output still advertises a bare display_links tool after the per-session rename; see the inline finding on cli/src/ui/doctorInlineMedia.ts:227.

    Suggested fix:

    console.log(chalk.gray(
        `  Display links tool: ${buildDisplayLinksToolName(session.id)}`
    ))

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • The prior Major concurrent-session routing issue is resolved by the per-session tool name across registration, forwarding, approval, and fallback invocation.
  • Two Minor inconsistencies remain: Android can retain invisible invalid rows, and doctor guidance names the obsolete tool.
  • Residual risk: the real Cursor ACP click/copy path and concurrent two-session routing remain manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub app-build, build, drift-gate, integration, and package-tests checks passed; the main test check was still pending at review time.
  • Suggested: add an Android retention case with invalid-only URL/text arrays, assert doctor output includes the session-specific name, and complete the manual Cursor ACP checks from the PR test plan.

HAPI Bot

Comment thread cli/src/ui/doctorInlineMedia.ts Outdated
Retention now uses the same URL/text parsers as normalize so invalid-only
payloads are not kept. Doctor prints each Cursor session's
hapi_<sessionId>_display_links name.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions 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.

Findings

  • [Major] Fail closed when redacting exact-copy tool input — malformed texts values and extra row fields can reach persisted ACP tool-call messages, evidence shared/src/displayLinks.ts:211.

    Suggested fix:

    if (Object.prototype.hasOwnProperty.call(record, "texts") === false) return input
    const texts = Array.isArray(record.texts)
        ? record.texts.map((item) => {
            if (typeof item === "string") return DISPLAY_LINKS_REDACTED_VALUE
            const row = item && typeof item === "object" && Array.isArray(item) === false
                ? item as Record<string, unknown>
                : null
            const title = typeof row?.title === "string" ? row.title : undefined
            return title
                ? { value: DISPLAY_LINKS_REDACTED_VALUE, title }
                : { value: DISPLAY_LINKS_REDACTED_VALUE }
        })
        : DISPLAY_LINKS_REDACTED_VALUE
    return { ...record, texts }
  • [Major] Mark Android exact-copy clips as sensitive — Android 13+ otherwise shows copied tokens in the system clipboard preview, evidence android/app/src/main/kotlin/app/hapi/companion/feature/chat/blocks/DisplayLinksBlockView.kt:100.

    Suggested fix:

    val clip = ClipData.newPlainText(textTitle ?: "copy", text.value).apply {
        description.extras = PersistableBundle().apply {
            putBoolean("android.content.extra.IS_SENSITIVE", true)
        }
    }
    clipboard.setPrimaryClip(clip)

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • The prior Android retention and doctor tool-name findings are resolved at the latest head.
  • Two Major secret-handling gaps remain: malformed ACP inputs can bypass persistence redaction, and Android clipboard previews can expose copied exact-copy values.
  • Residual risk: the real Cursor ACP click/copy path and concurrent two-session routing remain manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub package-tests, app-build, test, build, drift-gate, and integration checks passed.
  • Suggested: add malformed/non-array/extra-field redaction cases through AcpMessageHandler and convertAgentMessage; manually verify Android 13+ clipboard UI suppresses the value; complete the Cursor ACP checks from the PR test plan.

HAPI Bot

Comment thread shared/src/displayLinks.ts Outdated
Whitelist exact-copy row shape so backup/malformed texts cannot persist
secrets; mark ClipData EXTRA_IS_SENSITIVE so Android 13+ hides clipboard
previews for token cards.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions 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.

Findings

  • [Major] Whitelist the complete display_links tool input before persistence/export — redactDisplayLinksToolInput now sanitizes malformed texts rows, but return { ...record, texts } still preserves arbitrary top-level fields and extra fields nested under urls. ACP emits raw tool input before MCP schema validation, and Markdown export reuses this redactor, so { texts: [{ value: secret }], backup: secret } or a URL row carrying backup: secret still leaks the exact-copy bytes into stored tool-call history and exported Markdown. Evidence shared/src/displayLinks.ts:229.

    Suggested fix:

    const safe: Record<string, unknown> = {}
    if (Object.prototype.hasOwnProperty.call(record, 'urls')) {
        safe.urls = safeParseDisplayLinksInput(record.urls)
    }
    if (Object.prototype.hasOwnProperty.call(record, 'texts')) {
        safe.texts = Array.isArray(record.texts)
            ? record.texts.map((item) => {
                if (typeof item === 'string') return DISPLAY_LINKS_REDACTED_VALUE
                const row = item && typeof item === 'object' && !Array.isArray(item)
                    ? item as Record<string, unknown>
                    : null
                const title = typeof row?.title === 'string' ? row.title : undefined
                return title ? { value: DISPLAY_LINKS_REDACTED_VALUE, title } : { value: DISPLAY_LINKS_REDACTED_VALUE }
            })
            : DISPLAY_LINKS_REDACTED_VALUE
    }
    if (typeof record.sessionId === 'string') safe.sessionId = record.sessionId
    return safe

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • The two prior Major findings are resolved at the latest head.
  • One Major export/persistence redaction gap remains because unknown fields outside texts are retained.
  • Residual risk: the real Cursor ACP click/copy path and concurrent two-session routing remain manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • GitHub integration, package-tests, test, Android build, fixture drift-gate, and iOS app-build checks passed.
  • Suggested: add top-level and urls extra-secret-field cases through redactDisplayLinksToolInput, AcpMessageHandler, convertAgentMessage, and Markdown export.

HAPI Bot

Comment thread shared/src/displayLinks.ts
Rebuild the persisted/exported tool-call shape from urls/texts/sessionId so
arbitrary top-level or nested backup fields cannot leak secrets.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions 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.

Findings

  • [Major] Fail closed for non-object display_links inputs — ACP treats strings and arrays as usable raw input, but the redactor returns them unchanged. A malformed pre-schema-validation call can therefore persist/export exact-copy bytes despite the new object-field whitelist. Evidence shared/src/displayLinks.ts:209; triggered by cli/src/agent/backends/acp/AcpMessageHandler.ts:29 and cli/src/agent/backends/acp/AcpMessageHandler.ts:764.
    Suggested fix:
    if (input == null) return input
    if (typeof input !== 'object' || Array.isArray(input)) {
        return DISPLAY_LINKS_REDACTED_VALUE
    }
  • [Major] Keep exact-copy secrets off Universal Clipboard — this feature explicitly carries secrets/tokens, but assigning UIPasteboard.general.string uses the unrestricted general pasteboard with no expiration, allowing cross-device clipboard sync and indefinite retention. Evidence ios/Hapi/Features/Chat/Blocks/DisplayLinksBlockView.swift:42.
    Suggested fix:
    import UniformTypeIdentifiers
    
    UIPasteboard.general.setItems(
        [[UTType.utf8PlainText.identifier: text.value]],
        options: [
            .localOnly: true,
            .expirationDate: Date().addingTimeInterval(120),
        ]
    )

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • The prior unknown-field redaction finding is addressed at the latest head, but malformed primitive/array inputs still bypass redaction.
  • The new iOS exact-copy action needs local-only, expiring clipboard handling before secrets are safe to copy.
  • Residual risk: the real Cursor ACP click/copy path and concurrent two-session routing remain manually unverified.

Testing

  • Not run (automation; PR code was not executed per review security constraints).
  • Observed CI: integration, package-tests, app-build, build, and fixture drift-gate passed; the main test job was still pending at review time.
  • Suggested: add primitive/array redaction cases through redactDisplayLinksToolInput, AcpMessageHandler, convertAgentMessage, and Markdown export; extract/test the iOS pasteboard helper with local-only and expiration options.

HAPI Bot

Comment thread shared/src/displayLinks.ts Outdated
Comment thread ios/Hapi/Features/Chat/Blocks/DisplayLinksBlockView.swift Outdated
Fail closed when ACP feeds a string/array as tool input. Exact-copy on iOS
uses localOnly + 120s expiration instead of Universal Clipboard.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions 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.

Findings

  • None.

Questions

  • None.

Summary
Review mode: follow-up after new commits

  • No additional high-confidence issues found in the full latest diff.
  • The prior primitive/array redaction and unrestricted iOS pasteboard findings are addressed at this head.
  • Residual risk: the real Cursor ACP click/copy flow and concurrent two-session routing remain manually unverified.

Testing

  • Not run locally (automation; PR code was not executed per review security constraints).
  • Observed CI: test, integration, Android build, iOS package/app builds, and fixture drift-gate passed.

HAPI Bot

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): display_links MCP to paint clickable URLs into session chat

1 participant