mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-25 06:33:13 +08:00
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:
@@ -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">
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user