适用版本 0.2.0。本文覆盖安装、初始化、配置、启动、提交任务、观察、运维、排障与边界。
架构与设计约束见 agent-task-scheduler-spec.md,真实资格证据见
qualification-status.md。四个 Submitter harness 的分层测试命令和
排障顺序见 testing-the-submitter.md。
四类进程,全部在单节点上:
| 进程 | 命令 | 身份 | 职责 |
|---|---|---|---|
| Master | python3 -m agent_scheduler.cli.main serve |
root | REST 控制面、WSS 服务端、调度器、观察界面 |
| Worker | python3 -m agent_scheduler.cli.main worker |
root | 主动外连 WSS、采集 hy-smi、管理容器生命周期 |
| MCP Adapter | python3 -m agent_scheduler.cli.main mcp |
Submitter | stdio JSON-RPC,把 Agent 工具调用翻译成 REST |
| Submitter Agent | 由 qualify 拉起,或人工 |
zz_chentian |
写 Proposal、应对评审、轮询 Task |
Master 内部还会以子进程方式调用 Claude 扮演两个角色:
- Processor — 把一版 Proposal Markdown 规范化成严格 schema 的
ProposalFacts - Reviewer — 独立审阅,返回
APPROVE/REQUEST_CHANGES/REJECT
数据流:
Submitter Agent
│ (MCP stdio)
▼
MCP Adapter ──REST──► Master ──► Processor(Claude) ──► Facts
│ │
│ ◄── Reviewer(Claude) ◄──┘
│
├─ 确定性签名编译 (RFC 8785 + SHA-256 + Ed25519)
├─ 调度:准入 / 排队 / GPU 与容器 Lease
│
└──WSS──► Worker ──► docker start/exec/stop
│
└──► NFS Ground Truth(产物 / 日志 / 事件)
所有状态变更都落在 NFS Ground Truth,快照可删除并从不可变历史重建。
要求 Python >=3.10(开发环境 3.12)、可用的 pip、docker 与 hy-smi。
python3 -m pip install -e '.[test]'本地门禁:
python3 -m pytest
python3 -m ruff check .
python3 -m mypy src涉及真实计费或真实硬件的测试必须显式 opt-in,分三层,成本依次上升:
# T1:先确认 shell 没有 RUN_REAL_*;严格零成本命令见 testing-the-submitter.md
# T2:单个 Agent 真实连通性检查——建一个 Proposal 就停,Master 用 fake harness 即可,不碰 GPU
RUN_REAL_CLAUDE=1 python3 -m pytest tests/test_real_onboarding.py -m real_claude
RUN_REAL_CODEX=1 python3 -m pytest tests/test_real_onboarding.py -m real_codex
RUN_REAL_PI=1 python3 -m pytest tests/test_real_onboarding.py -m real_pi
RUN_REAL_DSH=1 python3 -m pytest tests/test_real_onboarding.py -m real_dsh
# T3:单个 Agent 完整 1/2/4/8 卡资格闭环——真实 GPU,复用容器严格串行,一次只跑一个
RUN_REAL_GPU=1 RUN_FULL_QUALIFICATION=1 RUN_REAL_CODEX=1 \
python3 -m pytest tests/test_real_qualification.py -m 'real_codex and real_gpu'四个 Agent(Claude Code、Codex CLI、pi、dsh)都能跑 T2/T3;四家的一次性安装前置
(如 pi install npm:pi-mcp-adapter、dsh plugin --profile headless add dsh-mcp-bridge)
见 从 Agent 会话提交。
python3 -m agent_scheduler.cli.main init-runtime --state-root /public/share/agent-scheduler-mvp在 <state-root>/secrets/(目录 0700,文件 0600,仅 root 可读)生成五项:
| 文件 | 用途 |
|---|---|
worker-api-key |
Worker WSS 的 Bearer 凭据 |
ed25519-private.pem / ed25519-public.pem |
编译产物签名密钥对 |
ed25519-key-id |
密钥标识,写入签名负载 |
tls-private-key.pem |
loopback TLS 私钥 |
另在 <state-root>/tls/(目录 0750,文件 0640,属组与 state-root 相同)生成:
| 文件 | 用途 |
|---|---|
certificate.pem |
loopback 自签证书——公开材料,用于客户端验证 Master,不用于认证调用方 |
证书刻意放在 secrets/ 之外:它是非机密材料,只要 OS 账号属于 state-root 的属组就能读到,
不需要 root。这正是 python3 -m agent_scheduler.cli.main mcp 命令能以非 root 的 Submitter 账号运行的原因——
它只需要这一个文件,从不读取另外五项。
0.2.0 之前创建的 state root 证书仍在旧路径 <state-root>/secrets/tls-certificate.pem
(0600,仅 root 可读),没有 tls/ 目录。不做迁移就直接重启会导致 serve、worker、
qualify 和全部 Admin 子命令立即报 FileNotFoundError 退出——init-runtime 拒绝在已有
身份上重跑,所以这不是一个会自愈的问题。升级前手动执行一次:
state_root=/public/share/agent-scheduler-mvp # 换成实际路径
group=$(stat -c '%G' "$state_root")
install -d -m 0750 "$state_root/tls"
chgrp "$group" "$state_root/tls"
mv "$state_root/secrets/tls-certificate.pem" "$state_root/tls/certificate.pem"
chmod 0640 "$state_root/tls/certificate.pem"
chgrp "$group" "$state_root/tls/certificate.pem"之后再重启 Master/Worker。该操作只搬动证书这一份非机密文件,不影响另外五项密钥或已有的 签名/审计历史。
任一文件已存在时命令直接拒绝,不覆盖。 需要轮换必须人工归档后重建。
这些密钥永远不得进入 Git、Task 负载、argv、观察 API 或普通日志。仓库里的 .env
不由本项目读取,运维 shell 需在启动前自行导出环境变量。
全部通过环境变量,进程启动时校验,非法值直接拒绝启动。
| 变量 | 默认 | 说明 |
|---|---|---|
AGENT_SCHEDULER_STATE_ROOT |
/public/share/agent-scheduler-mvp |
NFS Ground Truth 根目录 |
AGENT_SCHEDULER_PROFILE |
未设置(生产) | 设为 qualification 启用资格 profile |
AGENT_SCHEDULER_VRAM_THRESHOLD |
见下 | GPU 准入的 VRAM 上限(百分比) |
AGENT_SCHEDULER_HARNESS_MODE |
fake |
fake 或 claude |
AGENT_SCHEDULER_WORKER_MODE |
remote |
fake、local 或 remote |
AGENT_SCHEDULER_ALLOWED_USERS |
zz_chentian |
逗号分隔的 Submitter 白名单 |
AGENT_SCHEDULER_MAX_WORKERS |
1 |
代码模型允许 TaskUnit <= max_workers |
AGENT_SCHEDULER_AUTO_SCHEDULE |
1 |
0 表示只接受 Admin tick 手动推进 |
AGENT_SCHEDULER_USERNAME |
— | MCP Adapter 的 Submitter 身份 |
| profile | 默认值 | 硬上限 |
|---|---|---|
生产(未设 PROFILE) |
2.0 |
2.0 |
qualification |
97.0 |
97.0 |
超过所属 profile 的硬上限会在启动时抛 ValueError。资格 profile 的放宽由运维显式批准,
必须在事件中留痕,调度器不得为了让任务通过而自行抬高阈值。
真实角色使用受限非交互调用:
claude --print --no-session-persistence --disable-slash-commands \
--setting-sources "" --permission-mode dontAsk --tools "" \
--allowedTools <仅 MCP 工具> --strict-mcp-config --mcp-config <生成的配置> \
--system-prompt-file prompts/<role>.md \
--output-format stream-json --verbose --json-schema
内置工具、slash command、session 持久化、外部 settings/MCP 全部禁用。
子进程只继承最小 allowlist:HOME、PATH、ANTHROPIC_API_KEY、ANTHROPIC_AUTH_TOKEN、
ANTHROPIC_BASE_URL、代理变量。两个凭据有其一即可。
失败重试为首次调用 + 最多 3 次重试(共 4 次),且仅对可重试的 stderr 生效。
三个终端共用同一套环境:
export AGENT_SCHEDULER_STATE_ROOT=/public/share/agent-scheduler-mvp
export AGENT_SCHEDULER_PROFILE=qualification
export AGENT_SCHEDULER_HARNESS_MODE=claude
export AGENT_SCHEDULER_WORKER_MODE=remote
export ANTHROPIC_AUTH_TOKEN=... # 或 ANTHROPIC_API_KEY终端 1 — Master:
python3 -m agent_scheduler.cli.main serve [--host 127.0.0.1] [--port 8443]终端 2 — Worker(主动外连,每 10 秒上报真实 hy-smi):
python3 -m agent_scheduler.cli.main worker [--uri wss://127.0.0.1:8443/api/v1/worker/ws]curl -sk https://127.0.0.1:8443/health{"status":"ready","workers":1,"integrity":"valid",
"qualification":true,"harness_mode":"claude","worker_mode":"remote"}workers 必须为 1,integrity 必须为 valid。integrity 非 valid 表示事件流校验失败
(断行、序列缺口或跨流绑定错误),此时不要提交任务,先用 inspect 排查。
浏览器打开 https://127.0.0.1:8443/(自签证书;远程访问请用 SSH tunnel,不要暴露端口)。
生成一份 MCP 配置指向本地 adapter:
{
"mcpServers": {
"submitter": {
"command": "python3",
"args": ["-m", "agent_scheduler.cli.main", "mcp",
"--base-url", "https://127.0.0.1:8443",
"--username", "zz_chentian"]
}
}
}非 root 的 Submitter 账号能读到
<state-root>/tls/certificate.pem不代表这条命令在当前 主机上一定能跑起来——如果项目的 Python 解释器本身装在只有 root 能进的目录下 (例如/root),非 root 账号会在执行这条命令本身时失败,而不是在读证书时失败。这是 独立于本文档权限修复的主机配置问题,参见 从 Agent 会话提交 · 安全提醒。
工具清单:
| 工具 | 必填参数 | 说明 |
|---|---|---|
create_proposal |
markdown, idempotency_key |
创建 Proposal |
reply |
proposal_id, markdown, idempotency_key |
提交完整替换修订 |
confirm_revision |
proposal_id, revision_id, idempotency_key |
显式确认当前修订送审 |
get_reviews |
proposal_id |
读评审决定、理由与当前 Facts |
get_proposal |
proposal_id |
读 Proposal 状态 |
resume |
proposal_id, idempotency_key |
从可恢复状态继续 |
cancel |
proposal_id |
取消非终态 Proposal |
get_task |
task_id |
读不可变 Task 与当前状态 |
cancel_task |
task_id |
取消排队或运行中的 Task |
wait_for_task |
task_id |
轮询 Task,最长 30 秒 |
wait_for_events |
proposal_id, after_sequence |
轮询事件,最长 30 秒 |
get_logs |
task_id, unit_id, execution_id, name |
从 offset 读 Framework 日志 |
每次 create/reply/confirm 都要用唯一幂等键。重复键配同一负载返回缓存结果;
配不同负载返回 409 IDEMPOTENCY_CONFLICT。
BASE=https://127.0.0.1:8443
H='-H Content-Type:application/json -H X-Username:zz_chentian'
# 创建
curl -sk $BASE/api/v1/proposals $H -H 'Idempotency-Key: create-1' \
-d "$(jq -Rn --rawfile m proposal.md '{markdown:$m}')"
# 确认送审
curl -sk $BASE/api/v1/proposals/$PROP/confirm $H -H 'Idempotency-Key: confirm-1' \
-d '{"revision_id":"'"$REV"'"}'
# 被要求修改后:读评审 → 提完整替换修订 → 重新确认
curl -sk $BASE/api/v1/proposals/$PROP/reviews
curl -sk $BASE/api/v1/proposals/$PROP/replies $H -H 'Idempotency-Key: reply-1' \
-d "$(jq -Rn --rawfile m proposal-v2.md '{markdown:$m}')"
# 查 Task
curl -sk $BASE/api/v1/tasks/$TASKpython3 -m agent_scheduler.cli.main qualify [--base-url https://127.0.0.1:8443] [--timeout N] \
[--harness claude|codex|pi|dsh]拉起真实 Submitter(默认 Claude Code,--harness 选其他三家),一次提交 1/2/4/8 卡
四个 Proposal,然后独立验证完整证据包。四个 harness 各自产出独立可验证的证据包——
--harness 只决定谁写 Proposal,Master 内部的 Processor/Reviewer 始终是 Claude。
详见第 11 节。
必须按这个顺序给出全部 15 个小节,内容完整,不允许 TBD:
# Proposal
## Identity
## Objective
## Success Criteria
## Workload and Code
## Container
## Resources
## Commands
## Inputs and Mounts
## Environment
## Networking and Privileges
## Timeout and Cleanup
## Framework Logs
## Business Logs and Outputs
## Multi-node Coordination
## Risks and Notes每份 Proposal 必须写明:Submitter zz_chentian;Worker worker-local-01;
容器 fh-sglang-deepseek-v4-flash,容器用户 root;镜像 digest;冻结 launcher 及其 SHA-256;
Proposal 唯一的产物与业务日志路径;有界的前台命令与总超时。
资格任务的运行命令是完全确定的,Processor 不得改写:
kind:container_path_bashcontainer_path:/data/fh/agent-gpu-task-scheduler/scripts/run_torch_collective_smoke.shsha256:c1cf6dee074e03c026dd7272e358d7b15c65b6ff3b6ae1c1f71e7efae341de0cargv:恰好两个位置参数,依次为产物路径、业务日志路径。不接受任何 flag。 world size 由调度器注入的HIP_VISIBLE_DEVICES推导,不走 argv。
路径约定(<pid> 为 proposal_id):
| 字段 | 值 |
|---|---|
argv[0] |
/data/agent-scheduler-mvp/outputs/<pid>.json |
argv[1] |
/data/agent-scheduler-mvp/logs/<pid>.log |
required_outputs |
上者把前缀 /data 换成 /public/share,恰好一项 |
required_logs |
同上,恰好一项 |
容器内 /data 即宿主 /public/share 的 bind mount。产物路径在执行前必须不存在。
违反任一条会得到 422 INVALID_PROPOSAL,错误体里带具体原因。
CLARIFYING ──► AWAITING_CONFIRMATION ──► IN_REVIEW ──┬─► APPROVED ──► COMPILING ──► COMPILED
▲ ├─► CHANGES_REQUESTED ──┐
└─────────────────────────────┘ │
└───────────── reply ◄─────────────────────────────────┘
└─► REJECTED
终态:COMPILED、REJECTED、CANCELLED、EXPIRED、COMPILE_FAILED、PROCESSING_ERROR。
AWAITING_CONFIRMATION 有 30 分钟 Submitter 截止时间;Proposal 整体 7 天后过期。
CREATED ─► QUEUED ─► PREPARING ─► RESERVED ─► DISPATCHED ─► STARTING ─► RUNNING ─► FINALIZING ─► COMPLETED
│ └─► FAILED
└─► BLOCKED └─► CLEANUP_FAILED
└─► RECONCILIATION_REQUIRED
BLOCKED:准入不满足(VRAM 超阈值、容器被占、GPU 有租约)。资格 profile 下等待超过 30 分钟会以QUALIFICATION_GPU_WAIT_EXPIRED终止。CLEANUP_FAILED/RECONCILIATION_REQUIRED:容器停止未被docker inspect确认, 租约刻意保留,需要 Adminreconcile人工确认后释放。- Master 重启后,原本活跃的 Task 一律标记
RECONCILIATION_REQUIRED,Worker 以 offline/UNKNOWN 载入,等待重新连接。
AVAILABLE / DRIFTED / UNKNOWN。理论空闲但 VRAM 超过当前 profile 阈值的 GPU 记为
DRIFTED;需要连续 3 次低于阈值才恢复 AVAILABLE。
Submitter 接口需要 X-Username 头;写操作需要 Idempotency-Key。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /health |
就绪、Worker 数、事件完整性、当前 profile |
| POST | /api/v1/proposals |
创建,{"markdown": "..."},201 |
| GET | /api/v1/proposals/{id} |
读状态 |
| POST | /api/v1/proposals/{id}/replies |
完整替换修订 |
| POST | /api/v1/proposals/{id}/confirm |
{"revision_id": "..."} |
| GET | /api/v1/proposals/{id}/reviews |
评审 + 当前 Facts |
| GET | /api/v1/proposals/{id}/events |
?after_sequence=N |
| POST | /api/v1/proposals/{id}/resume |
恢复 |
| POST | /api/v1/proposals/{id}/cancel |
取消 |
| GET | /api/v1/tasks/{id} |
Task + 当前状态 |
| POST | /api/v1/tasks/{id}/cancel |
取消 |
| GET | /api/v1/logs/{task}/{unit}/{exec}/{name} |
Framework 日志字节,?offset=N |
| GET | /api/v1/observe/summary |
Master profile、Worker 与 GPU、Proposal、容器状态 |
| GET | /api/v1/observe/events/{type}/{id} |
只读事件 |
| GET | / |
观察界面(每 2 秒刷新,只读) |
/api/v1/observe/** 是 GET-only,其他方法一律 405。
观察界面用的就是 observe/summary 这一个端点,2 秒轮询。为此 summary 里的几项做了缓存与瘦身:
docker inspect 与目录索引缓存 2 秒,integrity 缓存 30 秒(它要解析全部事件文件,不能按轮询频率跑),
framework_logs 与 audit_streams 返回 {"count", "recent"} 而不是全量路径列表。
不要用 /health 做高频轮询,它每次都会完整校验事件流。
错误体统一为 {"error_code", "message", "object_id", "current_state", "request_id"}。
| 状态码 | 含义 | 正确反应 |
|---|---|---|
| 403 | USERNAME_NOT_ALLOWED |
换合法 Submitter 身份 |
| 404 | NOT_FOUND |
对象不存在 |
| 409 | 状态冲突(CHANGES_REQUESTED、INVALID_STATE、IDEMPOTENCY_CONFLICT、ROUND_LIMIT 等) |
读状态后走对应流程,不要盲目重试 |
| 422 | INVALID_PROPOSAL、IDEMPOTENCY_REQUIRED |
改内容,重试没用 |
| 503 | 正在 drain | 等待或联系运维 |
MCP Adapter 会把 error_code 与 message 原样透传给 Agent,不会只丢一个状态行。
全部走 loopback 管理面并写审计,必须给 --actor 与 --reason:
python3 -m agent_scheduler.cli.main tick --actor admin --reason 'manual scheduler pass'
python3 -m agent_scheduler.cli.main drain --actor admin --reason maintenance
python3 -m agent_scheduler.cli.main compile-retry --proposal-id PROP --actor admin --reason fixed-validator
python3 -m agent_scheduler.cli.main reconcile --execution-id EXEC --actor admin --reason verified-stopped
python3 -m agent_scheduler.cli.main reload-users --users zz_chentian --actor admin --reason policy-updatedrain之后新提交返回503,在途 Task 继续跑完。reconcile是唯一能释放CLEANUP_FAILED/RECONCILIATION_REQUIRED所持租约的路径, 且要求人工已确认容器确实停止。- 没有单 GPU 强制释放命令,这是刻意的。
python3 -m agent_scheduler.cli.main inspect --state-root /public/share/agent-scheduler-mvp \
--kind {events|immutable|snapshot|leases} \
[--object-type TYPE] [--object-id ID]<state-root>/
├── secrets/ # 0700,仅 root 可读的密钥(不含 TLS 证书)
├── tls/ # 0750,certificate.pem 0640,属组与 state-root 相同——非 root Submitter 可读
├── immutable/ # 不可变对象,按类型分目录
│ ├── revisions/ facts/ reviews/ compilation-contexts/
│ ├── tasks/ plans/ manifests/ task-status-history/
│ ├── lease-history/ idempotency/ proposal-states/
│ ├── harness/ # 每次 Claude 调用的完整审计(argv/退出码/stdout)
│ ├── worker-samples/ # 原始 hy-smi 行
│ ├── worker-evidence/ worker-replies/
│ └── qualification-runs/ qualification-gates/
├── events/ # 追加写 NDJSON,每条 fsync
│ ├── proposals/ tasks/ workers/
├── snapshots/ # 可删除,可从 immutable 重建
├── framework-logs/ # <task>/<unit>/<exec>/run-001.json,默认保留 30 天
├── worker-inbox/ # Worker 侧证据
├── outputs/ # 业务产物
└── logs/ # 业务日志
immutable/ 与 events/ 是权威来源;snapshots/ 删掉可重建,不要手工编辑任何一个。
python3 -m agent_scheduler.cli.main qualify [--harness claude|codex|pi|dsh]流程:跑本地门禁并留证 → 检查 /health profile → 生成该 harness 的接入配置 → 拉起真实
Submitter 一次提交四个 Proposal → 轮询到终态 → 独立验证证据包。结果不依赖 Submitter
自述——驱动按 Qualification Run: <run_id> 标记从 Ground Truth 反推 items,四个 harness
在结构化输出能力上的差异因此不影响正确性。
验证器默认失败,逐项检查:run_id 绑定、门禁记录、Task/Plan/Manifest 签名、
hy-smi 样本新鲜度(≤30 秒且早于计划创建时间)、WSS 协议事件、Worker 证据、
Docker exec 记录、产物新鲜度、逐 rank 数值输出、残留租约为零、容器已停止。
历史证据、伪造证据、过期样本都无法让它通过。
输出:
{"schema_version":"v1","run_id":"qual_...","status":"COMPLETED",
"items":[{"card_count":1,"proposal_id":"...","task_id":"...","state":"COMPLETED"}, ...],
"reason":"..."}status 只有 COMPLETED 与 BLOCKED_QUALIFICATION 两种。
代码门禁与 Fake 通过不代表真实资格完成,只有本命令返回 COMPLETED 才算。
| 现象 | 原因与处理 |
|---|---|
/health 里 workers: 0 |
Worker 未连上。检查 Worker 日志、secrets/worker-api-key 是否与 Master 同一 state root |
/health 里 integrity 非 valid |
事件流断行 / 序列缺口 / 跨流绑定错误。用 inspect --kind events 定位,不要继续提交 |
创建返回 422 INVALID_PROPOSAL |
读 message。最常见是 argv 不是两个位置参数,或产物路径没带 proposal_id |
创建返回 409 IDEMPOTENCY_CONFLICT |
同一幂等键配了不同负载,换新键 |
确认返回 409 CHANGES_REQUESTED |
Reviewer 要求修改。get_reviews → 完整替换修订 reply → 用新键 confirm |
Task 长期 BLOCKED |
看 observe/summary 的 GPU 状态。VRAM 超阈值、GPU 有租约或容器被占 |
GPU 卡在 DRIFTED |
需要连续 3 次采样低于阈值才恢复,约 30 秒 |
Task 进 CLEANUP_FAILED |
容器停止未被确认,租约刻意保留。人工确认后 reconcile |
容器内 import torch 报 librocm_smi64.so.2 |
已由 ExecutionPlan 把镜像自带的 /opt/dtk/.hyhal/rocm_smi/lib 前置到 LD_LIBRARY_PATH 解决,不要改镜像 |
| Claude 角色启动失败 | 确认 ANTHROPIC_AUTH_TOKEN 或 ANTHROPIC_API_KEY 已导出,且 claude 在 PATH 上 |
qualify 返回 BLOCKED_QUALIFICATION |
读 reason,它会指明是哪一项证据不成立 |
排查 Claude 角色行为时,immutable/harness/ 里有每次调用的完整 argv、退出码与 stdout。
MVP 不实现:真实认证与多租户隔离、镜像 pull、动态依赖安装、业务级自动重试、 数据库、HA、崩溃自动恢复、真实多 Worker / gang 调度、性能 SLO。
信任模型:Master、Worker、容器内 root 同属可信管理域,不防御作恶或出错的 root Worker。
唯一的外部身份是 Submitter(目标 zz_chentian;0.2.0 首版资格允许 root Claude Code)。
资源约束:单 Worker worker-local-01,8 张 K100_AI;唯一复用容器
fh-sglang-deepseek-v4-flash,严格串行,每个 Task 结束后必须 stopped。
调度器永不安装或修改容器依赖。