fix: 接入 dsflash-go model catalog
This commit is contained in:
@@ -44,8 +44,8 @@ AgentRun 是面向 UniDesk 与 HWLAB 的共享 Agent 执行基础设施。本仓
|
||||
## Critical v0.1 Implementation Stack Rule
|
||||
|
||||
- P0: AgentRun `v0.1` 自研 runtime、CLI、manager、runner、backend adapter、Codex backend 和后续 scheduler 的优先实现语言是 Bun + TypeScript;官方 TypeScript CLI 入口是 `scripts/agentrun-cli.ts`,G14/CI/人工非交互命令使用 `./scripts/agentrun` 启动同一入口,复杂逻辑拆入 `scripts/src/` 和 `src/`。
|
||||
- P0: `backendProfile=codex`、`backendProfile=deepseek` 与 `backendProfile=minimax-m3` 都必须通过同一个 Codex CLI app-server stdio backend kind 执行,启动受控 `codex app-server --listen stdio://`,使用 JSON-RPC 方法 `initialize`、`thread/start` 或 `thread/resume`、`turn/start`;DeepSeek 和 MiniMax-M3 都是 profile/config/SecretRef 选择,不是直接 Responses HTTP 代理、独立 fake provider 或文本 fallback。
|
||||
- P0: `codex`、`deepseek` 与 `minimax-m3` profile 必须使用 profile-scoped SecretRef 和 writable `CODEX_HOME`,不得互相 fallback、复用运行态文件或污染默认 `codex` profile;切换顺序必须可验证为 `codex -> deepseek -> minimax-m3 -> codex` 均独立。
|
||||
- P0: `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3` 与 `backendProfile=dsflash-go` 都必须通过同一个 Codex CLI app-server stdio backend kind 执行,启动受控 `codex app-server --listen stdio://`,使用 JSON-RPC 方法 `initialize`、`thread/start` 或 `thread/resume`、`turn/start`;DeepSeek、MiniMax-M3 和 dsflash-go 都是 profile/config/SecretRef/model catalog 选择,不是直接 Responses HTTP 代理、独立 fake provider 或文本 fallback。
|
||||
- P0: `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile 必须使用 profile-scoped SecretRef 和 writable `CODEX_HOME`,不得互相 fallback、复用运行态文件或污染默认 `codex` profile;`dsflash-go` 还必须携带 profile-scoped `model-catalog.json`;切换顺序必须可验证为 `codex -> deepseek -> minimax-m3 -> dsflash-go -> codex` 均独立。
|
||||
- P0: 实现 Codex stdio backend/profile 前必须参考 UniDesk Code Queue 的 `src/components/microservices/code-queue/src/code-agent/codex.ts`、`common.ts`,以及 HWLAB 的 `internal/cloud/codex-stdio-session.mjs`、`scripts/code-agent-chat-smoke.mjs`、`docs/reference/spec-v02-deepseek-proxy.md`、`docs/reference/code-agent-chat-readiness.md`;复用协议、redaction、trace、failure 分类、profile overlay 和 Secret projection 经验,不复制环境专用路径或明文密钥。
|
||||
|
||||
## 长期参考文档
|
||||
@@ -63,7 +63,7 @@ AgentRun 是面向 UniDesk 与 HWLAB 的共享 Agent 执行基础设施。本仓
|
||||
- `docs/reference/spec-v01-agentrun-mgr.md`:v0.1 manager REST API、tenant boundary、runner claim 和 event/status authority。
|
||||
- `docs/reference/spec-v01-agentrun-runner.md`:v0.1 短生命周期 runner、claim/poll/report、日志和 failureKind。
|
||||
- `docs/reference/spec-v01-backend-adapter.md`:v0.1 backend adapter 合同、event normalization、failure mapping 和 redaction。
|
||||
- `docs/reference/spec-v01-backend-codex.md`:v0.1 Codex app-server stdio backend、`codex`/`deepseek`/`minimax-m3` profile、`~/.codex` 测试凭据 Secret projection 和真实 turn 验收。
|
||||
- `docs/reference/spec-v01-backend-codex.md`:v0.1 Codex app-server stdio backend、`codex`/`deepseek`/`minimax-m3`/`dsflash-go` profile、`~/.codex` 测试凭据 Secret projection、model catalog 和真实 turn 验收。
|
||||
- `docs/reference/spec-v01-cli.md`:v0.1 AgentRun CLI 命令族、JSON 输出、短返回和日志可见。
|
||||
- `docs/reference/spec-v01-scheduler.md`:v0.1 自动 scheduler 的 deferred 边界。
|
||||
- `docs/reference/architecture.md`:AgentRun 产品边界、服务架构、MVP 阶段、RESTful API 模型和数据模型。
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@
|
||||
},
|
||||
{
|
||||
"name": "dsflash-go-secret-projection",
|
||||
"secretRef": { "name": "agentrun-v01-provider-dsflash-go", "keys": ["auth.json", "config.toml"] },
|
||||
"secretRef": { "name": "agentrun-v01-provider-dsflash-go", "keys": ["auth.json", "config.toml", "model-catalog.json"] },
|
||||
"projectionPath": "/var/run/agentrun/secrets/dsflash-go-0",
|
||||
"runtimeCopyPath": "/home/agentrun/.codex-dsflash-go",
|
||||
"profile": "dsflash-go",
|
||||
|
||||
@@ -4,7 +4,7 @@ Backend adapter 是 runner 与具体 Code Agent 工具之间的适配层。`v0.1
|
||||
|
||||
## 在系统中的职责划分
|
||||
|
||||
- 根据 `backendProfile` 选择具体 backend profile;`v0.1` 必须支持 `codex`、`deepseek` 与 `minimax-m3`,三者共享同一个 `codex-app-server-stdio` backend kind。
|
||||
- 根据 `backendProfile` 选择具体 backend profile;`v0.1` 必须支持 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go`,四者共享同一个 `codex-app-server-stdio` backend kind。
|
||||
- 接收 manager 持久化后的 run、command 和 executionPolicy;不得自行扩大 workspace、network、approval 或 secret scope。
|
||||
- 调用具体 backend,并把 backend 输出归一化为 AgentRun events。
|
||||
- 负责 provider/auth/backend/protocol 错误到 failureKind 的映射。
|
||||
@@ -28,7 +28,7 @@ Adapter 输入必须来自 manager 保存的 run/command 和 Kubernetes Secret p
|
||||
|
||||
Backend adapter 消费 RuntimeAssembly 中的 `BackendImageRef` 和 `ProfileRef` 结果,但不定义四要素字段;四要素权威见 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md)。
|
||||
|
||||
`v0.1` 的第一真实 adapter 是 Codex stdio adapter。它必须走 Codex CLI app-server JSON-RPC over stdio;adapter 合同把 Codex 的 thread、turn、notification、tool lifecycle 和 stderr/exit 信息归一化为 AgentRun 标准 events。`codex`、`deepseek` 与 `minimax-m3` 只是该 adapter 的 profile/config/SecretRef 选择,不允许复制多套协议实现。
|
||||
`v0.1` 的第一真实 adapter 是 Codex stdio adapter。它必须走 Codex CLI app-server JSON-RPC over stdio;adapter 合同把 Codex 的 thread、turn、notification、tool lifecycle 和 stderr/exit 信息归一化为 AgentRun 标准 events。`codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 只是该 adapter 的 profile/config/SecretRef 选择,不允许复制多套协议实现。
|
||||
|
||||
## HWLAB v0.2 Code Agent 能力吸收
|
||||
|
||||
@@ -40,7 +40,7 @@ Backend adapter 的第一阶段实现应吸收 HWLAB v0.2 已验证的 Codex std
|
||||
| completed 判定 | `docs/reference/code-agent-chat-readiness.md` | 只有 Codex turn terminal completed 且 assistant reply 可聚合时才输出 completed;assistant delta、item completed、stdout 或 transport close 不能单独完成。 |
|
||||
| assistant stream 和 trace | `internal/cloud/code-agent-trace-store.ts`、`internal/cloud/codex-stdio-session-turn-state.ts` | assistant delta 只能作为 stream/progress 证据;长输出过程中可以输出有界 `assistant_message.source=agent-message-delta-progress` 快照,但 `replyAuthority=false` 且不得参与最终 reply 聚合;每个非空 completed `agentMessage` item 必须输出一个 `assistant_message` event,保留 `itemId` 和顺序;`item/agentMessage:started`、`item/agentMessage:completed` 这类 lifecycle 不得额外持久化为 `backend_status`,避免同一消息在 Web/CLI trace 中重复渲染;最终 result reply 必须优先来自最后一个 completed `agentMessage` item,不能把 commentary/progress delta 与 final response 直接串接。event 必须保留 `threadId`、`turnId`、session 摘要和 redacted backend metadata。 |
|
||||
| command/tool output bounded | `docs/reference/code-agent-chat-readiness.md`、`web/hwlab-cloud-web/app-trace.ts` | `tool_call` 和 `command_output` 必须记录状态、摘要、字节数、截断标记;完整大输出只能通过后续 log/artifact 引用。 |
|
||||
| provider/profile 隔离 | `internal/cloud/code-agent-contract.ts` | `codex`、`deepseek` 与 `minimax-m3` 共享同一 backend kind,但必须使用 profile-scoped SecretRef、model/base-url/config 和 writable runtime home。 |
|
||||
| provider/profile 隔离 | `internal/cloud/code-agent-contract.ts` | `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 共享同一 backend kind,但必须使用 profile-scoped SecretRef、model/base-url/config/model catalog 和 writable runtime home。 |
|
||||
| Secret redaction | `internal/cloud/code-agent-trace-store.ts` | `OPENAI_API_KEY`、auth/config、token、password、kubeconfig、URL credential 不得进入 event、result、log 或 health。 |
|
||||
|
||||
## Backend Profile Registry
|
||||
@@ -52,6 +52,7 @@ Backend adapter 的第一阶段实现应吸收 HWLAB v0.2 已验证的 Codex std
|
||||
| `codex` | `codex-app-server-stdio` | `codex-app-server-jsonrpc-stdio` | `stdio` | `codex app-server --listen stdio://` | 已有主闭环,必须保持默认兼容。 |
|
||||
| `deepseek` | `codex-app-server-stdio` | `codex-app-server-jsonrpc-stdio` | `stdio` | `codex app-server --listen stdio://` | 已实现 profile;必须用独立 SecretRef 和 profile-scoped `CODEX_HOME` 完成真实联调。 |
|
||||
| `minimax-m3` | `codex-app-server-stdio` | `codex-app-server-jsonrpc-stdio` | `stdio` | `codex app-server --listen stdio://` | 已实现 profile;必须用独立 SecretRef 和 profile-scoped `CODEX_HOME` 完成真实联调。 |
|
||||
| `dsflash-go` | `codex-app-server-stdio` | `codex-app-server-jsonrpc-stdio` | `stdio` | `codex app-server --listen stdio://` | 已实现 profile;必须用独立 SecretRef、profile-scoped `CODEX_HOME` 和 `model-catalog.json` 完成 1M context 联调。 |
|
||||
|
||||
Registry 只表达能力和选择边界,不读取 Secret 值。Manager 负责校验 `backendProfile` 是否在 allowlist 内,并校验 `executionPolicy.secretScope.providerCredentials` 是否存在匹配 profile 的 SecretRef;runner 只为当前 run 选择的 profile 准备 Secret projection 和 runtime home。
|
||||
|
||||
@@ -83,6 +84,7 @@ Adapter 必须把 backend 错误映射为稳定 failureKind:
|
||||
| `provider-auth-failed` | provider credential 或 auth file 无效、上游返回 401/403。 |
|
||||
| `provider-rate-limited` | 上游限流或 quota 错误。 |
|
||||
| `provider-unavailable` | 上游 provider availability/transient 失败,包括 HTTP 5xx/503、`Service Unavailable`、`responseStreamDisconnected` 携带 5xx 状态码、明确 `provider unavailable` 或 `temporary unavailable` 文案。 |
|
||||
| `provider-compact-unsupported` | provider 或 bridge 对 `/v1/responses/compact` / `/compact` 返回 404、not found、unsupported、no route 或 not implemented;通常表示 `dsflash-go` / Moon Bridge compact path 配置错误。 |
|
||||
| `backend-protocol-error` | backend 输出无法解析、协议字段缺失。 |
|
||||
| `backend-json-parse-error` | backend stdout 不是合法 JSON-RPC 行。 |
|
||||
| `backend-response-invalid` | backend JSON-RPC response/terminal notification 缺少必需字段。 |
|
||||
@@ -97,7 +99,7 @@ Adapter 必须把 backend 错误映射为稳定 failureKind:
|
||||
|
||||
- Adapter 只能看到运行时投影出来的最小 Secret 文件或 env;不得枚举整个 namespace Secret。
|
||||
- Adapter 不得把 Secret 值写入 event、trace、日志、CLI 输出、health 或 Postgres。
|
||||
- Codex backend 的 `auth.json` 和 `config.toml` 整体按敏感文件处理,即使其中包含非敏感配置,也不得输出原文。
|
||||
- Codex backend 的 `auth.json`、`config.toml` 和 profile-local `model-catalog.json` 整体按敏感文件处理,即使其中包含非敏感配置,也不得输出原文。
|
||||
- Provider base URL、model 名称和 profile 名称可以输出,但 URL credential、Authorization header、token、api_key、password 必须 redacted。
|
||||
|
||||
## 测试规格
|
||||
@@ -112,20 +114,21 @@ Adapter 必须把 backend 错误映射为稳定 failureKind:
|
||||
|
||||
### T3 真实 backend 联调
|
||||
|
||||
阅读本文、[spec-v01-backend-codex.md](spec-v01-backend-codex.md) 和 [spec-v01-validation.md](spec-v01-validation.md),然后分别用 `backendProfile=codex`、`backendProfile=deepseek` 与 `backendProfile=minimax-m3` 完成真实最短 turn。确认 adapter 输出真实 assistant/backend_status/terminal_status events,事件中包含 profile/backendKind/protocol 摘要,且没有 Secret 泄露。
|
||||
阅读本文、[spec-v01-backend-codex.md](spec-v01-backend-codex.md) 和 [spec-v01-validation.md](spec-v01-validation.md),然后分别用 `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3` 与 `backendProfile=dsflash-go` 完成真实最短 turn。确认 adapter 输出真实 assistant/backend_status/terminal_status events,事件中包含 profile/backendKind/protocol 摘要,且没有 Secret 泄露。
|
||||
|
||||
### T4 Profile isolation 自测试
|
||||
|
||||
阅读本文,然后用 fake Codex app-server 和多个不同的 profile Secret fixture 做自测试。确认 adapter 只选择 run 指定 profile 的 SecretRef 和 `CODEX_HOME`,`deepseek` 或 `minimax-m3` 缺失时失败为 `secret-unavailable`,不会 fallback 到 `codex`。
|
||||
阅读本文,然后用 fake Codex app-server 和多个不同的 profile Secret fixture 做自测试。确认 adapter 只选择 run 指定 profile 的 SecretRef 和 `CODEX_HOME`,`deepseek`、`minimax-m3` 或 `dsflash-go` 缺失时失败为 `secret-unavailable`,不会 fallback 到 `codex`。
|
||||
|
||||
## 规格的实现情况
|
||||
|
||||
| 规格项 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Backend adapter 合同 | 已定义 | 本文为 v0.1 adapter 权威。 |
|
||||
| 通用 adapter 模块 | 已实现 profile 形态 | `src/backend/adapter.ts` 作为 runner 进程内 adapter 入口;`codex`、`deepseek` 与 `minimax-m3` 均路由到同一 Codex stdio backend kind,不复制第二套协议实现。 |
|
||||
| 通用 adapter 模块 | 已实现 profile 形态 | `src/backend/adapter.ts` 作为 runner 进程内 adapter 入口;`codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 均路由到同一 Codex stdio backend kind,不复制第二套协议实现。 |
|
||||
| event normalization | 已实现主路径 | Codex backend 已把 backend_status、assistant_message、tool_call、command_output、error 和 terminal_status 归一化为 manager events;复杂事件审计按人工验收抽查。 |
|
||||
| failure mapping | 已实现主路径 | Codex backend 已覆盖 missing secret、auth/rate/availability、protocol、JSON parse、invalid response、spawn、timeout 和 cancel 分类;真实负向场景按 [spec-v01-validation.md](spec-v01-validation.md) T7 手动验收。 |
|
||||
| `deepseek` profile | 已实现/已通过主闭环 | 已进入 registry、validation、runner Secret selection、backend_status metadata、CLI secret render 和 fake stdio 自测试;真实综合联调已按 [spec-v01-validation.md](spec-v01-validation.md) T8 覆盖 `codex -> deepseek -> codex` 切换。 |
|
||||
| `minimax-m3` profile | 已实现/待真实主闭环 | 已进入 registry、validation、runner Secret selection、backend_status metadata、CLI secret render 和 fake stdio 自测试;真实综合联调需要按 [spec-v01-validation.md](spec-v01-validation.md) T8 覆盖 `codex -> deepseek -> minimax-m3 -> codex` 切换。 |
|
||||
| `dsflash-go` profile | 已实现/待真实主闭环 | 已进入 registry、validation、runner Secret selection、`model-catalog.json` projection、backend_status metadata、CLI secret render 和 fake stdio 自测试;真实综合联调需要覆盖 `backendProfile=dsflash-go`、`deepseek-v4-flash` 和 1M context metadata。 |
|
||||
| 多 backend 路由 | Deferred | 跨 backend kind 的自动路由和 scheduler capacity selection 不进入 v0.1。 |
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# v0.1 Codex Stdio Backend/Profile 规格
|
||||
|
||||
Codex stdio backend 是 AgentRun `v0.1` 的第一真实 Code Agent backend kind。它用于证明 runner、backend adapter、Kubernetes Secret projection、真实 provider 调用、event normalization 和 terminal status 的完整闭环。`v0.1` 在同一个 backend kind 下支持 `codex`、`deepseek` 与 `minimax-m3` 三个 profile;三者共享 Codex CLI app-server stdio 协议,只通过 profile/config/SecretRef 隔离上游和模型。
|
||||
Codex stdio backend 是 AgentRun `v0.1` 的第一真实 Code Agent backend kind。它用于证明 runner、backend adapter、Kubernetes Secret projection、真实 provider 调用、event normalization 和 terminal status 的完整闭环。`v0.1` 在同一个 backend kind 下支持 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 四个 profile;四者共享 Codex CLI app-server stdio 协议,只通过 profile/config/SecretRef/model catalog 隔离上游和模型。
|
||||
|
||||
## 在系统中的职责划分
|
||||
|
||||
- 作为 `backendProfile=codex`、`backendProfile=deepseek` 与 `backendProfile=minimax-m3` 的共同具体实现。
|
||||
- 作为 `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3` 与 `backendProfile=dsflash-go` 的共同具体实现。
|
||||
- 使用真实 Codex/Codex-compatible 配置执行最短 turn,不使用 fake provider 作为综合联调通过证据。
|
||||
- 消费 Kubernetes Secret projection 提供的 profile 专属 Codex `auth.json` 与 `config.toml`。
|
||||
- 消费 Kubernetes Secret projection 提供的 profile 专属 Codex `auth.json`、`config.toml` 和 profile-local `model-catalog.json`。
|
||||
- 把 Codex 输出归一化为 AgentRun 标准 events 和 terminal status。
|
||||
- 将 provider/auth/protocol/timeout/cancel 错误映射为 [spec-v01-backend-adapter.md](spec-v01-backend-adapter.md) 定义的 failureKind。
|
||||
|
||||
@@ -46,15 +46,16 @@ Adapter 通过 stdin 写入换行分隔 JSON-RPC 请求,通过 stdout 逐行
|
||||
| `codex` | `agentrun-v01-provider-codex` | operator 当前 Codex `auth.json`/`config.toml` | 现有默认 profile;实现 DeepSeek 时不得改变其默认模型、config authority 或真实联调路径。 |
|
||||
| `deepseek` | `agentrun-v01-provider-deepseek` | operator 准备的 DeepSeek-compatible Codex `auth.json`/`config.toml` | 使用同一 `codex app-server --listen stdio://` 协议,通过 `config.toml` 或等价 profile overlay 指向 DeepSeek-compatible upstream/model。 |
|
||||
| `minimax-m3` | `agentrun-v01-provider-minimax-m3` | 从 HWLAB Code Queue 现有 MiniMax API key 派生的 MiniMax-M3 Codex `auth.json`/`config.toml` | 沿 DeepSeek 相同路径使用 `codex app-server --listen stdio://`;`config.toml` 指向 MiniMax OpenAI-compatible upstream,模型为 `MiniMax-M3`,wire API 必须使用当前 Codex app-server 支持的 `responses`,不得继续使用已废弃的 `chat`。 |
|
||||
| `dsflash-go` | `agentrun-v01-provider-dsflash-go` | DeepSeek V4 Flash / OpenCode Zen Go Codex `auth.json`/`config.toml`/`model-catalog.json` | 沿 DeepSeek 相同路径使用 `codex app-server --listen stdio://`;`config.toml` 必须使用 `deepseek-v4-flash`、`model_context_window=1000000`、`model_auto_compact_token_limit=900000` 和固定 `model_catalog_json=/home/agentrun/.codex-dsflash-go/model-catalog.json`。 |
|
||||
|
||||
`deepseek` 的上游形态借鉴 HWLAB v0.2:DeepSeek 是 provider profile,通过 Responses-compatible bridge、Moon Bridge 或等价稳定服务暴露给 Codex CLI;AgentRun 不在 backend adapter 里手写 DeepSeek HTTP 转换器,也不把 DeepSeek 作为绕过 Codex app-server 的独立 backend kind。`minimax-m3` 也遵循同一原则:MiniMax-M3 是 Codex-compatible provider profile,不恢复旧 UniDesk Code Queue 的 MiniMax/OpenCode 直连路线,不新增独立 HTTP backend,不作为 fallback 或 judge backend。上游 base URL、模型和 provider 名称可以作为 redacted metadata 输出;API Key 和 `auth.json`/`config.toml` 原文不得输出。
|
||||
`deepseek` 的上游形态借鉴 HWLAB v0.2:DeepSeek 是 provider profile,通过 Responses-compatible bridge、Moon Bridge 或等价稳定服务暴露给 Codex CLI;AgentRun 不在 backend adapter 里手写 DeepSeek HTTP 转换器,也不把 DeepSeek 作为绕过 Codex app-server 的独立 backend kind。`minimax-m3` 也遵循同一原则:MiniMax-M3 是 Codex-compatible provider profile,不恢复旧 UniDesk Code Queue 的 MiniMax/OpenCode 直连路线,不新增独立 HTTP backend,不作为 fallback 或 judge backend。`dsflash-go` 是同一 Codex profile 机制下的 DeepSeek V4 Flash 1M context profile,必须通过 Moon Bridge / OpenCode Zen Go compatible path 和 model catalog 暴露模型元数据;若 compact path 不支持,adapter 必须输出 `provider-compact-unsupported`。上游 base URL、模型和 provider 名称可以作为 redacted metadata 输出;API Key、`auth.json`、`config.toml` 和 `model-catalog.json` 原文不得输出。
|
||||
|
||||
Profile 切换规则:
|
||||
|
||||
- `backendProfile` 是 run 的显式字段,manager 不得静默改写。
|
||||
- runner/backend 只读取与 `backendProfile` 同名的 provider credential;缺失则 `secret-unavailable`。
|
||||
- 每次 run 必须使用 profile-scoped writable `CODEX_HOME`。Kubernetes Job 默认把选中 profile 的 Secret projection 复制到该 Job 独占的 `/home/agentrun/.codex-<profile>`;host process 或复用进程必须使用 run/profile 独占目录,避免 `codex`、`deepseek` 与 `minimax-m3` 互相污染。
|
||||
- `deepseek` 和 `minimax-m3` 不得 fallback 到 `codex` Secret、模型或 upstream;`codex` 也不得读取其他 profile Secret。
|
||||
- 每次 run 必须使用 profile-scoped writable `CODEX_HOME`。Kubernetes Job 默认把选中 profile 的 Secret projection 复制到该 Job 独占的 `/home/agentrun/.codex-<profile>`;host process 或复用进程必须使用 run/profile 独占目录,避免 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 互相污染。
|
||||
- `deepseek`、`minimax-m3` 和 `dsflash-go` 不得 fallback 到 `codex` Secret、模型或 upstream;`codex` 也不得读取其他 profile Secret。
|
||||
- command payload 中显式提供 model 时可以透传给 Codex turn;未显式提供时以 profile `config.toml` 为 authority,不在 adapter 中写死默认模型。
|
||||
|
||||
## 测试凭据来源
|
||||
@@ -66,25 +67,26 @@ Profile 切换规则:
|
||||
~/.codex/config.toml
|
||||
```
|
||||
|
||||
这两个文件只能作为 Kubernetes Secret 创建或轮换的输入源,不能通过 hostPath 挂载进 Pod,不能复制进镜像,不能提交到 source branch、GitOps branch、artifact catalog、issue、PR、event、trace、日志或 CLI 输出。`codex`、`deepseek` 与 `minimax-m3` 可以来自不同 operator profile 目录或显式文件参数,但进入 Kubernetes 后必须是不同 SecretRef,除非后续规格明确批准某个共享 SecretRef 场景。`minimax-m3` 的 API key 输入源为 HWLAB Code Queue 现有 MiniMax secret;迁移时只允许把值写入 Kubernetes Secret,不得打印或落库。
|
||||
这些文件只能作为 Kubernetes Secret 创建或轮换的输入源,不能通过 hostPath 挂载进 Pod,不能复制进镜像,不能提交到 source branch、GitOps branch、artifact catalog、issue、PR、event、trace、日志或 CLI 输出。`codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 可以来自不同 operator profile 目录或显式文件参数,但进入 Kubernetes 后必须是不同 SecretRef,除非后续规格明确批准某个共享 SecretRef 场景。`minimax-m3` 的 API key 输入源为 HWLAB Code Queue 现有 MiniMax secret;`dsflash-go` 的 `model-catalog.json` 可由 AgentRun manager 生成或由 operator 显式提供;迁移时只允许把值写入 Kubernetes Secret,不得打印或落库。
|
||||
|
||||
`v0.1` 默认 Kubernetes Secret:
|
||||
|
||||
| 对象 | v0.1 规格 |
|
||||
| --- | --- |
|
||||
| Namespace | `agentrun-v01` |
|
||||
| Secret name | `agentrun-v01-provider-codex`、`agentrun-v01-provider-deepseek` 或 `agentrun-v01-provider-minimax-m3` |
|
||||
| Secret name | `agentrun-v01-provider-codex`、`agentrun-v01-provider-deepseek`、`agentrun-v01-provider-minimax-m3` 或 `agentrun-v01-provider-dsflash-go` |
|
||||
| Secret key | `auth.json`,来自 `~/.codex/auth.json` |
|
||||
| Secret key | `config.toml`,来自 `~/.codex/config.toml` |
|
||||
| Secret key | `model-catalog.json`,仅 `dsflash-go` 必需 |
|
||||
| Consumer | runner 或 backend adapter Pod |
|
||||
| Projection target | 只读 projection,再复制到当前 run/profile 的 writable `CODEX_HOME/auth.json` 和 `CODEX_HOME/config.toml` |
|
||||
| Projection target | 只读 projection,再复制到当前 run/profile 的 writable `CODEX_HOME/auth.json`、`CODEX_HOME/config.toml` 和 profile 需要的额外文件 |
|
||||
| File mode | 只读,建议 `0400` 或等价最小权限 |
|
||||
|
||||
Kubernetes Secret 的创建、轮换和权限控制属于集群密钥管理流程;source branch 只声明 SecretRef 名称、key 和 mount intent。`deploy/deploy.json` 和 rendered GitOps manifest 不得包含 Secret data。
|
||||
|
||||
## Runtime 行为
|
||||
|
||||
- Adapter 必须在调用 Codex 前验证 `auth.json` 和 `config.toml` 均存在且可读;缺失时返回 `secret-unavailable`。
|
||||
- Adapter 必须在调用 Codex 前验证 `auth.json` 和 `config.toml` 均存在且可读;`config.toml` 声明 `model_catalog_json` 时还必须验证目标文件可读;`dsflash-go` 缺少 `model_catalog_json` 时也必须在 provider 调用前返回 `secret-unavailable`。
|
||||
- Codex 运行时必须使用被投影的 `.codex` 目录;不得 fallback 到镜像内默认凭据或节点宿主机 `~/.codex`。
|
||||
- Codex stdio backend 不得设置 turn/session/conversation 的总时长 timeout;`executionPolicy.timeoutMs` 只能作为无 app-server 响应、无 notification、无 assistant/tool/event activity 的 idle timeout。长程任务只要持续产生可见 activity,就必须继续等待 `turn/completed`、取消或真实 transport failure。
|
||||
- 普通 turn command 失败只终结当前 command,不得把 reusable run/session 置为 terminal;后续 command 必须仍可进入同一个 run/runner。只有显式 cancel、runner lease/claim 失效、资源装配不可恢复或运行面退出才允许终结 run。
|
||||
@@ -96,7 +98,7 @@ Kubernetes Secret 的创建、轮换和权限控制属于集群密钥管理流
|
||||
|
||||
[spec-v01-secret-distribution.md](spec-v01-secret-distribution.md) 是 SecretRef、Kubernetes projection、redaction 和 missing secret failure 的权威。本文件只定义 Codex backend 对测试凭据文件的消费方式。
|
||||
|
||||
Run 的 `executionPolicy.secretScope` 应引用与 `backendProfile` 匹配的 provider SecretRef 的 `auth.json` 和 `config.toml`,而不是携带 provider credential 或文件内容。
|
||||
Run 的 `executionPolicy.secretScope` 应引用与 `backendProfile` 匹配的 provider SecretRef 的 `auth.json`、`config.toml` 和 profile 需要的额外文件,尤其是 `dsflash-go` 的 `model-catalog.json`,而不是携带 provider credential 或文件内容。
|
||||
|
||||
## 测试规格
|
||||
|
||||
@@ -116,6 +118,10 @@ Run 的 `executionPolicy.secretScope` 应引用与 `backendProfile` 匹配的 pr
|
||||
|
||||
阅读本文、[spec-v01-secret-distribution.md](spec-v01-secret-distribution.md) 和 [spec-v01-validation.md](spec-v01-validation.md),然后用 `backendProfile=minimax-m3` 创建真实 run 并提交一个最短 `turn` command。确认 runner 仍调用 `codex app-server --listen stdio://`,但使用 `agentrun-v01-provider-minimax-m3` 的 profile SecretRef 和独立 `CODEX_HOME`;manager 可查询 profile 为 `minimax-m3` 的 backend_status、assistant 或 error event、terminal_status,且 Secret value 未泄露。
|
||||
|
||||
### T2d 真实 dsflash-go profile 最短 turn
|
||||
|
||||
阅读本文、[spec-v01-provider-profile-management.md](spec-v01-provider-profile-management.md) 和 [spec-v01-validation.md](spec-v01-validation.md),然后用 `backendProfile=dsflash-go` 创建真实 run 并提交一个最短 `turn` command。确认 runner 仍调用 `codex app-server --listen stdio://`,但使用 `agentrun-v01-provider-dsflash-go` 的 profile SecretRef、独立 `CODEX_HOME` 和 `model-catalog.json`;manager 可查询 profile 为 `dsflash-go`、model 为 `deepseek-v4-flash`、context window 为 1M 的 backend_status、assistant 或 error event、terminal_status,且 Secret value 未泄露。
|
||||
|
||||
### T3 Missing auth/config failure
|
||||
|
||||
阅读本文,然后分别移除或改名 Secret 中的 `auth.json`、`config.toml` key,启动真实 run。确认 adapter 在调用 provider 前失败为 `secret-unavailable`,failure response 为 JSON,日志不包含 Secret value。
|
||||
@@ -130,7 +136,7 @@ Run 的 `executionPolicy.secretScope` 应引用与 `backendProfile` 匹配的 pr
|
||||
|
||||
### T6 Profile switching isolation
|
||||
|
||||
阅读本文,然后在真实 `agentrun-v01` 运行面按顺序执行 `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3`、`backendProfile=codex` 四个最短 turn。确认第二个 run 使用 DeepSeek profile,第三个 run 使用 MiniMax-M3 profile,前后两个 `codex` run 仍使用原 Codex profile;四者的 event、log、backend_status、model/upstream metadata 和 failureKind 不互相污染,且任何一个 profile SecretRef 缺失都不会 fallback 到另一个 profile。
|
||||
阅读本文,然后在真实 `agentrun-v01` 运行面按顺序执行 `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3`、`backendProfile=dsflash-go`、`backendProfile=codex` 五个最短 turn。确认第二个 run 使用 DeepSeek profile,第三个 run 使用 MiniMax-M3 profile,第四个 run 使用 dsflash-go profile,前后两个 `codex` run 仍使用原 Codex profile;五者的 event、log、backend_status、model/upstream metadata 和 failureKind 不互相污染,且任何一个 profile SecretRef 缺失都不会 fallback 到另一个 profile。
|
||||
|
||||
### T7 Stale thread resume failed
|
||||
|
||||
@@ -162,4 +168,5 @@ Run 的 `executionPolicy.secretScope` 应引用与 `backendProfile` 匹配的 pr
|
||||
| 真实 provider turn | 已通过主闭环 | 真实 Codex provider turn 已经通过 RESTful API 和 CLI 综合联调;每次发布仍按 [spec-v01-validation.md](spec-v01-validation.md) 手动复验。 |
|
||||
| `deepseek` profile | 已实现/已通过主闭环 | 代码已支持 `agentrun-v01-provider-deepseek`、独立 `CODEX_HOME`、同一 `codex app-server --listen stdio://` 协议和 profile metadata;真实 Kubernetes SecretRef、runner Job 和 Codex stdio turn 已通过主闭环。 |
|
||||
| `minimax-m3` profile | 已实现/待真实主闭环 | 代码已支持 `agentrun-v01-provider-minimax-m3`、独立 `CODEX_HOME`、同一 `codex app-server --listen stdio://` 协议和 profile metadata;真实 Kubernetes SecretRef、runner Job 和 Codex stdio turn 需要完成 AgentRun CLI 手动验收。 |
|
||||
| `dsflash-go` profile | 已实现/待真实主闭环 | 代码已支持 `agentrun-v01-provider-dsflash-go`、独立 `CODEX_HOME`、`model-catalog.json`、同一 `codex app-server --listen stdio://` 协议和 profile metadata;真实 Kubernetes SecretRef、runner Job、Codex stdio turn 和 HWLAB 原入口 CaseRun 需要完成验收。 |
|
||||
| hostPath `~/.codex` | 不采用 | 只能通过 Kubernetes Secret projection 注入。 |
|
||||
|
||||
@@ -48,7 +48,7 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
./scripts/agentrun runner job --dry-run --run-id <runId> --command-id <commandId> --image <image>
|
||||
./scripts/agentrun runner jobs --run-id <runId> [--command-id <commandId>]
|
||||
./scripts/agentrun runner job-status [runnerJobId] --run-id <runId>
|
||||
./scripts/agentrun secrets codex render --dry-run [--profile codex|deepseek|minimax-m3] [--codex-home <dir>]
|
||||
./scripts/agentrun secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go] [--codex-home <dir>] [--model-catalog-file <file>]
|
||||
./scripts/agentrun provider-profiles list
|
||||
./scripts/agentrun provider-profiles show <profile>
|
||||
./scripts/agentrun provider-profiles remove <profile>
|
||||
@@ -68,9 +68,9 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
./scripts/agentrun queue cancel <taskId> [--reason <text>]
|
||||
./scripts/agentrun queue dispatch <taskId> [--json-file <dispatch.json>]
|
||||
./scripts/agentrun queue refresh <taskId>
|
||||
./scripts/agentrun sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|M3] [--reader-id <reader>]
|
||||
./scripts/agentrun sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--reader-id <reader>]
|
||||
./scripts/agentrun sessions show <sessionId> [--reader-id <reader>]
|
||||
./scripts/agentrun sessions turn [sessionId] --json-file <run-base.json> --prompt-file <file> [--profile codex|deepseek|minimax-m3|M3] [--runner-json-file <job.json>] [--no-runner-job]
|
||||
./scripts/agentrun sessions turn [sessionId] --json-file <run-base.json> --prompt-file <file> [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file <job.json>] [--no-runner-job]
|
||||
./scripts/agentrun sessions steer <sessionId> --prompt-file <file>
|
||||
./scripts/agentrun sessions cancel <sessionId> [--reason <text>]
|
||||
./scripts/agentrun sessions trace <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>]
|
||||
@@ -89,9 +89,9 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
- `server status` 必须同时返回本地 pid/port/logPath 状态和 `/health/readiness` 结果;即使 readiness 失败,也要输出结构化 JSON 和 failure details。
|
||||
- `server logs` 必须返回有界日志尾部、bytes、truncated 和 logPath;找不到日志文件时也必须返回非空 JSON。
|
||||
- `server stop` 必须按 pidFile 与端口进程清理本地 manager,并返回 before/after 状态;不得要求人工用 `ps/kill/ss` 组合命令清理常见临时服务。
|
||||
- `secrets codex render --dry-run` 返回 Codex stdio profile Secret 创建计划、输入文件 bytes/hash、SecretRef、manifest 摘要和 apply 命令形状;`--profile codex` 默认 Secret name 为 `agentrun-v01-provider-codex`,`--profile deepseek` 默认 Secret name 为 `agentrun-v01-provider-deepseek`,`--profile minimax-m3` 默认 Secret name 为 `agentrun-v01-provider-minimax-m3`;它不得输出 Secret value 或执行 Kubernetes 写操作。
|
||||
- `secrets codex render --dry-run` 返回 Codex stdio profile Secret 创建计划、输入文件 bytes/hash、SecretRef、manifest 摘要和 apply 命令形状;`--profile codex` 默认 Secret name 为 `agentrun-v01-provider-codex`,`--profile deepseek` 默认 Secret name 为 `agentrun-v01-provider-deepseek`,`--profile minimax-m3` 默认 Secret name 为 `agentrun-v01-provider-minimax-m3`,`--profile dsflash-go` 默认 Secret name 为 `agentrun-v01-provider-dsflash-go` 并包含 `model-catalog.json`;它不得输出 Secret value 或执行 Kubernetes 写操作。
|
||||
- `provider-profiles` 命令族调用 manager REST 管理 API,覆盖 profile status、删除、API Key 写入和 canary 验证。`set-key --key-stdin` 从 stdin 读取 API Key,响应只显示 SecretRef、resourceVersion、hash 后缀和 failureKind;不得输出 key、Codex auth/config 或 Secret data。
|
||||
- `backends list` 必须显示 `codex`、`deepseek` 与 `minimax-m3` profile 的 backendKind、protocol、transport、command、requiredSecretKeys 和状态;不得因为某个 provider Secret 尚未配置就隐藏 capability。
|
||||
- `backends list` 必须显示 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile 的 backendKind、protocol、transport、command、requiredSecretKeys 和状态;`dsflash-go` 的 `requiredSecretKeys` 必须包含 `model-catalog.json`;不得因为某个 provider Secret 尚未配置就隐藏 capability。
|
||||
- `queue dispatch` 是 Q2 的受控手动调度入口,只对单个 task 显式创建 attempt 和 Core run/command/runner job;不得伪装成自动 scheduler。
|
||||
- `queue refresh` 只根据 Queue task 中保存的 Core run/command 引用回写 Queue attempt 状态,不读取 Core trace 反推 commander 或统计。
|
||||
- `queue show` 必须返回 task/attempt summary、state、read cursor、stats 相关字段和 `sessionPath`;不得返回或代理完整 output/trace。
|
||||
@@ -128,7 +128,7 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
|
||||
### T5 Backend profile CLI 切换
|
||||
|
||||
阅读本文、[spec-v01-backend-codex.md](spec-v01-backend-codex.md) 和 [spec-v01-validation.md](spec-v01-validation.md),然后用正式 CLI 分别创建 `backendProfile=codex`、`backendProfile=deepseek` 与 `backendProfile=minimax-m3` 的 run,按 `codex -> deepseek -> minimax-m3 -> codex` 顺序执行真实 runner。确认 CLI 输出非空 JSON,backend_status 显示正确 profile/backendKind/protocol,缺失对应 profile SecretRef 时返回 `secret-unavailable`,不会 fallback 到 `codex` 或其他 profile。
|
||||
阅读本文、[spec-v01-backend-codex.md](spec-v01-backend-codex.md) 和 [spec-v01-validation.md](spec-v01-validation.md),然后用正式 CLI 分别创建 `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3` 与 `backendProfile=dsflash-go` 的 run,按 `codex -> deepseek -> minimax-m3 -> dsflash-go -> codex` 顺序执行真实 runner。确认 CLI 输出非空 JSON,backend_status 显示正确 profile/backendKind/protocol,`dsflash-go` 显示 `deepseek-v4-flash`、1M/900k context 和 model catalog 摘要,缺失对应 profile SecretRef 时返回 `secret-unavailable`,不会 fallback 到 `codex` 或其他 profile。
|
||||
|
||||
### T5.1 Provider profile 管理 CLI
|
||||
|
||||
@@ -156,3 +156,4 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
| `deepseek` profile CLI | 已实现/已通过主闭环 | `secrets codex render --profile deepseek`、`backends list`、`runner start --backend`、`runner job` 和 JSON 错误可见性已实现;真实 CLI/RESTful 联调已通过 `codex -> deepseek -> codex` 切换主闭环。 |
|
||||
| Provider profile 管理 CLI | 已实现 | `provider-profiles list/show/remove/set-key/validate` 调用 manager REST API,用于 HWLAB 委托和 operator 验收;输出必须持续保持 Secret/API Key 脱敏。 |
|
||||
| `minimax-m3` profile CLI | 已实现/待真实主闭环 | `secrets codex render --profile minimax-m3`、`backends list`、`runner start --backend`、`runner job`、`sessions turn --profile minimax-m3|M3` 和 JSON 错误可见性已实现;真实 CLI/RESTful 联调需要按 `codex -> deepseek -> minimax-m3 -> codex` 手动验收。 |
|
||||
| `dsflash-go` profile CLI | 已实现/待真实主闭环 | `secrets codex render --profile dsflash-go --model-catalog-file`、`backends list`、`runner start --backend`、`runner job`、`sessions turn --profile dsflash-go` 和 JSON 错误可见性已实现;真实 CLI/RESTful 联调需要按 `codex -> deepseek -> minimax-m3 -> dsflash-go -> codex` 手动验收,并确认 compact 404 分类为 `provider-compact-unsupported`。 |
|
||||
|
||||
@@ -30,7 +30,7 @@ AgentRun 不承担 HWLAB 用户鉴权。它只做机器层 contract 校验:调
|
||||
| `codex` | `codex-app-server-stdio` | `agentrun-v01/agentrun-v01-provider-codex` | Codex API profile。 |
|
||||
| `deepseek` | `codex-app-server-stdio` | `agentrun-v01/agentrun-v01-provider-deepseek` | DeepSeek profile,经 HWLAB Moon Bridge 到 DeepSeek 官方 upstream。 |
|
||||
| `minimax-m3` | `codex-app-server-stdio` | `agentrun-v01/agentrun-v01-provider-minimax-m3` | MiniMax-M3 profile。 |
|
||||
| `dsflash-go` | `codex-app-server-stdio` | `agentrun-v01/agentrun-v01-provider-dsflash-go` | DeepSeek V4 Flash profile,经 HWLAB Moon Bridge 到 OpenCode Zen Go upstream。 |
|
||||
| `dsflash-go` | `codex-app-server-stdio` | `agentrun-v01/agentrun-v01-provider-dsflash-go` | DeepSeek V4 Flash profile,经 HWLAB Moon Bridge 到 OpenCode Zen Go upstream;Secret 额外要求 `model-catalog.json`,模型目录与 `config.toml` 均声明 1M/900k context。 |
|
||||
|
||||
动态 profile slug 必须匹配小写 slug 规则 `^[a-z0-9][a-z0-9-]{0,63}$`,并固定映射到同 namespace 内 `agentrun-v01-provider-<profile>` SecretRef,required keys 仍为 `auth.json` 与 `config.toml`。profile 管理 API 不得允许任意 namespace、任意 Secret name、`runtime-default` 或不符合 slug 规则的 profile。新增普通 OpenAI/Codex-compatible provider profile 时,管理员应优先通过 `provider-profiles set-config` / `set-key` / `validate` 创建动态 slug;只有需要新的 backend kind、特殊装配规则、额外 Secret key、租户策略或产品级固定友好名时,才更新本规格、内建 capability、CLI 示例和验证规格。
|
||||
|
||||
@@ -103,7 +103,7 @@ Secret 缺失时仍要返回 profile capability,并把状态标为 `configured
|
||||
规则:
|
||||
|
||||
- `apiKey` 只在本次 request 内用于生成 Secret data,不能落入 Postgres、event、trace、日志或响应。
|
||||
- Manager 写入 profile 对应 Kubernetes Secret 的 `auth.json` 和 `config.toml`,并返回新的 `resourceVersion` 与不可逆 hash 后缀。
|
||||
- Manager 写入 profile 对应 Kubernetes Secret 的 `auth.json` 和 `config.toml`,并返回新的 `resourceVersion` 与不可逆 hash 后缀;`dsflash-go` 同时生成或保留 `model-catalog.json`。
|
||||
- Manager 可记录 `delegatedBy` 的脱敏审计信息,但不把它作为用户鉴权依据。
|
||||
- 非 HWLAB 委托调用可以用于 operator CLI,但也必须走同一 schema 和 redaction。
|
||||
- 非法 profile、非法 baseUrl、SecretRef scope 越界、Kubernetes 写入失败和 config render 失败必须结构化失败。
|
||||
@@ -138,6 +138,17 @@ upstream: DeepSeek 官方 API
|
||||
|
||||
AgentRun 不直接拥有 DeepSeek 官方 upstream URL 的业务路由;它只把 Codex app-server 请求送到 HWLAB Moon Bridge。若 HWLAB bridge 需要独立 upstream Secret 或 rollout,AgentRun 管理 API 必须在响应中返回 `requiresExternalBridgeUpdate=true` 或由 HWLAB 委托请求显式声明 bridge 同步已完成。AgentRun 不得把 `deepseek` 配置改到 `hyueapi.com`,也不得因 DeepSeek 失败 fallback 到 `codex`。
|
||||
|
||||
## dsflash-go 配置规则
|
||||
|
||||
`dsflash-go` 是内建 DeepSeek V4 Flash / OpenCode Zen Go profile,不是普通动态 slug。它必须通过 Codex stdio profile 形态运行,并满足以下固定规则:
|
||||
|
||||
- `model` 必须是 `deepseek-v4-flash`,不得被 `deepseek-chat` 或其他模型覆盖。
|
||||
- `config.toml` 必须声明 `model_context_window = 1000000` 和 `model_auto_compact_token_limit = 900000`。
|
||||
- `config.toml` 必须声明 `model_catalog_json = "/home/agentrun/.codex-dsflash-go/model-catalog.json"`;Secret 中必须存在同名 `model-catalog.json`,其中 `deepseek-v4-flash` 的 context window 与 `config.toml` 一致。
|
||||
- base URL 必须指向 HWLAB Moon Bridge service 或 wrapper-local bridge,禁止指向 `hyueapi.com`;当前 G14 v0.2 默认服务入口是 `http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1`。
|
||||
- `PUT /credential` 与 `PUT /config` 均必须在不打印 Secret value 的前提下生成或保留 `model-catalog.json`;状态查询只显示 key presence/hash 摘要。
|
||||
- 若上游 compact 路径返回 404、not found、unsupported、no route 或 not implemented,adapter 必须归类为 `provider-compact-unsupported`,避免被泛化成 `backend-failed`。
|
||||
|
||||
## Secret 与 RBAC
|
||||
|
||||
Manager ServiceAccount 需要最小 Secret 管理权限,只允许 `get`、`list`、`create`、`replace`、`delete`、`patch` 受控 provider profile Secret:
|
||||
@@ -206,6 +217,10 @@ Manager 审计事件允许记录:profile、action、delegatedBy.system、deleg
|
||||
|
||||
用 HWLAB CLI 或 AgentRun CLI 对一个临时动态 slug 执行 `set-config`、`set-key`、`list`、`remove`。通过证据必须显示 profile SecretRef 为 `agentrun-v01-provider-<slug>`、`configured=true` 只在 `auth.json` 和 `config.toml` 同时存在时成立、删除后 collection list 不再包含该 slug,并且整个过程没有 AgentRun/HWLAB service code change、PR、PipelineRun 或 rollout 作为前置条件。
|
||||
|
||||
### T8 dsflash-go model catalog
|
||||
|
||||
用 `./scripts/agentrun provider-profiles set-key dsflash-go --key-stdin` 或同源 HWLAB 委托 API 写入测试 key。确认输出只包含脱敏 SecretRef、resourceVersion/hash 后缀和 validation identity;Kubernetes Secret 中存在 `auth.json`、`config.toml`、`model-catalog.json` 三个 key;`config.toml` 使用 `deepseek-v4-flash`、1M/900k context 和固定 `model_catalog_json` 路径;`provider-profiles show dsflash-go` 必须显示三项 key presence,且不输出任何 Secret value。
|
||||
|
||||
## 实现状态
|
||||
|
||||
| 能力 | 状态 | 说明 |
|
||||
@@ -215,5 +230,6 @@ Manager 审计事件允许记录:profile、action、delegatedBy.system、deleg
|
||||
| 动态 profile slug | 已实现 | 小写 slug 通过 `agentrun-v01-provider-<slug>` SecretRef 动态生效;普通 provider API Key/config 轮换不需要为每个新 slug 修改服务代码或触发专门 CI/CD。 |
|
||||
| CLI 管理入口 | 已实现 | `./scripts/agentrun provider-profiles list/show/remove/set-key/set-config/validate` 调用 manager REST API,不直连 Secret value。 |
|
||||
| DeepSeek Secret 写入 | 已实现/需硬化 | 已按受控 SecretRef 更新 `auth.json`/`config.toml` 并保持 HWLAB Moon Bridge 官方链路;后续必须去除 credential update 产生 `last-applied-configuration` 注解的副作用。 |
|
||||
| `dsflash-go` model catalog | 已实现 | `dsflash-go` 使用 `deepseek-v4-flash`、1M/900k context、固定 `model_catalog_json` 路径和 Secret 内 `model-catalog.json`;compact unsupported 明确归类为 `provider-compact-unsupported`。 |
|
||||
| Provider canary | 已实现 | canary 通过真实 run/command/runner-job 路径执行,并返回 validationId、runId、commandId、jobName 和 terminal status。 |
|
||||
| HWLAB 委托信任边界 | 已验证 | HWLAB v0.2 通过 Cloud API 委托调用本 API;AgentRun 不读取 HWLAB Web session,也不做用户级鉴权。 |
|
||||
|
||||
@@ -47,7 +47,7 @@ P0 最小 JSON 形态:
|
||||
|
||||
| credential 类别 | 装配归属 | 运行时投影 | 规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| Provider credential | `ProfileRef` / `executionPolicy.secretScope.providerCredentials[]` | profile-scoped 只读 Secret projection,再复制到 per-run writable `CODEX_HOME` | 只服务 `codex`/`deepseek`/`minimax-m3` backend profile;缺失为 `secret-unavailable`,不得 fallback。 |
|
||||
| Provider credential | `ProfileRef` / `executionPolicy.secretScope.providerCredentials[]` | profile-scoped 只读 Secret projection,再复制到 per-run writable `CODEX_HOME` | 只服务 `codex`/`deepseek`/`minimax-m3`/`dsflash-go` backend profile;`dsflash-go` 额外包含 `model-catalog.json`;缺失为 `secret-unavailable`,不得 fallback。 |
|
||||
| Git resource credential | `ResourceBundleRef.credentialRef` | 只服务 resource materialization 的 Git fetch/checkout | 只能用于拉取 `ResourceBundleRef.repoUrl` 对应代码,不得暴露给 agent shell 作为通用 GitHub token。 |
|
||||
| Tool credential | `executionPolicy.secretScope.toolCredentials[]` | 由 runner 按 tool scope 投影为文件或 env,并只暴露给当前 run/command 允许的工具 | 用于 GitHub PR、issue、UniDesk SSH passthrough、artifact registry 等 agent shell 工具能力;不等同于 AgentRun integration,不触发 GitHub sink/OA/Event 之类外部动作记录。 |
|
||||
| Short-lived execution context | runner-job `transientEnv` | 单次 Job env,response/dry-run/event 只显示 name/hash | 只用于业务 dispatcher 生成的短期或 owner-scoped runtime context,例如 HWLAB HWPOD runtime API key、runtime URL 和非敏感服务地址;不得承载 provider credential、GitHub token、UniDesk SSH client token 或长期 SSH key。 |
|
||||
@@ -97,7 +97,7 @@ HWLAB v0.2 原有 Code Agent 已经验证了 profile、session、workspace 和 S
|
||||
|
||||
| HWLAB v0.2 基线能力 | HWLAB 参考入口 | RuntimeAssembly 承接字段 | 承接规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| provider profile 可切换 | `internal/cloud/code-agent-contract.ts` | `ProfileRef.profile`、`ProfileRef.secretRef` | `deepseek`、`minimax-m3` 与 `codex` 只选择 profile/config/SecretRef,不复制 backend 协议;缺失 Secret 必须失败,不 fallback。 |
|
||||
| provider profile 可切换 | `internal/cloud/code-agent-contract.ts` | `ProfileRef.profile`、`ProfileRef.secretRef` | `deepseek`、`minimax-m3`、`dsflash-go` 与 `codex` 只选择 profile/config/SecretRef/model catalog,不复制 backend 协议;缺失 Secret 必须失败,不 fallback。 |
|
||||
| Codex app-server thread 复用 | `internal/cloud/codex-stdio-session.ts`、`internal/cloud/code-agent-session-registry.ts` | `SessionRef.sessionId`、`conversationId`、`threadId` | AgentRun 保存 backend thread/session 摘要;不保存 API KEY、`auth.json`、`config.toml` 或完整 `CODEX_HOME`。 |
|
||||
| 固定 `/workspace/hwlab` 代码上下文 | `internal/cloud/code-agent-contract.ts` | `ResourceBundleRef.repoUrl`、`ref` / `workspaceRef.branch`、materialized `commitId` | 用 Git-only repo/ref checkout 取代 HWLAB Pod 内固定路径;runner checkout 到隔离 workspace,并在 event/result 记录实际 commit。 |
|
||||
| writable `CODEX_HOME` 与 Secret 投影分离 | `docs/reference/code-agent-chat-readiness.md` | `ProfileRef` + runner runtime home | Secret 只读投影,复制到当前 run/profile writable runtime home;profile 间不共享。 |
|
||||
@@ -116,9 +116,10 @@ HWLAB Workbench 的 project/workspace 不属于 RuntimeAssembly 四要素,也
|
||||
|
||||
### ProfileRef
|
||||
|
||||
- `profile` 在 v0.1 只允许 `codex`、`deepseek` 或 `minimax-m3`。
|
||||
- `profile` 在 v0.1 只允许 `codex`、`deepseek`、`minimax-m3` 或 `dsflash-go`。
|
||||
- `secretRef` 只保存 Secret 名称和 key,不保存值。
|
||||
- 当前 profile 只能读取当前 profile 的 SecretRef;缺失必须 `secret-unavailable`,不能 fallback 到另一个 profile。
|
||||
- `dsflash-go` 的 `secretRef.keys` 必须包含 `auth.json`、`config.toml` 与 `model-catalog.json`;legacy 调用方漏传 `model-catalog.json` 时由 manager/runner 按 profile capability 归一补齐 projection,但不能降低运行时 readiness 校验。
|
||||
- profile Secret 只读投影,backend 需要可写目录时复制到 per-run/profile runtime home。
|
||||
|
||||
### SessionRef
|
||||
@@ -225,9 +226,10 @@ skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.m
|
||||
- `codex` run 只挂载 `agentrun-v01-provider-codex`。
|
||||
- `deepseek` run 只挂载 `agentrun-v01-provider-deepseek`。
|
||||
- `minimax-m3` run 只挂载 `agentrun-v01-provider-minimax-m3`。
|
||||
- `codex -> deepseek -> minimax-m3 -> codex` 切换后,`CODEX_HOME`、SecretRef、backend_status 不互相污染。
|
||||
- 删除或缺失 `deepseek`/`minimax-m3` SecretRef 时必须 `secret-unavailable`,不能 fallback 到 `codex`。
|
||||
- 所有输出不得包含 Secret value、`auth.json` 或 `config.toml` 明文。
|
||||
- `dsflash-go` run 只挂载 `agentrun-v01-provider-dsflash-go`,并包含 `auth.json`、`config.toml` 与 `model-catalog.json`。
|
||||
- `codex -> deepseek -> minimax-m3 -> dsflash-go -> codex` 切换后,`CODEX_HOME`、SecretRef、backend_status、model catalog 不互相污染。
|
||||
- 删除或缺失 `deepseek`/`minimax-m3`/`dsflash-go` SecretRef 时必须 `secret-unavailable`,不能 fallback 到 `codex`。
|
||||
- 所有输出不得包含 Secret value、`auth.json`、`config.toml` 或 `model-catalog.json` 明文。
|
||||
|
||||
### A2b Tool credential 验收
|
||||
|
||||
@@ -268,7 +270,7 @@ skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.m
|
||||
| 要素 | v0.1 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `BackendImageRef` | 部分实现 | CI/CD 已使用 digest-pinned runtime image;当前 runner/backend 仍复用 agentrun 镜像。 |
|
||||
| `ProfileRef` | 已实现/已通过 HWLAB v0.2 原入口复测 | `codex`、`deepseek` 与 `minimax-m3` 已通过 SecretRef、writable runtime home 和真实 stdio turn 验证;MiniMax-M3 已通过 HWLAB 显式 session 原入口复测,后续只允许作为 profile/config/SecretRef 选择,不新增直连 backend。 |
|
||||
| `ProfileRef` | 已实现/待 dsflash-go 真实主闭环 | `codex`、`deepseek` 与 `minimax-m3` 已通过 SecretRef、writable runtime home 和真实 stdio turn 验证;MiniMax-M3 已通过 HWLAB 显式 session 原入口复测。`dsflash-go` 已补齐 SecretRef/model catalog 装配、自测试和 legacy key 归一,仍需完成真实 runtime 与 HWLAB 原入口复测;后续只允许作为 profile/config/SecretRef/model catalog 选择,不新增直连 backend。 |
|
||||
| `SessionRef` | 已实现最小持久化 | manager 持久化 `sessionId/conversationId/threadId`,run 创建会解析既有 session,runner 按 threadId resume;session 不保存 credential 文件,TTL/GC 后续细化。 |
|
||||
| `SessionRef` | v0.1.1 已实现/已通过 HWLAB v0.2 原入口复测 | manager 持久化 `sessionId/conversationId/threadId` + 每个 session 绑 RWO PVC(`agentrun-v01-session-<sessionId>`),runner Job 把 PVC 直接挂到 `${CODEX_HOME}/<codex_rollout_subdir>`,codex app-server 自己落盘;runner pod 删除后 replacement runner 仍复用同一 SessionRef/PVC/thread,禁止 copy/restore、replacement threadId 和 fake resume。 |
|
||||
| `ResourceBundleRef` | 已实现 `kind="gitbundle"` materialization/promptRefs/tools/skillDirs 装配 | `repoUrl + ref/materialized commit + bundles[]` 已进入 run schema 和 runner checkout,workspace 受 `AGENTRUN_WORKSPACE_ROOT` 限制,event/result 记录 requested ref/commit、actual commit、tree/workspace/bundles 摘要;`tools/` PATH、`promptRefs` thread-start 注入和 `.agents/skills` 目录发现已实现。 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# v0.1 Secret 与 provider credential 分发规格
|
||||
|
||||
本文定义 AgentRun `v0.1` 的 Secret 和 Code Agent provider credential 分发边界。真实 Code Agent backend 需要上游模型凭据;Codex stdio profile 测试凭据以 `~/.codex/auth.json` 与 `~/.codex/config.toml` 形态为输入源,通过 Kubernetes Secret 投影进入 runner/backend Pod。这些值不得进入 Git source、GitOps branch、artifact catalog、event、trace、日志或 CLI 输出。
|
||||
本文定义 AgentRun `v0.1` 的 Secret 和 Code Agent provider credential 分发边界。真实 Code Agent backend 需要上游模型凭据;Codex stdio profile 测试凭据以 `~/.codex/auth.json`、`~/.codex/config.toml` 和必要的 profile-local model catalog 形态为输入源,通过 Kubernetes Secret 投影进入 runner/backend Pod。这些值不得进入 Git source、GitOps branch、artifact catalog、event、trace、日志或 CLI 输出。
|
||||
|
||||
在装配 SPEC 中,本文承担 SecretRef、projection、rotation 和 redaction 规则;运行时 credential 必须先归入 `ProfileRef`、`ResourceBundleRef.credentialRef` 或 `executionPolicy.secretScope.toolCredentials[]`,再由 runner Job 装配。权威装配模型见 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md)。
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
| Secret 类别 | 用途 | 默认消费者 | v0.1 规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| Postgres DSN | manager 连接 durable store | `agentrun-mgr` | 只通过 `agentrun-v01-mgr-db/DATABASE_URL` 注入。 |
|
||||
| Codex stdio profile 凭据文件 | 真实 Code Agent backend 调上游模型 | runner 或 backend adapter | `codex`、`deepseek` 与 `minimax-m3` 均使用 `auth.json`/`config.toml` 文件形态,只通过 profile-scoped Kubernetes SecretRef 文件投影注入,不写入 run payload。 |
|
||||
| Codex stdio profile 凭据文件 | 真实 Code Agent backend 调上游模型 | runner 或 backend adapter | `codex`、`deepseek` 与 `minimax-m3` 使用 `auth.json`/`config.toml` 文件形态;`dsflash-go` 额外要求 `model-catalog.json`。这些文件只通过 profile-scoped Kubernetes SecretRef 文件投影注入,不写入 run payload。 |
|
||||
| Git SSH deploy key | Tekton checkout source/GitOps promotion,Argo 读取 GitOps branch | Tekton、Argo CD | 只存在于 `agentrun-ci` 或 `argocd` Secret;不进入 runtime Pod。 |
|
||||
| Registry credential | push/pull private registry | Tekton、runtime imagePullSecret | 只作为 ServiceAccount/imagePullSecret 引用。 |
|
||||
| Tool credential | GitHub PR、issue、UniDesk SSH passthrough、artifact registry 等 agent shell/tool 授权 | runner/backend adapter | 必须通过 `executionPolicy.secretScope.toolCredentials[]` 的 SecretRef 装配进入运行时;不是 Queue integration,也不能用 `transientEnv` 承载长期 credential。 |
|
||||
@@ -31,7 +31,8 @@
|
||||
| Codex Provider Secret | `agentrun-v01-provider-codex` keys `auth.json`、`config.toml` |
|
||||
| DeepSeek Provider Secret | `agentrun-v01-provider-deepseek` keys `auth.json`、`config.toml` |
|
||||
| MiniMax-M3 Provider Secret | `agentrun-v01-provider-minimax-m3` keys `auth.json`、`config.toml` |
|
||||
| Provider projection target | 只读 `/var/run/agentrun/secrets/<profile>-<index>/auth.json`、`config.toml`,再复制到当前 run/profile 的 writable `CODEX_HOME` |
|
||||
| dsflash-go Provider Secret | `agentrun-v01-provider-dsflash-go` keys `auth.json`、`config.toml`、`model-catalog.json` |
|
||||
| Provider projection target | 只读 `/var/run/agentrun/secrets/<profile>-<index>/auth.json`、`config.toml` 和 profile 需要的额外文件,再复制到当前 run/profile 的 writable `CODEX_HOME` |
|
||||
| Provider config | 非敏感 base URL/model 可以来自 `config.toml` 或 ConfigMap;credential value 不得放入 ConfigMap。 |
|
||||
| Tekton Git SSH Secret | `agentrun-ci/agentrun-git-ssh` |
|
||||
| Argo Git SSH Secret | `argocd/agentrun-git-ssh` |
|
||||
@@ -50,17 +51,18 @@
|
||||
~/.codex/config.toml
|
||||
```
|
||||
|
||||
这两个文件只能作为 Kubernetes Secret 创建或轮换的输入源。`codex` profile 默认使用 operator 当前 Codex 配置;`deepseek` profile 使用 operator 准备的 DeepSeek-compatible Codex 配置,可以来自另一个 `--codex-home` 或显式 `--auth-file`/`--config-file`;`minimax-m3` profile 使用从 HWLAB Code Queue 现有 MiniMax API key 派生的 Codex 配置,模型固定为 `MiniMax-M3`,wire API 使用当前 Codex app-server 支持的 `responses`。禁止把宿主机 `~/.codex` 以 hostPath 挂入 runner/backend Pod,禁止复制进镜像,禁止提交到 source branch、GitOps branch、artifact catalog、issue、PR、event、trace、日志或 CLI 输出。
|
||||
这两个文件只能作为 Kubernetes Secret 创建或轮换的输入源。`codex` profile 默认使用 operator 当前 Codex 配置;`deepseek` profile 使用 operator 准备的 DeepSeek-compatible Codex 配置,可以来自另一个 `--codex-home` 或显式 `--auth-file`/`--config-file`;`minimax-m3` profile 使用从 HWLAB Code Queue 现有 MiniMax API key 派生的 Codex 配置,模型固定为 `MiniMax-M3`,wire API 使用当前 Codex app-server 支持的 `responses`;`dsflash-go` 使用 `deepseek-v4-flash`、1M/900k context 和 `model-catalog.json`。禁止把宿主机 `~/.codex` 以 hostPath 挂入 runner/backend Pod,禁止复制进镜像,禁止提交到 source branch、GitOps branch、artifact catalog、issue、PR、event、trace、日志或 CLI 输出。
|
||||
|
||||
默认 Secret projection 规则:
|
||||
|
||||
| 项目 | v0.1 规格 |
|
||||
| --- | --- |
|
||||
| Kubernetes Secret | `agentrun-v01/agentrun-v01-provider-codex`、`agentrun-v01/agentrun-v01-provider-deepseek` 或 `agentrun-v01/agentrun-v01-provider-minimax-m3` |
|
||||
| Kubernetes Secret | `agentrun-v01/agentrun-v01-provider-codex`、`agentrun-v01/agentrun-v01-provider-deepseek`、`agentrun-v01/agentrun-v01-provider-minimax-m3` 或 `agentrun-v01/agentrun-v01-provider-dsflash-go` |
|
||||
| Secret key | `auth.json`,来自 `~/.codex/auth.json` |
|
||||
| Secret key | `config.toml`,来自 `~/.codex/config.toml` |
|
||||
| Projection path | 只读 Secret projection 挂到 `/var/run/agentrun/secrets/<profile>-<index>/auth.json` 和 `config.toml`;该路径只作为 credential source。 |
|
||||
| Runtime config path | runner 启动时把当前 `backendProfile` 授权的 Secret projection 复制到 writable `CODEX_HOME`,Kubernetes Job 默认使用该 Job 独占的 `/home/agentrun/.codex-<profile>/auth.json` 和 `config.toml`;复用进程必须使用 run/profile 独占目录。 |
|
||||
| Secret key | `model-catalog.json`,仅 `dsflash-go` 必需,必须与 `config.toml` 的 `model_catalog_json` 指向同一 runtime 路径。 |
|
||||
| Projection path | 只读 Secret projection 挂到 `/var/run/agentrun/secrets/<profile>-<index>/auth.json`、`config.toml` 和 profile 需要的额外文件;该路径只作为 credential source。 |
|
||||
| Runtime config path | runner 启动时把当前 `backendProfile` 授权的 Secret projection 复制到 writable `CODEX_HOME`,Kubernetes Job 默认使用该 Job 独占的 `/home/agentrun/.codex-<profile>/auth.json`、`config.toml` 和 profile 需要的额外文件;复用进程必须使用 run/profile 独占目录。 |
|
||||
| Projection mode | 只读,建议 `0400` 或等价最小权限 |
|
||||
| Runtime env | `HOME=/home/agentrun`,`CODEX_HOME=/home/agentrun/.codex-<profile>`,`AGENTRUN_CODEX_SECRET_HOME=<projection path>`;不得 fallback 到节点宿主机 home。 |
|
||||
|
||||
@@ -101,6 +103,15 @@ Run 的 `executionPolicy.secretScope` 只能包含引用,不包含值。provid
|
||||
"keys": ["auth.json", "config.toml"],
|
||||
"mountPath": "~/.codex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile": "dsflash-go",
|
||||
"secretRef": {
|
||||
"namespace": "agentrun-v01",
|
||||
"name": "agentrun-v01-provider-dsflash-go",
|
||||
"keys": ["auth.json", "config.toml", "model-catalog.json"],
|
||||
"mountPath": "~/.codex"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allowCredentialEcho": false
|
||||
@@ -112,8 +123,9 @@ Run 的 `executionPolicy.secretScope` 只能包含引用,不包含值。provid
|
||||
- `allowCredentialEcho` 必须固定为 `false`。
|
||||
- `secretRef.namespace` 默认只能是 run 所在 lane namespace 或明确批准的 platform namespace。
|
||||
- manager 可以保存 `secretRef`,但不得读取 Secret 值后存库。
|
||||
- runner/backend adapter 获得 Secret 的方式必须来自 Kubernetes env/file projection 或受限 Secret API 读取;Codex 默认从只读 Secret projection 复制 `auth.json` 和 `config.toml` 到 writable `CODEX_HOME` 后启动 app-server,不得通过 run payload、event、CLI 参数或日志传递。
|
||||
- runner/backend adapter 只能选择与 run `backendProfile` 同名的 provider credential;`backendProfile=deepseek` 或 `backendProfile=minimax-m3` 缺少 matching SecretRef 时必须 `secret-unavailable`,不得 fallback 到 `codex` 或另一个 profile。
|
||||
- runner/backend adapter 获得 Secret 的方式必须来自 Kubernetes env/file projection 或受限 Secret API 读取;Codex 默认从只读 Secret projection 复制 `auth.json`、`config.toml` 和 profile 需要的额外文件到 writable `CODEX_HOME` 后启动 app-server,不得通过 run payload、event、CLI 参数或日志传递。
|
||||
- runner/backend adapter 只能选择与 run `backendProfile` 同名的 provider credential;`backendProfile=deepseek`、`backendProfile=minimax-m3` 或 `backendProfile=dsflash-go` 缺少 matching SecretRef 时必须 `secret-unavailable`,不得 fallback 到 `codex` 或另一个 profile。
|
||||
- manager 会按内建 profile 的 `requiredSecretKeys` 规范化 `secretRef.keys`;消费侧旧 payload 即使只提交 `auth.json`/`config.toml`,`dsflash-go` runner Job 也必须投影 `model-catalog.json`。若实际 Kubernetes Secret 缺少该 key,run 必须在装配或 readiness 阶段失败为结构化 `secret-unavailable`。
|
||||
- Secret projection 不能直接作为 `CODEX_HOME`。Codex app-server 会读取并可能维护默认配置、PATH 或运行态文件;把只读 Secret volume 直接挂到 `CODEX_HOME` 会造成启动期写入失败。v0.1 的固定边界是:Secret volume 只读、`/home/agentrun` 由 `emptyDir` 提供可写 runtime home、复制动作只发生在 runner/backend 容器内且不打印文件内容。
|
||||
- SecretRef 不存在或 RBAC 不允许时,run 必须失败为结构化 `failureKind=secret-unavailable` 或等价错误,不得降级成无凭证重试风暴。
|
||||
- `toolCredentials` 的 SecretRef/projection/redaction 规则以 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md) 为准;本文只约束 Secret value 不落库、不输出、不进入 Git source 或 GitOps Secret data。
|
||||
@@ -143,9 +155,9 @@ Tekton promotion
|
||||
Argo CD
|
||||
-> syncs workload references to agentrun-v01
|
||||
Kubernetes Secret
|
||||
-> created from profile-specific auth.json and config.toml by operator or approved secret-management flow
|
||||
-> created from profile-specific auth.json, config.toml and required extra files by operator or approved secret-management flow
|
||||
runner/backend Pod
|
||||
-> receives Codex auth/config via read-only file projection
|
||||
-> receives Codex auth/config/model catalog via read-only file projection
|
||||
-> copies authorized files into writable CODEX_HOME before starting Codex app-server
|
||||
```
|
||||
|
||||
@@ -153,21 +165,22 @@ Secret 创建和轮换不由 source branch 自动生成;source branch 只声
|
||||
|
||||
## Codex Secret dry-run 工具
|
||||
|
||||
`v0.1` 提供只读 CLI 工具,用 operator 本地 `~/.codex/auth.json` 与 `~/.codex/config.toml` 形态的文件构造 Kubernetes Secret 创建计划:
|
||||
`v0.1` 提供只读 CLI 工具,用 operator 本地 `~/.codex/auth.json`、`~/.codex/config.toml` 和 profile 需要的额外文件构造 Kubernetes Secret 创建计划:
|
||||
|
||||
```bash
|
||||
./scripts/agentrun secrets codex render --dry-run [--profile codex|deepseek|minimax-m3]
|
||||
./scripts/agentrun secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go]
|
||||
```
|
||||
|
||||
可选参数:
|
||||
|
||||
- `--codex-home <dir>`:覆盖默认 `~/.codex` 输入目录。
|
||||
- `--profile <name>`:默认 `codex`;`deepseek` 和 `minimax-m3` 使用同一文件形态但默认 Secret name 分别为 `agentrun-v01-provider-deepseek`、`agentrun-v01-provider-minimax-m3`。
|
||||
- `--profile <name>`:默认 `codex`;`deepseek`、`minimax-m3` 和 `dsflash-go` 使用同一文件形态但默认 Secret name 分别为对应 `agentrun-v01-provider-<profile>`。
|
||||
- `--auth-file <path>` / `--config-file <path>`:分别覆盖输入文件路径。
|
||||
- `--model-catalog-file <path>`:覆盖 `dsflash-go` 的 `model-catalog.json` 输入文件路径。
|
||||
- `--namespace <name>`:默认 `agentrun-v01`。
|
||||
- `--secret-name <name>`:默认随 profile 变化,`codex` 为 `agentrun-v01-provider-codex`,`deepseek` 为 `agentrun-v01-provider-deepseek`,`minimax-m3` 为 `agentrun-v01-provider-minimax-m3`。
|
||||
|
||||
输出必须是 JSON,并且只包含 `namespace`、`secretName`、`keys`、每个输入文件的 `bytes`、`sha256`/`contentHash`、整体 hash、redaction 状态、apply 命令形状和 Secret manifest 摘要。输出不得包含 Secret value、`auth.json` 明文、`config.toml` 明文、base64 `data` 字段或可直接恢复 credential 的内容。工具只支持 `--dry-run`;不得执行 `kubectl apply`。
|
||||
输出必须是 JSON,并且只包含 `namespace`、`secretName`、`keys`、每个输入文件的 `bytes`、`sha256`/`contentHash`、整体 hash、redaction 状态、apply 命令形状和 Secret manifest 摘要。输出不得包含 Secret value、`auth.json` 明文、`config.toml` 明文、`model-catalog.json` 明文、base64 `data` 字段或可直接恢复 credential 的内容。工具只支持 `--dry-run`;不得执行 `kubectl apply`。
|
||||
|
||||
失败必须结构化返回 `failureKind`:缺文件、不可读文件或空 credential 归类为 `secret-unavailable`;非法 JSON/TOML 归类为 `schema-invalid`。
|
||||
|
||||
@@ -186,7 +199,7 @@ Secret 创建和轮换不由 source branch 自动生成;source branch 只声
|
||||
|
||||
### T2 Runner credential projection
|
||||
|
||||
阅读本文,然后分别启动 `backendProfile=codex`、`backendProfile=deepseek` 与 `backendProfile=minimax-m3` 的最小 backend runner dry-run,确认 Pod file projection 挂在 `/var/run/agentrun/secrets/...` 且只读,`/home/agentrun` 是 writable runtime home,runner/backend 只把当前 profile 授权文件复制到 `CODEX_HOME` 后再启动 Codex;event、日志和 CLI 输出只显示 redacted credential source,不显示文件内容。
|
||||
阅读本文,然后分别启动 `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3` 与 `backendProfile=dsflash-go` 的最小 backend runner dry-run,确认 Pod file projection 挂在 `/var/run/agentrun/secrets/...` 且只读,`/home/agentrun` 是 writable runtime home,runner/backend 只把当前 profile 授权文件复制到 `CODEX_HOME` 后再启动 Codex;`dsflash-go` 必须同时投影并复制 `model-catalog.json`;event、日志和 CLI 输出只显示 redacted credential source,不显示文件内容。
|
||||
|
||||
### T3 Missing secret failure
|
||||
|
||||
@@ -202,6 +215,7 @@ Secret 创建和轮换不由 source branch 自动生成;source branch 只声
|
||||
| Codex auth/config file projection | 已实现主路径 | backend readiness 检查 `auth.json`/`config.toml` 可读性,缺失时返回 `secret-unavailable`;真实 runner Job 将只读 projection 复制到 writable `CODEX_HOME`。 |
|
||||
| DeepSeek profile SecretRef | 已实现/已通过主闭环 | 已新增 `agentrun-v01-provider-deepseek` render、GitOps/RBAC 引用、Job projection、profile 选择和负向 missing-secret 自测试;真实 Secret 创建与 Kubernetes Job projection 已通过主闭环,轮换仍由 Kubernetes 密钥管理流程完成。 |
|
||||
| MiniMax-M3 profile SecretRef | 已实现/待真实主闭环 | 已新增 `agentrun-v01-provider-minimax-m3` render、GitOps/RBAC 引用、Job projection、profile 选择和负向 missing-secret 自测试;真实 Secret 创建使用 HWLAB Code Queue 现有 MiniMax API key,轮换仍由 Kubernetes 密钥管理流程完成。 |
|
||||
| dsflash-go profile SecretRef | 已实现/待真实主闭环 | 已新增 `agentrun-v01-provider-dsflash-go` 的 `model-catalog.json` required key、Secret render、Job projection、writable `CODEX_HOME` 复制和负向 readiness;真实 profile turn 仍需按 provider 管理 canary 和 HWLAB 原入口复测。 |
|
||||
| Tool credential SecretRef | 已实现最小 env projection | `executionPolicy.secretScope.toolCredentials[]` 已支持 `tool=github`、`tool=unidesk-ssh` 与 `projection.kind=env`,runner Job 通过 Kubernetes `secretKeyRef` 注入 env;CLI、event、runner job response 和 dry-run 只显示 SecretRef/projection 元数据,不输出值。 |
|
||||
| redaction 最小规则 | 已实现主路径 | Secret dry-run 工具、event、Job dry-run 输出、self-test 和真实主闭环均不打印 Secret value;复杂审计按 [spec-v01-validation.md](spec-v01-validation.md) 人工抽查。 |
|
||||
| 外部 secret manager | 未采用 | 如需 Vault/ExternalSecrets/SOPS,后续单独更新规格。 |
|
||||
|
||||
@@ -10,7 +10,7 @@ AgentRun 是面向 UniDesk 与 HWLAB 的共享 Code Agent 执行基础设施。`
|
||||
|
||||
- `agentrun-mgr` 是公共 RESTful API 和 durable facts authority,负责 run、command、event、runner、backend、lease 的持久状态和鉴权前置边界。
|
||||
- `agentrun-runner` 是短生命周期 per-run 或 per-attempt 执行者,必须从 manager claim run,并把 event、heartbeat 和 terminal status 写回 manager。
|
||||
- Backend adapter 隐藏具体 Agent 工具协议,`v0.1` 使用一个真实 Codex stdio backend kind 形成闭环,并在该 kind 下支持 `codex`、`deepseek` 与 `minimax-m3` profile;其他 backend kind 不进入第一波实现。
|
||||
- Backend adapter 隐藏具体 Agent 工具协议,`v0.1` 使用一个真实 Codex stdio backend kind 形成闭环,并在该 kind 下支持 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile;其他 backend kind 不进入第一波实现。
|
||||
- AgentRun CLI 是受控操作入口,负责创建 run、提交 command、轮询 events、手动启动 runner 和查看 backend capability;CLI 不等待完整模型 turn。
|
||||
- RuntimeAssembly 是 runner/backend 启动前的装配 SPEC,负责把 backend image、profile credential、session、Git-only resource bundle 和 tool credential scope 统一成受控 Job 输入;装配权威规格见 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md)。
|
||||
- Provider Profile 管理是 `agentrun-mgr` 的服务端委托能力,负责 profile status、API Key 写入、Secret/config 更新和 canary 验证;HWLAB 只把已鉴权管理动作委托给 AgentRun,AgentRun 不做 HWLAB 用户鉴权。权威规格见 [spec-v01-provider-profile-management.md](spec-v01-provider-profile-management.md)。
|
||||
@@ -27,23 +27,24 @@ AgentRun 是面向 UniDesk 与 HWLAB 的共享 Code Agent 执行基础设施。`
|
||||
|
||||
AgentRun `v0.1` 的自研组件优先使用 Bun + TypeScript 实现:`agentrun-mgr`、`agentrun-runner`、backend adapter、Codex backend、AgentRun CLI 和后续 scheduler 都属于该边界。官方 TypeScript CLI 入口固定为 `scripts/agentrun-cli.ts`,入口只做参数解析和路由,复杂逻辑拆到 `scripts/src/` 与 `src/`;G14/CI/人工非交互命令使用 `./scripts/agentrun` launcher 启动同一入口。Postgres、Kubernetes、Tekton、Argo CD、YAML manifest 和 shell 级容器启动命令属于外部运行面或部署面,不受“必须 TypeScript 实现”的约束。
|
||||
|
||||
`backendProfile=codex`、`backendProfile=deepseek` 与 `backendProfile=minimax-m3` 的 `v0.1` 协议固定为同一个 Codex CLI app-server JSON-RPC over stdio backend kind:runner/backend adapter 启动受控 `codex app-server --listen stdio://` 子进程,经 stdin/stdout 发送换行分隔 JSON-RPC,请求顺序至少覆盖 `initialize`、`thread/start` 或 `thread/resume`、`turn/start`。`backendProfile` 只选择 profile/config/SecretRef;`backendKind`、`protocol` 和进程生命周期仍是 `codex-app-server-stdio`。直接调用 Responses HTTP、OpenAI SDK、`codex exec` 一次性输出或文本 fallback 只能作为诊断/自测试辅助,不能作为 Codex backend 综合联调通过证据。
|
||||
`backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3` 与 `backendProfile=dsflash-go` 的 `v0.1` 协议固定为同一个 Codex CLI app-server JSON-RPC over stdio backend kind:runner/backend adapter 启动受控 `codex app-server --listen stdio://` 子进程,经 stdin/stdout 发送换行分隔 JSON-RPC,请求顺序至少覆盖 `initialize`、`thread/start` 或 `thread/resume`、`turn/start`。`backendProfile` 只选择 profile/config/SecretRef/model catalog;`backendKind`、`protocol` 和进程生命周期仍是 `codex-app-server-stdio`。直接调用 Responses HTTP、OpenAI SDK、`codex exec` 一次性输出或文本 fallback 只能作为诊断/自测试辅助,不能作为 Codex backend 综合联调通过证据。
|
||||
|
||||
实现参考必须优先读取并吸收两个成熟代码路径:UniDesk Code Queue 的 Bun/TS `src/components/microservices/code-queue/src/code-agent/codex.ts` 与 `common.ts`,以及 HWLAB v0.2 的 `internal/cloud/codex-stdio-session.mjs`、`scripts/code-agent-chat-smoke.mjs`、`docs/reference/spec-v02-deepseek-proxy.md`、`docs/reference/code-agent-chat-readiness.md`。AgentRun 复用的是 stdio JSON-RPC、session/turn 生命周期、trace、redaction、Secret projection、profile overlay、DeepSeek/Moon Bridge 分层诊断和 failureKind 经验,不复制 UniDesk/HWLAB 的环境专用路径、业务 prompt、bridge host、namespace 或明文凭据。
|
||||
|
||||
## Backend Profile 边界
|
||||
|
||||
`v0.1` 需要支持三个可手动选择的 backend profile,但不引入完整多 backend 调度:
|
||||
`v0.1` 需要支持四个可手动选择的 backend profile,但不引入完整多 backend 调度:
|
||||
|
||||
| backendProfile | backendKind | v0.1 处理 | SecretRef | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `codex` | `codex-app-server-stdio` | 保留,P0 | `agentrun-v01-provider-codex` | 现有 GPT/Codex profile,必须保持默认行为不回归。 |
|
||||
| `deepseek` | `codex-app-server-stdio` | 新增,P0 | `agentrun-v01-provider-deepseek` | DeepSeek-compatible Codex profile;通过 profile 专属 `auth.json`/`config.toml` 或等价 SecretRef 配置上游、模型和 base URL。 |
|
||||
| `minimax-m3` | `codex-app-server-stdio` | 新增,P0 | `agentrun-v01-provider-minimax-m3` | MiniMax-M3 OpenAI-compatible Codex profile;沿 DeepSeek 相同路径由 `auth.json`/`config.toml` 配置上游、模型和 base URL。 |
|
||||
| `dsflash-go` | `codex-app-server-stdio` | 新增,P0 | `agentrun-v01-provider-dsflash-go` | DeepSeek V4 Flash 1M context profile;除 `auth.json`/`config.toml` 外必须携带 profile-scoped `model-catalog.json`,由 Moon Bridge / OpenCode Zen Go compatible path 暴露模型元数据。 |
|
||||
|
||||
完整多 backend 路由仍然 deferred。`v0.1` 只允许 manager/runner 按 run 中的 `backendProfile` 显式选择 `codex`、`deepseek` 或 `minimax-m3`,并在 capability 中报告三者共享同一个 `protocol=codex-app-server-jsonrpc-stdio` 与 `transport=stdio`。旧 MiniMax/OpenCode 直连路线废弃,不进入 AgentRun Queue 首版,也不作为 fallback 或 judge backend。
|
||||
完整多 backend 路由仍然 deferred。`v0.1` 只允许 manager/runner 按 run 中的 `backendProfile` 显式选择 `codex`、`deepseek`、`minimax-m3` 或 `dsflash-go`,并在 capability 中报告四者共享同一个 `protocol=codex-app-server-jsonrpc-stdio` 与 `transport=stdio`。旧 MiniMax/OpenCode 直连路线废弃,不进入 AgentRun Queue 首版,也不作为 fallback 或 judge backend。
|
||||
|
||||
`codex`、`deepseek` 与 `minimax-m3` 之间不得隐式 fallback:缺少 `deepseek` 或 `minimax-m3` SecretRef 时必须失败为 `secret-unavailable`,不能改用 `codex` Secret;任一 profile 运行失败也不能重试到另一个 profile。同一轮发布的综合联调必须证明 `codex -> deepseek -> minimax-m3 -> codex` 的切换不会污染彼此的 SecretRef、`CODEX_HOME`、模型或 upstream 配置。
|
||||
`codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 之间不得隐式 fallback:缺少 `deepseek`、`minimax-m3` 或 `dsflash-go` SecretRef 时必须失败为 `secret-unavailable`,不能改用 `codex` Secret;任一 profile 运行失败也不能重试到另一个 profile。同一轮发布的综合联调必须证明 `codex -> deepseek -> minimax-m3 -> dsflash-go -> codex` 的切换不会污染彼此的 SecretRef、`CODEX_HOME`、模型、model catalog 或 upstream 配置。
|
||||
|
||||
## 内部架构
|
||||
|
||||
@@ -82,7 +83,7 @@ Runner inbound API 只允许本地或私有诊断,不作为业务客户端入
|
||||
| `agentrun-mgr` | 长驻服务 | 保留,P0 | 公共 RESTful API、durable facts、idempotency、runner claim、event append 和 status authority。 | `spec-v01-agentrun-mgr.md` |
|
||||
| `agentrun-runner` | 短生命周期执行入口 | 保留,P0 | per-run/per-attempt executor;claim run、poll command、调用 backend、写回 events/status。 | `spec-v01-agentrun-runner.md` |
|
||||
| Backend adapter | 执行适配层 | 保留,P0 | 统一 backend capability、event normalization、error mapping 和 credential boundary。 | `spec-v01-backend-adapter.md` |
|
||||
| Codex stdio backend profiles | 具体 Agent backend | 保留,P0 | `v0.1` 使用一个真实 Codex app-server stdio backend kind,必须支持 `codex`、`deepseek` 与 `minimax-m3` 三个 profile;完整多 backend 路由仍 deferred。 | `spec-v01-backend-codex.md` |
|
||||
| Codex stdio backend profiles | 具体 Agent backend | 保留,P0 | `v0.1` 使用一个真实 Codex app-server stdio backend kind,必须支持 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` 四个 profile;完整多 backend 路由仍 deferred。 | `spec-v01-backend-codex.md` |
|
||||
| AgentRun CLI | CLI/Job 工具 | 保留,P0 | JSON 输出、短返回、run/command/event/runner/backend 操作入口。 | `spec-v01-cli.md` |
|
||||
| Postgres durable store | 稳定外部服务 | 保留,P0 | 使用 `agentrun-v01-postgres` 保存 runs、commands、events、runners、backends、leases 和 migration ledger;不使用 file/sqlite 作为 v0.1 durable store。 | `spec-v01-postgres.md` |
|
||||
| Secret distribution | 系统能力 | 保留,P0 | Provider credential 只通过 Kubernetes SecretRef、ServiceAccount/RBAC 和 runner env/file projection 分发;Codex 测试凭据使用 `~/.codex/auth.json` 与 `~/.codex/config.toml` 生成 Secret projection;source、GitOps、logs 和 events 不保存明文。 | `spec-v01-secret-distribution.md` |
|
||||
@@ -106,7 +107,7 @@ Manager SA RBAC 增量:`persistentvolumeclaims: [create, get, list, watch, del
|
||||
|
||||
禁止路径:fake app-server mock 通过;强制重发同 sessionId 同 threadId 蒙混;`thread/resume:no rollout found` 改写为 `thread/start` + replacement threadId(PR #78 已否决);`idleTimeoutMs` 拉成永驻当成本特性;runner Job 启动后再做 copy/restore(撤掉的路径,禁止复活)。
|
||||
| AgentRun Queue | 系统能力 | 新增,P0 规格 | 直接吸收 UniDesk Code Queue 的队列能力;第一版只做 RESTful API、CLI 和轻量轮询,不做 Event/OA/integrations,不迁移历史数据。 | `spec-v01-queue.md` |
|
||||
| 多 backend 路由 | 系统能力 | Deferred | `v0.1` 不做跨 backend kind 的自动路由和调度;仅支持同一 Codex stdio backend kind 下的 `codex`/`deepseek`/`minimax-m3` profile 手动选择。旧 MiniMax/OpenCode 直连路线不进入 Queue 首版。 | 后续版本 spec |
|
||||
| 多 backend 路由 | 系统能力 | Deferred | `v0.1` 不做跨 backend kind 的自动路由和调度;仅支持同一 Codex stdio backend kind 下的 `codex`/`deepseek`/`minimax-m3`/`dsflash-go` profile 手动选择。旧 MiniMax/OpenCode 直连路线不进入 Queue 首版。 | 后续版本 spec |
|
||||
| UI | 前端 | Deferred | `v0.1` 不要求独立 UI;UniDesk/HWLAB canary 可通过 CLI/API 验证。 | 后续版本 spec |
|
||||
| judge/retry 自动化 | 系统能力 | Deferred | `v0.1` 只定义基础 terminal 和 failure visibility,不实现复杂 judge。 | 后续版本 spec |
|
||||
|
||||
@@ -150,7 +151,7 @@ Run create 的最小字段合同:
|
||||
| `projectId` | 必填,例如 `pikasTech/unidesk`、`pikasTech/HWLAB`。 |
|
||||
| `workspaceRef` | 必填,描述 source/worktree/workspace,不由 runner 猜测。 |
|
||||
| `providerId` | 必填,例如 `G14`、`D601`;只表示目标 provider,不直接授予权限。 |
|
||||
| `backendProfile` | 必填,`v0.1` allowlist 为 `codex`、`deepseek` 与 `minimax-m3`;三者共享 `codex-app-server-stdio` backend kind。 |
|
||||
| `backendProfile` | 必填,`v0.1` allowlist 为 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go`;四者共享 `codex-app-server-stdio` backend kind。 |
|
||||
| `executionPolicy` | 必填或由 manager 显式写入默认值,至少包含 sandbox、approval、timeout、network 和 secret scope。 |
|
||||
| `traceSink` | 字段必须存在;可以为 `null` 或显式 sink,表示标准事件是否需要镜像给 tenant。 |
|
||||
|
||||
@@ -216,4 +217,5 @@ Manager 负责校验、保存和返回这些字段;runner 只能消费已保
|
||||
| `codex` profile | 已实现/已通过主闭环 | Codex app-server stdio backend 已有协议、失败分类、脱敏和 fake self-test;真实 Codex provider turn 已通过 RESTful API 与 CLI 主闭环。 |
|
||||
| `deepseek` profile | 已实现/已通过主闭环 | DeepSeek 已作为同一 Codex stdio backend kind 的 profile/config/SecretRef 选择进入 v0.1;自测试覆盖 registry、runner Secret 选择、fake stdio turn 和无 fallback,真实 `agentrun-v01` 已通过 `codex -> deepseek -> codex` 切换联调。 |
|
||||
| `minimax-m3` profile | 已实现/待真实主闭环 | MiniMax-M3 已作为同一 Codex stdio backend kind 的 profile/config/SecretRef 选择进入 v0.1;自测试覆盖 registry、runner Secret 选择、fake stdio turn 和无 fallback,真实 `agentrun-v01` 仍需用 AgentRun CLI 手动验收。 |
|
||||
| `dsflash-go` profile | 已实现/待真实主闭环 | dsflash-go 已作为同一 Codex stdio backend kind 的 DeepSeek V4 Flash 1M profile 进入 v0.1;自测试覆盖 registry、model catalog、runner Secret 选择、legacy SecretRef 归一、fake stdio turn 和 compact 404 failureKind,真实 `agentrun-v01` 仍需用 AgentRun CLI/HWLAB 原入口验收。 |
|
||||
| 自动 scheduler | Deferred | 不作为 `v0.1` 第一阶段验收目标。 |
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
- Codex Secret projection 必须先保持只读,再复制到 writable `CODEX_HOME` 后启动 app-server;综合联调不得把只读 Secret volume 直接当作 `CODEX_HOME` 的通过证据。
|
||||
- 真实 `agentrun-mgr`、runner Job 或受控 runner process、真实 backend adapter。
|
||||
- 至少一个真实 Code Agent provider turn;Codex stdio backend 必须通过 `codex app-server --listen stdio://` 的 JSON-RPC stdio turn 完成,mock、fixture、source-only、dry-run、fake provider、直接 Responses HTTP 或 `codex exec` 一次性输出不能作为通过证据。如果 provider credential SecretRef 缺失,综合联调必须标记 blocked,不能降级为 mock pass。
|
||||
- 若变更涉及 backend profile,综合联调必须分别覆盖 `backendProfile=codex`、`backendProfile=deepseek` 与 `backendProfile=minimax-m3`,并按 `codex -> deepseek -> minimax-m3 -> codex` 顺序证明 profile 切换不互相污染。
|
||||
- 若变更涉及 backend profile,综合联调必须分别覆盖 `backendProfile=codex`、`backendProfile=deepseek`、`backendProfile=minimax-m3` 与 `backendProfile=dsflash-go`,并按 `codex -> deepseek -> minimax-m3 -> dsflash-go -> codex` 顺序证明 profile 切换不互相污染;`dsflash-go` 还必须证明 `model-catalog.json` 与 1M/900k context metadata 生效。
|
||||
|
||||
综合联调最小闭环:
|
||||
|
||||
@@ -107,7 +107,7 @@ CLI 与 RESTful API 可以复用同一个真实 run 做联调。若两者观察
|
||||
| ResourceBundleRef | 使用 `repoUrl + ref/branch + bundles[]` 启动 runner | runner checkout 到允许 workspace,event/result 能回答 repo、requested ref/commit、actual commit、workspace 摘要;不使用 host path、cloud-api artifact revision 或 CI/CD rollout 状态作为 bundle 内容来源。 |
|
||||
| Resource prompt/skill assembly | 使用同一 `ResourceBundleRef.kind="gitbundle"` 指定 `bundles[]` 和 `promptRefs` | 新 thread 首轮注入 initial prompt 和 gitbundle skill facts;resume 不重复注入;required prompt 缺失 blocked;不使用用户长 prompt、旧硬编码 prompt、镜像 `/app/skills` 或默认 Codex skill registry 替代。 |
|
||||
| DS 短 prompt 探测 | 通过正式 CLI/Web 等价入口向真实 `backendProfile=deepseek` 发送短 prompt | “可见 skill”回复包含 gitbundle `.agents/skills` 中的业务 skill 而不只是 Codex 默认系统 skill;“当前 HWLAB 初始规则”能回答 hwpod/HWPOD 四要素/禁止路径;“编译 D601-F103-V2”能触发 `hwpod -> hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node` 正向链路。 |
|
||||
| ProfileRef/SecretRef | 分别验证 `codex`、`deepseek` 与 `minimax-m3` profile | 只使用当前 profile SecretRef;缺失时 `secret-unavailable`,不 fallback,不泄露 Secret 值。 |
|
||||
| ProfileRef/SecretRef | 分别验证 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile | 只使用当前 profile SecretRef;`dsflash-go` 必须投影 `model-catalog.json`;缺失时 `secret-unavailable`,不 fallback,不泄露 Secret 值。 |
|
||||
| bounded output | 触发工具/命令输出摘要 | result/event 只含摘要、字节数、截断标记和必要引用,不把大 stdout/stderr 塞入单个 JSON 响应。 |
|
||||
|
||||
这组验收吸收 HWLAB v0.2 的成熟判定口径,但不验证 HWLAB 用户鉴权、HWPOD 授权、Workbench UI、`traceId -> runId` 业务映射或 HWLAB result/trace schema 转换。
|
||||
@@ -163,7 +163,7 @@ CLI 与 RESTful API 可以复用同一个真实 run 做联调。若两者观察
|
||||
- 对同一 run 使用相同 idempotency key 提交相同 command 两次,再用相同 key 提交不同 payload,确认前两次返回同一个 command,第三次结构化失败。
|
||||
- 对同一 pending run 启动两个真实 runner 或重复 claim,确认只有一个 owner 成功,失败方为 `runner-lease-conflict` 或等价 failureKind,且不继续调用 backend。
|
||||
- 用不存在的 provider SecretRef 创建 run 并启动 runner,确认失败为 `secret-unavailable`,不会降级为 mock pass,也不会打印 Secret value。
|
||||
- 用 `backendProfile=deepseek` 或 `backendProfile=minimax-m3` 但只提供 `codex` SecretRef 的 run 启动 runner,确认失败为 `secret-unavailable`,不会 fallback 到 `codex`。
|
||||
- 用 `backendProfile=deepseek`、`backendProfile=minimax-m3` 或 `backendProfile=dsflash-go` 但只提供 `codex` SecretRef 的 run 启动 runner,确认失败为 `secret-unavailable`,不会 fallback 到 `codex`;`dsflash-go` 缺少 `model-catalog.json` 时也必须在 provider 调用前失败。
|
||||
- 对同一 run 分页读取 events,确认 `seq` 单调、`afterSeq` 翻页无重复、重复读取同一页不会改变 durable facts。
|
||||
|
||||
T7 只定义人工验收的检查面和判定口径。若后续为减少人工操作引入 helper 命令,它只能输出手动步骤、当前证据或 dry-run 计划,不能把这些负向场景改造成阻断发布的自动门禁。
|
||||
@@ -172,12 +172,13 @@ T7 只定义人工验收的检查面和判定口径。若后续为减少人工
|
||||
|
||||
阅读本文、[spec-v01-services.md](spec-v01-services.md)、[spec-v01-backend-codex.md](spec-v01-backend-codex.md) 和 [spec-v01-secret-distribution.md](spec-v01-secret-distribution.md),然后在真实 `agentrun-v01` 运行面用正式 CLI 和 RESTful API 手动验证以下内容:
|
||||
|
||||
- `GET /api/v1/backends` 和 `./scripts/agentrun backends list` 同时列出 `codex`、`deepseek` 与 `minimax-m3`,并显示三者共享 `backendKind=codex-app-server-stdio`、`protocol=codex-app-server-jsonrpc-stdio`、`transport=stdio`。
|
||||
- `GET /api/v1/backends` 和 `./scripts/agentrun backends list` 同时列出 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go`,并显示四者共享 `backendKind=codex-app-server-stdio`、`protocol=codex-app-server-jsonrpc-stdio`、`transport=stdio`;`dsflash-go` 的 `requiredSecretKeys` 必须包含 `model-catalog.json`。
|
||||
- 用 `backendProfile=codex` 完成一个真实 app-server stdio turn,记录 runId、commandId、terminal_status 和 redacted backend_status。
|
||||
- 用 `backendProfile=deepseek` 完成一个真实 app-server stdio turn,确认 SecretRef 为 `agentrun-v01-provider-deepseek`,runtime `CODEX_HOME` 与 `codex` run 隔离,assistant 回复非空或失败被正确归类为 provider blocker。
|
||||
- 用 `backendProfile=minimax-m3` 完成一个真实 app-server stdio turn,确认 SecretRef 为 `agentrun-v01-provider-minimax-m3`,runtime `CODEX_HOME` 与 `codex`/`deepseek` run 隔离,assistant 回复非空或失败被正确归类为 provider blocker。
|
||||
- 再次用 `backendProfile=codex` 完成真实 turn,确认没有继承 DeepSeek 或 MiniMax-M3 model/base URL/config。
|
||||
- 删除或替换 `deepseek`/`minimax-m3` SecretRef 后复测,必须 `secret-unavailable`,不能使用 `codex` SecretRef 或默认 Codex config。
|
||||
- 用 `backendProfile=dsflash-go` 完成一个真实 app-server stdio turn,确认 SecretRef 为 `agentrun-v01-provider-dsflash-go`,runtime `CODEX_HOME` 与其他 profile 隔离,`config.toml` 指向 `deepseek-v4-flash`、1M/900k context 和 profile-local `model_catalog_json`;assistant 回复非空或失败被正确归类为 provider blocker,compact 404 必须归类为 `provider-compact-unsupported`。
|
||||
- 再次用 `backendProfile=codex` 完成真实 turn,确认没有继承 DeepSeek、MiniMax-M3 或 dsflash-go model/base URL/config/model catalog。
|
||||
- 删除或替换 `deepseek`/`minimax-m3`/`dsflash-go` SecretRef 后复测,必须 `secret-unavailable`,不能使用 `codex` SecretRef 或默认 Codex config。
|
||||
|
||||
T8 是涉及 backend profile 变更时的综合联调标准;不涉及 backend profile 的普通发布仍至少执行已有真实主闭环。
|
||||
|
||||
|
||||
+6
-3
@@ -13,7 +13,7 @@ import { renderCodexProviderSecretPlan } from "./secret-render.js";
|
||||
import type { BackendProfile, CommandRecord, JsonRecord, JsonValue, RunRecord, SessionSummary } from "../../src/common/types.js";
|
||||
import { AgentRunError, errorToJson } from "../../src/common/errors.js";
|
||||
import type { RunnerOnceOptions } from "../../src/runner/run-once.js";
|
||||
import { isBackendProfile } from "../../src/common/backend-profiles.js";
|
||||
import { backendProfileSpec, isBackendProfile } from "../../src/common/backend-profiles.js";
|
||||
|
||||
interface ParsedArgs {
|
||||
positional: string[];
|
||||
@@ -387,12 +387,14 @@ async function renderCodexSecret(args: ParsedArgs): Promise<JsonRecord> {
|
||||
const codexHome = optionalFlag(args, "codex-home");
|
||||
const authFile = optionalFlag(args, "auth-file");
|
||||
const configFile = optionalFlag(args, "config-file");
|
||||
const modelCatalogFile = optionalFlag(args, "model-catalog-file");
|
||||
const namespace = optionalFlag(args, "namespace");
|
||||
const secretName = optionalFlag(args, "secret-name");
|
||||
if (profile) options.profile = profile;
|
||||
if (codexHome) options.codexHome = codexHome;
|
||||
if (authFile) options.authFile = authFile;
|
||||
if (configFile) options.configFile = configFile;
|
||||
if (modelCatalogFile) options.modelCatalogFile = modelCatalogFile;
|
||||
if (namespace) options.namespace = namespace;
|
||||
if (secretName) options.secretName = secretName;
|
||||
return renderCodexProviderSecretPlan(options);
|
||||
@@ -707,7 +709,8 @@ function jsonObjectFlag(args: ParsedArgs, name: string): JsonRecord | null {
|
||||
}
|
||||
|
||||
function defaultExecutionPolicy(profile: BackendProfile): JsonRecord {
|
||||
return { sandbox: "workspace-write", approval: "never", timeoutMs: 900000, network: "enabled", secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile, secretRef: { name: `agentrun-v01-provider-${profile}`, keys: ["auth.json", "config.toml"] } }] } };
|
||||
const keys = [...(backendProfileSpec(profile)?.requiredSecretKeys ?? ["auth.json", "config.toml"])] as string[];
|
||||
return { sandbox: "workspace-write", approval: "never", timeoutMs: 900000, network: "enabled", secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile, secretRef: { name: `agentrun-v01-provider-${profile}`, keys } }] } };
|
||||
}
|
||||
|
||||
function copyOptionalFlag(args: ParsedArgs, target: JsonRecord, flagName: string, key = flagName.replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase())): void {
|
||||
@@ -792,7 +795,7 @@ function help(): JsonRecord {
|
||||
"queue cancel <taskId> [--reason <text>]",
|
||||
"queue dispatch <taskId> [--json-file <dispatch.json>] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>]",
|
||||
"queue refresh <taskId>",
|
||||
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go] [--codex-home <dir>] [--namespace agentrun-v01] [--secret-name <name>]",
|
||||
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go] [--codex-home <dir>] [--model-catalog-file <file>] [--namespace agentrun-v01] [--secret-name <name>]",
|
||||
"provider-profiles list",
|
||||
"provider-profiles show <profile>",
|
||||
"provider-profiles config <profile>",
|
||||
|
||||
@@ -12,13 +12,14 @@ export interface CodexSecretRenderOptions {
|
||||
codexHome?: string;
|
||||
authFile?: string;
|
||||
configFile?: string;
|
||||
modelCatalogFile?: string;
|
||||
namespace?: string;
|
||||
secretName?: string;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
interface SecretFileSummary extends JsonRecord {
|
||||
key: "auth.json" | "config.toml";
|
||||
key: string;
|
||||
source: string;
|
||||
bytes: number;
|
||||
sha256: string;
|
||||
@@ -26,13 +27,12 @@ interface SecretFileSummary extends JsonRecord {
|
||||
}
|
||||
|
||||
interface SecretSourceFile {
|
||||
key: "auth.json" | "config.toml";
|
||||
key: string;
|
||||
path: string;
|
||||
validate: (content: string, file: string) => unknown;
|
||||
}
|
||||
|
||||
const defaultNamespace = "agentrun-v01";
|
||||
const secretKeys = ["auth.json", "config.toml"] as const;
|
||||
const credentialKeyPattern = /(?:api[_-]?key|token|password|secret|credential|authorization|auth)/iu;
|
||||
|
||||
export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOptions = {}): Promise<JsonRecord> {
|
||||
@@ -46,10 +46,8 @@ export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOp
|
||||
const namespace = nonEmpty(options.namespace, defaultNamespace);
|
||||
const secretName = nonEmpty(options.secretName, spec?.defaultSecretName ?? "agentrun-v01-provider-codex");
|
||||
const codexHome = resolvePath(nonEmpty(options.codexHome, path.join(os.homedir(), ".codex")));
|
||||
const sources: SecretSourceFile[] = [
|
||||
{ key: "auth.json", path: resolvePath(options.authFile ?? path.join(codexHome, "auth.json")), validate: validateAuthJson },
|
||||
{ key: "config.toml", path: resolvePath(options.configFile ?? path.join(codexHome, "config.toml")), validate: validateConfigToml },
|
||||
];
|
||||
const secretKeys = [...(spec?.requiredSecretKeys ?? ["auth.json", "config.toml"])] as string[];
|
||||
const sources = secretKeys.map((key): SecretSourceFile => sourceForSecretKey(key, codexHome, options));
|
||||
|
||||
const files: SecretFileSummary[] = [];
|
||||
const hash = createHash("sha256");
|
||||
@@ -89,7 +87,7 @@ export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOp
|
||||
manifestSummary,
|
||||
apply: {
|
||||
attempted: false,
|
||||
command: `kubectl create secret generic ${secretName} -n ${namespace} --from-file=auth.json=<redacted> --from-file=config.toml=<redacted> --dry-run=client -o yaml | kubectl apply -f -`,
|
||||
command: `kubectl create secret generic ${secretName} -n ${namespace} ${secretKeys.map((key) => `--from-file=${key}=<redacted>`).join(" ")} --dry-run=client -o yaml | kubectl apply -f -`,
|
||||
note: "本命令只展示形状;v0.1 工具不会执行 kubectl apply,也不会输出 Secret data。",
|
||||
},
|
||||
redaction: {
|
||||
@@ -101,6 +99,13 @@ export async function renderCodexProviderSecretPlan(options: CodexSecretRenderOp
|
||||
};
|
||||
}
|
||||
|
||||
function sourceForSecretKey(key: string, codexHome: string, options: CodexSecretRenderOptions): SecretSourceFile {
|
||||
if (key === "auth.json") return { key, path: resolvePath(options.authFile ?? path.join(codexHome, key)), validate: validateAuthJson };
|
||||
if (key === "config.toml") return { key, path: resolvePath(options.configFile ?? path.join(codexHome, key)), validate: validateConfigToml };
|
||||
if (key === "model-catalog.json") return { key, path: resolvePath(options.modelCatalogFile ?? path.join(codexHome, key)), validate: validateModelCatalogJson };
|
||||
return { key, path: resolvePath(path.join(codexHome, key)), validate: validateNonEmptyFile(key) };
|
||||
}
|
||||
|
||||
async function readSecretInput(source: SecretSourceFile): Promise<string> {
|
||||
try {
|
||||
await access(source.path, fsConstants.R_OK);
|
||||
@@ -154,6 +159,36 @@ function validateConfigToml(content: string, file: string): unknown {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function validateModelCatalogJson(content: string, file: string): unknown {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch {
|
||||
throw new AgentRunError("schema-invalid", "model-catalog.json is not valid JSON", { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
throw new AgentRunError("schema-invalid", "model-catalog.json must contain a JSON object", { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
|
||||
}
|
||||
const models = (parsed as { models?: unknown }).models;
|
||||
if (!Array.isArray(models) || models.length === 0) {
|
||||
throw new AgentRunError("schema-invalid", "model-catalog.json must contain a non-empty models array", { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
|
||||
}
|
||||
for (const [index, model] of models.entries()) {
|
||||
const item = typeof model === "object" && model !== null && !Array.isArray(model) ? model as JsonRecord : null;
|
||||
if (!item) throw new AgentRunError("schema-invalid", `model-catalog.json models[${index}] must be an object`, { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
|
||||
if (typeof item.slug !== "string" || item.slug.trim().length === 0) throw new AgentRunError("schema-invalid", `model-catalog.json models[${index}].slug is required`, { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
|
||||
if (typeof item.context_window !== "number" || !Number.isFinite(item.context_window) || item.context_window <= 0) throw new AgentRunError("schema-invalid", `model-catalog.json models[${index}].context_window must be a positive number`, { httpStatus: 2, details: { key: "model-catalog.json", path: file } });
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function validateNonEmptyFile(key: string): (content: string, file: string) => unknown {
|
||||
return (content: string, file: string) => {
|
||||
if (content.length === 0) throw new AgentRunError("secret-unavailable", `${key} is empty`, { httpStatus: 2, details: { key, path: file } });
|
||||
return content;
|
||||
};
|
||||
}
|
||||
|
||||
function hasNonEmptyCredentialField(value: unknown): boolean {
|
||||
if (Array.isArray(value)) return value.some((item) => hasNonEmptyCredentialField(item));
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { accessSync, constants as fsConstants } from "node:fs";
|
||||
import { accessSync, constants as fsConstants, readdirSync, readFileSync } from "node:fs";
|
||||
import { chmod, copyFile, mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
@@ -351,6 +351,7 @@ export class CodexStdioBackendSession {
|
||||
...backendMetadata(options),
|
||||
protocol: codexProtocol,
|
||||
runtime: runtimeSummary(options, env, resolveCodexHome(options)),
|
||||
config: codexConfigSummary(resolveCodexHome(options), options.backendProfile ?? "codex"),
|
||||
},
|
||||
});
|
||||
const clientOptions: ConstructorParameters<typeof CodexStdioClient>[0] = {
|
||||
@@ -386,7 +387,7 @@ async function runCodexStdioTurnWithSession(options: CodexStdioTurnOptions, sess
|
||||
const codexHome = resolveCodexHome(options);
|
||||
const projectionFailure = await prepareProjectedCodexHome(codexHome, options.env?.AGENTRUN_CODEX_SECRET_HOME ?? process.env.AGENTRUN_CODEX_SECRET_HOME);
|
||||
if (projectionFailure) return projectionFailure;
|
||||
const secretFailure = codexHomeReadiness(codexHome);
|
||||
const secretFailure = codexHomeReadiness(codexHome, options.backendProfile ?? "codex");
|
||||
if (secretFailure) return secretFailure;
|
||||
const env = childEnv(options, codexHome);
|
||||
const events: BackendEvent[] = [];
|
||||
@@ -561,7 +562,7 @@ async function prepareProjectedCodexHome(codexHome: string, projectedHome: strin
|
||||
if (path.resolve(projectedHome) === path.resolve(codexHome)) return null;
|
||||
try {
|
||||
await mkdir(codexHome, { recursive: true, mode: 0o700 });
|
||||
for (const fileName of ["auth.json", "config.toml"]) {
|
||||
for (const fileName of projectedCodexHomeFiles(projectedHome)) {
|
||||
await copyFile(path.join(projectedHome, fileName), path.join(codexHome, fileName));
|
||||
await chmod(path.join(codexHome, fileName), 0o600);
|
||||
}
|
||||
@@ -588,16 +589,18 @@ async function prepareProjectedCodexHome(codexHome: string, projectedHome: strin
|
||||
}
|
||||
}
|
||||
|
||||
function codexHomeReadiness(codexHome: string): BackendTurnResult | null {
|
||||
function codexHomeReadiness(codexHome: string, profile: BackendProfile): BackendTurnResult | null {
|
||||
const auth = fileReadable(`${codexHome}/auth.json`);
|
||||
const config = fileReadable(`${codexHome}/config.toml`);
|
||||
if (auth.readable && config.readable) return null;
|
||||
const modelCatalog = auth.readable && config.readable ? modelCatalogReadiness(codexHome, profile) : null;
|
||||
if (auth.readable && config.readable && !modelCatalog) return null;
|
||||
const payload = {
|
||||
failureKind: "secret-unavailable",
|
||||
projection: {
|
||||
codexHome: pathSummary(codexHome),
|
||||
authJson: auth,
|
||||
configToml: config,
|
||||
...(modelCatalog ? { modelCatalogJson: modelCatalog } : {}),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
} satisfies JsonRecord;
|
||||
@@ -612,6 +615,88 @@ function codexHomeReadiness(codexHome: string): BackendTurnResult | null {
|
||||
};
|
||||
}
|
||||
|
||||
function projectedCodexHomeFiles(projectedHome: string): string[] {
|
||||
const required = ["auth.json", "config.toml"];
|
||||
try {
|
||||
const optionalFiles = readdirSync(projectedHome).filter((name) => name === "model-catalog.json");
|
||||
return [...required, ...optionalFiles];
|
||||
} catch {
|
||||
return required;
|
||||
}
|
||||
}
|
||||
|
||||
function modelCatalogReadiness(codexHome: string, profile: BackendProfile): JsonRecord | null {
|
||||
let configToml = "";
|
||||
try {
|
||||
configToml = readFileSync(path.join(codexHome, "config.toml"), "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const modelCatalogPath = modelCatalogJsonPathFromConfig(configToml, codexHome);
|
||||
if (!modelCatalogPath) {
|
||||
return profile === "dsflash-go" ? { present: false, requiredByConfig: false, requiredByProfile: true, valuesPrinted: false } : null;
|
||||
}
|
||||
const readiness = fileReadable(modelCatalogPath);
|
||||
return readiness.readable ? null : { ...readiness, requiredByConfig: true, valuesPrinted: false };
|
||||
}
|
||||
|
||||
function modelCatalogJsonPathFromConfig(configToml: string, codexHome: string): string | null {
|
||||
const match = configToml.match(/^\s*model_catalog_json\s*=\s*"([^"]+)"\s*$/mu);
|
||||
if (!match?.[1]) return null;
|
||||
return path.isAbsolute(match[1]) ? match[1] : path.join(codexHome, match[1]);
|
||||
}
|
||||
|
||||
function codexConfigSummary(codexHome: string, profile: BackendProfile): JsonRecord {
|
||||
const configPath = path.join(codexHome, "config.toml");
|
||||
let configToml = "";
|
||||
try {
|
||||
configToml = readFileSync(configPath, "utf8");
|
||||
} catch {
|
||||
return { available: false, valuesPrinted: false };
|
||||
}
|
||||
const model = tomlStringValue(configToml, "model");
|
||||
const providerName = tomlStringValue(configToml, "model_provider");
|
||||
const baseUrl = tomlStringValue(configToml, "base_url");
|
||||
const modelCatalogPath = modelCatalogJsonPathFromConfig(configToml, codexHome);
|
||||
return {
|
||||
available: true,
|
||||
profile,
|
||||
model,
|
||||
providerName,
|
||||
baseUrl: baseUrl ? redactedUrlSummary(baseUrl) : null,
|
||||
wireApi: tomlStringValue(configToml, "wire_api"),
|
||||
contextWindow: tomlNumberValue(configToml, "model_context_window"),
|
||||
autoCompactTokenLimit: tomlNumberValue(configToml, "model_auto_compact_token_limit"),
|
||||
modelCatalogJson: modelCatalogPath ? { ...pathSummary(modelCatalogPath), readable: fileReadable(modelCatalogPath).readable } : null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function tomlStringValue(configToml: string, key: string): string | null {
|
||||
const match = configToml.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "mu"));
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function tomlNumberValue(configToml: string, key: string): number | null {
|
||||
const match = configToml.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*([0-9][0-9_]*)\\s*$`, "mu"));
|
||||
if (!match?.[1]) return null;
|
||||
const value = Number(match[1].replace(/_/gu, ""));
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function redactedUrlSummary(value: string): JsonRecord {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return { protocol: url.protocol.replace(/:$/u, ""), hostname: url.hostname, port: url.port || null, pathname: url.pathname, valuesPrinted: false };
|
||||
} catch {
|
||||
return { valid: false, fingerprint: shortHash(value), valuesPrinted: false };
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
function normalizeCodexNotification(message: JsonRecord, suppressed: SuppressedNotificationSummary): { events: BackendEvent[]; assistantDelta?: { itemId: string | null; text: string }; completedAssistantMessage?: CompletedAssistantMessage; threadId?: string; turnId?: string; terminal?: { status: TerminalStatus; failureKind: FailureKind | null; message: string | null } } {
|
||||
const method = typeof message.method === "string" ? message.method : "unknown";
|
||||
const params = asRecordAt(message, "params");
|
||||
@@ -1063,6 +1148,7 @@ function classifyCodexErrorRecord(error: JsonRecord, fallback: FailureKind): Fai
|
||||
|
||||
function classifyMessageFailureKind(message: string, fallback: FailureKind): FailureKind {
|
||||
const text = String(message || "").toLowerCase();
|
||||
if (isProviderCompactUnsupportedMessage(text)) return "provider-compact-unsupported";
|
||||
if (/invalid[_ -]?prompt/u.test(text) && /invalid function arguments json string|tool_call_id/u.test(text)) return "provider-invalid-tool-call";
|
||||
if (/invalid function arguments json string/u.test(text)) return "provider-invalid-tool-call";
|
||||
if (/rate.?limit|too many requests|\b429\b/u.test(text)) return "provider-rate-limited";
|
||||
@@ -1073,6 +1159,10 @@ function classifyMessageFailureKind(message: string, fallback: FailureKind): Fai
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function isProviderCompactUnsupportedMessage(text: string): boolean {
|
||||
return /responses\/compact|\/compact\b/u.test(text) && /\b404\b|not found|unsupported|no route|not implemented/u.test(text);
|
||||
}
|
||||
|
||||
function isProviderUnavailableMessage(text: string): boolean {
|
||||
if (/\b(?:http(?:\s+status)?|status(?:\s+code)?|code)\s*[:=]?\s*5\d\d\b/u.test(text)) return true;
|
||||
if (/\b5\d\d\b/u.test(text) && /service unavailable|bad gateway|gateway timeout|internal server error|provider|upstream|response\s*stream\s*disconnected|responsestreamdisconnected/u.test(text)) return true;
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface BackendProfileSpec {
|
||||
transport: "stdio";
|
||||
command: "codex app-server --listen stdio://";
|
||||
status: "registered";
|
||||
requiredSecretKeys: ["auth.json", "config.toml"];
|
||||
requiredSecretKeys: readonly string[];
|
||||
defaultSecretName: string;
|
||||
profileIsolation: "profile-scoped-codex-home";
|
||||
description: string;
|
||||
@@ -59,7 +59,7 @@ const builtinBackendProfileSpecs: readonly BackendProfileSpec[] = [
|
||||
transport: "stdio",
|
||||
command: "codex app-server --listen stdio://",
|
||||
status: "registered",
|
||||
requiredSecretKeys: ["auth.json", "config.toml"],
|
||||
requiredSecretKeys: ["auth.json", "config.toml", "model-catalog.json"],
|
||||
defaultSecretName: "agentrun-v01-provider-dsflash-go",
|
||||
profileIsolation: "profile-scoped-codex-home",
|
||||
description: "DeepSeek V4 Flash profile through OpenCode Zen Go Moon Bridge",
|
||||
@@ -142,7 +142,7 @@ export function backendCapabilities(): JsonRecord[] {
|
||||
return builtinBackendProfileSpecs.map(backendCapability);
|
||||
}
|
||||
|
||||
export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[]): string {
|
||||
export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[], options: { requiredSecretKeysByProfile?: Record<string, readonly string[]> } = {}): string {
|
||||
const specs = profiles
|
||||
? profiles.map((profile) => {
|
||||
const spec = backendProfileSpec(profile);
|
||||
@@ -151,13 +151,14 @@ export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[
|
||||
})
|
||||
: builtinBackendProfileSpecs;
|
||||
return specs.map((spec) => {
|
||||
const requiredSecretKeys = options.requiredSecretKeysByProfile?.[spec.profile] ?? spec.requiredSecretKeys;
|
||||
const capabilities = JSON.stringify({
|
||||
backendKind: spec.backendKind,
|
||||
protocol: spec.protocol,
|
||||
transport: spec.transport,
|
||||
command: spec.command,
|
||||
requiredSecretKeys: spec.requiredSecretKeys,
|
||||
defaultSecretRef: { name: spec.defaultSecretName, keys: spec.requiredSecretKeys },
|
||||
requiredSecretKeys,
|
||||
defaultSecretRef: { name: spec.defaultSecretName, keys: requiredSecretKeys },
|
||||
profileIsolation: spec.profileIsolation,
|
||||
description: spec.description,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { JsonRecord } from "./types.js";
|
||||
|
||||
export const dsflashGoModelSlug = "deepseek-v4-flash";
|
||||
export const dsflashGoModelCatalogFile = "model-catalog.json";
|
||||
export const dsflashGoRuntimeModelCatalogPath = "/home/agentrun/.codex-dsflash-go/model-catalog.json";
|
||||
|
||||
const dsflashGoModelCatalog: JsonRecord = {
|
||||
models: [
|
||||
{
|
||||
slug: dsflashGoModelSlug,
|
||||
display_name: "DeepSeek V4 Flash via OpenCode Zen Go",
|
||||
description: "DeepSeek V4 Flash exposed to Codex through OpenCode Zen Go and a Responses-compatible Moon Bridge profile.",
|
||||
default_reasoning_level: "xhigh",
|
||||
supported_reasoning_levels: [
|
||||
{ effort: "low", description: "Fast responses with lighter reasoning" },
|
||||
{ effort: "medium", description: "Balanced reasoning" },
|
||||
{ effort: "high", description: "Deep reasoning" },
|
||||
{ effort: "xhigh", description: "Maximum reasoning" },
|
||||
],
|
||||
shell_type: "shell_command",
|
||||
visibility: "list",
|
||||
supported_in_api: true,
|
||||
priority: 0,
|
||||
additional_speed_tiers: [],
|
||||
service_tiers: [],
|
||||
availability_nux: null,
|
||||
upgrade: null,
|
||||
base_instructions: "You are Codex, a coding agent based on DeepSeek V4 Flash. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.",
|
||||
model_messages: {},
|
||||
supports_reasoning_summaries: true,
|
||||
default_reasoning_summary: "auto",
|
||||
support_verbosity: false,
|
||||
apply_patch_tool_type: "freeform",
|
||||
web_search_tool_type: "text_and_image",
|
||||
truncation_policy: { mode: "tokens", limit: 10000 },
|
||||
supports_parallel_tool_calls: true,
|
||||
supports_image_detail_original: false,
|
||||
context_window: 1_000_000,
|
||||
max_context_window: 1_000_000,
|
||||
auto_compact_token_limit: 900_000,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: [],
|
||||
input_modalities: ["text"],
|
||||
supports_search_tool: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function dsflashGoModelCatalogJson(): string {
|
||||
return `${JSON.stringify(dsflashGoModelCatalog, null, 2)}\n`;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export type FailureKind =
|
||||
| "provider-auth-failed"
|
||||
| "provider-rate-limited"
|
||||
| "provider-invalid-tool-call"
|
||||
| "provider-compact-unsupported"
|
||||
| "provider-unavailable"
|
||||
| "infra-failed"
|
||||
| "session-store-evicted"
|
||||
|
||||
@@ -185,18 +185,16 @@ export function validateExecutionPolicy(record: JsonRecord): ExecutionPolicy {
|
||||
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) throw new AgentRunError("schema-invalid", "executionPolicy.timeoutMs must be a positive number", { httpStatus: 400 });
|
||||
const secretScope = asRecord(record.secretScope ?? {}, "executionPolicy.secretScope");
|
||||
if (secretScope.allowCredentialEcho !== undefined && secretScope.allowCredentialEcho !== false) throw new AgentRunError("tenant-policy-denied", "allowCredentialEcho must be false", { httpStatus: 403 });
|
||||
const providerCredentials = Array.isArray(secretScope.providerCredentials) ? secretScope.providerCredentials : [];
|
||||
for (const credential of providerCredentials) {
|
||||
const rawProviderCredentials = Array.isArray(secretScope.providerCredentials) ? secretScope.providerCredentials : [];
|
||||
const providerCredentials: NonNullable<ExecutionPolicy["secretScope"]["providerCredentials"]> = [];
|
||||
for (const credential of rawProviderCredentials) {
|
||||
const item = asRecord(credential, "providerCredential");
|
||||
const profile = typeof item.profile === "string" ? item.profile.trim() : "";
|
||||
if (profile.length === 0) throw new AgentRunError("schema-invalid", "provider credential profile is required", { httpStatus: 400 });
|
||||
if (!isBackendProfile(profile)) throw new AgentRunError("schema-invalid", `provider credential profile ${profile} must be a lowercase slug`, { httpStatus: 400, details: { pattern: backendProfilePatternText } });
|
||||
const secretRef = asRecord(item.secretRef, "providerCredential.secretRef");
|
||||
if (typeof secretRef.name !== "string" || secretRef.name.length === 0) throw new AgentRunError("schema-invalid", "provider credential secretRef.name is required", { httpStatus: 400 });
|
||||
const keys = Array.isArray(secretRef.keys) ? secretRef.keys : [];
|
||||
for (const requiredKey of backendProfileSpec(profile)?.requiredSecretKeys ?? []) {
|
||||
if (!keys.includes(requiredKey)) throw new AgentRunError("schema-invalid", `provider credential ${profile} secretRef.keys must include ${requiredKey}`, { httpStatus: 400 });
|
||||
}
|
||||
const secretRef = validateSecretRef(asRecord(item.secretRef, "providerCredential.secretRef"));
|
||||
const keys = [...new Set([...(secretRef.keys ?? []), ...(backendProfileSpec(profile)?.requiredSecretKeys ?? [])])];
|
||||
providerCredentials.push({ profile, secretRef: { ...secretRef, ...(keys.length > 0 ? { keys } : {}) } });
|
||||
}
|
||||
const toolCredentials = validateToolCredentials(secretScope.toolCredentials);
|
||||
const secretScopeResult: ExecutionPolicy["secretScope"] = { allowCredentialEcho: false };
|
||||
|
||||
@@ -162,6 +162,16 @@ SET execution_policy = replace(replace(replace(execution_policy::text,
|
||||
WHERE execution_policy IS NOT NULL AND execution_policy::text LIKE '%ofcx-go%';
|
||||
DELETE FROM agentrun_backends WHERE profile = 'ofcx-go';
|
||||
INSERT INTO agentrun_backends (profile, capabilities, capacity, health, updated_at)
|
||||
VALUES ${backendCapabilitiesSqlValues(["dsflash-go"], { requiredSecretKeysByProfile: { "dsflash-go": ["auth.json", "config.toml"] } })}
|
||||
ON CONFLICT (profile) DO UPDATE SET
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
capacity = EXCLUDED.capacity,
|
||||
health = EXCLUDED.health,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
`;
|
||||
|
||||
const dsflashGoModelCatalogBackendProfileMigrationSql = `
|
||||
INSERT INTO agentrun_backends (profile, capabilities, capacity, health, updated_at)
|
||||
VALUES ${backendCapabilitiesSqlValues(["dsflash-go"])}
|
||||
ON CONFLICT (profile) DO UPDATE SET
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
@@ -360,6 +370,11 @@ const postgresMigrations: MigrationDefinition[] = [
|
||||
checksum: checksumSql(dsflashGoBackendProfileMigrationSql),
|
||||
sql: dsflashGoBackendProfileMigrationSql,
|
||||
},
|
||||
{
|
||||
id: "009_v01_dsflash_go_model_catalog",
|
||||
checksum: checksumSql(dsflashGoModelCatalogBackendProfileMigrationSql),
|
||||
sql: dsflashGoModelCatalogBackendProfileMigrationSql,
|
||||
},
|
||||
];
|
||||
|
||||
export function postgresMigrationContract(): JsonRecord {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import { spawn } from "node:child_process";
|
||||
import { AgentRunError } from "../common/errors.js";
|
||||
import { backendProfileSpec, backendProfileSpecs, isBackendProfileSlug } from "../common/backend-profiles.js";
|
||||
import { dsflashGoModelCatalogFile, dsflashGoModelCatalogJson, dsflashGoModelSlug, dsflashGoRuntimeModelCatalogPath } from "../common/model-catalogs.js";
|
||||
import type { AgentRunStore } from "./store.js";
|
||||
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue } from "../common/types.js";
|
||||
import { asRecord, validateBackendProfile } from "../common/validation.js";
|
||||
@@ -33,6 +34,7 @@ interface ProfileConfig {
|
||||
envKey: string;
|
||||
wireApi: "responses";
|
||||
displayName: string;
|
||||
modelCatalogPath?: string;
|
||||
}
|
||||
|
||||
interface RenderedConfig {
|
||||
@@ -110,13 +112,14 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
|
||||
const profile = validateBackendProfile(profileValue);
|
||||
const spec = requiredSpec(profile);
|
||||
const record = asRecord(body ?? {}, "providerProfileConfig");
|
||||
const configToml = configTomlField(record);
|
||||
const configToml = configTomlField(record, profile);
|
||||
const delegatedBy = delegatedBySummary(record.delegatedBy);
|
||||
const namespace = profileNamespace(options);
|
||||
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
|
||||
const existingData = asOptionalRecord(existingSecret?.data);
|
||||
const authJsonData = dataKey(existingData, "auth.json");
|
||||
const credentialHashSuffix = hashDataKey(existingData, "auth.json");
|
||||
const secretData = providerProfileSecretData({ profile, authJsonData, configToml, existingData });
|
||||
const secretManifest: JsonRecord = {
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
@@ -136,14 +139,14 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
|
||||
},
|
||||
},
|
||||
type: "Opaque",
|
||||
data: providerProfileSecretData({ authJsonData, configToml }),
|
||||
data: secretData,
|
||||
};
|
||||
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
|
||||
return {
|
||||
action: "provider-profile-config-updated",
|
||||
mutation: true,
|
||||
profile,
|
||||
configured: Boolean(authJsonData),
|
||||
configured: hasRequiredKeys(secretData, spec.requiredSecretKeys),
|
||||
secretRef: secretRefSummary(profile, namespace),
|
||||
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
|
||||
credentialHashSuffix: credentialHashSuffix ?? null,
|
||||
@@ -169,8 +172,11 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
||||
const record = asRecord(body ?? {}, "providerProfileCredential");
|
||||
const credential = credentialAuthJson(record);
|
||||
const delegatedBy = delegatedBySummary(record.delegatedBy);
|
||||
const renderedConfig = await renderedConfigForWrite(profile, record, options);
|
||||
const namespace = profileNamespace(options);
|
||||
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
|
||||
const existingData = asOptionalRecord(existingSecret?.data);
|
||||
const renderedConfig = await renderedConfigForWrite(profile, record, options);
|
||||
const secretData = providerProfileSecretData({ profile, authJsonData: base64Data(credential.authJson), configToml: renderedConfig.configToml, existingData });
|
||||
const secretManifest: JsonRecord = {
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
@@ -190,17 +196,14 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
||||
},
|
||||
},
|
||||
type: "Opaque",
|
||||
data: {
|
||||
"auth.json": base64Data(credential.authJson),
|
||||
"config.toml": base64Data(renderedConfig.configToml),
|
||||
},
|
||||
data: secretData,
|
||||
};
|
||||
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
|
||||
return {
|
||||
action: "provider-profile-credential-updated",
|
||||
mutation: true,
|
||||
profile,
|
||||
configured: true,
|
||||
configured: hasRequiredKeys(secretData, spec.requiredSecretKeys),
|
||||
secretRef: secretRefSummary(profile, namespace),
|
||||
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
|
||||
credentialHashSuffix: shortHash(credential.authJson),
|
||||
@@ -357,12 +360,18 @@ function profileFromSecretName(name: string | null): string | null {
|
||||
return profile.length > 0 ? profile : null;
|
||||
}
|
||||
|
||||
function providerProfileSecretData(input: { authJsonData?: string | null; configToml: string }): JsonRecord {
|
||||
function providerProfileSecretData(input: { profile: BackendProfile; authJsonData?: string | null; configToml: string; existingData?: JsonRecord | null }): JsonRecord {
|
||||
const data: JsonRecord = { "config.toml": base64Data(input.configToml) };
|
||||
if (input.authJsonData) data["auth.json"] = input.authJsonData;
|
||||
const modelCatalogData = input.profile === "dsflash-go" ? dataKey(input.existingData ?? null, dsflashGoModelCatalogFile) ?? defaultModelCatalogData(input.profile) : null;
|
||||
if (modelCatalogData) data[dsflashGoModelCatalogFile] = modelCatalogData;
|
||||
return data;
|
||||
}
|
||||
|
||||
function defaultModelCatalogData(profile: BackendProfile): string | null {
|
||||
return profile === "dsflash-go" ? base64Data(dsflashGoModelCatalogJson()) : null;
|
||||
}
|
||||
|
||||
function credentialAuthJson(record: JsonRecord): { authJson: string; source: "auth-json" | "api-key" } {
|
||||
const authJson = record.authJson;
|
||||
if (typeof authJson === "string" && authJson.trim().length > 0) return { authJson: authJsonField(authJson), source: "auth-json" };
|
||||
@@ -397,6 +406,7 @@ function renderConfigToml(config: ProfileConfig): string {
|
||||
"network_access = \"enabled\"",
|
||||
`model_context_window = ${contextWindow}`,
|
||||
`model_auto_compact_token_limit = ${autoCompactTokenLimit}`,
|
||||
...(config.modelCatalogPath ? [`model_catalog_json = ${tomlString(config.modelCatalogPath)}`] : []),
|
||||
"approvals_reviewer = \"user\"",
|
||||
"",
|
||||
`[model_providers.${config.providerName}]`,
|
||||
@@ -439,6 +449,7 @@ function renderedConfigFromProfileConfig(config: ProfileConfig, source: string):
|
||||
baseUrl: config.baseUrl,
|
||||
authField: config.envKey,
|
||||
wireApi: config.wireApi,
|
||||
modelCatalogJson: config.modelCatalogPath ?? null,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
@@ -461,7 +472,18 @@ async function existingConfigToml(profile: BackendProfile, options: ProviderProf
|
||||
function existingConfigAllowed(profile: BackendProfile, configToml: string): boolean {
|
||||
if (configToml.trim().length === 0) return false;
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && configToml.includes("hyueapi.com")) return false;
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && !configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local")) return false;
|
||||
if (profile === "deepseek" && !configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local")) return false;
|
||||
if (profile === "dsflash-go") {
|
||||
if (!configToml.includes(dsflashGoModelSlug)) return false;
|
||||
if (modelCatalogPathFromConfigToml(configToml) !== dsflashGoRuntimeModelCatalogPath) return false;
|
||||
const baseUrl = baseUrlFromConfigToml(configToml);
|
||||
if (!baseUrl) return false;
|
||||
try {
|
||||
if (!isAllowedDsflashBridgeUrl(new URL(baseUrl))) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -475,9 +497,10 @@ function configTomlFromData(data: JsonRecord | null, profile: BackendProfile): s
|
||||
}
|
||||
}
|
||||
|
||||
function configTomlField(record: JsonRecord): string {
|
||||
function configTomlField(record: JsonRecord, profile: BackendProfile): string {
|
||||
const value = record.configToml;
|
||||
if (typeof value !== "string" || value.trim().length === 0) throw new AgentRunError("schema-invalid", "configToml is required", { httpStatus: 400 });
|
||||
validateConfigTomlForProfile(profile, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -489,6 +512,7 @@ function configField(value: unknown, profile: BackendProfile): ProfileConfig {
|
||||
const baseUrl = optionalString(record.baseUrl) ?? defaults.baseUrl;
|
||||
const providerName = optionalString(record.providerName) ?? defaults.providerName;
|
||||
const envKey = optionalString(record.envKey) ?? defaults.envKey;
|
||||
if (profile === "dsflash-go" && model !== dsflashGoModelSlug) throw new AgentRunError("schema-invalid", `dsflash-go model must be ${dsflashGoModelSlug}`, { httpStatus: 400 });
|
||||
validateBaseUrl(profile, baseUrl);
|
||||
if (!/^[A-Z_][A-Z0-9_]{0,63}$/u.test(envKey)) throw new AgentRunError("schema-invalid", "config.envKey must be an uppercase env name", { httpStatus: 400 });
|
||||
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(providerName)) throw new AgentRunError("schema-invalid", "config.providerName must be a provider identifier", { httpStatus: 400 });
|
||||
@@ -513,7 +537,8 @@ function defaultConfig(profile: BackendProfile): ProfileConfig {
|
||||
baseUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1",
|
||||
envKey: "OPENAI_API_KEY",
|
||||
wireApi: "responses",
|
||||
displayName: "OpenCode",
|
||||
displayName: "Moon Bridge OpenCode Zen Go Flash",
|
||||
modelCatalogPath: dsflashGoRuntimeModelCatalogPath,
|
||||
};
|
||||
}
|
||||
if (profile === "minimax-m3") {
|
||||
@@ -537,7 +562,7 @@ function defaultConfig(profile: BackendProfile): ProfileConfig {
|
||||
}
|
||||
|
||||
function profileUsesMoonBridge(profile: BackendProfile, configToml: string): boolean {
|
||||
return profile === "deepseek" || profile === "dsflash-go" || configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local");
|
||||
return profile === "deepseek" || profile === "dsflash-go" || /hwlab-deepseek-proxy\.hwlab-v02\.svc\.cluster\.local|moon|bridge/iu.test(configToml);
|
||||
}
|
||||
|
||||
function validateBaseUrl(profile: BackendProfile, value: string): void {
|
||||
@@ -547,12 +572,50 @@ function validateBaseUrl(profile: BackendProfile, value: string): void {
|
||||
} catch {
|
||||
throw new AgentRunError("schema-invalid", "config.baseUrl must be a valid URL", { httpStatus: 400 });
|
||||
}
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && url.hostname === "hyueapi.com") {
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && isHyueHost(url.hostname)) {
|
||||
throw new AgentRunError("tenant-policy-denied", `${profile} profile must use HWLAB Moon Bridge, not hyueapi.com`, { httpStatus: 403 });
|
||||
}
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && url.hostname !== "hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local") {
|
||||
throw new AgentRunError("tenant-policy-denied", `${profile} profile baseUrl must point to HWLAB v0.2 Moon Bridge`, { httpStatus: 403 });
|
||||
if (profile === "deepseek" && url.hostname !== "hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local") {
|
||||
throw new AgentRunError("tenant-policy-denied", "deepseek profile baseUrl must point to HWLAB v0.2 DeepSeek bridge", { httpStatus: 403 });
|
||||
}
|
||||
if (profile === "dsflash-go" && !isAllowedDsflashBridgeUrl(url)) {
|
||||
throw new AgentRunError("tenant-policy-denied", "dsflash-go profile baseUrl must point to a Moon Bridge service or wrapper-local bridge", { httpStatus: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
function validateConfigTomlForProfile(profile: BackendProfile, configToml: string): void {
|
||||
if ((profile === "deepseek" || profile === "dsflash-go") && /hyueapi\.com/iu.test(configToml)) {
|
||||
throw new AgentRunError("tenant-policy-denied", `${profile} profile must use HWLAB Moon Bridge, not hyueapi.com`, { httpStatus: 403 });
|
||||
}
|
||||
if (profile === "deepseek" && !configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local")) {
|
||||
throw new AgentRunError("tenant-policy-denied", "deepseek profile config.toml must point to HWLAB v0.2 DeepSeek bridge", { httpStatus: 403 });
|
||||
}
|
||||
if (profile !== "dsflash-go") return;
|
||||
if (!configToml.includes(dsflashGoModelSlug)) throw new AgentRunError("schema-invalid", `dsflash-go config.toml must use model ${dsflashGoModelSlug}`, { httpStatus: 400 });
|
||||
if (modelCatalogPathFromConfigToml(configToml) !== dsflashGoRuntimeModelCatalogPath) throw new AgentRunError("schema-invalid", `dsflash-go config.toml must set model_catalog_json to ${dsflashGoRuntimeModelCatalogPath}`, { httpStatus: 400 });
|
||||
const baseUrl = baseUrlFromConfigToml(configToml);
|
||||
if (!baseUrl) throw new AgentRunError("schema-invalid", "dsflash-go config.toml must include model_providers base_url", { httpStatus: 400 });
|
||||
validateBaseUrl(profile, baseUrl);
|
||||
}
|
||||
|
||||
function isHyueHost(hostname: string): boolean {
|
||||
return hostname === "hyueapi.com" || hostname.endsWith(".hyueapi.com");
|
||||
}
|
||||
|
||||
function isAllowedDsflashBridgeUrl(url: URL): boolean {
|
||||
if (url.hostname === "127.0.0.1" || url.hostname === "localhost") return true;
|
||||
if (url.hostname === "hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local") return true;
|
||||
return url.hostname.endsWith(".svc.cluster.local") && /moon|bridge|deepseek|opencode/iu.test(url.hostname);
|
||||
}
|
||||
|
||||
function baseUrlFromConfigToml(configToml: string): string | null {
|
||||
const match = configToml.match(/^\s*base_url\s*=\s*"([^"]+)"\s*$/mu);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function modelCatalogPathFromConfigToml(configToml: string): string | null {
|
||||
const match = configToml.match(/^\s*model_catalog_json\s*=\s*"([^"]+)"\s*$/mu);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function contextWindowSettings(model: string): { contextWindow: number; autoCompactTokenLimit: number } {
|
||||
|
||||
@@ -267,13 +267,19 @@ function credentialProjections(run: RunRecord, namespace: string): CredentialPro
|
||||
const credentials = (policy.secretScope.providerCredentials ?? []).filter((item) => item.profile === run.backendProfile);
|
||||
return credentials.map((item, index) => ({
|
||||
profile: item.profile,
|
||||
secretRef: item.secretRef.namespace ? item.secretRef : { ...item.secretRef, namespace },
|
||||
secretRef: credentialSecretRef(item.profile, item.secretRef, namespace),
|
||||
volumeName: sanitizeVolumeName(`${String(item.profile)}-${index}`),
|
||||
runtimeMountPath: normalizeMountPath(item.secretRef.mountPath, String(item.profile)),
|
||||
projectionMountPath: `/var/run/agentrun/secrets/${sanitizeVolumeName(`${String(item.profile)}-${index}`)}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function credentialSecretRef(profile: string, secretRef: SecretRef, namespace: string): SecretRef {
|
||||
const spec = backendProfileSpec(profile);
|
||||
const keys = [...new Set([...(secretRef.keys ?? []), ...(spec?.requiredSecretKeys ?? [])])];
|
||||
return { ...secretRef, namespace: secretRef.namespace ?? namespace, ...(keys.length > 0 ? { keys } : {}) };
|
||||
}
|
||||
|
||||
function toolCredentialProjections(run: RunRecord, namespace: string): ToolCredentialProjection[] {
|
||||
const policy: ExecutionPolicy = run.executionPolicy;
|
||||
const credentials = policy.secretScope.toolCredentials ?? [];
|
||||
|
||||
@@ -13,9 +13,11 @@ const selfTest: SelfTestCase = async () => {
|
||||
(error) => error instanceof AgentRunError && error.failureKind === "infra-failed" && error.message.includes("DATABASE_URL is required"),
|
||||
);
|
||||
const postgresContract = postgresMigrationContract();
|
||||
assert.equal(postgresContract.latestMigrationId, "008_v01_dsflash_go_backend_profile");
|
||||
assert.equal(postgresContract.latestMigrationId, "009_v01_dsflash_go_model_catalog");
|
||||
assert.equal((postgresContract.migrationIds as string[]).includes("008_v01_dsflash_go_backend_profile"), true);
|
||||
assert.equal((postgresContract.migrationIds as string[]).includes("009_v01_dsflash_go_model_catalog"), true);
|
||||
assert.ok(typeof (postgresContract.checksums as Record<string, string>)["008_v01_dsflash_go_backend_profile"] === "string" && (postgresContract.checksums as Record<string, string>)["008_v01_dsflash_go_backend_profile"].length > 0);
|
||||
assert.ok(typeof (postgresContract.checksums as Record<string, string>)["009_v01_dsflash_go_model_catalog"] === "string" && (postgresContract.checksums as Record<string, string>)["009_v01_dsflash_go_model_catalog"].length > 0);
|
||||
assert.equal((postgresContract.checksums as Record<string, string>)["002_v01_backend_profiles"], "928b5c490cc4539cb64ecef34784557601b2724fa2870570f16a53576804e49c");
|
||||
assert.ok(Array.isArray(postgresContract.requiredTables));
|
||||
assert.ok(postgresContract.requiredTables.includes("agentrun_schema_migrations"));
|
||||
|
||||
@@ -96,11 +96,30 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
sourceCommit: "self-test",
|
||||
});
|
||||
assertRunnerJobUsesWritableCodexHome(dsflashGoRendered.manifest as JsonRecord, context.deepseekHome, "dsflash-go-0", "/var/run/agentrun/secrets/dsflash-go-0");
|
||||
assertRunnerJobSecretKeys(dsflashGoRendered, "dsflash-go", ["auth.json", "config.toml", "model-catalog.json"]);
|
||||
assertRunnerJobDoesNotMountProfile(dsflashGoRendered.manifest as JsonRecord, "codex-0");
|
||||
assertRunnerJobDoesNotMountProfile(dsflashGoRendered.manifest as JsonRecord, "deepseek-0");
|
||||
assertRunnerJobDoesNotMountProfile(dsflashGoRendered.manifest as JsonRecord, "minimax-m3-0");
|
||||
assertNoSecretLeak(dsflashGoRendered);
|
||||
|
||||
const legacyDsflashRun = await client.post("/api/v1/runs", {
|
||||
tenantId: "unidesk",
|
||||
projectId: "pikasTech/unidesk",
|
||||
workspaceRef: { kind: "host-path", path: context.workspace },
|
||||
providerId: "G14",
|
||||
backendProfile: "dsflash-go",
|
||||
executionPolicy: {
|
||||
sandbox: "workspace-write",
|
||||
approval: "never",
|
||||
timeoutMs: 15_000,
|
||||
network: "default",
|
||||
secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile: "dsflash-go", secretRef: { name: "agentrun-v01-provider-dsflash-go", keys: ["auth.json", "config.toml"], mountPath: context.deepseekHome } }] },
|
||||
},
|
||||
traceSink: null,
|
||||
}) as RunRecord;
|
||||
const legacyDsflashCredential = legacyDsflashRun.executionPolicy.secretScope.providerCredentials?.[0];
|
||||
assert.deepEqual(legacyDsflashCredential?.secretRef.keys, ["auth.json", "config.toml", "model-catalog.json"]);
|
||||
|
||||
const fakeKubectl = path.join(context.tmp, "fake-kubectl.js");
|
||||
const createdManifest = path.join(context.tmp, "created-runner-job.json");
|
||||
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
||||
@@ -208,7 +227,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01");
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/sessions");
|
||||
assert.equal(envMap.get("AGENTRUN_CODEX_ROLLOUT_SUBDIR"), "sessions");
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
@@ -278,6 +297,13 @@ function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCode
|
||||
assert.notEqual(value("CODEX_HOME"), value("AGENTRUN_CODEX_SECRET_HOME"));
|
||||
}
|
||||
|
||||
function assertRunnerJobSecretKeys(rendered: JsonRecord, profile: string, expectedKeys: string[]): void {
|
||||
const refs = rendered.secretRefs as JsonRecord[];
|
||||
const ref = refs.find((item) => item.profile === profile);
|
||||
assert.ok(ref, `${profile} SecretRef summary must be present`);
|
||||
assert.deepEqual(ref.keys, expectedKeys);
|
||||
}
|
||||
|
||||
function assertRunnerJobDoesNotMountProfile(manifest: JsonRecord, volumeName: string): void {
|
||||
const spec = manifest.spec as JsonRecord;
|
||||
const template = spec.template as JsonRecord;
|
||||
|
||||
@@ -62,8 +62,10 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal(dsflashGoResult.terminalStatus, "completed");
|
||||
await access(path.join(dsflashGoHome, "auth.json"));
|
||||
await access(path.join(dsflashGoHome, "config.toml"));
|
||||
await access(path.join(dsflashGoHome, "model-catalog.json"));
|
||||
const dsflashGoEvents = await client.get(`/api/v1/runs/${dsflashGo.runId}/events?afterSeq=0&limit=100`) as { items?: Array<{ type: string; payload: unknown }> };
|
||||
assert.ok(dsflashGoEvents.items?.some((event) => event.type === "backend_status" && JSON.stringify(event.payload).includes("dsflash-go")), "dsflash-go backend_status should include profile metadata");
|
||||
assert.ok(dsflashGoEvents.items?.some((event) => event.type === "backend_status" && JSON.stringify(event.payload).includes("\"contextWindow\":1000000")), "dsflash-go backend_status should include 1M context metadata");
|
||||
assertNoSecretLeak(dsflashGoEvents);
|
||||
|
||||
await assert.rejects(
|
||||
@@ -213,6 +215,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-401-rpc-error", expectedStatus: "failed", expectedFailureKind: "provider-auth-failed" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-429-terminal", expectedStatus: "failed", expectedFailureKind: "provider-rate-limited" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-invalid-tool-call", expectedStatus: "failed", expectedFailureKind: "provider-invalid-tool-call" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-compact-404-terminal", expectedStatus: "failed", expectedFailureKind: "provider-compact-unsupported" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-503-rpc-error", expectedStatus: "failed", expectedFailureKind: "provider-unavailable" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-503-terminal", expectedStatus: "failed", expectedFailureKind: "provider-unavailable" });
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "provider-503-retry-event", expectedStatus: "failed", expectedFailureKind: "provider-unavailable", expectRetryError: true });
|
||||
@@ -227,7 +230,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
await runSessionStorageSubdirCase({ client, managerUrl: server.baseUrl, context });
|
||||
await runSessionStorageNoSecretLeakCase({ client, managerUrl: server.baseUrl, context });
|
||||
|
||||
return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "runner-lease-conflict-recovery", "codex-stdio-fake-turn", "codex-stdio-projected-writable-home", "codex-stdio-deepseek-profile-fake-turn", "codex-stdio-dsflash-go-profile-fake-turn", "codex-stdio-minimax-m3-profile-fake-turn", "codex-stdio-deepseek-missing-secret-no-fallback", "codex-stdio-minimax-m3-missing-secret-no-fallback", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-final-agent-message-only", "codex-stdio-web-search-progress", "codex-stdio-stale-thread-resume-failed", "codex-stdio-live-tool-events", "codex-stdio-noisy-reasoning-suppression", "codex-stdio-missing-turn-result", "codex-stdio-provider-auth-failed", "codex-stdio-provider-rate-limited", "codex-stdio-provider-invalid-tool-call", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-idle-timeout-progress-refresh", "codex-stdio-command-failure-keeps-run-open", "codex-stdio-secret-unavailable", "codex-stdio-spawn-failure"] };
|
||||
return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "runner-lease-conflict-recovery", "codex-stdio-fake-turn", "codex-stdio-projected-writable-home", "codex-stdio-deepseek-profile-fake-turn", "codex-stdio-dsflash-go-profile-fake-turn", "codex-stdio-dsflash-go-config-metadata", "codex-stdio-minimax-m3-profile-fake-turn", "codex-stdio-deepseek-missing-secret-no-fallback", "codex-stdio-minimax-m3-missing-secret-no-fallback", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-final-agent-message-only", "codex-stdio-web-search-progress", "codex-stdio-stale-thread-resume-failed", "codex-stdio-live-tool-events", "codex-stdio-noisy-reasoning-suppression", "codex-stdio-missing-turn-result", "codex-stdio-provider-auth-failed", "codex-stdio-provider-rate-limited", "codex-stdio-provider-invalid-tool-call", "codex-stdio-provider-compact-unsupported", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-idle-timeout-progress-refresh", "codex-stdio-command-failure-keeps-run-open", "codex-stdio-secret-unavailable", "codex-stdio-spawn-failure"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
const dsflashGoSecretPlan = await renderCodexProviderSecretPlan({ profile: "dsflash-go", codexHome: context.deepseekHome, dryRun: true });
|
||||
assert.equal(dsflashGoSecretPlan.secretName, "agentrun-v01-provider-dsflash-go");
|
||||
assert.equal(dsflashGoSecretPlan.profile, "dsflash-go");
|
||||
assert.deepEqual(dsflashGoSecretPlan.keys, ["auth.json", "config.toml", "model-catalog.json"]);
|
||||
assert.equal(JSON.stringify(dsflashGoSecretPlan).includes("test-token-material-deepseek"), false);
|
||||
assert.equal(JSON.stringify(dsflashGoSecretPlan).includes("deepseek-test"), false);
|
||||
assert.equal(JSON.stringify(dsflashGoSecretPlan).includes("deepseek-v4-flash"), false);
|
||||
|
||||
await assert.rejects(
|
||||
() => renderCodexProviderSecretPlan({ codexHome: path.join(context.tmp, "missing-codex-home"), dryRun: true }),
|
||||
|
||||
@@ -159,7 +159,7 @@ process.exit(1);
|
||||
assert.equal(config.configTomlPrinted, true);
|
||||
assert.equal(JSON.stringify(config).includes("redacted-fixture"), false);
|
||||
|
||||
const updatedConfigToml = "model = \"fixture-updated\"\n";
|
||||
const updatedConfigToml = "model = \"fixture-updated\"\nbase_url = \"http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1\"\n";
|
||||
const updatedConfig = await client.put("/api/v1/provider-profiles/deepseek/config", {
|
||||
configToml: updatedConfigToml,
|
||||
delegatedBy: { system: "hwlab-v02", userId: "u1", username: "tester", requestId: "req-config-selftest" },
|
||||
@@ -243,10 +243,14 @@ process.exit(1);
|
||||
const createdData = createdSecretManifest.data as JsonRecord;
|
||||
const createdAuthJson = Buffer.from(String(createdData["auth.json"]), "base64").toString("utf8");
|
||||
const createdConfigToml = Buffer.from(String(createdData["config.toml"]), "base64").toString("utf8");
|
||||
const createdModelCatalog = Buffer.from(String(createdData["model-catalog.json"]), "base64").toString("utf8");
|
||||
assert.equal(createdAuthJson.includes(secretText), true);
|
||||
assert.equal(createdAuthJson.includes("OPENAI_API_KEY"), true);
|
||||
assert.equal(createdConfigToml.includes("deepseek-v4-flash"), true);
|
||||
assert.equal(createdConfigToml.includes("model_catalog_json = \"/home/agentrun/.codex-dsflash-go/model-catalog.json\""), true);
|
||||
assert.equal(createdConfigToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local"), true);
|
||||
assert.equal(JSON.parse(createdModelCatalog).models[0].context_window, 1000000);
|
||||
assert.equal(JSON.parse(createdModelCatalog).models[0].auto_compact_token_limit, 900000);
|
||||
const dsflashShown = await client.get("/api/v1/provider-profiles/dsflash-go") as JsonRecord;
|
||||
assert.equal(dsflashShown.configured, true);
|
||||
assert.equal(dsflashShown.failureKind, null);
|
||||
@@ -341,7 +345,7 @@ process.exit(1);
|
||||
assert.equal(finalValidation.status, "completed");
|
||||
assert.equal(JSON.stringify(finalValidation).includes(secretText), false);
|
||||
assertNoSecretLeak(finalValidation);
|
||||
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-set-auth-json-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dynamic-slug-roundtrip", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
||||
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-set-auth-json-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dsflash-go-model-catalog", "provider-profile-dynamic-slug-roundtrip", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -128,6 +128,20 @@ for await (const line of rl) {
|
||||
respond(message.id, { turn });
|
||||
continue;
|
||||
}
|
||||
if (mode === "provider-compact-404-terminal") {
|
||||
turnCounter += 1;
|
||||
const turn = {
|
||||
id: `turn_selftest_${turnCounter}`,
|
||||
status: "failed",
|
||||
error: {
|
||||
message: "Error running remote compact task: unexpected status 404 Not Found: 404 page not found, url: http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1/responses/compact",
|
||||
},
|
||||
};
|
||||
notify("turn/started", { turn: { id: turn.id, status: "running" } });
|
||||
notify("turn/completed", { turn });
|
||||
respond(message.id, { turn });
|
||||
continue;
|
||||
}
|
||||
if (mode === "provider-503-retry-event") {
|
||||
turnCounter += 1;
|
||||
const turn = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import assert from "node:assert/strict";
|
||||
import { ManagerClient } from "../mgr/client.js";
|
||||
import type { BackendProfile, JsonRecord } from "../common/types.js";
|
||||
import { backendProfileSpec } from "../common/backend-profiles.js";
|
||||
import { dsflashGoModelCatalogJson } from "../common/model-catalogs.js";
|
||||
|
||||
export interface SelfTestContext {
|
||||
root: string;
|
||||
@@ -41,7 +42,8 @@ export async function createSelfTestContext(root: string): Promise<SelfTestConte
|
||||
await writeFile(path.join(codexHome, "auth.json"), JSON.stringify({ token: "test-token-material" }));
|
||||
await writeFile(path.join(codexHome, "config.toml"), "model = \"gpt-test\"\n");
|
||||
await writeFile(path.join(deepseekHome, "auth.json"), JSON.stringify({ token: "test-token-material-deepseek" }));
|
||||
await writeFile(path.join(deepseekHome, "config.toml"), "model = \"deepseek-test\"\n");
|
||||
await writeFile(path.join(deepseekHome, "config.toml"), "model_provider = \"opencode\"\nmodel = \"deepseek-v4-flash\"\nreview_model = \"deepseek-v4-flash\"\nmodel_context_window = 1000000\nmodel_auto_compact_token_limit = 900000\nmodel_catalog_json = \"model-catalog.json\"\n[model_providers.opencode]\nname = \"OpenCode\"\nbase_url = \"http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n");
|
||||
await writeFile(path.join(deepseekHome, "model-catalog.json"), dsflashGoModelCatalogJson());
|
||||
await writeFile(path.join(minimaxM3Home, "auth.json"), JSON.stringify({ token: "test-token-material-minimax-m3" }));
|
||||
await writeFile(path.join(minimaxM3Home, "config.toml"), "model = \"MiniMax-M3\"\nmodel_provider = \"minimax\"\n[model_providers.minimax]\nname = \"MiniMax\"\nbase_url = \"https://api.minimaxi.com/v1\"\nenv_key = \"MINIMAX_API_KEY\"\nwire_api = \"responses\"\n");
|
||||
await writeFile(path.join(workspace, "README.md"), "self-test workspace\n");
|
||||
@@ -93,7 +95,7 @@ function providerCredentials(context: Pick<SelfTestContext, "codexHome"> & Parti
|
||||
profile,
|
||||
secretRef: {
|
||||
name: backendProfileSpec(profile)?.defaultSecretName ?? `agentrun-v01-provider-${profile}`,
|
||||
keys: ["auth.json", "config.toml"],
|
||||
keys: [...(backendProfileSpec(profile)?.requiredSecretKeys ?? ["auth.json", "config.toml"])],
|
||||
mountPath: profileSecretHome(context, profile),
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user