Files
openswarm/backend/apps/outputs/outputs.py
T
Arnav NavalandCursor 64dfd50d12 [arnav] remove dead code identified by audit
Each removal verified by checking actual production callers (frontend,
electron, internal HTTP, MCP-server subprocesses) — not just test
references. Symbols whose only callers were tests are removed along
with those tests.

Production removals (~390 LOC):

- backend/main.py
  - websocket_session: drop `agent:edit_message` WS branch. Frontend
    only ever uses HTTP `POST /api/agents/sessions/{id}/edit_message`
    (frontend/src/shared/state/agentsSlice.ts); nothing on the wire
    sends a WS message of this type.

- backend/apps/agents/agent_manager.py
  - AgentManager._build_connected_tools_context (~80 LOC): zero call
    sites in production; the connected-tools system-prompt context is
    built inline in _compose_system_prompt now.
  - AgentManager._approx_tokens / _summarize_message_block: pure
    helpers whose only callers were tests. The compaction path uses
    LLM-driven _maybe_compact instead.

- backend/apps/agents/browser_agent.py
  - clear_browser_history: only used by tests. _browser_history is
    pruned via the size cap inline.
  - MODEL_MAP constant: never read.

- backend/apps/agents/mcp_preflight.py
  - DISCOVERY_SCAFFOLDING (~25-line system-prompt block): defined but
    never appended anywhere. The header comment described an intended
    use that the codebase no longer has.

- backend/apps/agents/providers/registry.py
  - thinking_params_for, _is_9router_available, OPENROUTER_BASE_URL,
    get_context_window: zero callers in production. Thinking-params
    routing is done by the provider classes directly; 9Router presence
    is detected at request time; context-window numbers are stamped
    onto sessions from BUILTIN_MODELS at launch.

- backend/apps/agents/tools/{base,web}.py
  - BaseTool.get_schema (abstract) + WebSearchTool/WebFetchTool
    overrides: production code in backend/apps/web/web.py instantiates
    these tools and only calls .execute(); the JSON-schema lives in
    the HTTP wrapper, not on the tool class.

- backend/apps/outputs/outputs.py
  - _resolve_model + MODEL_MAP: tests-only.
  - load_output: docstring claimed it was a public helper for "other
    modules" but no module imported it.

- backend/apps/service/client.py
  - set_user_id, the _user_id module global, and the dead cache short-
    circuit in _get_user_id: setter was tests-only. _get_user_id now
    reads user_email directly from settings on every call.

- backend/apps/settings/credentials.py
  - get_provider_credentials: zero callers. The sibling get_agent_sdk_env
    is kept (it has the explicit "Legacy helpers" keep-comment).

Test updates:

- test_agent_manager_unit.py: drop _approx_tokens / _summarize_message_block
  cases (5 tests), update module docstring index.
- test_browser_agent_unit.py: drop clear_browser_history cases (2 tests)
  and the unused _Boom helper class in the repr-fallback test.
- test_outputs_unit.py: drop _resolve_model / load_output cases
  (4 tests), update docstring + import list.
- test_v2_invariants.py: drop get_context_window tests + get_schema
  assertions on web tools (kept name + BaseTool inheritance checks).
- test_service.py: rewrite the 4 set_user_id-driven tests to drive
  user_id through settings.user_email instead, so _get_user_id's live
  envelope-stamping path stays covered.

Verification:
- ruff --select F401,F811,F841 backend/  →  clean.
- pytest backend/tests/ → 1167 passed, 1 deselected (pre-existing
  sandbox git test, unrelated). No tests dropped silently — every
  deletion is paired with the corresponding test removal/rewrite.
- Dead-code scan re-run: dead WS events 1→0, Tier-2 high-confidence
  14→11 (residue is SDK-callback `context` params + Pydantic `cls`
  validators — both false positives vulture can't see through),
  vulture total 165→145.

Total diff: -565 / +34 LOC across 15 files.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 18:49:30 -05:00

573 lines
21 KiB
Python

import json
import os
import re
import logging
import mimetypes
import base64
from datetime import datetime
from contextlib import asynccontextmanager
from fastapi import HTTPException
from fastapi.responses import Response
from backend.auth import get_auth_token
from jsonschema import validate as schema_validate, ValidationError as SchemaValidationError
from backend.config.Apps import SubApp
from backend.apps.outputs.models import (
Output, OutputCreate, OutputUpdate, OutputExecute, OutputExecuteResult,
AutoRunRequest, AutoRunConfig, AutoRunAgentRequest,
WorkspaceSeedRequest,
)
from backend.apps.outputs.executor import execute_backend_code
from backend.apps.outputs.view_builder_templates import VIEW_BUILDER_SKILL, VIEW_TEMPLATE_FILES
from backend.apps.settings.settings import load_settings
logger = logging.getLogger(__name__)
def _get_anthropic_client(api_model: str | None = None):
"""Create an AsyncAnthropic client using the API key from app settings.
When `api_model` is provided and carries a 9Router prefix (cc/, cx/, gc/),
the client is pointed at 9Router so non-Anthropic aux calls don't 400 on
api.anthropic.com. Without an api_model we fall back to the default
connection-mode-driven client.
"""
from backend.apps.settings.credentials import (
get_anthropic_client,
get_anthropic_client_for_model,
)
settings = load_settings()
if api_model:
return get_anthropic_client_for_model(settings, api_model)
return get_anthropic_client(settings)
def _validate_against_schema(data: dict, schema: dict) -> str | None:
"""Validate *data* against *schema*. Return an error string or None."""
try:
schema_validate(instance=data, schema=schema)
return None
except SchemaValidationError as exc:
path = " -> ".join(str(p) for p in exc.absolute_path) if exc.absolute_path else "(root)"
return f"Schema validation failed at {path}: {exc.message}"
from backend.config.paths import OUTPUTS_DIR as DATA_DIR, OUTPUTS_WORKSPACE_DIR as WORKSPACE_DIR
def _build_data_injection(input_json: str, result_json: str) -> str:
"""Build a <script> tag that sets OUTPUT_INPUT / OUTPUT_BACKEND_RESULT
and listens for postMessage updates."""
return (
"<script>\n"
"(function() {\n"
" window.OUTPUT_INPUT = " + input_json + ";\n"
" window.OUTPUT_BACKEND_RESULT = " + result_json + ";\n"
" window.addEventListener('message', function(e) {\n"
" if (e.data && e.data.type === 'OUTPUT_DATA') {\n"
" window.OUTPUT_INPUT = e.data.input || {};\n"
" window.OUTPUT_BACKEND_RESULT = e.data.backendResult || null;\n"
" window.dispatchEvent(new CustomEvent('output-data-ready'));\n"
" }\n"
" });\n"
"})();\n"
"</script>"
)
def _inject_data_into_html(html: str, input_json: str = "{}", result_json: str = "null") -> str:
injection = _build_data_injection(input_json, result_json)
if "</head>" in html:
return html.replace("</head>", f"{injection}\n</head>", 1)
if "<body" in html:
return html.replace("<body", f"{injection}\n<body", 1)
return f"{injection}\n{html}"
# URL schemes / prefixes that must NOT have ?token= appended. These are either
# external (CDNs, mailto) or non-network references that the auth middleware
# never sees. Anything else is treated as a same-origin relative URL pointing
# at our /api/outputs/.../serve/ subtree, which DOES need the token.
_ABSOLUTE_URL_PREFIXES = (
"http://", "https://", "//", "data:", "blob:",
"mailto:", "tel:", "javascript:", "about:", "#",
)
_HREF_SRC_ATTR_RE = re.compile(
r"""(\s(?:href|src))\s*=\s*(["'])([^"']+)\2""",
re.IGNORECASE,
)
def _inject_token_into_relative_urls(html: str, token: str) -> str:
"""Append `?token=<t>` to every relative href/src in the served HTML.
Browsers strip the parent iframe URL's query string before resolving
relative `<link href="styles.css">` / `<script src="x.js">`, so without
this rewrite the sub-resource fetch lands at the auth middleware with no
credentials and gets a 401. Idempotent: skips URLs that already carry a
`token=` param. Skips absolute URLs (CDN, data:, etc.) — see prefix list.
"""
if not token:
return html
def _patch(match: re.Match) -> str:
attr, quote, url = match.group(1), match.group(2), match.group(3)
lowered = url.lower().lstrip()
if lowered.startswith(_ABSOLUTE_URL_PREFIXES):
return match.group(0)
if "token=" in url:
return match.group(0)
# Split off any hash fragment so `?token=` lands in the query, not in
# the fragment: `page.html?v=1#sec` → `page.html?v=1&token=X#sec`.
hash_idx = url.find("#")
if hash_idx >= 0:
base, frag = url[:hash_idx], url[hash_idx:]
else:
base, frag = url, ""
sep = "&" if "?" in base else "?"
return f'{attr}={quote}{base}{sep}token={token}{frag}{quote}'
return _HREF_SRC_ATTR_RE.sub(_patch, html)
def _decode_data_param(d: str) -> tuple[str, str]:
"""Decode the base64-encoded _d query param into (input_json, result_json)."""
try:
decoded = json.loads(base64.b64decode(d))
input_json = json.dumps(decoded.get("i", {}))
result_json = json.dumps(decoded.get("r", None))
return input_json, result_json
except Exception:
return "{}", "null"
@asynccontextmanager
async def outputs_lifespan():
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(WORKSPACE_DIR, exist_ok=True)
yield
outputs = SubApp("outputs", outputs_lifespan)
def _load_all() -> list[Output]:
result = []
if not os.path.exists(DATA_DIR):
return result
for fname in os.listdir(DATA_DIR):
if fname.endswith(".json"):
with open(os.path.join(DATA_DIR, fname)) as f:
result.append(Output(**json.load(f)))
return result
def _save(output: Output):
with open(os.path.join(DATA_DIR, f"{output.id}.json"), "w") as f:
json.dump(output.model_dump(), f, indent=2)
def _load(output_id: str) -> Output:
path = os.path.join(DATA_DIR, f"{output_id}.json")
if not os.path.exists(path):
raise HTTPException(status_code=404, detail="Output not found")
with open(path) as f:
return Output(**json.load(f))
def _walk_directory(folder: str) -> dict[str, str]:
"""Walk a directory tree and return {relative_path: content} for all text files."""
files: dict[str, str] = {}
if not os.path.isdir(folder):
return files
for root, _dirs, filenames in os.walk(folder):
for fname in filenames:
full_path = os.path.join(root, fname)
rel_path = os.path.relpath(full_path, folder)
try:
with open(full_path) as f:
files[rel_path] = f.read()
except Exception:
pass
return files
# ---------------------------------------------------------------------------
# File-serving endpoints (for iframe preview with multi-file support)
# ---------------------------------------------------------------------------
@outputs.router.get("/workspace/{workspace_id}/serve/{filepath:path}")
async def serve_workspace_file(workspace_id: str, filepath: str, _d: str = ""):
"""Serve a file from a workspace folder. For index.html, inject OUTPUT data."""
folder = os.path.join(WORKSPACE_DIR, workspace_id)
full_path = os.path.normpath(os.path.join(folder, filepath))
if not full_path.startswith(os.path.normpath(folder)):
raise HTTPException(status_code=403, detail="Path traversal not allowed")
if not os.path.isfile(full_path):
raise HTTPException(status_code=404, detail="File not found")
with open(full_path) as f:
content = f.read()
if filepath == "index.html":
input_json, result_json = _decode_data_param(_d) if _d else ("{}", "null")
content = _inject_data_into_html(content, input_json, result_json)
# Iframe sub-resource fetches (<link>, <script src>, <img>) drop the
# parent's ?token= query string, so rewrite the HTML to put the token
# back on every relative URL — otherwise sub-resources 401.
content = _inject_token_into_relative_urls(content, get_auth_token())
mime, _ = mimetypes.guess_type(filepath)
return Response(content=content, media_type=mime or "text/plain")
@outputs.router.get("/{output_id}/serve/{filepath:path}")
async def serve_output_file(output_id: str, filepath: str, _d: str = ""):
"""Serve a file from a saved output's files dict. For index.html, inject OUTPUT data."""
output = _load(output_id)
content = output.files.get(filepath)
if content is None:
raise HTTPException(status_code=404, detail="File not found in output")
if filepath == "index.html":
input_json, result_json = _decode_data_param(_d) if _d else ("{}", "null")
content = _inject_data_into_html(content, input_json, result_json)
content = _inject_token_into_relative_urls(content, get_auth_token())
mime, _ = mimetypes.guess_type(filepath)
return Response(content=content, media_type=mime or "text/plain")
# ---------------------------------------------------------------------------
# CRUD + workspace endpoints
# ---------------------------------------------------------------------------
@outputs.router.get("/list")
async def list_outputs():
return {"outputs": [o.model_dump() for o in _load_all()]}
@outputs.router.get("/workspace/{workspace_id}")
async def read_workspace(workspace_id: str):
"""Read all files from an output workspace folder."""
folder = os.path.join(WORKSPACE_DIR, workspace_id)
if not os.path.isdir(folder):
raise HTTPException(status_code=404, detail="Workspace not found")
files = _walk_directory(folder)
meta = None
if "meta.json" in files:
try:
meta = json.loads(files["meta.json"])
except (json.JSONDecodeError, ValueError):
pass
# Include `path` so the frontend can rehydrate without re-calling /seed.
# /seed unconditionally overwrites, which would clobber any in-progress edits
# the agent made since the last save.
return {"files": files, "meta": meta, "path": os.path.abspath(folder)}
@outputs.router.post("/workspace/seed")
async def seed_workspace(body: WorkspaceSeedRequest):
"""Create a workspace folder and optionally pre-seed it with files."""
folder = os.path.join(WORKSPACE_DIR, body.workspace_id)
os.makedirs(folder, exist_ok=True)
if body.files:
for rel_path, content in body.files.items():
full_path = os.path.normpath(os.path.join(folder, rel_path))
if not full_path.startswith(os.path.normpath(folder)):
continue
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w") as f:
f.write(content)
else:
for rel_path, content in VIEW_TEMPLATE_FILES.items():
full_path = os.path.join(folder, rel_path)
with open(full_path, "w") as f:
f.write(content)
with open(os.path.join(folder, "SKILL.md"), "w") as f:
f.write(VIEW_BUILDER_SKILL)
if body.meta:
with open(os.path.join(folder, "meta.json"), "w") as f:
json.dump(body.meta, f, indent=2)
return {"path": os.path.abspath(folder)}
@outputs.router.put("/workspace/{workspace_id}/file/{filepath:path}")
async def write_workspace_file(workspace_id: str, filepath: str, body: dict):
"""Write (create/overwrite) a single file in a workspace."""
folder = os.path.join(WORKSPACE_DIR, workspace_id)
if not os.path.isdir(folder):
raise HTTPException(status_code=404, detail="Workspace not found")
full_path = os.path.normpath(os.path.join(folder, filepath))
if not full_path.startswith(os.path.normpath(folder)):
raise HTTPException(status_code=403, detail="Path traversal not allowed")
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w") as f:
f.write(body.get("content", ""))
return {"ok": True}
@outputs.router.delete("/workspace/{workspace_id}/file/{filepath:path}")
async def delete_workspace_file(workspace_id: str, filepath: str):
"""Delete a single file from a workspace."""
folder = os.path.join(WORKSPACE_DIR, workspace_id)
if not os.path.isdir(folder):
raise HTTPException(status_code=404, detail="Workspace not found")
full_path = os.path.normpath(os.path.join(folder, filepath))
if not full_path.startswith(os.path.normpath(folder)):
raise HTTPException(status_code=403, detail="Path traversal not allowed")
if os.path.isfile(full_path):
os.remove(full_path)
parent = os.path.dirname(full_path)
while parent != os.path.normpath(folder):
if os.path.isdir(parent) and not os.listdir(parent):
os.rmdir(parent)
parent = os.path.dirname(parent)
else:
break
return {"ok": True}
@outputs.router.get("/{output_id}")
async def get_output(output_id: str):
return _load(output_id).model_dump()
@outputs.router.post("/create")
async def create_output(body: OutputCreate):
now = datetime.now().isoformat()
output = Output(
name=body.name,
description=body.description,
icon=body.icon,
input_schema=body.input_schema,
files=body.files,
auto_run_config=body.auto_run_config,
thumbnail=body.thumbnail,
created_at=now,
updated_at=now,
)
_save(output)
pass
return {"ok": True, "output": output.model_dump()}
@outputs.router.put("/{output_id}")
async def update_output(output_id: str, body: OutputUpdate):
output = _load(output_id)
for k, v in body.model_dump(exclude_none=True).items():
if k == "auto_run_config" and isinstance(v, dict):
v = AutoRunConfig(**v)
setattr(output, k, v)
output.updated_at = datetime.now().isoformat()
_save(output)
return {"ok": True, "output": output.model_dump()}
@outputs.router.delete("/{output_id}")
async def delete_output(output_id: str):
_load(output_id)
path = os.path.join(DATA_DIR, f"{output_id}.json")
if os.path.exists(path):
os.remove(path)
return {"ok": True}
AUTO_RUN_SYSTEM_PROMPT = """\
You generate structured JSON data matching a given schema.
The user provides a prompt describing what data to generate and a JSON Schema.
Return ONLY valid JSON that conforms to the schema. No markdown fences, no extra text, no explanation.
Every required field must be present. Use realistic, meaningful data.\
"""
@outputs.router.post("/auto-run")
async def auto_run_output(body: AutoRunRequest):
"""Use an LLM to generate input data matching the schema, then optionally execute backend code."""
try:
import anthropic # noqa: F401 -- availability probe; client built below via helper
except ImportError:
return {"error": "anthropic SDK not installed", "input_data": None, "backend_result": None}
schema_str = json.dumps(body.input_schema, indent=2)
user_message = f"Schema:\n```json\n{schema_str}\n```\n\nGenerate data for: {body.prompt}"
# Resolve body.model via the registry so non-Anthropic selections are
# routed through 9Router with the correct prefix (cx/, gc/).
# If body.model is unset or unknown, fall back to whichever aux model
# is available (prefers Claude, else any connected subscription).
from backend.apps.agents.providers.registry import (
_find_builtin_model,
resolve_model_id_for_sdk,
resolve_aux_model,
)
settings = load_settings()
if body.model and _find_builtin_model(body.model) is not None:
api_model = resolve_model_id_for_sdk(body.model, settings)
else:
try:
api_model, _ = await resolve_aux_model(settings, preferred_tier="haiku")
except ValueError as e:
return {"error": str(e), "input_data": None, "backend_result": None}
client = _get_anthropic_client(api_model)
try:
resp = await client.messages.create(
model=api_model,
max_tokens=4000,
system=AUTO_RUN_SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_message}],
)
from backend.apps.agents.agent_manager import _safe_resp_text
raw = _safe_resp_text(resp).strip()
if not raw:
return {"error": "Aux model returned no content.", "input_data": None, "backend_result": None}
if raw.startswith("```"):
raw = raw.split("\n", 1)[1] if "\n" in raw else raw[3:]
if raw.endswith("```"):
raw = raw[:-3]
input_data = json.loads(raw)
validation_err = _validate_against_schema(input_data, body.input_schema)
if validation_err:
return {"input_data": input_data, "backend_result": None, "error": validation_err}
backend_result = None
stdout_text = None
stderr_text = None
error = None
if body.backend_code:
try:
exec_result = await execute_backend_code(body.backend_code, input_data)
backend_result = exec_result.result
stdout_text = exec_result.stdout
stderr_text = exec_result.stderr
except Exception as e:
error = str(e)
return {"input_data": input_data, "backend_result": backend_result, "stdout": stdout_text, "stderr": stderr_text, "error": error}
except json.JSONDecodeError:
return {"error": "Failed to parse generated data as JSON", "input_data": None, "backend_result": None}
except Exception as e:
logger.exception("Auto-run failed")
return {"error": str(e), "input_data": None, "backend_result": None}
@outputs.router.post("/execute")
async def execute_output(body: OutputExecute):
output = _load(body.output_id)
validation_err = _validate_against_schema(body.input_data, output.input_schema)
if validation_err:
return OutputExecuteResult(
output_id=output.id,
output_name=output.name,
frontend_code=output.frontend_code,
input_data=body.input_data,
backend_result=None,
error=validation_err,
).model_dump()
backend_result = None
stdout_text = None
stderr_text = None
error = None
if output.backend_code:
try:
exec_result = await execute_backend_code(
output.backend_code, body.input_data
)
backend_result = exec_result.result
stdout_text = exec_result.stdout
stderr_text = exec_result.stderr
except Exception as e:
error = str(e)
return OutputExecuteResult(
output_id=output.id,
output_name=output.name,
frontend_code=output.frontend_code,
input_data=body.input_data,
backend_result=backend_result,
stdout=stdout_text,
stderr=stderr_text,
error=error,
).model_dump()
AUTO_RUN_AGENT_SYSTEM_PROMPT = """\
You are a data-gathering agent. Your job is to use the available tools to collect \
real data, then render it into a structured View.
You have access to MCP tools (e.g. Gmail, calendar, etc.) that let you fetch live data. \
Use them as needed to fulfil the user's request.
When you have gathered enough data, call the **RenderOutput** tool with:
- `output_id`: `{output_id}`
- `input_data`: a JSON object conforming to this schema:
```json
{schema}
```
Do NOT fabricate data. Use the tools to get real information, then structure it to match \
the schema above. If a tool call fails, report the error clearly.\
"""
@outputs.router.post("/auto-run-agent")
async def auto_run_agent(body: AutoRunAgentRequest):
"""Launch a temporary agent session that uses MCP tools to gather data for a view."""
from backend.apps.agents.agent_manager import agent_manager, FULL_TOOLS
from backend.apps.agents.models import AgentConfig
output = _load(body.output_id)
schema_str = json.dumps(body.input_schema or output.input_schema, indent=2)
system_prompt = AUTO_RUN_AGENT_SYSTEM_PROMPT.format(
output_id=body.output_id,
schema=schema_str,
)
allowed_tools = list(FULL_TOOLS)
for tool_name in body.forced_tools:
if tool_name not in allowed_tools:
allowed_tools.append(tool_name)
config = AgentConfig(
name=f"AutoRun: {output.name}",
model=body.model,
mode="agent",
system_prompt=system_prompt,
allowed_tools=allowed_tools,
max_turns=20,
)
session = await agent_manager.launch_agent(config)
await agent_manager.send_message(
session.id,
body.prompt,
context_paths=body.context_paths if body.context_paths else None,
forced_tools=body.forced_tools if body.forced_tools else None,
)
return {"session_id": session.id}
@outputs.router.delete("/auto-run-agent/{session_id}")
async def cleanup_auto_run_agent(session_id: str):
"""Delete a temporary auto-run agent session."""
from backend.apps.agents.agent_manager import agent_manager
try:
await agent_manager.delete_session(session_id)
except Exception as e:
logger.warning(f"Auto-run agent cleanup failed for {session_id}: {e}")
return {"ok": True}