Home / Harness plan (expert review, 2026-10-10)

Harness plan (expert review, 2026-10-10)

This is the plan from the expert review that produced the phone tool gateway. Most P0 items are deployed: the harness text, result trimming, skills, share_file, the startup changes and the policy fixes. The :chat preset was not deployed: its reasoning-effort parameter name is unconfirmed. P1 is still open. Lines marked [CONFIRM] were not verified when the plan was written.

Verdict: the model is fine (same Qwen scores 97/100 under Claude Code). The phone path fails on harness discipline, all of it in code we own. Keep Open WebUI as the thin phone client; make the gateway the harness. Do not adopt OpenCode/Qwen Code for the phone path tonight (reasoning in section 5).

Sandbox note (VERIFIED): pip install fastmcp==3.4.8 works here and all of gateway.py's imports resolve. gateway.py's docstring says 4.1.0; live is 3.4.8. Pin 3.4.8 everywhere.

1. Diagnosis (evidence in brackets)

Tool use inconsistent

Skills feel absent: only reachable via pc_Skill (behind search, write-intent, Claude-skills-only). No catalogue, no progressive disclosure. Open WebUI 0.11's built-in view_skill needs built-in tools on (+5,350 prefix tokens, TOOL-CALLING.md) — reject.

Startup: (a) mcpo starts ~8 stdio servers sequentially, twice (above); (b) npx -y and uvx resolve at each start; (c) BALANCED unloads the 17 GB model after 30 idle min [profiles.json], so the first phone message after a break pays a model load plus a cold tool-bundle prefill (15.6 s measured). Nothing logs startup time.

Files: Qwen writes to C:\AI\shared; nothing serves it. tailscale serve changes need UAC, but /tts (8092) is on the tailnet and already proxies /approvals to 8093 — reuse it.

2. P0 tonight (gateway + bundle; all testable here)

P0-1 Harness text: instructions + tool descriptions (gateway.py, new prompts/phone-system.md)

Spec:

Tests: list_tools shows rewritten descriptions; pinned count/order unchanged; tool-list hash identical across two calls. Effect: fewer "no tool" refusals and wrong-name calls; target ≥8/10 on a 10-prompt phone eval (tests/eval_prompts.json, run on PC). Risk: one cold prefill. Rollback: remove config key.

P0-2 Result truncation + loop hygiene (gateway.py Guard)

Tests: 100 KB result trimmed to spec; small result untouched; non-text content passes; unknown name returns suggestions. Effect: no single call can wipe the window; the model self-corrects names. Risk: none material. Rollback: max_result_chars: 0.

P0-3 skills tool (new skills.py, mounted as local FastMCP tool, pinned)

Tests: fixture tree with 3 skills (one bad frontmatter); search ranks by description; load strips frontmatter; ../ rejected; missing root tolerated. Effect: skills usable from the phone for ~120 prefix tokens. Rollback: unpin.

P0-4 share_file outbox (gateway tool + approvals.py route + tts_server.py proxy)

Tests: traversal and symlink escape rejected; token/name mismatch 404; TTL purge; CSP header on .html; zip of a dir; size cap; URL from public_base; end-to-end curl against the approvals server on a free port. Effect: tap the link on the laptop, the site opens. Risk: anyone on the tailnet holding the 128-bit URL can fetch that file (TTL-bounded; acceptable). Rollback: remove tool from config.

P0-5 Startup and duplication

Tests: config JSON validity; gateway builds from the example config with a fake stdio server. Effect: one Claude Code process instead of two, ~20-40 s faster hub start [INFERRED], fewer cold model loads. Rollback: backup/ in the bundle.

P0-6 Policy fixes

pc_Skill → hide; share_file, skills → read; max_identical_calls stays 3. Per-preset: add qwen3.8-27b:chat in llama-swap config.yaml = thinking on, chat_template_kwargs.reasoning_effort: medium (drops the xhigh instruction), Qwen thinking sampling (temp 1.0/top_p 0.95/top_k 20), and confirm thinking_budget_tokens vs the real param name on the PC before relying on it [CONFIRM]. Phone preset uses :chat; multi-step jobs use :code.

3. P1 next

P1-1 Server-side agent tool (the real harness)

agent(task, preset=":code", max_steps=25): a loop inside the gateway against http://127.0.0.1:8090/v1 with a compact Claude-Code-style system prompt (plan → act → verify, todo list, one tool per step, P0-2 truncation), full catalogue via BM25, same Guard. Transcript persisted to state/agent-<id>.json; on NEEDS APPROVAL returns {status:"paused", approvals:[ids]}, resumes with agent(resume=id); returns a summary plus share_file links. Tests against a mock OpenAI server. Risks: single-slot contention (outer request waits; host-RAM cache restores its prefix, measured 1.5 s), Open WebUI tool timeout [CONFIRM]. This is what lets "build me a website" finish in one phone message.

P1-2 Lazy downstream spawn

Cache the catalogue (state/catalog.json), serve list/search from it, connect a server on first call_tool for its namespace; parallel connect on cold start. Risk: stale schemas; refresh on each full start.

P1-3 Measurement

Per-request cache_n/prompt_n, steps and wall time in the audit log; 10-prompt phone eval script; stats page shows "tool steps this reply".

4. Harness choice

Keep Open WebUI + gateway. The same model and tools score 97/100 under a disciplined harness; the phone path lacks the discipline, not the model or the plumbing. OpenCode/Qwen Code are desktop coding agents: large fixed prompts (Codex measured 15.8K vs 6.5K), no PIN-approval hook, a second chat history, another tailnet surface to secure. Try Qwen Code later as a desktop CLI beside claude-qwen, not as the phone harness. P1-1 gives us the agent loop where we control prompt, truncation, approvals and audit.