Metadata-Version: 2.4
Name: mawflow-host
Version: 0.2.5
Summary: Headless node runner for MultiAgentWorker
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: cryptography<50,>=46
Requires-Dist: httpx<1.0,>=0.28
Requires-Dist: keyring<26.0,>=25.7
Requires-Dist: mawflow-seed-kit==2.0.0
Requires-Dist: PyYAML<7.0,>=6.0
Requires-Dist: qrcode<8.0,>=7.4
Requires-Dist: websocket-client<2.0,>=1.8
Provides-Extra: dev
Requires-Dist: pytest<9.0,>=8.4; extra == "dev"

# Node Runner

这里用于放置节点执行器。

节点执行器是多机协作里的执行层，后续负责：

- 向控制面注册和心跳上报
- 拉取待执行任务
- 在本机执行命令或 Agent 流程
- 回传日志、状态和产物
- 在任务失败时执行重试或恢复逻辑

当前运行模型已经支持两层：

- 单节点运行：一个进程对应一个逻辑节点
- 宿主服务运行：一台宿主机只启动一个 `host supervisor`，由它按控制面下发的节点配置动态拉起、关闭和重启本机子节点进程

## 当前目录约定

- `src/`
  运行时代码
- `resources/host-program-rules/`
  可通过宿主机增量包同步的低风险 prompt / planner / 执行约束规则。Node Runner 生成 Codex prompt 时会读取其中的 `codex-prompt-rules.json`；文件缺失时回退到内置默认规则。
- `configs/`
  节点侧配置模板和启动配置
- `tests/`
  Node Runner 的基础测试

## 当前对接说明

- 协议文档见 [PROTOCOL.md](PROTOCOL.md)
- 首期先对接：
  - 节点注册
  - 心跳上报
  - 节点列表读取

## 当前实现状态

- 已提供 Python 版最小可运行骨架
- 已提供 TOML 配置加载
- 已提供注册与心跳循环
- 已提供任务领取与状态回报
- 已提供 `stub_local` 占位执行器
- 已提供 `codex_prompt` 第一版桥接执行器
- 已提供 `codex_headless` 非交互 Codex CLI 执行器
- 已提供 `agent_bridge_prompt` / `agent_bridge_headless` Agent Dialect Bridge 执行模式，保留 Codex 官方路径的同时生成中立 agent 产物
- 已提供官方 CLI 优先、官方 API runner 次之、集成 CLI 和 cc-switch fallback 的 provider runner skeleton；当前测试使用 fake CLI/API，不调用真实外部 provider
- 已提供本地状态快照、环境快照和日志输出
- 已支持每条任务写入可读记录文件与事件日志
- 已支持每次心跳刷新本地 `current-briefing.json / current-plan.md / current-tasks.md / node-memory.md / project-memory.md`，并在执行中同步到当前任务 workspace
- 已支持 `host supervisor` 模式：宿主机注册、拉取受管节点配置、动态维护子节点进程、写聚合状态文件

## 本地运行方式

```bash
cd code/node-runner
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp configs/node-runner.example.toml configs/node-runner.local.toml
export MAW_NODE_ENROLLMENT_TOKEN="<node-enrollment-token>"
export MAW_HOST_ENROLLMENT_TOKEN="<host-registration-token>"   # optional
maw-node-runner --config configs/node-runner.local.toml --once
```

宿主服务模式：

```bash
cd code/node-runner
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp configs/host-supervisor.example.toml /tmp/host-supervisor.local.toml
maw-node-runner --mode host-supervisor --config /tmp/host-supervisor.local.toml --once
```

`[control_plane]` 里建议优先使用 `node_enrollment_token` / `host_enrollment_token` 配合环境变量注入；为了兼容旧配置，`enrollment_token` 仍然会被识别为节点注册 token。

## 当前执行模式

- `stub_local`
  当前默认模式。Node Runner 会领取任务、创建独立 workspace、写入任务快照和结果摘要，然后将任务回报为完成。
- `codex_prompt`
  当前稳定推荐模式。Node Runner 会领取任务、生成可直接发给 Codex 的 prompt 与交接文件，并将任务回报为 `blocked`，表示等待 Codex / 人工继续接手。
- `codex_headless`
  实验模式。Node Runner 会领取任务、在 workspace 中写入 `task.json`、`codex-prompt.md`、`codex-handoff.json` 等上下文文件，然后直接调用本机 `codex exec`。执行成功回报 `completed`，失败回报 `failed`。
- `agent_bridge_prompt`
  Bridge 交接模式。Node Runner 会生成 `maw-task.*`、`agent-task.json`、`agent-prompt.md`、`agent-command.json` 等中立产物，不真实调用 provider，然后把任务置为 handoff/blocked，等待人工或外部 agent 接手。
- `agent_bridge_headless`
  Bridge headless 模式。Node Runner 通过 provider registry 按 `official_cli -> official_api_runner -> deepseek_api_runner -> integration_cli -> cc_switch_fallback` 选择 runner，执行后写入 `agent-output/*` 与 `agent-result-normalized.json`，再归一为 MAW 兼容的 `execution-result.*`、`task-record.md`、`task-events.jsonl` 等产物。非 Codex provider 默认用于 planner/reviewer/docs/log_analysis/patch_suggestion 等低风险任务。

## Codex 额度切换

`codex_headless` 现在支持多账号额度接力：

- 可在 `[codex]` 下配置 `[[codex.accounts]]`，为每个账号指定独立 `key / codex_home / home`
- 当某个账号命中 `insufficient_quota / quota exceeded / usage limit` 一类错误时，Node Runner 会把该账号标记为冷却中，并立即切到下一个可用账号继续同一任务
- 如果这台宿主机上的所有账号都没有额度了，Node Runner 会先等待最早恢复的账号；如果配置了正向等待上限且仍然没有可用账号，任务会进入 `waiting_human` 并生成节点恢复审批
- 在 `host supervisor` 模式下，所有子节点默认共享同一个 `codex-account-pool.json`，避免多节点同时反复撞到同一个已耗尽账号

推荐字段：

- `quota_exhausted_cooldown_seconds`
  某个账号被判定额度耗尽后，默认冷却多久再重新尝试
- `quota_wait_poll_seconds`
  所有账号都耗尽时，等待期间多久做一次心跳和状态刷新
- `quota_wait_timeout_seconds`
  可选总等待上限；设为 `0` 表示继续等待最新恢复的账号
- `account_pool_state_file`
  账号池共享状态文件；单节点模式可留空，宿主服务模式建议让整台宿主机共享

最小示例：

```toml
[codex]
quota_exhausted_cooldown_seconds = 1800
quota_wait_poll_seconds = 60
quota_wait_timeout_seconds = 0
account_pool_state_file = "/tmp/codex-account-pool.json"

[[codex.accounts]]
key = "acct-a"
codex_home = "/Users/example/.codex-acct-a"
home = "/Users/example/.home-acct-a"

[[codex.accounts]]
key = "acct-b"
codex_home = "/Users/example/.codex-acct-b"
home = "/Users/example/.home-acct-b"
quota_exhausted_cooldown_seconds = 2400
```

## Host Supervisor 模式

当运行在 `--mode host-supervisor` 时：

- 宿主服务会先完成宿主机注册
- 然后调用控制面 `GET /api/v2/hosts/{host_key}/managed-nodes`
- 控制面返回这台宿主机当前应维护的逻辑节点清单，以及模板生效后的最终运行参数
- 宿主服务为每个逻辑节点生成独立运行目录和临时配置
- 如果后台调整了节点配置、暂停了节点或把节点迁走，宿主服务会自动停止或重启对应子进程
- 宿主服务会把聚合状态写到 `runtime/node-runner-state.json`，保持现有“看最近任务”的脚本逻辑可继续使用
- 宿主服务支持 `apply_host_incremental_package`，用于同步 `resources/host-program-rules` 这类低风险规则文件；用户要求更新宿主机程序且未明确禁止重启时，下发成功后立即执行宿主机服务重启流程，让下一轮任务使用最新规则。

## Host Manager Web

Host Manager 默认入口已迁移为 React/Vite 静态应用，继续由 Python Local API 在 `127.0.0.1:8765` 同源提供，并随 Python wheel 和三平台 Host Program 发布包离线分发。前端源码、构建命令和兼容路由见 [`web/host-manager/README.md`](web/host-manager/README.md)。

复杂运维功能已迁入 React 控制面；其中“AI 服务 → 账号池”保留 Codex 官方账号添加、登录、启用/禁用、移除，以及按指定账号和项目入口打开 Codex CLI。`/local/legacy` 固定返回 410，内嵌旧控制台已物理删除。Local API 会话、CSRF、敏感信息裁剪和 localhost 信任边界保持不变；平台托管设备可在“更新与诊断”中显式检查、确认和回滚 Host Program，客户自管设备的 Host update/doctor 继续要求在本机 CLI 审阅计划后显式执行。

## HostService 与厂商路由

客户自托管宿主机可把一台物理机器拆成多个 HostService。每个 HostService 隔离账号池、HOME、CODEX_HOME、CC_SWITCH_CONFIG_DIR、runtime、workspace 和日志。

Node Runner 配置支持：

```toml
[host_service]
key = "customer-pc-01-api-planner"
agent_runtime = "codex_cli"
routing_mode = "cc_switch_cli"
account_pool_kind = "api_key_provider"
allowed_vendors = ["deepseek", "qwen"]
preferred_vendor = "deepseek"

[cc_switch]
enabled = true
app = "codex"
config_dir = "/opt/maw-host/host-services/api-planner/.cc-switch"
proxy_port = 15722
```

旧配置不包含上述段时，默认按 `codex_official` 兼容处理。节点只上报角色和厂商偏好，不保存明文 API Key；非 Codex provider 的真实密钥只保存在客户本机 Host Manager SecretStore。

Agent Bridge provider 配置使用禁用模板，不包含真实 key：

```toml
[agent_bridge]
enabled = false
default_runner_kind = "official_cli"
default_dialect = "codex_v1"
fallback_order = ["official_cli", "official_api_runner", "deepseek_api_runner", "integration_cli", "cc_switch_fallback"]

[agent_cli.claude_code]
enabled = false
vendor = "claude_code"
runner_kind = "official_cli"
executable = "claude"
dialect = "claude_code_v1"

[agent_api.deepseek]
enabled = false
vendor = "deepseek"
runner_kind = "deepseek_api_runner"
dialect = "deepseek_api_v1"
api_base_url = "https://api.deepseek.com"
secret_ref = "env:DEEPSEEK_API_KEY"
supports_workspace_write = false
default_intent = "patch_suggestion"
# 高质量规划/验收池可改为 model=deepseek-v4-pro；默认池使用 deepseek-v4-flash。
command_template = ["model=deepseek-v4-flash", "api_protocol=chat_completions"]

[[account_pools]]
key = "pool-deepseek-main"
pool_kind = "api_key_provider"
vendor = "deepseek"
provider_key = "deepseek-main"
runner_kind = "deepseek_api_runner"
dialect = "deepseek_api_v1"
model = "deepseek-v4-flash"
priority = 20
selection_strategy = "priority_first"
allowed_models = ["deepseek-v4-flash"]
runner_kinds = ["deepseek_api_runner"]

[[account_pools.accounts]]
key = "deepseek-main"
priority = 20
model = "deepseek-v4-flash"
secret_ref = "env:DEEPSEEK_API_KEY"

[[runtime_profiles]]
key = "profile-deepseek-planner"
display_name = "DeepSeek planner via MAW runner"
account_pool_key = "pool-deepseek-main"
account_selector = "all"
vendor = "deepseek"
runner_kind = "deepseek_api_runner"
dialect = "deepseek_api_v1"
model = "deepseek-v4-flash"
reasoning_effort = "medium"
codex_cli_enabled = true
codex_provider_mode = "responses_bridge"
workspace_write_policy = "report_only"
allowed_node_roles = ["planner", "acceptance", "executor"]
allowed_models = ["deepseek-v4-flash", "deepseek-v4-pro"]

[[node_account_pool_grants]]
node_name = "customer-pc-01-planner"
account_pool_key = "pool-deepseek-main"
priority = 20
runner_kinds = ["deepseek_api_runner"]
role_scope = ["planner", "acceptance", "executor"]

[[node_runtime_profile_grants]]
node_name = "customer-pc-01-planner"
runtime_profile_key = "profile-deepseek-planner"
priority = 20
role_scope = ["planner", "acceptance", "executor"]
```

Host Supervisor 生成受管子节点配置时，会把控制面下发的 `agent_capabilities.runner_kind` 和 `agent_capabilities.target_dialect` 转换为 `[agent_bridge]` 的默认 runner/dialect，并连同 `[agent_cli.*]` / `[agent_api.*]` provider sections 写入临时 node-runner TOML。

`[[account_pools]]` 负责本机账号与 credential ref，`[[runtime_profiles]]` 负责一次运行使用的 provider、runner、dialect、model、reasoning_effort 和 workspace 写入边界，`[[node_runtime_profile_grants]]` 是节点最终可领取配置项的授权入口。未配置显式 RuntimeProfile 时，Node Runner 会从 Codex accounts 或启用的 `[agent_api.*]` provider 自动合成兼容 profile；配置显式 profile grant 后，节点只会在授权 profile 内按 grant priority、profile priority、pool priority 和账号 priority 选择。优先级数值越小越靠前。DeepSeek 作为独立 provider profile 使用 MAW 自有 `deepseek_api_runner`，默认模型为 `deepseek-v4-flash`；复杂规划、验收或高质量审计池可以单独配置 `deepseek-v4-pro`。`codex_provider_mode = "responses_bridge"` 仅用于保留“用 Codex CLI 打开 DeepSeek provider bridge”的本机交互方式，不把 DeepSeek 伪装成 Codex 官方账号。Host Manager 打开 API Key Provider 的 Codex CLI 时默认使用 no-auth 隔离 `HOME` / `CODEX_HOME` 和短 TTL `MAW_PROVIDER_BRIDGE_TOKEN`，不要求选择或登录 Codex 账号；如确需复用某个 Codex 登录态，可在本地页使用“承载登录打开”。启动前 Host Manager 会在 shadow `CODEX_HOME/model-catalogs/maw-provider-models.json` 写入当前 provider 模型元数据，并通过 `model_catalog_json` 传给 Codex，避免 `deepseek-v4-flash` 触发 fallback metadata warning。

## 当前记录文件约定

每条任务都应该在对应 workspace 中生成这些记录文件：

- `task-record.md`
  给人阅读的任务摘要，说明谁执行、怎么执行、产物在哪。
- `task-events.jsonl`
  机器可读事件流，方便面板或后续审计消费。新写入事件统一包含 `seq / event_type / task_id / node_id / timestamp / message / payload / source`，并继续兼容旧字段 `event / node_name / details`。
- `task-ai-run.json`
  Agent Bridge / provider 运行账本，记录 host、HostService、节点、账号池、账号 hash、provider、runner、dialect、model、开始/结束时间、token 和 workspace write 边界，不记录明文 secret。
- `execution-result.md`
  当前执行模式的结果摘要。
- `execution-result.json`
  结构化执行摘要，至少包含 `touched_files / commands_executed / tests_run / tests_passed / tests_failed / artifacts_written / final_status`。
- `artifact-manifest.json`
  当前 workspace 下的关键产物索引，记录 `task_id / kind / path / summary`。
- `environment-snapshot.json`
  当前节点最近一次注册 / 心跳采集到的本机环境快照，会同时保留在 runtime 根目录，并同步到活跃 task workspace。

围绕 Codex 交接，当前实现还会重点写出下面这些文件：

- `task.json`
  任务原始快照。当前由 Node Runner 直接把控制面下发的任务 JSON 落盘，便于后续核对 `key`、`title`、`assignee`、`executor_target`、`task_notes` 等字段。
- `codex-prompt.md`
  交给 Codex 的最终 prompt。`codex_prompt` 模式会把它同时复制到 `runtime/codex-inbox/<task_key>.prompt.md`；`codex_headless` 模式会把它作为 `codex exec` 的标准输入。
- `execution-result.md`
  当前任务在本节点上的结果摘要。注意它不一定表示“业务任务已经做完”：在 `codex_prompt` 模式里，这个文件通常会写成 `blocked`，表示 Node Runner 已完成交接，等待 Codex 或人工继续接手。
- `changed-files.txt`
  仅在 `codex_headless` 模式下、且仓库存在未提交改动时生成。现在优先基于当前 task workspace 的 `owned_paths + git status` 收口；如果拿不到 scoped 结果，会退化到当前 workspace 的 git 状态并在 `changed-files.json` 标明 fallback 来源。

围绕 Agent Bridge，当前还会写出中立 agent 产物；它们不替代 Codex 或 MAW 兼容产物：

- `maw-task.json` / `maw-task.md`
  MAW canonical task 输入与人工摘要。
- `agent-task.json`
  provider/dialect 结构化任务输入。
- `agent-prompt.md`
  provider 可读 prompt，不直接复用 `codex-prompt.md`。
- `agent-command.json`
  runner kind、argv/API 摘要、cwd 和输出路径；只记录 secret reference，不记录明文密钥。
- `agent-output/stdout.log` / `agent-output/stderr.log`
  Bridge headless runner 输出。
- `agent-result-normalized.json`
  Result Normalizer 归一输出，供 Node Runner 写回 `execution-result.*` 和任务记录。

最小示例：

```text
workspaces/<task_key>/
  task.json
  codex-prompt.md
  execution-result.md
  environment-snapshot.json
  changed-files.txt        # only when codex_headless sees repo changes
```

```json
{
  "key": "task-win-runner-handoff-files",
  "title": "完善 Node Runner 任务交接文件说明与示例",
  "assignee": "win-4090-01",
  "executor_target": "win-4090-01",
  "task_notes": "限制在 code/node-runner 和 ops/hosts/windows-headless-node ..."
}
```

```md
# Task task-win-runner-handoff-files

- executor: codex_prompt
- node: win-4090-01
- status: blocked
```

## Artifact 回传（推荐开启）

当 `artifact_sync.enabled = true` 时，Node Runner 会在任务进入终态（`completed / failed / blocked`）后，把当前 workspace 同步到服务端的 artifact 目录，用于面板直接展示真实文件。

当前同步策略不是复制完整 workspace，而是在本机 `runtime/artifact-bundles/<workspace_key>/` 先构建最小长期保留 bundle，再同步到服务端 `artifact_root/<workspace_key>/`。默认保留任务审计、恢复、工作台预览和 canonical AI 输出所需文件，例如：

- `task.json`
- `task-runtime.json` / `task-runtime.md`
- `task-record.md`
- `execution-result.md` / `execution-result.json`
- `verification.md`
- `changeset.json`
- `work-record.json`
- `task-events.jsonl`
- `codex-prompt.md`
- `codex-handoff.json`
- `current-briefing.json`
- `artifact-manifest.json`
- `environment-snapshot.json`
- `continuation-summary.*`
- `current-plan.md` / `current-tasks.md`
- `project-inputs.md`
- `project-template-context.md`
- `component-guides-index.md`

`artifacts/**` 和 `workspace/artifacts/**` 下的交付物会随 bundle 保留；依赖、构建和缓存目录默认不进入长期 artifact，包括 `node_modules/`、`.venv/`、`dist/`、`build/`、`.next/`、`coverage/`、`.pytest_cache/`、`.mypy_cache/`、`.ruff_cache/` 和 `.git/`。单文件超过当前上限时也会跳过，避免大体积临时文件打满服务器磁盘。

bundle 内的 `artifact-manifest.json` 会记录 `bundle_version`、`generated_at`、源 workspace、保留文件、跳过文件和跳过目录。旧任务的 artifact 目录没有新版 manifest 时，中枢仍按目录递归索引兼容展示。

## 恢复与审批

- 任务进入 `waiting_human` 后，Node Runner 会释放节点占用，控制面保留 `resume_node_name / resume_workspace_key / workspace_path / failure_kind=approval_required`。
- 审批通过后，任务会重新回到 `planned`，优先复用原 workspace；如果原节点离线或恢复上下文缺失，会安全降级为 `unassigned + handoff required`。
- 节点重启后，如果发现本地仍有活跃 workspace，会先尝试从 `execution-result.* / session-record.md / continuation-summary.*` 恢复，而不是直接重建脏会话。

支持两种模式：

- `mode = "local"`
  适用于节点和服务器在同一台机器上，直接复制到本地 `artifact_root`。
- `mode = "ssh"`
  适用于节点与服务器分离，通过 `ssh/scp/rsync` 同步到远端 `artifact_root`。

最小配置示例：

```toml
[artifact_sync]
enabled = true
mode = "ssh"
artifact_root = "/www/wwwroot/ai.lingboqianji.cn/shared/artifacts"
ssh_host = "101.201.33.172"
ssh_user = "root"
ssh_port = 22
ssh_identity_file = "~/.ssh/multiagentworker_ubuntu_root_ed25519"
rsync_executable = "rsync"
```
