Skip to content
Merged
33 changes: 28 additions & 5 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,34 @@ groq, cerebras, etc.).

Agent behavior configuration. Applied during bootstrap (when `provider` is present).

| Field | Type | Default | Description |
| ---------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `skipOnboarding` | boolean | `true` | Skip `openclaw onboard` in headless mode. Set `false` to run onboarding (requires interactive terminal). |
| `toolsProfile` | string | `"full"` | Agent tools profile (`"full"`, `"coding"`, `"messaging"`, etc.). |
| `sandbox` | boolean | — | Set to `false` to disable sandbox mode (`agents.defaults.sandbox.mode off`). |
| Field | Type | Default | Description |
| -------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `skipOnboarding` | boolean | `true` | Skip `openclaw onboard` in headless mode. Set `false` to run onboarding (requires interactive terminal). |
| `toolsProfile` | string | `"full"` | Agent tools profile (`"full"`, `"coding"`, `"messaging"`, etc.). |
| `sandbox` | boolean | `false` | Sandbox the agent (`agents.defaults.sandbox.mode`). Default off matches clawctl's trusted-operator trust model; set `true` to opt back in to sandboxing. |
| `elevated.allowFrom` | object | — | Override the auto-derived `tools.elevated.allowFrom` map. Each key is a channel name (`telegram`, `discord`, …); the value is an array of sender IDs allowed elevated. |

### Security defaults clawctl applies

On a fresh bootstrap, clawctl configures OpenClaw for a **single, trusted
operator** — the same person owns the host and the VM, and the gateway
is reachable only over localhost or Tailscale. The post-onboard step
runs `openclaw exec-policy preset yolo` (sets `tools.exec.security=full`
and matching approvals defaults) and additionally writes:

| Setting | Value | Why |
| ------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------- |
| `tools.profile` | `agent.toolsProfile` (`"full"`) | Baseline tool policy. |
| `agents.defaults.sandbox.mode` | `off` | No sandbox by default; flip via `agent.sandbox: true` if desired. |
| `tools.exec.security` | `full` | Agent may exec arbitrary commands. |
| `commands.config` | `true` | Agent may use the `/config` slash command to introspect/adjust its config. |
| `tools.elevated.enabled` | `true` | Privileged surfaces available to trusted senders. |
| `tools.elevated.allowFrom.<channel>` | auto-derived | Mirrors each channel's `allowFrom`; override via `agent.elevated.allowFrom`. |

Operators who want a tighter posture can override individual settings
via the `openclaw` passthrough below (e.g.
`"openclaw": { "tools.exec.security": "allowlist" }`), or by running
`openclaw exec-policy preset cautious` / `deny-all` inside the VM.

## `channels`

Expand Down
27 changes: 22 additions & 5 deletions packages/capabilities/src/capabilities/one-password/op-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ export async function provisionOpWrapper(ctx: CapabilityContext): Promise<Provis

/**
* Install exec-approvals.json for the 1Password CLI.
*
* This file is clawctl-managed — it sets the per-agent exec policy that
* the OpenClaw gateway intersects with `tools.exec.*` config. Defaults
* mirror clawctl's trusted-operator trust model: `security=full` so the
* agent can exec freely. The op pattern is kept in the allowlist as
* forward-compatible documentation: if the operator later flips to
* restrictive mode (security=allowlist) via `openclaw approvals` or
* by editing this file, op still works without them remembering to
* re-add it.
*
* Earlier versions of this capability wrote `security=deny` /
* `agents.main.security=allowlist` defaults, which combined with
* recent upstream policy-intersection rules (per-agent override beats
* config request) silently blocked all exec on a fresh install. We
* now own this file in the permissive direction; operators who want
* lock-down customize via `openclaw approvals` after bootstrap.
*
* Conditional — only installs if op is available.
*/
export async function provisionExecApprovals(ctx: CapabilityContext): Promise<ProvisionResult> {
Expand All @@ -100,14 +117,14 @@ export async function provisionExecApprovals(ctx: CapabilityContext): Promise<Pr
{
version: 1,
defaults: {
security: "deny",
ask: "on-miss",
askFallback: "deny",
security: "full",
ask: "off",
askFallback: "full",
},
agents: {
main: {
security: "allowlist",
ask: "on-miss",
security: "full",
ask: "off",
allowlist: [{ pattern: "~/.local/bin/op" }],
},
},
Expand Down
69 changes: 68 additions & 1 deletion packages/host-core/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,76 @@ export async function bootstrapOpenclaw(
}
configCmds.push(`openclaw config set tools.profile ${config.agent?.toolsProfile ?? "full"}`);
configCmds.push("openclaw config set agents.defaults.workspace /mnt/project/data/workspace");
if (config.agent?.sandbox === false) {

// Trusted-operator default: sandbox off unless the operator explicitly
// requests it (agent.sandbox === true). clawctl owns the host+VM, so
// sandbox-as-protection adds friction without buying isolation we don't
// already have. Earlier behavior gated this on opt-in (sandbox === false);
// we invert because the new tools.exec policy layer means "no opt-in"
// resulted in a blocked-by-default gateway.
if (config.agent?.sandbox !== true) {
configCmds.push("openclaw config set agents.defaults.sandbox.mode off");
}

// Apply the trusted-operator exec policy: tools.exec.security=full,
// ask=off, host=gateway in the gateway config, plus matching defaults
// in ~/.openclaw/exec-approvals.json. Without this the host approvals
// file's defaults (and any per-agent override) silently intersect
// tools.profile=full down to "exec denied" — see
// tasks/2026-05-14_*/TASK.md for the full schema map.
configCmds.push("openclaw exec-policy preset yolo");

// Allow the agent to use the /config slash command. Disabled upstream
// by default; for a trusted single-operator setup it's the obvious
// way for the agent to introspect/adjust its own configuration.
configCmds.push("openclaw config set commands.config true");

// Channel-derived sender allowlist. The IDs the operator put in each
// channel's allowFrom are the same humans they want to grant elevated
// and owner privileges to — repeating them in three places is the
// friction we set out to remove. Explicit agent.elevated.allowFrom
// overrides the auto-derived map.
const channelAllowFrom: Record<string, (string | number)[]> = {};
if (config.channels) {
for (const [channelName, channelConfig] of Object.entries(config.channels)) {
if (channelConfig.enabled === false) continue;
const af = (channelConfig as { allowFrom?: unknown }).allowFrom;
if (Array.isArray(af) && af.length > 0) {
channelAllowFrom[channelName] = af as (string | number)[];
}
}
}
const elevatedAllowFrom: Record<string, (string | number)[]> = { ...channelAllowFrom };
const explicitElevated = config.agent?.elevated?.allowFrom;
if (explicitElevated) {
for (const [k, v] of Object.entries(explicitElevated)) {
elevatedAllowFrom[k] = v;
}
}

// Elevated tool access: privileged tool surfaces gated by sender.
configCmds.push("openclaw config set tools.elevated.enabled true");
for (const [channelName, ids] of Object.entries(elevatedAllowFrom)) {
const json = JSON.stringify(ids);
configCmds.push(`openclaw config set tools.elevated.allowFrom.${channelName} '${json}'`);
}

// Command-owner allowlist. Required separately from commands.config:
// commands.config gates "is the /config slash enabled at all", and
// commands.ownerAllowFrom gates "is *this sender* allowed to call it"
// for the owner-only family (/config, /diagnostics, /export-trajectory,
// exec approvals, …). Format is "<channel>:<id>" — flatten the
// channel-derived map into that shape.
const ownerIds: string[] = [];
for (const [channelName, ids] of Object.entries(elevatedAllowFrom)) {
for (const id of ids) {
ownerIds.push(`${channelName}:${id}`);
}
}
if (ownerIds.length > 0) {
configCmds.push(`openclaw config set commands.ownerAllowFrom '${JSON.stringify(ownerIds)}'`);
}

configCmds.push(`openclaw config set gateway.auth.token "${gatewayToken}"`);

// Tailscale gateway mode (serve/funnel/off) — defaults to "serve" when
Expand Down
4 changes: 4 additions & 0 deletions packages/host-core/src/schema-derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ function deriveFieldSchema(field: CapabilityConfigField): z.ZodTypeAny {
}
break;
}
case "stringList": {
schema = z.array(z.string());
break;
}
default:
schema = z.string();
}
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export interface CapabilityMigration {
// ---------------------------------------------------------------------------

/** Field types supported by the config definition / TUI form. */
export type ConfigFieldType = "text" | "password" | "select" | "toggle";
export type ConfigFieldType = "text" | "password" | "select" | "toggle" | "stringList";

/**
* Recursive JSON Pointer paths for nested config objects.
Expand Down
6 changes: 3 additions & 3 deletions packages/types/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ const telegramChannel: ChannelDef = {
{
path: "allowFrom",
label: "Allow From",
type: "text",
type: "stringList",
placeholder: "user_id_1, user_id_2",
help: {
title: "Allowed Users",
lines: [
"Comma-separated Telegram user IDs allowed to DM the bot.",
"Leave empty to use pairing mode (approve via CLI).",
"Telegram user IDs allowed to DM the bot. JSON array in config files,",
"comma-separated in the TUI. Leave empty to use pairing mode (approve via CLI).",
],
},
},
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export const agentSchema = z.object({
skipOnboarding: z.boolean().optional(),
toolsProfile: z.string().optional(),
sandbox: z.boolean().optional(),
elevated: z
.object({
allowFrom: z.record(z.string(), z.array(z.union([z.string(), z.number()]))).optional(),
})
.optional(),
});

export const toolsSchema = z.record(
Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ export interface InstanceConfig {
agent?: {
skipOnboarding?: boolean;
toolsProfile?: string;
/** Sandbox agent execution. Default `false` (off) for trusted-operator setups. */
sandbox?: boolean;
/**
* Elevated tool access overrides. By default clawctl mirrors each
* channel's `allowFrom` into `tools.elevated.allowFrom.<channel>`,
* so the same IDs that can DM the bot can also trigger elevated
* surfaces. Set this to override the auto-derived defaults.
*/
elevated?: {
allowFrom?: Record<string, (string | number)[]>;
};
};

/** Model provider configuration. Required for full bootstrap. */
Expand Down
Loading
Loading