Files
next-tjwater-agent/README.md
T
jiang 7383ed6d13 feat(agent): migrate OpenCode 2 runtime and sandbox tools
Upgrade the runtime and native plugin API to OpenCode 2.0.24. Restore
session-scoped learning, credential renewal, typed project queries and
controlled frontend actions from the legacy Agent.

Replace host Shell execution with an approved Landlock/seccomp executor
and persistent per-session workspaces. Add typed Bocha search and Tianditu
geocoding bridges using the owning session credential. Require a working
kernel sandbox before startup and persist workspace files in deployments.

Validation completed before this commit: TypeScript checks, 150 Bun tests,
Docker build and UID 10001 sandbox execution, native Shell approval and
session recovery. Tianditu succeeded; Bocha currently reports exhausted
account quota. Unrelated investigation-report contract work is excluded.
2026-10-08 15:46:37 +08:00

169 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TJWater 供水 Agent 实验服务
基于 OpenCode 的实验性供水 Agent 服务,用于编排 TJWater 供水管网前端交互。排水数据、排水分析流程和排水前端交互不在本仓库范围内。
本仓库与生产环境中的 `TJWaterAgent` 历史代码相互独立。浏览器身份和项目范围由 TJWater Server 验证,业务访问使用类型化工具。
## 当前边界
- 生产请求必须携带 `Authorization: Bearer ...` 和 `X-Project-Id`,Server 验证身份及项目权限。开发环境允许无凭据的本地对话,但不能访问 Server 业务工具。
- 不包含 `cli/`、`tjwater_cli` OpenCode 工具或自由形式的 CLI 命令桥接。
- Agent 的结构化界面输出使用 `agent-ui@1` UIEnvelope 事件;受控浏览器操作使用 `frontend-action@1` 前端动作。
- Server 访问通过有限类型化工具;`web_search` 和 `geocode` 使用现有 Server 搜索、地理编码接口。Shell 仅在经过审批的会话 OS 沙箱中执行本地文件计算。
## 主要 API
```text
GET /health
GET /api/v1/agent/chat/models
GET /api/v1/agent/chat/ui-registry
GET /api/v1/agent/chat/frontend-action-registry
POST /api/v1/agent/chat/frontend-actions/:action_id/result
POST /api/v1/agent/chat/session
POST /api/v1/agent/chat/task-executions
GET /api/v1/agent/chat/sessions
GET /api/v1/agent/chat/session/:session_id
GET /api/v1/agent/chat/session/:session_id/stream
POST /api/v1/agent/chat/stream
POST /api/v1/agent/chat/abort
```
工况 worker 通过 `POST /api/v1/agent/chat/task-executions` 提交 Server 已持久化的 `task_id`、`run_id` 和 `execution_request_id`。三者缺一即拒绝;Agent 不从进程内 Map 推断当前执行代次。Agent claim 成功后每 20 秒对该 run 心跳,正常流结束显式调用 `complete`,失败或中止显式调用 `fail`。计划、报告和完成命令均携带同一 run 身份,Server 是活动 run 与租约状态的唯一权威。
聊天 UIMessage 流与工作台业务事件流相互独立;业务状态由 Server 的 `/api/v1/workbench/events` 推送,不能混入 token SSE。
`POST /api/v1/agent/chat/stream` 通过 SSE 返回事件流。主要事件如下:
| 事件 | 用途 |
| --- | --- |
| `progress` | Agent 规划和工具执行进度 |
| `token` | 助手回复的增量文本 |
| `ui_envelope` | 供可信前端渲染的结构化界面描述 |
| `frontend_action` | 请求可信前端执行受控动作 |
| `tool_call` | 用于兼容和调试的旧版工具调用事件 |
| `permission_request` | 运行时权限请求 |
| `question_request` | 运行时向用户发起的澄清请求 |
| `todo_update` | Agent 待办事项更新 |
| `session_title` | 会话标题更新 |
| `done` | 本次运行完成 |
| `error` | 本次运行失败或被中止 |
## 本地上下文
服务通过 `src/context/localContext.ts` 提供本地上下文,默认值如下:
```text
AGENT_LOCAL_USER_ID=local-user
AGENT_LOCAL_PROJECT_ID=local-project
AGENT_LOCAL_NETWORK=local-network
```
供水业务数据只来自用户输入、经过校验的事件上下文或已启用的受控工具。`web_search`、`geocode` 和 `query_project_data` 已开放,通过当前会话的可信凭据访问 Server;不接受模型指定的 URL、鉴权头或项目范围。`get_project_context`、`list_scada_assets` 和 `query_scada_readings` 仍未开放。
实验期间可以通过以下请求头覆盖本地上下文:
```text
X-Agent-Local-User
X-Agent-Local-Project
X-Agent-Local-Network
X-Trace-Id
```
## UIEnvelope 与前端动作
UIEnvelope 注册表通过以下接口提供:
```text
GET /api/v1/agent/chat/ui-registry
```
该接口用于兼容 `agent-ui@1` 协议。目前没有注册图表语法、界面组件或 UIEnvelope 动作。
前端动作使用独立的注册表:
```text
GET /api/v1/agent/chat/frontend-action-registry
```
当前提供 `render_scada_analysis` 和 `clear_scada_analysis` 两个地图叠加层动作。浏览器需要在流式请求中声明 `frontend-action@1` 能力,执行动作后再通过以下接口回传结果:
```text
POST /api/v1/agent/chat/frontend-actions/:action_id/result
```
协议详情参见 [Agent UI 协议](docs/agent-ui-protocol.md)。
## 本地开发
```bash
bun install
bun run dev
bun run check
bun test tests/**/*.test.ts
```
`bun run check` 会检查服务代码和 `.opencode` 工具的类型。
工况任务工具通过本机内部桥访问 Agent,再由 Agent 使用当前请求的短期产品凭据调用 TJWater Server。`embedded` 模式会把启动时生成的内部密钥传给子进程;`client` 模式必须在 Agent 与外部 OpenCode 服务中配置相同的 `AGENT_INTERNAL_TOKEN` 后分别重启。缺少共享密钥时 Agent 会拒绝启动,工具桥也不会退化为仅凭 session ID 访问。
独立管理 Agent 与 OpenCode 时必须显式配置稳定的 `AGENT_INTERNAL_TOKEN`;如果 `bun --watch` 使用自动生成的密钥重启 Agent,仍在运行的 OpenCode 子进程会保留旧密钥,内部工具桥将拒绝请求。需要保留现有开发服务做隔离联调时,可运行 `bun run start:integration:isolated`。它默认使用 Agent `18787`、OpenCode `14096`,从当前仓库目录加载 `opencode.json`、`.opencode/` 与现有 provider 环境,并把 Agent Session、memory、result ref、Skill 及 OpenCode XDG 运行数据写入 `data/integration-18787/`,不会复用默认运行实例的数据目录。可用 `INTEGRATION_AGENT_PORT`、`INTEGRATION_OPENCODE_PORT`、`INTEGRATION_DATA_DIR` 覆盖这些隔离值;启动器不会输出内部密钥。
## CI/CD
Gitea Actions 通过 `.gitea/workflows/package.yml` 复用
`OrgTJWater/ci-templates` 的容器发布流程。手动触发只构建镜像并在 Dev
服务器运行候选验证;从 `main` 创建并推送 `v*` tag 后,候选验证通过才会
提升为正式 `agent` 服务。
仓库需要配置 `REGISTRY_USERNAME`、`REGISTRY_PASSWORD` 和
`DEV_DEPLOY_SSH_KEY` 三个 Gitea Secrets。`.env`、持久化数据和日志不会写入镜像;它们由 Dev 服务器通过环境文件和卷挂载。
## OpenCode 2 接入
运行时使用 `@opencode/client@2.0.24`,私有进程默认启动 `opencode2 serve`。
Ubuntu 安装:`bun add -g --trust @opencode/cli@2.0.24`,然后检查 `opencode2 --version`。
`OPENCODE_COMMAND` 可指定绝对路径。Client 模式除共享 `AGENT_INTERNAL_TOKEN` 外,还需
用 `OPENCODE_CLIENT_PASSWORD` 提供外部 V2 Server 的密码;嵌入模式自动捕获密码,不写日志。
业务工具由 `.opencode/plugins/tjwater-tools.ts` 的 V2 插件显式注册,直接提供给模型,
保留业务输入验证及前端动作的原生 call ID。启动前检查版本、模型、工具完整性;缺失即失败。
`@opencode-ai/sdk` 仅为现有产品 SSE 契约提供开发期类型,不参与 V2 请求或进程启动。
工具变化:`bash` → `shell`,`task` → `subagent`,`apply_patch` → `patch`。
V2 内置目录不含 `todo`、`todoread`、`todowrite`;本项目插件恢复会话内持久化的
`todoread`、`todowrite`,并将更新继续转换为 `todo_update`。问答改用 Session Form API,
权限回复使用 session ID;文本、推理、工具与错误事件在运行时转换为现有前端 SSE。
已移除 V2 忽略的 `server` 和 `experimental.continue_loop_on_deny` 配置,权限转换为
有序 `permissions` 规则,保留敏感路径静态拒绝、shell/edit 审批和禁用子代理。
官方依据:[迁移指南](https://opencode.ai/v2/docs/migrate-v1/)、
[插件](https://opencode.ai/v2/docs/build/plugins/)、[工具](https://opencode.ai/v2/docs/tools/)、
[客户端](https://opencode.ai/v2/docs/build/client/)。旧 `dev.opencode.ai` beta 文档及
`@opencode-ai/*@next` 标签不可作为当前稳定版安装依据。
## 旧 Agent 的 P0/P1 能力迁移
- `memory_manager` 和 `skill_manager` 经内部令牌保护的 HTTP 桥接调用主服务,共用主服务的会话上下文和存储。身份与项目范围不由模型传入;新增记忆仍须先读取同 scope。
- 交互会话收到 `credential_refresh_required` 后,前端续期并向 `POST /api/v1/agent/chat/session/:session_id/credentials` 提交 `request_id`;令牌只在认证 header 中传递,不写入会话文件。工具请求最多重试一次,403 不触发续期。续期超时或新凭据再次被拒绝时停止本次执行。
- 后台 worker 使用已有 OAuth provider,依据 Server 的 claimed/running 运行快照向上述接口更新凭据,body 为 `{}`。后台运行不等待浏览器。若凭据已失效而无法写回失败,Server 的租约恢复仍负责终结失效运行。
- 普通会话通过 `query_project_data` 读取有限数据源:供水元素属性、SCADA 设备与历史、元素历史、分析运行及分页结果。历史窗口最长24小时;历史查询接口按 scada.view 只读权限校验,数据清洗仍需 scada.clean;SCADA 字段只接受 `monitored_value/cleaned_value`,物理指标和单位以设备元数据为准。任务会话仍使用任务专属查询工具。
- `locate_features`、`zoom_to_map`、`show_chart`、`view_history` 使用 `frontend-action@1`,等待实际浏览器回执。地图缺失资产返回 missing_ids;历史缺数不生成假曲线。图表仅接收有限、对齐的数值序列。历史展示当前最多100个非空样本,不代表完整高频历史。
- 业务阶段以 progress 快照保存,保留并行操作和错误;任务中的分支、等待和证据仍由 execution-plan@2 表达。`final_answer` 用于最终答复呈现,在运行成功结束后发布;不强制结束模型循环,也不代表工单或业务任务完成。任务运行仍由 Server 的 complete/fail 接口确认。
Shell 已迁移为 Landlock + seccomp 内核沙箱,替换 OpenCode 2 自带的 `shell` 工具;其他宿主 Shell 创建入口关闭。Code Mode 嵌套调用也不能恢复原执行器。此边界覆盖 Shell 与子进程,OpenCode 服务和类型化工具仍在可信服务进程运行。参考 [OpenCode V2 插件接口](https://opencode.ai/v2/docs/build/plugins/) 和 [权限文档](https://opencode.ai/v2/docs/permissions/)。
- Ubuntu/Linux 必须支持 Landlock ABI >= 4,并安装 Python 3、`libseccomp2`。当前 WSL Ubuntu 实测 ABI 7。启动时实际执行一次限制网络的探测;失败则不监听 HTTP,不回退到宿主 Shell。`/health` 的 `sandbox` 显示探测结果。Docker 已加入所需库和执行脚本,宿主内核也必须支持 Landlock。
- `CONVERSATION_WORKSPACE_ROOT` 默认 `./.agent-workspaces`,与受保护的 `data/`、`logs/` 分开。每个会话使用唯一 `conversation-*` 目录,Native Session Location 保存绑定;恢复后继续使用原目录,旧的 checkout 会话在继续执行前迁入新目录。新分支使用独立空目录,不复制源码、凭据或其他会话文件。
- 容器部署时须将 `/app/.agent-workspaces` 挂载到持久卷,并保留 OpenCode 的会话数据库卷。仅挂载 `/app/data` 不能保留新会话文件,例如 Compose 增加 `./agent-workspaces:/app/.agent-workspaces`。此次迁移未修改本地忽略的 Compose 配置或远端部署。
- 根目录只读,可写文件放在 `files/`、`tmp/`、`.cache/`,避免模型生成的插件或配置被 OpenCode 自动加载。原生文件工具也检查规范路径、符号链接和会话范围,保留 `.env`、`data`、`logs` 静态拒绝与编辑审批。
- root 容器中的 Shell 降为 UID/GID 10001;普通 Ubuntu 用户运行时保留本身 UID,由 Landlock/seccomp 提供限制。环境变量只保留明确的基础运行白名单,模型、用户和内部服务凭据不进入沙箱。系统程序、Python 环境和 Skill 目录只读,网络 socket、进程检查/发信号及脱离进程组被拒绝。
- Shell 的 timeout 单位是秒,默认 120,最大 1800。stdout 默认限 1 MiB(`SANDBOX_MAX_OUTPUT_BYTES`),stderr 最多 256 KiB,单文件写入最多 128 MiB。较长 stdout/stderr 保存在本会话 files/,响应返回 3000 字节预览和 stdout_file/stderr_file 路径,避免落到无法读取的 OpenCode 全局截断文件。超时、取消、输出超限及命令结束均清理该进程组,不提供持久后台进程。会话文件保留在配置根目录,当前未增加自动文件保留期限或附件下载接口。
- Shell 和文件修改即使在 `approval_mode=always` 也会请求用户确认;常规业务工具沿用自动批准。配置中的 deny 继续生效。
`web_search` 对接 `POST /api/v1/web-searches`(Bocha),支持 query、五种 freshness、summary、1–50 条结果及域名 include/exclude。`geocode` 对接 `POST /api/v1/geocoding-requests`(天地图),校验成功状态和坐标范围,并返回 `center=[lon,lat]`、`source_crs=EPSG:4326`。坐标可直接用于 `zoom_to_map`。两个工具使用现有凭据续期机制,403 不触发续期;服务密钥只配置在 Server 的 `BOCHA_API_KEY` 和 `TIANDITU_GEOCODER_TOKEN` 中。
client 模式的外部 OpenCode 需要使用 `serviceOpencodeConfig` 加载可信插件包 `.opencode/tjwater-plugin/` 和 Agent 指令,并设置与 Agent 相同的绝对 `TJWATER_AGENT_WORKSPACE_ROOT`(等于 `CONVERSATION_WORKSPACE_ROOT`)、内部桥地址和密钥。隔离联调启动器已完成这些设置。2.0.24 的显式插件配置要求目录包;冷 Location 的模型发现必须等插件初始化完成。
P2 的大结果文件中转、供水瓶颈和服务区算法未迁入。