Commit Graph
106 Commits
Author SHA1 Message Date
host 9e56c1d266 code-to10 goal live update: POST /tasks/{id}/goal updates a running goal loop at the next round boundary
Server writes goal + monotonic goal_version + task_goal_history in one
transaction (CAS on version, approve_merge only, new-semantic goal tasks
only, terminal/merged 409, same-goal noop). Worker mirror thread (30s)
and breakpoint-resume claim hook write the update as a signal file next
to host-state.json; host run_goal_loop applies it at round boundaries
(top of round and before the declaration check, so a declaration made
for the old goal in a turn during which the goal changed is voided),
with an explicit old->new notice, same thread/worktree, rounds/usage/
budget/failed counters untouched. handler method accounting is a
contract update: 239 total / 223 mixins / 16 in server.py.

(cherry picked from commit 6841f287219d2b36cfbb8803642344b84dae4377)

Reconcile-port: handler_lifecycle.py contents redirected — _update_goal_task method + _goal_update_reject/_goal_signal_status pure functions + goal_live_update import into cloud/server.py (target keeps 6-mixin split); _task_to_dict goal fields into cloud/handler_support.py; migrate columns into cloud/server.py migrate_tasks_table; test patch target handler_lifecycle→server; test_handler_split_conformance.py kept at target version, to be re-pinned for the unified line in a final commit.
2026-11-03 02:49:27 +00:00
host 773945d136 v2 evidence: bind release evidence with ts/nonce, pin actual-only quota, ledger onboarding checklist, ledger failure outbox, read-only gate verifier (no live wiring)
- cloud/evidence_binding.py: bind/validate/nonce single-use store;
  release endpoint consumes nonce on first validated submission,
  replay -> 409 evidence_replayed, stale/future -> 409
  evidence_stale, partial -> 400, absent -> old path + label.
  Binding blocks exact replay and stale evidence, NOT restamping
  (unsigned evidence cannot prove content freshness).
- cloud/quota.py docstring: actual-only is the v2 integration
  position (already converged in v1 base via fef09ae, NOT in
  main); original PR semantics (#86 all-rows vs #90 actual-only)
  still pending coordinator confirmation at merge time.
- cloud/ledger_onboarding.py: DDL checklist referencing
  PG_DDL/PG_MARKER_DDL constants, read-only verify_ledger_ready
  and ledger_setup_report; default stays OFF, PG DDL stays an
  operator action (never claimed as executed).
- cloud/ledger_outbox.py + worker hook: ledger write failures
  go to a JSONL outbox in addition to the log and can be
  replayed; task outcome still unaffected (rule 7 kept),
  audit fail-closed untouched -- asymmetry documented, not
  silently changed.
- cloud/gate_verification.py: read-only verifier over an
  injected reader (match/mismatch/skipped); live wiring in
  merge/release is NOT possible on the current contract (no
  repo/ref in body or task row, no guaranteed trusted token),
  proven in the module header; no fake verification claimed.
- cloud/tests/test_v2_evidence.py: 18 new tests, all passing.

(cherry picked from commit bd85e33cd5e846fa6ee5d7e2440182dbd9c758b0)
2026-11-03 02:49:26 +00:00
host dc556b0c19 audit: lifecycle hook writes the attempt ledger (gap 3 follow-up)
- cloud/attempt_hook.py (new): canonical policy -- one counted
  dispatch = one row, is_defect always False from the worker;
  human/first rounds and human breakpoint resumes count;
  platform auto-resume reruns, coordinator fan-out, and
  pre-execution gate failures do not; transient retries inside
  one dispatch are not new attempts. Platform origin survives
  via the task_attempt_origin_markers table (same-transaction
  write on requeue, queued_at generation guard on consume).
  Defects are explicit-recorder only; corrections are
  append-only (record_correction, last-wins).
- cloud/worker.py: Worker(attempt_ledger_hook=False) + CLI
  --attempt-ledger-hook (default OFF = gap-3 behaviour exactly);
  marker write in auto_resume_interrupted, marker consume +
  classify in run_one, one ledger write at each terminal point
  (cancel branch, normal terminal block); all hook writes
  best-effort, never fail the task or block a requeue.
- cloud/attempt_ledger.py: corrects_attempt_no column (new DDL
  supersedes gap 3's pre-merge DDL) + record_correction +
  correction-aware aggregation; read path uses SELECT * so
  gap-3-era tables still read; cost code untouched.
- cloud/tests/test_attempt_hook.py: 32 tests (policy, markers,
  corrections, worker integration, mixed endpoint).
- docs/arch-conformance-metrics.md: policy, switch, PG manual
  steps (new PG_DDL + PG_MARKER_DDL), remaining user decisions.
2026-11-03 02:49:26 +00:00
host 41e8c98512 feat: cloud 门禁/超时/成本/沙箱收敛为单一判定源
- 新增标准库叶子层 timeouts/gates/quota/sandbox_policy:
  审批 3600 与单轮 600 各有名常量与环境覆盖层,已建项
  expires_at 永不重算;目录/控制/合并状态门禁统一判定,
  目录违规无任何覆盖参数;配额取数与比较唯一来源
  (USD 先于无价、命中均为 >=、无价只计 actual 行);
  /tmp 隔离矩阵与失联清理判定纯函数化。
- 调用点委托:server/worker/automations/auto_memory 的
  配额 SQL 与比较、server/tree/marker 的目录解析、
  marker 的控制门、sandbox 的任务容器名正则、两处 300 秒
  陈旧阈值默认值均收敛,别名与错误文案不变。
- 统一两个 P0 口径分歧:服务端无价计数由全部 usd IS NULL
  行改为 actual-only;显式不限额用户在服务端准入亦不受
  无价上限约束(与 worker 运行时一致)。
- 不新增门禁种类与 record 模式(主线无此实现);不改
  skill_seed/skills/SKILL.md;不改 v1.1 契约与分支保护。
- 新增 test_arch_converge.py 18 项。
2026-11-03 02:49:25 +00:00
host 06710cf47b feat: 沙箱规模——/tmp 顺序加严、注册表寻址与失联精确清理、超时边界抽取
- Docker /tmp tmpfs 移到全部 -v 之前(父挂载在前,seed 子挂载在后);
  宿主级 bwrap /tmp 仍宣告未隔离,口径见 tmp_isolation_status()
- workers 表补 host/pid/address(老库幂等补列),心跳透传与保留语义,
  _claim 写 claimed_by_worker;orphaned_containers 精确识别失联容器,
  Worker.cleanup_orphaned_containers 默认只杀 pid_verified 的精确容器名
- docker_run_timeout() 纯函数抽取(行为与原内联一致),README 补边界与
  未验证清单;新增 test_sandbox_scale.py 20 项
2026-11-03 02:49:25 +00:00
host 143ab30094 feat: cost/quota set — dual-column priced reports, creator split, hard cutoff, approval-timeout auto-threshold
- CostStore: per-model breakdown, report_by_creator (JOIN tasks.created_by),
  dual-column reports with priced_usd_micros, unpriced_models listed
  separately and a billing note (proxy price, not the relay bill);
  _report itself untouched, old exact-key tests keep passing.
- Hard cutoff: cost.quota_cutoff_reason is the single USD/unpriced-cap
  decision; worker._quota_exceeded now delegates to it (identical
  messages); claim gate, turn boundary and poll kill stop spend and
  record interrupted + quota reason + audit; auto-resume re-checks
  live quota before requeueing.
- Approval timeout (G7 ops metric): cloud/approval_timeout.py —
  default stays 600s, >20% timeout rate recommends 1800s, <5% keeps,
  mid-range keeps; stats + recommendation + info log; wired into
  /metrics (member projects, no log spam) and a global-admin
  /admin/ops/approval-timeout endpoint (sweeps overdue first).
- Server: task/project cost endpoints return dual-column reports;
  new GET /users/me/cost and GET /admin/cost-by-creator.
- Tests: cloud/tests/test_cost_quota.py, 10 tests.
2026-11-03 02:49:25 +00:00
host 8ea54cf66b 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-03 02:48:55 +00:00
host 7ba5bfaf81 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-03 02:48:54 +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 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 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 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 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 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 ccb5a8638d feat: 任务详情 LLM 请求数(cost_records 存 completions) 2026-10-30 12:58:41 +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 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 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
openqj cb6de0e3aa Merge pull request #178 from openqj/feat-playwright-mcp
feat: 浏览器自动化 MCP(Playwright)
2026-10-30 05:39:27 +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
host 2fd73c1121 任务列表 task-list 标记 + 首轮项目上下文注入 2026-10-30 05:11:06 +08:00
host 73288f08db 待办:todo-list/create/update 标记工具 + 首轮未完成待办注入 2026-10-30 05:11:06 +08:00
host 49fc27eef1 feat: Docker 沙箱 /tmp tmpfs 隔离 + 可选根只读(内层 bwrap 本体不在本仓修复范围) 2026-10-30 04:17:09 +08:00
host 7a920cee59 fix: imagegen MCP env 必须写死本地网关 base URL
首轮生产验收失败根因:_prepare_tenant 只把 IMAGE_GEN_* 透传
进 MCP env,没写 IMAGE_GEN_BASE_URL,也没带 SUB2API_BASE_URL,
于是 imagegen.py 回落到公网 sub2.diante.me,而它不在
Tinyproxy 白名单里,API 调用 403 Filtered。
修法:worker 在写 MCP env 前用 imagegen.build_mcp_env 算好
base(显式 IMAGE_GEN_BASE_URL > SUB2API_BASE_URL),并把
网关 host 加进 NO_PROXY;组装逻辑抽成可单测 helper,新增
2 项回归测试(本地网关解析、显式 base 优先)。
2026-10-30 03:07:31 +08:00
openqj da7b8e3a4f Merge pull request #167 from openqj/feat-imagegen-mcp
feat: 图片生成 MCP 工具 image_generate(中转站图片分组)
2026-10-30 03:03:07 +08:00
host 33bd14638f feat: 图片生成 MCP 工具 image_generate(中转站图片分组)
- cloud/imagegen.py:POST /v1/images/generations,解析 url 或
  b64_json,下载落盘到工作区;basename 校验防路径逃逸;
  缺 IMAGE_GEN_API_KEY 时明确报未配置,绝不回退任务自己
  的分组 2 key(分组 2 allow_image=False,回退只会 404)。
- cloud/imagegen_mcp.py:stdio MCP,工具 image_generate。
- worker --imagegen-mcp:租户 codex-home 播种脚本与
  [mcp_servers.imagegen],容器启动前写 env(系统级
  IMAGE_GEN_* + 代理 + NO_PROXY 网关 host)。
- image-workflow 技能:有 image_generate 时用它生成,
  无工具或报未配置时明说无法生成;改图暂无专用工具。
- 测试:imagegen 9 项 + worker 播种/env 3 项。
实测依据(2026-10-30):分组 4 key 调 gpt-image-2.5-flare
返回 200,URL 图为真实 PNG 1024x1024;此前分组 4 账号
schedulable=False 时全模型 404,调度开启后恢复。
部署注意(主机侧,不在代码里):env 需配 IMAGE_GEN_API_KEY;
Tinyproxy 白名单需加 qinglanapi.top(下载返回图 URL 用);
autoscaler 需加 --imagegen-mcp。
2026-10-30 02:53:30 +08:00
host 700641fa37 fix(worker): 无 result_text_full 列的老库/手工表写回回落,不拖垮终局
CI 失败根因:P0 取消守卫测试 mock 掉 _ensure_result_columns
并手工建表(无新列),带新列的 UPDATE 直接
OperationalError。修法:带新列写回失败且错误含
result_text_full 时回滚并用旧 SQL 重写(摘要照写、完整版
丢弃),其它错误原样重抛。
2026-10-30 02:46:37 +08:00
host 92b7f8e8ff feat(tasks): 完整结果另存 result_text_full,详情不再只剩 2000 字
旧链路三层截断:sandbox stdout 截 2000 字、worker 写回截
4000 字,长答案尾部在详情里永远查不到(只把上限调大是
把截断点后移,不是修掉)。修法:截断版保持不变(列表/
摘要用),完整版另存:
- sandbox _result_from_runner/_parse_host_result(local 与
  docker dispatch 路)回包新增 stdout_full(上限 200000 字,
  只防无界增长);goal 路结论在尾部、tail 已保结论,不动。
- worker _write_execution_result 新增 result_text_full 列
  (_ensure_result_columns + server migrate_tasks_table 幂等
  补列),终局与取消分支均透传 stdout_full。
- server:任务 dict 加 result_full_available(不直接塞全文
  进列表负载),新增 GET /tasks/{id}/result-full(read 权限
  同款校验,无完整版时 404,不回摘要冒充完整)。
单测:test_queue_sandbox 45 项(含新增 stdout_full 1 项)、
test_marker_tools 30 项。
2026-10-30 02:45:51 +08:00
openqj 6a5a837aac Merge pull request #165 from openqj/feat-doc-tools-result-cap
文档/图片工具进任务镜像 + 结果截断改为提取 text 上限 20000 字
2026-10-30 02:19:46 +08:00
host c1d25d11b1 feat: 文档/图片工具进任务镜像 + 结果截断从截断 JSON 改为提取 text 上限 20000 字 2026-10-30 02:03:14 +08:00
openqj aa5bf7da72 Merge pull request #157 from openqj/feat-final-audit-detail
fix(worker): 终态审计带上 error,复发卡死可定位
2026-10-30 01:53:39 +08:00
host f4fb4000e1 fix(worker): 终态审计带上 error,复发卡死可定位
2026-10-29 任务 A:认领后 61 秒 interrupted,但终态审计只记
task_id、自动续跑又把 error_text 覆盖掉,事后无法区分取消、
配额、host 返回 interrupted 还是其它失败;容器 --rm 后日志
也没了,根因至今无法从留存证据证明(不是 sweep:阈值 300
秒;不是外部取消:该时段无取消审计;是 worker 自己写回
interrupted,但第一轮的 error 已被第二轮 failed 覆盖)。

本批不谎称找到根因,只补可定位性:run_one 终态审计在
interrupted/failed 时 detail 带 error(截 500 字、去换行),
完成态仍只记 task_id;分段审计 worker_exec_start/error/done
已在现 main,复发时最后一段 + 终态 error 即能定位。
单测 test_final_audit_detail_carries_error:失败派发后
worker_failed 审计 detail 以 'A:' 开头且含 error 文本。
2026-10-30 01:38:01 +08:00
host 3a06205d8b feat: 跨 worker 真 kill(kill_requests 意图表 + 收殓器)与自动化触发入闸
机制 A 移植自旧 PR #101(分支 feat-residual-b,头 895e004),
不合旧 PR:旧基线 6e3eb7b 已 CONFLICTING,本批重做到现 main
6d4f87e(#151 合并后)上。

- kill_requests 意图表:cloud/db.py POSTGRES_SCHEMA + migrations
  清单补表;server.ensure_kill_requests_table / request_kill
  (同一任务只留一条 pending,写失败返回 False);init_db 的
  SQLite 分支补建表(PG 分支由 POSTGRES_SCHEMA 全量建表覆盖)。
- 取消/sweep 写请求 best-effort:_cancel_task 在 commit 后调
  request_kill(失败不挡取消);sweep_stale_tasks 在同一事务
  内为被扫任务写 system 请求。
- worker 收殓器 _reap_kill_requests:owner 活着(心跳停滞未超
  300 秒,COALESCE(last_heartbeat, updated_at))绝不抢杀;只
  有 owner 已死且任务仍 interrupted 时,按任务派生容器名
  (executor.container_name,codex-<安全字符任务 id>)在本机
  真杀,绝不按前缀扫、绝不杀自由传入的名字;本机无容器时
  请求照常终结;任务行失踪或已不在 interrupted 时请求直接
  终结;单条异常只记日志。run loop 每轮调一次,放在 sweep
  块外(旧 PR 只在 sweep 块内,sweep 关掉时请求会积压)。
- 自动化入闸只补两条旁路:worker._coord_sync_children 建子任
  务前过 _quota_exceeded(超限跳过建任务、记审计,同一 worker
  内同一分工只记一次);automations.run_due_automation
  (claim 抢单 -> 配额门 -> 建任务 -> 记产出,超限记
  skipped_quota 不进失败重试)。triage/webhook/续跑/后续/
  分叉未动。
- 单测:旧 PR 三份单测原样移植(Worker/Server 签名与现 main
  兼容,未改用例语义)。
2026-10-30 01:32:35 +08:00
host 71e31b22a0 websearch: MCP env 写进租户 config,不只靠 docker -e
2026-10-30 实测:Codex bash 里有 SEARXNG_URL + proxy,
MCP 链却只有 duckduckgo、DNS 瞬间失败——Codex 的 MCP
子进程不继承 Codex 进程 env;给租户 config 加
[mcp_servers.websearch.env] 后同一任务立刻返回真实
SearXNG 结果。修法:worker 在 _docker_task_context
算出网关后、容器启动前,用 ensure_websearch_mcp_env
把 proxy + 搜索 env 写进租户 config(网关此时才已知,
播种时写不死;写失败只记日志不阻塞)。
测试:新增 test_websearch_mcp_env 4 项,相关共 33 项全过。
2026-10-30 01:16:49 +08:00
host 08d56b15ce websearch: SearXNG 默认只查 bing,360search
默认 general 类别在 Docker 任务容器里要 20.0 秒才回
(wikipedia/wikidata/brave/duckduckgo/google 引擎全超时,
只有 bing+360search 回),撞 MCP 默认 20 秒超时;随后
DuckDuckGo 经代理也超时,整条链失败。代理本身无问题
(Tinyproxy 白名单早已放行 searx 与 duckduckgo)。
修法:_provider_searxng 默认 engines=bing,360search,
SEARXNG_ENGINES 可覆盖、空值恢复旧行为;worker 搜索
env 透传补 SEARXNG_ENGINES。容器实测:钉住后 0.26 秒
回 3 条(首条北京天气),auto 链 0.15 秒成功;旧 20.0 秒。
测试:新增 test_websearch_searxng 3 项,MCP/worker 共
21 项全过。生产未部署。
2026-10-30 01:16:49 +08:00
host 378cddfb1e align: 修正技能播种路径:codex-home/skills/ 才是用户技能目录
2026-10-30 主机实测(临时 codex-home,生产未动)纠正:
先播 .agents/skills/ 时 skills/list 只返回 5 个系统技能,
3 个自有技能一个没出现;改播 skills/ 后返回 8 个,
3 个自有技能以 scope=user 出现。系统技能本身就在
skills/.system/ 下。
- skill_seed DEST_SUBDIR 由 .agents/skills 改为 skills,
  模块文档与 worker 注释同步修正。
- 测试期望路径同步改为 skills/。
- 对照表补实测结论与附带发现:Codex 自带系统技能
  imagegen,图片生成在 Codex 本体里不是无技能。

验证:skill_seed + chatgpt skills + websearch worker
共 14 项全过。主机探针文件已全部清理,生产未动。
2026-10-30 01:16:49 +08:00
host cf9d17f035 align: 第二批:把技能包播种进租户 codex-home
仿 websearch MCP 播种,默认关:
- cloud/skill_seed.py:cloud/skills/ 的 SKILL.md 拷进租户
  codex-home/.agents/skills/(个人级技能路径,见模块
  文档的路径依据与诚实边界);幂等,租户改过的文件
  绝不覆盖(上报 differs),缺包不阻塞任务。
- cloud/worker.py:新增 --seed-skills,_tenant_dirs 里
  调用播种;老租户下一次任务自动合并;播种失败只记
  日志、不阻塞任务(技能是可选流程说明)。
- cloud/tests/test_skill_seed_worker.py:播种助手 + worker
  开关,仿 test_websearch_worker.py 的结构。
- docs/chatgpt-codex-alignment.md:补第二批说明与实测
  待办(真实 Docker 任务 skills/list + $live-info 调用,
  通过前不宣称生产任务已能用这些技能)。

验证:skill_seed + chatgpt skills + websearch worker
共 14 项全过。本批不改生产配置、不部署,autoscaler
加 --seed-skills 是部署时另行的主机步骤。
2026-10-30 01:16:49 +08:00
host 6f0eec71b6 worker: cover goal docker paths with prestart stage logging
PR #145 added docker_run_* lines only to _run_in_container, but
goal/goal-resume/goal-resume-breakpoint call _call_runner directly
(to keep full stdout for goal conclusions), so the production live
test (a goal task) produced zero docker_run_ lines. Extract a
_docker_call_staged helper and route all four task paths through it;
error lines log the class name then re-raise so each caller's
kill+failed handling is unchanged.

Also fix audit attribution: audit_log has no task_id column, and
worker_exec_done/error details carried no task id, so per-task
queries missed them (the rows existed). All three exec details now
start with task_id=. Tests: 2 new goal-path stage cases + a done
detail task-id assertion (8 total in test_prestart_stages).
2026-10-29 17:46:12 +08:00
host 9d90d97046 worker: prestart stage logging for dispatch stalls
Claim-to-dispatch stalls (cf. 2026-10-29 task A: 61s to interrupted
with zero detail) were invisible because docker run blocks with
captured output and watchdog kills were silent. Add three layers:
- worker_exec_start/done/error audit rows in _run_attempt (PG,
  survives --rm), start/done pairing shows whether an attempt died
  inside the executor call;
- docker_run_start/done/error log lines in DockerExecutor (container
  + image only, never the inner command/prompt);
- stdout tail (last 2000 chars) to the worker log on failed or
  interrupted results, and a log line on both watchdog kill paths.
Tests: cloud/tests/test_prestart_stages.py, 6 cases incl. a
no-prompt-in-logs assertion.
2026-10-29 17:07:53 +08:00
host e74f20608b worker: auto-resume falls back to fresh goal for never-started tasks
2026-10-29 live test: task A was interrupted 61s after claim but had
never written anything (no host-state entry, no worktree, no trace,
no checkpoint); auto-resume ran goal-resume, which host rightly
refused, and the task failed. Refusal only makes sense for tasks
with progress but a lost breakpoint (those go through the P0
checkpoint rebuild); for a never-started task the only meaningful
outcome is a fresh run, identical to its first attempt (same task
id, same goal, worktree does not exist yet so nothing is
overwritten).

Change (claim path only, via _resolve_breakpoint_resume):
- rebuild from checkpoint first, unchanged;
- if rebuild fails AND _task_never_started holds (state entry,
  checkpoint row, worktree dir, trace file all absent), clear the
  breakpoint flag so the run takes the fresh goal path, with a
  worker_fresh_restart audit event;
- any evidence of prior execution, or any check failure,
  keeps the old refusal path (fail-closed); the auto-resume
  count cap and no-progress stall guard still bound retries.

Tests: cloud/tests/test_neverstart_fresh.py, 10 items (fallback
holds, entry/checkpoint/worktree/trace presence each block it,
corrupt state blocks it, rebuild still wins when a checkpoint
exists); test_goal_resume's breakpoint-routing test now sets up
a task with a checkpoint so it still exercises goal-resume.
Full suites: cloud 1178 (skipped 4), host 104, deploy 39, green.
2026-10-29 16:45:07 +08:00
host 8c719c7e62 断点镜像状态标签回落:host-state 条目写 status 不是 goal_status
生产实弹:极小 goal 任务的 timeline 里 checkpoint goal_status 为
None——host.py 的任务条目只有 status(goal-running/goal-met),
没有 goal_status 键,镜像原样读 goal_status 永远是空。修法:
entry.get('goal_status') or entry.get('status')。轮次、thread_id、
state_sha 等关键字段不受影响(熔断只看轮次),纯标签 fidelity
修复;单测加 status-only 条目镜像断言。
2026-10-29 15:31:07 +08:00
host 585242fc8f 长任务 P0 三件:断点进库、错误分类+熔断、Run/Step 时间线
断点进库:task_checkpoints 表(PG 新库 + 老库幂等补表 + 迁移清单
对齐);worker 在 goal 任务执行期间每 30 秒把磁盘 host-state 断点
镜像进库,终局再补一次;断点续跑认领前,若 host-state 条目丢失
但库里有断点,先按库断点重建最小条目再跑,库里也没有断点时不
造假、走原有拒绝路径。

错误分类+熔断:sandbox 四处解析加永久错误分支(KeyError、配置
缺失等无结论异常退出判 failed 死信,不再判 interrupted;host 已
明言 interrupted 的不受影响,未知错误不分类);worker 自动恢复
加退避(10 秒起指数退避、封顶 300 秒)与无进展熔断(库断点轮次
连续 3 次不变即标 auto_resume_stalled 等人工)。

时间线:task_steps 表 + host 每轮写 host-steps 步骤文件 + worker
导入;GET /tasks/{id}/timeline 一次返回 steps 与 checkpoints;
认领、暂停、自动恢复、终局都记步骤。旧任务返回空数组,不编造。

单测:cloud/tests/test_p0_three.py 14 项(分类、熔断、镜像重建、
步骤导入去重、timeline 接口)。
2026-10-29 15:13:01 +08:00
host ae3aff8602 feat: 长任务无敌补齐三件:连续失败上限 + worker退出审计 + 配额恢复自动续跑
1. host goal 循环连续失败护栏:3 轮连续失败即中断出口,
   任务记 interrupted、可断点续跑,不再无限空转
   (2026-10-29 实弹:断点后 274 轮全失败)。
2. worker 退出杀残留路径补审计 worker_shutdown_interrupted,
   detail 为任务 id,与 sweep_interrupted 同口径。
3. 配额中断自动恢复:每轮现查配额,仍超限/查不到时留
   在 interrupted 不重排队,下轮再查;现查为 None 时
   (新月份/额度上调/无价上限重置)才 CAS 回 queued。

单测:配额仍超限不动、配额恢复续跑、连续 3 轮失败停转。
2026-10-29 13:07:57 +08:00
host c9cfe22a36 feat: 长任务中断自动恢复,仅用户暂停才暂停,不限时
- 新增 tasks 列 paused_by_user/interrupted_reason/auto_resume_count/no_time_limit(幂等迁移,server+worker 双侧)
- POST /tasks/{id}/pause:用户暂停标 interrupted + paused_by_user=1;/cancel 同款标记;人工 /retry、/goal-resume 清标记
- sweep 标 sweep、配额标 quota、协调员孤儿标 coordinator_orphan、worker 退出标 worker_shutdown
- worker.run_loop 每轮后 auto_resume_interrupted:只接系统中断的 goal 任务,CAS 回 queued + goal_resume_pending=1(G10 断点续跑),用户暂停/配额/孤儿/非 goal 不接,上限 50 次防无限烧额度
- no_time_limit=1 的任务 worker/Docker 级不限时(其它任务仍默认 1800 秒;host 单轮 600 秒仍是单轮护栏)
- 前置 cherry-pick:G10 断点续跑透传 model_provider(否则 Docker 断点续跑在 sub2api 下必死,自动恢复无法实证;本 PR  supersedes #127,勿另行合并 #127)
2026-10-29 11:37:06 +08:00
host a19b1567bb fix: G10 断点续跑透传 model_provider(sub2api 续跑失败修法)
断点续跑容器不带 --model-provider 时 Server 的 provider 表里
没有 sub2api,thread/resume 报 provider not found(probe15 实弹)。
Docker/Local 断点方法加可选 model_provider 并转发,worker 两处
调用点传 gateway_provider;补单测断言断点命令含
--model-provider sub2api(旧代码下两新测试 1 失败 1 报错)。
2026-10-29 11:28:39 +08:00
host ebfae7b992 worker: always start capacity pool threads in run_all
run_all previously sized its pool from the ready snapshot at round start (min(capacity, ready_count)). If a round started with only 1 ready task (e.g. a just-claimed PM), only 1 thread ran; that thread entered a marker wait (up to 3600s) after its model turn, and children created/retried mid-round had no free thread to claim them. run_loop is blocked joining that round, so no new round starts; if the other worker had already scaled down, the queued child waited out the full wait timeout unclaimed. Fix: always start capacity threads. Idle threads exit immediately when no task is ready and nothing is active, and poll every 0.2s while something is active, so mid-round arrivals are claimed. Test: run_all with capacity=2, 1 initial task whose mocked run_one creates a child then polls for it; without the fix the PM poll times out in 5s (1 result), with the fix both tasks dispatch in the same round (0.2s).
2026-10-29 05:41:35 +08:00
host 14757f15c4 feat: 子任务默认模型/强度(child_model/child_effort)
主任务可显式声明派生默认,或在 prompt 里写“子任务用 gpt-6-luna max”
由服务端解析填入;marker task-create 与协调员分工未显式写模型/强度时
按 块/分工 > 派生默认 > 父任务模型/强度 继承,fork 复制派生默认,
continue 可改派生默认;console 新建/继续表单与任务详情同步展示。
2026-10-29 05:27:22 +08:00
host ccb22c960d worker: heartbeat parent task during marker poll/wait loops
Marker waits run after _run_attempt has cleared _running, so the run_loop heartbeat thread no longer refreshes the waiting parent. sweep_stale_tasks (default 300s) then kills a parent waiting up to the 3600s marker timeout, as seen in the marker live test (PM2 swept mid wait-threads). Poll and wait loops now heartbeat the parent each iteration (interval clamped to <=60s, well under the 300s threshold), best-effort, without touching _running, so no new capacity leak and cancel still works (empty _running entry has no proc/container to kill). Tests: 2 new sweep-survival tests (stale heartbeat at -299s, 2s wait, sweep must spare the parent); both fail 2/2 on pre-fix code and pass with the fix.
2026-10-29 04:40:47 +08:00