

























@@ -223,9 +223,35 @@ vi.mock("./tools/gateway.js", () => ({
223223callGatewayTool: callGatewayToolMock,
224224}));
225225226+const resolveNodeIdFromListMock = vi.hoisted(() =>
227+vi.fn((nodes: Array<{ nodeId: string; displayName?: string }>, query?: string) => {
228+if (!query) {
229+if (nodes.length === 1) {
230+return nodes[0].nodeId;
231+}
232+throw new Error("node required");
233+}
234+const byId = nodes.find((n) => n.nodeId === query);
235+if (byId) {
236+return byId.nodeId;
237+}
238+const byName = nodes.find((n) => n.displayName === query);
239+if (byName) {
240+return byName.nodeId;
241+}
242+if (query.length >= 6) {
243+const byPrefix = nodes.find((n) => n.nodeId.startsWith(query));
244+if (byPrefix) {
245+return byPrefix.nodeId;
246+}
247+}
248+throw new Error(`unknown node: ${query}`);
249+}),
250+);
251+226252vi.mock("./tools/nodes-utils.js", () => ({
227253listNodes: listNodesMock,
228-resolveNodeIdFromList: vi.fn(() => "node-1"),
254+resolveNodeIdFromList: resolveNodeIdFromListMock,
229255}));
230256231257vi.mock("../logger.js", () => ({
@@ -2659,6 +2685,145 @@ describe("executeNodeHostCommand", () => {
26592685expectSystemRunInvoke({ invokeTimeoutMs: 35_000, runTimeoutMs: 0 });
26602686});
266126872688+it("allows exec when requestedNode is display name matching boundNode's device", async () => {
2689+listNodesMock.mockResolvedValue([
2690+{
2691+nodeId: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2692+displayName: "home-wsl-debian",
2693+commands: ["system.run"],
2694+platform: process.platform,
2695+},
2696+]);
2697+const result = await executeNodeHostCommand({
2698+command: "echo hello",
2699+workdir: "/tmp/work",
2700+env: {},
2701+security: "full",
2702+ask: "off",
2703+defaultTimeoutSec: 30,
2704+approvalRunningNoticeMs: 0,
2705+warnings: [],
2706+agentId: "test-agent",
2707+sessionKey: "test-session",
2708+boundNode: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2709+requestedNode: "home-wsl-debian",
2710+});
2711+expect(result.details?.status).toBeDefined();
2712+});
2713+2714+it("allows exec when requestedNode is partial ID matching boundNode's device", async () => {
2715+listNodesMock.mockResolvedValue([
2716+{
2717+nodeId: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2718+displayName: "home-wsl-debian",
2719+commands: ["system.run"],
2720+platform: process.platform,
2721+},
2722+]);
2723+const result = await executeNodeHostCommand({
2724+command: "echo hello",
2725+workdir: "/tmp/work",
2726+env: {},
2727+security: "full",
2728+ask: "off",
2729+defaultTimeoutSec: 30,
2730+approvalRunningNoticeMs: 0,
2731+warnings: [],
2732+agentId: "test-agent",
2733+sessionKey: "test-session",
2734+boundNode: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2735+requestedNode: "f2396b588d391d",
2736+});
2737+expect(result.details?.status).toBeDefined();
2738+});
2739+2740+it("rejects exec when requestedNode resolves to a different node than boundNode", async () => {
2741+listNodesMock.mockResolvedValue([
2742+{
2743+nodeId: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2744+displayName: "home-wsl-debian",
2745+commands: ["system.run"],
2746+platform: process.platform,
2747+},
2748+{
2749+nodeId: "aaaa1111bbbb2222cccc3333dddd4444eeee5555ffff6666aaa7777bbb88889999",
2750+displayName: "other-node",
2751+commands: ["system.run"],
2752+platform: process.platform,
2753+},
2754+]);
2755+await expect(
2756+executeNodeHostCommand({
2757+command: "echo hello",
2758+workdir: "/tmp/work",
2759+env: {},
2760+security: "full",
2761+ask: "off",
2762+defaultTimeoutSec: 30,
2763+approvalRunningNoticeMs: 0,
2764+warnings: [],
2765+agentId: "test-agent",
2766+sessionKey: "test-session",
2767+boundNode: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2768+requestedNode: "other-node",
2769+}),
2770+).rejects.toThrow("exec node not allowed (bound to f2396b588d391d30");
2771+});
2772+2773+it("preserves original error when requestedNode matches no known node", async () => {
2774+listNodesMock.mockResolvedValue([
2775+{
2776+nodeId: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2777+displayName: "home-wsl-debian",
2778+commands: ["system.run"],
2779+platform: process.platform,
2780+},
2781+]);
2782+await expect(
2783+executeNodeHostCommand({
2784+command: "echo hello",
2785+workdir: "/tmp/work",
2786+env: {},
2787+security: "full",
2788+ask: "off",
2789+defaultTimeoutSec: 30,
2790+approvalRunningNoticeMs: 0,
2791+warnings: [],
2792+agentId: "test-agent",
2793+sessionKey: "test-session",
2794+requestedNode: "nonexistent-node",
2795+}),
2796+).rejects.toThrow(
2797+"requested node not found: nonexistent-node (unknown node: nonexistent-node)",
2798+);
2799+});
2800+2801+it("allows exec when boundNode is a display name matching the same device as requestedNode", async () => {
2802+listNodesMock.mockResolvedValue([
2803+{
2804+nodeId: "f2396b588d391d30a79d300e196a17cf197f34969b5e2485d2734c953567f44e",
2805+displayName: "home-wsl-debian",
2806+commands: ["system.run"],
2807+platform: process.platform,
2808+},
2809+]);
2810+const result = await executeNodeHostCommand({
2811+command: "echo hello",
2812+workdir: "/tmp/work",
2813+env: {},
2814+security: "full",
2815+ask: "off",
2816+defaultTimeoutSec: 30,
2817+approvalRunningNoticeMs: 0,
2818+warnings: [],
2819+agentId: "test-agent",
2820+sessionKey: "test-session",
2821+boundNode: "home-wsl-debian",
2822+requestedNode: "home-wsl-debian",
2823+});
2824+expect(result.details?.status).toBeDefined();
2825+});
2826+26622827it("auto-reviews strict inline-eval commands with full/off host policy when node policy is available", async () => {
26632828const inlinePlan = {
26642829argv: ["python3", "-c", "print(1)"],
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。