diff --git a/services/edge-agent/dist/agent.mjs b/services/edge-agent/dist/agent.mjs index 2a4c8073..c85c2bcd 100644 --- a/services/edge-agent/dist/agent.mjs +++ b/services/edge-agent/dist/agent.mjs @@ -151,6 +151,10 @@ function buildTransportHeartbeatState(brokerState = {}) { }; } +function isShellAccessEnabled(config = {}) { + return config.enableShellAccess === true; +} + function normalizeBrokerBaseUrl(value) { const trimmed = String(value || "").trim().replace(/\/+$/, ""); if (trimmed === "") { @@ -1785,6 +1789,10 @@ export async function processPolledShellAction(config, action, shell, fetchImpl } try { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } + if (actionType === "OPEN") { await shell.open(payload); } else if (actionType === "INPUT") { @@ -1919,18 +1927,30 @@ function createBrokerBridge({ } if (message.type === "OPEN_ROOT_SHELL") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } await shell.open(message.payload || {}); return; } if (message.type === "SHELL_INPUT") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } shell.input(message.payload || {}); return; } if (message.type === "RESIZE_ROOT_SHELL") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } shell.resize(message.payload || {}); return; } if (message.type === "CLOSE_ROOT_SHELL") { + if (!isShellAccessEnabled(config)) { + throw new Error("Shell access is disabled by local configuration"); + } shell.close(message.payload || {}); } } catch { diff --git a/services/edge-agent/test/agent.test.mjs b/services/edge-agent/test/agent.test.mjs index d3471839..d79ec584 100644 --- a/services/edge-agent/test/agent.test.mjs +++ b/services/edge-agent/test/agent.test.mjs @@ -17,6 +17,7 @@ import { getRelayStatus, loadConfig, parseCliArgs, + processPolledShellAction, processPolledCommand, runCli, runUpdate, @@ -670,6 +671,7 @@ test("startAgent reports API polling metadata, executes polled commands, and upl commandPollRetryDelayMs: 5, shellActionPollTimeoutSeconds: 0, shellActionPollRetryDelayMs: 5, + enableShellAccess: true, })); const heartbeats = []; @@ -928,6 +930,61 @@ test("startAgent reports API polling metadata, executes polled commands, and upl } }); +test("processPolledShellAction denies shell access when locally disabled", async () => { + const submissions = []; + const fakeFetch = async (url, options = {}) => { + if (/\/shell-actions\/\d+\/result$/.test(String(url))) { + submissions.push({ url, body: JSON.parse(options.body) }); + return { + ok: true, + async json() { + return { data: { acknowledged: true } }; + }, + }; + } + + throw new Error(`Unexpected URL: ${url}`); + }; + + const shell = { + async open() { + throw new Error("should not run"); + }, + input() { + throw new Error("should not run"); + }, + resize() { + throw new Error("should not run"); + }, + close() { + throw new Error("should not run"); + }, + }; + + const result = await processPolledShellAction( + { + apiUrl: "https://api.example.test", + gatewayId: 42, + agentToken: "agent-token", + enableShellAccess: false, + }, + { + id: 501, + actionType: "OPEN", + payload: { + sessionId: 44, + }, + }, + shell, + fakeFetch + ); + + assert.equal(result.ok, false); + assert.match(result.error, /Shell access is disabled/); + assert.equal(submissions.length, 1); + assert.equal(submissions[0].body.ok, false); +}); + test("status helpers report config without exposing the agent token", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-status-")); const configPath = path.join(tempDir, "config.json");