Architecture
Kucatoo-Code is a Flask web app layered over an async Python "model harness" library. Two OS processes and three TCP listeners make up a running system:
| Listener | Default bind | Process | Started by |
|---|---|---|---|
| Flask app (HTTP + SSE) | 127.0.0.1:5000 |
run_webapp.py |
manual or supervisor |
| Voice WS proxy | 127.0.0.1:5002 |
same process, daemon thread | run_webapp.py |
| Supervisor control API | 127.0.0.1:5001 |
server_manager.py |
manual (python server_manager.py) |
The three listeners
Flask app — :5000
run_webapp.py puts the project root on sys.path, imports model_harness.web.app.app (built at import time, app.py:312), warns if the bind is exposed without a token, and calls app.run(host, port, threaded=True, debug=False) (run_webapp.py:75); threaded=True lets other requests proceed while an SSE stream is open. Bind from HOST / PORT env (run_webapp.py:57-58).
Supervisor — :5001
server_manager.py is a pure-stdlib HTTP service (no Flask, no model_harness imports) that can start / stop / restart the Flask app even when the app is down — which is exactly why it is a separate process and not a set of Flask routes (server_manager.py:1-11). It binds 127.0.0.1 only (server_manager.py:544); port from MANAGER_PORT (server_manager.py:60). Endpoints (JSON, CORS-enabled for the :5000 page, server_manager.py:512-538):
GET /server/status→{running, managed, pid, started_at, port}POST /server/start//server/stop//server/restart→{ok, action, detail}
The managed PID lives in data/server.pid (server_manager.py:53); liveness = PID alive AND an HTTP probe of the port (server_manager.py:266-273). Start spawns run_webapp.py detached with the venv interpreter when present, stdout appended to logs/server.log (server_manager.py:316-335). An unmanaged process holding port 5000 is reported as managed: false and is killed on stop / taken over on start. Auth: optional MANAGER_TOKEN bearer (env only — this process does not read .env), unset = open (server_manager.py:66, server_manager.py:80-105).
Voice WS proxy — :5002 (and why it is outside Werkzeug)
Voice Chat (speech-to-speech) needs a WebSocket to the Qwen Omni realtime endpoint, but browsers cannot set the Authorization header on a WS handshake — so a local proxy adds the DASHSCOPE_API_KEY header and relays bidirectionally to wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime (voice_ws_server.py:27-28, voice_ws_server.py:49-70).
The proxy does not ride the Flask server: Werkzeug's WS layer (flask-sock + simple-websocket) intermittently emits frames Chrome rejects with "Invalid frame header" (voice_ws_server.py:1-8). The dedicated server uses the websockets library on its own asyncio loop in a daemon thread — the same stack already proven for the upstream DashScope connection. run_webapp.py starts it via voice_ws_server.ensure_started(host=host) (run_webapp.py:68-69); host/port configurable via VOICE_WS_HOST / VOICE_WS_PORT (voice_ws_server.py:118-127). The legacy in-Flask /ws/voice route still exists (app.py:172-302), but :5002 is what the launcher starts.
Component diagram
flowchart LR
Browser[Browser UI]
subgraph AppProc[run_webapp.py process]
Flask[Flask app :5000 threaded]
RT[HarnessRuntime bg asyncio thread]
H[ModelHarness 8 subsystems]
V[Voice WS proxy :5002 thread]
Flask --> RT --> H
end
Browser -->|HTTP + SSE| Flask
Browser -->|WS voice| V
V -->|WSS + auth header| DS[DashScope Qwen Omni realtime]
H -->|HTTPS| P[LLM providers Kimi GLM DeepSeek etc]
Mgr[server_manager.py process :5001]
Browser -->|server control CORS| Mgr
Mgr -->|spawn stop restart| AppProc
Flask --> D[DATA_DIR default data/]
H --> D
DATA_DIR resolution
Generated-file locations are resolved once at startup by _resolve_storage_paths() (app.py:82-111) into app.config["STORAGE_PATHS"]: DATA_DIR (default <project>/data) roots everything; PROMPTS_DIR, RESPONSES_DIR, LOG_DIR, TRACES_DIR are per-type overrides defaulting to subdirectories of it.
Resolution rules (_resolve_path, app.py:62-79): unset/empty → default; ~ expands; relative paths resolve against the project root (BASE_DIR, app.py:50), not the cwd; absolute paths are honored. All directories are created with mkdir(parents=True, exist_ok=True). .env is loaded from the project root with override=True so its keys win over stale shell exports (app.py:52-59).
The app then sets the HARNESS_DATA_DIR env var (app.py:137) so the factory roots subsystem storage (memory DBs, schedules, skills, knowledge bases, user profiles) at the same place. Factory precedence: explicit data_dir arg > DATA_DIR env > HARNESS_DATA_DIR env > config default "data" (factory.py:207-213). The tool workspace root (where agent file tools write) is separate: explicit arg > persisted active_workspace.json > WORKSPACE_DIR env > sibling of the data dir (factory.py:39-76). Note: server_manager.py has its own fixed DATA_DIR = ROOT / "data" for server.pid (server_manager.py:52) — it ignores the app's DATA_DIR override.
Data stores
Persistent state lives under DATA_DIR (<DATA_DIR>, default <project>/data). SQLite databases:
| File | Module | Contents |
|---|---|---|
clients.db |
web/clients.py |
clients (Stripe subscriptions per Clerk user + hosting deployment fields tier/vm_size/memory_mb/volume_gb, added by _migrate() on older DBs), trial_usage (trial query/token counters), subscriber_keys (BYOK provider keys per Clerk user, plaintext — mask to last-4 on read via list_subscriber_keys), deployment_events (append-only audit trail: deploys, tier changes, terminations — see Hosting) |
guests.db |
web/guests.py |
guest_tokens — shareable expiring guest tokens with quotas |
memory/mtm.db |
memory/mtm.py |
MTM mid-term memory (WAL + busy timeout) |
| skills registry DB | skills/registry.py (path from skills/system.py) |
versioned skill packages |
JSONL file stores (not databases — the Usage tab reports are aggregated from these, no SQL involved):
history.jsonl— query history (routes_common.py:107-118)analysis_log.jsonl— trace analyses (routes_trace.py:266)workspace_stats/<workspace>.jsonl— per-workspace usage/cost events (routes_workspaces.py:109)
In-memory only: per-run cost tracking (mesh/cost_tracker.py) resets on restart.
Directory map
Project root highlights: run_webapp.py, server_manager.py, static/ and templates/ (Flask assets, app.py:116-117), data/, logs/, workspace/, tests/, docs/.
model_harness/ subpackages (H-numbers are the build phases):
core/—ModelHarnessfacade, config, provider registry, shared interfaces/errorsmesh/— provider mesh (H1): routing, fallback chains, circuit breaker, cost trackingtools/— tool system (H2): registry, schema generation, sandboxed execution;tools/ported/holds the workspace file toolsagents/— agent framework (H3): react / planner / reflective strategies, agent modesmemory/— tiered memory (H4): STM + MTM (SQLite) + LTM (vectors), embedding providerworkflow/— DAG workflow engine + cron/interval/webhook scheduler (H5)skills/— composable, versioned skill packages (H6)domains/— domain adaptation + RAG knowledge bases (H7)user_profiles/— preferences + interaction history (H8)web/— the Flask layer described on this page (routes, runtime bridge, auth, SSE)validation/— closed-loop runtime validator (build → boot → probe → fix, in Docker)audio/— TTS / STT / STS provider integration (MiniMax, Qwen) + pricingevals/— agent evaluation harness (golden datasets, trajectory grading, LLM-as-judge)guards/— content-compliance guards (pre-prompt / post-response scanning)lsp/— LSP bridge: JSON-RPC language-server client +lsp_*toolsmcp/— MCP (Model Context Protocol) clientskilldag/— optional SkillDAG sidecar: prompt enrichment + post-run skill miningteams/— role-based multi-agent orchestration built onagents/factory.py(package root) — presets:create_minimal_harness,create_agent_harness,create_full_harness
Lazy harness initialization
Importing the web package is side-effect-free: the module-level HarnessRuntime singleton (runtime.py:215) builds nothing until the first get_harness() / run() / stream() call. On that first touch, _ensure_started() (runtime.py:57-88):
- Creates a dedicated asyncio loop in a daemon thread named
harness-async-loop. One long-lived loop is required because the harness runs persistent background tasks (mesh health checks, scheduler loop) that must outlive any single request (runtime.py:1-23). - Builds the harness on that loop via
create_full_harness(start_scheduler=True)(runtime.py:101), wiring all 8 subsystems in dependency order (factory.py:165-310) with the scheduler as a fire-and-forget background task (factory.py:264-273). Construction has a 120 s timeout; failure tears the loop down so a retry is not wedged (runtime.py:82-86).
Flask↔async bridge (runtime.py:157-209): run(coro) schedules a coroutine and blocks for its result (JSON endpoints); stream(agen) bridges an async generator to a sync generator via a queue.Queue (SSE endpoints), cancelling the producer if the client disconnects. Shutdown runs on atexit (app.py:304-306).
Direct-mode query lifecycle
POST /api/query (routes_query.py:212) parses the body, enforces guest gates, validates model + image/reasoning/context-file inputs, then returns Response(stream_with_context(_query_stream(...)), mimetype="text/event-stream") (routes_query.py:309-325). With use_agent false, _direct_stream_async (routes_query.py:569) runs:
- Explicit
remember …prompts short-circuit into amemory_proposalevent (no model call). - SkillDAG enrichment and memory recall (top-5) build an optional system block; session history is prepended when
session_idis set. harness.query_stream(...)(core/harness.py:347) is pumped through anasyncio.Queueso reasoning deltas reach the client in real time.- Terminal
donecarriesmodel_used,cost_usd, token counts (with cache hits),elapsed,estimated; history, workspace stats, and SkillDAG mining are recorded fail-open.
SSE wire protocol (streaming.py:1-30): single data: {"event": <type>, "data": <payload>} lines. Event types: content, reasoning, thought, tool_call, step, observation, permission_request, question, memory_proposal, cap_warning, done, error.
sequenceDiagram
participant B as Browser
participant F as Flask :5000
participant R as HarnessRuntime
participant H as Harness
participant M as Provider mesh
B->>F: POST /api/query prompt model
F->>F: role check + guest gates + input validation
F->>R: stream(_direct_stream_async)
R->>R: lazy _ensure_started first call only
R->>H: schedule async generator on bg loop
H->>H: SkillDAG + memory recall + session history
H->>M: query_stream prompt
M-->>H: content chunks + reasoning deltas
H-->>R: asyncio.Queue items
R-->>F: queue.Queue bridge
F-->>B: SSE reasoning and content events
H-->>F: done event tokens cost elapsed
F-->>B: SSE done
Agent-mode run lifecycle
With use_agent: true, _agent_run_async (routes_query.py:879) replaces the direct stream:
agent_framework.create_agent(...)withagent_type(react / planner / reflective; default reflective),mode(explore / plan / write_test / write_no_test; invalid → EXPLORE),max_steps(default 60),max_time_sec(0 = unlimited),auto_confirm. When the active workspace is the app's own source tree, the mode is clamped to read-only EXPLORE (routes_query.py:226-231).- The agent is registered in
live_agent_runskeyed bysession_id, enabling the mid-run controls/api/agent/auto-approveand/api/agent/instruct. - Confirmation/question handlers are installed unconditionally: a gated tool call (
write_file,execute_python) withauto_confirmoff emits apermission_requestevent and parks the run until the client POSTs to/api/permissions/respond(timeout ~300 s = denial); PLAN-modeask_useremits aquestionevent the same way. af.run_agent_streamingis pumped through a mergedasyncio.Queue; deduplicated events:step(status transitions),thought(new or grown suffix),tool_call(ACTING transition only),observation(OBSERVING), streamedcontentchunks,cap_warningnear budgets (routes_query.py:1016-1092).- Terminal
donecarries per-run token sums (from steps, not the agent's lifetime counter),context_prompt, estimated cost,plan_file(PLAN mode writesPlan-N.mdto the workspace root),stop_reason,resumable(routes_query.py:1247-1261). Cap-stopped runs (max_steps/max_tokens/max_time) persist message history soPOST /api/agent/continueresumes with full context (routes_query.py:426-482).
Auth
Role-based access (auth.py:1-24): every request resolves a role in resolve_role (auth.py:142-164), enforced by a before_request guard (auth.py:193-233).
- owner — Google OAuth sign-in (when
GOOGLE_CLIENT_IDis configured), or any request when no auth is configured at all — the friction-free local default. - token — the
API_TOKENenv bearer (Authorization: Bearer <token>or?token=), constant-time compared; runtime-toggleable viaauth_settings.jsonin DATA_DIR (auth.py:88-103). - guest — shareable expiring trial tokens in
<DATA_DIR>/guests.db(guests.py:1-8): sandboxed workspace<workspace>/guests/<token-prefix>/set per request via a contextvar and cleared on teardown (auth.py:178-184,auth.py:235-241); model allowlist + query/token quotas in/api/query(routes_query.py:269-286); 403guest_forbiddenon/api/visionand/api/video(auth.py:46,auth.py:224-231). - none — 401 on
/api/*once any auth is configured. Page gating (redirect to/auth/login) applies only when OAuth is configured; token-only setups keep pages open so the in-app token field works (auth.py:199-212).
Binding beyond localhost with API_TOKEN unset triggers a loud startup warning listing the exposed routes — never a hard failure (warn_if_exposed, auth.py:256-272; surfaced in run_webapp.py:60-64). The supervisor's MANAGER_TOKEN is independent of all this.