87beb00bdb
PR A for #770: docs + migration 007 + RBAC + types foundation. - 新增 failureKind session-store-evicted,用于区分 PVC 缺失与真协议错误 - 新增 migration 007_v01_session_state_storage:sessions 表增加 storage_* 列 + 索引 - mgr SA RBAC 增量:persistentvolumeclaims: [create, get, list, watch, delete] - 6 份 SPEC 升级(runtime-assembly / hwlab-manual-dispatch / backend-codex T7b / agentrun-runner / agentrun-mgr / services) - 显式禁止:fake app-server mock、replacement threadId、runner 启动后 copy/restore、idleTimeoutMs 拉永驻 - selftest 断言更新到 007_v01_session_state_storage 后续 PR B/C 在此基础上接入 mgr 端 PVC 生命周期 + runner 端 mount + backend 端 observability。
214 lines
19 KiB
Markdown
214 lines
19 KiB
Markdown
# v0.1 agentrun-runner 服务规格
|
||
|
||
`agentrun-runner` 是 AgentRun `v0.1` 的手动启动执行入口。它以 per-run runner Job 方式运行,必须从 `agentrun-mgr` claim run,调用 backend adapter,并把 events、heartbeat、command ack 和 command terminal status 写回 manager。同一 runner Job 在 idle timeout 内必须继续 poll 同一 run 的后续 command,不得把每个 turn 都变成重新 bundle 和新 runner Job。
|
||
|
||
## 在系统中的职责划分
|
||
|
||
- 作为 Kubernetes Job 或受控 host process 启动;不作为普通业务客户端直接调用的长驻公共服务。
|
||
- 从 manager register、claim run、续租 lease、poll commands、ack command、append events、patch command status;只有 runner 级不可恢复失败或显式 run terminal 时才 patch run status。
|
||
- 根据 run 中的 `backendProfile` 和 `executionPolicy.secretScope` 调用 backend adapter。
|
||
- 根据 manager 解析出的 RuntimeAssembly materialize backend image、profile Secret、session 和初始资源;四要素字段权威见 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md),本文只规定 runner 消费边界。
|
||
- 将 backend stdout/stderr、assistant message、tool call、error 和 command terminal status 归一化为 manager event。
|
||
- 提供可定位的 job/process identity、logPath、attempt id 和 failureKind。
|
||
- 不直连 Postgres,不扩大 workspace、network、approval 或 secret scope。
|
||
|
||
## 内部架构
|
||
|
||
`v0.1` 默认 runner 形态是 `agentrun-v01` namespace 中的短生命周期 Job,Job 名称建议使用 `agentrun-v01-runner-<runId>-<attempt>`。短生命周期指 Job 不作为公共长驻服务;Job 内部必须支持同一 run 的多 command loop,直到 run 被 cancel/terminal、lease 冲突或 idle timeout。MVP 允许 CLI 启动受控本地 process,但该 process 仍必须通过 manager API claim/report。
|
||
|
||
Runner 自研代码优先使用 Bun + TypeScript。Kubernetes Job 和 CLI 启动的 host process 必须进入同一套 TS runner 模块,避免一套 Job 逻辑和一套本地调试逻辑分叉;容器镜像可以直接运行 TS 入口或运行由同一源码构建出的 JS artifact。
|
||
|
||
Runner 启动参数必须显式包含:
|
||
|
||
- manager API base URL。
|
||
- runId 和 attemptId。
|
||
- backendProfile。
|
||
- logPath 或 Kubernetes job/pod identity。
|
||
- source commit/build metadata。
|
||
|
||
Runner Secret 只能通过 Kubernetes Secret projection、ServiceAccount/RBAC 或受控 Secret API 读取获得。Codex 测试凭据投影规则见 [spec-v01-secret-distribution.md](spec-v01-secret-distribution.md) 和 [spec-v01-backend-codex.md](spec-v01-backend-codex.md)。
|
||
|
||
Kubernetes Job runner 必须把 credential source 与 runtime home 分开:Secret volume 只读挂在 `/var/run/agentrun/secrets/...`,`/home/agentrun` 由 `emptyDir` 提供可写空间,`CODEX_HOME` 指向当前 run/profile 的 writable runtime home,`AGENTRUN_CODEX_SECRET_HOME` 指向当前 `backendProfile` 对应的只读 projection。runner/backend 在启动 provider 前只复制授权文件,不打印内容。`codex`、`deepseek` 与 `minimax-m3` profile 不得共享同一个可写 runtime home,除非它们运行在不同的 per-run Kubernetes Job 且该目录由 Job 独占 emptyDir 提供。
|
||
|
||
RuntimeAssembly P0 中 `SessionRef` 可以显式为 `null`,runner 不得把完整 `CODEX_HOME`、Secret projection 或节点 host path 当作 session store。`ResourceBundleRef` P0 收敛为 Git-only;runner 已支持把 `repoUrl + full commitId` checkout 到 `AGENTRUN_WORKSPACE_ROOT` 下的隔离目录,并记录 commit/tree 摘要,不能把用户上传文件或 env dump 混入 Git-only bundle。`toolAliases`、`promptRefs` 和 `skillRefs` 都属于该 Git-only bundle 的非敏感子资源:runner 必须在同一 checkout 中解析、校验和装配,不能从镜像默认目录、host path、旧 prompt 常量或用户长 prompt 中补齐。
|
||
|
||
### v0.1.1 Session state 持久化(per-session RWO PVC 直接挂载)
|
||
|
||
Runner 启动时若 run 携带 `storage_kind='pvc'` 的 session 引用,必须在 Job manifest 渲染阶段加一个 `agentrun-sessions` volume,把 `agentrun-v01-session-<sessionId>` PVC 直接挂到 `${CODEX_HOME}/<codex_rollout_subdir>`,并把以下 env 透传给 backend:
|
||
|
||
- `AGENTRUN_SESSION_PVC_NAME`:PVC 名(`agentrun-v01-session-<sessionId>`)。
|
||
- `AGENTRUN_SESSION_PVC_NAMESPACE`:默认 `agentrun-v01`。
|
||
- `AGENTRUN_SESSION_PVC_MOUNT_PATH`:默认 `${CODEX_HOME}/<codex_rollout_subdir>`。
|
||
- `AGENTRUN_CODEX_ROLLOUT_SUBDIR`:默认 `sessions`。
|
||
|
||
边界:
|
||
|
||
- PVC 与 `runner-home` emptyDir 父目录共存;`auth.json` / `config.toml` / `state_*.sqlite` / `memories/` / `tmp/` / `skills/` 仍走 emptyDir;codex rollout JSONL 走 PVC。
|
||
- `automountServiceAccountToken=false` 不变;runner 不通过 k8s API 读 PVC 内容,只把 PVC 摘要通过 `POST /api/v1/sessions/:id/storage/refresh` 写回 manager。
|
||
- PVC 不存在 + `storage_kind='pvc'` 时 runner 启动前 manager 端短路返回 `session-store-evicted`;runner 不知道该 session。
|
||
- 禁止 runner 启动后做 copy/restore(PR #78 已回退的 replacement 逻辑同源,禁止任何变体)。
|
||
|
||
Runner K8s manifest 增量(spec 形态参考):
|
||
|
||
```yaml
|
||
volumes:
|
||
- name: runner-home # emptyDir(auth.json, config.toml, state_*.sqlite...)
|
||
emptyDir: {}
|
||
- name: agentrun-sessions # 新增 PVC RWO
|
||
persistentVolumeClaim:
|
||
claimName: agentrun-v01-session-<sessionId>
|
||
volumeMounts:
|
||
- name: runner-home
|
||
mountPath: /home/agentrun
|
||
- name: agentrun-sessions
|
||
mountPath: /home/agentrun/.codex-<profile>/<codex_rollout_subdir> # <-- 唯一改这一行
|
||
```
|
||
|
||
### ResourceBundle 子资源装配
|
||
|
||
Runner materialize `ResourceBundleRef` 后必须按固定顺序处理子资源:先创建 `toolAliases` wrapper,再装配 `skillRefs` registry,最后读取 `promptRefs` 并生成当前 command 的 assembled initial prompt 摘要。任何子资源缺失或非法都必须按对应 failureKind 阻塞当前 command,不得继续运行后让模型自行猜测。
|
||
|
||
`skillRefs` 聚合规则:
|
||
|
||
- 聚合目录默认在 materialized workspace 下的 `.agents/skills`,每个 skill 使用稳定目录名,例如 `.agents/skills/<aggregateAs || name>/SKILL.md`。
|
||
- Runner 必须把该聚合目录通过环境或 backend option 暴露给 Codex runtime;对于需要兼容 Codex skill discovery 的实现,应设置 `AGENTRUN_SKILLS_DIRS` 或等价字段,并可同步设置业务方约定的 `<PROJECT>_CODE_AGENT_SKILLS_DIRS`。
|
||
- 聚合只允许 symlink 或 copy 当前 bundle 内的 skill root,不允许引用 `/app/skills`、host path、Secret volume 或用户上传临时目录。
|
||
- Event/result 只输出 skill name、manifest path 摘要、hash、bytes、version/commit metadata、required 和 aggregate target。
|
||
|
||
`promptRefs` 装配规则:
|
||
|
||
- Runner 只读取当前 bundle 内的 prompt 文件,按数组顺序生成 `initialPrompt`。
|
||
- 对没有 `threadId`、将执行 `thread/start` 的第一条 turn,runner/backend 必须把 `initialPrompt` 注入到该 turn 的输入前缀,并记录 `initialPromptInjected=true`。
|
||
- 对已有 `SessionRef.threadId` 或 command `payload.threadId`、将执行 `thread/resume` 的 turn,runner/backend 不得重复注入 `initialPrompt`,必须记录 `initialPromptInjected=false` 和 `reason=thread-resume`。
|
||
- `initialPrompt` 不得包含会话历史;用户本轮 message 仍来自 command payload 原文。
|
||
|
||
## HWLAB v0.2 执行经验承接
|
||
|
||
Runner 承接的是 HWLAB v0.2 原有 Code Agent 的执行层经验,不承接 HWLAB cloud-api 的业务路由和权限判断。实现时优先参考 HWLAB 已验证的代码路径,而不是重新定义 Codex session、trace 和输出裁剪语义:
|
||
|
||
| HWLAB v0.2 参考能力 | 参考入口 | Runner 承接规则 |
|
||
| --- | --- | --- |
|
||
| Codex app-server stdio thread/turn 生命周期 | `internal/cloud/codex-stdio-session.ts` | 有 command `payload.threadId` 或 `SessionRef.threadId` 时执行 resume,再 start turn;无标准 `threadId` 时 start thread;events、result 和 session record 都以 `threadId` 为唯一 thread identity;turn terminal 才能上报 completed。 |
|
||
| cancel/interrupt | `internal/cloud/server-code-agent-http.ts`、`internal/cloud/codex-stdio-session.ts` | runner 必须轮询 manager cancel 状态并中止 backend;backend 不支持精确 interrupt 时终止受控进程组。 |
|
||
| runnerTrace 事件可见性 | `internal/cloud/code-agent-trace-store.ts` | backend 输出必须转成 manager events;每个 terminal/错误/取消都要有事件和 final status。 |
|
||
| workspace-write 边界 | `internal/cloud/code-agent-contract.ts` | runner 只使用 ResourceBundleRef materialized workspace,不猜 HWLAB Pod 的 `/workspace/hwlab` 或 host path。 |
|
||
| prompt/skill 装配 | `internal/cloud/codex-stdio-session.ts`、`internal/cloud/codex-stdio-session-helpers.ts` | HWLAB 旧业务 prompt 与 skill discovery 经验迁入 ResourceBundleRef `promptRefs`/`skillRefs`;runner 只按 Git commit/path 装配,不内建 HWLAB 业务。 |
|
||
| Secret 与 writable CODEX_HOME 分离 | `internal/cloud/code-agent-contract.ts`、`docs/reference/code-agent-chat-readiness.md` | profile Secret 只读投影,复制到当前 run/profile writable runtime home;不同 profile 不共享 runtime home。 |
|
||
| bounded stdout/stderr | `docs/reference/code-agent-chat-readiness.md` | `command_output` 记录摘要、字节数、截断标记和必要引用;不得把大输出直接塞进单个 event/result。 |
|
||
|
||
Kubernetes Job runner 必须设置有限保留时间。`v0.1` 默认 `ttlSecondsAfterFinished=86400`,用于保留最近完成 Job 的调试窗口,同时避免长期堆积 `Completed` runner Job 污染运行面观察。该 TTL 是 Job manifest 的运行面属性,不是 CI/CD 门禁;需要延长保留时间时必须通过受控 Job render/input 显式覆盖,并在 issue 或 PR 中说明原因。
|
||
|
||
## Runner 生命周期
|
||
|
||
标准状态方向:
|
||
|
||
```text
|
||
starting -> registered -> claimed -> running -> terminal
|
||
starting -> registered -> claim_failed
|
||
claimed -> running -> backend_failed
|
||
claimed -> running -> cancelled
|
||
claimed -> lease_lost
|
||
```
|
||
|
||
规则:
|
||
|
||
- runner 必须先 register,再 claim run;claim 失败不能继续调用 backend。
|
||
- lease heartbeat 必须通过 manager lease/status 可观察;不得把周期性心跳或 backend running tick 写成 durable trace event 刷屏。长 turn 只在 `backend-turn-finished` 中输出有界 progress 摘要;过期或冲突时写入 failure event 或明确退出原因。
|
||
- command 只能从 manager poll;不得从本地文件或临时参数伪造正式 command。
|
||
- runner 的普通 poll 只选择 pending `turn`;当 backend adapter 暴露 active turn control 后,runner 才在同 run 内轮询 pending `steer` command,ack 后调用 backend 的 steer 能力并单独终结该 steer command。active turn 结束后到达的 steer 必须结构化 blocked,不得启动新 turn,也不得把 run 标为 terminal。
|
||
- backend 产生的所有可见输出必须先经过 adapter normalization 和 redaction,再 append 到 manager;backend_status 至少包含 redacted profile/backendKind/protocol 摘要。
|
||
- 单个 command terminal 上报后 runner 不应立即退出,而应继续 poll 同一 run 的 pending command,直到 idle timeout、lease 冲突或 run terminal。退出码与 runner loop 终态必须一致或在日志中可解释。
|
||
|
||
## Manager API 交互
|
||
|
||
Runner 只使用 manager 私有 API:
|
||
|
||
```http
|
||
POST /api/v1/runners/register
|
||
POST /api/v1/runs/:runId/claim
|
||
PATCH /api/v1/runs/:runId/lease
|
||
GET /api/v1/runs/:runId/commands?afterSeq=0&limit=20
|
||
POST /api/v1/runs/:runId/events
|
||
PATCH /api/v1/runs/:runId/status
|
||
POST /api/v1/commands/:commandId/ack
|
||
PATCH /api/v1/commands/:commandId/status
|
||
```
|
||
|
||
`PATCH /api/v1/commands/:commandId/status` 是普通 turn 完成的权威上报入口;它只能终结 command,并可更新 run 的 SessionRef/thread 摘要。`PATCH /api/v1/runs/:runId/status` 只用于 runner 级不可恢复失败或显式 run terminal,不得在每个成功 turn 后调用。
|
||
|
||
Runner inbound HTTP 不是业务 API。若实现本地诊断端点,只允许 `GET /health` 或 `GET /debug/status`,并且只能暴露在本地或 pod 内部调试面。
|
||
|
||
## Failure 与 Redaction
|
||
|
||
Runner 必须把以下失败归类为结构化 failureKind:
|
||
|
||
- `secret-unavailable`:SecretRef 缺失、RBAC 拒绝或 Secret projection 不完整。
|
||
- `provider-auth-failed`:上游 provider 鉴权失败。
|
||
- `provider-unavailable`:上游 provider 返回 HTTP 5xx/503、`Service Unavailable`、携带 5xx 的 `responseStreamDisconnected` 或明确 temporary/provider unavailable 文案;这是外部 provider availability blocker,不得归为本地 `backend-failed`。
|
||
- `backend-failed`:backend 进程退出、协议错误或返回 terminal error。
|
||
- `runner-lease-conflict`:claim/lease 被其他 runner 持有。
|
||
- `infra-failed`:Job 启动、网络、manager API 或文件系统基础设施失败。
|
||
- `cancelled`:收到 interrupt/cancel 且已停止执行。
|
||
|
||
Runner 日志必须实时 flush 到文件或 pod log,CLI 启动 runner 时必须返回 logPath 或 job/pod identity。日志、event、trace 和 CLI 输出不得出现 provider credential、`auth.json`、`config.toml` 内容、DSN password、token 或 URL credential。
|
||
|
||
## Runner Job 最小状态
|
||
|
||
HWLAB v0.2 原有 Code Agent 在 cloud-api 进程内执行,失败时依赖本地 trace 定位;AgentRun 将执行迁到 runner Job 后,runner 必须把最小定位事实交回 manager。`runner-jobs` 创建响应和后续查询至少包含:
|
||
|
||
| 字段 | 规则 |
|
||
| --- | --- |
|
||
| `attemptId` | 同一 command 的一次执行尝试;重复 idempotency key 和相同 payload 返回同一 attempt。 |
|
||
| `jobName` / `namespace` | Kubernetes Job identity;不得要求业务客户端自己构造名称。 |
|
||
| `runnerId` | runner register 后的执行者身份。 |
|
||
| `podIdentity` / `logPath` | 至少提供一种可定位 runner 日志的 redacted 引用。 |
|
||
| `phase` / `exitCode` | 若可用,返回 Job/Pod/container 的最小状态摘要;不可用时必须说明 `waitingFor` 或 failureKind。 |
|
||
| `startedAt` / `finishedAt` | 用于区分 pending、running、terminal 和 TTL retention 窗口。 |
|
||
|
||
这些字段只用于可观测性,不授予调用方直接操作 Kubernetes Job 或读取 Secret 的权限。
|
||
|
||
## 测试规格
|
||
|
||
### T1 Runner 启动可见性
|
||
|
||
阅读 `AGENTS.md`、本文和 [spec-v01-cli.md](spec-v01-cli.md),然后用正式 AgentRun CLI 为一个真实 run 启动 runner。确认 CLI 立即返回 JSON,包含 runId、attemptId、job/process identity、logPath 和后续 poll command;不得等待完整模型 turn。
|
||
|
||
### T2 Claim 与 lease 冲突
|
||
|
||
阅读本文和 [spec-v01-agentrun-mgr.md](spec-v01-agentrun-mgr.md),然后对同一个 run 启动两个 runner。确认只有一个 runner claim 成功,失败方输出结构化 failureKind,并且 manager events 中能看到冲突或拒绝原因。
|
||
|
||
### T3 Backend event round-trip
|
||
|
||
阅读本文和 [spec-v01-backend-adapter.md](spec-v01-backend-adapter.md),然后用真实 backend 执行一个最短 turn。确认 runner append assistant/output/error/backend_status/terminal_status 中的必要 events,event seq 单调,terminal status 可通过 manager 查询。
|
||
|
||
### T4 Missing Secret failure
|
||
|
||
阅读本文和 [spec-v01-secret-distribution.md](spec-v01-secret-distribution.md),然后分别用缺失 `codex` SecretRef、缺失 `deepseek` SecretRef 与缺失 `minimax-m3` SecretRef 的 run 启动 runner。确认 runner 不调用 provider,run 失败为 `secret-unavailable` 或等价 failureKind,不 fallback 到另一个 profile,日志和事件不泄露 Secret 值。
|
||
|
||
### T5 Profile switching
|
||
|
||
阅读本文和 [spec-v01-backend-codex.md](spec-v01-backend-codex.md),然后按 `codex -> deepseek -> minimax-m3 -> codex` 顺序启动四个真实 runner Job。确认每个 Job 只挂载和复制当前 profile 的 SecretRef,`CODEX_HOME` 互相隔离,且前后两个 `codex` run 不受 `deepseek` 或 `minimax-m3` run 的 config/model/upstream 影响。
|
||
|
||
### T6 Same-run runner command loop
|
||
|
||
阅读本文和 [spec-v01-hwlab-manual-dispatch.md](spec-v01-hwlab-manual-dispatch.md),然后在同一 run 中提交两条 `turn` command,只启动一次 runner Job。确认第一条 completed 后 run 仍为 non-terminal,runner 在 idle timeout 内处理第二条 command,`resource-bundle-materialized` 只记录一次,两个 command result 按 commandId 独立返回 reply/terminal。
|
||
|
||
### T7 Resource prompt/skill 装配
|
||
|
||
阅读本文和 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md),然后创建一个带 `ResourceBundleRef.promptRefs`、`skillRefs` 和 `toolAliases` 的真实或自测试 run。确认 runner 从同一 full commit checkout 装配 prompt、skill 和工具 alias;新 thread 首轮显示 `initialPromptInjected=true`,assistant 能看见 required skill 摘要;第二轮 resume 显示 `initialPromptInjected=false`,且没有拼接第一轮历史 prompt。删除 required skill 或 prompt 后,command 必须 blocked 为 `skill-unavailable` 或 `prompt-unavailable`。
|
||
|
||
## 规格的实现情况
|
||
|
||
| 规格项 | 状态 | 说明 |
|
||
| --- | --- | --- |
|
||
| `agentrun-runner` 服务规格 | 已定义 | 本文为 v0.1 runner 权威。 |
|
||
| Kubernetes Job runner | 已实现/已通过主闭环 | `runner job` 通过 manager REST 创建 Kubernetes Job,固定使用 `agentrun-v01-runner` ServiceAccount、manager URL、runId/commandId/attemptId、executionPolicy、SecretRef 文件投影、writable Codex runtime home、idle timeout 和有限 TTL;真实 `agentrun-v01` runner Job 已完成 Codex turn。 |
|
||
| host process runner | 已实现 | `runner start` 和 `src/runner/main.ts` 进入同一套 `runOnce`,可通过 manager register/claim/poll/report 执行自测试,并支持 `--one-shot` 或 idle timeout 控制。 |
|
||
| claim/lease/report client | 已实现 | 已拆出 runner manager API client,覆盖 register、claim、lease heartbeat、poll command、ack、append event、command status 和必要 run status;live runtime 通过 manager 写入 Postgres durable store。 |
|
||
| cancel observation | 已实现最小闭环 | runner 在 backend 执行期间轮询 run/command cancel,触发 AbortController 中止 Codex stdio backend,并按 `cancelled` 上报 command/run 终态。 |
|
||
| SessionRef/ResourceBundleRef 消费 | 已实现最小闭环/待 promptRefs 与 skillRefs | runner 会使用 run 中的 SessionRef threadId 执行 resume,并 materialize Git-only ResourceBundleRef 到隔离 workspace 后再启动 backend;`toolAliases` 已实现,`promptRefs` 和 `skillRefs` 装配按本规格待补齐。 |
|
||
| 同 run/runner 多 turn | 已实现最小闭环 | runner 在同一 Job 中 materialize bundle 一次后循环 poll command;普通 turn completed 只终结 command,run 保持可继续接后续 turn,直到 idle timeout 或 run terminal。 |
|
||
| runner redaction | 已实现主路径 | runner/backend event 和 Job 输出使用 redaction;复杂审计仍按 [spec-v01-validation.md](spec-v01-validation.md) 的人工验收抽查。 |
|
||
| `deepseek` profile runner selection | 已实现/已通过主闭环 | Runner Job 和 host runner 已按 run `backendProfile` 选择 matching SecretRef、projection、`CODEX_HOME` 和 backend metadata;真实 Kubernetes Job 已完成 `codex -> deepseek -> codex` 切换联调。 |
|
||
| `minimax-m3` profile runner selection | 已实现/待真实主闭环 | Runner Job 和 host runner 已按 run `backendProfile=minimax-m3` 选择 matching SecretRef、projection、`CODEX_HOME` 和 backend metadata;真实 Kubernetes Job 需要完成 MiniMax-M3 CLI 手动联调后收口。 |
|