diff --git a/CHANGELOG.md b/CHANGELOG.md index d773132b0..3fe7513d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.clawd.bot ### Fixes - Web UI: hide internal `message_id` hints in chat bubbles. - Heartbeat: normalize target identifiers for consistent routing. +- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco. - Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b. - macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman. diff --git a/docs/tools/elevated.md b/docs/tools/elevated.md index 8b561b473..863c53a1f 100644 --- a/docs/tools/elevated.md +++ b/docs/tools/elevated.md @@ -6,9 +6,10 @@ read_when: # Elevated Mode (/elevated directives) ## What it does -- `/elevated on` is a **shortcut** for `exec.host=gateway` + `exec.security=full` (approvals still apply). +- `/elevated on` runs on the gateway host and keeps exec approvals (same as `/elevated ask`). - `/elevated full` runs on the gateway host **and** auto-approves exec (skips exec approvals). - `/elevated ask` runs on the gateway host but keeps exec approvals (same as `/elevated on`). +- `on`/`ask` do **not** force `exec.security=full`; configured security/ask policy still applies. - Only changes behavior when the agent is **sandboxed** (otherwise exec already runs on the host). - Directive forms: `/elevated on|off|ask|full`, `/elev on|off|ask|full`. - Only `on|off|ask|full` are accepted; anything else returns a hint and does not change state. @@ -18,8 +19,8 @@ read_when: - **Per-session state**: `/elevated on|off|ask|full` sets the elevated level for the current session key. - **Inline directive**: `/elevated on|ask|full` inside a message applies to that message only. - **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned. -- **Host execution**: elevated forces `exec` onto the gateway host with full security. -- **Approvals**: `full` skips exec approvals; `on`/`ask` still honor them. +- **Host execution**: elevated forces `exec` onto the gateway host; `full` also sets `security=full`. +- **Approvals**: `full` skips exec approvals; `on`/`ask` honor them when allowlist/ask rules require. - **Unsandboxed agents**: no-op for location; only affects gating, logging, and status. - **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used. diff --git a/docs/tools/exec.md b/docs/tools/exec.md index 068be74ee..e2088137b 100644 --- a/docs/tools/exec.md +++ b/docs/tools/exec.md @@ -24,7 +24,7 @@ Background sessions are scoped per agent; `process` only sees sessions from the - `security` (`deny | allowlist | full`): enforcement mode for `gateway`/`node` - `ask` (`off | on-miss | always`): approval prompts for `gateway`/`node` - `node` (string): node id/name for `host=node` -- `elevated` (bool): alias for `host=gateway` + `security=full` when sandboxed and allowed +- `elevated` (bool): request elevated mode (gateway host); `security=full` is only forced when elevated resolves to `full` Notes: - `host` defaults to `sandbox`. diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index 6606ae008..fd260c11d 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -150,4 +150,35 @@ describe("exec approvals", () => { expect(result.details.status).toBe("completed"); expect(calls).not.toContain("exec.approval.request"); }); + + it("requires approval for elevated ask when allowlist misses", async () => { + const { callGatewayTool } = await import("./tools/gateway.js"); + const calls: string[] = []; + let resolveApproval: (() => void) | undefined; + const approvalSeen = new Promise((resolve) => { + resolveApproval = resolve; + }); + + vi.mocked(callGatewayTool).mockImplementation(async (method) => { + calls.push(method); + if (method === "exec.approval.request") { + resolveApproval?.(); + return { decision: "deny" }; + } + return { ok: true }; + }); + + const { createExecTool } = await import("./bash-tools.exec.js"); + const tool = createExecTool({ + ask: "on-miss", + security: "allowlist", + approvalRunningNoticeMs: 0, + elevated: { enabled: true, allowed: true, defaultLevel: "ask" }, + }); + + const result = await tool.execute("call4", { command: "echo ok", elevated: true }); + expect(result.details.status).toBe("approval-pending"); + await approvalSeen; + expect(calls).toContain("exec.approval.request"); + }); });