




















@@ -0,0 +1,312 @@
1+import crypto from "node:crypto";
2+import type { AgentToolResult } from "@mariozechner/pi-agent-core";
3+import {
4+type ExecApprovalsFile,
5+type ExecAsk,
6+type ExecSecurity,
7+type SystemRunApprovalPlan,
8+evaluateShellAllowlist,
9+hasDurableExecApproval,
10+resolveExecApprovalsFromFile,
11+} from "../infra/exec-approvals.js";
12+import {
13+describeInterpreterInlineEval,
14+detectInterpreterInlineEvalArgv,
15+} from "../infra/exec-inline-eval.js";
16+import { buildNodeShellCommand } from "../infra/node-shell.js";
17+import { parsePreparedSystemRunPayload } from "../infra/system-run-approval-context.js";
18+import type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.js";
19+import type { ExecToolDetails } from "./bash-tools.exec-types.js";
20+import { callGatewayTool } from "./tools/gateway.js";
21+import { listNodes, resolveNodeIdFromList } from "./tools/nodes-utils.js";
22+23+export type NodeExecutionTarget = {
24+nodeId: string;
25+platform?: string | null;
26+argv: string[];
27+env: Record<string, string> | undefined;
28+invokeTimeoutMs: number;
29+};
30+31+export type PreparedNodeRun = {
32+plan: SystemRunApprovalPlan;
33+argv: string[];
34+rawCommand: string;
35+cwd: string | undefined;
36+agentId: string | undefined;
37+sessionKey: string | undefined;
38+};
39+40+export type NodeApprovalAnalysis = {
41+analysisOk: boolean;
42+allowlistSatisfied: boolean;
43+durableApprovalSatisfied: boolean;
44+inlineEvalHit: ReturnType<typeof detectInterpreterInlineEvalArgv>;
45+};
46+47+export function shouldSkipNodeApprovalPrepare(params: {
48+hostSecurity: ExecSecurity;
49+hostAsk: ExecAsk;
50+strictInlineEval?: boolean;
51+}): boolean {
52+return (
53+params.hostSecurity === "full" && params.hostAsk === "off" && params.strictInlineEval !== true
54+);
55+}
56+57+export function formatNodeRunToolResult(params: {
58+raw: unknown;
59+startedAt: number;
60+cwd: string | undefined;
61+}): AgentToolResult<ExecToolDetails> {
62+const payload =
63+params.raw && typeof params.raw === "object"
64+ ? (params.raw as { payload?: unknown }).payload
65+ : undefined;
66+const payloadObj =
67+payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
68+const stdout = typeof payloadObj.stdout === "string" ? payloadObj.stdout : "";
69+const stderr = typeof payloadObj.stderr === "string" ? payloadObj.stderr : "";
70+const errorText = typeof payloadObj.error === "string" ? payloadObj.error : "";
71+const success = typeof payloadObj.success === "boolean" ? payloadObj.success : false;
72+const exitCode = typeof payloadObj.exitCode === "number" ? payloadObj.exitCode : null;
73+return {
74+content: [
75+{
76+type: "text",
77+text: stdout || stderr || errorText || "",
78+},
79+],
80+details: {
81+status: success ? "completed" : "failed",
82+ exitCode,
83+durationMs: Date.now() - params.startedAt,
84+aggregated: [stdout, stderr, errorText].filter(Boolean).join("\n"),
85+cwd: params.cwd,
86+} satisfies ExecToolDetails,
87+};
88+}
89+90+export async function resolveNodeExecutionTarget(
91+params: ExecuteNodeHostCommandParams,
92+): Promise<NodeExecutionTarget> {
93+if (params.boundNode && params.requestedNode && params.boundNode !== params.requestedNode) {
94+throw new Error(`exec node not allowed (bound to ${params.boundNode})`);
95+}
96+const nodeQuery = params.boundNode || params.requestedNode;
97+const nodes = await listNodes({});
98+if (nodes.length === 0) {
99+throw new Error(
100+"exec host=node requires a paired node (none available). This requires a companion app or node host.",
101+);
102+}
103+let nodeId: string;
104+try {
105+nodeId = resolveNodeIdFromList(nodes, nodeQuery, !nodeQuery);
106+} catch (err) {
107+if (!nodeQuery && String(err).includes("node required")) {
108+throw new Error(
109+"exec host=node requires a node id when multiple nodes are available (set tools.exec.node or exec.node).",
110+{ cause: err },
111+);
112+}
113+throw err;
114+}
115+const nodeInfo = nodes.find((entry) => entry.nodeId === nodeId);
116+const supportsSystemRun = Array.isArray(nodeInfo?.commands)
117+ ? nodeInfo?.commands?.includes("system.run")
118+ : false;
119+if (!supportsSystemRun) {
120+throw new Error(
121+"exec host=node requires a node that supports system.run (companion app or node host).",
122+);
123+}
124+125+return {
126+ nodeId,
127+platform: nodeInfo?.platform,
128+argv: buildNodeShellCommand(params.command, nodeInfo?.platform),
129+env: params.requestedEnv ? { ...params.requestedEnv } : undefined,
130+invokeTimeoutMs: Math.max(
131+10_000,
132+(typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec) *
133+1000 +
134+5_000,
135+),
136+};
137+}
138+139+export function buildNodeSystemRunInvoke(params: {
140+target: NodeExecutionTarget;
141+command: string[];
142+rawCommand: string;
143+cwd: string | undefined;
144+timeoutSec: number | undefined;
145+agentId: string | undefined;
146+sessionKey: string | undefined;
147+approved?: boolean;
148+approvalDecision?: "allow-once" | "allow-always" | null;
149+runId?: string;
150+suppressNotifyOnExit?: boolean;
151+notifyOnExit?: boolean;
152+systemRunPlan?: SystemRunApprovalPlan;
153+}): Record<string, unknown> {
154+return {
155+nodeId: params.target.nodeId,
156+command: "system.run",
157+params: {
158+command: params.command,
159+rawCommand: params.rawCommand,
160+ ...(params.systemRunPlan ? { systemRunPlan: params.systemRunPlan } : {}),
161+ ...(params.cwd != null ? { cwd: params.cwd } : {}),
162+env: params.target.env,
163+timeoutMs: typeof params.timeoutSec === "number" ? params.timeoutSec * 1000 : undefined,
164+agentId: params.agentId,
165+sessionKey: params.sessionKey,
166+approved: params.approved,
167+approvalDecision: params.approvalDecision ?? undefined,
168+runId: params.runId ?? undefined,
169+suppressNotifyOnExit:
170+params.suppressNotifyOnExit === true || params.notifyOnExit === false ? true : undefined,
171+},
172+idempotencyKey: crypto.randomUUID(),
173+};
174+}
175+176+export async function invokeNodeSystemRunDirect(params: {
177+request: ExecuteNodeHostCommandParams;
178+target: NodeExecutionTarget;
179+}): Promise<AgentToolResult<ExecToolDetails>> {
180+const startedAt = Date.now();
181+const raw = await callGatewayTool(
182+"node.invoke",
183+{ timeoutMs: params.target.invokeTimeoutMs },
184+buildNodeSystemRunInvoke({
185+target: params.target,
186+command: params.target.argv,
187+rawCommand: params.request.command,
188+cwd: params.request.workdir,
189+timeoutSec: params.request.timeoutSec,
190+agentId: params.request.agentId,
191+sessionKey: params.request.sessionKey,
192+notifyOnExit: params.request.notifyOnExit,
193+}),
194+);
195+return formatNodeRunToolResult({ raw, startedAt, cwd: params.request.workdir });
196+}
197+198+export async function prepareNodeSystemRun(params: {
199+request: ExecuteNodeHostCommandParams;
200+target: NodeExecutionTarget;
201+}): Promise<PreparedNodeRun> {
202+const prepareRaw = await callGatewayTool(
203+"node.invoke",
204+{ timeoutMs: 15_000 },
205+{
206+nodeId: params.target.nodeId,
207+command: "system.run.prepare",
208+params: {
209+command: params.target.argv,
210+rawCommand: params.request.command,
211+ ...(params.request.workdir != null ? { cwd: params.request.workdir } : {}),
212+agentId: params.request.agentId,
213+sessionKey: params.request.sessionKey,
214+},
215+idempotencyKey: crypto.randomUUID(),
216+},
217+);
218+const prepared = parsePreparedSystemRunPayload(prepareRaw?.payload);
219+if (!prepared) {
220+throw new Error("invalid system.run.prepare response");
221+}
222+return {
223+plan: prepared.plan,
224+argv: prepared.plan.argv,
225+rawCommand: prepared.plan.commandText,
226+cwd: prepared.plan.cwd ?? params.request.workdir,
227+agentId: prepared.plan.agentId ?? params.request.agentId,
228+sessionKey: prepared.plan.sessionKey ?? params.request.sessionKey,
229+};
230+}
231+232+export async function analyzeNodeApprovalRequirement(params: {
233+request: ExecuteNodeHostCommandParams;
234+target: NodeExecutionTarget;
235+prepared: PreparedNodeRun;
236+hostSecurity: ExecSecurity;
237+hostAsk: ExecAsk;
238+}): Promise<NodeApprovalAnalysis> {
239+const baseAllowlistEval = evaluateShellAllowlist({
240+command: params.request.command,
241+allowlist: [],
242+safeBins: new Set(),
243+cwd: params.request.workdir,
244+env: params.request.env,
245+platform: params.target.platform,
246+trustedSafeBinDirs: params.request.trustedSafeBinDirs,
247+});
248+let analysisOk = baseAllowlistEval.analysisOk;
249+let allowlistSatisfied = false;
250+let durableApprovalSatisfied = false;
251+const inlineEvalHit =
252+params.request.strictInlineEval === true
253+ ? (baseAllowlistEval.segments
254+.map((segment) =>
255+detectInterpreterInlineEvalArgv(segment.resolution?.effectiveArgv ?? segment.argv),
256+)
257+.find((entry) => entry !== null) ?? null)
258+ : null;
259+if (inlineEvalHit) {
260+params.request.warnings.push(
261+`Warning: strict inline-eval mode requires explicit approval for ${describeInterpreterInlineEval(
262+ inlineEvalHit,
263+ )}.`,
264+);
265+}
266+if ((params.hostAsk === "always" || params.hostSecurity === "allowlist") && analysisOk) {
267+try {
268+const approvalsSnapshot = await callGatewayTool<{ file: string }>(
269+"exec.approvals.node.get",
270+{ timeoutMs: 10_000 },
271+{ nodeId: params.target.nodeId },
272+);
273+const approvalsFile =
274+approvalsSnapshot && typeof approvalsSnapshot === "object"
275+ ? approvalsSnapshot.file
276+ : undefined;
277+if (approvalsFile && typeof approvalsFile === "object") {
278+const resolved = resolveExecApprovalsFromFile({
279+file: approvalsFile as ExecApprovalsFile,
280+agentId: params.request.agentId,
281+overrides: { security: "full" },
282+});
283+// Allowlist-only precheck; safe bins are node-local and may diverge.
284+const allowlistEval = evaluateShellAllowlist({
285+command: params.request.command,
286+allowlist: resolved.allowlist,
287+safeBins: new Set(),
288+cwd: params.request.workdir,
289+env: params.request.env,
290+platform: params.target.platform,
291+trustedSafeBinDirs: params.request.trustedSafeBinDirs,
292+});
293+durableApprovalSatisfied = hasDurableExecApproval({
294+analysisOk: allowlistEval.analysisOk,
295+segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,
296+allowlist: resolved.allowlist,
297+commandText: params.prepared.rawCommand,
298+});
299+allowlistSatisfied = allowlistEval.allowlistSatisfied;
300+analysisOk = allowlistEval.analysisOk;
301+}
302+} catch {
303+// Fall back to requiring approval if node approvals cannot be fetched.
304+}
305+}
306+return {
307+ analysisOk,
308+ allowlistSatisfied,
309+ durableApprovalSatisfied,
310+ inlineEvalHit,
311+};
312+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。