feat(sandbox): add sandbox explain inspector
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
- Auto-reply: prefer `RawBody` for command/directive parsing (WhatsApp + Discord) and prevent fallback runs from clobbering concurrent session updates. (#643) — thanks @mcinteerj.
|
- Auto-reply: prefer `RawBody` for command/directive parsing (WhatsApp + Discord) and prevent fallback runs from clobbering concurrent session updates. (#643) — thanks @mcinteerj.
|
||||||
- Cron: `wakeMode: "now"` waits for heartbeat completion (and retries when the main lane is busy). (#666) — thanks @roshanasingh4.
|
- Cron: `wakeMode: "now"` waits for heartbeat completion (and retries when the main lane is busy). (#666) — thanks @roshanasingh4.
|
||||||
- Agents/OpenAI: fix Responses tool-only → follow-up turn handling (avoid standalone `reasoning` items that trigger 400 “required following item”).
|
- Agents/OpenAI: fix Responses tool-only → follow-up turn handling (avoid standalone `reasoning` items that trigger 400 “required following item”).
|
||||||
|
- Sandbox: add `clawdbot sandbox explain` (effective policy inspector + fix-it keys); improve “sandbox jail” tool-policy/elevated errors with actionable config key paths; link to docs.
|
||||||
- Hooks/Gmail: keep Tailscale serve path at `/` while preserving the public path. (#668) — thanks @antons.
|
- Hooks/Gmail: keep Tailscale serve path at `/` while preserving the public path. (#668) — thanks @antons.
|
||||||
- Hooks/Gmail: allow Tailscale target URLs to preserve internal serve paths.
|
- Hooks/Gmail: allow Tailscale target URLs to preserve internal serve paths.
|
||||||
- Auth: update Claude Code keychain credentials in-place during refresh sync; share JSON file helpers; add CLI fallback coverage.
|
- Auth: update Claude Code keychain credentials in-place during refresh sync; share JSON file helpers; add CLI fallback coverage.
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: Sandbox CLI
|
||||||
|
summary: "Manage sandbox containers and inspect effective sandbox policy"
|
||||||
|
read_when: "You are managing sandbox containers or debugging sandbox/tool-policy behavior."
|
||||||
|
status: active
|
||||||
|
---
|
||||||
|
|
||||||
# Sandbox CLI
|
# Sandbox CLI
|
||||||
|
|
||||||
Manage Docker-based sandbox containers for isolated agent execution.
|
Manage Docker-based sandbox containers for isolated agent execution.
|
||||||
@@ -8,6 +15,17 @@ Clawdbot can run agents in isolated Docker containers for security. The `sandbox
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
### `clawdbot sandbox explain`
|
||||||
|
|
||||||
|
Inspect the **effective** sandbox mode/scope/workspace access, sandbox tool policy, and elevated gates (with fix-it config key paths).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot sandbox explain
|
||||||
|
clawdbot sandbox explain --session agent:main:main
|
||||||
|
clawdbot sandbox explain --agent work
|
||||||
|
clawdbot sandbox explain --json
|
||||||
|
```
|
||||||
|
|
||||||
### `clawdbot sandbox list`
|
### `clawdbot sandbox list`
|
||||||
|
|
||||||
List all sandbox containers with their status and configuration.
|
List all sandbox containers with their status and configuration.
|
||||||
@@ -56,7 +74,7 @@ docker pull clawdbot-sandbox:latest
|
|||||||
docker tag clawdbot-sandbox:latest clawdbot-sandbox:bookworm-slim
|
docker tag clawdbot-sandbox:latest clawdbot-sandbox:bookworm-slim
|
||||||
|
|
||||||
# Update config to use new image
|
# Update config to use new image
|
||||||
# Edit clawdbot.config.json: agent.sandbox.docker.image
|
# Edit config: agents.defaults.sandbox.docker.image (or agents.list[].sandbox.docker.image)
|
||||||
|
|
||||||
# Recreate containers
|
# Recreate containers
|
||||||
clawdbot sandbox recreate --all
|
clawdbot sandbox recreate --all
|
||||||
@@ -65,7 +83,7 @@ clawdbot sandbox recreate --all
|
|||||||
### After changing sandbox configuration
|
### After changing sandbox configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Edit clawdbot.config.json: agent.sandbox.*
|
# Edit config: agents.defaults.sandbox.* (or agents.list[].sandbox.*)
|
||||||
|
|
||||||
# Recreate to apply new config
|
# Recreate to apply new config
|
||||||
clawdbot sandbox recreate --all
|
clawdbot sandbox recreate --all
|
||||||
|
|||||||
@@ -29,6 +29,14 @@
|
|||||||
"source": "/cron/",
|
"source": "/cron/",
|
||||||
"destination": "/cron-jobs"
|
"destination": "/cron-jobs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/sandbox",
|
||||||
|
"destination": "/gateway/sandbox-vs-tool-policy-vs-elevated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/sandbox/",
|
||||||
|
"destination": "/gateway/sandbox-vs-tool-policy-vs-elevated"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/model",
|
"source": "/model",
|
||||||
"destination": "/models"
|
"destination": "/models"
|
||||||
|
|||||||
77
docs/gateway/sandbox-vs-tool-policy-vs-elevated.md
Normal file
77
docs/gateway/sandbox-vs-tool-policy-vs-elevated.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: Sandbox vs Tool Policy vs Elevated
|
||||||
|
summary: "Why a tool is blocked: sandbox runtime, tool allow/deny policy, and elevated bash gates"
|
||||||
|
read_when: "You hit 'sandbox jail' or see a tool/elevated refusal and want the exact config key to change."
|
||||||
|
status: active
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sandbox vs Tool Policy vs Elevated
|
||||||
|
|
||||||
|
Clawdbot has three related (but different) controls:
|
||||||
|
|
||||||
|
1. **Sandbox** (`agents.defaults.sandbox.*` / `agents.list[].sandbox.*`) decides **where tools run** (Docker vs host).
|
||||||
|
2. **Tool policy** (`tools.*`, `tools.sandbox.tools.*`, `agents.list[].tools.*`) decides **which tools are available/allowed**.
|
||||||
|
3. **Elevated** (`tools.elevated.*`, `agents.list[].tools.elevated.*`) is a **bash-only escape hatch** to run on the host when you’re sandboxed.
|
||||||
|
|
||||||
|
## Quick debug
|
||||||
|
|
||||||
|
Use the inspector to see what Clawdbot is *actually* doing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot sandbox explain
|
||||||
|
clawdbot sandbox explain --session agent:main:main
|
||||||
|
clawdbot sandbox explain --agent work
|
||||||
|
clawdbot sandbox explain --json
|
||||||
|
```
|
||||||
|
|
||||||
|
It prints:
|
||||||
|
- effective sandbox mode/scope/workspace access
|
||||||
|
- whether the session is currently sandboxed (main vs non-main)
|
||||||
|
- effective sandbox tool allow/deny (and whether it came from agent/global/default)
|
||||||
|
- elevated gates and fix-it key paths
|
||||||
|
|
||||||
|
## Sandbox: where tools run
|
||||||
|
|
||||||
|
Sandboxing is controlled by `agents.defaults.sandbox.mode`:
|
||||||
|
- `"off"`: everything runs on the host.
|
||||||
|
- `"non-main"`: only non-main sessions are sandboxed (common “surprise” for groups/channels).
|
||||||
|
- `"all"`: everything is sandboxed.
|
||||||
|
|
||||||
|
See [Sandboxing](/gateway/sandboxing) for the full matrix (scope, workspace mounts, images).
|
||||||
|
|
||||||
|
## Tool policy: which tools exist/are callable
|
||||||
|
|
||||||
|
Two layers matter:
|
||||||
|
- **Global/per-agent tool policy**: `tools.allow`/`tools.deny` and `agents.list[].tools.allow`/`agents.list[].tools.deny`
|
||||||
|
- **Sandbox tool policy** (only applies when sandboxed): `tools.sandbox.tools.allow`/`tools.sandbox.tools.deny` and `agents.list[].tools.sandbox.tools.*`
|
||||||
|
|
||||||
|
Rules of thumb:
|
||||||
|
- `deny` always wins.
|
||||||
|
- If `allow` is non-empty, everything else is treated as blocked.
|
||||||
|
|
||||||
|
## Elevated: bash-only “run on host”
|
||||||
|
|
||||||
|
Elevated does **not** grant extra tools; it only affects `bash`.
|
||||||
|
- If you’re sandboxed, `/elevated on` (or `bash` with `elevated: true`) runs on the host.
|
||||||
|
- If you’re already running direct, elevated is effectively a no-op (still gated).
|
||||||
|
|
||||||
|
Gates:
|
||||||
|
- Enablement: `tools.elevated.enabled` (and optionally `agents.list[].tools.elevated.enabled`)
|
||||||
|
- Sender allowlists: `tools.elevated.allowFrom.<provider>` (and optionally `agents.list[].tools.elevated.allowFrom.<provider>`)
|
||||||
|
|
||||||
|
See [Elevated Mode](/tools/elevated).
|
||||||
|
|
||||||
|
## Common “sandbox jail” fixes
|
||||||
|
|
||||||
|
### “Tool X blocked by sandbox tool policy”
|
||||||
|
|
||||||
|
Fix-it keys (pick one):
|
||||||
|
- Disable sandbox: `agents.defaults.sandbox.mode=off` (or per-agent `agents.list[].sandbox.mode=off`)
|
||||||
|
- Allow the tool inside sandbox:
|
||||||
|
- remove it from `tools.sandbox.tools.deny` (or per-agent `agents.list[].tools.sandbox.tools.deny`)
|
||||||
|
- or add it to `tools.sandbox.tools.allow` (or per-agent allow)
|
||||||
|
|
||||||
|
### “I thought this was main, why is it sandboxed?”
|
||||||
|
|
||||||
|
In `"non-main"` mode, group/channel keys are *not* main. Use the main session key (shown by `sandbox explain`) or switch mode to `"off"`.
|
||||||
|
|
||||||
@@ -78,6 +78,10 @@ Tool allow/deny policies still apply before sandbox rules. If a tool is denied
|
|||||||
globally or per-agent, sandboxing doesn’t bring it back.
|
globally or per-agent, sandboxing doesn’t bring it back.
|
||||||
|
|
||||||
`tools.elevated` is an explicit escape hatch that runs `bash` on the host.
|
`tools.elevated` is an explicit escape hatch that runs `bash` on the host.
|
||||||
|
|
||||||
|
Debugging:
|
||||||
|
- Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.
|
||||||
|
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for the “why is this blocked?” mental model.
|
||||||
Keep it locked down.
|
Keep it locked down.
|
||||||
|
|
||||||
## Multi-agent overrides
|
## Multi-agent overrides
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ This allows you to run multiple agents with different security profiles:
|
|||||||
- Public-facing agents in sandboxes
|
- Public-facing agents in sandboxes
|
||||||
|
|
||||||
For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
|
For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) and `clawdbot sandbox explain`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Note:
|
|||||||
## Setting a session default
|
## Setting a session default
|
||||||
- Send a message that is **only** the directive (whitespace allowed), e.g. `/elevated on`.
|
- Send a message that is **only** the directive (whitespace allowed), e.g. `/elevated on`.
|
||||||
- Confirmation reply is sent (`Elevated mode enabled.` / `Elevated mode disabled.`).
|
- Confirmation reply is sent (`Elevated mode enabled.` / `Elevated mode disabled.`).
|
||||||
- If elevated access is disabled or the sender is not on the approved allowlist, the directive replies `elevated is not available right now.` and does not change session state.
|
- If elevated access is disabled or the sender is not on the approved allowlist, the directive replies with an actionable error (runtime sandboxed/direct + failing config key paths) and does not change session state.
|
||||||
- Send `/elevated` (or `/elevated:`) with no argument to see the current elevated level.
|
- Send `/elevated` (or `/elevated:`) with no argument to see the current elevated level.
|
||||||
|
|
||||||
## Availability + allowlists
|
## Availability + allowlists
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ describe("bash tool backgrounding", () => {
|
|||||||
command: "echo hi",
|
command: "echo hi",
|
||||||
elevated: true,
|
elevated: true,
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow("elevated is not available right now.");
|
).rejects.toThrow("tools.elevated.allowFrom.<provider>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not default to elevated when not allowed", async () => {
|
it("does not default to elevated when not allowed", async () => {
|
||||||
|
|||||||
@@ -194,7 +194,24 @@ export function createBashTool(
|
|||||||
: elevatedDefaultOn;
|
: elevatedDefaultOn;
|
||||||
if (elevatedRequested) {
|
if (elevatedRequested) {
|
||||||
if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) {
|
if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) {
|
||||||
throw new Error("elevated is not available right now.");
|
const runtime = defaults?.sandbox ? "sandboxed" : "direct";
|
||||||
|
const gates: string[] = [];
|
||||||
|
if (!elevatedDefaults?.enabled) {
|
||||||
|
gates.push("enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)");
|
||||||
|
} else {
|
||||||
|
gates.push("allowFrom (tools.elevated.allowFrom.<provider> / agents.list[].tools.elevated.allowFrom.<provider>)");
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
`elevated is not available right now (runtime=${runtime}).`,
|
||||||
|
`Failing gates: ${gates.join(", ")}`,
|
||||||
|
"Fix-it keys:",
|
||||||
|
"- tools.elevated.enabled",
|
||||||
|
"- tools.elevated.allowFrom.<provider>",
|
||||||
|
"- agents.list[].tools.elevated.enabled",
|
||||||
|
"- agents.list[].tools.elevated.allowFrom.<provider>",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
logInfo(
|
logInfo(
|
||||||
`bash: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
|
`bash: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import type {
|
|||||||
AgentToolResult,
|
AgentToolResult,
|
||||||
} from "@mariozechner/pi-agent-core";
|
} from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
normalizeThinkLevel,
|
normalizeThinkLevel,
|
||||||
type ThinkLevel,
|
type ThinkLevel,
|
||||||
} from "../auto-reply/thinking.js";
|
} from "../auto-reply/thinking.js";
|
||||||
|
|
||||||
import { sanitizeContentBlocksImages } from "./tool-images.js";
|
import { sanitizeContentBlocksImages } from "./tool-images.js";
|
||||||
|
import { formatSandboxToolPolicyBlockedMessage } from "./sandbox.js";
|
||||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||||
|
|
||||||
export type EmbeddedContextFile = { path: string; content: string };
|
export type EmbeddedContextFile = { path: string; content: string };
|
||||||
@@ -431,11 +433,26 @@ export function isContextOverflowError(errorMessage?: string): boolean {
|
|||||||
|
|
||||||
export function formatAssistantErrorText(
|
export function formatAssistantErrorText(
|
||||||
msg: AssistantMessage,
|
msg: AssistantMessage,
|
||||||
|
opts?: { cfg?: ClawdbotConfig; sessionKey?: string },
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (msg.stopReason !== "error") return undefined;
|
if (msg.stopReason !== "error") return undefined;
|
||||||
const raw = (msg.errorMessage ?? "").trim();
|
const raw = (msg.errorMessage ?? "").trim();
|
||||||
if (!raw) return "LLM request failed with an unknown error.";
|
if (!raw) return "LLM request failed with an unknown error.";
|
||||||
|
|
||||||
|
const unknownTool =
|
||||||
|
raw.match(/unknown tool[:\s]+["']?([a-z0-9_-]+)["']?/i) ??
|
||||||
|
raw.match(
|
||||||
|
/tool\s+["']?([a-z0-9_-]+)["']?\s+(?:not found|is not available)/i,
|
||||||
|
);
|
||||||
|
if (unknownTool?.[1]) {
|
||||||
|
const rewritten = formatSandboxToolPolicyBlockedMessage({
|
||||||
|
cfg: opts?.cfg,
|
||||||
|
sessionKey: opts?.sessionKey,
|
||||||
|
toolName: unknownTool[1],
|
||||||
|
});
|
||||||
|
if (rewritten) return rewritten;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for context overflow (413) errors
|
// Check for context overflow (413) errors
|
||||||
if (isContextOverflowError(raw)) {
|
if (isContextOverflowError(raw)) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1607,7 +1607,10 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const message =
|
const message =
|
||||||
lastAssistant?.errorMessage?.trim() ||
|
lastAssistant?.errorMessage?.trim() ||
|
||||||
(lastAssistant
|
(lastAssistant
|
||||||
? formatAssistantErrorText(lastAssistant)
|
? formatAssistantErrorText(lastAssistant, {
|
||||||
|
cfg: params.config,
|
||||||
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
|
})
|
||||||
: "") ||
|
: "") ||
|
||||||
(timedOut
|
(timedOut
|
||||||
? "LLM request timed out."
|
? "LLM request timed out."
|
||||||
@@ -1648,7 +1651,10 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
const errorText = lastAssistant
|
const errorText = lastAssistant
|
||||||
? formatAssistantErrorText(lastAssistant)
|
? formatAssistantErrorText(lastAssistant, {
|
||||||
|
cfg: params.config,
|
||||||
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (errorText) replyItems.push({ text: errorText, isError: true });
|
if (errorText) replyItems.push({ text: errorText, isError: true });
|
||||||
|
|||||||
67
src/agents/sandbox-explain.test.ts
Normal file
67
src/agents/sandbox-explain.test.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
formatSandboxToolPolicyBlockedMessage,
|
||||||
|
resolveSandboxConfigForAgent,
|
||||||
|
resolveSandboxToolPolicyForAgent,
|
||||||
|
} from "./sandbox.js";
|
||||||
|
|
||||||
|
describe("sandbox explain helpers", () => {
|
||||||
|
it("prefers agent overrides > global > defaults (sandbox tool policy)", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
sandbox: { mode: "all", scope: "agent" },
|
||||||
|
},
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "work",
|
||||||
|
workspace: "~/clawd-work",
|
||||||
|
tools: { sandbox: { tools: { allow: ["write"] } } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tools: { sandbox: { tools: { allow: ["read"], deny: ["browser"] } } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolved = resolveSandboxConfigForAgent(cfg, "work");
|
||||||
|
expect(resolved.tools.allow).toEqual(["write"]);
|
||||||
|
expect(resolved.tools.deny).toEqual(["browser"]);
|
||||||
|
|
||||||
|
const policy = resolveSandboxToolPolicyForAgent(cfg, "work");
|
||||||
|
expect(policy.allow).toEqual(["write"]);
|
||||||
|
expect(policy.sources.allow.source).toBe("agent");
|
||||||
|
expect(policy.deny).toEqual(["browser"]);
|
||||||
|
expect(policy.sources.deny.source).toBe("global");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes config key paths + main-session hint for non-main mode", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
sandbox: { mode: "non-main", scope: "agent" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
sandbox: {
|
||||||
|
tools: {
|
||||||
|
deny: ["browser"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const msg = formatSandboxToolPolicyBlockedMessage({
|
||||||
|
cfg,
|
||||||
|
sessionKey: "agent:main:whatsapp:group:G1",
|
||||||
|
toolName: "browser",
|
||||||
|
});
|
||||||
|
expect(msg).toBeTruthy();
|
||||||
|
expect(msg).toContain('Tool "browser" blocked by sandbox tool policy');
|
||||||
|
expect(msg).toContain("mode=non-main");
|
||||||
|
expect(msg).toContain("tools.sandbox.tools.deny");
|
||||||
|
expect(msg).toContain("agents.defaults.sandbox.mode=off");
|
||||||
|
expect(msg).toContain("Use main session key (direct): agent:main:main");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -19,12 +19,17 @@ import {
|
|||||||
loadConfig,
|
loadConfig,
|
||||||
STATE_DIR_CLAWDBOT,
|
STATE_DIR_CLAWDBOT,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import { normalizeAgentId, normalizeMainKey } from "../routing/session-key.js";
|
import {
|
||||||
|
buildAgentMainSessionKey,
|
||||||
|
normalizeAgentId,
|
||||||
|
normalizeMainKey,
|
||||||
|
} from "../routing/session-key.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import {
|
import {
|
||||||
resolveAgentConfig,
|
resolveAgentConfig,
|
||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
|
resolveSessionAgentId,
|
||||||
} from "./agent-scope.js";
|
} from "./agent-scope.js";
|
||||||
import { syncSkillsToWorkspace } from "./skills.js";
|
import { syncSkillsToWorkspace } from "./skills.js";
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +49,24 @@ export type SandboxToolPolicy = {
|
|||||||
deny?: string[];
|
deny?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SandboxToolPolicySource = {
|
||||||
|
source: "agent" | "global" | "default";
|
||||||
|
/**
|
||||||
|
* Config key path hint for humans.
|
||||||
|
* (Arrays use `agents.list[].…` form.)
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SandboxToolPolicyResolved = {
|
||||||
|
allow: string[];
|
||||||
|
deny: string[];
|
||||||
|
sources: {
|
||||||
|
allow: SandboxToolPolicySource;
|
||||||
|
deny: SandboxToolPolicySource;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type SandboxWorkspaceAccess = "none" | "ro" | "rw";
|
export type SandboxWorkspaceAccess = "none" | "ro" | "rw";
|
||||||
|
|
||||||
export type SandboxBrowserConfig = {
|
export type SandboxBrowserConfig = {
|
||||||
@@ -377,6 +400,65 @@ function resolveSandboxAgentId(scopeKey: string): string | undefined {
|
|||||||
return resolveAgentIdFromSessionKey(trimmed);
|
return resolveAgentIdFromSessionKey(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveSandboxToolPolicyForAgent(
|
||||||
|
cfg?: ClawdbotConfig,
|
||||||
|
agentId?: string,
|
||||||
|
): SandboxToolPolicyResolved {
|
||||||
|
const agentConfig =
|
||||||
|
cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined;
|
||||||
|
const agentAllow = agentConfig?.tools?.sandbox?.tools?.allow;
|
||||||
|
const agentDeny = agentConfig?.tools?.sandbox?.tools?.deny;
|
||||||
|
const globalAllow = cfg?.tools?.sandbox?.tools?.allow;
|
||||||
|
const globalDeny = cfg?.tools?.sandbox?.tools?.deny;
|
||||||
|
|
||||||
|
const allowSource = Array.isArray(agentAllow)
|
||||||
|
? ({
|
||||||
|
source: "agent",
|
||||||
|
key: "agents.list[].tools.sandbox.tools.allow",
|
||||||
|
} satisfies SandboxToolPolicySource)
|
||||||
|
: Array.isArray(globalAllow)
|
||||||
|
? ({
|
||||||
|
source: "global",
|
||||||
|
key: "tools.sandbox.tools.allow",
|
||||||
|
} satisfies SandboxToolPolicySource)
|
||||||
|
: ({
|
||||||
|
source: "default",
|
||||||
|
key: "tools.sandbox.tools.allow",
|
||||||
|
} satisfies SandboxToolPolicySource);
|
||||||
|
|
||||||
|
const denySource = Array.isArray(agentDeny)
|
||||||
|
? ({
|
||||||
|
source: "agent",
|
||||||
|
key: "agents.list[].tools.sandbox.tools.deny",
|
||||||
|
} satisfies SandboxToolPolicySource)
|
||||||
|
: Array.isArray(globalDeny)
|
||||||
|
? ({
|
||||||
|
source: "global",
|
||||||
|
key: "tools.sandbox.tools.deny",
|
||||||
|
} satisfies SandboxToolPolicySource)
|
||||||
|
: ({
|
||||||
|
source: "default",
|
||||||
|
key: "tools.sandbox.tools.deny",
|
||||||
|
} satisfies SandboxToolPolicySource);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allow: Array.isArray(agentAllow)
|
||||||
|
? agentAllow
|
||||||
|
: Array.isArray(globalAllow)
|
||||||
|
? globalAllow
|
||||||
|
: DEFAULT_TOOL_ALLOW,
|
||||||
|
deny: Array.isArray(agentDeny)
|
||||||
|
? agentDeny
|
||||||
|
: Array.isArray(globalDeny)
|
||||||
|
? globalDeny
|
||||||
|
: DEFAULT_TOOL_DENY,
|
||||||
|
sources: {
|
||||||
|
allow: allowSource,
|
||||||
|
deny: denySource,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveSandboxConfigForAgent(
|
export function resolveSandboxConfigForAgent(
|
||||||
cfg?: ClawdbotConfig,
|
cfg?: ClawdbotConfig,
|
||||||
agentId?: string,
|
agentId?: string,
|
||||||
@@ -396,6 +478,8 @@ export function resolveSandboxConfigForAgent(
|
|||||||
perSession: agentSandbox?.perSession ?? agent?.perSession,
|
perSession: agentSandbox?.perSession ?? agent?.perSession,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toolPolicy = resolveSandboxToolPolicyForAgent(cfg, agentId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
|
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
|
||||||
scope,
|
scope,
|
||||||
@@ -416,14 +500,8 @@ export function resolveSandboxConfigForAgent(
|
|||||||
agentBrowser: agentSandbox?.browser,
|
agentBrowser: agentSandbox?.browser,
|
||||||
}),
|
}),
|
||||||
tools: {
|
tools: {
|
||||||
allow:
|
allow: toolPolicy.allow,
|
||||||
agentConfig?.tools?.sandbox?.tools?.allow ??
|
deny: toolPolicy.deny,
|
||||||
cfg?.tools?.sandbox?.tools?.allow ??
|
|
||||||
DEFAULT_TOOL_ALLOW,
|
|
||||||
deny:
|
|
||||||
agentConfig?.tools?.sandbox?.tools?.deny ??
|
|
||||||
cfg?.tools?.sandbox?.tools?.deny ??
|
|
||||||
DEFAULT_TOOL_DENY,
|
|
||||||
},
|
},
|
||||||
prune: resolveSandboxPruneConfig({
|
prune: resolveSandboxPruneConfig({
|
||||||
scope,
|
scope,
|
||||||
@@ -443,6 +521,92 @@ function shouldSandboxSession(
|
|||||||
return sessionKey.trim() !== mainKey.trim();
|
return sessionKey.trim() !== mainKey.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveSandboxRuntimeStatus(params: {
|
||||||
|
cfg?: ClawdbotConfig;
|
||||||
|
sessionKey?: string;
|
||||||
|
}): {
|
||||||
|
agentId: string;
|
||||||
|
sessionKey: string;
|
||||||
|
mainSessionKey: string;
|
||||||
|
mode: SandboxConfig["mode"];
|
||||||
|
sandboxed: boolean;
|
||||||
|
toolPolicy: SandboxToolPolicyResolved;
|
||||||
|
} {
|
||||||
|
const sessionKey = params.sessionKey?.trim() ?? "";
|
||||||
|
const agentId = resolveSessionAgentId({
|
||||||
|
sessionKey,
|
||||||
|
config: params.cfg,
|
||||||
|
});
|
||||||
|
const cfg = params.cfg;
|
||||||
|
const sandboxCfg = resolveSandboxConfigForAgent(cfg, agentId);
|
||||||
|
const mainSessionKey = buildAgentMainSessionKey({
|
||||||
|
agentId,
|
||||||
|
mainKey: normalizeMainKey(cfg?.session?.mainKey),
|
||||||
|
});
|
||||||
|
const sandboxed = sessionKey
|
||||||
|
? shouldSandboxSession(sandboxCfg, sessionKey, mainSessionKey)
|
||||||
|
: false;
|
||||||
|
return {
|
||||||
|
agentId,
|
||||||
|
sessionKey,
|
||||||
|
mainSessionKey,
|
||||||
|
mode: sandboxCfg.mode,
|
||||||
|
sandboxed,
|
||||||
|
toolPolicy: resolveSandboxToolPolicyForAgent(cfg, agentId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSandboxToolPolicyBlockedMessage(params: {
|
||||||
|
cfg?: ClawdbotConfig;
|
||||||
|
sessionKey?: string;
|
||||||
|
toolName: string;
|
||||||
|
}): string | undefined {
|
||||||
|
const tool = params.toolName.trim().toLowerCase();
|
||||||
|
if (!tool) return undefined;
|
||||||
|
|
||||||
|
const runtime = resolveSandboxRuntimeStatus({
|
||||||
|
cfg: params.cfg,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
});
|
||||||
|
if (!runtime.sandboxed) return undefined;
|
||||||
|
|
||||||
|
const deny = new Set(normalizeToolList(runtime.toolPolicy.deny));
|
||||||
|
const allow = normalizeToolList(runtime.toolPolicy.allow);
|
||||||
|
const allowSet = allow.length > 0 ? new Set(allow) : null;
|
||||||
|
const blockedByDeny = deny.has(tool);
|
||||||
|
const blockedByAllow = allowSet ? !allowSet.has(tool) : false;
|
||||||
|
if (!blockedByDeny && !blockedByAllow) return undefined;
|
||||||
|
|
||||||
|
const reasons: string[] = [];
|
||||||
|
const fixes: string[] = [];
|
||||||
|
if (blockedByDeny) {
|
||||||
|
reasons.push("deny list");
|
||||||
|
fixes.push(`Remove "${tool}" from ${runtime.toolPolicy.sources.deny.key}.`);
|
||||||
|
}
|
||||||
|
if (blockedByAllow) {
|
||||||
|
reasons.push("allow list");
|
||||||
|
fixes.push(
|
||||||
|
`Add "${tool}" to ${runtime.toolPolicy.sources.allow.key} (or set it to [] to allow all).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(
|
||||||
|
`Tool "${tool}" blocked by sandbox tool policy (mode=${runtime.mode}).`,
|
||||||
|
);
|
||||||
|
lines.push(`Session: ${runtime.sessionKey || "(unknown)"}`);
|
||||||
|
lines.push(`Reason: ${reasons.join(" + ")}`);
|
||||||
|
lines.push("Fix:");
|
||||||
|
lines.push(`- agents.defaults.sandbox.mode=off (disable sandbox)`);
|
||||||
|
for (const fix of fixes) lines.push(`- ${fix}`);
|
||||||
|
if (runtime.mode === "non-main") {
|
||||||
|
lines.push(`- Use main session key (direct): ${runtime.mainSessionKey}`);
|
||||||
|
}
|
||||||
|
lines.push(`- See: clawdbot sandbox explain --session ${runtime.sessionKey}`);
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function slugifySessionKey(value: string) {
|
function slugifySessionKey(value: string) {
|
||||||
const trimmed = value.trim() || "session";
|
const trimmed = value.trim() || "session";
|
||||||
const hash = crypto
|
const hash = crypto
|
||||||
|
|||||||
@@ -828,7 +828,7 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("elevated is not available right now.");
|
expect(text).toContain("agents.list[].tools.elevated.enabled");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -875,7 +875,7 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("elevated is not available right now.");
|
expect(text).toContain("agents.list[].tools.elevated.allowFrom.whatsapp");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -556,7 +556,7 @@ describe("trigger handling", () => {
|
|||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("elevated is not available right now.");
|
expect(text).toContain("tools.elevated.enabled");
|
||||||
|
|
||||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||||
const store = JSON.parse(storeRaw) as Record<
|
const store = JSON.parse(storeRaw) as Record<
|
||||||
@@ -795,7 +795,7 @@ describe("trigger handling", () => {
|
|||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).not.toBe("elevated is not available right now.");
|
expect(text).not.toContain("elevated is not available right now");
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -876,7 +876,7 @@ describe("trigger handling", () => {
|
|||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("elevated is not available right now.");
|
expect(text).toContain("tools.elevated.allowFrom.discord");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import {
|
|||||||
isEmbeddedPiRunStreaming,
|
isEmbeddedPiRunStreaming,
|
||||||
resolveEmbeddedSessionLane,
|
resolveEmbeddedSessionLane,
|
||||||
} from "../agents/pi-embedded.js";
|
} from "../agents/pi-embedded.js";
|
||||||
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js";
|
import {
|
||||||
|
ensureSandboxWorkspaceForSession,
|
||||||
|
resolveSandboxRuntimeStatus,
|
||||||
|
} from "../agents/sandbox.js";
|
||||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
@@ -216,15 +219,22 @@ function resolveElevatedPermissions(params: {
|
|||||||
agentId: string;
|
agentId: string;
|
||||||
ctx: MsgContext;
|
ctx: MsgContext;
|
||||||
provider: string;
|
provider: string;
|
||||||
}): { enabled: boolean; allowed: boolean } {
|
}): { enabled: boolean; allowed: boolean; failures: Array<{ gate: string; key: string }> } {
|
||||||
const globalConfig = params.cfg.tools?.elevated;
|
const globalConfig = params.cfg.tools?.elevated;
|
||||||
const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools
|
const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools
|
||||||
?.elevated;
|
?.elevated;
|
||||||
const globalEnabled = globalConfig?.enabled !== false;
|
const globalEnabled = globalConfig?.enabled !== false;
|
||||||
const agentEnabled = agentConfig?.enabled !== false;
|
const agentEnabled = agentConfig?.enabled !== false;
|
||||||
const enabled = globalEnabled && agentEnabled;
|
const enabled = globalEnabled && agentEnabled;
|
||||||
if (!enabled) return { enabled, allowed: false };
|
const failures: Array<{ gate: string; key: string }> = [];
|
||||||
if (!params.provider) return { enabled, allowed: false };
|
if (!globalEnabled) failures.push({ gate: "enabled", key: "tools.elevated.enabled" });
|
||||||
|
if (!agentEnabled)
|
||||||
|
failures.push({ gate: "enabled", key: "agents.list[].tools.elevated.enabled" });
|
||||||
|
if (!enabled) return { enabled, allowed: false, failures };
|
||||||
|
if (!params.provider) {
|
||||||
|
failures.push({ gate: "provider", key: "ctx.Provider" });
|
||||||
|
return { enabled, allowed: false, failures };
|
||||||
|
}
|
||||||
|
|
||||||
const discordFallback =
|
const discordFallback =
|
||||||
params.provider === "discord"
|
params.provider === "discord"
|
||||||
@@ -236,7 +246,16 @@ function resolveElevatedPermissions(params: {
|
|||||||
allowFrom: globalConfig?.allowFrom,
|
allowFrom: globalConfig?.allowFrom,
|
||||||
discordFallback,
|
discordFallback,
|
||||||
});
|
});
|
||||||
if (!globalAllowed) return { enabled, allowed: false };
|
if (!globalAllowed) {
|
||||||
|
failures.push({
|
||||||
|
gate: "allowFrom",
|
||||||
|
key:
|
||||||
|
params.provider === "discord" && discordFallback
|
||||||
|
? "tools.elevated.allowFrom.discord (or discord.dm.allowFrom fallback)"
|
||||||
|
: `tools.elevated.allowFrom.${params.provider}`,
|
||||||
|
});
|
||||||
|
return { enabled, allowed: false, failures };
|
||||||
|
}
|
||||||
|
|
||||||
const agentAllowed = agentConfig?.allowFrom
|
const agentAllowed = agentConfig?.allowFrom
|
||||||
? isApprovedElevatedSender({
|
? isApprovedElevatedSender({
|
||||||
@@ -245,7 +264,44 @@ function resolveElevatedPermissions(params: {
|
|||||||
allowFrom: agentConfig.allowFrom,
|
allowFrom: agentConfig.allowFrom,
|
||||||
})
|
})
|
||||||
: true;
|
: true;
|
||||||
return { enabled, allowed: globalAllowed && agentAllowed };
|
if (!agentAllowed) {
|
||||||
|
failures.push({
|
||||||
|
gate: "allowFrom",
|
||||||
|
key: `agents.list[].tools.elevated.allowFrom.${params.provider}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { enabled, allowed: globalAllowed && agentAllowed, failures };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatElevatedUnavailableMessage(params: {
|
||||||
|
runtimeSandboxed: boolean;
|
||||||
|
failures: Array<{ gate: string; key: string }>;
|
||||||
|
sessionKey?: string;
|
||||||
|
}): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(
|
||||||
|
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
|
||||||
|
);
|
||||||
|
if (params.failures.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
`Failing gates: ${params.failures
|
||||||
|
.map((f) => `${f.gate} (${f.key})`)
|
||||||
|
.join(", ")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
lines.push(
|
||||||
|
"Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("Fix-it keys:");
|
||||||
|
lines.push("- tools.elevated.enabled");
|
||||||
|
lines.push("- tools.elevated.allowFrom.<provider>");
|
||||||
|
lines.push("- agents.list[].tools.elevated.enabled");
|
||||||
|
lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
|
||||||
|
if (params.sessionKey) {
|
||||||
|
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReplyFromConfig(
|
export async function getReplyFromConfig(
|
||||||
@@ -473,19 +529,31 @@ export async function getReplyFromConfig(
|
|||||||
sessionCtx.Provider?.trim().toLowerCase() ??
|
sessionCtx.Provider?.trim().toLowerCase() ??
|
||||||
ctx.Provider?.trim().toLowerCase() ??
|
ctx.Provider?.trim().toLowerCase() ??
|
||||||
"";
|
"";
|
||||||
const { enabled: elevatedEnabled, allowed: elevatedAllowed } =
|
const elevated = resolveElevatedPermissions({
|
||||||
resolveElevatedPermissions({
|
cfg,
|
||||||
cfg,
|
agentId,
|
||||||
agentId,
|
ctx,
|
||||||
ctx,
|
provider: messageProviderKey,
|
||||||
provider: messageProviderKey,
|
});
|
||||||
});
|
const elevatedEnabled = elevated.enabled;
|
||||||
|
const elevatedAllowed = elevated.allowed;
|
||||||
|
const elevatedFailures = elevated.failures;
|
||||||
if (
|
if (
|
||||||
directives.hasElevatedDirective &&
|
directives.hasElevatedDirective &&
|
||||||
(!elevatedEnabled || !elevatedAllowed)
|
(!elevatedEnabled || !elevatedAllowed)
|
||||||
) {
|
) {
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
return { text: "elevated is not available right now." };
|
const runtimeSandboxed = resolveSandboxRuntimeStatus({
|
||||||
|
cfg,
|
||||||
|
sessionKey: ctx.SessionKey,
|
||||||
|
}).sandboxed;
|
||||||
|
return {
|
||||||
|
text: formatElevatedUnavailableMessage({
|
||||||
|
runtimeSandboxed,
|
||||||
|
failures: elevatedFailures,
|
||||||
|
sessionKey: ctx.SessionKey,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const requireMention = resolveGroupRequireMention({
|
const requireMention = resolveGroupRequireMention({
|
||||||
@@ -621,6 +689,8 @@ export async function getReplyFromConfig(
|
|||||||
storePath,
|
storePath,
|
||||||
elevatedEnabled,
|
elevatedEnabled,
|
||||||
elevatedAllowed,
|
elevatedAllowed,
|
||||||
|
elevatedFailures,
|
||||||
|
messageProviderKey,
|
||||||
defaultProvider,
|
defaultProvider,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
aliasIndex,
|
aliasIndex,
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ import {
|
|||||||
resolveConfiguredModelRef,
|
resolveConfiguredModelRef,
|
||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import { resolveSandboxConfigForAgent } from "../../agents/sandbox.js";
|
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveAgentMainSessionKey,
|
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
saveSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
@@ -72,6 +71,31 @@ const withOptions = (line: string, options: string) =>
|
|||||||
const formatElevatedRuntimeHint = () =>
|
const formatElevatedRuntimeHint = () =>
|
||||||
`${SYSTEM_MARK} Runtime is direct; sandboxing does not apply.`;
|
`${SYSTEM_MARK} Runtime is direct; sandboxing does not apply.`;
|
||||||
|
|
||||||
|
function formatElevatedUnavailableText(params: {
|
||||||
|
runtimeSandboxed: boolean;
|
||||||
|
failures?: Array<{ gate: string; key: string }>;
|
||||||
|
sessionKey?: string;
|
||||||
|
}): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(
|
||||||
|
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
|
||||||
|
);
|
||||||
|
const failures = params.failures ?? [];
|
||||||
|
if (failures.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
`Failing gates: ${failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
lines.push(
|
||||||
|
"Fix-it keys: tools.elevated.enabled, tools.elevated.allowFrom.<provider>, agents.list[].tools.elevated.*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (params.sessionKey) {
|
||||||
|
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
const maskApiKey = (value: string): string => {
|
const maskApiKey = (value: string): string => {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) return "missing";
|
if (!trimmed) return "missing";
|
||||||
@@ -452,6 +476,8 @@ export async function handleDirectiveOnly(params: {
|
|||||||
storePath?: string;
|
storePath?: string;
|
||||||
elevatedEnabled: boolean;
|
elevatedEnabled: boolean;
|
||||||
elevatedAllowed: boolean;
|
elevatedAllowed: boolean;
|
||||||
|
elevatedFailures?: Array<{ gate: string; key: string }>;
|
||||||
|
messageProviderKey?: string;
|
||||||
defaultProvider: string;
|
defaultProvider: string;
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
aliasIndex: ModelAliasIndex;
|
aliasIndex: ModelAliasIndex;
|
||||||
@@ -496,22 +522,10 @@ export async function handleDirectiveOnly(params: {
|
|||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
});
|
});
|
||||||
const agentDir = resolveAgentDir(params.cfg, activeAgentId);
|
const agentDir = resolveAgentDir(params.cfg, activeAgentId);
|
||||||
const runtimeIsSandboxed = (() => {
|
const runtimeIsSandboxed = resolveSandboxRuntimeStatus({
|
||||||
const sessionKey = params.sessionKey?.trim();
|
cfg: params.cfg,
|
||||||
if (!sessionKey) return false;
|
sessionKey: params.sessionKey,
|
||||||
const agentId = resolveSessionAgentId({
|
}).sandboxed;
|
||||||
sessionKey,
|
|
||||||
config: params.cfg,
|
|
||||||
});
|
|
||||||
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId);
|
|
||||||
if (sandboxCfg.mode === "off") return false;
|
|
||||||
const mainKey = resolveAgentMainSessionKey({
|
|
||||||
cfg: params.cfg,
|
|
||||||
agentId,
|
|
||||||
});
|
|
||||||
if (sandboxCfg.mode === "all") return true;
|
|
||||||
return sessionKey !== mainKey;
|
|
||||||
})();
|
|
||||||
const shouldHintDirectRuntime =
|
const shouldHintDirectRuntime =
|
||||||
directives.hasElevatedDirective && !runtimeIsSandboxed;
|
directives.hasElevatedDirective && !runtimeIsSandboxed;
|
||||||
|
|
||||||
@@ -709,7 +723,13 @@ export async function handleDirectiveOnly(params: {
|
|||||||
if (directives.hasElevatedDirective && !directives.elevatedLevel) {
|
if (directives.hasElevatedDirective && !directives.elevatedLevel) {
|
||||||
if (!directives.rawElevatedLevel) {
|
if (!directives.rawElevatedLevel) {
|
||||||
if (!elevatedEnabled || !elevatedAllowed) {
|
if (!elevatedEnabled || !elevatedAllowed) {
|
||||||
return { text: "elevated is not available right now." };
|
return {
|
||||||
|
text: formatElevatedUnavailableText({
|
||||||
|
runtimeSandboxed: runtimeIsSandboxed,
|
||||||
|
failures: params.elevatedFailures,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const level = currentElevatedLevel ?? "off";
|
const level = currentElevatedLevel ?? "off";
|
||||||
return {
|
return {
|
||||||
@@ -729,7 +749,13 @@ export async function handleDirectiveOnly(params: {
|
|||||||
directives.hasElevatedDirective &&
|
directives.hasElevatedDirective &&
|
||||||
(!elevatedEnabled || !elevatedAllowed)
|
(!elevatedEnabled || !elevatedAllowed)
|
||||||
) {
|
) {
|
||||||
return { text: "elevated is not available right now." };
|
return {
|
||||||
|
text: formatElevatedUnavailableText({
|
||||||
|
runtimeSandboxed: runtimeIsSandboxed,
|
||||||
|
failures: params.elevatedFailures,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
sandboxListCommand,
|
sandboxListCommand,
|
||||||
sandboxRecreateCommand,
|
sandboxRecreateCommand,
|
||||||
} from "../commands/sandbox.js";
|
} from "../commands/sandbox.js";
|
||||||
|
import { sandboxExplainCommand } from "../commands/sandbox-explain.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
@@ -19,7 +20,8 @@ Examples:
|
|||||||
clawdbot sandbox list --browser # List only browser containers
|
clawdbot sandbox list --browser # List only browser containers
|
||||||
clawdbot sandbox recreate --all # Recreate all containers
|
clawdbot sandbox recreate --all # Recreate all containers
|
||||||
clawdbot sandbox recreate --session main # Recreate specific session
|
clawdbot sandbox recreate --session main # Recreate specific session
|
||||||
clawdbot sandbox recreate --agent mybot # Recreate agent containers`,
|
clawdbot sandbox recreate --agent mybot # Recreate agent containers
|
||||||
|
clawdbot sandbox explain # Explain effective sandbox config`,
|
||||||
|
|
||||||
list: `
|
list: `
|
||||||
Examples:
|
Examples:
|
||||||
@@ -55,6 +57,13 @@ Filter options:
|
|||||||
Modifiers:
|
Modifiers:
|
||||||
--browser Only affect browser containers (not regular sandbox)
|
--browser Only affect browser containers (not regular sandbox)
|
||||||
--force Skip confirmation prompt`,
|
--force Skip confirmation prompt`,
|
||||||
|
|
||||||
|
explain: `
|
||||||
|
Examples:
|
||||||
|
clawdbot sandbox explain
|
||||||
|
clawdbot sandbox explain --session agent:main:main
|
||||||
|
clawdbot sandbox explain --agent work
|
||||||
|
clawdbot sandbox explain --json`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function createRunner(
|
function createRunner(
|
||||||
@@ -129,4 +138,26 @@ export function registerSandboxCli(program: Command) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- Explain Command ---
|
||||||
|
|
||||||
|
sandbox
|
||||||
|
.command("explain")
|
||||||
|
.description("Explain effective sandbox/tool policy for a session/agent")
|
||||||
|
.option("--session <key>", "Session key to inspect (defaults to agent main)")
|
||||||
|
.option("--agent <id>", "Agent id to inspect (defaults to derived agent)")
|
||||||
|
.option("--json", "Output result as JSON", false)
|
||||||
|
.addHelpText("after", EXAMPLES.explain)
|
||||||
|
.action(
|
||||||
|
createRunner((opts) =>
|
||||||
|
sandboxExplainCommand(
|
||||||
|
{
|
||||||
|
session: opts.session as string | undefined,
|
||||||
|
agent: opts.agent as string | undefined,
|
||||||
|
json: Boolean(opts.json),
|
||||||
|
},
|
||||||
|
defaultRuntime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/commands/sandbox-explain.test.ts
Normal file
49
src/commands/sandbox-explain.test.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
let mockCfg: unknown = {};
|
||||||
|
|
||||||
|
vi.mock("../config/config.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadConfig: vi.fn().mockImplementation(() => mockCfg),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sandbox explain command", () => {
|
||||||
|
it("prints JSON shape + fix-it keys", async () => {
|
||||||
|
mockCfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
sandbox: { tools: { deny: ["browser"] } },
|
||||||
|
elevated: { enabled: true, allowFrom: { whatsapp: ["*"] } },
|
||||||
|
},
|
||||||
|
session: { store: "/tmp/clawdbot-test-sessions-{agentId}.json" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { sandboxExplainCommand } = await import("./sandbox-explain.js");
|
||||||
|
|
||||||
|
const logs: string[] = [];
|
||||||
|
await sandboxExplainCommand(
|
||||||
|
{ json: true, session: "agent:main:main" },
|
||||||
|
{
|
||||||
|
log: (msg: string) => logs.push(msg),
|
||||||
|
error: (msg: string) => logs.push(msg),
|
||||||
|
exit: (_code: number) => {},
|
||||||
|
} as unknown as Parameters<typeof sandboxExplainCommand>[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
const out = logs.join("");
|
||||||
|
const parsed = JSON.parse(out);
|
||||||
|
expect(parsed).toHaveProperty("docsUrl", "https://docs.clawd.bot/sandbox");
|
||||||
|
expect(parsed).toHaveProperty("sandbox.mode", "all");
|
||||||
|
expect(parsed).toHaveProperty("sandbox.tools.sources.allow.source");
|
||||||
|
expect(Array.isArray(parsed.fixIt)).toBe(true);
|
||||||
|
expect(parsed.fixIt).toContain("agents.defaults.sandbox.mode=off");
|
||||||
|
expect(parsed.fixIt).toContain("tools.sandbox.tools.deny");
|
||||||
|
});
|
||||||
|
});
|
||||||
339
src/commands/sandbox-explain.ts
Normal file
339
src/commands/sandbox-explain.ts
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
loadSessionStore,
|
||||||
|
resolveAgentMainSessionKey,
|
||||||
|
resolveMainSessionKey,
|
||||||
|
resolveStorePath,
|
||||||
|
} from "../config/sessions.js";
|
||||||
|
import {
|
||||||
|
buildAgentMainSessionKey,
|
||||||
|
normalizeAgentId,
|
||||||
|
normalizeMainKey,
|
||||||
|
parseAgentSessionKey,
|
||||||
|
resolveAgentIdFromSessionKey,
|
||||||
|
} from "../routing/session-key.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
||||||
|
import {
|
||||||
|
resolveSandboxConfigForAgent,
|
||||||
|
resolveSandboxToolPolicyForAgent,
|
||||||
|
} from "../agents/sandbox.js";
|
||||||
|
|
||||||
|
type SandboxExplainOptions = {
|
||||||
|
session?: string;
|
||||||
|
agent?: string;
|
||||||
|
json: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SANDBOX_DOCS_URL = "https://docs.clawd.bot/sandbox";
|
||||||
|
|
||||||
|
const KNOWN_PROVIDER_KEYS = new Set([
|
||||||
|
"whatsapp",
|
||||||
|
"telegram",
|
||||||
|
"discord",
|
||||||
|
"slack",
|
||||||
|
"signal",
|
||||||
|
"imessage",
|
||||||
|
"webchat",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normalizeExplainSessionKey(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
agentId: string;
|
||||||
|
session?: string;
|
||||||
|
}): string {
|
||||||
|
const raw = (params.session ?? "").trim();
|
||||||
|
if (!raw) {
|
||||||
|
return resolveAgentMainSessionKey({
|
||||||
|
cfg: params.cfg,
|
||||||
|
agentId: params.agentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (raw.includes(":")) return raw;
|
||||||
|
if (raw === "global") return "global";
|
||||||
|
return buildAgentMainSessionKey({
|
||||||
|
agentId: params.agentId,
|
||||||
|
mainKey: normalizeMainKey(raw),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferProviderFromSessionKey(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
sessionKey: string;
|
||||||
|
}): string | undefined {
|
||||||
|
const parsed = parseAgentSessionKey(params.sessionKey);
|
||||||
|
if (!parsed) return undefined;
|
||||||
|
const rest = parsed.rest.trim();
|
||||||
|
if (!rest) return undefined;
|
||||||
|
const parts = rest.split(":").filter(Boolean);
|
||||||
|
if (parts.length === 0) return undefined;
|
||||||
|
const configuredMainKey = normalizeMainKey(params.cfg.session?.mainKey);
|
||||||
|
if (parts[0] === configuredMainKey) return undefined;
|
||||||
|
const candidate = parts[0]?.trim().toLowerCase();
|
||||||
|
return candidate && KNOWN_PROVIDER_KEYS.has(candidate) ? candidate : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveActiveProvider(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
agentId: string;
|
||||||
|
sessionKey: string;
|
||||||
|
}): string | undefined {
|
||||||
|
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||||
|
agentId: params.agentId,
|
||||||
|
});
|
||||||
|
const store = loadSessionStore(storePath);
|
||||||
|
const entry = store[params.sessionKey];
|
||||||
|
const candidate = (
|
||||||
|
entry?.lastProvider ??
|
||||||
|
entry?.providerOverride ??
|
||||||
|
entry?.provider ??
|
||||||
|
""
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (candidate && KNOWN_PROVIDER_KEYS.has(candidate)) return candidate;
|
||||||
|
return inferProviderFromSessionKey({
|
||||||
|
cfg: params.cfg,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveElevatedAllowListForProvider(params: {
|
||||||
|
provider: string;
|
||||||
|
allowFrom?: Record<string, Array<string | number> | undefined>;
|
||||||
|
discordFallback?: Array<string | number>;
|
||||||
|
}): Array<string | number> | undefined {
|
||||||
|
switch (params.provider) {
|
||||||
|
case "whatsapp":
|
||||||
|
return params.allowFrom?.whatsapp;
|
||||||
|
case "telegram":
|
||||||
|
return params.allowFrom?.telegram;
|
||||||
|
case "discord": {
|
||||||
|
const hasExplicit = Boolean(
|
||||||
|
params.allowFrom && Object.hasOwn(params.allowFrom, "discord"),
|
||||||
|
);
|
||||||
|
if (hasExplicit) return params.allowFrom?.discord;
|
||||||
|
return params.discordFallback;
|
||||||
|
}
|
||||||
|
case "slack":
|
||||||
|
return params.allowFrom?.slack;
|
||||||
|
case "signal":
|
||||||
|
return params.allowFrom?.signal;
|
||||||
|
case "imessage":
|
||||||
|
return params.allowFrom?.imessage;
|
||||||
|
case "webchat":
|
||||||
|
return params.allowFrom?.webchat;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sandboxExplainCommand(
|
||||||
|
opts: SandboxExplainOptions,
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
): Promise<void> {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
|
||||||
|
const defaultAgentId = resolveAgentIdFromSessionKey(resolveMainSessionKey(cfg));
|
||||||
|
const resolvedAgentId = normalizeAgentId(
|
||||||
|
opts.agent?.trim()
|
||||||
|
? opts.agent
|
||||||
|
: opts.session?.trim()
|
||||||
|
? resolveAgentIdFromSessionKey(opts.session)
|
||||||
|
: defaultAgentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionKey = normalizeExplainSessionKey({
|
||||||
|
cfg,
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
session: opts.session,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sandboxCfg = resolveSandboxConfigForAgent(cfg, resolvedAgentId);
|
||||||
|
const toolPolicy = resolveSandboxToolPolicyForAgent(cfg, resolvedAgentId);
|
||||||
|
const mainSessionKey = resolveAgentMainSessionKey({
|
||||||
|
cfg,
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
});
|
||||||
|
const sessionIsSandboxed =
|
||||||
|
sandboxCfg.mode === "all"
|
||||||
|
? true
|
||||||
|
: sandboxCfg.mode === "off"
|
||||||
|
? false
|
||||||
|
: sessionKey.trim() !== mainSessionKey.trim();
|
||||||
|
|
||||||
|
const provider = resolveActiveProvider({
|
||||||
|
cfg,
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
sessionKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agentConfig = resolveAgentConfig(cfg, resolvedAgentId);
|
||||||
|
const elevatedGlobal = cfg.tools?.elevated;
|
||||||
|
const elevatedAgent = agentConfig?.tools?.elevated;
|
||||||
|
const elevatedGlobalEnabled = elevatedGlobal?.enabled !== false;
|
||||||
|
const elevatedAgentEnabled = elevatedAgent?.enabled !== false;
|
||||||
|
const elevatedEnabled = elevatedGlobalEnabled && elevatedAgentEnabled;
|
||||||
|
|
||||||
|
const discordFallback =
|
||||||
|
provider === "discord" ? cfg.discord?.dm?.allowFrom : undefined;
|
||||||
|
const globalAllow = provider
|
||||||
|
? resolveElevatedAllowListForProvider({
|
||||||
|
provider,
|
||||||
|
allowFrom: elevatedGlobal?.allowFrom as unknown as Record<
|
||||||
|
string,
|
||||||
|
Array<string | number> | undefined
|
||||||
|
>,
|
||||||
|
discordFallback,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const agentAllow = provider
|
||||||
|
? resolveElevatedAllowListForProvider({
|
||||||
|
provider,
|
||||||
|
allowFrom: elevatedAgent?.allowFrom as unknown as Record<
|
||||||
|
string,
|
||||||
|
Array<string | number> | undefined
|
||||||
|
>,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const allowTokens = (values?: Array<string | number>) =>
|
||||||
|
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||||
|
const globalAllowTokens = allowTokens(globalAllow);
|
||||||
|
const agentAllowTokens = allowTokens(agentAllow);
|
||||||
|
|
||||||
|
const elevatedAllowedByConfig =
|
||||||
|
elevatedEnabled &&
|
||||||
|
Boolean(provider) &&
|
||||||
|
globalAllowTokens.length > 0 &&
|
||||||
|
(elevatedAgent?.allowFrom ? agentAllowTokens.length > 0 : true);
|
||||||
|
|
||||||
|
const elevatedAlwaysAllowedByConfig =
|
||||||
|
elevatedAllowedByConfig &&
|
||||||
|
globalAllowTokens.includes("*") &&
|
||||||
|
(elevatedAgent?.allowFrom ? agentAllowTokens.includes("*") : true);
|
||||||
|
|
||||||
|
const elevatedFailures: Array<{ gate: string; key: string }> = [];
|
||||||
|
if (!elevatedGlobalEnabled) {
|
||||||
|
elevatedFailures.push({ gate: "enabled", key: "tools.elevated.enabled" });
|
||||||
|
}
|
||||||
|
if (!elevatedAgentEnabled) {
|
||||||
|
elevatedFailures.push({
|
||||||
|
gate: "enabled",
|
||||||
|
key: "agents.list[].tools.elevated.enabled",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (provider && globalAllowTokens.length === 0) {
|
||||||
|
elevatedFailures.push({
|
||||||
|
gate: "allowFrom",
|
||||||
|
key:
|
||||||
|
provider === "discord" && discordFallback
|
||||||
|
? "tools.elevated.allowFrom.discord (or discord.dm.allowFrom fallback)"
|
||||||
|
: `tools.elevated.allowFrom.${provider}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (provider && elevatedAgent?.allowFrom && agentAllowTokens.length === 0) {
|
||||||
|
elevatedFailures.push({
|
||||||
|
gate: "allowFrom",
|
||||||
|
key: `agents.list[].tools.elevated.allowFrom.${provider}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixIt: string[] = [];
|
||||||
|
if (sandboxCfg.mode !== "off") {
|
||||||
|
fixIt.push("agents.defaults.sandbox.mode=off");
|
||||||
|
fixIt.push("agents.list[].sandbox.mode=off");
|
||||||
|
}
|
||||||
|
fixIt.push("tools.sandbox.tools.allow");
|
||||||
|
fixIt.push("tools.sandbox.tools.deny");
|
||||||
|
fixIt.push("agents.list[].tools.sandbox.tools.allow");
|
||||||
|
fixIt.push("agents.list[].tools.sandbox.tools.deny");
|
||||||
|
fixIt.push("tools.elevated.enabled");
|
||||||
|
if (provider) fixIt.push(`tools.elevated.allowFrom.${provider}`);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
docsUrl: SANDBOX_DOCS_URL,
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
sessionKey,
|
||||||
|
mainSessionKey,
|
||||||
|
sandbox: {
|
||||||
|
mode: sandboxCfg.mode,
|
||||||
|
scope: sandboxCfg.scope,
|
||||||
|
perSession: sandboxCfg.scope === "session",
|
||||||
|
workspaceAccess: sandboxCfg.workspaceAccess,
|
||||||
|
workspaceRoot: sandboxCfg.workspaceRoot,
|
||||||
|
sessionIsSandboxed,
|
||||||
|
tools: {
|
||||||
|
allow: toolPolicy.allow,
|
||||||
|
deny: toolPolicy.deny,
|
||||||
|
sources: toolPolicy.sources,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elevated: {
|
||||||
|
enabled: elevatedEnabled,
|
||||||
|
provider,
|
||||||
|
allowedByConfig: elevatedAllowedByConfig,
|
||||||
|
alwaysAllowedByConfig: elevatedAlwaysAllowedByConfig,
|
||||||
|
allowFrom: {
|
||||||
|
global: provider ? globalAllowTokens : undefined,
|
||||||
|
agent: elevatedAgent?.allowFrom && provider ? agentAllowTokens : undefined,
|
||||||
|
},
|
||||||
|
failures: elevatedFailures,
|
||||||
|
},
|
||||||
|
fixIt,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
runtime.log(`${JSON.stringify(payload, null, 2)}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push("Effective sandbox:");
|
||||||
|
lines.push(` agentId: ${payload.agentId}`);
|
||||||
|
lines.push(` sessionKey: ${payload.sessionKey}`);
|
||||||
|
lines.push(` mainSessionKey: ${payload.mainSessionKey}`);
|
||||||
|
lines.push(
|
||||||
|
` runtime: ${payload.sandbox.sessionIsSandboxed ? "sandboxed" : "direct"}`,
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
` mode=${payload.sandbox.mode} scope=${payload.sandbox.scope} perSession=${payload.sandbox.perSession}`,
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
` workspaceAccess=${payload.sandbox.workspaceAccess} workspaceRoot=${payload.sandbox.workspaceRoot}`,
|
||||||
|
);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Sandbox tool policy:");
|
||||||
|
lines.push(
|
||||||
|
` allow (${payload.sandbox.tools.sources.allow.source}): ${payload.sandbox.tools.allow.join(", ") || "(empty)"}`,
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
` deny (${payload.sandbox.tools.sources.deny.source}): ${payload.sandbox.tools.deny.join(", ") || "(empty)"}`,
|
||||||
|
);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Elevated:");
|
||||||
|
lines.push(` enabled: ${payload.elevated.enabled}`);
|
||||||
|
lines.push(` provider: ${payload.elevated.provider ?? "(unknown)"}`);
|
||||||
|
lines.push(` allowedByConfig: ${payload.elevated.allowedByConfig}`);
|
||||||
|
if (payload.elevated.failures.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
` failing gates: ${payload.elevated.failures
|
||||||
|
.map((f) => `${f.gate} (${f.key})`)
|
||||||
|
.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (payload.sandbox.mode === "non-main" && payload.sandbox.sessionIsSandboxed) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(
|
||||||
|
`Hint: sandbox mode is non-main; use main session key to run direct: ${payload.mainSessionKey}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Fix-it:");
|
||||||
|
for (const key of payload.fixIt) lines.push(` - ${key}`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`Docs: ${payload.docsUrl}`);
|
||||||
|
|
||||||
|
runtime.log(`${lines.join("\n")}\n`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user