feat(agent): sandbox conversation analysis
This commit is contained in:
+116
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user