Files
next-tjwater-agent/src/runtime/opencode2-config.ts
T
jiang 6a49d2209e feat(agent): unify MCP tools and adopt Responses delivery
Move project and host queries into native MCP with host-owned OAuth/context,
while retaining memory, skills, actions and publication plugins. Add nearby-model
queries and explicit chat/task/learning model defaults.

Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped
session ownership, pending input edits and investigation communication.

Disable local-provider discovery: Server /health allowed native vLLM to probe
/v1/models on port 8000. A native before/after regression prevents recurrence.

Validation: 170 focused Bun tests; service/plugin typechecks; native discovery,
provider-discovery and input-inbox smoke checks; no paid model inference.
2026-10-19 18:46:58 +08:00

97 lines
6.6 KiB
TypeScript

import { PRODUCT_MCP_TOOL_IDS, QUERY_MCP_TOOL_IDS, queryMcpToolId } from "./product-mcp-tools.js";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { normalizeMcpConfig } from "./mcp-config.js";
import { compileOpencodeModels, loadAgentModelConfig, type AgentModelConfig } from "../chat/modelConfig.js";
import { DEFAULT_SESSION_WORKSPACE_ROOT } from "./conversationWorkspace.js";
import { TASK_AGENT, TASK_READER_AGENT, TASK_READER_TOOLS } from "./task-agent-policy.js";
const agentMessageTools = [queryMcpToolId("list_investigation_tasks"), queryMcpToolId("get_investigation_task"), "send_task_message", queryMcpToolId("read_agent_messages"), "reply_agent_message"];
type PermissionRule = { action: string; resource: string; effect: "allow" | "ask" | "deny" };
const deniedProductQuery = (name: string, rules: PermissionRule[]) => rules.some((rule) => {
if (rule.effect !== "deny") return false;
const backslash = String.fromCharCode(92);
const special = `${backslash}.^$+()[]{}|`;
const pattern = Array.from(rule.action).map((char) => char === "*" ? ".*" : char === "?" ? "." : special.includes(char) ? backslash + char : char).join("");
return new RegExp(`^${pattern}$`).test(name);
});
const dataPatterns = new Set(["data/**", "**/data/**"]);
// Exempt only managed session locations from ancestor data/ denials. Earlier
// explicit read/edit denials are reapplied; later administrator rules still win.
export const sessionWorkspacePermissions = (rules: PermissionRule[], workspaceRoot: string) => {
const root = resolve(workspaceRoot).replaceAll("\\", "/");
const result: PermissionRule[] = [];
for (const [index, rule] of rules.entries()) {
result.push(rule);
if (!["read", "edit"].includes(rule.action) || rule.effect !== "deny" || !dataPatterns.has(rule.resource)) continue;
if (rules.slice(index + 1).some((next) => next.action === rule.action && next.effect === "deny" && dataPatterns.has(next.resource))) continue;
if (rule.action === "read") {
result.push({ action: "read", resource: `${root}/conversation-*`, effect: "allow" }, { action: "read", resource: `${root}/conversation-*/**`, effect: "allow" });
for (const scope of ["project-*", "personal"]) {
const workspace = `${root}/actor-*/${scope}/session-*/workspace`;
result.push({ action: "read", resource: workspace, effect: "allow" }, { action: "read", resource: `${workspace}/**`, effect: "allow" });
}
} else {
for (const child of ["files", "tmp", ".cache"]) result.push({ action: "edit", resource: `${root}/conversation-*/${child}/**`, effect: "ask" });
for (const scope of ["project-*", "personal"]) for (const child of ["files", "tmp", ".cache"]) {
result.push({ action: "edit", resource: `${root}/actor-*/${scope}/session-*/workspace/${child}/**`, effect: "ask" });
}
}
result.push(...rules.slice(0, index).filter((previous) => previous.action === rule.action && previous.effect === "deny" && !dataPatterns.has(previous.resource)));
}
return result;
};
// A conversation Location can be outside the checkout. Load only the trusted
// service assets explicitly, rather than relying on its current directory.
export function serviceOpencodeConfig(input: Record<string, unknown>, models: AgentModelConfig = loadAgentModelConfig(), workspaceRoot = process.env.TJWATER_AGENT_WORKSPACE_ROOT ?? process.env.CONVERSATION_WORKSPACE_ROOT ?? DEFAULT_SESSION_WORKSPACE_ROOT) {
const agents = (input.agents ?? {}) as Record<string, unknown>;
const instruction = (agents.instruction ?? {}) as Record<string, unknown>;
const system = readFileSync(resolve(".opencode/agents/instruction.md"), "utf8")
.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, "");
return {
...input,
...compileOpencodeModels(models),
permissions: sessionWorkspacePermissions([...(input.permissions ?? []) as PermissionRule[], ...QUERY_MCP_TOOL_IDS.filter((name) => !deniedProductQuery(name, (input.permissions ?? []) as PermissionRule[])).map((action): PermissionRule => ({ action, resource: "*", effect: "allow" }))], workspaceRoot),
snapshots: false,
mcp: normalizeMcpConfig(input.mcp),
plugins: ["*", ...((input.plugins ?? []) as unknown[]), resolve(".opencode/tjwater-plugin"),
"-opencode.browser", "-opencode.browser.*",
// The wildcard also loads local-model discovery. vLLM probes port 8000,
// mistaking TJWater Server's /health for a model server. Models, including
// local providers, must come from agent-models.json instead of discovery.
"-opencode.provider.vllm", "-opencode.provider.lmstudio", "-opencode.provider.ollama"],
skills: [resolve(process.env.OPENCODE_SKILLS_ROOT_DIR ?? ".opencode/skills")],
agents: { ...agents,
instruction: {
description: "TJWater structured data and UI agent", mode: "primary", ...instruction, model: models.defaults.chat, system,
permissions: [
...agentMessageTools.map((action): PermissionRule => ({ action, resource: "*", effect: action.startsWith("mcp_") && deniedProductQuery(action, (input.permissions ?? []) as PermissionRule[]) ? "deny" : "allow" })),
...((instruction.permissions ?? []) as PermissionRule[]),
{ action: "subagent", resource: "*", effect: "deny" },
],
},
[TASK_AGENT]: {
description: "后台工况调查,协调只读子任务并统一发布进展与报告", mode: "primary",
model: models.defaults.task,
system: `${system}\n\n${readFileSync(resolve(".opencode/agents/investigation.md"), "utf8")}`,
permissions: [
...agentMessageTools.map((action): PermissionRule => ({ action, resource: "*", effect: [queryMcpToolId("read_agent_messages"), "reply_agent_message"].includes(action) && !(action.startsWith("mcp_") && deniedProductQuery(action, (input.permissions ?? []) as PermissionRule[])) ? "allow" : "deny" })),
{ action: "subagent", resource: "*", effect: "deny" },
{ action: "subagent", resource: TASK_READER_AGENT, effect: "allow" },
],
},
[TASK_READER_AGENT]: {
description: "独立核查一项调查问题,仅查询当前任务授权资料并返回证据编号、发现与局限", mode: "subagent",
system: readFileSync(resolve(".opencode/agents/investigation-reader.md"), "utf8"),
permissions: [
{ action: "*", resource: "*", effect: "deny" },
...TASK_READER_TOOLS.filter((name) => !PRODUCT_MCP_TOOL_IDS.includes(name) || !deniedProductQuery(name, (input.permissions ?? []) as PermissionRule[])).map((action) => ({ action, resource: "*", effect: "allow" })),
],
},
},
};
}