fix(ui): serve the upstream issues URL from the backend

The UI security audit (security-ui.sh, layer A1) forbids hardcoded
external URLs in graph-ui source — the callout's GitHub issues link
tripped it on all pr-smoke legs. Follow the established pattern for
external targets (/api/repo-info deep-links): /api/ui-config now
carries upstream_issues_url and the callout consumes it, rendering the
issue button only when the backend provides an https URL. The protocol
check uses a regex literal on purpose: a bare protocol string in source
also aborts the audit's URL extraction.

Verified: security-ui.sh passes locally; /api/ui-config serves the URL;
the callout opens with the prefilled link in the browser; frontend
build + tests green.

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-07-09 03:38:34 +02:00
parent 660b90d94a
commit 3eb294fa11
2 changed files with 54 additions and 17 deletions
+47 -16
View File
@@ -1,9 +1,24 @@
import { useEffect, useState } from "react";
import type { GraphNode } from "../lib/types";
/* Upstream tracker for indexing gaps — issues land on the codebase-memory-mcp
* project itself (the tool that failed to index), never the user's repo. */
const UPSTREAM_ISSUES_URL = "https://github.com/DeusData/codebase-memory-mcp/issues/new";
/* Upstream tracker for indexing gaps. The URL is served by the backend
* (/api/ui-config) — the UI security audit forbids hardcoded external URLs in
* graph-ui source, so external targets come from an auditable backend
* response (same pattern as the /api/repo-info deep-links). */
let issuesUrlRequest: Promise<string | null> | null = null;
function fetchIssuesUrl(): Promise<string | null> {
issuesUrlRequest ??= fetch("/api/ui-config")
.then((r) => (r.ok ? r.json() : null))
.then((cfg) => {
const url: unknown = cfg?.upstream_issues_url;
/* Accept only an https URL (regex literal on purpose — the UI security
* audit greps source for protocol strings). */
return typeof url === "string" && /^https:\/\//.test(url) ? url : null;
})
.catch(() => null);
return issuesUrlRequest;
}
interface MissedCalloutProps {
node: GraphNode;
@@ -11,7 +26,7 @@ interface MissedCalloutProps {
onClose: () => void;
}
function buildIssueUrl(path: string, project: string | null): string {
function buildIssueUrl(base: string, path: string, project: string | null): string {
const title = `Indexing gap: ${path}`;
const body = [
"## Not fully indexed (best-effort coverage signal)",
@@ -25,17 +40,20 @@ function buildIssueUrl(path: string, project: string | null): string {
"",
"_Reported from the graph UI's missed-coverage view._",
].join("\n");
return `${UPSTREAM_ISSUES_URL}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
return `${base}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
}
function buildAgentPrompt(path: string, project: string | null): string {
function buildAgentPrompt(issuesUrl: string | null, path: string, project: string | null): string {
const where = issuesUrl
? `file a GitHub issue at ${issuesUrl}`
: "file a GitHub issue on the codebase-memory-mcp project";
return (
`codebase-memory-mcp could not fully index \`${path}\`` +
(project ? ` (project \`${project}\`)` : "") +
" — best-effort coverage signal. Please: " +
"1) call the index_status MCP tool and note this file's flagged line ranges under parse_partial; " +
"2) read those ranges in the file and summarize which construct fails to parse; " +
`3) file a GitHub issue at ${UPSTREAM_ISSUES_URL} titled "Indexing gap: ${path}" ` +
`3) ${where}, titled "Indexing gap: ${path}", ` +
"with the summary — include a minimal reproducible snippet ONLY if the code is shareable."
);
}
@@ -46,6 +64,17 @@ function buildAgentPrompt(path: string, project: string | null): string {
export function MissedCallout({ node, project, onClose }: MissedCalloutProps) {
const path = node.file_path || node.name;
const [copied, setCopied] = useState(false);
const [issuesUrl, setIssuesUrl] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchIssuesUrl().then((url) => {
if (!cancelled) setIssuesUrl(url);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (!copied) return;
@@ -55,7 +84,7 @@ export function MissedCallout({ node, project, onClose }: MissedCalloutProps) {
const copyPrompt = async () => {
try {
await navigator.clipboard.writeText(buildAgentPrompt(path, project));
await navigator.clipboard.writeText(buildAgentPrompt(issuesUrl, path, project));
setCopied(true);
} catch {
/* clipboard unavailable (permissions/insecure context) — leave the
@@ -103,14 +132,16 @@ export function MissedCallout({ node, project, onClose }: MissedCalloutProps) {
>
{copied ? "✓ Copied — paste it to your agent" : "Copy agent prompt"}
</button>
<a
href={buildIssueUrl(path, project)}
target="_blank"
rel="noreferrer"
className="text-[12px] font-medium rounded-md border border-white/10 bg-white/[0.03] text-foreground/80 hover:bg-white/[0.07] px-3 py-1.5 transition-all"
>
File a GitHub issue (prefilled) ↗
</a>
{issuesUrl && (
<a
href={buildIssueUrl(issuesUrl, path, project)}
target="_blank"
rel="noreferrer"
className="text-[12px] font-medium rounded-md border border-white/10 bg-white/[0.03] text-foreground/80 hover:bg-white/[0.07] px-3 py-1.5 transition-all"
>
File a GitHub issue (prefilled) ↗
</a>
)}
</div>
<p className="text-[10px] leading-snug text-foreground/35 mt-1">
+7 -1
View File
@@ -116,7 +116,13 @@ static void handle_ui_config(cbm_http_conn_t *c, const cbm_http_req_t *req) {
if (cfg) {
cbm_config_close(cfg);
}
cbm_http_replyf(c, 200, g_cors_json, "{\"lang\":\"%s\"}", lang_buf);
/* upstream_issues_url: where the missed-coverage callout (#963) sends
* edge-case reports. Served from the backend on purpose — the UI security
* audit forbids hardcoded external URLs in graph-ui source (external
* targets must come from an auditable backend response, same pattern as
* the /api/repo-info deep-links). */
cbm_http_replyf(c, 200, g_cors_json, "{\"lang\":\"%s\",\"upstream_issues_url\":\"%s\"}",
lang_buf, "https://github.com/DeusData/codebase-memory-mcp/issues/new");
}
/* ── Server state ─────────────────────────────────────────────── */