Reuse OpenCode 2 HTTP/stdio clients with per-session read-only grants, native approval and sanitized owner-scoped MCP status. Narrow the environment inherited by trusted local MCP processes. Validated in an isolated checkout with bun run check and 20 focused tests.
18 KiB
TJWater 供水 Agent 实验服务
基于 OpenCode 的实验性供水 Agent 服务,用于编排 TJWater 供水管网前端交互。排水数据、排水分析流程和排水前端交互不在本仓库范围内。
本仓库与生产环境中的 TJWaterAgent 历史代码相互独立。浏览器身份和项目范围由 TJWater Server 验证,业务访问使用类型化工具。
当前边界
- 生产请求必须携带
Authorization: Bearer ...和X-Project-Id,Server 验证身份及项目权限。开发环境允许无凭据的本地对话,但不能访问 Server 业务工具。 - 不包含
cli/、tjwater_cliOpenCode 工具或自由形式的 CLI 命令桥接。 - Agent 的结构化界面输出使用
agent-ui@1UIEnvelope 事件;受控浏览器操作使用frontend-action@1前端动作。 - Server 访问通过有限类型化工具;
web_search和geocode使用现有 Server 搜索、地理编码接口。Shell 仅在经过审批的会话 OS 沙箱中执行本地文件计算。
主要 API
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/mcp
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 提供本地上下文,默认值如下:
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 仍未开放。
实验期间可以通过以下请求头覆盖本地上下文:
X-Agent-Local-User
X-Agent-Local-Project
X-Agent-Local-Network
X-Trace-Id
UIEnvelope 与前端动作
UIEnvelope 注册表通过以下接口提供:
GET /api/v1/agent/chat/ui-registry
该接口用于兼容 agent-ui@1 协议。目前没有注册图表语法、界面组件或 UIEnvelope 动作。
前端动作使用独立的注册表:
GET /api/v1/agent/chat/frontend-action-registry
当前提供 render_scada_analysis 和 clear_scada_analysis 两个地图叠加层动作。浏览器需要在流式请求中声明 frontend-action@1 能力,执行动作后再通过以下接口回传结果:
POST /api/v1/agent/chat/frontend-actions/:action_id/result
协议详情参见 Agent UI 协议。
本地开发
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 审批和禁用子代理。
官方依据:迁移指南、
插件、工具、
客户端。旧 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 插件接口 和 权限文档。
- 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 的大结果文件中转、供水瓶颈和服务区算法未迁入。
外部 MCP 客户端
供水 Agent 复用 OpenCode 2.0.24 的 MCP 客户端,支持远程 Streamable HTTP、管理员配置的本地 stdio、原生 OAuth、工具目录更新和取消调用。当前 MCP 正式协议为 2026-07-28,配置依据见 OpenCode MCP 文档。本次接入不发布 TJWater MCP 服务,也不增加 MCP Apps 或 Tasks 的前端界面。
仓库中的 mcp.servers 默认是 {}。管理员在服务的 opencode.json 或 OPENCODE_CONFIG_CONTENT 中配置连接;模型和浏览器不能提交 MCP 地址、进程命令或认证头。所有服务名必须使用 mcp_ 前缀,其余字符限英文字母、数字、下划线和连字符。下面是配置片段,示例默认禁用;替换为实际地址/程序后再启用:
{
"mcp": {
"timeout": { "startup": 10000, "catalog": 10000, "execution": 60000 },
"servers": {
"mcp_docs": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
"protocol": "auto",
"oauth": false,
"headers": { "Authorization": "Bearer {env:MCP_DOCS_TOKEN}" },
"disabled": true
},
"mcp_company": {
"type": "local",
"command": ["node", "/opt/company-mcp/server.js"],
"cwd": "/opt/company-mcp",
"environment": { "MCP_API_KEY": "{env:COMPANY_MCP_API_KEY}" },
"protocol": "legacy",
"disabled": true
}
}
}
}
远程默认 protocol: "auto",探测新协议并兼容旧协议;本地默认 legacy,避免对旧 stdio 程序额外启动探测进程。只服务新协议时可显式设为 "2026-07-28"。codemode 沿用原生默认值 true,设为 false 可直接暴露工具;两种入口执行相同的会话策略。超时单位为毫秒,全局默认启动/目录各 10 秒、执行 60 秒,已有显式全局值和每个服务的覆盖值优先。
只有管理员核实为只读的工具才自动放行:在已有 permissions 数组的 mcp_* 审批规则后追加精确工具名的 allow,保留原有敏感路径拒绝规则。例如:
{ "action": "mcp_docs_search", "resource": "*", "effect": "allow" }
工具的有效名称为 <server>_<tool>,不支持的字符由 OpenCode 规范为 _,以状态接口实际返回的名称为准。宽泛的 mcp_* allow、服务自报的 readOnlyHint、历史“始终允许”都不构成只读白名单。后续匹配的拒绝规则继续生效。普通聊天和任务协作中,白名单外调用进入现有 permission_request 审批,即使 approval_mode=always 也不会自动批准;后台 task_execution 只能调用白名单工具,学习复盘 review 禁止 MCP 调用,不等待浏览器审批。原生通用 MCP 资源列举/读取入口也受限制:交互会话审批,后台和复盘会话禁止,不能绕过逐个工具的白名单。外部结果是检索资料,不能直接替代 Server 已保存的业务证据或改变调用权限。
GET /api/v1/agent/chat/session/:session_id/mcp 沿用聊天接口的 Bearer、X-Project-Id 与会话归属校验。它返回 servers,每项仅含 name、type、status、configured_protocol、tools。状态为 connected/pending/disabled/failed/needs_auth;configured_protocol 是配置模式,不是协商后的版本。响应不包含连接地址、命令、认证头、OAuth 凭据或原始错误;查询不会创建或迁移会话目录。插件通过内部令牌保护的 /internal/mcp/session-policy 获取主服务已有的会话策略,不能凭请求 body 伪造用户或项目。
需要 OAuth 的服务应省略 oauth: false。管理员用同一服务账号、配置、环境和 XDG 数据目录执行 opencode2 mcp auth mcp_docs,认证流程、PKCE 与令牌续期由 OpenCode 管理;本项目不向前端暴露 OAuth 凭据,也不把 TJWater 的 Bearer 或项目头自动转交给外部 MCP。原生客户端发送的 OpenCode session ID 仅供调用关联,不是授权凭据。
本地 stdio 是管理员信任的宿主进程,不在会话 Shell 的 Landlock/seccomp 沙箱中。插件将继承环境中除基础运行变量以外的值置空,只添加该服务明确配置的环境变量,内部 Agent/OpenCode 凭据变量始终置空;它仍具有服务账号的宿主文件和网络权限,因此环境收敛不等于 OS 沙箱。
配置结构错误会使启动失败;可选服务的连接失败或 needs_auth 不使核心 Agent 的 /health 变为未就绪。client 模式须同步更新可信插件和服务配置,缺少新版状态 RPC 将不能通过 warmup。禁用或移除服务并重启运行时即可回退,无需数据迁移。
本次定向验证:bun run check、bun test tests/runtime/mcpConfig.test.ts tests/opencode/mcpPolicy.test.ts tests/routes/mcpRoutes.test.ts tests/runtime/opencode.test.ts tests/frontendAction/callIdPlugin.test.ts。可选的本地原生联调为 bun run tests/integration/mcp-smoke.ts:使用临时 XDG 目录和样例 HTTP/stdio 服务,结束后清理自己的进程与数据,不请求模型。