Commit Graph
100 Commits
Author SHA1 Message Date
host a2ed8f8a79 refactor: remaining longfunc small extractions after #231 (no module split)
Only pure computation/validation sub-blocks of _handle_github_webhook,
_create_followup, _deliver_to_thread and _task_chat_history are extracted;
all DB writes, audits, external calls and side-effect blocks stay in the
callers, error strings/codes and response shapes verbatim. _deliver_to_thread
uses the shared goal classifier only for /goal input: its non-goal terminal
path still goes to CAS revive (not 409) and its merged path keeps its own
wording. New tests: test_longfunc_remaining.py (15).
2026-11-04 20:54:04 +00:00
host 48afcf42da refactor: extract minimal pure helpers from _continue_task, _task_wait, _task_thread_read and _metrics
_continue_task 226->168 via _validate_continue_options;
_task_wait 158->139 via _validate_wait_params;
_task_thread_read 187->152 via _summarize_trace_entries;
_metrics 123->104 via _summarize_metrics_tasks.
No behaviour, message, status or route change; error strings verbatim.
New tests: test_longfunc_extra (9). Cloud full suite: 1439 OK, skipped=4.
Follows #229 pattern; must merge after #229 and be rebased/checked then.
2026-11-04 20:54:04 +00:00
host 200c8be16f refactor: extract minimal helpers from _dispatch, _dispatch_project and _create_task
Move the /tasks/* sub-route out of _dispatch into _dispatch_task_route
(query passed explicitly; the files/tree/thread branches use it), move
the push-subscription DELETE branch out of _dispatch_project into
_project_push_subscription_delete, and turn the goal-validation block
of _create_task into the pure module function
_validate_task_goal_options (returns error string instead of sending
400, caller sends it unchanged). Statements are moved verbatim except
indentation and the goal send+return -> return-error transform; no
behaviour change. server.py module split intentionally not done: it
remains a larger, higher-risk refactor.

Add test_longfunc_split covering the pure goal validator's success
paths and all eight error strings.
2026-11-04 20:54:04 +00:00
host 9e90efd5ee workers: reap pid_verified stale workers with pre-kill audit and post-kill verification 2026-11-04 20:54:04 +00:00
host 32e264054c workers: land host/pid/address columns with idempotent migration and pid_verified stale classification
- ensure_workers_table: new tables include host TEXT, pid BIGINT,
  address TEXT; old tables gain them via _ensure_hostpid_columns
  (PRAGMA column check, missing-only ALTER, old rows keep NULL,
  never fabricates host/pid). SQLite and Postgres share this path
  (PG via the db adapter's PRAGMA translation); POSTGRES_SCHEMA
  deliberately still excludes workers, which is volatile heartbeat
  data in UNMIGRATED_TABLES (adding it broke the P2 migration
  alignment tests, verified and reverted).
- heartbeat/register: given host/pid/address refresh, omitted
  values preserve the old row; address has no derived source.
  Local worker passes host_name + own pid; HTTP heartbeat only
  stores caller-supplied values, never the server's own.
- classify_stale_workers: kill only when age strictly exceeds the
  threshold AND host matches local host AND pid is proven dead;
  equal threshold, foreign host, missing pid, or live/unknown pid
  are list_only. Classification only, never kills, never matches
  by name/prefix, never touches infra containers.
2026-11-04 20:54:04 +00:00
host 26630bafec docs(skill): add software-engineering README and same-dir reference test
SKILL.md header pointed at same-dir README.md but the pack had no
such file (CHANGELOG was supplied by #217, README was not). This
track adds the README only: purpose, chapter map, appendix
provenance entry point, seeding behaviour, and known boundaries.
It states no rule, changes no SKILL.md semantics, and does not
duplicate the CHANGELOG/script explanations -- overlapping parts
are pointers, with an explicit division-of-labour section.
Deletion retention is written only as the tombstone-design
placeholder, deferring to the skill_seed implementation, because
no tombstone implementation has landed. Test pins that every
file SKILL.md cites as same-directory exists (CHANGELOG, README).
2026-11-04 20:54:04 +00:00
host 99ce8cf72d docs(skill): add appendix provenance sources, checker and CHANGELOG
SKILL.md claimed its appendix was spliced verbatim from
templates/roles/ by a generation script (see CHANGELOG), but the pack
contained none of the three. Add the nine role sources (mechanically
split from the v0.2 appendix; SKILL.md untouched, zero semantic
change), tools/check_appendix.py (verify + --emit, stdlib only, never
edits SKILL.md), CHANGELOG.md, and test_skill_appendix_provenance.py
(5 tests). Seeding unchanged: skill_seed copies SKILL.md only, so
templates/tools/CHANGELOG never reach the tenant home.
2026-11-04 20:54:04 +00:00
host 93f2b350ac feat: hard merge gate by default (CI + directory + contract + coordinator + reviews)
- cloud/merge_gate_hard.py (new): pure hard gate; mode resolution is
  env-only (unset/invalid -> hard, fail-closed); five checks plus
  branch protection must be protected; unknown/unprotected block
  even with explicit confirm; no-evidence blocks (fail-closed);
  directory check reuses #209 check_directory_isolation (v0.3 rule);
  blocks codes + merge_gate_blocked audit detail are the track-3
  pushback interface (no pushback state machine in this track).
- cloud/server.py: POST /tasks/{id}/merge uses hard mode by default;
  record/enforce/off only when explicitly set via env (preserving
  #209 semantics); body merge_gate_mode is ignored server-side so
  callers cannot downgrade the gate; every gate block writes a
  merge_gate_blocked audit before returning 409, status unchanged;
  hard-mode body type errors return 400.
- docs/merge-gate-branch-protection.md (new): protection payload
  (PR + strict status checks + no force-push + no deletion +
  enforce_admins), empty required_checks = unconfigured, Pro/public
  prerequisites, 403 causes; states protection is NOT claimed enabled.
- SKILL.md (gate-mechanism wording only): four checks -> five checks;
  body + coordinator + client appendix gate paragraphs updated to
  default hard, merge_gate_blocked audit, payload doc reference.
- tests: 31 new hard-gate tests (pure + endpoint + skill text);
  #209 endpoint tests now set record env explicitly (their intent);
  test_server_merge 200-path calls now carry full hard evidence.
2026-11-04 20:54:04 +00:00
host f6d721158e feat: configurable merge gate (branch protection + directory isolation) and skill body/appendix checks
- cloud/merge_gate.py: pure gate; directory check reuses the v0.3
  git diff --name-only merge-base command; protection modes
  record (default, explicit confirm required when unprotected),
  enforce (always blocks when unprotected), off (explicit only).
  Confirm covers protection risk only, never directory violations.
  Protection status is caller/env-declared only; unknown is a
  recorded risk, not a claim of protection.
- server POST /tasks/{id}/merge: applies the gate only when gate
  evidence is supplied (or env declares protection); no-evidence
  calls behave as before plus a merge_gate field in the response.
  Blocked merges return 409 without changing status; confirmed
  unprotected merges write a merge_gate_risk audit entry.
- skill_seed.py: whole-directory seeding evaluated and rejected
  (cannot preserve tenant deletions; no benefit while the pack
  holds only SKILL.md); SKILL.md-only behaviour unchanged.
- software-engineering SKILL.md: gate paragraph added to the body
  and to the coordinator + client appendix sections (v0.2 text on
  this base; port to v0.3 text on rebase after #199 merges).
- tests: 16 new tests covering gate modes, directory reuse,
  body/appendix bidirectional assertions naming the missing side,
  SKILL.md-only seeding and tenant-edit preservation.
2026-11-04 20:54:04 +00:00
host 04a2c4d7f8 feat: software-engineering skill v0.5 (G7/G8 ops + iteration)
Stacked on v0.3 + v0.4 (HEAD d6522ba feat-full-auto): v0.3/v0.4
content preserved, not reverted. Adds G7 post-release ops and
observation (monitoring, errors, cost/quota, feedback, hotfix loop,
rollback triggers, ops manual) and G8 iteration and scaling (v1.1
handover list, contract v1->v2 protocol, new-end onboarding,
scale/performance, deprecation), with G6->G7 and G7->G8 gates,
DAG, gate table, coordinator derivation, G0-G8 todos, checklist,
09-ops and 10-iteration role templates, and ops-manual / v1.1
handover templates in both body and appendix. Settles web+React+
multi-viewport focus, cross-viewport E2E, web single-end release
with iOS/Android native postposed, open-source-first, and the
TypeScript full-stack default (user-request-only language change;
codex-app-server itself stays Python). Pending #199 and #205
merge, then one rebase round. PR only, do not merge or deploy.
2026-11-04 20:54:04 +00:00
host cd85157b54 feat: full-auto mode v0.4 (host executor + skill + CI fix loop, stacked on v0.3)
Host auto executor (cloud/full_auto.py, stdlib only): default off
(FULL_AUTO_ENABLED + verified installation), forbidden actions
fail-closed (delete repo, force-push, visibility change, secret
read/expose, branch-protection delete/bypass, GitHub account
auto-registration), token only in request header, audit log
redacted, branch protection requires PR + strict checks +
no force-push + no deletions + enforce_admins, merge gate
(CI green + directory check + contract version + coordinator
approval), CI fix loop max 3 then escalate, rollback only via
revert PR or previous tag. Skill: v0.4 full-auto mode, coordinator
reviews, host executor executes, task agent never executes
merge/deploy; repo-creation honesty branch (org via App token,
user repo needs_user_once); no web auto-registration of GitHub
accounts. Tests: 19 new, cloud full 1451 OK (skipped=4).
2026-11-04 20:54:04 +00:00
host c76ef9dcec feat: software-engineering skill v0.3 (P1-1..P1-4 + P2)
- P1-1 directory-isolation executable check: git diff --name-only
  against merge-base, output recorded in merge record
- P1-2 derivation checklist table (8 required items, missing one blocks dispatch)
- P1-3 QA split into single-client review and cross-client E2E with
  fixed deliverables and agent-test attribution
- P1-4 sub-task slim-pack trimming rules (listed chapters only, never full copies)
- P2: inter-thread Q&A default policy, docs/decisions.md append-only,
  per-client onboarding docs (web first at G4a), v1.1 handover checklist
- Body and appendix kept in sync; new lightweight keyword test
2026-11-04 20:54:04 +00:00
host 6ff3855419 coverage85-threshold: 覆盖率门槛 75% -> 85%(实测 85.2565%,余量 57 行) 2026-11-04 20:54:04 +00:00
host 1f06a3ef30 coverage85-step4e-fix: misc 不再 import stdlib queue,避免遮蔽 test_queue_sandbox 的 cloud/queue 导入 2026-11-04 20:54:04 +00:00
host 887d8f7afb coverage85-step4f: host 纯函数与小方法长尾测试 2026-11-04 20:54:04 +00:00
host 1ba96dc61c coverage85-step4e: workers/autoscaler/playwright 长尾测试(+29 项) 2026-11-04 20:54:04 +00:00
host 3a310c062b coverage85-step4d: host main 尾部命令块测试(+30 项) 2026-11-04 20:54:04 +00:00
host 6c123bfb23 coverage85-step4c: server 长尾端点与子路由测试(+19 项) 2026-11-04 20:54:04 +00:00
host d62185a610 coverage85-step4b: worker marker_tools 派发剩余分支测试(+20 项) 2026-11-04 20:54:04 +00:00
host 1c2e4e0aca coverage85-step4a: worker run_one/_run_attempt 缺口测试(+34 项) 2026-11-04 20:54:04 +00:00
host 5a0652b346 coverage85-step3: 运行器统一加根路径,tests/ E402 清零
新增 run_tests.py:三套件统一入口,只加仓库根(deploy 套件
再加 deploy/ 目录在根之前),各套件走独立子进程;verify 作
业改走运行器,coverage 作业因子进程测不到覆盖率保持原
discover 命令。tests/ 13 个文件的 sys.path 前置 import 全部
改回正常 import(含 test_host_trace 的 try/except 回落);
deploy 的 deploy 目录插入与 verify-multiuser 的 spec 加载按
陷阱 2 如实保留(仅删 verify-multiuser 冗余的根插入)。
.coveragerc omit 加 */run_tests.py,使新运行器文件不计入
生产覆盖率。门槛未动(仍 75%),未补新测试。
2026-11-04 20:54:04 +00:00
host b9cf903a73 coverage85-step2d: 新测试 lint 清理(删无用导入与变量) 2026-11-04 20:54:04 +00:00
host 35fbd767ed coverage85-step2c: host 剩余缺口真实行为测试(+18 项)
- merge_task:成功、验测失败回滚、-X ours 自动解成功(分支
  新增行全在主仓)与丢改动回滚;run_health_check(好/坏/损坏
  state/损坏 pidfile/备份存在);notify_failure(无命令/写标记/
  坏模板/失败 rc/注入文本不执行)。
- stub Server:dispatch(含 seed-dir 与无 worktree 拒绝)、
  dispatch_from_branch、run_task_turn 三态与重试预算门、
  run_goal_loop(验测达标/标记达标/未达标/预算中断/回合异常)。
- main() 进程内:init/config/doctor/failures/health/daemon-status
  /daemon-stop/sleep/github-workflow/pr/review-comment/discard/
  automations-cron-line/restore-state/recover/dispatch/status/list/
  thread-*/skills/apps/mcp/run/compact/archive/code-review/fork/
  delegate/interrupt/merge/goal-resume;daemonize 子进程双 fork
  写 pidfile 与 marker 验证。
生产代码零改动;dispatch 建的 worktree 在 tearDown 清理。
2026-11-04 20:54:04 +00:00
host 83581568fb coverage85-step2b: server/worker 剩余缺口真实行为测试(+24 项)
- server:_dispatch_project(项目 PATCH repo_url、成员邀请/改角
  色/删除与 404/403/409 族)、_create_task 校验 400 族与合法变体
  (goal/协调员/plan/环境)、trace/agents/trace-stream(终态 SSE
  snapshot+done)、GitHub webhook 去重与坏体。
- worker:_gateway_context(xcmapi/缺 origin/缺属主/假 sub2api
  key 与协调员父属主回查)、_ensure_gateway 缓存、_docker_task_
  context(代理/租户网络失败/搜索透传)、_deliver_marker_tools
 (守卫空跑、创建派生、goal 转换、轮询快照)、协调员孤儿扫尾、
  run_one/run_all/run_loop/main 空库路径。
生产代码零改动。
2026-11-04 20:54:04 +00:00
host b91955e914 coverage85-step2a: MCP/deploy/verify-multiuser 剩余缺口真实行为测试(+43 项)
- playwright/imagegen/websearch 三个 MCP:handle_request 全工具分发、
  通知/错误分支、main() 进程内 stdio 循环(pipe/StringIO + 假浏
  览器/假依赖)、LineReader 分块与 EOF、真实 RemoteBridge 构造、
  flat-copy 回落导入;三文件孤立覆盖率 98%(余 __main__ 与
  通知型 handler-error else 分支,见回执)。
- deploy.py:check_clean_tree 脏/暂存/放行、archive 失败、remote
  助手、transfer 全路失败与 sha 不符、git 源冲突与干跑、tar
  路径干跑/构建/部署成功与失败;孤立 92%(余行由既有 deploy
  测试覆盖,__main__ 除外)。
- verify-multiuser main():真实本地控制面 13/13 通过且清理归零、
  死端口与 stub 项目失败早退、清理触发器异常报 leftover。
生产代码零改动。
2026-11-04 20:54:04 +00:00
host 54d216bff9 coverage85-step1: server/worker/host 主力缺口真实行为测试(+113 项)
只补 cloud/server.py、cloud/worker.py、host.py 的真实行为测试,
不动生产代码:
- server:助手函数(路径/租户/提取/链路读/配额/限流/心跳/
  host CLI stub 脚本)40 项
- worker:纯助手 + Worker DB/认领/容量/配额/跟进/租户/协调员
  孤儿/回退 token,真实 SQLite 与真实子进程 36 项
- host:假 app-server 脚本走真实 JSON-RPC 协议,run_turn
  成功/超时/中断/失败/历史错误回捞 + 状态/git/PR 助手 37 项
不含 mock 被测代码、无 skip、无 no-coverage、无空循环刷行。
2026-11-04 20:54:04 +00:00
host 537f421e6e ci3: lint/mypy 第二阶段清理 + 覆盖率 76% -> 78%(门槛不动)
基底:#251 头 174b631(堆叠于 #242 feat-ci-expand 之上),
基线 origin/main aebc6238;本轨不改 #251 已定语义、不动
workflow 门槛、不删检查、不加 skip。

lint(ruff E,F@120):109 -> 10
- F401 43 -> 0:删未用导入;try/except 双绑定的名字出现
  “打地鼠”(删 except 分支后 try 分支又被标出),两分支
  均已核对正文无引用后清掉;MCP 三模块的 Error 名不删,
  改走 importlib 属性绑定(见下),两路径绑定都保留。
- E702 40 -> 0:全在 host.py main() 的 argparse 段,纯分行,
  tokenize 按分号 token 拆(含 2377 行多行语句只断首个分号);
  一次 5 空格缩进失误已回滚重做并以 ast + 全量 host 测试验证。
- E501 13 -> 0:host.py 5 项随 E702 分行连带清掉;mcp_server
  3 项把描述拆成相邻字面量(AST 比对 24 条工具描述与原值
  完全一致,注意 ruff 按东亚宽度计长,CJK 算 2);test_host
  5 项断开 git commit 参数表。
- E402 8 -> 10:生产侧 2 项清掉(db.py _re、github_app
  _base64 移到顶部,之前无引用、无重名);剩余 10 项全是
  tests/ 的 sys.path 前置 import(6 项旧 + 4 个新测试文件
  沿用同一既定模式),移上去会破坏单文件直跑,有意保留。
- F841 4 -> 0:只去绑定、不删调用(run_task、DB 入队、
  tarfile 上下文、git 提交),各有注释与原测试/新测试覆盖;
  host dispatch_from_branch 新增真实 git 仓库回归测试。
- F811 1 -> 0:server 本地 _row_get 与 peer_messages.row_get
  逐行相同,删本地定义,全部调用点解析到同一 import;
  新增 test_server_row_get(同一对象 + 三路行为)。

mypy(生产范围):24 -> 0,零新增 type: ignore
- no-redef 9 -> 0:MCP 三模块 try/except 两次 from-import
  改 importlib.import_module + 属性绑定(cloud 优先、flat
  copy 回落、内层 ImportError 同样回落,语义与原来一致;
  flat-copy 子进程测试证实回落后 __module__ 为顶层名);
  server _row_get 随 F811 消除。
- arg-type 10 -> 0:harness2_tasks shared 改 TypedDict
  逐键精确类型;CodexProbeAdapter 补 no-op validate_tree
  (_adapter_validate 本就跳过缺失方法,行为一致)。
- var-annotated 3 -> 0:sub2api _SHARED_ADMIN/_login_slots、
  verify-multiuser RESULTS 加精确注解。
- assignment/misc 2 -> 0:marker_tools ZoneInfo 先声明
  Any 再 try import(草稿先验 mypy 通过)。

bandit:总数 286 不变,High 仍 4 项,全在 host.py,本轨不改
(属第 3 项白名单轨范围),只做 triage:
- 1319 run_verify:无插值,整串验测命令,管道/&& 须 shell
  语义(新增 test_host_run_verify 断言管道与 && 真实生效)。
- 1497 notify:模板来自算子配置,三处插值全部 shlex.quote,
  证据为既有 test_notify_failure_quotes_shell_metacharacters
  (; touch 与 $(touch ) 不执行)。
- 2099/2103 goal verify:无插值,任务配置验测串,同 run_verify。
- 无新增 High;pip-audit 0 漏洞。

覆盖率(同口径三套件 coverage 计量):75.96% -> 77.87%
(22392 行/缺 5385 -> 22437 行/缺 4963,显示 76% -> 78%)
- client.py 0% -> 90%(pipe 假子进程 + main 分支)
- run_parallel.py 0% -> 97%、migrate 包装 0% -> 95%
- playwright_mcp 37% -> 62%、imagegen_mcp 47% -> 79%、
  websearch_mcp 51% -> 86%、deploy.py 56% -> 77%
- verify-multiuser 0% -> 27%(仅 check/req helper;main()
  是主机侧真实多用户实弹验证,按门禁不建生产任务、
  不以 mock 冒充,仍为缺口)
- host.py 55% -> 57%(文件 2020 -> 2060 行系 E702 分行
  的计量口径变化,非代码膨胀)
- 未到 85%:门槛保持 75% 不动,不删低覆盖模块出统计口径;
  缺口主力仍是 server.py / worker.py / host.py 大文件,
  下一阶段须针对性深测,非本轨快赢可及。

验证:三套件 1576 -> 1629 项全过(tests 127、cloud/tests
1449 skipped=4、deploy/tests 53);ruff E9 通过;mypy 0;
bandit High 4(同上);pip-audit 0 漏洞;试跑产物已删,
提交后工作树干净。
2026-11-04 20:54:04 +00:00
host a502725dc4 ci2: fix worker F821 NameError, mark harness2 sha1 non-security, rename E741 var
- worker._tenant_dirs: playwright refresh log used undefined 'task';
  use the method argument project_id (ruff F821, real bug: NameError
  aborted tenant seeding when a playwright script was refreshed).
  Regression: cloud/tests/test_worker_playwright_seed_f821.py (2 tests;
  pre-fix run reproduces NameError at worker.py:2780).
- harness2.assign_ids: hashlib.sha1(..., usedforsecurity=False) -- the
  digest is only a deterministic shuffle seed, not a security use;
  digest value unchanged, bandit B324 no longer fires (High 5 -> 4).
- harness2.select_final: rename ambiguous loop var 'l' to 'lab'
  (ruff E741, behaviour-invariant).
- Deliberately NOT changed: host.py shell=True x4 (see PR body -- all
  four are intentional shell commands, changing semantics is unsafe),
  remaining ruff 109 / mypy 24, workflow thresholds (75% coverage
  gate unchanged; no gate was relaxed or removed).
2026-11-04 20:53:46 +00:00
host 00d4f82f48 ci: expand host-verify with lint, typecheck, security, depscan and coverage gates
Phase 1 of audit-hardening CI expansion (baseline origin/main aebc623):
- lint: ruff E9 hard gate; full E,F@120 report-only (baseline 111 items)
- typecheck: mypy default mode, production scope, report-only (baseline 24)
- security: bandit production scope, report-only (baseline 287, High 5)
- depscan: pip-audit on requirements-ci.txt, hard gate (0 vulns; repo has
  no production dependency manifest, see summary)
- coverage: production 58 files, fail-under=75 (baseline measured 76%)
- release build: E9 + pip-audit hard gates before packaging; full reports
  stay in host-verify to avoid drift
No production code changed; CI billing failure mode unchanged.
2026-11-04 20:53:46 +00:00
host fa4588a119 CI: guard coordinator pushback test flips on parent still running (run #20 flake: post-commit flip re-failed requeued child on slow runner; test-only, no production change)
ci / test (push) Successful in 35m52s
2026-11-05 02:54:22 +08:00
host 159d7112ae Audit N-12: goal pushback preamble reaches breakpoint resume via --resume-note
ci / test (push) Failing after 35m34s
Goal tasks pushed back by the audit loop lost the packet preamble on
the next round: claim-time _apply_audit_loop_packet pasted it into the
in-memory prompt/goal, but the next round collided with the existing
host task name, fell back to breakpoint goal-resume (no --goal-text),
and the host reused the stored goal, discarding the preamble.

Fix (minimal, direction (c) trimmed): host goal-resume gains
--resume-note, accepted only by the breakpoint branch and prepended
to the first turn prompt of this resume only -- never written back
to the stored goal, never touching rounds or verification. The worker
stashes the same preamble as the memory-only _audit_loop_resume_note
at claim time and forwards it on breakpoint resume (Local/Docker
goal_resume_task and the popen goal-resume command, each guarded to
ignore the note when goal_text is set for a new-goal resume).
Accounting untouched: audit 3-round cap, goal_max_rounds/quota, and
DB goal columns are unchanged; non-goal pushback is unchanged.

Tests: cloud/tests/test_audit_n12.py (9 tests) -- host first-turn-only
note + stored-goal/rounds, worker exists-fallback end-to-end with
--resume-note, non-goal regression, goal-column accounting, and
Local/Docker/popen command shapes. Full cloud suite: 1760 OK
(skipped=11), baseline 1751 + 9 new; tests/test_host.py: 62 OK.
2026-11-04 18:02:23 +00:00
host bc19f09dff Audit batch 6: B1 zero-gate sentinel, I1 known-to-lack marker, I2 wire error returns row status, M1 max-rounds label, M2 chosen-last convention, M5 escalate write retry, M6/M7 clear/pass CAS tests 2026-11-04 18:02:23 +00:00
host 2adc605373 Audit batch 5: I-9, I-12, I-14, N-1..N-12, single-task wire single-transaction 2026-11-04 18:02:23 +00:00
host cdac8039b3 Audit batch 4: I-6 whitespace audit format, I-7 explicit non-sub2api capability gate, I-8 coord gate attribution, I-10 checked gates, I-11 gate-without-finding escalate, I-13 structured-first classification 2026-11-04 18:02:23 +00:00
host 06e4e16bbb refactor: audit batch 3 (I-3 capability verdict, I-4 policy merge)
- I-3: new models.evaluate_capability returning a CapabilityVerdict
  (ok/missing/unknown from check, known/unknown_missing from split,
  status/verb derivations); decide_routing, worker gateway check,
  worker persisted recheck, dry-run tier+gateway checks and server
  shadow endpoint all call it. Blocking rules stay at the call
  sites (gateway has no worker-default fallback, recheck blocks
  only on known, dry-run/shadow never block); all user-visible
  audit details, dry-run notes and shadow notes are byte-identical.
- I-4: new models.merge_policy_documents shared by
  load_routing_policy and dry_run_from_env; the old tiers-update
  block is kept verbatim (it was a no-op after update(): JSON
  tiers replace the whole map, file tiers absent from JSON are
  dropped -- preserved, not fixed, as that would be a behaviour
  change). dropped_tiers/partial_failures remain load-time
  records, not merge products.
- Tests: test_audit_batch3.py, 13 tests (verdict matrix + parity
  with check/split, decide/dry-run wording, merge boundaries +
  300 seeded random pairs + loader/dry-run agreement).
  Full suite: 1694 OK (skipped=11), baseline 1681 + 13, 0 failures.
2026-11-04 18:02:23 +00:00
host a7f77c8a98 fix: audit batch 2 visibility (I-1 shadow dropped rows, I-2 partial policy failure, I-5 failure policy table)
- I-1: evaluate_shadow counts dropped rows by cause (unparseable /
  unknown_tier / explicit / duplicate) with the identity
  audit_rows_total == routed_task_n + dropped_total; the shadow
  endpoint passes created_at/id through and reports audit totals
  next to sample counts.
- I-2 (option b): parse logs a warning naming each tier dropped for
  an invalid mode/requires; load attaches partial_failures to the
  policy; the worker records a task-level
  worker_routing_policy_load_failed audit for partial failures
  (same action as full failure, detail says failure=partial);
  the shadow report carries the policy fingerprint, dropped tiers
  and partial failures (no task context on that path, so no
  task-level audit there).
- I-5: coordinator gate audit lookup failure now raises
  _CoordGateLookupFailed and the merge converts it to escalate
  (no requeue, no round consumed); persisted-model lookup failure
  and required-tools check failure each write a named audit row
  before falling through to dispatch/allow; audit-loop evaluate
  failure stays escalate (regression test).
- Test fixtures: coordinator suites build audit_log via AuthStore
  as production's first real audit does, since their stubbed
  _audit never creates the table.
2026-11-04 18:02:22 +00:00
host 44e0f4f1d6 fix: audit batch 1 (B-A migration audit_loop INTEGER, B-C gateway failure clears running slot)
- B-A: migrations.py tasks backfill loop now types audit_loop as
  INTEGER (other 6 columns stay TEXT), matching db.py backfill,
  server/worker ensure lists; TEXT readback '0' is truthy and
  would silently enable the audit loop for audit_loop=0 rows.
- B-C: gateway preparation failure early-return now calls
  _running_clear(task_id) next to _cleanup_task_seed, matching
  missing-key/repo/capability/tool gates; previously the capacity
  slot leaked until worker exit (default capacity 1 blocks reclaim).
- Tests: cloud/tests/test_audit_batch1.py (3 tests): migration
  ALTER types, INTEGER 0 readback not enabled via real worker
  row mapping, gateway failure clears slot and capacity reclaim.
  _claim doc 'three gates' list left unchanged (full early-return
  gate enumeration not verified this batch; residual wording).
- No other audit items touched (I-1..I-14, N-1..N-12).
2026-11-04 18:02:22 +00:00
host e3423865aa test: 9.5-code review minors (M1 conversion test, M5 no-fingerprint audit test, M2/M4 wording)
- M1: test_goal_branch_via_convert_queued_then_claim actually calls
  server._goal_convert_queued on a queued auto-resumed task before
  claiming, instead of only inserting the post-conversion state.
- M5: test_worker_routing_audit_rows_carry_no_fingerprint drives a
  real run_one with an active policy and asserts worker_routing
  audit rows exist and carry no fingerprint (negative control
  verified: injecting fingerprint into the detail fails it).
- M2: 'all callers' wording tightened to the real counts
  (load_routing_policy: 2 production call sites; evaluate_shadow: 1).
- M4: content_fingerprint docstring now states the fingerprint covers
  the full normalized payload including fallback_models, which the
  router deliberately does not apply (fail-safe direction).
No behaviour change; M3 DESIGN.md edits are outside the repo.
2026-11-04 18:02:22 +00:00
host b0347b374d fix: PG init backfill rollback scope + audit-loop state upsert registration
- db: translate INSERT OR IGNORE ... SELECT to ON CONFLICT DO NOTHING
  (VALUES-only regex left SELECT form untranslated -> PG syntax error);
  register task_audit_loop_state (task_id) in _OR_REPLACE_PK so PG
  INSERT OR REPLACE becomes a real upsert instead of DO NOTHING.
- server: _backfill_default_quotas uses SAVEPOINT containment and no
  longer commits the caller's transaction, so a backfill failure cannot
  roll back init_db's migrate ALTERs.
- worker: _coord_child_rows rolls back before column-fallback retries so
  a missing-column failure on PG does not cascade into
  InFailedSqlTransaction.
- tests: test_pg_init_fix.py (translation, SQLite backfill/upsert,
  real-PG fresh-init + merge + upsert + backfill containment, skipped
  without CLOUD_TEST_PG_DSN/driver).
2026-11-04 18:02:22 +00:00
host d99853b684 test: cover id DESC tie-break for coord gate audit query (M1) 2026-11-04 18:02:22 +00:00
host 2387c21f36 fix: coord audit-loop review B1 + I1-I3 (gate scope, merged alignment, single-tx pushback, store-failure escalate) 2026-11-04 18:02:22 +00:00
host 017f76010f feat: coordinator audit-loop wiring at merge point
Parent audit loop sits after all children are aggregated and
before the parent final state; the single-task _audit_loop_wire
is never applied to parents or children. Parent gates are
per-child gates child:<name> (completed only); execution
failures map to one deterministic finding per child; subjective
findings are split off before evaluate and only audited as
needs_review. Gate-rejected children (capability/tool/approval
audit rows, or distinctive error markers) bypass evaluate and
escalate directly without consuming a round; tolerated, paused
and quota-missing parts are excluded, never findings. Rounds
are counted only in the parent task_audit_loop_state; on
pushback the packet is stored on the parent and split per failed
child into that child's audit_loop_packet column, consumed once
by the existing packet-injection path at next claim; pass and
escalate clear parent and child packets. POST /tasks now allows
audit_loop on coordinator parents (the old 400 rationale is
gone); children still take no external switch.
2026-11-04 18:02:22 +00:00
host f2005689fd fix: wire audit loop into worker completion path (pushback delivery, escalate, packet preamble)
Audit-loop wiring, worker completion path only: after the final
write-back (and only when the final is completed/failed, not quota,
not cancelled, not goal-converted, and the task carries audit_loop),
collect this attempt's deterministic gate results (execution,
goal_verify only when host reported goal_met, capability /
required_tools / gateway as passed-or-inapplicable markers, since a
pre-dispatch gate rejection early-returns before any runner round
and requeueing it would deterministically re-fail the same gate)
and call audit_loop_evaluate. Pushback stores the packet JSON in
tasks.audit_loop_packet and CAS-requeues the task; the next claim
prepends the packet preamble to prompt (and goal) with a data-only
separator, DB prompt untouched. Pass clears the packet. Escalate
sets failed (VALID_STATUSES has no needs_review, never invented)
with an escalation error and clears the packet; subjective
needs_review leaves the outcome untouched and only audits.
New tasks.audit_loop / audit_loop_packet columns follow the
attempts DDL pattern in worker ensure, server migrate, PG schema,
PG backfill, migrations backfill, and POST /tasks accepts
audit_loop (True/1 only, coordinator tasks rejected like v9).
Coordinator parent tasks return via _run_coordinator before the
attempt loop and coordinator children never inherit audit_loop,
so coordinator wiring is left as legacy, unchanged.
2026-11-04 18:02:22 +00:00
host 37b1aadc02 fix: bounded audit loop (deterministic gates only, 3 pushbacks, escalate)
Audit loop engine (cloud/audit_loop.py): Finding/PushbackPacket/LoopState pure functions; deterministic gates only, subjective -> needs_review; packet completeness enforced; standards-weakening -> escalate; regression and missing-gate checks enforce full re-run; MAX_PUSHBACKS=3, round>=3 -> escalate. Persistence in new single-row task_audit_loop_state (attempts DDL pattern: module CREATE_TABLE_SQL + db.py POSTGRES_SCHEMA + migrations list + server init + worker ensure); task_attempts NOT reused (attempt_no PK collision) and task_followups NOT reused (user messages). Worker.audit_loop_evaluate is the explicit opt-in hook (task audit_loop field only, no env switch, never called by run_one); escalate audits worker_audit_loop_escalated; corrupted/missing count fail-closes to escalate.
2026-11-04 18:02:21 +00:00
host 1b8b6e525d fix: 9.5-code reachable items (parse unification, goal branch, fail-open logs, M7 test, fingerprint)
Parse: agent_tools/tool_approvals column parsing unified into agent_def shared helpers (parse None = undeclared; server keeps None, worker maps to []/{} at _task_from_row, dispatch behaviour unchanged). Goal: _apply_resume_preamble goal branch proven reachable via _goal_convert_queued on a queued auto-resumed task, kept with a test and corrected docs (not dead code). Fail-open: audit write failure and preamble lookup failure now log warnings (best-effort unchanged); partial policy failure and shadow parse drops documented as known gaps. M7: shadow endpoint explicit_capability_failures_truncated test. Fingerprint: RoutingPolicy.content_fingerprint (canonical JSON sha256, version excluded) surfaced only in dry-run output, never in audit rows. M1 wording: legacy gap stated as fixed stock persisting per-task until completion, not converging.
2026-11-04 18:02:21 +00:00
host d95d678379 fix: N1 persisted-model recheck (recheck + block)
N1-1: writeback scope verified too broad (any chosen, incl. shadow
applied=False, worker_default and worker_explicit) and narrowed to
source=policy + applied only; writeback now records a
worker_model_persisted audit row as the identity marker.
N1: persisted models are rechecked against current tier requires
before dispatch on later attempts (fixes4 split known/unknown):
active + known-missing fails fast (worker_capability_recheck_
rejected, no runner), unknown-only and shadow only audit.
User-explicit, worker-CLI-explicit and gateway-filled tasks skip
the recheck; legacy models are recognised by a policy applied
worker_routing row. Recheck never rewrites tasks.model and never
re-decides.
2026-11-04 18:02:21 +00:00
host 18e3f1c82d fix: third-audit gate findings (explicit split, shadow explicit failures, N2-N5, N1 note)
Explicit gateway capability now splits known/unknown: known-missing
explicit models fail-fast under active (same shape as the gateway
default gate), unknown explicit models stay audit-only, shadow never
blocks; explicit priority now protects which model the user chose,
not that the chosen model must run. Shadow report reads
worker_capability_gateway_explicit rows as explicit_capability_failures
with a field note distinguishing the three explicit counts.
N2: coordinator requeue CAS rowcount=0 branch covered via a proxy
connection test (no refactor needed). N3: shadow endpoint test on
the create_server + real HTTP skeleton (capability, truncated, and
explicit-failure fields). N4: creation-layer rejection tests for
tools/instructions/tool_approvals on goal/coordinator tasks.
N5: model writeback failure logs a warning (best-effort kept).
N1: writeback capability-recheck trade-off documented in comment
only; recheck itself is a later batch.
2026-11-04 18:02:21 +00:00
host 621c08d10f fix: second-audit gate findings (B1, I1-I8, M1-M10)
B1: gateway capability check now triggers on walking the sub2api
gateway, not on the gateway having filled a model; task-level
explicit models are audited (worker_capability_gateway_explicit)
but never blocked, preserving explicit priority; gateway-default
models still fail-fast under active.
I1: dry-run adds a gateway-model section, sourced only from
CLOUD_SUB2API_DEFAULT_MODEL (never queries the relay); an active
tier whose requires the gateway model fails or cannot verify
blocks, shadow only warns.
I2: shadow review gate now counts completed samples
(completed >= 20), same basis for sample_shortfall; the endpoint
attaches per-tier capability status (read-only policy load) and
flags capability-failing tiers in the recommendation.
I3: configured-but-broken routing policy logs a warning in
load_routing_policy (unconfigured stays silent); the worker sets
routing_policy_load_failed and audits
worker_routing_policy_load_failed per task.
I4: resume preamble wraps prior output/error as data with an
explicit do-not-execute constraint; position verified in run_one
(tree -> preamble -> project -> original) and the docs corrected
from the false 'closest to the original prompt' claim.
I5: decided model is written back with a CAS UPDATE
(model IS NULL OR model=''), so later attempts reuse it;
explicit models are never overwritten.
I6: agent_* columns added to worker._ensure_result_columns and
db.init_postgres_schema backfill; migrations backfills them for
tasks before insert (sender_task_id pattern); column-level tests
for agent_tool_approvals and task_attempts.
I7: coordinator requeue adds AND status=<snapshot> (rowcount 0
skips requeue/audit) and filters paused_by_user / user_cancel /
user_pause children, which are never revived or cleared.
I8: explicit-model tasks (including coordinator children that
inherited a parent model) audit a source=explicit worker_routing
row; evaluate_shadow counts it as per-tier explicit_n only,
never routed_n, never a dedup slot.
M1/M2/M3/M5/M6/M8/M10: dedup slot moved after tier validation,
shadow sort doc fixed, truncated recommendation notes truncation,
seed cleanup on gateway/quota/goal-cap early returns, tools-only
coordinator definitions rejected, preamble goal branch marked
effectively unreachable, DESIGN.md drift corrected (outside repo).
M4/M7/M9: documentation/new-field only (pass_rate_fully_measured,
whitespace-model shadow-drop note, bound-note race note); old
field semantics unchanged.
2026-11-04 18:02:21 +00:00
host f9a77577c5 fix: audit findings (fallback dead code, silent fallback, registry re-parse, shadow sort, gateway capability)
- RoutingPolicy.fallback_models: move the cleaning loop into
  __init__ (it was unreachable after tier_mode's return, so the
  attribute never existed); still parsed but not applied.
- Capability fallback: RoutingDecision gains capability_fallback;
  the worker audits worker_capability_fallback when it fires, so
  a tier-model rejection followed by a worker-default fallback
  is no longer invisible (source stays worker_default, no
  model_routing event, so shadow samples are unaffected).
- Registry: resolve_registry_agent checks skills directly
  against the loaded definition instead of re-parsing a
  hand-built dict, so future fields cannot be silently dropped
  the way tool_approvals was in v9.
- Shadow: evaluate_shadow sorts rows earliest-first by
  created_at (id tiebreak) when timestamps are present, so
  dedup no longer depends on caller row order; the server
  path (which strips created_at) is unchanged.
- Gateway: when sub2api fills the default model, the worker
  now checks it against the tier's capability requirements
  with the same mode semantics as v8 -- shadow only audits,
  active fails fast with worker_capability_gate_rejected and
  never calls the runner. No worker-default fallback here:
  the gateway model is the task's gateway model, falling back
  to a worker default would run against the wrong gateway.
2026-11-04 18:02:21 +00:00
host a51919f24b feat: tool approval declarations (v9)
Agent definitions gain tool_approvals: known tool -> ask/auto/deny.
Unknown tool/policy is a 400, and required+deny is a 400
contradiction. The declaration is persisted (tasks.agent_tool_
approvals, PG + migrate), echoed in GET and the creation response
(via to_summary), and resolved for registry definitions -- fixing
a latent drop where resolve_registry_agent re-parsed without new
fields. At dispatch the worker audits the declaration and fails
fast on a defensive required+deny re-check (v3 tools-gate shape,
runner never called); goal/coordinator tasks reject the field at
creation like instructions. effective_tool_approvals maps a
declaration + host global approval to the actual host answer,
with ask_honoured always False: host answers elicitation with
one global switch and carries no tool identity, and the approvals
queue is record-only with no runtime ask channel, so per-tool
interception and credential scope are explicitly NOT claimed.
2026-11-04 18:02:20 +00:00
host cd33a929ca feat: capability-based routing (v8)
Policy carries an operator-declared model_capabilities directory
and per-tier requires (tool_calling/reasoning/vision booleans,
min_context_tokens). decide_routing checks the policy tier model
in active mode: capable applies, incapable falls back to a capable
worker default, and if both fail or are unknown it returns
capability_blocked, which the worker turns into a failed task
(v3 tools-gate shape, runner never called). Shadow mode only
annotates the reason, never blocks. Explicit task and worker CLI
models bypass the check by design. Invalid requires drops only
that tier (fail-closed, like invalid tier mode). Dry-run reports
per-tier capability status and blocks on active tiers that fail
or cannot be verified.
2026-11-04 18:02:20 +00:00
host 362dffba4f feat: evaluation loop (v7, deterministic scoring only)
Production code had no golden tasks, scorer, or rubric
(harness2.py states this explicitly); routing shadow gives
cost/status only and admits counterfactual not_measured,
so 'is the suggested model actually better' had no
mechanism. This track adds a pure module + CLI that scores
already-completed task records against human-curated golden
tasks and compares two runs, without running tasks, spending
model cost, touching the DB, or activating anything.

Golden tasks: JSON {id, prompt, tier, checks}; only four
deterministic check types (contains, not_contains,
status_completed, max_tokens, max_cost_usd, the last in
cost.py microdollar units); invalid definitions fail loudly.
Scoring: per-task score = passed / measured checks;
cost checks with missing/unpriced data are unknown, excluded
from the denominator and reported separately (never counted
as pass or fail); empty result_text is a string-check fail,
not unknown. all_pass means 'all measured checks pass' and
is reported alongside fully_measured_n so a pass rate is
never misread as 'all checks passed'. Comparison: per-run
stats, paired delta over golden tasks present in BOTH runs
(unpaired listed separately, missing listed, unmatched
records excluded with a warning), duplicate task_id rejected.
Paired n < 20 (same threshold as shadow) => ready_for_
decision false, no significance or p-values computed.

Deliberately NOT included: a task runner (golden tasks
still need a separate run-and-record step or manual task
creation), an LLM judge or quality score, and any routing
activation suggestion. A contains check can be gamed by
echoing, so golden sets must be human-curated with checks
that require actually doing the work; the example file in
eval/ is format-only, not a decision-ready set. Tests:
test_eval_loop.py (8 tests).
2026-11-04 18:02:20 +00:00
host 19fd326a73 feat: attempt ledger + resume preamble (v6)
Foundry 'platform restarts with original input' gap, after
reading the existing mechanisms: creation idempotency
(UNIQUE project_id+idempotency_key), goal checkpoints
(task_checkpoints per round, same-thread breakpoint resume
with no-progress fuse), merge/cost/notification CAS, and
coordinator child reuse already exist. The uncovered segment
is non-goal platform-interrupted tasks: auto_resume requeues
them fresh in the same worktree after clearing result/error/
interrupted_reason, so the new run gets only the original
prompt and cannot tell what the previous attempt had done.

This track adds, in worker/server layer only (no host.py or
codex thread change): task_attempts ledger, recorded by
auto_resume_interrupted in the same transaction before
clearing (attempt_no = old auto_resume_count + 1, kind
goal_breakpoint|platform_fresh, interrupted_reason,
checkpoint_round, result/error truncated to 2000 chars,
INSERT OR IGNORE keeps the first record on duplicate);
resume preamble injected in-memory only at claim for
platform_fresh reruns (previous attempt facts, truncated to
1000 chars, plus check-worktree-first and verify-before-
recreating-external-artifacts instructions; skipped for
coordinator/_resume/breakpoint tasks and when no record
exists, never fabricated); attempts surfaced read-only in
GET /tasks/{id}/timeline under existing project-read
permission. Fresh PG schema, migrations table list, and
server/worker ensure paths share one DDL.

Deliberately NOT claimed: the preamble is an instruction,
not a hard guarantee like creation idempotency or merge
CAS; the ledger does not track worktree files or external
artifacts (no worktree snapshot, executor-dependent paths;
hard dedup for those still rests on their existing gates);
manual retry paths do not go through auto_resume, so they
get no ledger record or preamble. Tests:
test_attempts.py (7 tests).
2026-11-04 18:02:10 +00:00
host c15c224570 feat: bounded subagent delegation (v5)
Coordinator parts gain three enforced bounds, validated in
validate_parts and carried through the coordinator_parts JSON
(no schema change): timeout_seconds (1..86400, per-attempt
wall clock from coordinator start; the coordinator poll loop
cancels only that child with the same cancel mechanism as
the global timeout, other parts keep waiting, audit
coordinator_child_bound_exceeded), on_failure fail|tolerate
(tolerated failures do not fail the coordinator, are named
in the report head, and their failed children are not
requeued on coordinator re-run while interrupted children
still are), and result_chars (0..2000, default 500, enforced
in build_report; the 4000-char report writeback cap still
applies). evaluate_outcome is the single outcome rule used
by both build_report and the worker error text, so tolerated
failures never leak into the coordinator failure reason.
Unknown part names fail closed.

Deliberately NOT added, after reading the execution layer:
per-part token/cost budget (cost is recorded only on
completion, so a running child's spend is unobservable and
creator quota is monthly, not per part), per-part round cap
(normal tasks have no per-task round cap in the worker),
and write-scope isolation (sandbox is per-project worktree,
a scope string in a prompt would be an instruction, not a
guarantee). Tests: test_delegation_bounds.py (8 tests).
2026-11-04 18:01:44 +00:00
host 7f60f12639 feat: agent version pinning (v4)
resolve_registry_agent in cloud/agent_def.py: agent_name may be
<name> or <name>@<version>, agent_version may also be given
separately; both given must agree or 400. Unpinned loads
<name>.json (default); pinned loads <name>.<version>.json and
requires the file's own version field to match, with no silent
fallback to the default file. Version strings are restricted
to alphanumerics/._- (no path separators, no ..); the
agents-dir traversal guard is a second line of defence.
Server: agent_version is only for agent_name (inline agent
with agent_version is 400); stored agent_name is the base
name, not the @ form. Snapshot fixture:
agents/research-brief.v1.json (same content as the default
v1 file). Tests: test_agent_version.py (7 tests).
2026-11-04 18:01:44 +00:00
host 8fe4ad2f38 feat: routing policy dry-run (v0d, never applies)
New module cloud/routing_dryrun.py: dry-run a merged policy
document or an env mapping (file first, JSON overrides whole
tier entries, CLOUD_ROUTING_MODE overrides global mode only,
never explicit tier modes), reporting per-tier effective
model/mode/mode-source, dropped tiers with reasons, absent and
unknown tiers, and would_activate/would_shadow/would_off.
Dropped or unknown tiers block (CLI exit 1); warnings alone
(no version, fallback_models parsed but not applied, failed
file/JSON sources that the loader silently ignores) do not
block but are always printed. Model names are NOT validated
against a catalog (deliberately not whitelisted) and sample
counts are NOT checked here (that is the shadow report's job);
the CLI never writes env, files, or worker state.

Tests: test_routing_dryrun.py (8 tests).
2026-11-04 18:01:44 +00:00
host 38357c472d feat: per-tier routing activation (v0c)
RoutingPolicy gains tier_modes: a tier entry may be a string
(model, mode inherits global) or {model, mode}. The explicit
tier mode overrides the global mode in both directions, so plan
can be active while goal/default stay shadow, and one tier can
be rolled back to off without touching the others. An invalid
tier mode drops that tier entirely (fail-closed: a typo must
never accidentally activate a tier, and never kills other
tiers). Tier-off tasks return worker_default and are not
audited as policy/shadow, so they never inflate shadow sample
counts. Explicit task and worker-CLI models still win over any
tier mode.

Tests: test_routing_tier.py (5 tests).
2026-11-04 18:01:44 +00:00
host bac2a39377 feat: routing shadow evaluation v0b (read-only, never auto-activates)
New pure module cloud/routing_shadow.py: parse worker_routing
audit details (old format without suggested model parses with
suggested unknown, never guessed), evaluate per-tier routed
samples with one-task-one-sample dedup, explicit tasks counted
separately, status under the chosen model, token/USD with
unpriced tokens kept apart, and a min-20 gate per tier. The
report explicitly marks the counterfactual not_measured:
shadow suggested models never ran, so no suggested-vs-chosen
cost/quality comparison is possible. Worker audit detail now
includes suggested/chosen for future rows. GET /routing-shadow
serves the report (project read permission, truncated flag
over 10000 audit rows, cost table ensured so a fresh DB does
not 500).

Tests: test_routing_shadow.py (5 tests).
2026-11-04 18:01:44 +00:00
host fd37505c48 feat: agent tools as required tools with tenant tool gate
Agent definitions gain tools (websearch/imagegen/playwright,
unknown tool is a 400), stored as agent_tools JSON and returned
by GET /tasks. This is deliberately required-tools, not an
allowlist: create_thread has no tool allowlist and tenant homes
are per-project shared, so blocking other tools cannot be
claimed. Instead the worker fails fast before any model call
when a declared tool has no [mcp_servers.<tool>] section in
the tenant codex-home config.toml (checked after _tenant_dirs
seeding, so a section seeded earlier by another worker counts
as present). The gate runs only for tenant-isolated Docker
workers; local/cloud workers skip it, documented, not verified.

Tests: test_agent_tools.py (8 tests).
2026-11-04 18:01:44 +00:00
host 1672ab9ff2 feat: agent definition v2 (registry, persistence, developerInstructions channel)
Registry: POST /tasks accepts agent_name, loading <name>.json
from repo-root agents/ (traversal-guarded, skill-pack re-validated);
agent and agent_name together is a 400. Persistence: tasks gains
agent_name/agent_version/agent_instructions (migrate + PG schema),
returned by GET /tasks; stored prompt stays clean. Channel: local
and docker dispatch pass --developer-instructions to host.py
dispatch -> create_thread(developerInstructions=...); run/fork do
not re-send. Cloud executor has no such channel, so the worker
falls back to the v1 marked header in the in-memory dispatch
prompt only (DB prompt stays clean). Goal/coordinator tasks
still reject instructions.

Tests: test_agent_def_v2.py (7 tests) + updated v1 suite.
2026-11-04 18:01:44 +00:00
host 7bb3c37a10 feat: declarative agent definition v1 (inline agent on task creation)
Agent definition is a versioned JSON document bundling the task
fields this codebase already honours (model/effort/approval/
executor/plan), plus instructions and required skills. At task
creation, explicit body fields win per field over the definition;
unresolved fields stay NULL and follow the worker-default chain
(and rule routing v0 when model is left empty).

Instructions reach the model only as a marked header prepended
to the stored prompt, because the dispatch path has no
developerInstructions channel; goal and coordinator tasks reject
instructions. Skills are validated against the cloud/skills pack
at parse time; seeding itself stays worker-global, documented
as the v1 boundary. Routing policy is not part of a definition
in v1 (worker-global), and unknown fields are rejected so typos
cannot be silently dropped. No new tasks-table column in v1;
definition identity is echoed in the creation response and via
the prompt header.

Tests: test_agent_def.py (11 tests).
2026-11-04 18:01:44 +00:00
host e364970b46 feat: rule routing v0 for model selection (shadow/active, fail-closed)
Pre-dispatch routing only fills in tasks with no explicit model,
classified by structural signals only (plan > goal > default).
Explicit task model and worker CLI --model win over policy;
policy via CLOUD_ROUTING_POLICY_JSON/FILE + CLOUD_ROUTING_MODE.
Shadow mode records a suggestion without changing dispatch.
Only policy/shadow decisions emit a model_routing event, so the
existing events contract (fallback tests) is unchanged.
Fallback rules, effort/approval/sandbox/quota and provider are
untouched. Tests: test_models_routing.py (20 tests).
2026-11-04 18:01:44 +00:00
host b391fe9320 resident P0+P1: DNS-level SSRF pin, batch budget, daily cap/cooldown/retention, PG schema/migration, dual-connection deadlock isolation, retarget baseline reset, proxy and structured failures, metrics
- DNS 级 SSRF pin:解析后 pin IP 建连,防 DNS 重绑定
- 批量总预算与每日上限/抖动冷却/保留期
- PG schema 与迁移清单
- 双连接死锁隔离
- 改目标重置基线
- 显式代理配置与结构化失败原因
- /metrics resident 口径
2026-11-02 03:30:57 +00:00
host 6525bcafd0 audit: resident website loop architecture and code quality to 10/10
Fix F1 name race with partial unique index, F2 pause/end mid-poll race with guarded updates, F3 redirect/numeric-IP SSRF, F4 quota prefix check, F5 duplicate row_get, F6 audit ternary; S1 unused import, S2 backoff doc, S3 defensive tearDown. Directed 20 pass, full 1700 pass.
2026-11-01 19:58:03 +00:00
host 23725f92e6 docs: resident website loop verification summary 2026-11-01 19:43:04 +00:00
host 561b3891fb test: resident website loop end-to-end with real fetch 2026-11-01 19:42:40 +00:00
host 0722ca3fd8 feat: resident watch tasks with pause/resume/end
Long-running website monitoring: new cloud/resident.py layer on top of
the automations claim/quota model (no task state-machine change).
States running/paused/ended; pause preserves the content-hash cursor
and stops polling, resume re-checks from the cursor, end is terminal
(resume after end is 409, a new watch is required). Polling is a cheap
fetch + hash comparison; only a real change creates a queued model
task. Quota exhaustion pauses (quota_exceeded) instead of ending and
keeps the cursor. Driver is the worker run_loop round boundary;
server exposes create/list/get/update/pause/resume/end with audit.
Console UI intentionally not wired this round (see verification
summary for deviations).
2026-11-01 19:01:39 +00:00
host 3a56f83114 websearch: 默认 auto 付费冻结,SearXNG 优先 + 显式解冻开关
- 默认 auto 只走免费链 searxng -> duckduckgo;即使环境里有
  付费 key 也不调用 Perplexity/Exa/Tavily/Brave,SearXNG
  失败也不回落付费,默认使用零付费花费。
- 付费后端代码/测试/文档全部保留;显式 pin(tavily/exa/
  perplexity/brave)仍照常调用,是有意覆盖不是默认。
- 解冻开关 WEBSEARCH_ENABLE_PAID,仅精确值 "1" 才恢复旧链
  (perplexity/exa/tavily/brave/searxng/duckduckgo),默认关;
  开关经 worker search_env 透传进 MCP config env。
- HttpClient 超时/DNS 失败改报显式错误(timeout after Ns /
  URL error),绝不把超时写成“无结果”。
- SearXNG 默认参数核实:代码无 SEARXNG_URL 硬编码默认值;
  SEARXNG_ENGINES 默认 bing,360search 已在基线;WEBSEARCH_TIMEOUT
  默认 20 秒。主机 env 实测仅 SEARXNG_URL=http://searx.diante.me
  一行,故默认 pin 生效;文档 https 漂移改回 http(主机 nginx
  仅听 80、无 443 vhost,https 在 TLS 层即失败)。
- 旧测试更新原因:test_websearch_mcp 原断言默认 auto 为
  perplexity 优先,与新冻结语义直接冲突,故改为断言冻结后
  的 searxng -> duckduckgo 回退 + 不泄漏 key;旧 perplexity ->
  exa 回退断言移至解冻开关测试(WEBSEARCH_ENABLE_PAID=1 下)。
  未删断言、未加 skip、未放宽门禁。
- 文档另删 WEBSEARCH_PROVIDERS 说法:代码从未实现该变量。
- 未推、未建 PR、未合并、未部署、未改主机 env/nginx/filter/
  证书、未做付费后端 live 验证、未创建生产任务。
2026-11-01 14:15:54 +00:00
host e9fa43dd8b websearch: task-level allowed_hosts extension for scope=code + JS-heavy extract evaluation (mock-first)
- search(..., allowed_hosts=[...]) extends (never replaces) the 15-domain
  code list for that call only; scope=general + allowed_hosts raises,
  empty/illegal values raise before any HTTP, dot-boundary matching,
  extension values never in error strings or attempted entries.
- Native-filter parity: Tavily/Exa get the effective list, Perplexity
  skips its native filter past its 20-domain cap (post-filter stays
  authoritative), Brave/SearXNG/DuckDuckGo rely on post-filtering.
- MCP web_search gains allowed_hosts passthrough (only when supplied,
  so older search_fn fakes keep working).
- JS-heavy evaluation documented in docs/web-search-mcp.md, mock-first:
  direct fetch is shell-only by construction; provider extract can
  supply rendered text in mock only; live SearXNG in this VM env
  failed (remote closed connection) and paid-backend live effects are
  unvalidated blockers, not claimed.
- Tests: test_code_search_ext (9) + test_js_heavy_extract (5 + 1 live
  skipped); existing websearch/code-search suites still green.

(cherry picked from commit 9c26bbe848438e1f34bcb000700aeb4c284f4f23)
2026-11-01 09:06:28 +00:00
host 125b436ab3 browser: click-path allowlist, safe tenant script upgrade, allowlist activation plan
- click/click_ref: pre-click allowlist gate on resolvable anchor href
  (same check as navigate; miss refuses before the click executes);
  click/click_ref/click_at: post-click landing URL check.
  click_at has no pre-click gate by design (coordinates name no
  element); JS-driven navigation, evaluate, meta refresh and popups
  are documented as unintercepable with the Tinyproxy 403 backstop.
- Tenant script upgrade is now safe: only proven-unmodified scripts
  (manifest match, 2a569ac known-shipped SHA-256, missing, or
  current) auto-merge; tenant-modified scripts are never overwritten,
  are reported as skipped, and get one deduped audit record. The
  manifest never learns a tenant hash (else the next upgrade would
  mistake a modified file for untouched). Idempotent reruns.
- Activation candidate = deployed Tinyproxy programming set encoded
  with leading-dot entries (plain entries would regress
  docs.python.org); npmjs.org per the deployed filter, not npmjs.com.
  Host activation itself is NOT performed this round: only the plan,
  validation tests and rollback are delivered.
Tests: 28 new (click 13, upgrade 8, activation 7), all fake/no-network;
98 browser-related tests pass including prior suites.

(cherry picked from commit e4a93f5748ec815f163d0bc096ff4dc4b8c651f5)
2026-11-01 09:06:28 +00:00
host 1141d0d0f8 deploy: sync autoscaler unit with production host (seed-skills, imagegen-mcp, playwright-mcp)
仓库版 ExecStart 只带 --websearch-mcp,生产主机现行 unit 在此之外
还带 --seed-skills --imagegen-mcp --playwright-mcp;按仓库重装会静默
丢掉技能播种与两路 MCP。同步后仓库 ExecStart 与主机现行 ExecStart
逐 token 一致,并补 deploy/tests/test_autoscaler_unit.py(5 项)拦
后续漂移:四项齐全、无重复、均在 cloud/worker.py 有定义、不在
scaler 侧、--docker-runtime 仍为 runsc。

(cherry picked from commit bc3a23fc068cac189c0173b8e1c2befa7b832ed2)
2026-11-01 09:06:28 +00:00
host da7a9950fb feat: platform browser multi-tab, navigation allowlist, close cleanup, egress filter, allowlist worker wiring
- Multi-tab: browser_new_page / browser_list_pages /
  browser_switch_page / browser_close_page in playwright_mcp.py;
  each new page gets a fresh Playwright context, so cookies and
  storage are isolated between pages. Single-page tools act on
  the current page; switching to a missing index is an error,
  not a crash; closing the last page is an error (use
  browser_close).
- Navigation allowlist: BROWSER_ALLOWED_HOSTS (comma list,
  leading-dot suffix match, case/port-insensitive) checked in
  _check_allowed_url before launching Chromium; unset (or empty)
  means no check, the old behaviour. file:// always allowed;
  localhost/127.0.0.1 have no special treatment.
- Close cleanup: browser_close closes all pages and best-effort
  removes cwd browser-live.png / browser-live.json; custom
  screenshot files are untouched. list_pages needs no launch.
- Egress filter: 10 programming-source Tinyproxy BASIC rules
  (python.org, nodejs.org, go.dev, pkg.go.dev, rust-lang.org,
  developer.mozilla.org, crates.io, stackoverflow.com,
  cve.mitre.org, nvd.nist.gov); already-present github.com /
  pypi.org / npmjs.org not duplicated; searx.diante.me stays
  exact-only, no diante.me wildcard.
- Allowlist worker wiring: _docker_task_context now passes a
  set, non-empty BROWSER_ALLOWED_HOSTS from the worker env into
  [mcp_servers.playwright.env] in the tenant config, so the MCP
  process actually sees it (container -e alone does not reach
  MCP subprocesses, 2026-10-30). Unset/empty writes nothing and
  never overwrites; the value never goes into logs; it is not
  added to the websearch env block or the container env.
  ensure_playwright_mcp_env now returns False without rewriting
  when the result would be identical (canonical join: one blank
  line before the block), so a second prep leaves an unchanged
  tenant config alone; other sections untouched.
- Skill + docs: browser-automation SKILL.md multi-tab/allowlist
  section; docs/platform-browser.md comparison doc (new file).
- Tests (mock/fake only, no network, no real Chromium, no real
  credentials): cloud.tests.test_playwright +
  cloud.tests.test_websearch_worker, 48 tests OK, including 6
  new wiring tests (switch off passes no variable, switch on
  reaches MCP env verbatim, empty never written, old-tenant
  merge idempotent + no-rewrite, ensure no-op when identical,
  allowlist enforces only after wiring).
- NOT done (see PR body): no persistent profile, no credential
  vault, no approval card, no CAPTCHA bypass, in-page link
  navigation does not pass the allowlist, no real-Chromium live
  verification, host tinyproxy filter not reloaded.
2026-11-01 08:34:13 +00:00
host 397a46f850 websearch: add scope=code programming search + provider extract chain
- search(..., scope=code) restricts to 15 programming domains
  (official docs, GitHub, registries, Stack Overflow, CVE/NVD):
  native include_domains/search_domain_filter where supported
  (Tavily, Exa, Perplexity), post-filtering for all providers
  (Brave, SearXNG, DuckDuckGo have no native filter).
- web_fetch now uses fetch_best: Tavily extract, then Exa
  contents, then direct fetch; both-fail errors name both
  failures, direct-fetch 403 under the egress whitelist is
  fail-closed, not bypassed. Default general scope and direct
  fetch_url behaviour unchanged.
- MCP web_search gains scope enum (general/code); docs updated.
- Tests all mock HTTP, no real keys.
2026-11-01 08:16:48 +00:00
host 08484302ac console: 项目导航移到左侧栏,工作台不再承担项目模块
左侧栏 ProjectNav:项目完整列表(名 + 短 id + 我的角色,
选中高亮,点击切换)+ 创建项目(名/GitHub 仓库/repo_url)
+ 选中项目成员(邀请/改角色/新增移除);工作台删除
sec-projects 一节与任务列表里的项目下拉框,侧栏为唯一
项目选择器,自动选第一个项目逻辑不变。
薄模块补齐(均为已核实后端路由):成员移除 DELETE、
自动化详情(GET 单条 + 产出)与编辑 PATCH、分诊条目点进
任务/自动化详情、Workers 搜索与在线筛选、管理区设置配额
POST。主线新功能(Todos、Browser、taskMeta 等)原样保留,
未做任何回退。console 标记单测本地 53 项通过。
2026-10-30 07:43:54 +00:00
host 4428a721a6 feat: auto-resume platform-interrupted non-goal tasks
Deploy restarts kill running tasks: the worker marks them
interrupted (worker_shutdown on graceful exit, sweep on stale
reclaim), and auto_resume_interrupted used to accept goal tasks
only, so every non-goal task killed by a deploy stayed
interrupted forever unless a human retried it (the 2026-10-30
demo task died this way).

Now worker_shutdown/sweep interruptions are re-queued for
non-goal tasks too: the platform killed the task, so the
platform re-queues it. The re-queue is a fresh run, not a
breakpoint resume: goal_resume_pending is forced to 0 and any
stale resume_message is cleared, so the new run cannot be
hijacked by the goal-resume path or an old continue message;
files in the same worktree persist. All existing guards stay:
user pause, coordinator orphans, quota (still goal-only),
backoff, the 3-strike stall guard (non-goal tasks have no
checkpoints, so each platform re-queue counts as no-progress),
and the auto_resume_max cap. Other non-goal reasons (NULL,
quota, user cancel) still do not auto-resume.
2026-10-30 15:39:55 +08:00
host 2d644f901b feat(console): automation panel edit and richer list
- AutomationPanel rows now show title, prompt content (160-char preview with expand/collapse), repo_url, last/next run, run_once badge and cancelled time.

- Active automations get an inline edit form (name/prompt/interval/repo) that PATCHes only changed fields via the existing automation_update endpoint; backend semantics unchanged (no last_run_at reset, cancelled stays terminal).

- Rebuilt cloud/console.html with tsc + vite (build green); automation python tests 18/18 OK.
2026-10-30 15:31:21 +08:00
host 962bb50a1c feat: browser stream waits through round gaps instead of disconnecting
Stale bridge state used to end the stream on the first stale read,
so every round gap and every worker restart killed the watch (the
2026-10-30 demo watch died this way when a 15:03 deploy restarted
the worker). Now a stale state switches to a waiting heartbeat
every 5s while the task is still alive (running/queued/paused/
interrupted), and frames resume when the next round's MCP starts
polling. The stream ends only on a terminal task state, a missing
task row, or 10 minutes of continuous staleness. The panel shows
a waiting status and keeps the last frame. Also force-close the
SSE connection on stream end: HTTP/1.1 keep-alive otherwise left
terminal streams hanging with no EOF.
2026-10-30 15:14:37 +08:00
host b977a9dee8 feat: sync shared-home MCP scripts on control-plane startup
租户 config 的 mcp_servers.* 指向共享 home 路径,但宿主共享
home 里原先没有 MCP 脚本,宿主 app-server 启动 MCP 即文件
不存在、握手断掉,面板 mcp-servers-list 三服务器 toolsError
全是 handshake failed;14:43 的手工拷入能修一次,但下次
MCP 脚本改动后面板又会跑旧版。

本批只在服务端 main() 启动时同步一次:清单唯一定义在
cloud/shared_home_mcp.py(7 文件:playwright_mcp/browser/
remote、websearch_mcp/websearch、imagegen_mcp/imagegen,含
同目录兄弟模块),解析优先用 host-config-host.json 的
codex_home,兜底用 tenants_root 父目录/codex-home;缺失
拷入、不同刷新(原子替换)、相同不动;源缺失/目标不可写
只记 warning,绝不阻塞启动。create_server() 不做同步,
避免单测写向生产共享路径。

被否决的落点:worker _tenant_dirs(只在下个任务派发时跑,
面板要等下一个任务才更新,且不修共享路径本身);deploy
host_deploy.sh(会在 shell 里复制第二份清单,且重启不
部署时不刷新);面板请求时同步(把读请求变成写请求,
失败会把面板读数变成 502 风险)。
2026-10-30 07:08:51 +00:00
host ce7fb3afd6 feat: project directory tree + first-round snapshot
- cloud/tree.py: structured worktree tree (dirs first, ignore
  .git/node_modules, symlinks not followed, entry/depth
  truncation flags, text render for injection)
- GET /tasks/{id}/tree with /files auth + worktree resolution,
  max_depth 1..5, max_entries 1..500, escape 400, missing 404,
  cloud/cross-host 409
- MCP get_task_tree translating to the new endpoint
- worker first-round injection of a repo-checkout top-level
  snapshot (2 layers, 120 entries) after repo resolution,
  labelled as checkout snapshot not worktree; skipped for
  coordinator/resume/fork, silent when no host source
- docs/project-tree.md; tests for tree, endpoint, injection,
  MCP translation
2026-10-30 14:46:11 +08:00
host 70ad8d7d09 feat: 任务详情 LLM 进度与失败原因标识(列表失败原因摘要) 2026-10-30 14:38:23 +08:00
host 56b8f98b47 perf: Chromium 启动 flag 微调 + screencast 画质 q60->q50
Flag:加 --no-first-run、--no-default-browser-check、
--disable-default-apps、--disable-extensions、
--disable-background-networking、--disable-sync、
--disable-translate,只跳过首跑/装配活,不改渲染与工具
行为。实测(runsc 容器,launch + new_page,各 3 次交替):
旧中位数 0.889s、新 0.819s——只差 70ms 且在逐次噪声
(0.70-0.99s)里,几百毫秒的预期不成立;零成本顺手留着。
画质:screencast 与 eager 抓帧 q60 -> q50,分辨率
1280x800 不动,点击坐标系不受影响。实测同页文本+渐变:
62068 -> 56428 字节(-9%),不是经验法则的 30%;觉得糊
一句话改回 60。
2026-10-30 14:27:04 +08:00
host cbf5e65a7a perf: 小优化组 + 恢复浏览器页签
1. MCP 空闲睡眠 50ms -> 10ms:#187 后单轮 drain 约 0.3ms,
   100 次/s 空闲迭代约 3% 单核,请求与远控输入拾取延迟
   从平均 25ms 降到 5ms。
2. stream 轮询 50ms -> 20ms。
3. stream payload 加 url/title;面板 stream 处理合并 url/
   title,5 秒 state 轮询只在未观看时跑(观看时 stream 已
   全量覆盖,轮询纯冗余;不观看时保留轮询,避免会话开始
   后页眉 stale)。
4. 恢复浏览器页签:#176(ccb5a86)改写 TaskDetailPanel
   页签时顺带删了 BrowserPanel 的挂载,导致线上控制台
   自 #176 起就没有浏览器页签(面板文件一直在,只是不
   被 import,构建产物因此不变)。本次把 import、页签
   与 Card 原样挂回;构建产物含“开始观看”标记可证。
console.html 由 npm run build:console 重打(342484 字节)。
console-redesign/ 为构建脚本审阅产物,不进仓库。
2026-10-30 14:09:02 +08:00
host 885bf38790 feat: add software-engineering skill v0.2
Add cloud/skills/software-engineering/SKILL.md (686 lines, v0.2, appendix with 9 role templates travels inside SKILL.md because skill_seed only copies SKILL.md, not templates/). Update skill pack tests: EXPECTED set and real-pack list now include software-engineering, plus an explicit seed test asserting it lands at skills/software-engineering/SKILL.md, contains the appendix, and tenant edits are never overwritten. One-line wording tweak in the architect template avoids the existing secret-scan false positive on the substring sk- inside agent-task-contract.md; deployed file sha256 bd0436399ac05c6131b9281dad00973e50d13540e4c56ad21f21e2b5209bae8e (40759 bytes, 686 lines).
2026-10-30 06:00:31 +00:00
host fafc0734c7 perf: screencast 隔帧改每帧,跟手优先
Page.startScreencast everyNthFrame 2 -> 1,画质 q60、尺寸
1280x800 不动。静态页不产生额外成本(帧只在重绘时来,
且 #187 起桥只写变化的帧字节);收益全部落在真正需要跟
手的时候:滚动、拖拽、动画期间帧率翻倍。测试断言同步。
2026-10-30 13:50:18 +08:00
host 910168126d perf: 桥变化才写、stdin 优先、stream 50ms 轮询
1. 桥写放大:旧代码每 ~50ms 轮询都无条件重写 state 和帧
   文件(约 40 次原子 temp+replace/s 走 runsc gofer,
   泵持久返回同一帧、updated_at 每轮都变,字节其实不变)。
   现帧字节不变不写;state 只在 url/title/started/holder/
   streaming 变化时写,或 0.5s 心跳一次(判死阈值 30s,
   绰绰有余);静态页写量从 ~40次/s 降到 ~2次/s。
2. MCP 主循环 stdin 优先:旧循环先 drain 再读 stdin,
   每次工具调用都先付一轮文件 IO。现 pending 请求先处理,
   空闲轮询 drain,忙时 200ms 保底 drain 一次防饿死。
3. stream 轮询 200ms → 50ms,画面延迟下限随之下降。
附回归测试:无变化轮询不重写、帧变化立即写、holder
变化绕过节流立即写。
2026-10-30 13:36:24 +08:00
host 2d840a6c37 fix: navigate 后立即推一帧,静态页开流不再等重绘
screencast 帧只在重绘时来,页面可能在 startScreencast 之前
就已画好——静态页于是开流后一直无画面,直到下一次重绘。
navigate 后用 CDP captureScreenshot 抓一张当前视口塞进帧
队列,泵在下一轮就把桥帧文件写出来;泵跳过空 session 的
ack(eager 帧没有可确认的 screencast 帧)。失败静默,
screencast 本身仍照常流动。
2026-10-30 13:14:38 +08:00
host ccb5a8638d feat: 任务详情 LLM 请求数(cost_records 存 completions) 2026-10-30 12:58:41 +08:00
host f85de63120 fix: MCP 主循环 stdin 批量行饥饿
select + buffered readline 会丢批量到达的行:一行 readline
可能把多行一起读进用户态 buffer,之后 select 再也看不到
空 fd,buffer 里的行永远不被读。客户端在 initialize 后
习惯把 notification + 下一请求连写,MCP 于是只答 initialize
就卡死(生产 2ff8975 活体验证两跑复现,faulthandler 证实
主线程停在 select)。改用非阻塞 fd + 自有 buffer 的行泵,
每行按到达顺序必达;附带回归测试(批量行全达 + EOF 残行)。
2026-10-30 12:47:23 +08:00
host 6bcdeb8da1 fix: 浏览器远控传输层改文件桥(Unix socket 在 runsc 下宿主不可见)
根因(2026-10-30 活体实证):#181 的 .browser-remote.sock 假设
宿主能连容器内 socket,但生产 Docker 任务走 runsc/gVisor,
其 gofer 挂载下容器内建的 socket 只在容器内可见,宿主目录
里只有普通文件(browser-live.png 一直能透传)。后果:server
代理对真实任务一律 503,会话根本没活过。

修法:传输层改四个普通文件(worktree 内,原子写 + 追加日志):
- .browser-remote-state.json:MCP 每轮重写,mtime 即存活信号,
  server 判 30 秒无心跳为会话死亡(503);
- .browser-remote-frame.jpg:最新 screencast 帧(base64 文本);
- .browser-remote-input.jsonl:server 追加,MCP 按偏移量消费
  完整行,执行时再判 holder,半行等下一轮;
- .browser-remote-control.json:{holder, seq},MCP 首轮即应用,
  接管可先于浏览器启动。
- holder 竞态:control 写下后 state 文件约 50ms 才刷新,server
  的 input/stream 改判 effective_holder(控制文件 mtime 新者胜),
  否则接管后立刻点会误 409。
- 线程契约与控制权模型不变;MCP 主循环仍是唯一触页线程。
- 播种刷新(#181)让老租户自动拿到新版脚本。

测试:socket 测试改文件桥测试(桥轮询/偏移量/半行/陈旧态/
effective_holder + server 代理全链),全量 1357 项通过(skipped 4)。
2026-10-30 07:28:32 +08:00
host e81ba8d2bf feat: 会话内 LLM 自判 goal / 自动化标记工具,不靠关键词
agent 在回合输出里写 goal-convert 块,worker 在轮次边界
排成 /goal 自后续消息、走已有 followup 转换路径把本会话
转成 goal;写 automation-create 块,worker 在轮次边界
创建自动化定义(权限 create_task、配额整轮门、创建人跟
父任务走、name 缺失自动生成、首次时刻已过去单块拒绝、
bind heartbeat 可绑回本会话)。

automations 新增 first_run_at / run_once:首次时刻只管
首次、之后按间隔算;只跑一次跑过后 is_due / due / claim
三处都不再到期(SQL 到期条件与 Python 口径共用同一
_DUE_SQL);API 创建与更新同步接受这两个字段。

刻意不向 LLM 自动注入工具指南:避免 prompt 臃肿,格式
只写进 docs/marker-tools.md 与 README,需要时由用户在
会话里手动告诉 agent;服务端不按关键词再猜意图。

单测 cloud/tests/test_session_goal_automation.py 12 项,
相关回归 218 项全过。
2026-10-30 07:15:05 +08:00
host f1aab5415c feat: 不用斜杠,单发 goal 也触发 /goal 目标
触发词从仅 /goal 放宽到 /goal 或裸 goal(大小写不敏感,
单数 only,goals/mygoal 不算):goal 收xxx、goal,目标是
收xxx、发goal 收xxx 均同款触发,新建与主线转换入口共用
同一解析。讨论句式仍不触发:goal 的用法、goal 是什么、
讨论goal的用法、/goal的用法 均返回 None。
单测补 4 项:裸 goal 触发、裸 goal 讨论不触发、followup
裸 goal 转换、新建裸 goal。
2026-10-30 07:15:05 +08:00
host f3bd65c171 feat: 新建任务的 prompt 里写 /goal 也自动变 goal
上一提交只覆盖了已有主线的转换,新建任务的 prompt 里写
/goal 时仍会当成普通一轮任务跑。本提交补上 POST /tasks
入口:prompt 经 cloud/goal_command.py 解析后,目标即新任
务的 prompt 与 goal,goal_mode 强制为 1(命令优先于 body
里的 goal_mode=0),plan/验收命令冲突仍按新建规则 400,
空目标 400 不建任务;讨论里提到 /goal 不触发。
单测补 5 项:/goal 直写、整句、空目标、plan/验收冲突、
讨论 prompt 不变。
2026-10-30 07:15:05 +08:00
host 3096e04345 feat: 主线 /goal 命令自动建 goal
发给主线的一句话(含字面 /goal)即可自动建 goal,不用先
知道主线是进行中还是已终态,也不用勾选项:
- cloud/goal_command.py:server 与 worker 共用的纯文本解析,
  只认命令形状(/goal 开头、分隔符/目标标记、开启类动词),
  讨论里提到 /goal 不误触发;空目标返回空串供调用方 400。
- server followup/thread/message/continue 三入口同款:queued
  原地转 goal,已终态复活并转 goal,running 排队后由 worker
  在轮次边界转 goal;已是 goal 409、plan 400、merged 409。
- worker 排空:/goal 不当普通轮次跑,CAS 把 running 主线
  改回 queued 并写 goal/resume_message,下次认领走 goal-resume
  在同一 thread 上开新目标循环;取消/回收时 CAS 失败则把
  followup 放回 pending,绝不把取消过的任务改回 queued。
- 单测 14 项:解析、三态转换、拒绝口径、普通消息不受影响、
  continue/thread 同款、worker 轮次边界转换 + 下次 goal 跑通。
2026-10-30 07:15:05 +08:00
host 7589876cff feat: 控制台浏览器远控面板
- TaskDetailPanel 新增“浏览器”页签,内嵌 BrowserPanel。
- 开始观看:fetch + ReadableStream 读 SSE(EventSource 发不出
  X-API-Key),最新帧以 data URI 显示;状态每 5 秒轮询。
- 接管/交还:POST /browser/control;接管后点画面按 1280x800
  换算坐标 POST /browser/input,键盘直达页面。
- 产物:cloud/console.html 由 vite 单文件构建重打(341451 字节,
  含 browser/state 与“浏览器远控”标记)。
2026-10-30 07:01:48 +08:00
host 6f39ab3945 feat: 浏览器二期后端:远控服务 + ref 快照 + server 代理
- playwright_remote.py(新):MCP 进程内 Unix socket HTTP 服务,
  监听 worktree/.browser-remote.sock;GET /state、GET /stream
  (SSE 画面流)、POST /input、POST /control(接管/交还)。
  线程契约(实测):Playwright 同步调用只能在创建页面的线程,
  所以 HTTP 线程只读属性/读最新帧/入队,MCP 主循环用
  select 轮询(50ms) draining 输入并 pump screencast 帧
  (无 ack 则 CDP 首帧后停流)。
- 画面流走 CDP screencast 帧(学 chrome-devtools-mcp),不用
  定时截图;ffmpeg 录像暂不做。
- snapshot_refs/click_ref(学 playwright-mcp 的 ref 机制)+
  mouse_move/key_press/type_text;agent 工具在 holder=user 时
  直接报错,等用户交还。
- server.py:/tasks/{id}/browser/{state,stream,input,control}
  代理到 worktree socket,复用任务鉴权与 worktree 解析;
  会话不存在时 503(轮次结束即消失是设计)。
- 播种器:MCP 脚本是系统文件,版本变化时刷新老租户旧脚本
  (script_refreshed 打日志),否则远控只对新项目生效;
  新增 playwright_remote.py 进播种清单。
- 队列遮蔽修复:cloud/queue.py 会遮蔽 stdlib queue,改用
  SimpleQueue(deque+锁),仓库模式与 flat-copy 两路都通。
- 测试:新 20 项(remote 服务、server 代理、ref/输入/
  screencast fake),全量 1354 项通过(skipped 4)。
2026-10-30 06:44:24 +08:00
host 814fe8f7c2 feat: 浏览器一期:坐标点击 + live 截图
- browser_click_at(x, y):视口坐标点击(1280x800、左上角
  原点),越界与非数字拒绝;live 视图里用户在截图上的
  点击落点就走这个工具。
- browser_live_shot:固定文件 browser-live.png + 状态
  sidecar browser-live.json(URL/标题/时间戳),watcher
  轮询一个稳定路径即可;技能要求每次动作后都调一次,
  保持画面最新。
- MCP 新增 browser_click_at / browser_live_shot 两个工具;
  技能补 live 视图一节;测试 9 项(含 fake mouse、越界、
  live 文件与状态读回)。
2026-10-30 06:19:55 +08:00
host cd3952590b test: 技能播种清单期望集加 browser-automation 2026-10-30 05:30:58 +08:00
host f67867694f test: 技能清单期望集加 browser-automation 2026-10-30 05:22:25 +08:00
host c9a483282d feat: 浏览器自动化 MCP(Playwright)+ browser-automation 技能
- playwright_browser.Browser:单页 Chromium,navigate /
  snapshot(正文 8000 字截断标记)/ click / fill / evaluate /
  screenshot(工作区内)/ close;启动参数 --no-sandbox +
  --disable-dev-shm-usage(runsc + 1003 用户实测必需),
  代理显式传给 launch(Chromium 在 Linux 下不稳定地读 env
  代理),浏览器路径 PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright。
- playwright_mcp:stdio MCP,7 个 browser_* 工具,同一进程
  单 Browser 实例;测试用 fake browser / fake factory。
- worker:--playwright-mcp flag,_tenant_dirs 播种两个脚本 +
  [mcp_servers.playwright],_prepare_tenant 写 env(代理 +
  浏览器路径,MCP 子进程不继承 Codex 进程 env,镜像 ENV
  在此必须重写,缺了会报 Executable doesn't exist)。
- 技能 browser-automation:JS 页、翻页、表单、截图时用,
  静态页优先 web_fetch / web_search;截图须 PIL 读回验证。
- 镜像(层构建,非本 PR):FROM latest + fonts-noto-cjk +
  playwright + chromium(npmmirror)+ install-deps,3.77GB;
  runsc + 1003 + docker-codex 条件下 JS 执行、example.com
  标题、截图均已实测通过。
2026-10-30 05:14:09 +08:00
host 9d6430a4d0 成本配额与文件只读标记:task-cost/quota-status/task-files 2026-10-30 05:11:06 +08:00