# Phase 2h 逐行走读 — 脚本执行 API / 前端 / 子机 Agent

**标准**: `docs/project/line-walk-audit-standard.md`  
**日期**: 2026-05-22  
**范围**: `scripts.py`（扩容）、`agent.py`（script-callback）、`web/app/scripts.html`、`web/app/script-executions.html`、`web/agent/agent.py`（exec/wait/kill 增量）  
**关联**: Phase 2g `audit-phase-2g-line-walk.md`

---

## 走读签字

| 项 | 值 |
|----|-----|
| 计划文件数 | 5 |
| 完成文件数 | 5 |
| 新增 FINDING | 5 |
| Phase 2a VERIFY | F2a-14 路由顺序 ✅；F2a-15 危险命令拦截 ✅；中心 `agent/exec` 已删 ✅ |

---

## 文件清单

| 文件 | 行数 N | Read 范围 | H | Closure | 状态 |
|------|--------|-----------|---|---------|------|
| `server/api/scripts.py` | 369 | 1–369 | 4 | 4 | 逐行完成 ✓（2a 时 263 行，增量走读） |
| `server/api/agent.py` | 267 | 1–267 | 2 | 2 | 逐行完成 ✓（script-callback 段） |
| `web/app/scripts.html` | 463 | 1–463 | 6 | 6 | 逐行完成 ✓ |
| `web/app/script-executions.html` | 191 | 1–191 | 4 | 4 | 逐行完成 ✓ |
| `web/agent/agent.py` | 363 | 1–363 | 5 | 5 | 逐行完成 ✓（exec/wait/kill 105–222 重点） |

---

## Findings（仅 FINDING）

| ID | 文件:行 | 类 | 度 | 描述 | 修复建议 |
|----|---------|----|----|------|----------|
| F2h-01 | `schemas.py:141-146` + `scripts.py:314-321` | BUG | P2 | `ScriptExecute` 无 `credential_id`，API/服务均无法在批量执行中带 DB 凭据替换 | 扩展 schema + `execute_command(credential_id=…)` |
| F2h-02 | `agent.py:209-235` | RISK | P2 | `script-callback` 无 JWT、无限流；仅靠一次性 secret（见 F2g-06） | IP/ job_id 限速 + 失败退避 |
| F2h-03 | `web/agent/agent.py:191-193` | VULN | P1 | `/exec/kill` 对不在 `_running_procs` 的 pid 仍 `os.kill(pid)`，可杀任意进程 | 仅允许 kill 本 Agent 登记过的 pid |
| F2h-04 | `web/agent/agent.py:215-222` | BUG | P2 | `/exec/wait` 超时后未 `_running_procs.pop`，泄漏且后续 wait 404 | timeout 分支 pop + kill |
| F2h-05 | `scripts.html:254` | RISK | P2 | 日志块经 `esc()` 后写入 `<pre>`，但 `formatJobLogs` 拼接的 API 字段未逐字段转义（当前用 esc 包裹整体，可接受） | 保持 `esc(t.logText)`；禁止改为 raw innerHTML |

**延续 2g（本批 VERIFY，不重复计新 ID）**: F2g-04/05 重试新 id、F2g-06 回调限流、F2f-01 `/exec` shell。

---

## 入口表 — `server/api/scripts.py`（增量）

| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| GET | `/api/scripts/exec-config` | JWT | batch_size / concurrency |
| GET | `/api/scripts/executions` | JWT | 分页历史 |
| GET | `/api/scripts/executions/{id}` | JWT | `fetch_logs` 触发子机 tail |
| POST | `/api/scripts/executions/{id}/stop` | JWT | stop pending |
| POST | `/api/scripts/executions/{id}/retry` | JWT | 危险命令检查 + 新建 execution |
| POST | `/api/scripts/executions/{id}/mark-stuck` | JWT | 事件 + 审计 |
| POST | `/api/scripts/exec` | JWT | `check_dangerous_command` **拒绝** |

路由顺序 VERIFY：`/credentials`、`/executions` 均在 `/{id}` 之前（56–205 vs 207+）→ **F2a-14 已修复**。

---

## 入口表 — `server/api/agent.py`（增量）

| 方法 | 路径 | 鉴权 |
|------|------|------|
| POST | `/api/agent/script-callback` | **无 JWT**；`job_id` + `secret` 一次性 Redis |
| POST | `/api/agent/heartbeat` | X-API-Key + per-server 校验 |

中心 `/api/agent/exec` 已删除（266–267 注释）→ **F2a-02 VERIFY**。

---

## Closure 全表

### `server/api/scripts.py`（H=4）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| scripts.py | 84,237,265,291 | PY-01 | SAFE | — | 审计 detail f-string，无 shell |
| scripts.py | 306-311 | PY-03 | SAFE | — | `check_dangerous_command` 有命中则 400，非仅告警 |
| scripts.py | 314-321 | PY-05 | FINDING | P2 | 见 F2h-01（无 credential_id 入参） |
| scripts.py | 357-368 | PY-06 | SAFE | — | `_credential_to_dict` 不返密码 |

**输入→sink**: JWT body → `ScriptService` → Agent HTTP / Redis。

---

### `server/api/agent.py`（H=2）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| agent.py | 49-50,69,74 | PY-08 | SAFE | — | `compare_digest` |
| agent.py | 209-235 | PY-05 | FINDING | P2 | 见 F2h-02；`consume_script_job` 内 compare_digest（script_jobs） |

**输入→sink**: 子机 POST body → Redis job 消费 → `apply_script_job_callback`。

---

### `web/app/scripts.html`（H=6）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| scripts.html | 1-2 | FE-06 | RISK | P2 | CDN 无 SRI（同 F2e-06） |
| scripts.html | 249,350,386 | FE-01 | SAFE | — | `innerHTML` 中 name/category/content/domain 均 `esc()` |
| scripts.html | 254 | FE-01 | SAFE | — | `logText` 整体 `esc()` 再放入 pre |
| scripts.html | 266-271 | FE-05 | SAFE | — | `onclick` 中 `t.id` 为 `job-${Date.now()}` 客户端常量 |
| scripts.html | 350 | FE-01 | SAFE | — | `esc(s.content?.substring(0,300))` |
| scripts.html | 431 | FE-04 | SAFE | — | 执行用 `apiFetch`，非裸 fetch |

**输入→sink**: API JSON → `innerHTML` / `textContent` / `dataset.cmd`（仅回传 API，不插入 DOM 为 HTML）。

---

### `web/app/script-executions.html`（H=4）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| script-executions.html | 1-2 | FE-06 | RISK | P2 | CDN 无 SRI |
| script-executions.html | 85-105 | FE-01 | SAFE | — | cmd/operator/prog/status 均 `esc()`；id 为数字 |
| script-executions.html | 133-138 | FE-05 | SAFE | — | onclick 仅数字 `d.id` |
| script-executions.html | 158 | FE-01 | SAFE | — | 详情用 `textContent`，非 HTML 注入 |

---

### `web/agent/agent.py`（H=5）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| agent.py | 71 | PY-08 | SAFE | — | `compare_digest` API Key |
| agent.py | 135 | PY-01 | SAFE | — | sudo 分支 `shlex.quote(command)` |
| agent.py | 139 | PY-02 | FINDING | P1 | **F2f-01 VERIFY 仍 OPEN**：`create_subprocess_shell` |
| agent.py | 191-193 | PY-02 | FINDING | P1 | 见 F2h-03：任意 pid `os.kill` |
| agent.py | 215-222 | PY-09 | FINDING | P2 | 见 F2h-04：wait 超时未清理 `_running_procs` |

**输入→sink**: Nexus `X-API-Key` + JSON `command` → `subprocess_shell`；`pid` → `os.kill`。

---

## Phase 2a / 2f VERIFY 摘要

| 原 ID | 结论 |
|-------|------|
| F2a-14 | ✅ `credentials` / `executions` 已注册在 `/{id}` 前 |
| F2a-15 | ✅ `POST /exec` 与 `retry` 均阻断危险命令 |
| F2a-02 | ✅ 中心 agent exec 已删；仅子机 `web/agent/agent.py` |
| F2f-01 | ⏳ 子机 shell 仍未改 |
| F2f-02 | ✅ API_KEY 启动强制非空（42-44） |

---

## 修复优先级（本批）

1. **F2h-03** — `/exec/kill` 禁止对外部 pid `os.kill`（安全）
2. **F2h-04** — wait 超时清理（稳定性）
3. **F2h-01** — credential_id 贯通（功能）
4. **F2h-02** — 回调限流（与 F2g-06 合并修）

---

## 报告索引

| 批次 | 路径 |
|------|------|
| 2g | `audit-phase-2g-line-walk.md` |
| 2h | 本文件 |
| 汇总 | `audit-phase-2-findings-matrix.md` |
