rafinery
Read-only demoLive view of gallirohik/research-canvas at 1ebb712, scanned 20 September 2026 — nothing here can be edited.
Join the waitlistwhat this repo knows — every node a real file, every edge a declaration in its frontmatter
37 files in the brain repo
{
"schemaVersion": 1,
"emitter": 2,
"codeSha": "1ebb712b4a31969dd2ade1b59f92b18d1f456033",
"branch": "main",
"health": {
"verdict": "ITERATE"
},
"coverage": {
"domains": [
{
"domain": "agent-bridge",
"status": "mapped"
},
{
"domain": "agent-python",
"status": "mapped"
},
{
"domain": "agent-typescript",
"status": "mapped"
},
{
"domain": "build-tooling",
"status": "mapped"
},
{
"domain": "components",
"status": "mapped"
},
{
"domain": "data-persistence",
"status": "mapped"
},
{
"domain": "external-integrations",
"status": "mapped"
},
{
"domain": "routing-app-shell",
"status": "mapped"
},
{
"domain": "security",
"status": "mapped"
},
{
"domain": "state",
"status": "mapped"
},
{
"domain": "toolbox",
"status": "mapped"
}
]
},
"notes": [
{
"id": "add-agent-tool-howto",
"kind": "playbook",
"type": "how-to",
"domain": "agent-bridge",
"title": "How to add a new agent tool (and wire it to the canvas)",
"summary": "Add the no-op @tool schema, bind it in the chat node, route on its name, and — if it drives UI or streams a state field — mirror the string on the frontend (useCopilotAction) or in an emit_intermediate_state mapping",
"links": [
"langgraph-agent-convention",
"delete-resources-hitl-contract",
"agent-state-shape-contract",
"research-chat-flow"
],
"cites": [
{
"file": "agents/python/src/lib/chat.py",
"line": "16",
"token": "@tool"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "82",
"token": "bind_tools"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "156",
"token": "DeleteResources"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "52",
"token": "state_key"
},
{
"file": "src/components/ResearchCanvas.tsx",
"line": "37",
"token": "useCopilotAction"
}
],
"path": "brain/playbooks/add-agent-tool-howto.md"
},
{
"id": "model-selection-flow",
"kind": "playbook",
"type": "flow",
"domain": "agent-bridge",
"title": "How the model dropdown actually selects an LLM (it's state, not the agent name)",
"summary": "The ModelSelector writes ?coAgentsModel to the URL; that string becomes state.model and drives get_model on the backend — the agent NAME only branches google_genai vs not, and both names resolve to the same graph. The wiring is correct for all four options, but the anthropic MODEL ID is dead (claude-3-5-sonnet-20240620, retired 2025-10-28): check the id before the key",
"links": [
"agent-name-contract",
"langgraph-agent-convention",
"research-chat-flow"
],
"cites": [
{
"file": "src/components/ModelSelector.tsx",
"line": "23",
"token": "openai"
},
{
"file": "src/lib/model-selector-provider.tsx",
"line": "27",
"token": "coAgentsModel"
},
{
"file": "src/lib/model-selector-provider.tsx",
"line": "42",
"token": "research_agent"
},
{
"file": "src/app/Main.tsx",
"line": "12",
"token": "model"
},
{
"file": "agents/python/src/lib/model.py",
"line": "18",
"token": "state.get"
},
{
"file": "agents/python/src/lib/model.py",
"line": "32",
"token": "claude-3-5-sonnet-20240620"
},
{
"file": "agents/typescript/src/model.ts",
"line": "26",
"token": "claude-3-5-sonnet-20240620"
},
{
"file": "agents/python/src/lib/model.py",
"line": "41",
"token": "gemini-1.5-pro"
},
{
"file": "agents/python/src/lib/model.py",
"line": "49",
"token": "raise ValueError"
}
],
"path": "brain/playbooks/model-selection-flow.md"
},
{
"id": "research-chat-flow",
"kind": "playbook",
"type": "flow",
"domain": "agent-bridge",
"title": "End-to-end — chat message to canvas update via the LangGraph research agent",
"summary": "Authoritative trace (tight, cite-proven) of a CopilotChat turn through the /api/copilotkit runtime into the LangGraph agent graph and how streamed state signals render the canvas (download → chat → search/delete/fact-check → frontend render).",
"links": [
"agent-name-contract",
"agent-state-shape-contract",
"copilotkit-runtime-route-convention",
"langgraph-agent-convention",
"delete-resources-hitl-contract",
"model-selection-flow"
],
"cites": [
{
"file": "src/app/Main.tsx",
"line": "45",
"token": "CopilotChat"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "135",
"token": "runtime construction / deploymentUrl"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "82",
"token": "resource aggregation"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "160",
"token": "tool-call routing / ToolMessage"
},
{
"file": "agents/python/src/lib/search.py",
"line": "81",
"token": "copilotkit_emit_state / logs append"
},
{
"file": "agents/python/src/lib/fact_check.py",
"line": "137",
"token": "state[\"citations\"] assignment"
},
{
"file": "src/components/ResearchCanvas.tsx",
"line": "27",
"token": "useCoAgentStateRender (Progress)"
},
{
"file": "src/components/ResearchCanvas.tsx",
"line": "193",
"token": "report binding (Textarea value={state.report || \"\"})"
},
{
"file": "src/components/ResearchCanvas.tsx",
"line": "202",
"token": "citations guard (FactCheck render)"
}
],
"path": "brain/playbooks/research-chat-flow.md"
},
{
"id": "security-posture",
"kind": "playbook",
"type": "flow",
"domain": "security",
"title": "Trust boundaries, auth chokepoints, and secret handling — what is actually exposed",
"summary": "Two servers, one thin proxy and one wide-open agent: the Next.js route has a cosmetic x-api-key (the key ships in client JS) and the Python FastAPI agent has NO auth at all, so port 8000 is the real exposure; there is no user identity anywhere; the two SSRF guards on user-supplied URLs are the only guarded edges and both are DENY-PRIVATE, not allow-known — so the ?lgcDeploymentUrl one still forwards LANGSMITH_API_KEY to any public host a caller names (open P1 lgc-deployment-url-key-exfiltration)",
"links": [
"copilotkit-runtime-route-convention",
"env-and-integrations",
"langgraph-agent-convention",
"build-tooling-convention",
"research-chat-flow"
],
"cites": [
{
"file": "src/app/api/copilotkit/route.ts",
"line": "86",
"token": "export const POST"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "24",
"token": "isAuthorized"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "26",
"token": "Boolean(apiKey)"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "87-89",
"token": "Unauthorized"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "22",
"token": "visible to anyone via devtools"
},
{
"file": "src/app/page.tsx",
"line": "33",
"token": "NEXT_PUBLIC_COPILOTKIT_API_KEY"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "59",
"token": "isSafeDeploymentUrl"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "19",
"token": "langsmithApiKey"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "81",
"token": "addresses.every"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "123",
"token": "langsmithApiKey"
},
{
"file": "src/lib/model-selector-provider.tsx",
"line": "40",
"token": "lgcDeploymentUrl"
},
{
"file": "src/app/page.tsx",
"line": "24",
"token": "lgcDeploymentUrl"
},
{
"file": "agents/python/main.py",
"line": "17",
"token": "add_langgraph_fastapi_endpoint"
},
{
"file": "agents/python/main.py",
"line": "44",
"token": "0.0.0.0"
},
{
"file": "agents/python/main.py",
"line": "11",
"token": "load_dotenv"
},
{
"file": "agents/python/src/lib/download.py",
"line": "30",
"token": "_is_safe_url"
},
{
"file": "agents/python/src/lib/download.py",
"line": "41",
"token": "getaddrinfo"
},
{
"file": "agents/python/langgraph.json",
"line": "9",
"token": ".env"
},
{
"file": ".gitignore",
"line": "38",
"token": ".env"
}
],
"path": "brain/playbooks/security-posture.md",
"absent": "Depends"
},
{
"id": "agent-name-contract",
"kind": "rule",
"type": "contract",
"domain": "agent-bridge",
"title": "The agent-name strings wire the frontend to a LangGraph graph across 5 files",
"summary": "\"research_agent\" / \"research_agent_google_genai\" must match across the frontend agent prop, the runtime registry, and each agent's langgraph.json + endpoint path, or the coagent silently never connects",
"links": [
"agent-state-shape-contract",
"research-chat-flow",
"copilotkit-runtime-route-convention",
"model-selection-flow"
],
"cites": [
{
"file": "src/lib/model-selector-provider.tsx",
"line": "42",
"token": "research_agent"
},
{
"file": "src/lib/model-selector-provider.tsx",
"line": "44",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "109",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "110",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "112",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "113",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "121",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "124",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "126",
"token": "research_agent"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "129",
"token": "research_agent"
},
{
"file": "agents/python/langgraph.json",
"line": "6",
"token": "research_agent"
},
{
"file": "agents/python/langgraph.json",
"line": "7",
"token": "research_agent"
},
{
"file": "agents/python/main.py",
"line": "20",
"token": "research_agent"
},
{
"file": "agents/python/main.py",
"line": "22",
"token": "research_agent"
},
{
"file": "agents/python/main.py",
"line": "27",
"token": "research_agent"
},
{
"file": "agents/python/main.py",
"line": "29",
"token": "research_agent"
},
{
"file": "agents/typescript/langgraph.json",
"line": "6",
"token": "research_agent"
},
{
"file": "agents/typescript/langgraph.json",
"line": "7",
"token": "research_agent"
}
],
"path": "brain/rules/agent-name-contract.md",
"anchor": "research_agent"
},
{
"id": "agent-state-shape-contract",
"kind": "rule",
"type": "contract",
"domain": "state",
"title": "AgentState is a cross-process shape shared by the frontend and both agents",
"summary": "Six fields (model, research_question, report, resources, logs, citations) are defined across the TS frontend type and the Python agent; citations is Python+frontend ONLY (the TS agent's state.ts intentionally lags, verifiable-report plan decision) — a field the backend emits but the frontend doesn't expect (or vice versa) simply doesn't render",
"links": [
"agent-name-contract",
"research-chat-flow",
"copilotkit-runtime-route-convention"
],
"cites": [
{
"file": "src/lib/types.ts",
"line": "19",
"token": "AgentState"
},
{
"file": "src/lib/types.ts",
"line": "21",
"token": "research_question"
},
{
"file": "src/lib/types.ts",
"line": "22",
"token": "report"
},
{
"file": "src/lib/types.ts",
"line": "23",
"token": "resources"
},
{
"file": "src/lib/types.ts",
"line": "24",
"token": "logs"
},
{
"file": "src/lib/types.ts",
"line": "25",
"token": "citations"
},
{
"file": "agents/python/src/lib/state.py",
"line": "41",
"token": "AgentState"
},
{
"file": "agents/python/src/lib/state.py",
"line": "48",
"token": "research_question"
},
{
"file": "agents/python/src/lib/state.py",
"line": "51",
"token": "logs"
},
{
"file": "agents/python/src/lib/state.py",
"line": "52",
"token": "citations"
},
{
"file": "agents/typescript/src/state.ts",
"line": "19",
"token": "AgentStateAnnotation"
},
{
"file": "agents/typescript/src/state.ts",
"line": "21",
"token": "research_question"
},
{
"file": "agents/typescript/src/state.ts",
"line": "24",
"token": "logs"
}
],
"path": "brain/rules/agent-state-shape-contract.md",
"anchor": "none"
},
{
"id": "agent-typescript-parity",
"kind": "rule",
"type": "convention",
"domain": "agent-typescript",
"title": "The TypeScript agent is an alternate port of the Python agent (not run by default)",
"summary": "agents/typescript mirrors an OLDER Python graph with @langchain/langgraph — 5 nodes, no fact_check_node, no citations state; it is a lightly-used alternate that npm run dev does NOT start, so treat Python as the source of truth and expect the port to lag",
"links": [
"langgraph-agent-convention",
"agent-state-shape-contract",
"build-tooling-convention",
"state-persistence-convention"
],
"cites": [
{
"file": "agents/typescript/src/agent.ts",
"line": "15",
"token": "StateGraph"
},
{
"file": "agents/typescript/src/agent.ts",
"line": "23",
"token": "addConditionalEdges"
},
{
"file": "agents/typescript/src/agent.ts",
"line": "34",
"token": "interruptAfter"
},
{
"file": "agents/typescript/src/model.ts",
"line": "20",
"token": "openai"
},
{
"file": "agents/typescript/src/model.ts",
"line": "40",
"token": "throw new Error"
},
{
"file": "agents/typescript/src/state.ts",
"line": "19",
"token": "AgentStateAnnotation"
}
],
"path": "brain/rules/agent-typescript-parity.md"
},
{
"id": "build-tooling-convention",
"kind": "rule",
"type": "convention",
"domain": "build-tooling",
"title": "Dev/build wiring — concurrently runs UI + Python agent; the root app AND the TS agent are now de-workspaced, but vercel.json and readme.md still assume the upstream monorepo (readme.md is a non-exemplar — do not follow it)",
"summary": "npm run dev launches Next.js (3000) and the Python agent (8000) together; the root CopilotKit deps and — as of 2026-07-28 — agents/typescript were both de-workspaced to real npm ranges, so each installs standalone; vercel.json still cd's three levels up to run nx and readme.md still documents the upstream layout — the two stale-upstream leftovers that actually bite (of four left behind; dockerize.sh and wfcms-data.json are the harmless pair), and readme.md is the one that will actively mislead you",
"links": [
"langgraph-agent-convention",
"agent-typescript-parity",
"env-and-integrations"
],
"cites": [
{
"file": "package.json",
"line": "9",
"token": "concurrently"
},
{
"file": "package.json",
"line": "12",
"token": "\"dev:agent:py\""
},
{
"file": "package.json",
"line": "14",
"token": "install:agent:ts"
},
{
"file": "package.json",
"line": "17",
"token": "\"@copilotkit/react-core\""
},
{
"file": "package.json",
"line": "44",
"token": "\"nx\""
},
{
"file": "agents/typescript/package.json",
"line": "13",
"token": "\"@copilotkit/sdk-js\""
},
{
"file": "agents/typescript/package.json",
"line": "23",
"token": "\"langchain\""
},
{
"file": "agents/python/langgraph.json",
"line": "5",
"token": "graphs"
},
{
"file": "agents/python/src/agent.py",
"line": "40",
"token": "LANGGRAPH_FASTAPI"
},
{
"file": "vercel.json",
"line": "2",
"token": "nx run"
},
{
"file": "next.config.mjs",
"line": "3",
"token": "standalone"
},
{
"file": "agents/python/main.py",
"line": "41",
"token": "PORT"
},
{
"file": "agents/python/uv.lock",
"line": "1",
"token": "version"
},
{
"file": "package.json",
"line": "13",
"token": "uv sync"
},
{
"file": "readme.md",
"line": "24",
"token": "cd agent-py"
},
{
"file": "readme.md",
"line": "25",
"token": "poetry install"
},
{
"file": "readme.md",
"line": "60",
"token": "cd ./ui"
},
{
"file": "readme.md",
"line": "77",
"token": "remoteEndpoints"
},
{
"file": "readme.md",
"line": "108",
"token": "./agent-py"
},
{
"file": "wfcms-data.json",
"line": "5",
"token": "live_demo"
}
],
"path": "brain/rules/build-tooling-convention.md",
"absent": "langGraphPlatformEndpoint"
},
{
"id": "copilotkit-runtime-route-convention",
"kind": "rule",
"type": "convention",
"domain": "agent-bridge",
"title": "The /api/copilotkit route is the runtime bridge between browser and LangGraph",
"summary": "One Next.js route handler proxies the browser to the LangGraph agents; it uses EmptyAdapter (agents own the LLM), gates on a public \"api key\", toggles HTTP vs LangGraph-Cloud mode by URL param, and DNS-resolves the lgcDeploymentUrl before trusting it",
"links": [
"agent-name-contract",
"agent-state-shape-contract",
"research-chat-flow",
"env-and-integrations",
"security-posture"
],
"cites": [
{
"file": "src/app/api/copilotkit/route.ts",
"line": "18",
"token": "EmptyAdapter"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "86",
"token": "POST"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "105",
"token": "REMOTE_ACTION_URL"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "107",
"token": "CopilotRuntime"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "135",
"token": "copilotRuntimeNextJSAppRouterEndpoint"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "59",
"token": "allowedDeploymentHost (LGC_DEPLOYMENT_URL)"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "74",
"token": "dns.lookup"
},
{
"file": "src/app/page.tsx",
"line": "28",
"token": "CopilotKit"
},
{
"file": "src/app/page.tsx",
"line": "29",
"token": "runtimeUrl"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "26",
"token": "Boolean(apiKey)"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "87-89",
"token": "Unauthorized / isSafeDeploymentUrl host-check"
}
],
"path": "brain/rules/copilotkit-runtime-route-convention.md"
},
{
"id": "delete-resources-hitl-contract",
"kind": "rule",
"type": "contract",
"domain": "agent-bridge",
"title": "\"DeleteResources\" is the human-in-the-loop tool name shared by agent and frontend",
"summary": "The agent's DeleteResources tool routes to an interrupt, and the frontend renders the confirm dialog via useCopilotAction under the SAME string name — a mismatch means the delete never prompts and silently stalls",
"links": [
"research-chat-flow",
"agent-name-contract",
"add-agent-tool-howto"
],
"cites": [
{
"file": "src/components/ResearchCanvas.tsx",
"line": "38",
"token": "DeleteResources"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "32",
"token": "DeleteResources"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "87",
"token": "DeleteResources"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "156",
"token": "DeleteResources"
},
{
"file": "agents/typescript/src/agent.ts",
"line": "55",
"token": "DeleteResources"
},
{
"file": "agents/typescript/src/chat.ts",
"line": "42",
"token": "DeleteResources"
},
{
"file": "agents/typescript/src/chat.ts",
"line": "43",
"token": "DeleteResources"
},
{
"file": "agents/typescript/src/chat.ts",
"line": "62",
"token": "DeleteResources"
},
{
"file": "agents/typescript/src/chat.ts",
"line": "95",
"token": "DeleteResources"
}
],
"path": "brain/rules/delete-resources-hitl-contract.md",
"anchor": "DeleteResources"
},
{
"id": "design-system-convention",
"kind": "rule",
"type": "convention",
"domain": "components",
"title": "shadcn/ui (new-york) primitives + cn() + hardcoded brand accents",
"summary": "UI is shadcn 'new-york' Radix primitives in components/ui with CSS-variable tokens and the cn() merge helper; feature components layer on top but hardcode the #6766FC/#0E103D brand colors inline rather than as tokens",
"links": [
"nextjs-app-shell-convention"
],
"cites": [
{
"file": "components.json",
"line": "3",
"token": "new-york"
},
{
"file": "src/components/ui/button.tsx",
"line": "7",
"token": "buttonVariants"
},
{
"file": "src/lib/utils.ts",
"line": "4",
"token": "cn"
},
{
"file": "tailwind.config.ts",
"line": "13",
"token": "hsl(var(--background))"
},
{
"file": "src/app/globals.css",
"line": "41",
"token": "--radius"
},
{
"file": "src/components/AddResourceDialog.tsx",
"line": "35",
"token": "#6766FC"
},
{
"file": "src/app/Main.tsx",
"line": "21",
"token": "#0E103D"
},
{
"file": "src/components/ResearchCanvas.tsx",
"line": "1",
"token": "\"use client\""
}
],
"path": "brain/rules/design-system-convention.md"
},
{
"id": "env-and-integrations",
"kind": "rule",
"type": "convention",
"domain": "external-integrations",
"title": "Environment variables and external services this app reads",
"summary": "NEXT_PUBLIC_COPILOTKIT_API_KEY is REQUIRED for the app to function — unset means every POST /api/copilotkit 401s silently; beyond that the runtime route reads LangSmith/deployment keys, each agent reads MODEL + provider keys + TAVILY_API_KEY; OPENAI_API_KEY appears only commented-out in the runtime route while provider-specific keys (Anthropic/xAI/etc.) are read implicitly by the LangChain constructors in this repo's agent code. ANTHROPIC_API_KEY is not present in the cited files; a repo-wide grep is required to prove global absence.",
"links": [
"copilotkit-runtime-route-convention",
"langgraph-agent-convention",
"repo-toolbox",
"security-posture"
],
"cites": [
{
"file": "src/app/api/copilotkit/route.ts",
"line": "25",
"token": "NEXT_PUBLIC_COPILOTKIT_API_KEY"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "19",
"token": "LANGSMITH_API_KEY"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "102",
"token": "LGC_DEPLOYMENT_URL"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "105",
"token": "REMOTE_ACTION_URL"
},
{
"file": "agents/python/src/lib/model.py",
"line": "19",
"token": "MODEL"
},
{
"file": "agents/python/src/lib/model.py",
"line": "42",
"token": "GOOGLE_API_KEY"
},
{
"file": "agents/python/src/lib/model.py",
"line": "26",
"token": "ChatOpenAI"
},
{
"file": "agents/python/src/lib/search.py",
"line": "34",
"token": "TAVILY_API_KEY"
},
{
"file": "agents/python/main.py",
"line": "41",
"token": "PORT"
},
{
"file": "agents/typescript/src/model.ts",
"line": "33",
"token": "GOOGLE_API_KEY"
},
{
"file": "agents/typescript/src/search.ts",
"line": "38",
"token": "TAVILY_API_KEY"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "16",
"token": "OPENAI_API_KEY"
},
{
"file": "src/app/page.tsx",
"line": "33",
"token": "NEXT_PUBLIC_COPILOTKIT_API_KEY"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "26",
"token": "Boolean(apiKey)"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "87-89",
"token": "Unauthorized"
}
],
"path": "brain/rules/env-and-integrations.md",
"absent": "ANTHROPIC_API_KEY"
},
{
"id": "langgraph-agent-convention",
"kind": "rule",
"type": "convention",
"domain": "agent-python",
"title": "LangGraph research agent — node structure, no-op tool schemas, and model routing",
"summary": "The Python agent is a 6-node StateGraph (download→chat→search/delete/fact-check loop); tools are empty @tool schemas the model CALLS but the node handles; the concrete LLM is chosen at runtime by get_model from state.model. The TS agent still mirrors the older 5-node shape (fact_check_node not yet ported, verifiable-report plan decision)",
"links": [
"agent-typescript-parity",
"agent-state-shape-contract",
"delete-resources-hitl-contract",
"research-chat-flow",
"env-and-integrations",
"state-persistence-convention"
],
"cites": [
{
"file": "agents/python/src/agent.py",
"line": "18",
"token": "StateGraph"
},
{
"file": "agents/python/src/agent.py",
"line": "27",
"token": "set_entry_point"
},
{
"file": "agents/python/src/agent.py",
"line": "36",
"token": "interrupt_after"
},
{
"file": "agents/python/src/agent.py",
"line": "40",
"token": "LANGGRAPH_FASTAPI"
},
{
"file": "agents/python/src/agent.py",
"line": "13",
"token": "fact_check_node"
},
{
"file": "agents/python/src/agent.py",
"line": "24",
"token": "fact_check_node"
},
{
"file": "agents/python/src/agent.py",
"line": "31",
"token": "fact_check_node"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "43",
"token": "Literal"
},
{
"file": "agents/python/src/agent.py",
"line": "28",
"token": "add_edge"
},
{
"file": "agents/typescript/src/agent.ts",
"line": "23",
"token": "addConditionalEdges"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "37",
"token": "FactCheckReport"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "88",
"token": "FactCheckReport"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "160",
"token": "FactCheckReport"
},
{
"file": "agents/python/src/lib/fact_check.py",
"line": "44",
"token": "ExtractClaimChecks"
},
{
"file": "agents/python/src/lib/model.py",
"line": "23",
"token": "openai"
},
{
"file": "agents/python/src/lib/model.py",
"line": "49",
"token": "raise ValueError"
},
{
"file": "agents/python/src/lib/search.py",
"line": "35",
"token": "TavilyClient"
},
{
"file": "agents/python/src/lib/download.py",
"line": "30",
"token": "_is_safe_url"
}
],
"path": "brain/rules/langgraph-agent-convention.md",
"anchor": "FactCheckReport"
},
{
"id": "nextjs-app-shell-convention",
"kind": "rule",
"type": "convention",
"domain": "routing-app-shell",
"title": "Next.js App Router shell — a single client-rendered page under a thin RSC layout",
"summary": "The whole app is one route; layout.tsx is the only server component (fonts + metadata), and everything from page.tsx down is \"use client\" because the coagent hooks need the browser",
"links": [
"provider-nesting-contract",
"copilotkit-runtime-route-convention",
"design-system-convention"
],
"cites": [
{
"file": "src/app/layout.tsx",
"line": "22",
"token": "RootLayout"
},
{
"file": "src/app/layout.tsx",
"line": "6",
"token": "localFont"
},
{
"file": "src/app/page.tsx",
"line": "1",
"token": "\"use client\""
},
{
"file": "src/app/Main.tsx",
"line": "8",
"token": "Main"
},
{
"file": "src/app/globals.css",
"line": "15",
"token": "@layer base"
}
],
"path": "brain/rules/nextjs-app-shell-convention.md"
},
{
"id": "provider-nesting-contract",
"kind": "rule",
"type": "contract",
"domain": "routing-app-shell",
"title": "Provider nesting order — ModelSelectorProvider outside CopilotKit outside coagent hooks",
"summary": "useModelSelectorContext throws outside ModelSelectorProvider, and useCoAgent needs CopilotKit above it; the app-shell wrapping order in page.tsx is load-bearing and fails loudly if inverted",
"links": [
"nextjs-app-shell-convention",
"copilotkit-runtime-route-convention",
"agent-name-contract"
],
"cites": [
{
"file": "src/app/page.tsx",
"line": "13",
"token": "ModelSelectorProvider"
},
{
"file": "src/app/page.tsx",
"line": "21",
"token": "useModelSelectorContext"
},
{
"file": "src/app/page.tsx",
"line": "28",
"token": "CopilotKit"
},
{
"file": "src/app/page.tsx",
"line": "36",
"token": "Main"
},
{
"file": "src/lib/model-selector-provider.tsx",
"line": "66",
"token": "throw new Error"
}
],
"path": "brain/rules/provider-nesting-contract.md",
"anchor": "none"
},
{
"id": "repo-toolbox",
"kind": "rule",
"type": "convention",
"domain": "toolbox",
"title": "The committed agent toolbox — rafa skills, harness-neutral .agents skills, the rafinery MCP server, and permissions",
"summary": "The repo ships the rafa engineering SOP as 13 .claude skills + 5 agent cards + the /rafa command, PLUS 8 harness-neutral skills under .agents/skills (Claude Code · Codex · Cursor all read these; rafa.json records 7 installed versions — dev-loop is authored here, not installed), pinned to @rafinery/cli 0.18.5 and wired to the rafinery knowledge MCP over HTTP with a bearer key from env; scan/plan/build are DRIVEN by the rafa run ladder and brain recall is query-first, graded verified > derived > authored",
"links": [
"env-and-integrations",
"security-posture"
],
"cites": [
{
"file": ".claude/skills/rafa-scan/SKILL.md",
"line": "2",
"token": "rafa-scan"
},
{
"file": ".claude/commands/rafa.md",
"line": "2",
"token": "version"
},
{
"file": ".claude/commands/rafa.md",
"line": "220",
"token": "The driver"
},
{
"file": ".claude/commands/rafa.md",
"line": "240",
"token": ".rafa/runs/"
},
{
"file": ".mcp.json",
"line": "3",
"token": "rafinery"
},
{
"file": ".mcp.json",
"line": "7",
"token": "RAFA_MCP_KEY"
},
{
"file": ".claude/settings.json",
"line": "5",
"token": "@rafinery/cli"
},
{
"file": ".claude/settings.json",
"line": "4",
"token": "Read(.rafa"
},
{
"file": ".claude/settings.json",
"line": "10",
"token": "SessionStart"
},
{
"file": ".agents/skills/tdd/SKILL.md",
"line": "2",
"token": "tdd"
},
{
"file": ".agents/skills/vercel-composition-patterns/SKILL.md",
"line": "2",
"token": "vercel-composition-patterns"
},
{
"file": ".claude/rafa/contract.md",
"line": "642",
"token": "The query plane"
},
{
"file": ".claude/rafa/contract.md",
"line": "617",
"token": "query_schema"
},
{
"file": "rafa.json",
"line": "11",
"token": "skills"
},
{
"file": "rafa.json",
"line": "8",
"token": "0.18.5"
}
],
"path": "brain/rules/repo-toolbox.md"
},
{
"id": "resource-text-dominates-the-system-prompt",
"kind": "rule",
"type": "contract",
"domain": "agent-bridge",
"title": "Every resource's full page text is re-sent in the system prompt on every turn — capped in TypeScript, still uncapped in Python",
"summary": "Both chat nodes re-serialise all downloaded resources into the system prompt each turn, so one long article can exceed an entire per-minute token budget in a single request; the TS agent caps this (8k/resource, 24k total) as of 2026-07-28, the Python agent does NOT. Rule: any new node that injects resource text into a prompt MUST budget it (per-resource and aggregate caps, like MAX_TOTAL_RESOURCE_CHARS in the TypeScript agent) — the missing Python cap is an open P1 improvement, not permission to skip the cap",
"links": [
"langgraph-agent-convention",
"agent-typescript-parity",
"research-chat-flow"
],
"cites": [
{
"file": "agents/typescript/src/chat.ts",
"line": "114",
"token": "JSON.stringify(resources)"
},
{
"file": "agents/typescript/src/chat.ts",
"line": "21",
"token": "MAX_TOTAL_RESOURCE_CHARS"
},
{
"file": "agents/typescript/src/chat.ts",
"line": "73",
"token": "MAX_TOTAL_RESOURCE_CHARS"
},
{
"file": "agents/typescript/src/download.ts",
"line": "17",
"token": "MAX_RESOURCE_CHARS"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "109",
"token": "{resources}"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "74",
"token": "resources.append"
},
{
"file": "agents/python/src/lib/download.py",
"line": "78",
"token": "_RESOURCE_CACHE"
}
],
"path": "brain/rules/resource-text-dominates-the-system-prompt.md",
"anchor": "MAX_TOTAL_RESOURCE_CHARS"
},
{
"id": "state-persistence-convention",
"kind": "rule",
"type": "convention",
"domain": "data-persistence",
"title": "Nothing is persisted — resource text and graph checkpoints live in process memory, and the sqlite checkpointer deps are never used",
"summary": "There is no database, ORM, or client-side storage anywhere in this app: downloaded page text sits in a module-level _RESOURCE_CACHE dict and conversation state in an in-memory MemorySaver, so a backend restart silently empties both; langgraph-checkpoint-sqlite/aiosqlite are declared dependencies whose savers are never imported, and the TS agent compiles with no checkpointer at all",
"links": [
"langgraph-agent-convention",
"agent-typescript-parity",
"agent-state-shape-contract",
"research-chat-flow",
"resource-text-dominates-the-system-prompt"
],
"cites": [
{
"file": "agents/python/src/lib/download.py",
"line": "17",
"token": "_RESOURCE_CACHE"
},
{
"file": "agents/python/src/lib/download.py",
"line": "24",
"token": "_RESOURCE_CACHE"
},
{
"file": "agents/python/src/lib/download.py",
"line": "66",
"token": "_RESOURCE_CACHE"
},
{
"file": "agents/python/src/lib/download.py",
"line": "78",
"token": "_RESOURCE_CACHE"
},
{
"file": "agents/python/src/lib/download.py",
"line": "81",
"token": "_RESOURCE_CACHE"
},
{
"file": "agents/python/src/agent.py",
"line": "40",
"token": "LANGGRAPH_FASTAPI"
},
{
"file": "agents/python/src/agent.py",
"line": "45",
"token": "MemorySaver"
},
{
"file": "agents/python/src/agent.py",
"line": "47",
"token": "MemorySaver"
},
{
"file": "agents/python/src/agent.py",
"line": "48",
"token": "checkpointer"
},
{
"file": "agents/python/main.py",
"line": "12",
"token": "LANGGRAPH_FASTAPI"
},
{
"file": "agents/python/pyproject.toml",
"line": "24",
"token": "langgraph-checkpoint-sqlite"
},
{
"file": "agents/python/pyproject.toml",
"line": "25",
"token": "aiosqlite"
},
{
"file": "agents/typescript/src/agent.ts",
"line": "8",
"token": "MemorySaver"
},
{
"file": "agents/typescript/src/agent.ts",
"line": "33",
"token": "workflow.compile"
},
{
"file": "agents/python/src/lib/fact_check.py",
"line": "80",
"token": "get_resource"
},
{
"file": "src/components/ResearchCanvas.tsx",
"line": "87",
"token": "setState"
}
],
"path": "brain/rules/state-persistence-convention.md",
"anchor": "_RESOURCE_CACHE",
"absent": "drizzle"
}
],
"improvements": [
{
"id": "body-font-override-negates-geist",
"priority": "P3",
"category": "product",
"lens": "product",
"status": "open",
"title": "globals.css hardcodes Arial on body, negating the two Geist fonts loaded in layout",
"summary": "layout.tsx loads Geist Sans + Geist Mono via next/font/local and wires the CSS variables, but globals.css sets body font-family to Arial, so the loaded fonts never apply to body text — dead weight plus off-brand typography",
"fix": "Set body font-family to var(--font-geist-sans) (or drop the Geist loading if Arial is intended) (~5 min)",
"leverage": {
"impact": "low",
"effort": "low"
},
"blastRadius": [
"routing-app-shell",
"components"
],
"cites": [
{
"file": "src/app/globals.css",
"line": "6",
"token": "Arial"
},
{
"file": "src/app/layout.tsx",
"line": "6",
"token": "localFont"
}
],
"path": "improve/improvements/body-font-override-negates-geist.md"
},
{
"id": "coagent-initialstate-divergence",
"priority": "P2",
"category": "architecture",
"lens": "architecture",
"status": "open",
"title": "Two useCoAgent hooks for the same agent seed different initialState",
"summary": "Main.tsx seeded the full AgentState (model, research_question, resources, report, logs) while ResearchCanvas.tsx seeded only { model } for the same agent name — a silent inconsistency where the initial shape depended on mount order; both now call one shared createInitialAgentState factory",
"fix": "Share one initialState (extract a constant or seed the full shape in both) so the two hooks agree (~10 min)",
"leverage": {
"impact": "medium",
"effort": "low"
},
"blastRadius": [
"state",
"agent-bridge"
],
"cites": [
{
"file": "src/lib/types.ts",
"line": "32",
"token": "createInitialAgentState"
},
{
"file": "src/app/Main.tsx",
"line": "12",
"token": "createInitialAgentState"
},
{
"file": "src/components/ResearchCanvas.tsx",
"line": "24",
"token": "createInitialAgentState"
}
],
"path": "improve/improvements/coagent-initialstate-divergence.md"
},
{
"id": "copilotkit-runtime-transitive-cves",
"priority": "P2",
"category": "security",
"lens": "security",
"status": "open",
"title": "Three advisories ride in under @copilotkit/runtime — uuid, @hono/node-server, @ai-sdk/provider-utils",
"summary": "The CopilotKit runtime that backs the app's only server route pulls uuid@10.0.0 (missing buffer bounds check), @hono/node-server@1.19.15 (Windows path traversal in serve-static) and @ai-sdk/provider-utils@3.0.30 (uncontrolled resource consumption, no fix published); the caret range on @copilotkit/runtime means a lockfile refresh may clear the first two",
"fix": "Refresh the lockfile within the existing `^1.63.2` range (`pnpm update @copilotkit/runtime`), then re-run `npx @rafinery/cli audit`; add pnpm.overrides for uuid >= 13.0.1 / @hono/node-server >= 2.0.5 only if the refresh does not move them (~15 min)",
"leverage": {
"impact": "low",
"effort": "low"
},
"blastRadius": [
"agent-bridge",
"external-integrations",
"security"
],
"cites": [
{
"file": "package.json",
"line": "19",
"token": "@copilotkit/runtime"
},
{
"file": "pnpm-lock.yaml",
"line": "3669",
"token": "uuid@10.0.0"
},
{
"file": "pnpm-lock.yaml",
"line": "438",
"token": "'@hono/node-server@1.19.15'"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "7",
"token": "@copilotkit/runtime"
}
],
"path": "improve/improvements/copilotkit-runtime-transitive-cves.md"
},
{
"id": "default-nextjs-metadata",
"priority": "P2",
"category": "product",
"lens": "product",
"status": "open",
"title": "App ships the default \"Create Next App\" title and description to production",
"summary": "layout.tsx carried the create-next-app boilerplate metadata, so the browser tab, SEO, and social embeds all read \"Create Next App\" / \"Generated by create next app\" — now replaced with the real product title",
"fix": "Set metadata.title/description in layout.tsx to the real product (\"Research Helper\") (~2 min)",
"leverage": {
"impact": "high",
"effort": "low"
},
"blastRadius": [
"routing-app-shell"
],
"cites": [
{
"file": "src/app/layout.tsx",
"line": "18",
"token": "Research Helper"
},
{
"file": "src/app/layout.tsx",
"line": "19",
"token": "AI-assisted research canvas"
}
],
"path": "improve/improvements/default-nextjs-metadata.md"
},
{
"id": "download-error-sentinel-blocks-retry",
"priority": "P1",
"category": "correctness",
"lens": "correctness",
"status": "open",
"title": "The \"ERROR\" cache sentinel is truthy, so a URL that fails to download once is never retried",
"summary": "_download_resource writes the literal string \"ERROR\" into _RESOURCE_CACHE on any failure, but download_node's re-download test is `if not get_resource(url)` — \"ERROR\" is a non-empty (truthy) string, so one transient network blip permanently poisons that resource for the life of the process while the UI reports the download as done",
"fix": "Test for real content in download_node — `if get_resource(url) in (\"\", \"ERROR\")` — so errored URLs are retried instead of being cached as permanent failures (~10 min)",
"leverage": {
"impact": "high",
"effort": "low"
},
"blastRadius": [
"agent-python",
"data-persistence"
],
"cites": [
{
"file": "agents/python/src/lib/download.py",
"line": "97",
"token": "if not get_resource"
},
{
"file": "agents/python/src/lib/download.py",
"line": "24",
"token": "_RESOURCE_CACHE.get"
},
{
"file": "agents/python/src/lib/download.py",
"line": "66",
"token": "\"ERROR\""
},
{
"file": "agents/python/src/lib/download.py",
"line": "81",
"token": "\"ERROR\""
},
{
"file": "agents/python/src/lib/chat.py",
"line": "72",
"token": "content == \"ERROR\""
},
{
"file": "agents/python/src/lib/fact_check.py",
"line": "81",
"token": "(\"\", \"ERROR\")"
}
],
"path": "improve/improvements/download-error-sentinel-blocks-retry.md"
},
{
"id": "fact-check-node-unguarded-toolcall",
"priority": "P2",
"category": "correctness",
"lens": "correctness",
"status": "open",
"title": "fact_check_node indexes tool_calls[0] unguarded at four sites — the only node that breaks the repo's own guard convention",
"summary": "search_node and perform_delete_node both check `if ai_message.tool_calls` before indexing, but fact_check_node — the newest node — reads ai_message.tool_calls[0][\"id\"] at four sites with no guard, so any path that reaches it without a live tool call raises IndexError and aborts the graph run",
"fix": "Hoist one guard at the top of fact_check_node (bail early if `not ai_message.tool_calls`) and reuse the resolved id, mirroring search_node's shape (~10 min)",
"leverage": {
"impact": "medium",
"effort": "low"
},
"blastRadius": [
"agent-python"
],
"cites": [
{
"file": "agents/python/src/lib/fact_check.py",
"line": "65",
"token": "tool_calls[0]"
},
{
"file": "agents/python/src/lib/fact_check.py",
"line": "114",
"token": "tool_calls[0]"
},
{
"file": "agents/python/src/lib/fact_check.py",
"line": "129",
"token": "tool_calls[0]"
},
{
"file": "agents/python/src/lib/fact_check.py",
"line": "141",
"token": "tool_calls[0]"
},
{
"file": "agents/python/src/lib/search.py",
"line": "67",
"token": "if not ai_message.tool_calls"
},
{
"file": "agents/python/src/lib/delete.py",
"line": "26",
"token": "if ai_message.tool_calls"
}
],
"path": "improve/improvements/fact-check-node-unguarded-toolcall.md"
},
{
"id": "frontend-state-any-typing",
"priority": "P2",
"category": "architecture",
"lens": "architecture",
"status": "open",
"title": "AgentState types resources/logs as any[] while concrete types exist",
"summary": "The frontend AgentState declared resources and logs as any[], erasing type safety across the whole cross-process state contract even though Resource was defined in the same file and a Log shape existed on the backend; both are now concretely typed",
"fix": "Type resources as Resource[] and add a Log type for logs in types.ts (~5 min)",
"leverage": {
"impact": "medium",
"effort": "low"
},
"blastRadius": [
"state",
"components"
],
"cites": [
{
"file": "src/lib/types.ts",
"line": "23",
"token": "resources: Resource[]"
},
{
"file": "src/lib/types.ts",
"line": "24",
"token": "logs: Log[]"
},
{
"file": "src/lib/types.ts",
"line": "14",
"token": "export type Log"
}
],
"path": "improve/improvements/frontend-state-any-typing.md"
},
{
"id": "lgc-deployment-url-key-exfiltration",
"priority": "P1",
"category": "security",
"lens": "security",
"status": "fixed",
"title": "The SSRF guard blocks private targets but not public ones — any caller can redirect the proxy at their own host and receive LANGSMITH_API_KEY",
"summary": "?lgcDeploymentUrl= is read straight from the request URL and, when set, becomes the LangGraph SDK client's apiUrl with langsmithApiKey as its apiKey; isSafeDeploymentUrl only rejects hosts resolving to private addresses, so https://attacker.example passes and the server sends the LangSmith key to it — and the x-api-key gate in front of it is documented as not being access control. Fixed: isSafeDeploymentUrl now also requires the hostname to match LGC_DEPLOYMENT_URL's host, and page.tsx encodes the param.",
"fix": "Allowlist the deployment host instead of only denying private ones — compare the parsed hostname against LGC_DEPLOYMENT_URL's host (or an explicit env allowlist) and 400 on anything else; while in page.tsx, wrap the value in encodeURIComponent (~15 min)",
"leverage": {
"impact": "high",
"effort": "low"
},
"blastRadius": [
"agent-bridge",
"external-integrations",
"security"
],
"cites": [
{
"file": "src/app/api/copilotkit/route.ts",
"line": "109",
"token": "lgcDeploymentUrl"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "19",
"token": "LANGSMITH_API_KEY"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "139",
"token": "deploymentUrl"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "140",
"token": "langsmithApiKey"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "98",
"token": "addresses.every"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "22",
"token": "visible to anyone via devtools"
},
{
"file": "src/lib/model-selector-provider.tsx",
"line": "40",
"token": "lgcDeploymentUrl"
},
{
"file": "src/app/page.tsx",
"line": "24",
"token": "lgcDeploymentUrl"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "60",
"token": "allowedDeploymentHost"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "86",
"token": "hostname !== allowedDeploymentHost"
}
],
"path": "improve/improvements/lgc-deployment-url-key-exfiltration.md"
},
{
"id": "lgc-deployment-url-ssrf-asymmetry",
"priority": "P2",
"category": "security",
"lens": "security",
"status": "fixed",
"title": "The lgcDeploymentUrl SSRF guard checks host strings only — no DNS resolution, unlike the Python side",
"summary": "isSafeDeploymentUrl blocked private hostnames by literal/regex pattern but never resolved DNS, so a public hostname pointing at a private/metadata IP passed and the proxy forwarded langsmithApiKey to it — now fixed; the guard resolves DNS and checks every resolved address, mirroring download.py",
"fix": "Resolve the hostname and reject when any resolved address is private/loopback/link-local, mirroring download.py's _is_safe_url (~15 min)",
"leverage": {
"impact": "medium",
"effort": "medium"
},
"blastRadius": [
"agent-bridge",
"external-integrations",
"security"
],
"cites": [
{
"file": "src/app/api/copilotkit/route.ts",
"line": "59",
"token": "isSafeDeploymentUrl"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "74",
"token": "dns.lookup"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "29",
"token": "isPrivateIPv4"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "43",
"token": "isPrivateIPv6"
},
{
"file": "agents/python/src/lib/download.py",
"line": "41",
"token": "getaddrinfo"
}
],
"path": "improve/improvements/lgc-deployment-url-ssrf-asymmetry.md"
},
{
"id": "next-dependency-cves",
"priority": "P1",
"category": "security",
"lens": "security",
"status": "open",
"title": "next@15.5.15 carries 21 open advisories (10 high) — fixed only by the 16.x line",
"summary": "The exact-pinned direct dependency next@15.5.15 has 21 OSV advisories against it (10 high / 9 moderate / 2 low), spanning middleware-bypass, SSRF in Server Actions and rewrites, cache poisoning, XSS and DoS; every one is cleared by upgrading to >= 16.2.11",
"fix": "Bump the exact pin `\"next\": \"15.5.15\"` to >= 16.2.11 (a 15 -> 16 major; read the upgrade guide, then re-run `npx @rafinery/cli audit`) (~half a day)",
"leverage": {
"impact": "high",
"effort": "high"
},
"blastRadius": [
"routing-app-shell",
"agent-bridge",
"security",
"build-tooling"
],
"cites": [
{
"file": "package.json",
"line": "27",
"token": "next"
},
{
"file": "pnpm-lock.yaml",
"line": "2816",
"token": "next@15.5.15"
},
{
"file": "src/app/api/copilotkit/route.ts",
"line": "86",
"token": "export const POST"
}
],
"path": "improve/improvements/next-dependency-cves.md"
},
{
"id": "next-transitive-postcss-sharp-cves",
"priority": "P1",
"category": "security",
"lens": "security",
"status": "open",
"title": "postcss@8.4.31 and sharp@0.34.5 pulled in under next carry 4 advisories (3 high)",
"summary": "Two packages resolved transitively through next@15.5.15 are vulnerable — postcss 8.4.31 (arbitrary file read, path traversal in source-map auto-loading, XSS in stringify output) and sharp 0.34.5 (inherited libvips CVEs); both are fixable now with a pnpm override, without waiting on the next 15 -> 16 major",
"fix": "Add pnpm.overrides for `postcss: \">=8.5.18\"` and `sharp: \">=0.35.0\"`, reinstall, re-run `npx @rafinery/cli audit` (~15 min)",
"leverage": {
"impact": "medium",
"effort": "low"
},
"blastRadius": [
"build-tooling",
"security"
],
"cites": [
{
"file": "pnpm-lock.yaml",
"line": "3045",
"token": "postcss@8.4.31"
},
{
"file": "pnpm-lock.yaml",
"line": "3349",
"token": "sharp@0.34.5"
},
{
"file": "package.json",
"line": "39",
"token": "postcss"
},
{
"file": "postcss.config.mjs",
"line": "4",
"token": "tailwindcss"
}
],
"path": "improve/improvements/next-transitive-postcss-sharp-cves.md"
},
{
"id": "parallel-tool-calls-openai-only",
"priority": "P2",
"category": "correctness",
"lens": "correctness",
"status": "open",
"title": "parallel_tool_calls is disabled for ChatOpenAI only, but chat_node answers just tool_calls[0] for every provider",
"summary": "chat_node suppresses parallel tool calls by class-name check on ChatOpenAI alone, then handles the response by reading tool_calls[0] and emitting exactly one ToolMessage; on anthropic/google_genai/grok the model may return several tool calls in one message, leaving the rest without a matching tool result and the next turn rejected by the provider",
"fix": "Disable parallel tool calls for the other providers too (ChatAnthropic accepts it via bind_tools, Gemini via tool_config), or emit a ToolMessage for every entry in tool_calls rather than only [0] (~15 min)",
"leverage": {
"impact": "medium",
"effort": "low"
},
"blastRadius": [
"agent-python",
"agent-bridge"
],
"cites": [
{
"file": "agents/python/src/lib/chat.py",
"line": "79",
"token": "ChatOpenAI"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "80",
"token": "parallel_tool_calls"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "120",
"token": "tool_calls[0]"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "129",
"token": "tool_calls[0]"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "153",
"token": "tool_calls[0]"
},
{
"file": "agents/python/src/lib/model.py",
"line": "30",
"token": "ChatAnthropic"
},
{
"file": "agents/python/src/lib/model.py",
"line": "39",
"token": "ChatGoogleGenerativeAI"
}
],
"path": "improve/improvements/parallel-tool-calls-openai-only.md"
},
{
"id": "python-resource-text-uncapped",
"priority": "P0",
"category": "performance",
"lens": "performance",
"status": "fixed",
"title": "The TypeScript agent caps resource text in the system prompt; the Python agent — the one that actually runs — does not",
"summary": "chat_node serialises every downloaded resource's full page text into the system prompt on every turn with no truncation and no aggregate budget, so one long article can exceed an entire per-minute token allowance in a single request and 429 unrecoverably; the TS port fixed exactly this with MAX_RESOURCE_CHARS/MAX_TOTAL_RESOURCE_CHARS, but npm run dev starts the Python backend",
"fix": "Port the TS caps to Python — truncate each cached page to ~8000 chars with a [truncated] marker in download.py, and budget the aggregate to ~24000 chars in chat_node's resource loop (~20 min)",
"leverage": {
"impact": "high",
"effort": "low"
},
"blastRadius": [
"agent-python",
"agent-bridge",
"data-persistence"
],
"cites": [
{
"file": "agents/python/src/lib/chat.py",
"line": "109",
"token": "{resources}"
},
{
"file": "agents/python/src/lib/chat.py",
"line": "87",
"token": "resources.append"
},
{
"file": "agents/python/src/lib/download.py",
"line": "78",
"token": "_RESOURCE_CACHE"
},
{
"file": "agents/typescript/src/chat.ts",
"line": "21",
"token": "MAX_TOTAL_RESOURCE_CHARS"
},
{
"file": "agents/typescript/src/download.ts",
"line": "17",
"token": "MAX_RESOURCE_CHARS"
},
{
"file": "package.json",
"line": "11",
"token": "dev:agent:py"
}
],
"path": "improve/improvements/python-resource-text-uncapped.md"
},
{
"id": "resource-cache-unbounded",
"priority": "P3",
"category": "performance",
"lens": "performance",
"status": "open",
"title": "_RESOURCE_CACHE grows unbounded for the life of the process",
"summary": "Downloaded page content is memoized in a module-level dict that is never evicted or size-capped, so a long-running agent process accumulates every fetched URL's full markdown in memory",
"fix": "Cap the cache (LRU / max entries) or add a size/TTL bound in download.py (~15 min)",
"leverage": {
"impact": "low",
"effort": "medium"
},
"blastRadius": [
"agent-python",
"data-persistence"
],
"cites": [
{
"file": "agents/python/src/lib/download.py",
"line": "17",
"token": "_RESOURCE_CACHE"
}
],
"path": "improve/improvements/resource-cache-unbounded.md"
},
{
"id": "search-node-unguarded-toolcall",
"priority": "P1",
"category": "correctness",
"lens": "correctness",
"status": "open",
"title": "search_node assumes the model always returns a tool call — an empty response kills the run",
"summary": "search.py indexed tool_calls[0] with no guard; if a provider ignored the forced tool_choice (or returned text/refusal), an unhandled IndexError aborted the primary research flow mid-conversation — both indexing sites now carry an early-return guard",
"fix": "Guard tool_calls before indexing in search_node — if empty, emit a graceful log/message and return instead of throwing (~10 min)",
"leverage": {
"impact": "medium",
"effort": "low"
},
"blastRadius": [
"agent-python",
"agent-bridge"
],
"cites": [
{
"file": "agents/python/src/lib/search.py",
"line": "67",
"token": "if not ai_message.tool_calls"
},
{
"file": "agents/python/src/lib/search.py",
"line": "139",
"token": "if not ai_message_response.tool_calls"
}
],
"path": "improve/improvements/search-node-unguarded-toolcall.md"
},
{
"id": "stale-model-pins",
"priority": "P1",
"category": "ops",
"lens": "ops",
"status": "open",
"title": "The anthropic dropdown option is already dead — claude-3-5-sonnet-20240620 was retired and now 404s",
"summary": "get_model pins claude-3-5-sonnet-20240620, which Anthropic retired on 2025-10-28 and which now returns 404 on every request, so selecting the anthropic model in the picker fails at call time today — not eventually; gemini-1.5-pro is in the same aging class and both pins are duplicated verbatim in the TypeScript port",
"fix": "Replace the retired Anthropic pin with a current model id (claude-sonnet-5 is the documented replacement for this snapshot), re-check the Gemini pin against Google's current list, and update agents/typescript/src/model.ts in the same edit (~10 min)",
"leverage": {
"impact": "high",
"effort": "low"
},
"blastRadius": [
"agent-python",
"agent-typescript",
"external-integrations"
],
"cites": [
{
"file": "agents/python/src/lib/model.py",
"line": "32",
"token": "claude-3-5-sonnet-20240620"
},
{
"file": "agents/python/src/lib/model.py",
"line": "41",
"token": "gemini-1.5-pro"
},
{
"file": "agents/typescript/src/model.ts",
"line": "26",
"token": "claude-3-5-sonnet-20240620"
},
{
"file": "agents/typescript/src/model.ts",
"line": "32",
"token": "gemini-1.5-pro"
}
],
"path": "improve/improvements/stale-model-pins.md"
},
{
"id": "submit-message-settimeout-race",
"priority": "P3",
"category": "correctness",
"lens": "correctness",
"status": "open",
"title": "Chat submit clears logs then waits a hardcoded 30ms to sequence the state update",
"summary": "onSubmitMessage clears coagent logs then awaits an arbitrary 30ms setTimeout to let the state flush before the run starts — a timing hack that can race on a slow render and leave stale logs visible for the first frames",
"fix": "Sequence off the actual state commit rather than a fixed delay, or document why 30ms is required (~10 min)",
"leverage": {
"impact": "low",
"effort": "low"
},
"blastRadius": [
"agent-bridge",
"components"
],
"cites": [
{
"file": "src/app/Main.tsx",
"line": "49",
"token": "setState"
},
{
"file": "src/app/Main.tsx",
"line": "50",
"token": "setTimeout"
}
],
"path": "improve/improvements/submit-message-settimeout-race.md"
}
]
}