feat(agent): sandbox conversation analysis
Generic Container CI/CD / test-build-publish (push) Successful in 2m44s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m44s

This commit is contained in:
2026-08-25 16:08:33 +08:00
parent ce04704af2
commit 80cfc1f2ab
27 changed files with 2387 additions and 125 deletions
+116
View File
@@ -32,11 +32,20 @@ import {
import { buildChatRouter } from "./routes/chat.js";
import { buildAgentPublicRouter } from "./routes/publicApi.js";
import { opencodeRuntime } from "./runtime/opencode.js";
import {
executeSandboxCommand,
probeLandlockSandbox,
type SandboxProbe,
} from "./sandbox/landlockSandbox.js";
import {
getRuntimeSessionContext,
markRuntimeSessionAuthExpired,
type RuntimeSessionContext,
} from "./runtime/sessionContext.js";
import {
buildLargeCliResult,
stageLargeToolOutput,
} from "./runtime/toolOutputStaging.js";
import { ensureDirectory } from "./utils/fileStore.js";
import { SkillStore } from "./skills/store.js";
@@ -63,6 +72,7 @@ const resultReferenceResolver = new ResultReferenceResolver(
);
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
const credentialRefreshCoordinator = new CredentialRefreshCoordinator();
let sandboxProbe: SandboxProbe | null = null;
// 这个 token 只用于 OpenCode 子进程回调本服务的内部工具桥。
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
@@ -78,6 +88,7 @@ app.get("/health", async (_req, res) => {
ready: true,
warmed_up: true,
runtime,
sandbox: sandboxProbe,
sessions: sessionBridge.count(),
});
} catch (error) {
@@ -215,6 +226,7 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
const timeoutSec =
typeof req.body?.timeout === "number" && req.body.timeout > 0 ? req.body.timeout : 120;
const storeResult = req.body?.store_result === true;
if (!context.network) {
res.status(400).json({
@@ -309,6 +321,24 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
}
if (result.stdout.trim()) {
try {
const dataFile = await stageLargeToolOutput(context, result.stdout, {
contentType: "application/json",
extension: "json",
force: storeResult,
prefix: "cli",
});
if (dataFile) {
res.status(200).json(buildLargeCliResult(dataFile));
return;
}
} catch (error) {
res.status(409).json({
message: "large CLI result could not be staged",
detail: error instanceof Error ? error.message : String(error),
});
return;
}
res.status(200).type("application/json").send(result.stdout);
return;
}
@@ -321,6 +351,91 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
});
});
app.post("/internal/tools/sandbox-shell", async (req, res) => {
if (req.header("x-agent-internal-token") !== internalToken) {
res.status(403).json({ message: "forbidden" });
return;
}
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const command =
typeof req.body?.command === "string" ? req.body.command.trim() : "";
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
if (!context) {
res.status(404).json({ message: "session context not found", detail: sessionId });
return;
}
if (!context.workspaceDirectory) {
res.status(400).json({
message: "conversation workspace is required",
detail: "create a new conversation before running shell commands",
});
return;
}
if (!command) {
res.status(400).json({ message: "command is required" });
return;
}
const timeoutSeconds =
typeof req.body?.timeout === "number" && Number.isFinite(req.body.timeout)
? req.body.timeout
: 120;
try {
const result = await executeSandboxCommand(
context.workspaceDirectory,
command,
timeoutSeconds,
);
if (result.outcome === "timeout") {
res.status(504).json({
ok: false,
error: { code: "TIMEOUT", message: "sandbox command timed out" },
});
return;
}
if (result.outcome === "output_limit") {
res.status(502).json({
ok: false,
error: {
code: "OUTPUT_LIMIT_EXCEEDED",
message: `stdout exceeded ${config.MAX_CLI_OUTPUT_BYTES} bytes`,
},
});
return;
}
if (result.exitCode === 125) {
res.status(503).json({
ok: false,
error: { code: "SANDBOX_UNAVAILABLE", message: result.stderr },
});
return;
}
const dataFile = result.stdout
? await stageLargeToolOutput(context, result.stdout, {
contentType: "text/plain",
extension: "txt",
prefix: "shell",
})
: null;
res.json({
ok: result.exitCode === 0,
exit_code: result.exitCode,
signal: result.signal,
...(dataFile ? { data_file: dataFile } : { stdout: result.stdout }),
stderr: result.stderr || undefined,
stderr_truncated: result.stderrTruncated || undefined,
});
} catch (error) {
res.status(503).json({
ok: false,
error: {
code: "SANDBOX_UNAVAILABLE",
message: error instanceof Error ? error.message : String(error),
},
});
}
});
app.post("/internal/tools/store-render-ref", async (req, res) => {
if (req.header("x-agent-internal-token") !== internalToken) {
res.status(403).json({ message: "forbidden" });
@@ -626,6 +741,7 @@ app.use(
);
const bootstrap = async () => {
sandboxProbe = await probeLandlockSandbox();
await Promise.all([
sessionMetadataStore.initialize(),
sessionUiStateStore.initialize(),