Merge remote-tracking branch 'upstream/main' into fix/handle-sensitive-stop-reason

This commit is contained in:
MoerAI 2026-03-20 10:34:46 +09:00
commit 927331297c
382 changed files with 19501 additions and 8665 deletions

View File

@ -0,0 +1,71 @@
---
name: openclaw-test-heap-leaks
description: Investigate `pnpm test` memory growth, Vitest worker OOMs, and suspicious RSS increases in OpenClaw using the `scripts/test-parallel.mjs` heap snapshot tooling. Use when Codex needs to reproduce test-lane memory growth, collect repeated `.heapsnapshot` files, compare snapshots from the same worker PID, distinguish transformed-module retention from real data leaks, and fix or reduce the impact by patching cleanup logic or isolating hotspot tests.
---
# OpenClaw Test Heap Leaks
Use this skill for test-memory investigations. Do not guess from RSS alone when heap snapshots are available.
## Workflow
1. Reproduce the failing shape first.
- Match the real entrypoint if possible. For Linux CI-style unit failures, start with:
- `pnpm canvas:a2ui:bundle && OPENCLAW_TEST_MEMORY_TRACE=1 OPENCLAW_TEST_HEAPSNAPSHOT_INTERVAL_MS=60000 OPENCLAW_TEST_HEAPSNAPSHOT_DIR=.tmp/heapsnap OPENCLAW_TEST_WORKERS=2 OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144 pnpm test`
- Keep `OPENCLAW_TEST_MEMORY_TRACE=1` enabled so the wrapper prints per-file RSS summaries alongside the snapshots.
- If the report is about a specific shard or worker budget, preserve that shape.
2. Wait for repeated snapshots before concluding anything.
- Take at least two intervals from the same lane.
- Compare snapshots from the same PID inside one lane directory such as `.tmp/heapsnap/unit-fast/`.
- Use `scripts/heapsnapshot-delta.mjs` to compare either two files directly or the earliest/latest pair per PID in one lane directory.
3. Classify the growth before choosing a fix.
- If growth is dominated by Vite/Vitest transformed source strings, `Module`, `system / Context`, bytecode, descriptor arrays, or property maps, treat it as retained module graph growth in long-lived workers.
- If growth is dominated by app objects, caches, buffers, server handles, timers, mock state, sqlite state, or similar runtime objects, treat it as a likely cleanup or lifecycle leak.
4. Fix the right layer.
- For retained transformed-module growth in shared workers:
- Move hotspot files out of `unit-fast` by updating `test/fixtures/test-parallel.behavior.json`.
- Prefer `singletonIsolated` for files that are safe alone but inflate shared worker heaps.
- If the file should already have been peeled out by timings but is absent from `test/fixtures/test-timings.unit.json`, call that out explicitly. Missing timings are a scheduling blind spot.
- For real leaks:
- Patch the implicated test or runtime cleanup path.
- Look for missing `afterEach`/`afterAll`, module-reset gaps, retained global state, unreleased DB handles, or listeners/timers that survive the file.
5. Verify with the most direct proof.
- Re-run the targeted lane or file with heap snapshots enabled if the suite still finishes in reasonable time.
- If snapshot overhead pushes tests over Vitest timeouts, fall back to the same lane without snapshots and confirm the RSS trend or OOM is reduced.
- For wrapper-only changes, at minimum verify the expected lanes start and the snapshot files are written.
## Heuristics
- Do not call everything a leak. In this repo, large `unit-fast` growth can be a worker-lifetime problem rather than an application object leak.
- `scripts/test-parallel.mjs` and `scripts/test-parallel-memory.mjs` are the primary control points for wrapper diagnostics.
- The lane names printed by `[test-parallel] start ...` and `[test-parallel][mem] summary ...` tell you where to focus.
- When one or two files account for most of the delta and they are missing from timings, reducing impact by isolating them is usually the first pragmatic fix.
- When the same retained object families grow across multiple intervals in the same worker PID, trust the snapshots over intuition.
## Snapshot Comparison
- Direct comparison:
- `node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs before.heapsnapshot after.heapsnapshot`
- Auto-select earliest/latest snapshots per PID within one lane:
- `node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs --lane-dir .tmp/heapsnap/unit-fast`
- Useful flags:
- `--top 40`
- `--min-kb 32`
- `--pid 16133`
Read the top positive deltas first. Large positive growth in module-transform artifacts suggests lane isolation; large positive growth in runtime objects suggests a real leak.
## Output Expectations
When using this skill, report:
- The exact reproduce command.
- Which lane and PID were compared.
- The dominant retained object families from the snapshot delta.
- Whether the issue is a real leak or shared-worker retained module growth.
- The concrete fix or impact-reduction patch.
- What you verified, and what snapshot overhead prevented you from verifying.

View File

@ -0,0 +1,4 @@
interface:
display_name: "Test Heap Leaks"
short_description: "Investigate test OOMs with heap snapshots"
default_prompt: "Use $openclaw-test-heap-leaks to investigate test memory growth with heap snapshots and reduce its impact."

View File

@ -0,0 +1,265 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
function printUsage() {
console.error(
"Usage: node heapsnapshot-delta.mjs <before.heapsnapshot> <after.heapsnapshot> [--top N] [--min-kb N]",
);
console.error(
" or: node heapsnapshot-delta.mjs --lane-dir <dir> [--pid PID] [--top N] [--min-kb N]",
);
}
function fail(message) {
console.error(message);
process.exit(1);
}
function parseArgs(argv) {
const options = {
top: 30,
minKb: 64,
laneDir: null,
pid: null,
files: [],
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--top") {
options.top = Number.parseInt(argv[index + 1] ?? "", 10);
index += 1;
continue;
}
if (arg === "--min-kb") {
options.minKb = Number.parseInt(argv[index + 1] ?? "", 10);
index += 1;
continue;
}
if (arg === "--lane-dir") {
options.laneDir = argv[index + 1] ?? null;
index += 1;
continue;
}
if (arg === "--pid") {
options.pid = Number.parseInt(argv[index + 1] ?? "", 10);
index += 1;
continue;
}
options.files.push(arg);
}
if (!Number.isFinite(options.top) || options.top <= 0) {
fail("--top must be a positive integer");
}
if (!Number.isFinite(options.minKb) || options.minKb < 0) {
fail("--min-kb must be a non-negative integer");
}
if (options.pid !== null && (!Number.isInteger(options.pid) || options.pid <= 0)) {
fail("--pid must be a positive integer");
}
return options;
}
function parseHeapFilename(filePath) {
const base = path.basename(filePath);
const match = base.match(
/^Heap\.(?<stamp>\d{8}\.\d{6})\.(?<pid>\d+)\.0\.(?<seq>\d+)\.heapsnapshot$/u,
);
if (!match?.groups) {
return null;
}
return {
filePath,
pid: Number.parseInt(match.groups.pid, 10),
stamp: match.groups.stamp,
sequence: Number.parseInt(match.groups.seq, 10),
};
}
function resolvePair(options) {
if (options.laneDir) {
const entries = fs
.readdirSync(options.laneDir)
.map((name) => parseHeapFilename(path.join(options.laneDir, name)))
.filter((entry) => entry !== null)
.filter((entry) => options.pid === null || entry.pid === options.pid)
.toSorted((left, right) => {
if (left.pid !== right.pid) {
return left.pid - right.pid;
}
if (left.stamp !== right.stamp) {
return left.stamp.localeCompare(right.stamp);
}
return left.sequence - right.sequence;
});
if (entries.length === 0) {
fail(`No matching heap snapshots found in ${options.laneDir}`);
}
const groups = new Map();
for (const entry of entries) {
const group = groups.get(entry.pid) ?? [];
group.push(entry);
groups.set(entry.pid, group);
}
const candidates = Array.from(groups.values())
.map((group) => ({
pid: group[0].pid,
before: group[0],
after: group.at(-1),
count: group.length,
}))
.filter((entry) => entry.count >= 2);
if (candidates.length === 0) {
fail(`Need at least two snapshots for one PID in ${options.laneDir}`);
}
const chosen =
options.pid !== null
? (candidates.find((entry) => entry.pid === options.pid) ?? null)
: candidates.toSorted((left, right) => right.count - left.count || left.pid - right.pid)[0];
if (!chosen) {
fail(`No PID with at least two snapshots matched in ${options.laneDir}`);
}
return {
before: chosen.before.filePath,
after: chosen.after.filePath,
pid: chosen.pid,
snapshotCount: chosen.count,
};
}
if (options.files.length !== 2) {
printUsage();
process.exit(1);
}
return {
before: options.files[0],
after: options.files[1],
pid: null,
snapshotCount: 2,
};
}
function loadSummary(filePath) {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
const meta = data.snapshot?.meta;
if (!meta) {
fail(`Invalid heap snapshot: ${filePath}`);
}
const nodeFieldCount = meta.node_fields.length;
const typeNames = meta.node_types[0];
const strings = data.strings;
const typeIndex = meta.node_fields.indexOf("type");
const nameIndex = meta.node_fields.indexOf("name");
const selfSizeIndex = meta.node_fields.indexOf("self_size");
const summary = new Map();
for (let offset = 0; offset < data.nodes.length; offset += nodeFieldCount) {
const type = typeNames[data.nodes[offset + typeIndex]];
const name = strings[data.nodes[offset + nameIndex]];
const selfSize = data.nodes[offset + selfSizeIndex];
const key = `${type}\t${name}`;
const current = summary.get(key) ?? {
type,
name,
selfSize: 0,
count: 0,
};
current.selfSize += selfSize;
current.count += 1;
summary.set(key, current);
}
return {
nodeCount: data.snapshot.node_count,
summary,
};
}
function formatBytes(bytes) {
if (Math.abs(bytes) >= 1024 ** 2) {
return `${(bytes / 1024 ** 2).toFixed(2)} MiB`;
}
if (Math.abs(bytes) >= 1024) {
return `${(bytes / 1024).toFixed(1)} KiB`;
}
return `${bytes} B`;
}
function formatDelta(bytes) {
return `${bytes >= 0 ? "+" : "-"}${formatBytes(Math.abs(bytes))}`;
}
function truncate(text, maxLength) {
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}`;
}
function main() {
const options = parseArgs(process.argv.slice(2));
const pair = resolvePair(options);
const before = loadSummary(pair.before);
const after = loadSummary(pair.after);
const minBytes = options.minKb * 1024;
const rows = [];
for (const [key, next] of after.summary) {
const previous = before.summary.get(key) ?? { selfSize: 0, count: 0 };
const sizeDelta = next.selfSize - previous.selfSize;
const countDelta = next.count - previous.count;
if (sizeDelta < minBytes) {
continue;
}
rows.push({
type: next.type,
name: next.name,
sizeDelta,
countDelta,
afterSize: next.selfSize,
afterCount: next.count,
});
}
rows.sort(
(left, right) => right.sizeDelta - left.sizeDelta || right.countDelta - left.countDelta,
);
console.log(`before: ${pair.before}`);
console.log(`after: ${pair.after}`);
if (pair.pid !== null) {
console.log(`pid: ${pair.pid} (${pair.snapshotCount} snapshots found)`);
}
console.log(
`nodes: ${before.nodeCount} -> ${after.nodeCount} (${after.nodeCount - before.nodeCount >= 0 ? "+" : ""}${after.nodeCount - before.nodeCount})`,
);
console.log(`filter: top=${options.top} min=${options.minKb} KiB`);
console.log("");
if (rows.length === 0) {
console.log("No entries exceeded the minimum delta.");
return;
}
for (const row of rows.slice(0, options.top)) {
console.log(
[
formatDelta(row.sizeDelta).padStart(11),
`count ${row.countDelta >= 0 ? "+" : ""}${row.countDelta}`.padStart(10),
row.type.padEnd(16),
truncate(row.name || "(empty)", 96),
].join(" "),
);
}
}
main();

View File

@ -1,7 +1,7 @@
.git
.worktrees
# Sensitive files docker-setup.sh writes .env with OPENCLAW_GATEWAY_TOKEN
# Sensitive files scripts/docker/setup.sh writes .env with OPENCLAW_GATEWAY_TOKEN
# into the project root; keep it out of the build context.
.env
.env.*

3
.github/labeler.yml vendored
View File

@ -165,7 +165,10 @@
- "Dockerfile.*"
- "docker-compose.yml"
- "docker-setup.sh"
- "setup-podman.sh"
- ".dockerignore"
- "scripts/docker/setup.sh"
- "scripts/podman/setup.sh"
- "scripts/**/*docker*"
- "scripts/**/Dockerfile*"
- "scripts/sandbox-*.sh"

View File

@ -496,7 +496,9 @@ jobs:
run: pnpm test
- name: Verify npm pack under Node 22
run: pnpm release:check
run: |
node scripts/stage-bundled-plugin-runtime-deps.mjs
node --import tsx scripts/release-check.ts
skills-python:
needs: [docs-scope, changed-scope]

View File

@ -62,9 +62,9 @@ jobs:
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
# This smoke validates that the build-arg path preinstalls selected
# extension deps and that matrix plugin discovery stays healthy in the
# final runtime image.
# This smoke validates that the build-arg path preinstalls the matrix
# runtime deps declared by the plugin and that matrix discovery stays
# healthy in the final runtime image.
- name: Build extension Dockerfile smoke image
uses: useblacksmith/build-push-action@v2
with:
@ -84,9 +84,17 @@ jobs:
openclaw --version &&
node -e "
const Module = require(\"node:module\");
const matrixPackage = require(\"/app/extensions/matrix/package.json\");
const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\");
requireFromMatrix.resolve(\"@vector-im/matrix-bot-sdk/package.json\");
requireFromMatrix.resolve(\"@matrix-org/matrix-sdk-crypto-nodejs/package.json\");
const runtimeDeps = Object.keys(matrixPackage.dependencies ?? {});
if (runtimeDeps.length === 0) {
throw new Error(
\"matrix package has no declared runtime dependencies; smoke cannot validate install mirroring\",
);
}
for (const dep of runtimeDeps) {
requireFromMatrix.resolve(dep);
}
const { spawnSync } = require(\"node:child_process\");
const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" });
if (run.status !== 0) {

View File

@ -70,15 +70,18 @@
- Format check: `pnpm format` (oxfmt --check)
- Format fix: `pnpm format:fix` (oxfmt --write)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
- Hard gate: before any commit, `pnpm check` MUST be run and MUST pass for the change being committed.
- Hard gate: before any push to `main`, `pnpm check` MUST be run and MUST pass, and `pnpm test` MUST be run and MUST pass.
- For narrowly scoped changes, prefer narrowly scoped tests that directly validate the touched behavior. If no meaningful scoped test exists, say so explicitly and use the next most direct validation available.
- Preferred landing bar for pushes to `main`: `pnpm check` and `pnpm test`, with a green result when feasible.
- Scoped tests prove the change itself. `pnpm test` remains the default `main` landing bar; scoped tests do not replace full-suite gates by default.
- Hard gate: if the change can affect build output, packaging, lazy-loading/module boundaries, or published surfaces, `pnpm build` MUST be run and MUST pass before pushing `main`.
- Hard gate: do not commit or push with failing format, lint, type, build, or required test checks.
- Default rule: do not commit or push with failing format, lint, type, build, or required test checks when those failures are caused by the change or plausibly related to the touched surface.
- For narrowly scoped changes, if unrelated failures already exist on latest `origin/main`, state that clearly, report the scoped tests you ran, and ask before broadening scope into unrelated fixes or landing despite those failures.
- Do not use scoped tests as permission to ignore plausibly related failures.
## Coding Style & Naming Conventions
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
- Formatting/linting via Oxlint and Oxfmt; run `pnpm check` before commits.
- Formatting/linting via Oxlint and Oxfmt.
- Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required.
- Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only.
- Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting.

View File

@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#46663) Fixes #40146. Thanks @Takhoffman.
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
- Onboarding/custom providers: store Azure OpenAI and Azure AI Foundry custom endpoints with the Responses API config shape, normalized `/openai/v1` base URLs, and Azure-safe defaults so TUI and agent runs work after setup. (#49543) Thanks @kunalk16.
- Docker/live tests: mount external CLI auth homes into writable container copies, derive Codex OAuth expiry from JWT `exp`, refresh synced CLI creds instead of trusting stale cached expiry, and make gateway live probes wait on transcript output so `pnpm test:docker:all` stays green in Linux.
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. (#46722) Thanks @Takhoffman.
- Control UI/logging: make browser-safe logger imports avoid eager temp-dir resolution so the bundled Control UI no longer crashes to a blank screen when logging reaches `tmp-openclaw-dir`. (#48469) Fixes #48062. Thanks @7inspire.
@ -117,6 +118,7 @@ Docs: https://docs.openclaw.ai
- Slack/startup: harden `@slack/bolt` import interop across current bundled runtime shapes so Slack monitors no longer crash with `App is not a constructor` after plugin-sdk bundling changes. (#45953) Thanks @merc1305.
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup. (#47601) Thanks @ngutman.
- Gateway/WS handshake: raise the default pre-auth handshake timeout to 10 seconds and add `OPENCLAW_HANDSHAKE_TIMEOUT_MS` as a runtime override so busy local gateways stop dropping healthy CLI connections at 3 seconds. (#49262) Thanks @fuller-stack-dev.
- Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement for Control UI operator sessions when `gateway.auth.mode=none`, so reverse-proxied dashboards no longer get stuck on `pairing required` despite auth being explicitly disabled. (#47148) Thanks @ademczuk.
- Control UI/model switching: preserve the selected provider prefix when switching models from the chat dropdown, so multi-provider setups no longer send `anthropic/gpt-5.2`-style mismatches when the user picked `openai/gpt-5.2`. (#47581) Thanks @chrishham.
- Control UI/storage: scope persisted settings keys by gateway base path, with migration from the legacy shared key, so multiple gateways under one domain stop overwriting each other's dashboard preferences. (#47932) Thanks @bobBot-claw.
@ -137,6 +139,7 @@ Docs: https://docs.openclaw.ai
- Discord: enforce strict DM component allowlist auth (#49997) Thanks @joshavant.
- Stabilize plugin loader and Docker extension smoke (#50058) Thanks @joshavant.
- Telegram: stabilize pairing/session/forum routing and reply formatting tests (#50155) Thanks @joshavant.
- Hardening: refresh stale device pairing requests and pending metadata (#50695) Thanks @smaeljaish771 and @joshavant.
### Fixes
@ -161,6 +164,10 @@ Docs: https://docs.openclaw.ai
- Matrix: make onboarding status runtime-safe (#49995) Thanks @joshavant.
- Channels: stabilize lane harness and monitor tests (#50167) Thanks @joshavant.
- WhatsApp/active-listener: pin the active listener registry to a `globalThis` singleton so split WhatsApp bundle chunks share one listener map and outbound sends stop missing the registered session. (#47433) Thanks @clawdia67.
- Plugins/WhatsApp: share split-load singleton state for plugin command registration and active WhatsApp listeners so duplicate module graphs no longer lose native plugin commands or outbound listener state. (#50418) Thanks @huntharo.
- Onboarding/custom providers: keep Azure AI Foundry `*.services.ai.azure.com` custom endpoints on the selected compatibility path instead of forcing Responses, so chat-completions Foundry models still work after setup. Fixes #50528. (#50535) Thanks @obviyus.
- Plugins/update: let `openclaw plugins update <npm-spec>` target tracked npm installs by dist-tag or exact version, and preserve the recorded npm spec for later id-based updates. (#49998) Thanks @huntharo.
- Tests/CLI: reduce command-secret gateway test import pressure while keeping the real protocol payload validator in place, so the isolated lane no longer carries the heavier runtime-web and message-channel graphs. (#50663) Thanks @huntharo.
### Breaking

View File

@ -49,7 +49,7 @@ Model note: while many providers/models are supported, for the best experience a
## Install (recommended)
Runtime: **Node 22**.
Runtime: **Node 24 (recommended) or Node 22.16+**.
```bash
npm install -g openclaw@latest
@ -62,7 +62,7 @@ OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so i
## Quick start (TL;DR)
Runtime: **Node 22**.
Runtime: **Node 24 (recommended) or Node 22.16+**.
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)

View File

@ -8,27 +8,85 @@ import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
class LocationHandler(
internal interface LocationDataSource {
fun hasFinePermission(context: Context): Boolean
fun hasCoarsePermission(context: Context): Boolean
suspend fun fetchLocation(
desiredProviders: List<String>,
maxAgeMs: Long?,
timeoutMs: Long,
isPrecise: Boolean,
): LocationCaptureManager.Payload
}
private class DefaultLocationDataSource(
private val capture: LocationCaptureManager,
) : LocationDataSource {
override fun hasFinePermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
override fun hasCoarsePermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
override suspend fun fetchLocation(
desiredProviders: List<String>,
maxAgeMs: Long?,
timeoutMs: Long,
isPrecise: Boolean,
): LocationCaptureManager.Payload =
capture.getLocation(
desiredProviders = desiredProviders,
maxAgeMs = maxAgeMs,
timeoutMs = timeoutMs,
isPrecise = isPrecise,
)
}
class LocationHandler private constructor(
private val appContext: Context,
private val location: LocationCaptureManager,
private val dataSource: LocationDataSource,
private val json: Json,
private val isForeground: () -> Boolean,
private val locationPreciseEnabled: () -> Boolean,
) {
fun hasFineLocationPermission(): Boolean {
return (
ContextCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
)
}
constructor(
appContext: Context,
location: LocationCaptureManager,
json: Json,
isForeground: () -> Boolean,
locationPreciseEnabled: () -> Boolean,
) : this(
appContext = appContext,
dataSource = DefaultLocationDataSource(location),
json = json,
isForeground = isForeground,
locationPreciseEnabled = locationPreciseEnabled,
)
fun hasCoarseLocationPermission(): Boolean {
return (
ContextCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
fun hasFineLocationPermission(): Boolean = dataSource.hasFinePermission(appContext)
fun hasCoarseLocationPermission(): Boolean = dataSource.hasCoarsePermission(appContext)
companion object {
internal fun forTesting(
appContext: Context,
dataSource: LocationDataSource,
json: Json = Json { ignoreUnknownKeys = true },
isForeground: () -> Boolean = { true },
locationPreciseEnabled: () -> Boolean = { true },
): LocationHandler =
LocationHandler(
appContext = appContext,
dataSource = dataSource,
json = json,
isForeground = isForeground,
locationPreciseEnabled = locationPreciseEnabled,
)
}
@ -39,7 +97,7 @@ class LocationHandler(
message = "LOCATION_BACKGROUND_UNAVAILABLE: location requires OpenClaw to stay open",
)
}
if (!hasFineLocationPermission() && !hasCoarseLocationPermission()) {
if (!dataSource.hasFinePermission(appContext) && !dataSource.hasCoarsePermission(appContext)) {
return GatewaySession.InvokeResult.error(
code = "LOCATION_PERMISSION_REQUIRED",
message = "LOCATION_PERMISSION_REQUIRED: grant Location permission",
@ -49,9 +107,9 @@ class LocationHandler(
val preciseEnabled = locationPreciseEnabled()
val accuracy =
when (desiredAccuracy) {
"precise" -> if (preciseEnabled && hasFineLocationPermission()) "precise" else "balanced"
"precise" -> if (preciseEnabled && dataSource.hasFinePermission(appContext)) "precise" else "balanced"
"coarse" -> "coarse"
else -> if (preciseEnabled && hasFineLocationPermission()) "precise" else "balanced"
else -> if (preciseEnabled && dataSource.hasFinePermission(appContext)) "precise" else "balanced"
}
val providers =
when (accuracy) {
@ -61,7 +119,7 @@ class LocationHandler(
}
try {
val payload =
location.getLocation(
dataSource.fetchLocation(
desiredProviders = providers,
maxAgeMs = maxAgeMs,
timeoutMs = timeoutMs,

View File

@ -0,0 +1,88 @@
package ai.openclaw.app.node
import android.content.Context
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class LocationHandlerTest : NodeHandlerRobolectricTest() {
@Test
fun handleLocationGet_requiresLocationPermissionWhenNeitherFineNorCoarse() =
runTest {
val handler =
LocationHandler.forTesting(
appContext = appContext(),
dataSource =
FakeLocationDataSource(
fineGranted = false,
coarseGranted = false,
),
)
val result = handler.handleLocationGet(null)
assertFalse(result.ok)
assertEquals("LOCATION_PERMISSION_REQUIRED", result.error?.code)
}
@Test
fun handleLocationGet_requiresForegroundBeforeLocationPermission() =
runTest {
val handler =
LocationHandler.forTesting(
appContext = appContext(),
dataSource =
FakeLocationDataSource(
fineGranted = true,
coarseGranted = true,
),
isForeground = { false },
)
val result = handler.handleLocationGet(null)
assertFalse(result.ok)
assertEquals("LOCATION_BACKGROUND_UNAVAILABLE", result.error?.code)
}
@Test
fun hasFineLocationPermission_reflectsDataSource() {
val denied =
LocationHandler.forTesting(
appContext = appContext(),
dataSource = FakeLocationDataSource(fineGranted = false, coarseGranted = true),
)
assertFalse(denied.hasFineLocationPermission())
assertTrue(denied.hasCoarseLocationPermission())
val granted =
LocationHandler.forTesting(
appContext = appContext(),
dataSource = FakeLocationDataSource(fineGranted = true, coarseGranted = false),
)
assertTrue(granted.hasFineLocationPermission())
assertFalse(granted.hasCoarseLocationPermission())
}
}
private class FakeLocationDataSource(
private val fineGranted: Boolean,
private val coarseGranted: Boolean,
) : LocationDataSource {
override fun hasFinePermission(context: Context): Boolean = fineGranted
override fun hasCoarsePermission(context: Context): Boolean = coarseGranted
override suspend fun fetchLocation(
desiredProviders: List<String>,
maxAgeMs: Long?,
timeoutMs: Long,
isPrecise: Boolean,
): LocationCaptureManager.Payload {
throw IllegalStateException(
"LocationHandlerTest: fetchLocation must not run in this scenario",
)
}
}

View File

@ -9,6 +9,7 @@ struct ExecApprovalEvaluation {
let env: [String: String]
let resolution: ExecCommandResolution?
let allowlistResolutions: [ExecCommandResolution]
let allowAlwaysPatterns: [String]
let allowlistMatches: [ExecAllowlistEntry]
let allowlistSatisfied: Bool
let allowlistMatch: ExecAllowlistEntry?
@ -31,9 +32,16 @@ enum ExecApprovalEvaluator {
let shellWrapper = ExecShellWrapperParser.extract(command: command, rawCommand: rawCommand).isWrapper
let env = HostEnvSanitizer.sanitize(overrides: envOverrides, shellWrapper: shellWrapper)
let displayCommand = ExecCommandFormatter.displayString(for: command, rawCommand: rawCommand)
let allowlistRawCommand = ExecSystemRunCommandValidator.allowlistEvaluationRawCommand(
command: command,
rawCommand: rawCommand)
let allowlistResolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: rawCommand,
rawCommand: allowlistRawCommand,
cwd: cwd,
env: env)
let allowAlwaysPatterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
command: command,
cwd: cwd,
env: env)
let allowlistMatches = security == .allowlist
@ -60,6 +68,7 @@ enum ExecApprovalEvaluator {
env: env,
resolution: allowlistResolutions.first,
allowlistResolutions: allowlistResolutions,
allowAlwaysPatterns: allowAlwaysPatterns,
allowlistMatches: allowlistMatches,
allowlistSatisfied: allowlistSatisfied,
allowlistMatch: allowlistSatisfied ? allowlistMatches.first : nil,

View File

@ -378,7 +378,7 @@ private enum ExecHostExecutor {
let context = await self.buildContext(
request: request,
command: validatedRequest.command,
rawCommand: validatedRequest.displayCommand)
rawCommand: validatedRequest.evaluationRawCommand)
switch ExecHostRequestEvaluator.evaluate(
context: context,
@ -476,13 +476,7 @@ private enum ExecHostExecutor {
{
guard decision == .allowAlways, context.security == .allowlist else { return }
var seenPatterns = Set<String>()
for candidate in context.allowlistResolutions {
guard let pattern = ExecApprovalHelpers.allowlistPattern(
command: context.command,
resolution: candidate)
else {
continue
}
for pattern in context.allowAlwaysPatterns {
if seenPatterns.insert(pattern).inserted {
ExecApprovalsStore.addAllowlistEntry(agentId: context.agentId, pattern: pattern)
}

View File

@ -52,6 +52,23 @@ struct ExecCommandResolution {
return [resolution]
}
static func resolveAllowAlwaysPatterns(
command: [String],
cwd: String?,
env: [String: String]?) -> [String]
{
var patterns: [String] = []
var seen = Set<String>()
self.collectAllowAlwaysPatterns(
command: command,
cwd: cwd,
env: env,
depth: 0,
patterns: &patterns,
seen: &seen)
return patterns
}
static func resolve(command: [String], cwd: String?, env: [String: String]?) -> ExecCommandResolution? {
let effective = ExecEnvInvocationUnwrapper.unwrapDispatchWrappersForResolution(command)
guard let raw = effective.first?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
@ -101,6 +118,115 @@ struct ExecCommandResolution {
return self.resolveExecutable(rawExecutable: raw, cwd: cwd, env: env)
}
private static func collectAllowAlwaysPatterns(
command: [String],
cwd: String?,
env: [String: String]?,
depth: Int,
patterns: inout [String],
seen: inout Set<String>)
{
guard depth < 3, !command.isEmpty else {
return
}
if let token0 = command.first?.trimmingCharacters(in: .whitespacesAndNewlines),
ExecCommandToken.basenameLower(token0) == "env",
let envUnwrapped = ExecEnvInvocationUnwrapper.unwrap(command),
!envUnwrapped.isEmpty
{
self.collectAllowAlwaysPatterns(
command: envUnwrapped,
cwd: cwd,
env: env,
depth: depth + 1,
patterns: &patterns,
seen: &seen)
return
}
if let shellMultiplexer = self.unwrapShellMultiplexerInvocation(command) {
self.collectAllowAlwaysPatterns(
command: shellMultiplexer,
cwd: cwd,
env: env,
depth: depth + 1,
patterns: &patterns,
seen: &seen)
return
}
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
if shell.isWrapper {
guard let shellCommand = shell.command,
let segments = self.splitShellCommandChain(shellCommand)
else {
return
}
for segment in segments {
let tokens = self.tokenizeShellWords(segment)
guard !tokens.isEmpty else {
continue
}
self.collectAllowAlwaysPatterns(
command: tokens,
cwd: cwd,
env: env,
depth: depth + 1,
patterns: &patterns,
seen: &seen)
}
return
}
guard let resolution = self.resolve(command: command, cwd: cwd, env: env),
let pattern = ExecApprovalHelpers.allowlistPattern(command: command, resolution: resolution),
seen.insert(pattern).inserted
else {
return
}
patterns.append(pattern)
}
private static func unwrapShellMultiplexerInvocation(_ argv: [String]) -> [String]? {
guard let token0 = argv.first?.trimmingCharacters(in: .whitespacesAndNewlines), !token0.isEmpty else {
return nil
}
let wrapper = ExecCommandToken.basenameLower(token0)
guard wrapper == "busybox" || wrapper == "toybox" else {
return nil
}
var appletIndex = 1
if appletIndex < argv.count, argv[appletIndex].trimmingCharacters(in: .whitespacesAndNewlines) == "--" {
appletIndex += 1
}
guard appletIndex < argv.count else {
return nil
}
let applet = argv[appletIndex].trimmingCharacters(in: .whitespacesAndNewlines)
guard !applet.isEmpty else {
return nil
}
let normalizedApplet = ExecCommandToken.basenameLower(applet)
let shellWrappers = Set([
"ash",
"bash",
"dash",
"fish",
"ksh",
"powershell",
"pwsh",
"sh",
"zsh",
])
guard shellWrappers.contains(normalizedApplet) else {
return nil
}
return Array(argv[appletIndex...])
}
private static func parseFirstToken(_ command: String) -> String? {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }

View File

@ -12,14 +12,24 @@ enum ExecCommandToken {
enum ExecEnvInvocationUnwrapper {
static let maxWrapperDepth = 4
struct UnwrapResult {
let command: [String]
let usesModifiers: Bool
}
private static func isEnvAssignment(_ token: String) -> Bool {
let pattern = #"^[A-Za-z_][A-Za-z0-9_]*=.*"#
return token.range(of: pattern, options: .regularExpression) != nil
}
static func unwrap(_ command: [String]) -> [String]? {
self.unwrapWithMetadata(command)?.command
}
static func unwrapWithMetadata(_ command: [String]) -> UnwrapResult? {
var idx = 1
var expectsOptionValue = false
var usesModifiers = false
while idx < command.count {
let token = command[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
@ -28,6 +38,7 @@ enum ExecEnvInvocationUnwrapper {
}
if expectsOptionValue {
expectsOptionValue = false
usesModifiers = true
idx += 1
continue
}
@ -36,6 +47,7 @@ enum ExecEnvInvocationUnwrapper {
break
}
if self.isEnvAssignment(token) {
usesModifiers = true
idx += 1
continue
}
@ -43,10 +55,12 @@ enum ExecEnvInvocationUnwrapper {
let lower = token.lowercased()
let flag = lower.split(separator: "=", maxSplits: 1).first.map(String.init) ?? lower
if ExecEnvOptions.flagOnly.contains(flag) {
usesModifiers = true
idx += 1
continue
}
if ExecEnvOptions.withValue.contains(flag) {
usesModifiers = true
if !lower.contains("=") {
expectsOptionValue = true
}
@ -63,6 +77,7 @@ enum ExecEnvInvocationUnwrapper {
lower.hasPrefix("--ignore-signal=") ||
lower.hasPrefix("--block-signal=")
{
usesModifiers = true
idx += 1
continue
}
@ -70,8 +85,8 @@ enum ExecEnvInvocationUnwrapper {
}
break
}
guard idx < command.count else { return nil }
return Array(command[idx...])
guard !expectsOptionValue, idx < command.count else { return nil }
return UnwrapResult(command: Array(command[idx...]), usesModifiers: usesModifiers)
}
static func unwrapDispatchWrappersForResolution(_ command: [String]) -> [String] {
@ -84,10 +99,13 @@ enum ExecEnvInvocationUnwrapper {
guard ExecCommandToken.basenameLower(token) == "env" else {
break
}
guard let unwrapped = self.unwrap(current), !unwrapped.isEmpty else {
guard let unwrapped = self.unwrapWithMetadata(current), !unwrapped.command.isEmpty else {
break
}
current = unwrapped
if unwrapped.usesModifiers {
break
}
current = unwrapped.command
depth += 1
}
return current

View File

@ -3,6 +3,7 @@ import Foundation
struct ExecHostValidatedRequest {
let command: [String]
let displayCommand: String
let evaluationRawCommand: String?
}
enum ExecHostPolicyDecision {
@ -27,7 +28,10 @@ enum ExecHostRequestEvaluator {
rawCommand: request.rawCommand)
switch validatedCommand {
case let .ok(resolved):
return .success(ExecHostValidatedRequest(command: command, displayCommand: resolved.displayCommand))
return .success(ExecHostValidatedRequest(
command: command,
displayCommand: resolved.displayCommand,
evaluationRawCommand: resolved.evaluationRawCommand))
case let .invalid(message):
return .failure(
ExecHostError(

View File

@ -3,6 +3,7 @@ import Foundation
enum ExecSystemRunCommandValidator {
struct ResolvedCommand {
let displayCommand: String
let evaluationRawCommand: String?
}
enum ValidationResult {
@ -52,18 +53,43 @@ enum ExecSystemRunCommandValidator {
let envManipulationBeforeShellWrapper = self.hasEnvManipulationBeforeShellWrapper(command)
let shellWrapperPositionalArgv = self.hasTrailingPositionalArgvAfterInlineCommand(command)
let mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv
let inferred: String = if let shellCommand, !mustBindDisplayToFullArgv {
let formattedArgv = ExecCommandFormatter.displayString(for: command)
let previewCommand: String? = if let shellCommand, !mustBindDisplayToFullArgv {
shellCommand
} else {
ExecCommandFormatter.displayString(for: command)
nil
}
if let raw = normalizedRaw, raw != inferred {
if let raw = normalizedRaw, raw != formattedArgv, raw != previewCommand {
return .invalid(message: "INVALID_REQUEST: rawCommand does not match command")
}
return .ok(ResolvedCommand(displayCommand: normalizedRaw ?? inferred))
return .ok(ResolvedCommand(
displayCommand: formattedArgv,
evaluationRawCommand: self.allowlistEvaluationRawCommand(
normalizedRaw: normalizedRaw,
shellIsWrapper: shell.isWrapper,
previewCommand: previewCommand)))
}
static func allowlistEvaluationRawCommand(command: [String], rawCommand: String?) -> String? {
let normalizedRaw = self.normalizeRaw(rawCommand)
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
let shellCommand = shell.isWrapper ? self.trimmedNonEmpty(shell.command) : nil
let envManipulationBeforeShellWrapper = self.hasEnvManipulationBeforeShellWrapper(command)
let shellWrapperPositionalArgv = self.hasTrailingPositionalArgvAfterInlineCommand(command)
let mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv
let previewCommand: String? = if let shellCommand, !mustBindDisplayToFullArgv {
shellCommand
} else {
nil
}
return self.allowlistEvaluationRawCommand(
normalizedRaw: normalizedRaw,
shellIsWrapper: shell.isWrapper,
previewCommand: previewCommand)
}
private static func normalizeRaw(_ rawCommand: String?) -> String? {
@ -76,6 +102,20 @@ enum ExecSystemRunCommandValidator {
return trimmed.isEmpty ? nil : trimmed
}
private static func allowlistEvaluationRawCommand(
normalizedRaw: String?,
shellIsWrapper: Bool,
previewCommand: String?) -> String?
{
guard shellIsWrapper else {
return normalizedRaw
}
guard let normalizedRaw else {
return nil
}
return normalizedRaw == previewCommand ? normalizedRaw : nil
}
private static func normalizeExecutableToken(_ token: String) -> String {
let base = ExecCommandToken.basenameLower(token)
if base.hasSuffix(".exe") {

View File

@ -507,8 +507,7 @@ actor MacNodeRuntime {
persistAllowlist: persistAllowlist,
security: evaluation.security,
agentId: evaluation.agentId,
command: command,
allowlistResolutions: evaluation.allowlistResolutions)
allowAlwaysPatterns: evaluation.allowAlwaysPatterns)
if evaluation.security == .allowlist, !evaluation.allowlistSatisfied, !evaluation.skillAllow, !approvedByAsk {
await self.emitExecEvent(
@ -795,15 +794,11 @@ extension MacNodeRuntime {
persistAllowlist: Bool,
security: ExecSecurity,
agentId: String?,
command: [String],
allowlistResolutions: [ExecCommandResolution])
allowAlwaysPatterns: [String])
{
guard persistAllowlist, security == .allowlist else { return }
var seenPatterns = Set<String>()
for candidate in allowlistResolutions {
guard let pattern = ExecApprovalHelpers.allowlistPattern(command: command, resolution: candidate) else {
continue
}
for pattern in allowAlwaysPatterns {
if seenPatterns.insert(pattern).inserted {
ExecApprovalsStore.addAllowlistEntry(agentId: agentId, pattern: pattern)
}

View File

@ -45,7 +45,7 @@ import Testing
let nodePath = tmp.appendingPathComponent("node_modules/.bin/node")
let scriptPath = tmp.appendingPathComponent("bin/openclaw.js")
try makeExecutableForTests(at: nodePath)
try "#!/bin/sh\necho v22.0.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
try "#!/bin/sh\necho v22.16.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
try makeExecutableForTests(at: scriptPath)

View File

@ -240,7 +240,7 @@ struct ExecAllowlistTests {
#expect(resolutions[0].executableName == "touch")
}
@Test func `resolve for allowlist unwraps env assignments inside shell segments`() {
@Test func `resolve for allowlist preserves env assignments inside shell segments`() {
let command = ["/bin/sh", "-lc", "env FOO=bar /usr/bin/touch /tmp/openclaw-allowlist-test"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
@ -248,11 +248,11 @@ struct ExecAllowlistTests {
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.count == 1)
#expect(resolutions[0].resolvedPath == "/usr/bin/touch")
#expect(resolutions[0].executableName == "touch")
#expect(resolutions[0].resolvedPath == "/usr/bin/env")
#expect(resolutions[0].executableName == "env")
}
@Test func `resolve for allowlist unwraps env to effective direct executable`() {
@Test func `resolve for allowlist preserves env wrapper with modifiers`() {
let command = ["/usr/bin/env", "FOO=bar", "/usr/bin/printf", "ok"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
@ -260,8 +260,33 @@ struct ExecAllowlistTests {
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.count == 1)
#expect(resolutions[0].resolvedPath == "/usr/bin/printf")
#expect(resolutions[0].executableName == "printf")
#expect(resolutions[0].resolvedPath == "/usr/bin/env")
#expect(resolutions[0].executableName == "env")
}
@Test func `approval evaluator resolves shell payload from canonical wrapper text`() async {
let command = ["/bin/sh", "-lc", "/usr/bin/printf ok"]
let rawCommand = "/bin/sh -lc \"/usr/bin/printf ok\""
let evaluation = await ExecApprovalEvaluator.evaluate(
command: command,
rawCommand: rawCommand,
cwd: nil,
envOverrides: ["PATH": "/usr/bin:/bin"],
agentId: nil)
#expect(evaluation.displayCommand == rawCommand)
#expect(evaluation.allowlistResolutions.count == 1)
#expect(evaluation.allowlistResolutions[0].resolvedPath == "/usr/bin/printf")
#expect(evaluation.allowlistResolutions[0].executableName == "printf")
}
@Test func `allow always patterns unwrap env wrapper modifiers to the inner executable`() {
let patterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
command: ["/usr/bin/env", "FOO=bar", "/usr/bin/printf", "ok"],
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(patterns == ["/usr/bin/printf"])
}
@Test func `match all requires every segment to match`() {

View File

@ -21,13 +21,12 @@ struct ExecApprovalsStoreRefactorTests {
try await self.withTempStateDir { _ in
_ = ExecApprovalsStore.ensureFile()
let url = ExecApprovalsStore.fileURL()
let firstWriteDate = try Self.modificationDate(at: url)
let firstIdentity = try Self.fileIdentity(at: url)
try await Task.sleep(nanoseconds: 1_100_000_000)
_ = ExecApprovalsStore.ensureFile()
let secondWriteDate = try Self.modificationDate(at: url)
let secondIdentity = try Self.fileIdentity(at: url)
#expect(firstWriteDate == secondWriteDate)
#expect(firstIdentity == secondIdentity)
}
}
@ -81,12 +80,12 @@ struct ExecApprovalsStoreRefactorTests {
}
}
private static func modificationDate(at url: URL) throws -> Date {
private static func fileIdentity(at url: URL) throws -> Int {
let attributes = try FileManager().attributesOfItem(atPath: url.path)
guard let date = attributes[.modificationDate] as? Date else {
struct MissingDateError: Error {}
throw MissingDateError()
guard let identifier = (attributes[.systemFileNumber] as? NSNumber)?.intValue else {
struct MissingIdentifierError: Error {}
throw MissingIdentifierError()
}
return date
return identifier
}
}

View File

@ -77,6 +77,7 @@ struct ExecHostRequestEvaluatorTests {
env: [:],
resolution: nil,
allowlistResolutions: [],
allowAlwaysPatterns: [],
allowlistMatches: [],
allowlistSatisfied: allowlistSatisfied,
allowlistMatch: nil,

View File

@ -50,6 +50,20 @@ struct ExecSystemRunCommandValidatorTests {
}
}
@Test func `validator keeps canonical wrapper text out of allowlist raw parsing`() {
let command = ["/bin/sh", "-lc", "/usr/bin/printf ok"]
let rawCommand = "/bin/sh -lc \"/usr/bin/printf ok\""
let result = ExecSystemRunCommandValidator.resolve(command: command, rawCommand: rawCommand)
switch result {
case let .ok(resolved):
#expect(resolved.displayCommand == rawCommand)
#expect(resolved.evaluationRawCommand == nil)
case let .invalid(message):
Issue.record("unexpected invalid result: \(message)")
}
}
private static func loadContractCases() throws -> [SystemRunCommandContractCase] {
let fixtureURL = try self.findContractFixtureURL()
let data = try Data(contentsOf: fixtureURL)

View File

@ -1,3 +0,0 @@
### Fixes
- Gateway/session history: return `404` for unknown session history lookups, unsubscribe session lifecycle listeners during shutdown, add coverage for the new transcript and lifecycle helpers, and tighten session history plus live transcript tests so the Control UI session surfaces stay stable under restart and follow mode.

View File

@ -16,7 +16,7 @@ services:
## Uncomment the lines below to enable sandbox isolation
## (agents.defaults.sandbox). Requires Docker CLI in the image
## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use
## docker-setup.sh with OPENCLAW_SANDBOX=1 for automated setup.
## scripts/docker/setup.sh with OPENCLAW_SANDBOX=1 for automated setup.
## Set DOCKER_GID to the host's docker group GID (run: stat -c '%g' /var/run/docker.sock).
# - /var/run/docker.sock:/var/run/docker.sock
# group_add:

View File

@ -2,615 +2,11 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml"
IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}"
EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}"
HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}"
RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}"
SANDBOX_ENABLED=""
DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}"
TIMEZONE="${OPENCLAW_TZ:-}"
SCRIPT_PATH="$ROOT_DIR/scripts/docker/setup.sh"
fail() {
echo "ERROR: $*" >&2
exit 1
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing dependency: $1" >&2
exit 1
fi
}
is_truthy_value() {
local raw="${1:-}"
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
case "$raw" in
1 | true | yes | on) return 0 ;;
*) return 1 ;;
esac
}
read_config_gateway_token() {
local config_path="$OPENCLAW_CONFIG_DIR/openclaw.json"
if [[ ! -f "$config_path" ]]; then
return 0
fi
if command -v python3 >/dev/null 2>&1; then
python3 - "$config_path" <<'PY'
import json
import sys
path = sys.argv[1]
try:
with open(path, "r", encoding="utf-8") as f:
cfg = json.load(f)
except Exception:
raise SystemExit(0)
gateway = cfg.get("gateway")
if not isinstance(gateway, dict):
raise SystemExit(0)
auth = gateway.get("auth")
if not isinstance(auth, dict):
raise SystemExit(0)
token = auth.get("token")
if isinstance(token, str):
token = token.strip()
if token:
print(token)
PY
return 0
fi
if command -v node >/dev/null 2>&1; then
node - "$config_path" <<'NODE'
const fs = require("node:fs");
const configPath = process.argv[2];
try {
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
const token = cfg?.gateway?.auth?.token;
if (typeof token === "string" && token.trim().length > 0) {
process.stdout.write(token.trim());
}
} catch {
// Keep docker-setup resilient when config parsing fails.
}
NODE
fi
}
read_env_gateway_token() {
local env_path="$1"
local line=""
local token=""
if [[ ! -f "$env_path" ]]; then
return 0
fi
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%$'\r'}"
if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then
token="${line#OPENCLAW_GATEWAY_TOKEN=}"
fi
done <"$env_path"
if [[ -n "$token" ]]; then
printf '%s' "$token"
fi
}
ensure_control_ui_allowed_origins() {
if [[ "${OPENCLAW_GATEWAY_BIND}" == "loopback" ]]; then
return 0
fi
local allowed_origin_json
local current_allowed_origins
allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")"
current_allowed_origins="$(
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
config get gateway.controlUi.allowedOrigins 2>/dev/null || true
)"
current_allowed_origins="${current_allowed_origins//$'\r'/}"
if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then
echo "Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged."
return 0
fi
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null
echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind."
}
sync_gateway_mode_and_bind() {
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
config set gateway.mode local >/dev/null
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null
echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup."
}
contains_disallowed_chars() {
local value="$1"
[[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]]
}
is_valid_timezone() {
local value="$1"
[[ -e "/usr/share/zoneinfo/$value" && ! -d "/usr/share/zoneinfo/$value" ]]
}
validate_mount_path_value() {
local label="$1"
local value="$2"
if [[ -z "$value" ]]; then
fail "$label cannot be empty."
fi
if contains_disallowed_chars "$value"; then
fail "$label contains unsupported control characters."
fi
if [[ "$value" =~ [[:space:]] ]]; then
fail "$label cannot contain whitespace."
fi
}
validate_named_volume() {
local value="$1"
if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume."
fi
}
validate_mount_spec() {
local mount="$1"
if contains_disallowed_chars "$mount"; then
fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters."
fi
# Keep mount specs strict to avoid YAML structure injection.
# Expected format: source:target[:options]
if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then
fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces."
fi
}
require_cmd docker
if ! docker compose version >/dev/null 2>&1; then
echo "Docker Compose not available (try: docker compose version)" >&2
if [[ ! -f "$SCRIPT_PATH" ]]; then
echo "Docker setup script not found at $SCRIPT_PATH" >&2
exit 1
fi
if [[ -z "$DOCKER_SOCKET_PATH" && "${DOCKER_HOST:-}" == unix://* ]]; then
DOCKER_SOCKET_PATH="${DOCKER_HOST#unix://}"
fi
if [[ -z "$DOCKER_SOCKET_PATH" ]]; then
DOCKER_SOCKET_PATH="/var/run/docker.sock"
fi
if is_truthy_value "$RAW_SANDBOX_SETTING"; then
SANDBOX_ENABLED="1"
fi
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR"
validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR"
if [[ -n "$HOME_VOLUME_NAME" ]]; then
if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then
validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME"
else
validate_named_volume "$HOME_VOLUME_NAME"
fi
fi
if contains_disallowed_chars "$EXTRA_MOUNTS"; then
fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters."
fi
if [[ -n "$SANDBOX_ENABLED" ]]; then
validate_mount_path_value "OPENCLAW_DOCKER_SOCKET" "$DOCKER_SOCKET_PATH"
fi
if [[ -n "$TIMEZONE" ]]; then
if contains_disallowed_chars "$TIMEZONE"; then
fail "OPENCLAW_TZ contains unsupported control characters."
fi
if [[ ! "$TIMEZONE" =~ ^[A-Za-z0-9/_+\-]+$ ]]; then
fail "OPENCLAW_TZ must be a valid IANA timezone string (e.g. Asia/Shanghai)."
fi
if ! is_valid_timezone "$TIMEZONE"; then
fail "OPENCLAW_TZ must match a timezone in /usr/share/zoneinfo (e.g. Asia/Shanghai)."
fi
fi
mkdir -p "$OPENCLAW_CONFIG_DIR"
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows
# where the container (even as root) cannot create new host subdirectories.
mkdir -p "$OPENCLAW_CONFIG_DIR/identity"
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent"
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions"
export OPENCLAW_CONFIG_DIR
export OPENCLAW_WORKSPACE_DIR
export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}"
export OPENCLAW_IMAGE="$IMAGE_NAME"
export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}"
export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}"
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}"
export OPENCLAW_SANDBOX="$SANDBOX_ENABLED"
export OPENCLAW_DOCKER_SOCKET="$DOCKER_SOCKET_PATH"
export OPENCLAW_TZ="$TIMEZONE"
# Detect Docker socket GID for sandbox group_add.
DOCKER_GID=""
if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then
DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || stat -f '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")"
fi
export DOCKER_GID
if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then
EXISTING_CONFIG_TOKEN="$(read_config_gateway_token || true)"
if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then
OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN"
echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json"
else
DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)"
if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then
OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN"
echo "Reusing gateway token from $ROOT_DIR/.env"
elif command -v openssl >/dev/null 2>&1; then
OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
else
OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY'
import secrets
print(secrets.token_hex(32))
PY
)"
fi
fi
fi
export OPENCLAW_GATEWAY_TOKEN
COMPOSE_FILES=("$COMPOSE_FILE")
COMPOSE_ARGS=()
write_extra_compose() {
local home_volume="$1"
shift
local mount
local gateway_home_mount
local gateway_config_mount
local gateway_workspace_mount
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
services:
openclaw-gateway:
volumes:
YAML
if [[ -n "$home_volume" ]]; then
gateway_home_mount="${home_volume}:/home/node"
gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw"
gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace"
validate_mount_spec "$gateway_home_mount"
validate_mount_spec "$gateway_config_mount"
validate_mount_spec "$gateway_workspace_mount"
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
fi
for mount in "$@"; do
validate_mount_spec "$mount"
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
done
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
openclaw-cli:
volumes:
YAML
if [[ -n "$home_volume" ]]; then
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
fi
for mount in "$@"; do
validate_mount_spec "$mount"
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
done
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
validate_named_volume "$home_volume"
cat >>"$EXTRA_COMPOSE_FILE" <<YAML
volumes:
${home_volume}:
YAML
fi
}
# When sandbox is requested, ensure Docker CLI build arg is set for local builds.
# Docker socket mount is deferred until sandbox prerequisites are verified.
if [[ -n "$SANDBOX_ENABLED" ]]; then
if [[ -z "${OPENCLAW_INSTALL_DOCKER_CLI:-}" ]]; then
export OPENCLAW_INSTALL_DOCKER_CLI=1
fi
fi
VALID_MOUNTS=()
if [[ -n "$EXTRA_MOUNTS" ]]; then
IFS=',' read -r -a mounts <<<"$EXTRA_MOUNTS"
for mount in "${mounts[@]}"; do
mount="${mount#"${mount%%[![:space:]]*}"}"
mount="${mount%"${mount##*[![:space:]]}"}"
if [[ -n "$mount" ]]; then
VALID_MOUNTS+=("$mount")
fi
done
fi
if [[ -n "$HOME_VOLUME_NAME" || ${#VALID_MOUNTS[@]} -gt 0 ]]; then
# Bash 3.2 + nounset treats "${array[@]}" on an empty array as unbound.
if [[ ${#VALID_MOUNTS[@]} -gt 0 ]]; then
write_extra_compose "$HOME_VOLUME_NAME" "${VALID_MOUNTS[@]}"
else
write_extra_compose "$HOME_VOLUME_NAME"
fi
COMPOSE_FILES+=("$EXTRA_COMPOSE_FILE")
fi
for compose_file in "${COMPOSE_FILES[@]}"; do
COMPOSE_ARGS+=("-f" "$compose_file")
done
# Keep a base compose arg set without sandbox overlay so rollback paths can
# force a known-safe gateway service definition (no docker.sock mount).
BASE_COMPOSE_ARGS=("${COMPOSE_ARGS[@]}")
COMPOSE_HINT="docker compose"
for compose_file in "${COMPOSE_FILES[@]}"; do
COMPOSE_HINT+=" -f ${compose_file}"
done
ENV_FILE="$ROOT_DIR/.env"
upsert_env() {
local file="$1"
shift
local -a keys=("$@")
local tmp
tmp="$(mktemp)"
# Use a delimited string instead of an associative array so the script
# works with Bash 3.2 (macOS default) which lacks `declare -A`.
local seen=" "
if [[ -f "$file" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
local key="${line%%=*}"
local replaced=false
for k in "${keys[@]}"; do
if [[ "$key" == "$k" ]]; then
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
seen="$seen$k "
replaced=true
break
fi
done
if [[ "$replaced" == false ]]; then
printf '%s\n' "$line" >>"$tmp"
fi
done <"$file"
fi
for k in "${keys[@]}"; do
if [[ "$seen" != *" $k "* ]]; then
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
fi
done
mv "$tmp" "$file"
}
upsert_env "$ENV_FILE" \
OPENCLAW_CONFIG_DIR \
OPENCLAW_WORKSPACE_DIR \
OPENCLAW_GATEWAY_PORT \
OPENCLAW_BRIDGE_PORT \
OPENCLAW_GATEWAY_BIND \
OPENCLAW_GATEWAY_TOKEN \
OPENCLAW_IMAGE \
OPENCLAW_EXTRA_MOUNTS \
OPENCLAW_HOME_VOLUME \
OPENCLAW_DOCKER_APT_PACKAGES \
OPENCLAW_EXTENSIONS \
OPENCLAW_SANDBOX \
OPENCLAW_DOCKER_SOCKET \
DOCKER_GID \
OPENCLAW_INSTALL_DOCKER_CLI \
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \
OPENCLAW_TZ
if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
echo "==> Building Docker image: $IMAGE_NAME"
docker build \
--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \
--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
-t "$IMAGE_NAME" \
-f "$ROOT_DIR/Dockerfile" \
"$ROOT_DIR"
else
echo "==> Pulling Docker image: $IMAGE_NAME"
if ! docker pull "$IMAGE_NAME"; then
echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2
exit 1
fi
fi
# Ensure bind-mounted data directories are writable by the container's `node`
# user (uid 1000). Host-created dirs inherit the host user's uid which may
# differ, causing EACCES when the container tries to mkdir/write.
# Running a brief root container to chown is the portable Docker idiom --
# it works regardless of the host uid and doesn't require host-side root.
echo ""
echo "==> Fixing data-directory permissions"
# Use -xdev to restrict chown to the config-dir mount only — without it,
# the recursive chown would cross into the workspace bind mount and rewrite
# ownership of all user project files on Linux hosts.
# After fixing the config dir, only the OpenClaw metadata subdirectory
# (.openclaw/) inside the workspace gets chowned, not the user's project files.
docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh openclaw-cli -c \
'find /home/node/.openclaw -xdev -exec chown node:node {} +; \
[ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true'
echo ""
echo "==> Onboarding (interactive)"
echo "Docker setup pins Gateway mode to local."
echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)."
echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND"
echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN"
echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)."
echo "Install Gateway daemon: No (managed by Docker Compose)"
echo ""
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --mode local --no-install-daemon
echo ""
echo "==> Docker gateway defaults"
sync_gateway_mode_and_bind
echo ""
echo "==> Control UI origin allowlist"
ensure_control_ui_allowed_origins
echo ""
echo "==> Provider setup (optional)"
echo "WhatsApp (QR):"
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login"
echo "Telegram (bot token):"
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token <token>"
echo "Discord (bot token):"
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token <token>"
echo "Docs: https://docs.openclaw.ai/channels"
echo ""
echo "==> Starting gateway"
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) ---
if [[ -n "$SANDBOX_ENABLED" ]]; then
echo ""
echo "==> Sandbox setup"
# Build sandbox image if Dockerfile.sandbox exists.
if [[ -f "$ROOT_DIR/Dockerfile.sandbox" ]]; then
echo "Building sandbox image: openclaw-sandbox:bookworm-slim"
docker build \
-t "openclaw-sandbox:bookworm-slim" \
-f "$ROOT_DIR/Dockerfile.sandbox" \
"$ROOT_DIR"
else
echo "WARNING: Dockerfile.sandbox not found in $ROOT_DIR" >&2
echo " Sandbox config will be applied but no sandbox image will be built." >&2
echo " Agent exec may fail if the configured sandbox image does not exist." >&2
fi
# Defense-in-depth: verify Docker CLI in the running image before enabling
# sandbox. This avoids claiming sandbox is enabled when the image cannot
# launch sandbox containers.
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then
echo "WARNING: Docker CLI not found inside the container image." >&2
echo " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2
echo " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2
SANDBOX_ENABLED=""
fi
fi
# Apply sandbox config only if prerequisites are met.
if [[ -n "$SANDBOX_ENABLED" ]]; then
# Mount Docker socket via a dedicated compose overlay. This overlay is
# created only after sandbox prerequisites pass, so the socket is never
# exposed when sandbox cannot actually run.
if [[ -S "$DOCKER_SOCKET_PATH" ]]; then
SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml"
cat >"$SANDBOX_COMPOSE_FILE" <<YAML
services:
openclaw-gateway:
volumes:
- ${DOCKER_SOCKET_PATH}:/var/run/docker.sock
YAML
if [[ -n "${DOCKER_GID:-}" ]]; then
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
group_add:
- "${DOCKER_GID}"
YAML
fi
COMPOSE_ARGS+=("-f" "$SANDBOX_COMPOSE_FILE")
echo "==> Sandbox: added Docker socket mount"
else
echo "WARNING: OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2
echo " Sandbox requires Docker socket access. Skipping sandbox setup." >&2
SANDBOX_ENABLED=""
fi
fi
if [[ -n "$SANDBOX_ENABLED" ]]; then
# Enable sandbox in OpenClaw config.
sandbox_config_ok=true
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
config set agents.defaults.sandbox.mode "non-main" >/dev/null; then
echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2
sandbox_config_ok=false
fi
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
config set agents.defaults.sandbox.scope "agent" >/dev/null; then
echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2
sandbox_config_ok=false
fi
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then
echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2
sandbox_config_ok=false
fi
if [[ "$sandbox_config_ok" == true ]]; then
echo "Sandbox enabled: mode=non-main, scope=agent, workspaceAccess=none"
echo "Docs: https://docs.openclaw.ai/gateway/sandboxing"
# Restart gateway with sandbox compose overlay to pick up socket mount + config.
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
else
echo "WARNING: Sandbox config was partially applied. Check errors above." >&2
echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2
if ! docker compose "${BASE_COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
config set agents.defaults.sandbox.mode "off" >/dev/null; then
echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2
else
echo "Sandbox mode rolled back to off due to partial sandbox config failure."
fi
if [[ -n "${SANDBOX_COMPOSE_FILE:-}" ]]; then
rm -f "$SANDBOX_COMPOSE_FILE"
fi
# Ensure gateway service definition is reset without sandbox overlay mount.
docker compose "${BASE_COMPOSE_ARGS[@]}" up -d --force-recreate openclaw-gateway
fi
else
# Keep reruns deterministic: if sandbox is not active for this run, reset
# persisted sandbox mode so future execs do not require docker.sock by stale
# config alone.
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
config set agents.defaults.sandbox.mode "off" >/dev/null; then
echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2
fi
if [[ -f "$ROOT_DIR/docker-compose.sandbox.yml" ]]; then
rm -f "$ROOT_DIR/docker-compose.sandbox.yml"
fi
fi
echo ""
echo "Gateway running with host port mapping."
echo "Access from tailnet devices via the host's tailnet IP."
echo "Config: $OPENCLAW_CONFIG_DIR"
echo "Workspace: $OPENCLAW_WORKSPACE_DIR"
echo "Token: $OPENCLAW_GATEWAY_TOKEN"
echo ""
echo "Commands:"
echo " ${COMPOSE_HINT} logs -f openclaw-gateway"
echo " ${COMPOSE_HINT} exec openclaw-gateway node dist/index.js health --token \"$OPENCLAW_GATEWAY_TOKEN\""
exec "$SCRIPT_PATH" "$@"

View File

@ -1046,4 +1046,4 @@ node -e "import('./path/to/handler.ts').then(console.log)"
- [CLI Reference: hooks](/cli/hooks)
- [Bundled Hooks README](https://github.com/openclaw/openclaw/tree/main/src/hooks/bundled)
- [Webhook Hooks](/automation/webhook)
- [Configuration](/gateway/configuration#hooks)
- [Configuration](/gateway/configuration-reference#hooks)

View File

@ -116,7 +116,7 @@ Want “groups can only see folder X” instead of “no host access”? Keep `w
Related:
- Configuration keys and defaults: [Gateway configuration](/gateway/configuration#agentsdefaultssandbox)
- Configuration keys and defaults: [Gateway configuration](/gateway/configuration-reference#agents-defaults-sandbox)
- Debugging why a tool is blocked: [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)
- Bind mounts details: [Sandboxing](/gateway/sandboxing#custom-bind-mounts)

View File

@ -1,6 +1,5 @@
---
title: IRC
description: Connect OpenClaw to IRC channels and direct messages.
summary: "IRC plugin setup, access controls, and troubleshooting"
read_when:
- You want to connect OpenClaw to IRC channels or DMs
@ -17,18 +16,18 @@ IRC ships as an extension plugin, but it is configured in the main config under
1. Enable IRC config in `~/.openclaw/openclaw.json`.
2. Set at least:
```json
```json5
{
"channels": {
"irc": {
"enabled": true,
"host": "irc.libera.chat",
"port": 6697,
"tls": true,
"nick": "openclaw-bot",
"channels": ["#openclaw"]
}
}
channels: {
irc: {
enabled: true,
host: "irc.libera.chat",
port: 6697,
tls: true,
nick: "openclaw-bot",
channels: ["#openclaw"],
},
},
}
```
@ -75,7 +74,7 @@ If you see logs like:
Example (allow anyone in `#tuirc-dev` to talk to the bot):
```json5
```json55
{
channels: {
irc: {
@ -96,7 +95,7 @@ That means you may see logs like `drop channel … (missing-mention)` unless the
To make the bot reply in an IRC channel **without needing a mention**, disable mention gating for that channel:
```json5
```json55
{
channels: {
irc: {
@ -114,7 +113,7 @@ To make the bot reply in an IRC channel **without needing a mention**, disable m
Or to allow **all** IRC channels (no per-channel allowlist) and still reply without mentions:
```json5
```json55
{
channels: {
irc: {
@ -134,7 +133,7 @@ To reduce risk, restrict tools for that channel.
### Same tools for everyone in the channel
```json5
```json55
{
channels: {
irc: {
@ -155,7 +154,7 @@ To reduce risk, restrict tools for that channel.
Use `toolsBySender` to apply a stricter policy to `"*"` and a looser one to your nick:
```json5
```json55
{
channels: {
irc: {
@ -190,32 +189,32 @@ For more on group access vs mention-gating (and how they interact), see: [/chann
To identify with NickServ after connect:
```json
```json5
{
"channels": {
"irc": {
"nickserv": {
"enabled": true,
"service": "NickServ",
"password": "your-nickserv-password"
}
}
}
channels: {
irc: {
nickserv: {
enabled: true,
service: "NickServ",
password: "your-nickserv-password",
},
},
},
}
```
Optional one-time registration on connect:
```json
```json5
{
"channels": {
"irc": {
"nickserv": {
"register": true,
"registerEmail": "bot@example.com"
}
}
}
channels: {
irc: {
nickserv: {
register: true,
registerEmail: "bot@example.com",
},
},
},
}
```

View File

@ -204,6 +204,8 @@ Bootstrap cross-signing and verification state:
openclaw matrix verify bootstrap
```
Multi-account support: use `channels.matrix.accounts` with per-account credentials and optional `name`. See [Configuration reference](/gateway/configuration-reference#multi-account-all-channels) for the shared pattern.
Verbose bootstrap diagnostics:
```bash
@ -372,7 +374,7 @@ Planned improvement:
## Automatic verification notices
Matrix now posts verification lifecycle notices directly into the Matrix room as `m.notice` messages.
Matrix now posts verification lifecycle notices directly into the strict DM verification room as `m.notice` messages.
That includes:
- verification request notices
@ -381,7 +383,8 @@ That includes:
- SAS details (emoji and decimal) when available
Incoming verification requests from another Matrix client are tracked and auto-accepted by OpenClaw.
When SAS emoji verification becomes available, OpenClaw starts that SAS flow automatically for inbound requests and confirms its own side.
For self-verification flows, OpenClaw also starts the SAS flow automatically when emoji verification becomes available and confirms its own side.
For verification requests from another Matrix user/device, OpenClaw auto-accepts the request and then waits for the SAS flow to proceed normally.
You still need to compare the emoji or decimal SAS in your Matrix client and confirm "They match" there to complete the verification.
OpenClaw does not auto-accept self-initiated duplicate flows blindly. Startup skips creating a new request when a self-verification request is already pending.

View File

@ -260,15 +260,17 @@ This is often easier than hand-editing JSON manifests.
4. **Configure OpenClaw**
```json
```json5
{
"msteams": {
"enabled": true,
"appId": "<APP_ID>",
"appPassword": "<APP_PASSWORD>",
"tenantId": "<TENANT_ID>",
"webhook": { "port": 3978, "path": "/api/messages" }
}
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
appPassword: "<APP_PASSWORD>",
tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
```
@ -312,49 +314,49 @@ These are the **existing resourceSpecific permissions** in our Teams app manifes
Minimal, valid example with the required fields. Replace IDs and URLs.
```json
```json5
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
"manifestVersion": "1.23",
"version": "1.0.0",
"id": "00000000-0000-0000-0000-000000000000",
"name": { "short": "OpenClaw" },
"developer": {
"name": "Your Org",
"websiteUrl": "https://example.com",
"privacyUrl": "https://example.com/privacy",
"termsOfUseUrl": "https://example.com/terms"
$schema: "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
manifestVersion: "1.23",
version: "1.0.0",
id: "00000000-0000-0000-0000-000000000000",
name: { short: "OpenClaw" },
developer: {
name: "Your Org",
websiteUrl: "https://example.com",
privacyUrl: "https://example.com/privacy",
termsOfUseUrl: "https://example.com/terms",
},
"description": { "short": "OpenClaw in Teams", "full": "OpenClaw in Teams" },
"icons": { "outline": "outline.png", "color": "color.png" },
"accentColor": "#5B6DEF",
"bots": [
description: { short: "OpenClaw in Teams", full: "OpenClaw in Teams" },
icons: { outline: "outline.png", color: "color.png" },
accentColor: "#5B6DEF",
bots: [
{
"botId": "11111111-1111-1111-1111-111111111111",
"scopes": ["personal", "team", "groupChat"],
"isNotificationOnly": false,
"supportsCalling": false,
"supportsVideo": false,
"supportsFiles": true
}
botId: "11111111-1111-1111-1111-111111111111",
scopes: ["personal", "team", "groupChat"],
isNotificationOnly: false,
supportsCalling: false,
supportsVideo: false,
supportsFiles: true,
},
],
"webApplicationInfo": {
"id": "11111111-1111-1111-1111-111111111111"
webApplicationInfo: {
id: "11111111-1111-1111-1111-111111111111",
},
authorization: {
permissions: {
resourceSpecific: [
{ name: "ChannelMessage.Read.Group", type: "Application" },
{ name: "ChannelMessage.Send.Group", type: "Application" },
{ name: "Member.Read.Group", type: "Application" },
{ name: "Owner.Read.Group", type: "Application" },
{ name: "ChannelSettings.Read.Group", type: "Application" },
{ name: "TeamMember.Read.Group", type: "Application" },
{ name: "TeamSettings.Read.Group", type: "Application" },
{ name: "ChatMessage.Read.Chat", type: "Application" },
],
},
},
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "ChannelMessage.Read.Group", "type": "Application" },
{ "name": "ChannelMessage.Send.Group", "type": "Application" },
{ "name": "Member.Read.Group", "type": "Application" },
{ "name": "Owner.Read.Group", "type": "Application" },
{ "name": "ChannelSettings.Read.Group", "type": "Application" },
{ "name": "TeamMember.Read.Group", "type": "Application" },
{ "name": "TeamSettings.Read.Group", "type": "Application" },
{ "name": "ChatMessage.Read.Chat", "type": "Application" }
]
}
}
}
```
@ -500,20 +502,22 @@ Teams recently introduced two channel UI styles over the same underlying data mo
**Solution:** Configure `replyStyle` per-channel based on how the channel is set up:
```json
```json5
{
"msteams": {
"replyStyle": "thread",
"teams": {
"19:abc...@thread.tacv2": {
"channels": {
"19:xyz...@thread.tacv2": {
"replyStyle": "top-level"
}
}
}
}
}
channels: {
msteams: {
replyStyle: "thread",
teams: {
"19:abc...@thread.tacv2": {
channels: {
"19:xyz...@thread.tacv2": {
replyStyle: "top-level",
},
},
},
},
},
},
}
```
@ -616,16 +620,16 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid
**Agent tool:**
```json
```json5
{
"action": "send",
"channel": "msteams",
"target": "user:<id>",
"card": {
"type": "AdaptiveCard",
"version": "1.5",
"body": [{ "type": "TextBlock", "text": "Hello!" }]
}
action: "send",
channel: "msteams",
target: "user:<id>",
card: {
type: "AdaptiveCard",
version: "1.5",
body: [{ type: "TextBlock", text: "Hello!" }],
},
}
```
@ -669,25 +673,25 @@ openclaw message send --channel msteams --target "conversation:19:abc...@thread.
**Agent tool examples:**
```json
```json5
{
"action": "send",
"channel": "msteams",
"target": "user:John Smith",
"message": "Hello!"
action: "send",
channel: "msteams",
target: "user:John Smith",
message: "Hello!",
}
```
```json
```json5
{
"action": "send",
"channel": "msteams",
"target": "conversation:19:abc...@thread.tacv2",
"card": {
"type": "AdaptiveCard",
"version": "1.5",
"body": [{ "type": "TextBlock", "text": "Hello" }]
}
action: "send",
channel: "msteams",
target: "conversation:19:abc...@thread.tacv2",
card: {
type: "AdaptiveCard",
version: "1.5",
body: [{ type: "TextBlock", text: "Hello" }],
},
}
```

View File

@ -60,13 +60,13 @@ nak key generate
2. Add to config:
```json
```json5
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}"
}
}
channels: {
nostr: {
privateKey: "${NOSTR_PRIVATE_KEY}",
},
},
}
```
@ -96,23 +96,23 @@ Profile data is published as a NIP-01 `kind:0` event. You can manage it from the
Example:
```json
```json5
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"profile": {
"name": "openclaw",
"displayName": "OpenClaw",
"about": "Personal assistant DM bot",
"picture": "https://example.com/avatar.png",
"banner": "https://example.com/banner.png",
"website": "https://example.com",
"nip05": "openclaw@example.com",
"lud16": "openclaw@example.com"
}
}
}
channels: {
nostr: {
privateKey: "${NOSTR_PRIVATE_KEY}",
profile: {
name: "openclaw",
displayName: "OpenClaw",
about: "Personal assistant DM bot",
picture: "https://example.com/avatar.png",
banner: "https://example.com/banner.png",
website: "https://example.com",
nip05: "openclaw@example.com",
lud16: "openclaw@example.com",
},
},
},
}
```
@ -132,15 +132,15 @@ Notes:
### Allowlist example
```json
```json5
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"dmPolicy": "allowlist",
"allowFrom": ["npub1abc...", "npub1xyz..."]
}
}
channels: {
nostr: {
privateKey: "${NOSTR_PRIVATE_KEY}",
dmPolicy: "allowlist",
allowFrom: ["npub1abc...", "npub1xyz..."],
},
},
}
```
@ -155,14 +155,14 @@ Accepted formats:
Defaults: `relay.damus.io` and `nos.lol`.
```json
```json5
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"relays": ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nostr.wine"]
}
}
channels: {
nostr: {
privateKey: "${NOSTR_PRIVATE_KEY}",
relays: ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nostr.wine"],
},
},
}
```
@ -191,14 +191,14 @@ Tips:
docker run -p 7777:7777 ghcr.io/hoytech/strfry
```
```json
```json5
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"relays": ["ws://localhost:7777"]
}
}
channels: {
nostr: {
privateKey: "${NOSTR_PRIVATE_KEY}",
relays: ["ws://localhost:7777"],
},
},
}
```

View File

@ -67,7 +67,7 @@ If you use the `device-pair` plugin, you can do first-time device pairing entire
2. The bot replies with two messages: an instruction message and a separate **setup code** message (easy to copy/paste in Telegram).
3. On your phone, open the OpenClaw iOS app → Settings → Gateway.
4. Paste the setup code and connect.
5. Back in Telegram: `/pair approve`
5. Back in Telegram: `/pair pending` (review request IDs, role, and scopes), then approve.
The setup code is a base64-encoded JSON payload that contains:
@ -84,6 +84,10 @@ openclaw devices approve <requestId>
openclaw devices reject <requestId>
```
If the same device retries with different auth details (for example different
role/scopes/public key), the previous pending request is superseded and a new
`requestId` is created.
### Node pairing state storage
Stored under `~/.openclaw/devices/`:

View File

@ -99,7 +99,7 @@ Example:
}
```
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration-reference#multi-account-all-channels) for the shared pattern.
## Setup path B: register dedicated bot number (SMS, Linux)

View File

@ -346,7 +346,13 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
1. `/pair` generates setup code
2. paste code in iOS app
3. `/pair approve` approves latest pending request
3. `/pair pending` lists pending requests (including role/scopes)
4. approve the request:
- `/pair approve <requestId>` for explicit approval
- `/pair approve` when there is only one pending request
- `/pair approve latest` for most recent
If a device retries with changed auth details (for example role/scopes/public key), the previous pending request is superseded and the new request uses a different `requestId`. Re-run `/pair pending` before approving.
More details: [Pairing](/channels/pairing#pair-via-telegram-recommended-for-ios).

View File

@ -38,7 +38,7 @@ Healthy baseline:
| Group messages ignored | Check `requireMention` + mention patterns in config | Mention the bot or relax mention policy for that group. |
| Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Re-login and verify credentials directory is healthy. |
Full troubleshooting: [/channels/whatsapp#troubleshooting-quick](/channels/whatsapp#troubleshooting-quick)
Full troubleshooting: [/channels/whatsapp#troubleshooting](/channels/whatsapp#troubleshooting)
## Telegram
@ -90,7 +90,7 @@ Full troubleshooting: [/channels/slack#troubleshooting](/channels/slack#troubles
Full troubleshooting:
- [/channels/imessage#troubleshooting-macos-privacy-and-security-tcc](/channels/imessage#troubleshooting-macos-privacy-and-security-tcc)
- [/channels/imessage#troubleshooting](/channels/imessage#troubleshooting)
- [/channels/bluebubbles#troubleshooting](/channels/bluebubbles#troubleshooting)
## Signal

View File

@ -123,7 +123,7 @@ Prefer `allowFrom` for a hard allowlist. Use `allowedRoles` instead if you want
**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent.
Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/) (Convert your Twitch username to ID)
Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/) (Convert your Twitch username to ID)
## Token refresh (optional)

View File

@ -1,6 +1,5 @@
---
title: CI Pipeline
description: How the OpenClaw CI pipeline works
summary: "CI job graph, scope gates, and local command equivalents"
read_when:
- You need to understand why a CI job did or did not run

View File

@ -21,6 +21,9 @@ openclaw devices list
openclaw devices list --json
```
Pending request output includes the requested role and scopes so approvals can
be reviewed before you approve.
### `openclaw devices remove <deviceId>`
Remove one paired device entry.
@ -45,6 +48,11 @@ openclaw devices clear --yes --pending --json
Approve a pending device pairing request. If `requestId` is omitted, OpenClaw
automatically approves the most recent pending request.
Note: if a device retries pairing with changed auth details (role/scopes/public
key), OpenClaw supersedes the previous pending entry and issues a new
`requestId`. Run `openclaw devices list` right before approval to use the
current ID.
```
openclaw devices approve
openclaw devices approve <requestId>

View File

@ -111,6 +111,10 @@ openclaw devices list
openclaw devices approve <requestId>
```
If the node retries pairing with changed auth details (role/scopes/public key),
the previous pending request is superseded and a new `requestId` is created.
Run `openclaw devices list` again before approval.
The node host stores its node id, token, display name, and gateway connection info in
`~/.openclaw/node.json`.

View File

@ -138,14 +138,24 @@ state dir extensions root (`$OPENCLAW_STATE_DIR/extensions/<id>`). Use
### Update
```bash
openclaw plugins update <id>
openclaw plugins update <id-or-npm-spec>
openclaw plugins update --all
openclaw plugins update <id> --dry-run
openclaw plugins update <id-or-npm-spec> --dry-run
openclaw plugins update @openclaw/voice-call@beta
```
Updates apply to tracked installs in `plugins.installs`, currently npm and
marketplace installs.
When you pass a plugin id, OpenClaw reuses the recorded install spec for that
plugin. That means previously stored dist-tags such as `@beta` and exact pinned
versions continue to be used on later `update <id>` runs.
For npm installs, you can also pass an explicit npm package spec with a dist-tag
or exact version. OpenClaw resolves that package name back to the tracked plugin
record, updates that installed plugin, and records the new npm spec for future
id-based updates.
When a stored integrity hash exists and the fetched artifact hash changes,
OpenClaw prints a warning and asks for confirmation before proceeding. Use
global `--yes` to bypass prompts in CI/non-interactive runs.

View File

@ -46,7 +46,7 @@ JSON examples:
"activeMinutes": null,
"sessions": [
{ "agentId": "main", "key": "agent:main:main", "model": "gpt-5" },
{ "agentId": "work", "key": "agent:work:main", "model": "claude-opus-4-5" }
{ "agentId": "work", "key": "agent:work:main", "model": "claude-opus-4-6" }
]
}
```

View File

@ -1,13 +1,13 @@
---
summary: "Agent runtime (embedded pi-mono), workspace contract, and session bootstrap"
summary: "Agent runtime, workspace contract, and session bootstrap"
read_when:
- Changing agent runtime, workspace bootstrap, or session behavior
title: "Agent Runtime"
---
# Agent Runtime 🤖
# Agent Runtime
OpenClaw runs a single embedded agent runtime derived from **pi-mono**.
OpenClaw runs a single embedded agent runtime.
## Workspace (required)
@ -63,12 +63,11 @@ OpenClaw loads skills from three locations (workspace wins on name conflict):
Skills can be gated by config/env (see `skills` in [Gateway configuration](/gateway/configuration)).
## pi-mono integration
## Runtime boundaries
OpenClaw reuses pieces of the pi-mono codebase (models/tools), but **session management, discovery, and tool wiring are OpenClaw-owned**.
- No pi-coding agent runtime.
- No `~/.pi/agent` or `<workspace>/.pi` settings are consulted.
The embedded agent runtime is built on the Pi agent core (models, tools, and
prompt pipeline). Session management, discovery, tool wiring, and channel
delivery are OpenClaw-owned layers on top of that core.
## Sessions
@ -77,7 +76,7 @@ Session transcripts are stored as JSONL at:
- `~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl`
The session ID is stable and chosen by OpenClaw.
Legacy Pi/Tau session folders are **not** read.
Legacy session folders from other tools are not read.
## Steering while streaming

View File

@ -7,8 +7,6 @@ title: "Gateway Architecture"
# Gateway architecture
Last updated: 2026-01-22
## Overview
- A single longlived **Gateway** owns all messaging surfaces (WhatsApp via

View File

@ -31,7 +31,7 @@ You can optionally specify a different model for compaction summarization via `a
"agents": {
"defaults": {
"compaction": {
"model": "openrouter/anthropic/claude-sonnet-4-5"
"model": "openrouter/anthropic/claude-sonnet-4-6"
}
}
}

View File

@ -32,24 +32,42 @@ title: "Features"
## Full list
- WhatsApp integration via WhatsApp Web (Baileys)
- Telegram bot support (grammY)
- Discord bot support (channels.discord.js)
- Mattermost bot support (plugin)
- iMessage integration via local imsg CLI (macOS)
- Agent bridge for Pi in RPC mode with tool streaming
- Streaming and chunking for long responses
- Multi-agent routing for isolated sessions per workspace or sender
- Subscription auth for Anthropic and OpenAI via OAuth
- Sessions: direct chats collapse into shared `main`; groups are isolated
- Group chat support with mention based activation
- Media support for images, audio, and documents
- Optional voice note transcription hook
- WebChat and macOS menu bar app
- iOS node with pairing, Canvas, camera, screen recording, location, and voice features
- Android node with pairing, Connect tab, chat sessions, voice tab, Canvas/camera, plus device, notifications, contacts/calendar, motion, photos, and SMS commands
**Channels:**
<Note>
Legacy Claude, Codex, Gemini, and Opencode paths have been removed. Pi is the only
coding agent path.
</Note>
- WhatsApp, Telegram, Discord, iMessage (built-in)
- Mattermost, Matrix, MS Teams, Nostr, and more (plugins)
- Group chat support with mention-based activation
- DM safety with allowlists and pairing
**Agent:**
- Embedded agent runtime with tool streaming
- Multi-agent routing with isolated sessions per workspace or sender
- Sessions: direct chats collapse into shared `main`; groups are isolated
- Streaming and chunking for long responses
**Auth and providers:**
- 35+ model providers (Anthropic, OpenAI, Google, and more)
- Subscription auth via OAuth (e.g. OpenAI Codex)
- Custom and self-hosted provider support (vLLM, SGLang, Ollama, and any OpenAI-compatible or Anthropic-compatible endpoint)
**Media:**
- Images, audio, video, and documents in and out
- Voice note transcription
- Text-to-speech with multiple providers
**Apps and interfaces:**
- WebChat and browser Control UI
- macOS menu bar companion app
- iOS node with pairing, Canvas, camera, screen recording, location, and voice
- Android node with pairing, chat, voice, Canvas, camera, and device commands
**Tools and automation:**
- Browser automation, exec, sandboxing
- Web search (Brave, Perplexity, Gemini, Grok, Kimi, Firecrawl)
- Cron jobs and heartbeat scheduling
- Skills, plugins, and workflow pipelines (Lobster)

View File

@ -34,8 +34,8 @@ These files live under the workspace (`agents.defaults.workspace`, default
OpenClaw exposes two agent-facing tools for these Markdown files:
- `memory_search` semantic recall over indexed snippets.
- `memory_get` targeted read of a specific Markdown file/line range.
- `memory_search` -- semantic recall over indexed snippets.
- `memory_get` -- targeted read of a specific Markdown file/line range.
`memory_get` now **degrades gracefully when a file doesn't exist** (for example,
today's daily log before the first write). Both the builtin manager and the QMD
@ -94,709 +94,15 @@ For the full compaction lifecycle, see
## Vector memory search
OpenClaw can build a small vector index over `MEMORY.md` and `memory/*.md` so
semantic queries can find related notes even when wording differs.
Defaults:
- Enabled by default.
- Watches memory files for changes (debounced).
- Configure memory search under `agents.defaults.memorySearch` (not top-level
`memorySearch`).
- Uses remote embeddings by default. If `memorySearch.provider` is not set, OpenClaw auto-selects:
1. `local` if a `memorySearch.local.modelPath` is configured and the file exists.
2. `openai` if an OpenAI key can be resolved.
3. `gemini` if a Gemini key can be resolved.
4. `voyage` if a Voyage key can be resolved.
5. `mistral` if a Mistral key can be resolved.
6. Otherwise memory search stays disabled until configured.
- Local mode uses node-llama-cpp and may require `pnpm approve-builds`.
- Uses sqlite-vec (when available) to accelerate vector search inside SQLite.
- `memorySearch.provider = "ollama"` is also supported for local/self-hosted
Ollama embeddings (`/api/embeddings`), but it is not auto-selected.
Remote embeddings **require** an API key for the embedding provider. OpenClaw
resolves keys from auth profiles, `models.providers.*.apiKey`, or environment
variables. Codex OAuth only covers chat/completions and does **not** satisfy
embeddings for memory search. For Gemini, use `GEMINI_API_KEY` or
`models.providers.google.apiKey`. For Voyage, use `VOYAGE_API_KEY` or
`models.providers.voyage.apiKey`. For Mistral, use `MISTRAL_API_KEY` or
`models.providers.mistral.apiKey`. Ollama typically does not require a real API
key (a placeholder like `OLLAMA_API_KEY=ollama-local` is enough when needed by
local policy).
When using a custom OpenAI-compatible endpoint,
set `memorySearch.remote.apiKey` (and optional `memorySearch.remote.headers`).
### QMD backend (experimental)
Set `memory.backend = "qmd"` to swap the built-in SQLite indexer for
[QMD](https://github.com/tobi/qmd): a local-first search sidecar that combines
BM25 + vectors + reranking. Markdown stays the source of truth; OpenClaw shells
out to QMD for retrieval. Key points:
**Prereqs**
- Disabled by default. Opt in per-config (`memory.backend = "qmd"`).
- Install the QMD CLI separately (`bun install -g https://github.com/tobi/qmd` or grab
a release) and make sure the `qmd` binary is on the gateways `PATH`.
- QMD needs an SQLite build that allows extensions (`brew install sqlite` on
macOS).
- QMD runs fully locally via Bun + `node-llama-cpp` and auto-downloads GGUF
models from HuggingFace on first use (no separate Ollama daemon required).
- The gateway runs QMD in a self-contained XDG home under
`~/.openclaw/agents/<agentId>/qmd/` by setting `XDG_CONFIG_HOME` and
`XDG_CACHE_HOME`.
- OS support: macOS and Linux work out of the box once Bun + SQLite are
installed. Windows is best supported via WSL2.
**How the sidecar runs**
- The gateway writes a self-contained QMD home under
`~/.openclaw/agents/<agentId>/qmd/` (config + cache + sqlite DB).
- Collections are created via `qmd collection add` from `memory.qmd.paths`
(plus default workspace memory files), then `qmd update` + `qmd embed` run
on boot and on a configurable interval (`memory.qmd.update.interval`,
default 5m).
- The gateway now initializes the QMD manager on startup, so periodic update
timers are armed even before the first `memory_search` call.
- Boot refresh now runs in the background by default so chat startup is not
blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous
blocking behavior.
- Searches run via `memory.qmd.searchMode` (default `qmd search --json`; also
supports `vsearch` and `query`). If the selected mode rejects flags on your
QMD build, OpenClaw retries with `qmd query`. If QMD fails or the binary is
missing, OpenClaw automatically falls back to the builtin SQLite manager so
memory tools keep working.
- OpenClaw does not expose QMD embed batch-size tuning today; batch behavior is
controlled by QMD itself.
- **First search may be slow**: QMD may download local GGUF models (reranker/query
expansion) on the first `qmd query` run.
- OpenClaw sets `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` automatically when it runs QMD.
- If you want to pre-download models manually (and warm the same index OpenClaw
uses), run a one-off query with the agents XDG dirs.
OpenClaws QMD state lives under your **state dir** (defaults to `~/.openclaw`).
You can point `qmd` at the exact same index by exporting the same XDG vars
OpenClaw uses:
```bash
# Pick the same state dir OpenClaw uses
STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config"
export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache"
# (Optional) force an index refresh + embeddings
qmd update
qmd embed
# Warm up / trigger first-time model downloads
qmd query "test" -c memory-root --json >/dev/null 2>&1
```
**Config surface (`memory.qmd.*`)**
- `command` (default `qmd`): override the executable path.
- `searchMode` (default `search`): pick which QMD command backs
`memory_search` (`search`, `vsearch`, `query`).
- `includeDefaultMemory` (default `true`): auto-index `MEMORY.md` + `memory/**/*.md`.
- `paths[]`: add extra directories/files (`path`, optional `pattern`, optional
stable `name`).
- `sessions`: opt into session JSONL indexing (`enabled`, `retentionDays`,
`exportDir`).
- `update`: controls refresh cadence and maintenance execution:
(`interval`, `debounceMs`, `onBoot`, `waitForBootSync`, `embedInterval`,
`commandTimeoutMs`, `updateTimeoutMs`, `embedTimeoutMs`).
- `limits`: clamp recall payload (`maxResults`, `maxSnippetChars`,
`maxInjectedChars`, `timeoutMs`).
- `scope`: same schema as [`session.sendPolicy`](/gateway/configuration#session).
Default is DM-only (`deny` all, `allow` direct chats); loosen it to surface QMD
hits in groups/channels.
- `match.keyPrefix` matches the **normalized** session key (lowercased, with any
leading `agent:<id>:` stripped). Example: `discord:channel:`.
- `match.rawKeyPrefix` matches the **raw** session key (lowercased), including
`agent:<id>:`. Example: `agent:main:discord:`.
- Legacy: `match.keyPrefix: "agent:..."` is still treated as a raw-key prefix,
but prefer `rawKeyPrefix` for clarity.
- When `scope` denies a search, OpenClaw logs a warning with the derived
`channel`/`chatType` so empty results are easier to debug.
- Snippets sourced outside the workspace show up as
`qmd/<collection>/<relative-path>` in `memory_search` results; `memory_get`
understands that prefix and reads from the configured QMD collection root.
- When `memory.qmd.sessions.enabled = true`, OpenClaw exports sanitized session
transcripts (User/Assistant turns) into a dedicated QMD collection under
`~/.openclaw/agents/<id>/qmd/sessions/`, so `memory_search` can recall recent
conversations without touching the builtin SQLite index.
- `memory_search` snippets now include a `Source: <path#line>` footer when
`memory.citations` is `auto`/`on`; set `memory.citations = "off"` to keep
the path metadata internal (the agent still receives the path for
`memory_get`, but the snippet text omits the footer and the system prompt
warns the agent not to cite it).
**Example**
```json5
memory: {
backend: "qmd",
citations: "auto",
qmd: {
includeDefaultMemory: true,
update: { interval: "5m", debounceMs: 15000 },
limits: { maxResults: 6, timeoutMs: 4000 },
scope: {
default: "deny",
rules: [
{ action: "allow", match: { chatType: "direct" } },
// Normalized session-key prefix (strips `agent:<id>:`).
{ action: "deny", match: { keyPrefix: "discord:channel:" } },
// Raw session-key prefix (includes `agent:<id>:`).
{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
]
},
paths: [
{ name: "docs", path: "~/notes", pattern: "**/*.md" }
]
}
}
```
**Citations & fallback**
- `memory.citations` applies regardless of backend (`auto`/`on`/`off`).
- When `qmd` runs, we tag `status().backend = "qmd"` so diagnostics show which
engine served the results. If the QMD subprocess exits or JSON output cant be
parsed, the search manager logs a warning and returns the builtin provider
(existing Markdown embeddings) until QMD recovers.
### Additional memory paths
If you want to index Markdown files outside the default workspace layout, add
explicit paths:
```json5
agents: {
defaults: {
memorySearch: {
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
}
}
}
```
Notes:
- Paths can be absolute or workspace-relative.
- Directories are scanned recursively for `.md` files.
- By default, only Markdown files are indexed.
- If `memorySearch.multimodal.enabled = true`, OpenClaw also indexes supported image/audio files under `extraPaths` only. Default memory roots (`MEMORY.md`, `memory.md`, `memory/**/*.md`) stay Markdown-only.
- Symlinks are ignored (files or directories).
### Multimodal memory files (Gemini image + audio)
OpenClaw can index image and audio files from `memorySearch.extraPaths` when using Gemini embedding 2:
```json5
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-2-preview",
extraPaths: ["assets/reference", "voice-notes"],
multimodal: {
enabled: true,
modalities: ["image", "audio"], // or ["all"]
maxFileBytes: 10000000
},
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
```
Notes:
- Multimodal memory is currently supported only for `gemini-embedding-2-preview`.
- Multimodal indexing applies only to files discovered through `memorySearch.extraPaths`.
- Supported modalities in this phase: image and audio.
- `memorySearch.fallback` must stay `"none"` while multimodal memory is enabled.
- Matching image/audio file bytes are uploaded to the configured Gemini embedding endpoint during indexing.
- Supported image extensions: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.heic`, `.heif`.
- Supported audio extensions: `.mp3`, `.wav`, `.ogg`, `.opus`, `.m4a`, `.aac`, `.flac`.
- Search queries remain text, but Gemini can compare those text queries against indexed image/audio embeddings.
- `memory_get` still reads Markdown only; binary files are searchable but not returned as raw file contents.
### Gemini embeddings (native)
Set the provider to `gemini` to use the Gemini embeddings API directly:
```json5
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-001",
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
```
Notes:
- `remote.baseUrl` is optional (defaults to the Gemini API base URL).
- `remote.headers` lets you add extra headers if needed.
- Default model: `gemini-embedding-001`.
- `gemini-embedding-2-preview` is also supported: 8192 token limit and configurable dimensions (768 / 1536 / 3072, default 3072).
#### Gemini Embedding 2 (preview)
```json5
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-2-preview",
outputDimensionality: 3072, // optional: 768, 1536, or 3072 (default)
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
```
> **⚠️ Re-index required:** Switching from `gemini-embedding-001` (768 dimensions)
> to `gemini-embedding-2-preview` (3072 dimensions) changes the vector size. The same is true if you
> change `outputDimensionality` between 768, 1536, and 3072.
> OpenClaw will automatically reindex when it detects a model or dimension change.
If you want to use a **custom OpenAI-compatible endpoint** (OpenRouter, vLLM, or a proxy),
you can use the `remote` configuration with the OpenAI provider:
```json5
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
headers: { "X-Custom-Header": "value" }
}
}
}
}
```
If you don't want to set an API key, use `memorySearch.provider = "local"` or set
`memorySearch.fallback = "none"`.
Fallbacks:
- `memorySearch.fallback` can be `openai`, `gemini`, `voyage`, `mistral`, `ollama`, `local`, or `none`.
- The fallback provider is only used when the primary embedding provider fails.
Batch indexing (OpenAI + Gemini + Voyage):
- Disabled by default. Set `agents.defaults.memorySearch.remote.batch.enabled = true` to enable for large-corpus indexing (OpenAI, Gemini, and Voyage).
- Default behavior waits for batch completion; tune `remote.batch.wait`, `remote.batch.pollIntervalMs`, and `remote.batch.timeoutMinutes` if needed.
- Set `remote.batch.concurrency` to control how many batch jobs we submit in parallel (default: 2).
- Batch mode applies when `memorySearch.provider = "openai"` or `"gemini"` and uses the corresponding API key.
- Gemini batch jobs use the async embeddings batch endpoint and require Gemini Batch API availability.
Why OpenAI batch is fast + cheap:
- For large backfills, OpenAI is typically the fastest option we support because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously.
- OpenAI offers discounted pricing for Batch API workloads, so large indexing runs are usually cheaper than sending the same requests synchronously.
- See the OpenAI Batch API docs and pricing for details:
- [https://platform.openai.com/docs/api-reference/batch](https://platform.openai.com/docs/api-reference/batch)
- [https://platform.openai.com/pricing](https://platform.openai.com/pricing)
Config example:
```json5
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
fallback: "openai",
remote: {
batch: { enabled: true, concurrency: 2 }
},
sync: { watch: true }
}
}
}
```
Tools:
- `memory_search` — returns snippets with file + line ranges.
- `memory_get` — read memory file content by path.
Local mode:
- Set `agents.defaults.memorySearch.provider = "local"`.
- Provide `agents.defaults.memorySearch.local.modelPath` (GGUF or `hf:` URI).
- Optional: set `agents.defaults.memorySearch.fallback = "none"` to avoid remote fallback.
### How the memory tools work
- `memory_search` semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, score, provider/model, and whether we fell back from local → remote embeddings. No full file payload is returned.
- `memory_get` reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.
- Both tools are enabled only when `memorySearch.enabled` resolves true for the agent.
### What gets indexed (and when)
- File type: Markdown only (`MEMORY.md`, `memory/**/*.md`).
- Index storage: per-agent SQLite at `~/.openclaw/memory/<agentId>.sqlite` (configurable via `agents.defaults.memorySearch.store.path`, supports `{agentId}` token).
- Freshness: watcher on `MEMORY.md` + `memory/` marks the index dirty (debounce 1.5s). Sync is scheduled on session start, on search, or on an interval and runs asynchronously. Session transcripts use delta thresholds to trigger background sync.
- Reindex triggers: the index stores the embedding **provider/model + endpoint fingerprint + chunking params**. If any of those change, OpenClaw automatically resets and reindexes the entire store.
### Hybrid search (BM25 + vector)
When enabled, OpenClaw combines:
- **Vector similarity** (semantic match, wording can differ)
- **BM25 keyword relevance** (exact tokens like IDs, env vars, code symbols)
If full-text search is unavailable on your platform, OpenClaw falls back to vector-only search.
#### Why hybrid?
Vector search is great at “this means the same thing”:
- “Mac Studio gateway host” vs “the machine running the gateway”
- “debounce file updates” vs “avoid indexing on every write”
But it can be weak at exact, high-signal tokens:
- IDs (`a828e60`, `b3b9895a…`)
- code symbols (`memorySearch.query.hybrid`)
- error strings ("sqlite-vec unavailable")
BM25 (full-text) is the opposite: strong at exact tokens, weaker at paraphrases.
Hybrid search is the pragmatic middle ground: **use both retrieval signals** so you get
good results for both "natural language" queries and "needle in a haystack" queries.
#### How we merge results (the current design)
Implementation sketch:
1. Retrieve a candidate pool from both sides:
- **Vector**: top `maxResults * candidateMultiplier` by cosine similarity.
- **BM25**: top `maxResults * candidateMultiplier` by FTS5 BM25 rank (lower is better).
2. Convert BM25 rank into a 0..1-ish score:
- `textScore = 1 / (1 + max(0, bm25Rank))`
3. Union candidates by chunk id and compute a weighted score:
- `finalScore = vectorWeight * vectorScore + textWeight * textScore`
Notes:
- `vectorWeight` + `textWeight` is normalized to 1.0 in config resolution, so weights behave as percentages.
- If embeddings are unavailable (or the provider returns a zero-vector), we still run BM25 and return keyword matches.
- If FTS5 can't be created, we keep vector-only search (no hard failure).
This isn't "IR-theory perfect", but it's simple, fast, and tends to improve recall/precision on real notes.
If we want to get fancier later, common next steps are Reciprocal Rank Fusion (RRF) or score normalization
(min/max or z-score) before mixing.
#### Post-processing pipeline
After merging vector and keyword scores, two optional post-processing stages
refine the result list before it reaches the agent:
```
Vector + Keyword → Weighted Merge → Temporal Decay → Sort → MMR → Top-K Results
```
Both stages are **off by default** and can be enabled independently.
#### MMR re-ranking (diversity)
When hybrid search returns results, multiple chunks may contain similar or overlapping content.
For example, searching for "home network setup" might return five nearly identical snippets
from different daily notes that all mention the same router configuration.
**MMR (Maximal Marginal Relevance)** re-ranks the results to balance relevance with diversity,
ensuring the top results cover different aspects of the query instead of repeating the same information.
How it works:
1. Results are scored by their original relevance (vector + BM25 weighted score).
2. MMR iteratively selects results that maximize: × relevance (1λ) × max_similarity_to_selected`.
3. Similarity between results is measured using Jaccard text similarity on tokenized content.
The `lambda` parameter controls the trade-off:
- `lambda = 1.0` → pure relevance (no diversity penalty)
- `lambda = 0.0` → maximum diversity (ignores relevance)
- Default: `0.7` (balanced, slight relevance bias)
**Example — query: "home network setup"**
Given these memory files:
```
memory/2026-02-10.md → "Configured Omada router, set VLAN 10 for IoT devices"
memory/2026-02-08.md → "Configured Omada router, moved IoT to VLAN 10"
memory/2026-02-05.md → "Set up AdGuard DNS on 192.168.10.2"
memory/network.md → "Router: Omada ER605, AdGuard: 192.168.10.2, VLAN 10: IoT"
```
Without MMR — top 3 results:
```
1. memory/2026-02-10.md (score: 0.92) ← router + VLAN
2. memory/2026-02-08.md (score: 0.89) ← router + VLAN (near-duplicate!)
3. memory/network.md (score: 0.85) ← reference doc
```
With MMR (λ=0.7) — top 3 results:
```
1. memory/2026-02-10.md (score: 0.92) ← router + VLAN
2. memory/network.md (score: 0.85) ← reference doc (diverse!)
3. memory/2026-02-05.md (score: 0.78) ← AdGuard DNS (diverse!)
```
The near-duplicate from Feb 8 drops out, and the agent gets three distinct pieces of information.
**When to enable:** If you notice `memory_search` returning redundant or near-duplicate snippets,
especially with daily notes that often repeat similar information across days.
#### Temporal decay (recency boost)
Agents with daily notes accumulate hundreds of dated files over time. Without decay,
a well-worded note from six months ago can outrank yesterday's update on the same topic.
**Temporal decay** applies an exponential multiplier to scores based on the age of each result,
so recent memories naturally rank higher while old ones fade:
```
decayedScore = score × e^(-λ × ageInDays)
```
where `λ = ln(2) / halfLifeDays`.
With the default half-life of 30 days:
- Today's notes: **100%** of original score
- 7 days ago: **~84%**
- 30 days ago: **50%**
- 90 days ago: **12.5%**
- 180 days ago: **~1.6%**
**Evergreen files are never decayed:**
- `MEMORY.md` (root memory file)
- Non-dated files in `memory/` (e.g., `memory/projects.md`, `memory/network.md`)
- These contain durable reference information that should always rank normally.
**Dated daily files** (`memory/YYYY-MM-DD.md`) use the date extracted from the filename.
Other sources (e.g., session transcripts) fall back to file modification time (`mtime`).
**Example — query: "what's Rod's work schedule?"**
Given these memory files (today is Feb 10):
```
memory/2025-09-15.md → "Rod works Mon-Fri, standup at 10am, pairing at 2pm" (148 days old)
memory/2026-02-10.md → "Rod has standup at 14:15, 1:1 with Zeb at 14:45" (today)
memory/2026-02-03.md → "Rod started new team, standup moved to 14:15" (7 days old)
```
Without decay:
```
1. memory/2025-09-15.md (score: 0.91) ← best semantic match, but stale!
2. memory/2026-02-10.md (score: 0.82)
3. memory/2026-02-03.md (score: 0.80)
```
With decay (halfLife=30):
```
1. memory/2026-02-10.md (score: 0.82 × 1.00 = 0.82) ← today, no decay
2. memory/2026-02-03.md (score: 0.80 × 0.85 = 0.68) ← 7 days, mild decay
3. memory/2025-09-15.md (score: 0.91 × 0.03 = 0.03) ← 148 days, nearly gone
```
The stale September note drops to the bottom despite having the best raw semantic match.
**When to enable:** If your agent has months of daily notes and you find that old,
stale information outranks recent context. A half-life of 30 days works well for
daily-note-heavy workflows; increase it (e.g., 90 days) if you reference older notes frequently.
#### Configuration
Both features are configured under `memorySearch.query.hybrid`:
```json5
agents: {
defaults: {
memorySearch: {
query: {
hybrid: {
enabled: true,
vectorWeight: 0.7,
textWeight: 0.3,
candidateMultiplier: 4,
// Diversity: reduce redundant results
mmr: {
enabled: true, // default: false
lambda: 0.7 // 0 = max diversity, 1 = max relevance
},
// Recency: boost newer memories
temporalDecay: {
enabled: true, // default: false
halfLifeDays: 30 // score halves every 30 days
}
}
}
}
}
}
```
You can enable either feature independently:
- **MMR only** — useful when you have many similar notes but age doesn't matter.
- **Temporal decay only** — useful when recency matters but your results are already diverse.
- **Both** — recommended for agents with large, long-running daily note histories.
### Embedding cache
OpenClaw can cache **chunk embeddings** in SQLite so reindexing and frequent updates (especially session transcripts) don't re-embed unchanged text.
Config:
```json5
agents: {
defaults: {
memorySearch: {
cache: {
enabled: true,
maxEntries: 50000
}
}
}
}
```
### Session memory search (experimental)
You can optionally index **session transcripts** and surface them via `memory_search`.
This is gated behind an experimental flag.
```json5
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"]
}
}
}
```
Notes:
- Session indexing is **opt-in** (off by default).
- Session updates are debounced and **indexed asynchronously** once they cross delta thresholds (best-effort).
- `memory_search` never blocks on indexing; results can be slightly stale until background sync finishes.
- Results still include snippets only; `memory_get` remains limited to memory files.
- Session indexing is isolated per agent (only that agents session logs are indexed).
- Session logs live on disk (`~/.openclaw/agents/<agentId>/sessions/*.jsonl`). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts.
Delta thresholds (defaults shown):
```json5
agents: {
defaults: {
memorySearch: {
sync: {
sessions: {
deltaBytes: 100000, // ~100 KB
deltaMessages: 50 // JSONL lines
}
}
}
}
}
```
### SQLite vector acceleration (sqlite-vec)
When the sqlite-vec extension is available, OpenClaw stores embeddings in a
SQLite virtual table (`vec0`) and performs vector distance queries in the
database. This keeps search fast without loading every embedding into JS.
Configuration (optional):
```json5
agents: {
defaults: {
memorySearch: {
store: {
vector: {
enabled: true,
extensionPath: "/path/to/sqlite-vec"
}
}
}
}
}
```
Notes:
- `enabled` defaults to true; when disabled, search falls back to in-process
cosine similarity over stored embeddings.
- If the sqlite-vec extension is missing or fails to load, OpenClaw logs the
error and continues with the JS fallback (no vector table).
- `extensionPath` overrides the bundled sqlite-vec path (useful for custom builds
or non-standard install locations).
### Local embedding auto-download
- Default local embedding model: `hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB).
- When `memorySearch.provider = "local"`, `node-llama-cpp` resolves `modelPath`; if the GGUF is missing it **auto-downloads** to the cache (or `local.modelCacheDir` if set), then loads it. Downloads resume on retry.
- Native build requirement: run `pnpm approve-builds`, pick `node-llama-cpp`, then `pnpm rebuild node-llama-cpp`.
- Fallback: if local setup fails and `memorySearch.fallback = "openai"`, we automatically switch to remote embeddings (`openai/text-embedding-3-small` unless overridden) and record the reason.
### Custom OpenAI-compatible endpoint example
```json5
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_REMOTE_API_KEY",
headers: {
"X-Organization": "org-id",
"X-Project": "project-id"
}
}
}
}
}
```
Notes:
- `remote.*` takes precedence over `models.providers.openai.*`.
- `remote.headers` merge with OpenAI headers; remote wins on key conflicts. Omit `remote.headers` to use the OpenAI defaults.
semantic queries can find related notes even when wording differs. Hybrid search
(BM25 + vector) is available for combining semantic matching with exact keyword
lookups.
Memory search supports multiple embedding providers (OpenAI, Gemini, Voyage,
Mistral, Ollama, and local GGUF models), an optional QMD sidecar backend for
advanced retrieval, and post-processing features like MMR diversity re-ranking
and temporal decay.
For the full configuration reference -- including embedding provider setup, QMD
backend, hybrid search tuning, multimodal memory, and all config knobs -- see
[Memory configuration reference](/reference/memory-config).

View File

@ -151,4 +151,4 @@ Outbound message formatting is centralized in `messages`:
- `messages.responsePrefix`, `channels.<channel>.responsePrefix`, and `channels.<channel>.accounts.<id>.responsePrefix` (outbound prefix cascade), plus `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
- Reply threading via `replyToMode` and per-channel defaults
Details: [Configuration](/gateway/configuration#messages) and channel docs.
Details: [Configuration](/gateway/configuration-reference#messages) and channel docs.

View File

@ -255,7 +255,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
### Other bundled provider plugins
- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`)
- Example model: `openrouter/anthropic/claude-sonnet-4-5`
- Example model: `openrouter/anthropic/claude-sonnet-4-6`
- Kilo Gateway: `kilocode` (`KILOCODE_API_KEY`)
- Example model: `kilocode/anthropic/claude-opus-4.6`
- MiniMax: `minimax` (`MINIMAX_API_KEY`)

View File

@ -58,7 +58,7 @@ Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize
to `zai/*`.
Provider configuration examples (including OpenCode) live in
[/gateway/configuration](/gateway/configuration#opencode).
[/providers/opencode](/providers/opencode).
## "Model is not allowed" (and why replies stop)
@ -82,9 +82,9 @@ Example allowlist config:
```json5
{
agent: {
model: { primary: "anthropic/claude-sonnet-4-5" },
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
},
},

View File

@ -388,7 +388,7 @@ Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opu
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-5",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
@ -422,7 +422,7 @@ Keep WhatsApp on the fast agent, but route one DM to Opus:
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-5",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
@ -501,7 +501,7 @@ Notes:
## Per-Agent Sandbox and Tool Configuration
Starting with v2026.1.6, each agent can have its own sandbox and tool restrictions:
Each agent can have its own sandbox and tool restrictions:
```js
{

View File

@ -50,7 +50,7 @@ Legacy import-only file (still supported, but not the main store):
- `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use)
All of the above also respect `$OPENCLAW_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys)
All of the above also respect `$OPENCLAW_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration-reference#auth-storage)
For static secret refs and runtime snapshot activation behavior, see [Secrets Management](/gateway/secrets).

View File

@ -43,10 +43,39 @@
"label": "Releases",
"href": "https://github.com/openclaw/openclaw/releases",
"icon": "package"
},
{
"label": "Discord",
"href": "https://discord.com/invite/clawd",
"icon": "discord"
}
]
},
"redirects": [
{
"source": "/platforms/oracle",
"destination": "/install/oracle"
},
{
"source": "/platforms/digitalocean",
"destination": "/install/digitalocean"
},
{
"source": "/platforms/raspberry-pi",
"destination": "/install/raspberry-pi"
},
{
"source": "/brave-search",
"destination": "/tools/brave-search"
},
{
"source": "/perplexity",
"destination": "/tools/perplexity-search"
},
{
"source": "/tts",
"destination": "/tools/tts"
},
{
"source": "/messages",
"destination": "/concepts/messages"
@ -767,6 +796,14 @@
"source": "/gcp",
"destination": "/install/gcp"
},
{
"source": "/azure",
"destination": "/install/azure"
},
{
"source": "/install/azure/azure",
"destination": "/install/azure"
},
{
"source": "/platforms/fly",
"destination": "/install/fly"
@ -779,6 +816,10 @@
"source": "/platforms/gcp",
"destination": "/install/gcp"
},
{
"source": "/platforms/azure",
"destination": "/install/azure"
},
{
"source": "/platforms/macos-vm",
"destination": "/install/macos-vm"
@ -812,17 +853,9 @@
{
"tab": "Get started",
"groups": [
{
"group": "Home",
"pages": ["index"]
},
{
"group": "Overview",
"pages": ["start/showcase"]
},
{
"group": "Core concepts",
"pages": ["concepts/features"]
"pages": ["index", "start/showcase", "concepts/features"]
},
{
"group": "First steps",
@ -848,40 +881,46 @@
"groups": [
{
"group": "Install overview",
"pages": ["install/index", "install/installer"]
"pages": ["install/index", "install/installer", "install/node"]
},
{
"group": "Other install methods",
"group": "Containers",
"pages": [
"install/docker",
"install/podman",
"install/nix",
"install/ansible",
"install/bun"
"install/bun",
"install/docker",
"install/nix",
"install/podman"
]
},
{
"group": "Hosting",
"pages": [
"install/azure",
"install/digitalocean",
"install/docker-vm-runtime",
"install/exe-dev",
"install/fly",
"install/gcp",
"install/hetzner",
"install/kubernetes",
"vps",
"install/macos-vm",
"install/northflank",
"install/oracle",
"install/railway",
"install/raspberry-pi",
"install/render"
]
},
{
"group": "Maintenance",
"pages": ["install/updating", "install/migrating", "install/uninstall"]
},
{
"group": "Hosting and deployment",
"pages": [
"vps",
"install/kubernetes",
"install/fly",
"install/hetzner",
"install/gcp",
"install/macos-vm",
"install/exe-dev",
"install/railway",
"install/render",
"install/northflank"
"install/updating",
"install/migrating",
"install/uninstall",
"install/development-channels"
]
},
{
"group": "Advanced",
"pages": ["install/development-channels"]
}
]
},
@ -938,7 +977,6 @@
{
"group": "Fundamentals",
"pages": [
"pi",
"concepts/architecture",
"concepts/agent",
"concepts/agent-loop",
@ -946,13 +984,10 @@
"concepts/context",
"concepts/context-engine",
"concepts/agent-workspace",
"concepts/oauth"
"concepts/oauth",
"start/bootstrapping"
]
},
{
"group": "Bootstrapping",
"pages": ["start/bootstrapping"]
},
{
"group": "Sessions and memory",
"pages": [
@ -989,7 +1024,7 @@
"group": "Built-in tools",
"pages": [
"tools/apply-patch",
"brave-search",
"tools/brave-search",
"tools/btw",
"tools/diffs",
"tools/elevated",
@ -1000,7 +1035,7 @@
"tools/lobster",
"tools/loop-detection",
"tools/pdf",
"perplexity",
"tools/perplexity-search",
"tools/reactions",
"tools/thinking",
"tools/web"
@ -1011,7 +1046,8 @@
"pages": [
"tools/browser",
"tools/browser-login",
"tools/browser-linux-troubleshooting"
"tools/browser-linux-troubleshooting",
"tools/browser-wsl2-windows-remote-cdp-troubleshooting"
]
},
{
@ -1031,7 +1067,8 @@
"tools/skills",
"tools/skills-config",
"tools/clawhub",
"tools/plugin"
"tools/plugin",
"prose"
]
},
{
@ -1045,8 +1082,7 @@
"plugins/zalouser",
"plugins/manifest",
"plugins/agent-tools",
"tools/capability-cookbook",
"prose"
"tools/capability-cookbook"
]
},
{
@ -1074,7 +1110,7 @@
"nodes/talk",
"nodes/voicewake",
"nodes/location-command",
"tts"
"tools/tts"
]
}
]
@ -1087,12 +1123,8 @@
"pages": ["providers/index", "providers/models"]
},
{
"group": "Model concepts",
"pages": ["concepts/models"]
},
{
"group": "Configuration",
"pages": ["concepts/model-providers", "concepts/model-failover"]
"group": "Concepts and configuration",
"pages": ["concepts/models", "concepts/model-providers", "concepts/model-failover"]
},
{
"group": "Providers",
@ -1104,6 +1136,7 @@
"providers/deepgram",
"providers/github-copilot",
"providers/google",
"providers/groq",
"providers/huggingface",
"providers/kilocode",
"providers/litellm",
@ -1146,10 +1179,7 @@
"platforms/linux",
"platforms/windows",
"platforms/android",
"platforms/ios",
"platforms/digitalocean",
"platforms/oracle",
"platforms/raspberry-pi"
"platforms/ios"
]
},
{
@ -1198,6 +1228,7 @@
"gateway/heartbeat",
"gateway/doctor",
"gateway/logging",
"logging",
"gateway/gateway-lock",
"gateway/background-process",
"gateway/multiple-gateways",
@ -1228,6 +1259,7 @@
{
"group": "Networking and discovery",
"pages": [
"network",
"gateway/network-model",
"gateway/pairing",
"gateway/discovery",
@ -1261,51 +1293,76 @@
"group": "CLI commands",
"pages": [
"cli/index",
"cli/acp",
"cli/agent",
"cli/agents",
"cli/approvals",
"cli/browser",
"cli/channels",
"cli/clawbot",
"cli/completion",
"cli/config",
"cli/configure",
"cli/cron",
"cli/daemon",
"cli/dashboard",
"cli/devices",
"cli/directory",
"cli/dns",
"cli/docs",
"cli/doctor",
"cli/gateway",
"cli/health",
"cli/hooks",
"cli/logs",
"cli/memory",
"cli/message",
"cli/models",
"cli/node",
"cli/nodes",
"cli/onboard",
"cli/pairing",
"cli/plugins",
"cli/qr",
"cli/reset",
"cli/sandbox",
"cli/secrets",
"cli/security",
"cli/sessions",
"cli/setup",
"cli/skills",
"cli/status",
"cli/system",
"cli/tui",
"cli/uninstall",
"cli/update",
"cli/voicecall",
"cli/webhooks"
{
"group": "Gateway and service",
"pages": [
"cli/backup",
"cli/daemon",
"cli/doctor",
"cli/gateway",
"cli/health",
"cli/logs",
"cli/onboard",
"cli/reset",
"cli/secrets",
"cli/security",
"cli/setup",
"cli/status",
"cli/uninstall",
"cli/update"
]
},
{
"group": "Agents and sessions",
"pages": [
"cli/agent",
"cli/agents",
"cli/hooks",
"cli/memory",
"cli/message",
"cli/models",
"cli/sessions",
"cli/system"
]
},
{
"group": "Channels and messaging",
"pages": [
"cli/channels",
"cli/devices",
"cli/directory",
"cli/pairing",
"cli/qr",
"cli/voicecall"
]
},
{
"group": "Tools and execution",
"pages": [
"cli/approvals",
"cli/browser",
"cli/cron",
"cli/node",
"cli/nodes",
"cli/sandbox"
]
},
{
"group": "Configuration",
"pages": ["cli/config", "cli/configure", "cli/webhooks"]
},
{
"group": "Plugins and skills",
"pages": ["cli/plugins", "cli/skills"]
},
{
"group": "Interfaces",
"pages": ["cli/dashboard", "cli/tui"]
},
{
"group": "Utility",
"pages": ["cli/acp", "cli/clawbot", "cli/completion", "cli/dns", "cli/docs"]
}
]
},
{
@ -1329,12 +1386,14 @@
{
"group": "Technical reference",
"pages": [
"pi",
"reference/wizard",
"reference/token-use",
"reference/secretref-credential-surface",
"reference/prompt-caching",
"reference/api-usage-costs",
"reference/transcript-hygiene",
"reference/memory-config",
"date-time"
]
},
@ -1380,10 +1439,6 @@
"diagnostics/flags"
]
},
{
"group": "Node runtime",
"pages": ["install/node"]
},
{
"group": "Compaction internals",
"pages": ["reference/session-management-compaction"]

View File

@ -114,8 +114,8 @@ The provider id becomes the left side of your model ref:
modelArg: "--model",
modelAliases: {
"claude-opus-4-6": "opus",
"claude-opus-4-5": "opus",
"claude-sonnet-4-5": "sonnet",
"claude-opus-4-6": "opus",
"claude-sonnet-4-6": "sonnet",
},
sessionArg: "--session",
sessionMode: "existing",

View File

@ -35,7 +35,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
},
agent: {
workspace: "~/.openclaw/workspace",
model: { primary: "anthropic/claude-sonnet-4-5" },
model: { primary: "anthropic/claude-sonnet-4-6" },
},
channels: {
whatsapp: {
@ -238,15 +238,15 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
workspace: "~/.openclaw/workspace",
userTimezone: "America/Chicago",
model: {
primary: "anthropic/claude-sonnet-4-5",
primary: "anthropic/claude-sonnet-4-6",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
imageModel: {
primary: "openrouter/anthropic/claude-sonnet-4-5",
primary: "openrouter/anthropic/claude-sonnet-4-6",
},
models: {
"anthropic/claude-opus-4-6": { alias: "opus" },
"anthropic/claude-sonnet-4-5": { alias: "sonnet" },
"anthropic/claude-sonnet-4-6": { alias: "sonnet" },
"openai/gpt-5.2": { alias: "gpt" },
},
thinkingDefault: "low",
@ -271,7 +271,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
maxConcurrent: 3,
heartbeat: {
every: "30m",
model: "anthropic/claude-sonnet-4-5",
model: "anthropic/claude-sonnet-4-6",
target: "last",
directPolicy: "allow", // allow (default) | block
to: "+15555550123",
@ -520,7 +520,7 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
agent: {
workspace: "~/.openclaw/workspace",
model: {
primary: "anthropic/claude-sonnet-4-5",
primary: "anthropic/claude-sonnet-4-6",
fallbacks: ["anthropic/claude-opus-4-6"],
},
},

View File

@ -1,6 +1,5 @@
---
title: "Configuration Reference"
description: "Complete field-by-field reference for ~/.openclaw/openclaw.json"
summary: "Complete reference for every OpenClaw config key, defaults, and channel settings"
read_when:
- You need exact field-level config semantics or defaults
@ -1019,7 +1018,7 @@ Periodic heartbeat runs.
identifierPolicy: "strict", // strict | off | custom
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
model: "openrouter/anthropic/claude-sonnet-4-5", // optional compaction-only model override
model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override
memoryFlush: {
enabled: true,
softThresholdTokens: 6000,

View File

@ -112,11 +112,11 @@ When validation fails:
agents: {
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-5",
primary: "anthropic/claude-sonnet-4-6",
fallbacks: ["openai/gpt-5.2"],
},
models: {
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
"openai/gpt-5.2": { alias: "GPT" },
},
},
@ -251,7 +251,7 @@ When validation fails:
Build the image first: `scripts/sandbox-setup.sh`
See [Sandboxing](/gateway/sandboxing) for the full guide and [full reference](/gateway/configuration-reference#sandbox) for all options.
See [Sandboxing](/gateway/sandboxing) for the full guide and [full reference](/gateway/configuration-reference#agents-defaults-sandbox) for all options.
</Accordion>

View File

@ -69,11 +69,11 @@ Keep hosted models configured even when running local; use `models.mode: "merge"
agents: {
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-5",
primary: "anthropic/claude-sonnet-4-6",
fallbacks: ["lmstudio/minimax-m2.5-gs32", "anthropic/claude-opus-4-6"],
},
models: {
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
"lmstudio/minimax-m2.5-gs32": { alias: "MiniMax Local" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
},

View File

@ -399,7 +399,7 @@ Security defaults:
Docker installs and the containerized gateway live here:
[Docker](/install/docker)
For Docker gateway deployments, `docker-setup.sh` can bootstrap sandbox config.
For Docker gateway deployments, `scripts/docker/setup.sh` can bootstrap sandbox config.
Set `OPENCLAW_SANDBOX=1` (or `true`/`yes`/`on`) to enable that path. You can
override socket location with `OPENCLAW_DOCKER_SOCKET`. Full setup and env
reference: [Docker](/install/docker#enable-agent-sandbox-for-docker-gateway-opt-in).
@ -463,7 +463,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
## Related docs
- [OpenShell](/gateway/openshell) -- managed sandbox backend setup, workspace modes, and config reference
- [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox)
- [Sandbox Configuration](/gateway/configuration-reference#agents-defaults-sandbox)
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides and precedence
- [Security](/gateway/security)

View File

@ -5,7 +5,7 @@ read_when:
title: "Security"
---
# Security 🔒
# Security
> [!WARNING]
> **Personal assistant trust model:** this guidance assumes one trusted operator boundary per gateway (single-user/personal assistant model).
@ -25,7 +25,7 @@ This page explains hardening **within that model**. It does not claim hostile mu
## Quick check: `openclaw security audit`
See also: [Formal Verification (Security Models)](/security/formal-verification/)
See also: [Formal Verification (Security Models)](/security/formal-verification)
Run this regularly (especially after changing config or exposing network surfaces):

View File

@ -1,4 +1,5 @@
---
title: "Trusted Proxy Auth"
summary: "Delegate gateway authentication to a trusted reverse proxy (Pomerium, Caddy, nginx + OAuth)"
read_when:
- Running OpenClaw behind an identity-aware proxy

View File

@ -90,7 +90,7 @@ You can reference env vars directly in config string values using `${VAR_NAME}`
}
```
See [Configuration: Env var substitution](/gateway/configuration#env-var-substitution-in-config) for full details.
See [Configuration: Env var substitution](/gateway/configuration-reference#env-var-substitution) for full details.
## Secret refs vs `${ENV}` strings

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ title: "Help"
If you want a quick “get unstuck” flow, start here:
- **Troubleshooting:** [Start here](/help/troubleshooting)
- **Install sanity (Node/npm/PATH):** [Install](/install#nodejs--npm-path-sanity)
- **Install sanity (Node/npm/PATH):** [Install](/install/node#troubleshooting)
- **Gateway issues:** [Gateway troubleshooting](/gateway/troubleshooting)
- **Logs:** [Logging](/logging) and [Gateway logging](/gateway/logging)
- **Repairs:** [Doctor](/gateway/doctor)
@ -19,3 +19,10 @@ If you want a quick “get unstuck” flow, start here:
If youre looking for conceptual questions (not “something broke”):
- [FAQ (concepts)](/help/faq)
## Environment and debugging
- **Environment variables:** [Where OpenClaw loads env vars and precedence](/help/environment)
- **Debugging:** [Watch mode, raw streams, and dev profile](/help/debugging)
- **Testing:** [Test suites, live tests, and Docker runners](/help/testing)
- **Scripts:** [Repository helper scripts](/help/scripts)

View File

@ -308,7 +308,7 @@ This is the “common models” run we expect to keep working:
- OpenAI (non-Codex): `openai/gpt-5.2` (optional: `openai/gpt-5.1`)
- OpenAI Codex: `openai-codex/gpt-5.4`
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-5`)
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-6`)
- Google (Gemini API): `google/gemini-3.1-pro-preview` and `google/gemini-3-flash-preview` (avoid older Gemini 2.x models)
- Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash`
- Z.AI (GLM): `zai/glm-4.7`
@ -322,7 +322,7 @@ Run gateway smoke with tools + image:
Pick at least one per provider family:
- OpenAI: `openai/gpt-5.2` (or `openai/gpt-5-mini`)
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-5`)
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-6`)
- Google: `google/gemini-3-flash-preview` (or `google/gemini-3.1-pro-preview`)
- Z.AI (GLM): `zai/glm-4.7`
- MiniMax: `minimax/minimax-m2.5`

View File

@ -106,15 +106,19 @@ The Gateway is the single source of truth for sessions, routing, and channel con
openclaw onboard --install-daemon
```
</Step>
<Step title="Pair WhatsApp and start the Gateway">
<Step title="Chat">
Open the Control UI in your browser and send a message:
```bash
openclaw channels login
openclaw gateway --port 18789
openclaw dashboard
```
Or connect a channel ([Telegram](/channels/telegram) is fastest) and chat from your phone.
</Step>
</Steps>
Need the full install and dev setup? See [Quick start](/start/quickstart).
Need the full install and dev setup? See [Getting Started](/start/getting-started).
## Dashboard

View File

@ -9,7 +9,29 @@ title: "Ansible"
# Ansible Installation
The recommended way to deploy OpenClaw to production servers is via **[openclaw-ansible](https://github.com/openclaw/openclaw-ansible)** — an automated installer with security-first architecture.
Deploy OpenClaw to production servers with **[openclaw-ansible](https://github.com/openclaw/openclaw-ansible)** -- an automated installer with security-first architecture.
<Info>
The [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) repo is the source of truth for Ansible deployment. This page is a quick overview.
</Info>
## Prerequisites
| Requirement | Details |
| ----------- | --------------------------------------------------------- |
| **OS** | Debian 11+ or Ubuntu 20.04+ |
| **Access** | Root or sudo privileges |
| **Network** | Internet connection for package installation |
| **Ansible** | 2.14+ (installed automatically by the quick-start script) |
## What You Get
- **Firewall-first security** -- UFW + Docker isolation (only SSH + Tailscale accessible)
- **Tailscale VPN** -- secure remote access without exposing services publicly
- **Docker** -- isolated sandbox containers, localhost-only bindings
- **Defense in depth** -- 4-layer security architecture
- **Systemd integration** -- auto-start on boot with hardening
- **One-command setup** -- complete deployment in minutes
## Quick Start
@ -19,55 +41,50 @@ One-command install:
curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash
```
> **📦 Full guide: [github.com/openclaw/openclaw-ansible](https://github.com/openclaw/openclaw-ansible)**
>
> The openclaw-ansible repo is the source of truth for Ansible deployment. This page is a quick overview.
## What You Get
- 🔒 **Firewall-first security**: UFW + Docker isolation (only SSH + Tailscale accessible)
- 🔐 **Tailscale VPN**: Secure remote access without exposing services publicly
- 🐳 **Docker**: Isolated sandbox containers, localhost-only bindings
- 🛡️ **Defense in depth**: 4-layer security architecture
- 🚀 **One-command setup**: Complete deployment in minutes
- 🔧 **Systemd integration**: Auto-start on boot with hardening
## Requirements
- **OS**: Debian 11+ or Ubuntu 20.04+
- **Access**: Root or sudo privileges
- **Network**: Internet connection for package installation
- **Ansible**: 2.14+ (installed automatically by quick-start script)
## What Gets Installed
The Ansible playbook installs and configures:
1. **Tailscale** (mesh VPN for secure remote access)
2. **UFW firewall** (SSH + Tailscale ports only)
3. **Docker CE + Compose V2** (for agent sandboxes)
4. **Node.js 24 + pnpm** (runtime dependencies; Node 22 LTS, currently `22.16+`, remains supported for compatibility)
5. **OpenClaw** (host-based, not containerized)
6. **Systemd service** (auto-start with security hardening)
1. **Tailscale** -- mesh VPN for secure remote access
2. **UFW firewall** -- SSH + Tailscale ports only
3. **Docker CE + Compose V2** -- for agent sandboxes
4. **Node.js 24 + pnpm** -- runtime dependencies (Node 22 LTS, currently `22.16+`, remains supported)
5. **OpenClaw** -- host-based, not containerized
6. **Systemd service** -- auto-start with security hardening
Note: The gateway runs **directly on the host** (not in Docker), but agent sandboxes use Docker for isolation. See [Sandboxing](/gateway/sandboxing) for details.
<Note>
The gateway runs directly on the host (not in Docker), but agent sandboxes use Docker for isolation. See [Sandboxing](/gateway/sandboxing) for details.
</Note>
## Post-Install Setup
After installation completes, switch to the openclaw user:
<Steps>
<Step title="Switch to the openclaw user">
```bash
sudo -i -u openclaw
```
</Step>
<Step title="Run the onboarding wizard">
The post-install script guides you through configuring OpenClaw settings.
</Step>
<Step title="Connect messaging providers">
Log in to WhatsApp, Telegram, Discord, or Signal:
```bash
openclaw channels login
```
</Step>
<Step title="Verify the installation">
```bash
sudo systemctl status openclaw
sudo journalctl -u openclaw -f
```
</Step>
<Step title="Connect to Tailscale">
Join your VPN mesh for secure remote access.
</Step>
</Steps>
```bash
sudo -i -u openclaw
```
The post-install script will guide you through:
1. **Onboarding wizard**: Configure OpenClaw settings
2. **Provider login**: Connect WhatsApp/Telegram/Discord/Signal
3. **Gateway testing**: Verify the installation
4. **Tailscale setup**: Connect to your VPN mesh
### Quick commands
### Quick Commands
```bash
# Check service status
@ -86,115 +103,120 @@ openclaw channels login
## Security Architecture
### 4-Layer Defense
The deployment uses a 4-layer defense model:
1. **Firewall (UFW)**: Only SSH (22) + Tailscale (41641/udp) exposed publicly
2. **VPN (Tailscale)**: Gateway accessible only via VPN mesh
3. **Docker Isolation**: DOCKER-USER iptables chain prevents external port exposure
4. **Systemd Hardening**: NoNewPrivileges, PrivateTmp, unprivileged user
1. **Firewall (UFW)** -- only SSH (22) + Tailscale (41641/udp) exposed publicly
2. **VPN (Tailscale)** -- gateway accessible only via VPN mesh
3. **Docker isolation** -- DOCKER-USER iptables chain prevents external port exposure
4. **Systemd hardening** -- NoNewPrivileges, PrivateTmp, unprivileged user
### Verification
Test external attack surface:
To verify your external attack surface:
```bash
nmap -p- YOUR_SERVER_IP
```
Should show **only port 22** (SSH) open. All other services (gateway, Docker) are locked down.
Only port 22 (SSH) should be open. All other services (gateway, Docker) are locked down.
### Docker Availability
Docker is installed for **agent sandboxes** (isolated tool execution), not for running the gateway itself. The gateway binds to localhost only and is accessible via Tailscale VPN.
See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for sandbox configuration.
Docker is installed for agent sandboxes (isolated tool execution), not for running the gateway itself. See [Multi-Agent Sandbox and Tools](/tools/multi-agent-sandbox-tools) for sandbox configuration.
## Manual Installation
If you prefer manual control over the automation:
```bash
# 1. Install prerequisites
sudo apt update && sudo apt install -y ansible git
<Steps>
<Step title="Install prerequisites">
```bash
sudo apt update && sudo apt install -y ansible git
```
</Step>
<Step title="Clone the repository">
```bash
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
```
</Step>
<Step title="Install Ansible collections">
```bash
ansible-galaxy collection install -r requirements.yml
```
</Step>
<Step title="Run the playbook">
```bash
./run-playbook.sh
```
# 2. Clone repository
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
Alternatively, run directly and then manually execute the setup script afterward:
```bash
ansible-playbook playbook.yml --ask-become-pass
# Then run: /tmp/openclaw-setup.sh
```
# 3. Install Ansible collections
ansible-galaxy collection install -r requirements.yml
</Step>
</Steps>
# 4. Run playbook
./run-playbook.sh
# Or run directly (then manually execute /tmp/openclaw-setup.sh after)
# ansible-playbook playbook.yml --ask-become-pass
```
## Updating OpenClaw
## Updating
The Ansible installer sets up OpenClaw for manual updates. See [Updating](/install/updating) for the standard update flow.
To re-run the Ansible playbook (e.g., for configuration changes):
To re-run the Ansible playbook (for example, for configuration changes):
```bash
cd openclaw-ansible
./run-playbook.sh
```
Note: This is idempotent and safe to run multiple times.
This is idempotent and safe to run multiple times.
## Troubleshooting
### Firewall blocks my connection
<AccordionGroup>
<Accordion title="Firewall blocks my connection">
- Ensure you can access via Tailscale VPN first
- SSH access (port 22) is always allowed
- The gateway is only accessible via Tailscale by design
</Accordion>
<Accordion title="Service will not start">
```bash
# Check logs
sudo journalctl -u openclaw -n 100
If you're locked out:
# Verify permissions
sudo ls -la /opt/openclaw
- Ensure you can access via Tailscale VPN first
- SSH access (port 22) is always allowed
- The gateway is **only** accessible via Tailscale by design
# Test manual start
sudo -i -u openclaw
cd ~/openclaw
openclaw gateway run
```
### Service will not start
</Accordion>
<Accordion title="Docker sandbox issues">
```bash
# Verify Docker is running
sudo systemctl status docker
```bash
# Check logs
sudo journalctl -u openclaw -n 100
# Check sandbox image
sudo docker images | grep openclaw-sandbox
# Verify permissions
sudo ls -la /opt/openclaw
# Build sandbox image if missing
cd /opt/openclaw/openclaw
sudo -u openclaw ./scripts/sandbox-setup.sh
```
# Test manual start
sudo -i -u openclaw
cd ~/openclaw
pnpm start
```
### Docker sandbox issues
```bash
# Verify Docker is running
sudo systemctl status docker
# Check sandbox image
sudo docker images | grep openclaw-sandbox
# Build sandbox image if missing
cd /opt/openclaw/openclaw
sudo -u openclaw ./scripts/sandbox-setup.sh
```
### Provider login fails
Make sure you're running as the `openclaw` user:
```bash
sudo -i -u openclaw
openclaw channels login
```
</Accordion>
<Accordion title="Provider login fails">
Make sure you are running as the `openclaw` user:
```bash
sudo -i -u openclaw
openclaw channels login
```
</Accordion>
</AccordionGroup>
## Advanced Configuration
For detailed security architecture and troubleshooting:
For detailed security architecture and troubleshooting, see the openclaw-ansible repo:
- [Security Architecture](https://github.com/openclaw/openclaw-ansible/blob/main/docs/security.md)
- [Technical Details](https://github.com/openclaw/openclaw-ansible/blob/main/docs/architecture.md)
@ -202,7 +224,7 @@ For detailed security architecture and troubleshooting:
## Related
- [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) full deployment guide
- [Docker](/install/docker) containerized gateway setup
- [Sandboxing](/gateway/sandboxing) agent sandbox configuration
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) — per-agent isolation
- [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) -- full deployment guide
- [Docker](/install/docker) -- containerized gateway setup
- [Sandboxing](/gateway/sandboxing) -- agent sandbox configuration
- [Multi-Agent Sandbox and Tools](/tools/multi-agent-sandbox-tools) -- per-agent isolation

178
docs/install/azure.md Normal file
View File

@ -0,0 +1,178 @@
---
summary: "Run OpenClaw Gateway 24/7 on an Azure Linux VM with durable state"
read_when:
- You want OpenClaw running 24/7 on Azure with Network Security Group hardening
- You want a production-grade, always-on OpenClaw Gateway on your own Azure Linux VM
- You want secure administration with Azure Bastion SSH
- You want repeatable deployments with Azure Resource Manager templates
title: "Azure"
---
# OpenClaw on Azure Linux VM
This guide sets up an Azure Linux VM, applies Network Security Group (NSG) hardening, configures Azure Bastion (managed Azure SSH entry point), and installs OpenClaw.
## What youll do
- Deploy Azure compute and network resources with Azure Resource Manager (ARM) templates
- Apply Azure Network Security Group (NSG) rules so VM SSH is allowed only from Azure Bastion
- Use Azure Bastion for SSH access
- Install OpenClaw with the installer script
- Verify the Gateway
## Before you start
Youll need:
- An Azure subscription with permission to create compute and network resources
- Azure CLI installed (see [Azure CLI install steps](https://learn.microsoft.com/cli/azure/install-azure-cli) if needed)
<Steps>
<Step title="Sign in to Azure CLI">
```bash
az login # Sign in and select your Azure subscription
az extension add -n ssh # Extension required for Azure Bastion SSH management
```
</Step>
<Step title="Register required resource providers (one-time)">
```bash
az provider register --namespace Microsoft.Compute
az provider register --namespace Microsoft.Network
```
Verify Azure resource provider registration. Wait until both show `Registered`.
```bash
az provider show --namespace Microsoft.Compute --query registrationState -o tsv
az provider show --namespace Microsoft.Network --query registrationState -o tsv
```
</Step>
<Step title="Set deployment variables">
```bash
RG="rg-openclaw"
LOCATION="westus2"
TEMPLATE_URI="https://raw.githubusercontent.com/openclaw/openclaw/main/infra/azure/templates/azuredeploy.json"
PARAMS_URI="https://raw.githubusercontent.com/openclaw/openclaw/main/infra/azure/templates/azuredeploy.parameters.json"
```
</Step>
<Step title="Select SSH key">
Use your existing public key if you have one:
```bash
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
```
If you dont have an SSH key yet, run the following:
```bash
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519 -C "you@example.com"
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
```
</Step>
<Step title="Select VM size and OS disk size">
Set VM and disk sizing variables:
```bash
VM_SIZE="Standard_B2as_v2"
OS_DISK_SIZE_GB=64
```
Choose a VM size and OS disk size that are available in your Azure subscription/region and matches your workload:
- Start smaller for light usage and scale up later
- Use more vCPU/RAM/OS disk size for heavier automation, more channels, or larger model/tool workloads
- If a VM size is unavailable in your region or subscription quota, pick the closest available SKU
List VM sizes available in your target region:
```bash
az vm list-skus --location "${LOCATION}" --resource-type virtualMachines -o table
```
Check your current VM vCPU and OS disk size usage/quota:
```bash
az vm list-usage --location "${LOCATION}" -o table
```
</Step>
<Step title="Create the resource group">
```bash
az group create -n "${RG}" -l "${LOCATION}"
```
</Step>
<Step title="Deploy resources">
This command applies your selected SSH key, VM size, and OS disk size.
```bash
az deployment group create \
-g "${RG}" \
--template-uri "${TEMPLATE_URI}" \
--parameters "${PARAMS_URI}" \
--parameters location="${LOCATION}" \
--parameters vmSize="${VM_SIZE}" \
--parameters osDiskSizeGb="${OS_DISK_SIZE_GB}" \
--parameters sshPublicKey="${SSH_PUB_KEY}"
```
</Step>
<Step title="SSH into the VM through Azure Bastion">
```bash
RG="rg-openclaw"
VM_NAME="vm-openclaw"
BASTION_NAME="bas-openclaw"
ADMIN_USERNAME="openclaw"
VM_ID="$(az vm show -g "${RG}" -n "${VM_NAME}" --query id -o tsv)"
az network bastion ssh \
--name "${BASTION_NAME}" \
--resource-group "${RG}" \
--target-resource-id "${VM_ID}" \
--auth-type ssh-key \
--username "${ADMIN_USERNAME}" \
--ssh-key ~/.ssh/id_ed25519
```
</Step>
<Step title="Install OpenClaw (in the VM shell)">
```bash
curl -fsSL https://openclaw.ai/install.sh -o /tmp/openclaw-install.sh
bash /tmp/openclaw-install.sh
rm -f /tmp/openclaw-install.sh
openclaw --version
```
The installer script handles Node detection/installation and runs onboarding by default.
</Step>
<Step title="Verify the Gateway">
After onboarding completes:
```bash
openclaw gateway status
```
Most enterprise Azure teams already have GitHub Copilot licenses. If that is your case, we recommend choosing the GitHub Copilot provider in the OpenClaw onboarding wizard. See [GitHub Copilot provider](/providers/github-copilot).
The included ARM template uses Ubuntu image `version: "latest"` for convenience. If you need reproducible builds, pin a specific image version in `infra/azure/templates/azuredeploy.json` (you can list versions with `az vm image list --publisher Canonical --offer ubuntu-24_04-lts --sku server --all -o table`).
</Step>
</Steps>
## Next steps
- Set up messaging channels: [Channels](/channels)
- Pair local devices as nodes: [Nodes](/nodes)
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
- For more details on OpenClaw Azure deployment with the GitHub Copilot model provider: [OpenClaw on Azure with GitHub Copilot](https://github.com/johnsonshi/openclaw-azure-github-copilot)

View File

@ -6,49 +6,45 @@ read_when:
title: "Bun (Experimental)"
---
# Bun (experimental)
# Bun (Experimental)
Goal: run this repo with **Bun** (optional, not recommended for WhatsApp/Telegram)
without diverging from pnpm workflows.
<Warning>
Bun is **not recommended for gateway runtime** (known issues with WhatsApp and Telegram). Use Node for production.
</Warning>
⚠️ **Not recommended for Gateway runtime** (WhatsApp/Telegram bugs). Use Node for production.
## Status
- Bun is an optional local runtime for running TypeScript directly (`bun run …`, `bun --watch …`).
- `pnpm` is the default for builds and remains fully supported (and used by some docs tooling).
- Bun cannot use `pnpm-lock.yaml` and will ignore it.
Bun is an optional local runtime for running TypeScript directly (`bun run ...`, `bun --watch ...`). The default package manager remains `pnpm`, which is fully supported and used by docs tooling. Bun cannot use `pnpm-lock.yaml` and will ignore it.
## Install
Default:
<Steps>
<Step title="Install dependencies">
```sh
bun install
```
```sh
bun install
```
`bun.lock` / `bun.lockb` are gitignored, so there is no repo churn. To skip lockfile writes entirely:
Note: `bun.lock`/`bun.lockb` are gitignored, so theres no repo churn either way. If you want _no lockfile writes_:
```sh
bun install --no-save
```
```sh
bun install --no-save
```
</Step>
<Step title="Build and test">
```sh
bun run build
bun run vitest run
```
</Step>
</Steps>
## Build / Test (Bun)
## Lifecycle Scripts
```sh
bun run build
bun run vitest run
```
Bun blocks dependency lifecycle scripts unless explicitly trusted. For this repo, the commonly blocked scripts are not required:
## Bun lifecycle scripts (blocked by default)
- `@whiskeysockets/baileys` `preinstall` -- checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.16+`)
- `protobufjs` `postinstall` -- emits warnings about incompatible version schemes (no build artifacts)
Bun may block dependency lifecycle scripts unless explicitly trusted (`bun pm untrusted` / `bun pm trust`).
For this repo, the commonly blocked scripts are not required:
- `@whiskeysockets/baileys` `preinstall`: checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.16+`).
- `protobufjs` `postinstall`: emits warnings about incompatible version schemes (no build artifacts).
If you hit a real runtime issue that requires these scripts, trust them explicitly:
If you hit a runtime issue that requires these scripts, trust them explicitly:
```sh
bun pm trust @whiskeysockets/baileys protobufjs
@ -56,4 +52,4 @@ bun pm trust @whiskeysockets/baileys protobufjs
## Caveats
- Some scripts still hardcode pnpm (e.g. `docs:build`, `ui:*`, `protocol:check`). Run those via pnpm for now.
Some scripts still hardcode pnpm (for example `docs:build`, `ui:*`, `protocol:check`). Run those via pnpm for now.

View File

@ -4,7 +4,8 @@ read_when:
- You want to switch between stable/beta/dev
- You want to pin a specific version, tag, or SHA
- You are tagging or publishing prereleases
title: "Development Channels"
title: "Release Channels"
sidebarTitle: "Release Channels"
---
# Development channels

View File

@ -0,0 +1,129 @@
---
summary: "Host OpenClaw on a DigitalOcean Droplet"
read_when:
- Setting up OpenClaw on DigitalOcean
- Looking for a simple paid VPS for OpenClaw
title: "DigitalOcean"
---
# DigitalOcean
Run a persistent OpenClaw Gateway on a DigitalOcean Droplet.
## Prerequisites
- DigitalOcean account ([signup](https://cloud.digitalocean.com/registrations/new))
- SSH key pair (or willingness to use password auth)
- About 20 minutes
## Setup
<Steps>
<Step title="Create a Droplet">
<Warning>
Use a clean base image (Ubuntu 24.04 LTS). Avoid third-party Marketplace 1-click images unless you have reviewed their startup scripts and firewall defaults.
</Warning>
1. Log into [DigitalOcean](https://cloud.digitalocean.com/).
2. Click **Create > Droplets**.
3. Choose:
- **Region:** Closest to you
- **Image:** Ubuntu 24.04 LTS
- **Size:** Basic, Regular, 1 vCPU / 1 GB RAM / 25 GB SSD
- **Authentication:** SSH key (recommended) or password
4. Click **Create Droplet** and note the IP address.
</Step>
<Step title="Connect and install">
```bash
ssh root@YOUR_DROPLET_IP
apt update && apt upgrade -y
# Install Node.js 24
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt install -y nodejs
# Install OpenClaw
curl -fsSL https://openclaw.ai/install.sh | bash
openclaw --version
```
</Step>
<Step title="Run onboarding">
```bash
openclaw onboard --install-daemon
```
The wizard walks you through model auth, channel setup, gateway token generation, and daemon installation (systemd).
</Step>
<Step title="Add swap (recommended for 1 GB Droplets)">
```bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
```
</Step>
<Step title="Verify the gateway">
```bash
openclaw status
systemctl --user status openclaw-gateway.service
journalctl --user -u openclaw-gateway.service -f
```
</Step>
<Step title="Access the Control UI">
The gateway binds to loopback by default. Pick one of these options.
**Option A: SSH tunnel (simplest)**
```bash
# From your local machine
ssh -L 18789:localhost:18789 root@YOUR_DROPLET_IP
```
Then open `http://localhost:18789`.
**Option B: Tailscale Serve**
```bash
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
openclaw config set gateway.tailscale.mode serve
openclaw gateway restart
```
Then open `https://<magicdns>/` from any device on your tailnet.
**Option C: Tailnet bind (no Serve)**
```bash
openclaw config set gateway.bind tailnet
openclaw gateway restart
```
Then open `http://<tailscale-ip>:18789` (token required).
</Step>
</Steps>
## Troubleshooting
**Gateway will not start** -- Run `openclaw doctor --non-interactive` and check logs with `journalctl --user -u openclaw-gateway.service -n 50`.
**Port already in use** -- Run `lsof -i :18789` to find the process, then stop it.
**Out of memory** -- Verify swap is active with `free -h`. If still hitting OOM, use API-based models (Claude, GPT) rather than local models, or upgrade to a 2 GB Droplet.
## Next steps
- [Channels](/channels) -- connect Telegram, WhatsApp, Discord, and more
- [Gateway configuration](/gateway/configuration) -- all config options
- [Updating](/install/updating) -- keep OpenClaw up to date

View File

@ -71,6 +71,10 @@ ENV NODE_ENV=production
CMD ["node","dist/index.js"]
```
<Note>
The download URLs above are for x86_64 (amd64). For ARM-based VMs (e.g. Hetzner ARM, GCP Tau T2A), replace the download URLs with the appropriate ARM64 variants from each tool's release page.
</Note>
## Build and launch
```bash

File diff suppressed because it is too large Load Diff

View File

@ -16,9 +16,9 @@ This page assumes exe.dev's default **exeuntu** image. If you picked a different
1. [https://exe.new/openclaw](https://exe.new/openclaw)
2. Fill in your auth key/token as needed
3. Click on "Agent" next to your VM, and wait...
4. ???
5. Profit
3. Click on "Agent" next to your VM and wait for Shelley to finish provisioning
4. Open `https://<vm-name>.exe.xyz/` and paste your gateway token to authenticate
5. Approve any pending device pairing requests with `openclaw devices approve <requestId>`
## What you need

View File

@ -1,6 +1,5 @@
---
title: Fly.io
description: Deploy OpenClaw on Fly.io
summary: "Step-by-step Fly.io deployment for OpenClaw with persistent storage and HTTPS"
read_when:
- Deploying OpenClaw on Fly.io
@ -25,222 +24,228 @@ read_when:
3. Deploy with `fly deploy`
4. SSH in to create config or use Control UI
## 1) Create the Fly app
<Steps>
<Step title="Create the Fly app">
```bash
# Clone the repo
git clone https://github.com/openclaw/openclaw.git
cd openclaw
```bash
# Clone the repo
git clone https://github.com/openclaw/openclaw.git
cd openclaw
# Create a new Fly app (pick your own name)
fly apps create my-openclaw
# Create a new Fly app (pick your own name)
fly apps create my-openclaw
# Create a persistent volume (1GB is usually enough)
fly volumes create openclaw_data --size 1 --region iad
```
# Create a persistent volume (1GB is usually enough)
fly volumes create openclaw_data --size 1 --region iad
```
**Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose).
**Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose).
</Step>
## 2) Configure fly.toml
<Step title="Configure fly.toml">
Edit `fly.toml` to match your app name and requirements.
Edit `fly.toml` to match your app name and requirements.
**Security note:** The default config exposes a public URL. For a hardened deployment with no public IP, see [Private Deployment](#private-deployment-hardened) or use `fly.private.toml`.
**Security note:** The default config exposes a public URL. For a hardened deployment with no public IP, see [Private Deployment](#private-deployment-hardened) or use `fly.private.toml`.
```toml
app = "my-openclaw" # Your app name
primary_region = "iad"
```toml
app = "my-openclaw" # Your app name
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
OPENCLAW_PREFER_PNPM = "1"
OPENCLAW_STATE_DIR = "/data"
NODE_OPTIONS = "--max-old-space-size=1536"
[env]
NODE_ENV = "production"
OPENCLAW_PREFER_PNPM = "1"
OPENCLAW_STATE_DIR = "/data"
NODE_OPTIONS = "--max-old-space-size=1536"
[processes]
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
[processes]
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = false
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = false
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"
[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"
[mounts]
source = "openclaw_data"
destination = "/data"
```
[mounts]
source = "openclaw_data"
destination = "/data"
```
**Key settings:**
**Key settings:**
| Setting | Why |
| ------------------------------ | --------------------------------------------------------------------------- |
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
| `--allow-unconfigured` | Starts without a config file (you'll create one after) |
| `internal_port = 3000` | Must match `--port 3000` (or `OPENCLAW_GATEWAY_PORT`) for Fly health checks |
| `memory = "2048mb"` | 512MB is too small; 2GB recommended |
| `OPENCLAW_STATE_DIR = "/data"` | Persists state on the volume |
| Setting | Why |
| ------------------------------ | --------------------------------------------------------------------------- |
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
| `--allow-unconfigured` | Starts without a config file (you'll create one after) |
| `internal_port = 3000` | Must match `--port 3000` (or `OPENCLAW_GATEWAY_PORT`) for Fly health checks |
| `memory = "2048mb"` | 512MB is too small; 2GB recommended |
| `OPENCLAW_STATE_DIR = "/data"` | Persists state on the volume |
</Step>
## 3) Set secrets
<Step title="Set secrets">
```bash
# Required: Gateway token (for non-loopback binding)
fly secrets set OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32)
```bash
# Required: Gateway token (for non-loopback binding)
fly secrets set OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32)
# Model provider API keys
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
# Model provider API keys
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
# Optional: Other providers
fly secrets set OPENAI_API_KEY=sk-...
fly secrets set GOOGLE_API_KEY=...
# Optional: Other providers
fly secrets set OPENAI_API_KEY=sk-...
fly secrets set GOOGLE_API_KEY=...
# Channel tokens
fly secrets set DISCORD_BOT_TOKEN=MTQ...
```
# Channel tokens
fly secrets set DISCORD_BOT_TOKEN=MTQ...
```
**Notes:**
**Notes:**
- Non-loopback binds (`--bind lan`) require `OPENCLAW_GATEWAY_TOKEN` for security.
- Treat these tokens like passwords.
- **Prefer env vars over config file** for all API keys and tokens. This keeps secrets out of `openclaw.json` where they could be accidentally exposed or logged.
- Non-loopback binds (`--bind lan`) require `OPENCLAW_GATEWAY_TOKEN` for security.
- Treat these tokens like passwords.
- **Prefer env vars over config file** for all API keys and tokens. This keeps secrets out of `openclaw.json` where they could be accidentally exposed or logged.
</Step>
## 4) Deploy
<Step title="Deploy">
```bash
fly deploy
```
```bash
fly deploy
```
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
After deployment, verify:
After deployment, verify:
```bash
fly status
fly logs
```
```bash
fly status
fly logs
```
You should see:
You should see:
```
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
[discord] logged in to discord as xxx
```
```
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
[discord] logged in to discord as xxx
```
</Step>
## 5) Create config file
<Step title="Create config file">
SSH into the machine to create a proper config:
SSH into the machine to create a proper config:
```bash
fly ssh console
```
```bash
fly ssh console
```
Create the config directory and file:
Create the config directory and file:
```bash
mkdir -p /data
cat > /data/openclaw.json << 'EOF'
{
"agents": {
"defaults": {
"model": {
"primary": "anthropic/claude-opus-4-6",
"fallbacks": ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"]
},
"maxConcurrent": 4
},
"list": [
{
"id": "main",
"default": true
}
]
},
"auth": {
"profiles": {
"anthropic:default": { "mode": "token", "provider": "anthropic" },
"openai:default": { "mode": "token", "provider": "openai" }
}
},
"bindings": [
```bash
mkdir -p /data
cat > /data/openclaw.json << 'EOF'
{
"agentId": "main",
"match": { "channel": "discord" }
}
],
"channels": {
"discord": {
"enabled": true,
"groupPolicy": "allowlist",
"guilds": {
"YOUR_GUILD_ID": {
"channels": { "general": { "allow": true } },
"requireMention": false
"agents": {
"defaults": {
"model": {
"primary": "anthropic/claude-opus-4-6",
"fallbacks": ["anthropic/claude-sonnet-4-6", "openai/gpt-4o"]
},
"maxConcurrent": 4
},
"list": [
{
"id": "main",
"default": true
}
]
},
"auth": {
"profiles": {
"anthropic:default": { "mode": "token", "provider": "anthropic" },
"openai:default": { "mode": "token", "provider": "openai" }
}
}
},
"bindings": [
{
"agentId": "main",
"match": { "channel": "discord" }
}
],
"channels": {
"discord": {
"enabled": true,
"groupPolicy": "allowlist",
"guilds": {
"YOUR_GUILD_ID": {
"channels": { "general": { "allow": true } },
"requireMention": false
}
}
}
},
"gateway": {
"mode": "local",
"bind": "auto"
},
"meta": {}
}
},
"gateway": {
"mode": "local",
"bind": "auto"
},
"meta": {
"lastTouchedVersion": "2026.1.29"
}
}
EOF
```
EOF
```
**Note:** With `OPENCLAW_STATE_DIR=/data`, the config path is `/data/openclaw.json`.
**Note:** With `OPENCLAW_STATE_DIR=/data`, the config path is `/data/openclaw.json`.
**Note:** The Discord token can come from either:
**Note:** The Discord token can come from either:
- Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets)
- Config file: `channels.discord.token`
- Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets)
- Config file: `channels.discord.token`
If using env var, no need to add token to config. The gateway reads `DISCORD_BOT_TOKEN` automatically.
If using env var, no need to add token to config. The gateway reads `DISCORD_BOT_TOKEN` automatically.
Restart to apply:
Restart to apply:
```bash
exit
fly machine restart <machine-id>
```
```bash
exit
fly machine restart <machine-id>
```
## 6) Access the Gateway
</Step>
### Control UI
<Step title="Access the Gateway">
### Control UI
Open in browser:
Open in browser:
```bash
fly open
```
```bash
fly open
```
Or visit `https://my-openclaw.fly.dev/`
Or visit `https://my-openclaw.fly.dev/`
Paste your gateway token (the one from `OPENCLAW_GATEWAY_TOKEN`) to authenticate.
Paste your gateway token (the one from `OPENCLAW_GATEWAY_TOKEN`) to authenticate.
### Logs
### Logs
```bash
fly logs # Live logs
fly logs --no-tail # Recent logs
```
```bash
fly logs # Live logs
fly logs --no-tail # Recent logs
```
### SSH Console
### SSH Console
```bash
fly ssh console
```
```bash
fly ssh console
```
</Step>
</Steps>
## Troubleshooting
@ -442,22 +447,22 @@ If you need webhook callbacks (Twilio, Telnyx, etc.) without public exposure:
Example voice-call config with ngrok:
```json
```json5
{
"plugins": {
"entries": {
plugins: {
entries: {
"voice-call": {
"enabled": true,
"config": {
"provider": "twilio",
"tunnel": { "provider": "ngrok" },
"webhookSecurity": {
"allowedHosts": ["example.ngrok.app"]
}
}
}
}
}
enabled: true,
config: {
provider: "twilio",
tunnel: { provider: "ngrok" },
webhookSecurity: {
allowedHosts: ["example.ngrok.app"],
},
},
},
},
},
}
```
@ -488,3 +493,9 @@ With the recommended config (`shared-cpu-2x`, 2GB RAM):
- Free tier includes some allowance
See [Fly.io pricing](https://fly.io/docs/about/pricing/) for details.
## Next steps
- Set up messaging channels: [Channels](/channels)
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
- Keep OpenClaw up to date: [Updating](/install/updating)

View File

@ -65,274 +65,271 @@ For the generic Docker flow, see [Docker](/install/docker).
---
## 1) Install gcloud CLI (or use Console)
<Steps>
<Step title="Install gcloud CLI (or use Console)">
**Option A: gcloud CLI** (recommended for automation)
**Option A: gcloud CLI** (recommended for automation)
Install from [https://cloud.google.com/sdk/docs/install](https://cloud.google.com/sdk/docs/install)
Install from [https://cloud.google.com/sdk/docs/install](https://cloud.google.com/sdk/docs/install)
Initialize and authenticate:
Initialize and authenticate:
```bash
gcloud init
gcloud auth login
```
```bash
gcloud init
gcloud auth login
```
**Option B: Cloud Console**
**Option B: Cloud Console**
All steps can be done via the web UI at [https://console.cloud.google.com](https://console.cloud.google.com)
All steps can be done via the web UI at [https://console.cloud.google.com](https://console.cloud.google.com)
</Step>
---
<Step title="Create a GCP project">
**CLI:**
## 2) Create a GCP project
```bash
gcloud projects create my-openclaw-project --name="OpenClaw Gateway"
gcloud config set project my-openclaw-project
```
**CLI:**
Enable billing at [https://console.cloud.google.com/billing](https://console.cloud.google.com/billing) (required for Compute Engine).
```bash
gcloud projects create my-openclaw-project --name="OpenClaw Gateway"
gcloud config set project my-openclaw-project
```
Enable the Compute Engine API:
Enable billing at [https://console.cloud.google.com/billing](https://console.cloud.google.com/billing) (required for Compute Engine).
```bash
gcloud services enable compute.googleapis.com
```
Enable the Compute Engine API:
**Console:**
```bash
gcloud services enable compute.googleapis.com
```
1. Go to IAM & Admin > Create Project
2. Name it and create
3. Enable billing for the project
4. Navigate to APIs & Services > Enable APIs > search "Compute Engine API" > Enable
**Console:**
</Step>
1. Go to IAM & Admin > Create Project
2. Name it and create
3. Enable billing for the project
4. Navigate to APIs & Services > Enable APIs > search "Compute Engine API" > Enable
<Step title="Create the VM">
**Machine types:**
---
| Type | Specs | Cost | Notes |
| --------- | ------------------------ | ------------------ | -------------------------------------------- |
| e2-medium | 2 vCPU, 4GB RAM | ~$25/mo | Most reliable for local Docker builds |
| e2-small | 2 vCPU, 2GB RAM | ~$12/mo | Minimum recommended for Docker build |
| e2-micro | 2 vCPU (shared), 1GB RAM | Free tier eligible | Often fails with Docker build OOM (exit 137) |
## 3) Create the VM
**CLI:**
**Machine types:**
```bash
gcloud compute instances create openclaw-gateway \
--zone=us-central1-a \
--machine-type=e2-small \
--boot-disk-size=20GB \
--image-family=debian-12 \
--image-project=debian-cloud
```
| Type | Specs | Cost | Notes |
| --------- | ------------------------ | ------------------ | -------------------------------------------- |
| e2-medium | 2 vCPU, 4GB RAM | ~$25/mo | Most reliable for local Docker builds |
| e2-small | 2 vCPU, 2GB RAM | ~$12/mo | Minimum recommended for Docker build |
| e2-micro | 2 vCPU (shared), 1GB RAM | Free tier eligible | Often fails with Docker build OOM (exit 137) |
**Console:**
**CLI:**
1. Go to Compute Engine > VM instances > Create instance
2. Name: `openclaw-gateway`
3. Region: `us-central1`, Zone: `us-central1-a`
4. Machine type: `e2-small`
5. Boot disk: Debian 12, 20GB
6. Create
```bash
gcloud compute instances create openclaw-gateway \
--zone=us-central1-a \
--machine-type=e2-small \
--boot-disk-size=20GB \
--image-family=debian-12 \
--image-project=debian-cloud
```
</Step>
**Console:**
<Step title="SSH into the VM">
**CLI:**
1. Go to Compute Engine > VM instances > Create instance
2. Name: `openclaw-gateway`
3. Region: `us-central1`, Zone: `us-central1-a`
4. Machine type: `e2-small`
5. Boot disk: Debian 12, 20GB
6. Create
```bash
gcloud compute ssh openclaw-gateway --zone=us-central1-a
```
---
**Console:**
## 4) SSH into the VM
Click the "SSH" button next to your VM in the Compute Engine dashboard.
**CLI:**
Note: SSH key propagation can take 1-2 minutes after VM creation. If connection is refused, wait and retry.
```bash
gcloud compute ssh openclaw-gateway --zone=us-central1-a
```
</Step>
**Console:**
<Step title="Install Docker (on the VM)">
```bash
sudo apt-get update
sudo apt-get install -y git curl ca-certificates
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
```
Click the "SSH" button next to your VM in the Compute Engine dashboard.
Log out and back in for the group change to take effect:
Note: SSH key propagation can take 1-2 minutes after VM creation. If connection is refused, wait and retry.
```bash
exit
```
---
Then SSH back in:
## 5) Install Docker (on the VM)
```bash
gcloud compute ssh openclaw-gateway --zone=us-central1-a
```
```bash
sudo apt-get update
sudo apt-get install -y git curl ca-certificates
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
```
Verify:
Log out and back in for the group change to take effect:
```bash
docker --version
docker compose version
```
```bash
exit
```
</Step>
Then SSH back in:
<Step title="Clone the OpenClaw repository">
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
```
```bash
gcloud compute ssh openclaw-gateway --zone=us-central1-a
```
This guide assumes you will build a custom image to guarantee binary persistence.
Verify:
</Step>
```bash
docker --version
docker compose version
```
<Step title="Create persistent host directories">
Docker containers are ephemeral.
All long-lived state must live on the host.
---
```bash
mkdir -p ~/.openclaw
mkdir -p ~/.openclaw/workspace
```
## 6) Clone the OpenClaw repository
</Step>
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
```
<Step title="Configure environment variables">
Create `.env` in the repository root.
This guide assumes you will build a custom image to guarantee binary persistence.
```bash
OPENCLAW_IMAGE=openclaw:latest
OPENCLAW_GATEWAY_TOKEN=change-me-now
OPENCLAW_GATEWAY_BIND=lan
OPENCLAW_GATEWAY_PORT=18789
---
OPENCLAW_CONFIG_DIR=/home/$USER/.openclaw
OPENCLAW_WORKSPACE_DIR=/home/$USER/.openclaw/workspace
## 7) Create persistent host directories
GOG_KEYRING_PASSWORD=change-me-now
XDG_CONFIG_HOME=/home/node/.openclaw
```
Docker containers are ephemeral.
All long-lived state must live on the host.
Generate strong secrets:
```bash
mkdir -p ~/.openclaw
mkdir -p ~/.openclaw/workspace
```
```bash
openssl rand -hex 32
```
---
**Do not commit this file.**
## 8) Configure environment variables
</Step>
Create `.env` in the repository root.
<Step title="Docker Compose configuration">
Create or update `docker-compose.yml`.
```bash
OPENCLAW_IMAGE=openclaw:latest
OPENCLAW_GATEWAY_TOKEN=change-me-now
OPENCLAW_GATEWAY_BIND=lan
OPENCLAW_GATEWAY_PORT=18789
```yaml
services:
openclaw-gateway:
image: ${OPENCLAW_IMAGE}
build: .
restart: unless-stopped
env_file:
- .env
environment:
- HOME=/home/node
- NODE_ENV=production
- TERM=xterm-256color
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
ports:
# Recommended: keep the Gateway loopback-only on the VM; access via SSH tunnel.
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789"
command:
[
"node",
"dist/index.js",
"gateway",
"--bind",
"${OPENCLAW_GATEWAY_BIND}",
"--port",
"${OPENCLAW_GATEWAY_PORT}",
"--allow-unconfigured",
]
```
OPENCLAW_CONFIG_DIR=/home/$USER/.openclaw
OPENCLAW_WORKSPACE_DIR=/home/$USER/.openclaw/workspace
`--allow-unconfigured` is only for bootstrap convenience, it is not a replacement for a proper gateway configuration. Still set auth (`gateway.auth.token` or password) and use safe bind settings for your deployment.
GOG_KEYRING_PASSWORD=change-me-now
XDG_CONFIG_HOME=/home/node/.openclaw
```
</Step>
Generate strong secrets:
<Step title="Shared Docker VM runtime steps">
Use the shared runtime guide for the common Docker host flow:
```bash
openssl rand -hex 32
```
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
- [What persists where](/install/docker-vm-runtime#what-persists-where)
- [Updates](/install/docker-vm-runtime#updates)
**Do not commit this file.**
</Step>
---
<Step title="GCP-specific launch notes">
On GCP, if build fails with `Killed` or `exit code 137` during `pnpm install --frozen-lockfile`, the VM is out of memory. Use `e2-small` minimum, or `e2-medium` for more reliable first builds.
## 9) Docker Compose configuration
When binding to LAN (`OPENCLAW_GATEWAY_BIND=lan`), configure a trusted browser origin before continuing:
Create or update `docker-compose.yml`.
```bash
docker compose run --rm openclaw-cli config set gateway.controlUi.allowedOrigins '["http://127.0.0.1:18789"]' --strict-json
```
If you changed the gateway port, replace `18789` with your configured port.
```yaml
services:
openclaw-gateway:
image: ${OPENCLAW_IMAGE}
build: .
restart: unless-stopped
env_file:
- .env
environment:
- HOME=/home/node
- NODE_ENV=production
- TERM=xterm-256color
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
ports:
# Recommended: keep the Gateway loopback-only on the VM; access via SSH tunnel.
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789"
command:
[
"node",
"dist/index.js",
"gateway",
"--bind",
"${OPENCLAW_GATEWAY_BIND}",
"--port",
"${OPENCLAW_GATEWAY_PORT}",
]
```
</Step>
<Step title="Access from your laptop">
Create an SSH tunnel to forward the Gateway port:
---
```bash
gcloud compute ssh openclaw-gateway --zone=us-central1-a -- -L 18789:127.0.0.1:18789
```
Open in your browser:
## 10) Shared Docker VM runtime steps
`http://127.0.0.1:18789/`
Fetch a fresh tokenized dashboard link:
Use the shared runtime guide for the common Docker host flow:
```bash
docker compose run --rm openclaw-cli dashboard --no-open
```
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
- [What persists where](/install/docker-vm-runtime#what-persists-where)
- [Updates](/install/docker-vm-runtime#updates)
Paste the token from that URL.
If Control UI shows `unauthorized` or `disconnected (1008): pairing required`, approve the browser device:
---
## 11) GCP-specific launch notes
On GCP, if build fails with `Killed` or `exit code 137` during `pnpm install --frozen-lockfile`, the VM is out of memory. Use `e2-small` minimum, or `e2-medium` for more reliable first builds.
When binding to LAN (`OPENCLAW_GATEWAY_BIND=lan`), configure a trusted browser origin before continuing:
```bash
docker compose run --rm openclaw-cli config set gateway.controlUi.allowedOrigins '["http://127.0.0.1:18789"]' --strict-json
```
If you changed the gateway port, replace `18789` with your configured port.
## 12) Access from your laptop
Create an SSH tunnel to forward the Gateway port:
```bash
gcloud compute ssh openclaw-gateway --zone=us-central1-a -- -L 18789:127.0.0.1:18789
```
Open in your browser:
`http://127.0.0.1:18789/`
Fetch a fresh tokenized dashboard link:
```bash
docker compose run --rm openclaw-cli dashboard --no-open
```
Paste the token from that URL.
If Control UI shows `unauthorized` or `disconnected (1008): pairing required`, approve the browser device:
```bash
docker compose run --rm openclaw-cli devices list
docker compose run --rm openclaw-cli devices approve <requestId>
```
Need the shared persistence and update reference again?
See [Docker VM Runtime](/install/docker-vm-runtime#what-persists-where) and [Docker VM Runtime updates](/install/docker-vm-runtime#updates).
```bash
docker compose run --rm openclaw-cli devices list
docker compose run --rm openclaw-cli devices approve <requestId>
```
Need the shared persistence and update reference again?
See [Docker VM Runtime](/install/docker-vm-runtime#what-persists-where) and [Docker VM Runtime updates](/install/docker-vm-runtime#updates).
</Step>
</Steps>
---

View File

@ -72,162 +72,156 @@ For the generic Docker flow, see [Docker](/install/docker).
---
## 1) Provision the VPS
<Steps>
<Step title="Provision the VPS">
Create an Ubuntu or Debian VPS in Hetzner.
Create an Ubuntu or Debian VPS in Hetzner.
Connect as root:
Connect as root:
```bash
ssh root@YOUR_VPS_IP
```
```bash
ssh root@YOUR_VPS_IP
```
This guide assumes the VPS is stateful.
Do not treat it as disposable infrastructure.
This guide assumes the VPS is stateful.
Do not treat it as disposable infrastructure.
</Step>
---
<Step title="Install Docker (on the VPS)">
```bash
apt-get update
apt-get install -y git curl ca-certificates
curl -fsSL https://get.docker.com | sh
```
## 2) Install Docker (on the VPS)
Verify:
```bash
apt-get update
apt-get install -y git curl ca-certificates
curl -fsSL https://get.docker.com | sh
```
```bash
docker --version
docker compose version
```
Verify:
</Step>
```bash
docker --version
docker compose version
```
<Step title="Clone the OpenClaw repository">
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
```
---
This guide assumes you will build a custom image to guarantee binary persistence.
## 3) Clone the OpenClaw repository
</Step>
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
```
<Step title="Create persistent host directories">
Docker containers are ephemeral.
All long-lived state must live on the host.
This guide assumes you will build a custom image to guarantee binary persistence.
```bash
mkdir -p /root/.openclaw/workspace
---
# Set ownership to the container user (uid 1000):
chown -R 1000:1000 /root/.openclaw
```
## 4) Create persistent host directories
</Step>
Docker containers are ephemeral.
All long-lived state must live on the host.
<Step title="Configure environment variables">
Create `.env` in the repository root.
```bash
mkdir -p /root/.openclaw/workspace
```bash
OPENCLAW_IMAGE=openclaw:latest
OPENCLAW_GATEWAY_TOKEN=change-me-now
OPENCLAW_GATEWAY_BIND=lan
OPENCLAW_GATEWAY_PORT=18789
# Set ownership to the container user (uid 1000):
chown -R 1000:1000 /root/.openclaw
```
OPENCLAW_CONFIG_DIR=/root/.openclaw
OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace
---
GOG_KEYRING_PASSWORD=change-me-now
XDG_CONFIG_HOME=/home/node/.openclaw
```
## 5) Configure environment variables
Generate strong secrets:
Create `.env` in the repository root.
```bash
openssl rand -hex 32
```
```bash
OPENCLAW_IMAGE=openclaw:latest
OPENCLAW_GATEWAY_TOKEN=change-me-now
OPENCLAW_GATEWAY_BIND=lan
OPENCLAW_GATEWAY_PORT=18789
**Do not commit this file.**
OPENCLAW_CONFIG_DIR=/root/.openclaw
OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace
</Step>
GOG_KEYRING_PASSWORD=change-me-now
XDG_CONFIG_HOME=/home/node/.openclaw
```
<Step title="Docker Compose configuration">
Create or update `docker-compose.yml`.
Generate strong secrets:
```yaml
services:
openclaw-gateway:
image: ${OPENCLAW_IMAGE}
build: .
restart: unless-stopped
env_file:
- .env
environment:
- HOME=/home/node
- NODE_ENV=production
- TERM=xterm-256color
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
ports:
# Recommended: keep the Gateway loopback-only on the VPS; access via SSH tunnel.
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789"
command:
[
"node",
"dist/index.js",
"gateway",
"--bind",
"${OPENCLAW_GATEWAY_BIND}",
"--port",
"${OPENCLAW_GATEWAY_PORT}",
"--allow-unconfigured",
]
```
```bash
openssl rand -hex 32
```
`--allow-unconfigured` is only for bootstrap convenience, it is not a replacement for a proper gateway configuration. Still set auth (`gateway.auth.token` or password) and use safe bind settings for your deployment.
**Do not commit this file.**
</Step>
---
<Step title="Shared Docker VM runtime steps">
Use the shared runtime guide for the common Docker host flow:
## 6) Docker Compose configuration
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
- [What persists where](/install/docker-vm-runtime#what-persists-where)
- [Updates](/install/docker-vm-runtime#updates)
Create or update `docker-compose.yml`.
</Step>
```yaml
services:
openclaw-gateway:
image: ${OPENCLAW_IMAGE}
build: .
restart: unless-stopped
env_file:
- .env
environment:
- HOME=/home/node
- NODE_ENV=production
- TERM=xterm-256color
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
ports:
# Recommended: keep the Gateway loopback-only on the VPS; access via SSH tunnel.
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789"
command:
[
"node",
"dist/index.js",
"gateway",
"--bind",
"${OPENCLAW_GATEWAY_BIND}",
"--port",
"${OPENCLAW_GATEWAY_PORT}",
"--allow-unconfigured",
]
```
<Step title="Hetzner-specific access">
After the shared build and launch steps, tunnel from your laptop:
`--allow-unconfigured` is only for bootstrap convenience, it is not a replacement for a proper gateway configuration. Still set auth (`gateway.auth.token` or password) and use safe bind settings for your deployment.
```bash
ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP
```
---
Open:
## 7) Shared Docker VM runtime steps
`http://127.0.0.1:18789/`
Use the shared runtime guide for the common Docker host flow:
Paste your gateway token.
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
- [What persists where](/install/docker-vm-runtime#what-persists-where)
- [Updates](/install/docker-vm-runtime#updates)
---
## 8) Hetzner-specific access
After the shared build and launch steps, tunnel from your laptop:
```bash
ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP
```
Open:
`http://127.0.0.1:18789/`
Paste your gateway token.
---
</Step>
</Steps>
The shared persistence map lives in [Docker VM Runtime](/install/docker-vm-runtime#what-persists-where).
@ -249,3 +243,9 @@ For teams preferring infrastructure-as-code workflows, a community-maintained Te
This approach complements the Docker setup above with reproducible deployments, version-controlled infrastructure, and automated disaster recovery.
> **Note:** Community-maintained. For issues or contributions, see the repository links above.
## Next steps
- Set up messaging channels: [Channels](/channels)
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
- Keep OpenClaw up to date: [Updating](/install/updating)

View File

@ -9,158 +9,113 @@ title: "Install"
# Install
Already followed [Getting Started](/start/getting-started)? You're all set — this page is for alternative install methods, platform-specific instructions, and maintenance.
## Recommended: installer script
The fastest way to install. It detects your OS, installs Node if needed, installs OpenClaw, and launches onboarding.
<Tabs>
<Tab title="macOS / Linux / WSL2">
```bash
curl -fsSL https://openclaw.ai/install.sh | bash
```
</Tab>
<Tab title="Windows (PowerShell)">
```powershell
iwr -useb https://openclaw.ai/install.ps1 | iex
```
</Tab>
</Tabs>
To install without running onboarding:
<Tabs>
<Tab title="macOS / Linux / WSL2">
```bash
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
```
</Tab>
<Tab title="Windows (PowerShell)">
```powershell
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard
```
</Tab>
</Tabs>
For all flags and CI/automation options, see [Installer internals](/install/installer).
## System requirements
- **[Node 24 (recommended)](/install/node)** (Node 22 LTS, currently `22.16+`, is still supported for compatibility; the [installer script](#install-methods) will install Node 24 if missing)
- macOS, Linux, or Windows
- `pnpm` only if you build from source
- **Node 24** (recommended) or Node 22.16+ — the installer script handles this automatically
- **macOS, Linux, or Windows** — both native Windows and WSL2 are supported; WSL2 is more stable. See [Windows](/platforms/windows).
- `pnpm` is only needed if you build from source
<Note>
On Windows, we strongly recommend running OpenClaw under [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install).
</Note>
## Alternative install methods
## Install methods
### npm or pnpm
<Tip>
The **installer script** is the recommended way to install OpenClaw. It handles Node detection, installation, and onboarding in one step.
</Tip>
<Warning>
For VPS/cloud hosts, avoid third-party "1-click" marketplace images when possible. Prefer a clean base OS image (for example Ubuntu LTS), then install OpenClaw yourself with the installer script.
</Warning>
<AccordionGroup>
<Accordion title="Installer script" icon="rocket" defaultOpen>
Downloads the CLI, installs it globally via npm, and launches onboarding.
<Tabs>
<Tab title="macOS / Linux / WSL2">
```bash
curl -fsSL https://openclaw.ai/install.sh | bash
```
</Tab>
<Tab title="Windows (PowerShell)">
```powershell
iwr -useb https://openclaw.ai/install.ps1 | iex
```
</Tab>
</Tabs>
That's it — the script handles Node detection, installation, and onboarding.
To skip onboarding and just install the binary:
<Tabs>
<Tab title="macOS / Linux / WSL2">
```bash
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
```
</Tab>
<Tab title="Windows (PowerShell)">
```powershell
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard
```
</Tab>
</Tabs>
For all flags, env vars, and CI/automation options, see [Installer internals](/install/installer).
</Accordion>
<Accordion title="npm / pnpm" icon="package">
If you already manage Node yourself, we recommend Node 24. OpenClaw still supports Node 22 LTS, currently `22.16+`, for compatibility:
<Tabs>
<Tab title="npm">
```bash
npm install -g openclaw@latest
openclaw onboard --install-daemon
```
<Accordion title="sharp build errors?">
If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails, force prebuilt binaries:
```bash
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest
```
If you see `sharp: Please add node-gyp to your dependencies`, either install build tooling (macOS: Xcode CLT + `npm install -g node-gyp`) or use the env var above.
</Accordion>
</Tab>
<Tab title="pnpm">
```bash
pnpm add -g openclaw@latest
pnpm approve-builds -g # approve openclaw, node-llama-cpp, sharp, etc.
openclaw onboard --install-daemon
```
<Note>
pnpm requires explicit approval for packages with build scripts. After the first install shows the "Ignored build scripts" warning, run `pnpm approve-builds -g` and select the listed packages.
</Note>
</Tab>
</Tabs>
Want the current GitHub `main` head with a package-manager install?
If you already manage Node yourself:
<Tabs>
<Tab title="npm">
```bash
npm install -g github:openclaw/openclaw#main
npm install -g openclaw@latest
openclaw onboard --install-daemon
```
</Tab>
<Tab title="pnpm">
```bash
pnpm add -g openclaw@latest
pnpm approve-builds -g
openclaw onboard --install-daemon
```
```bash
pnpm add -g github:openclaw/openclaw#main
```
<Note>
pnpm requires explicit approval for packages with build scripts. Run `pnpm approve-builds -g` after the first install.
</Note>
</Accordion>
</Tab>
</Tabs>
<Accordion title="From source" icon="github">
For contributors or anyone who wants to run from a local checkout.
<Accordion title="Troubleshooting: sharp build errors (npm)">
If `sharp` fails due to a globally installed libvips:
<Steps>
<Step title="Clone and build">
Clone the [OpenClaw repo](https://github.com/openclaw/openclaw) and build:
```bash
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest
```
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm ui:build
pnpm build
```
</Step>
<Step title="Link the CLI">
Make the `openclaw` command available globally:
</Accordion>
```bash
pnpm link --global
```
### From source
Alternatively, skip the link and run commands via `pnpm openclaw ...` from inside the repo.
</Step>
<Step title="Run onboarding">
```bash
openclaw onboard --install-daemon
```
</Step>
</Steps>
For contributors or anyone who wants to run from a local checkout:
For deeper development workflows, see [Setup](/start/setup).
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install && pnpm ui:build && pnpm build
pnpm link --global
openclaw onboard --install-daemon
```
</Accordion>
</AccordionGroup>
Or skip the link and use `pnpm openclaw ...` from inside the repo. See [Setup](/start/setup) for full development workflows.
## Other install methods
### Install from GitHub main
```bash
npm install -g github:openclaw/openclaw#main
```
### Containers and package managers
<CardGroup cols={2}>
<Card title="Docker" href="/install/docker" icon="container">
Containerized or headless deployments.
</Card>
<Card title="Podman" href="/install/podman" icon="container">
Rootless container: run `setup-podman.sh` once, then the launch script.
Rootless container alternative to Docker.
</Card>
<Card title="Nix" href="/install/nix" icon="snowflake">
Declarative install via Nix.
Declarative install via Nix flake.
</Card>
<Card title="Ansible" href="/install/ansible" icon="server">
Automated fleet provisioning.
@ -170,50 +125,32 @@ For VPS/cloud hosts, avoid third-party "1-click" marketplace images when possibl
</Card>
</CardGroup>
## After install
Verify everything is working:
## Verify the install
```bash
openclaw --version # confirm the CLI is available
openclaw doctor # check for config issues
openclaw status # gateway status
openclaw dashboard # open the browser UI
openclaw gateway status # verify the Gateway is running
```
If you need custom runtime paths, use:
## Hosting and deployment
- `OPENCLAW_HOME` for home-directory based internal paths
- `OPENCLAW_STATE_DIR` for mutable state location
- `OPENCLAW_CONFIG_PATH` for config file location
Deploy OpenClaw on a cloud server or VPS:
See [Environment vars](/help/environment) for precedence and full details.
<CardGroup cols={3}>
<Card title="VPS" href="/vps">Any Linux VPS</Card>
<Card title="Docker VM" href="/install/docker-vm-runtime">Shared Docker steps</Card>
<Card title="Kubernetes" href="/install/kubernetes">K8s</Card>
<Card title="Fly.io" href="/install/fly">Fly.io</Card>
<Card title="Hetzner" href="/install/hetzner">Hetzner</Card>
<Card title="GCP" href="/install/gcp">Google Cloud</Card>
<Card title="Azure" href="/install/azure">Azure</Card>
<Card title="Railway" href="/install/railway">Railway</Card>
<Card title="Render" href="/install/render">Render</Card>
<Card title="Northflank" href="/install/northflank">Northflank</Card>
</CardGroup>
## Troubleshooting: `openclaw` not found
<Accordion title="PATH diagnosis and fix">
Quick diagnosis:
```bash
node -v
npm -v
npm prefix -g
echo "$PATH"
```
If `$(npm prefix -g)/bin` (macOS/Linux) or `$(npm prefix -g)` (Windows) is **not** in your `$PATH`, your shell can't find global npm binaries (including `openclaw`).
Fix — add it to your shell startup file (`~/.zshrc` or `~/.bashrc`):
```bash
export PATH="$(npm prefix -g)/bin:$PATH"
```
On Windows, add the output of `npm prefix -g` to your PATH.
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
</Accordion>
## Update / uninstall
## Update, migrate, or uninstall
<CardGroup cols={3}>
<Card title="Updating" href="/install/updating" icon="refresh-cw">
@ -226,3 +163,21 @@ Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
Remove OpenClaw completely.
</Card>
</CardGroup>
## Troubleshooting: `openclaw` not found
If the install succeeded but `openclaw` is not found in your terminal:
```bash
node -v # Node installed?
npm prefix -g # Where are global packages?
echo "$PATH" # Is the global bin dir in PATH?
```
If `$(npm prefix -g)/bin` is not in your `$PATH`, add it to your shell startup file (`~/.zshrc` or `~/.bashrc`):
```bash
export PATH="$(npm prefix -g)/bin:$PATH"
```
Then open a new terminal. See [Node setup](/install/node) for more details.

View File

@ -180,7 +180,7 @@ Designed for environments where you want everything under a local prefix (defaul
<Steps>
<Step title="Install local Node runtime">
Downloads a pinned supported Node tarball (currently default `22.22.0`) to `<prefix>/tools/node-v<version>` and verifies SHA-256.
Downloads a pinned supported Node LTS tarball (the version is embedded in the script and updated independently) to `<prefix>/tools/node-v<version>` and verifies SHA-256.
</Step>
<Step title="Ensure Git">
If Git is missing, attempts install via apt/dnf/yum on Linux or Homebrew on macOS.

View File

@ -138,7 +138,7 @@ OPENCLAW_NAMESPACE=my-namespace ./scripts/k8s/deploy.sh
Edit the `image` field in `scripts/k8s/manifests/deployment.yaml`:
```yaml
image: ghcr.io/openclaw/openclaw:2026.3.1
image: ghcr.io/openclaw/openclaw:latest # or pin to a specific version from https://github.com/openclaw/openclaw/releases
```
### Expose beyond port-forward

View File

@ -155,17 +155,17 @@ nano ~/.openclaw/openclaw.json
Add your channels:
```json
```json5
{
"channels": {
"whatsapp": {
"dmPolicy": "allowlist",
"allowFrom": ["+15551234567"]
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551234567"],
},
"telegram": {
"botToken": "YOUR_BOT_TOKEN"
}
}
telegram: {
botToken: "YOUR_BOT_TOKEN",
},
},
}
```
@ -209,15 +209,15 @@ Inside the VM:
Add to your OpenClaw config:
```json
```json5
{
"channels": {
"bluebubbles": {
"serverUrl": "http://localhost:1234",
"password": "your-api-password",
"webhookPath": "/bluebubbles-webhook"
}
}
channels: {
bluebubbles: {
serverUrl: "http://localhost:1234",
password: "your-api-password",
webhookPath: "/bluebubbles-webhook",
},
},
}
```

View File

@ -204,7 +204,9 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins
- Meaning: OpenClaw found a helper file path that escapes the plugin root or fails plugin boundary checks, so it refused to import it.
- What to do: reinstall the Matrix plugin from a trusted path, then rerun `openclaw doctor --fix` or restart the gateway.
`gateway: failed creating a Matrix migration snapshot; skipping Matrix migration for now: ...`
`- Failed creating a Matrix migration snapshot before repair: ...`
`- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".`
- Meaning: OpenClaw refused to mutate Matrix state because it could not create the recovery snapshot first.
- What to do: resolve the backup error, then rerun `openclaw doctor --fix` or restart the gateway.
@ -236,7 +238,7 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins
- Meaning: backup exists, but OpenClaw could not recover the recovery key automatically.
- What to do: run `openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"`.
`Failed inspecting legacy Matrix encrypted state for account "...": ...`
`Failed inspecting legacy Matrix encrypted state for account "..." (...): ...`
- Meaning: OpenClaw found the old encrypted store, but it could not inspect it safely enough to prepare recovery.
- What to do: rerun `openclaw doctor --fix`. If it repeats, keep the old state directory intact and recover using another verified Matrix client plus `openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"`.

View File

@ -6,187 +6,105 @@ read_when:
title: "Migration Guide"
---
# Migrating OpenClaw to a new machine
# Migrating OpenClaw to a New Machine
This guide migrates an OpenClaw Gateway from one machine to another **without redoing onboarding**.
This guide moves an OpenClaw gateway to a new machine without redoing onboarding.
The migration is simple conceptually:
## What Gets Migrated
- Copy the **state directory** (`$OPENCLAW_STATE_DIR`, default: `~/.openclaw/`) — this includes config, auth, sessions, and channel state.
- Copy your **workspace** (`~/.openclaw/workspace/` by default) — this includes your agent files (memory, prompts, etc.).
When you copy the **state directory** (`~/.openclaw/` by default) and your **workspace**, you preserve:
But there are common footguns around **profiles**, **permissions**, and **partial copies**.
- **Config** -- `openclaw.json` and all gateway settings
- **Auth** -- API keys, OAuth tokens, credential profiles
- **Sessions** -- conversation history and agent state
- **Channel state** -- WhatsApp login, Telegram session, etc.
- **Workspace files** -- `MEMORY.md`, `USER.md`, skills, and prompts
## Before you start (what you are migrating)
<Tip>
Run `openclaw status` on the old machine to confirm your state directory path.
Custom profiles use `~/.openclaw-<profile>/` or a path set via `OPENCLAW_STATE_DIR`.
</Tip>
### 1) Identify your state directory
## Migration Steps
Most installs use the default:
<Steps>
<Step title="Stop the gateway and back up">
On the **old** machine, stop the gateway so files are not changing mid-copy, then archive:
- **State dir:** `~/.openclaw/`
```bash
openclaw gateway stop
cd ~
tar -czf openclaw-state.tgz .openclaw
```
But it may be different if you use:
If you use multiple profiles (e.g. `~/.openclaw-work`), archive each separately.
- `--profile <name>` (often becomes `~/.openclaw-<profile>/`)
- `OPENCLAW_STATE_DIR=/some/path`
</Step>
If youre not sure, run on the **old** machine:
<Step title="Install OpenClaw on the new machine">
[Install](/install) the CLI (and Node if needed) on the new machine.
It is fine if onboarding creates a fresh `~/.openclaw/` -- you will overwrite it next.
</Step>
```bash
openclaw status
```
<Step title="Copy state directory and workspace">
Transfer the archive via `scp`, `rsync -a`, or an external drive, then extract:
Look for mentions of `OPENCLAW_STATE_DIR` / profile in the output. If you run multiple gateways, repeat for each profile.
```bash
cd ~
tar -xzf openclaw-state.tgz
```
### 2) Identify your workspace
Ensure hidden directories were included and file ownership matches the user that will run the gateway.
Common defaults:
</Step>
- `~/.openclaw/workspace/` (recommended workspace)
- a custom folder you created
<Step title="Run doctor and verify">
On the new machine, run [Doctor](/gateway/doctor) to apply config migrations and repair services:
Your workspace is where files like `MEMORY.md`, `USER.md`, and `memory/*.md` live.
```bash
openclaw doctor
openclaw gateway restart
openclaw status
```
### 3) Understand what you will preserve
</Step>
</Steps>
If you copy **both** the state dir and workspace, you keep:
## Common Pitfalls
- Gateway configuration (`openclaw.json`)
- Auth profiles / API keys / OAuth tokens
- Session history + agent state
- Channel state (e.g. WhatsApp login/session)
- Your workspace files (memory, skills notes, etc.)
<AccordionGroup>
<Accordion title="Profile or state-dir mismatch">
If the old gateway used `--profile` or `OPENCLAW_STATE_DIR` and the new one does not,
channels will appear logged out and sessions will be empty.
Launch the gateway with the **same** profile or state-dir you migrated, then rerun `openclaw doctor`.
</Accordion>
If you copy **only** the workspace (e.g., via Git), you do **not** preserve:
<Accordion title="Copying only openclaw.json">
The config file alone is not enough. Credentials live under `credentials/`, and agent
state lives under `agents/`. Always migrate the **entire** state directory.
</Accordion>
- sessions
- credentials
- channel logins
<Accordion title="Permissions and ownership">
If you copied as root or switched users, the gateway may fail to read credentials.
Ensure the state directory and workspace are owned by the user running the gateway.
</Accordion>
Those live under `$OPENCLAW_STATE_DIR`.
<Accordion title="Remote mode">
If your UI points at a **remote** gateway, the remote host owns sessions and workspace.
Migrate the gateway host itself, not your local laptop. See [FAQ](/help/faq#where-does-openclaw-store-its-data).
</Accordion>
## Migration steps (recommended)
<Accordion title="Secrets in backups">
The state directory contains API keys, OAuth tokens, and channel credentials.
Store backups encrypted, avoid insecure transfer channels, and rotate keys if you suspect exposure.
</Accordion>
</AccordionGroup>
### Step 0 - Make a backup (old machine)
On the **old** machine, stop the gateway first so files arent changing mid-copy:
```bash
openclaw gateway stop
```
(Optional but recommended) archive the state dir and workspace:
```bash
# Adjust paths if you use a profile or custom locations
cd ~
tar -czf openclaw-state.tgz .openclaw
tar -czf openclaw-workspace.tgz .openclaw/workspace
```
If you have multiple profiles/state dirs (e.g. `~/.openclaw-main`, `~/.openclaw-work`), archive each.
### Step 1 - Install OpenClaw on the new machine
On the **new** machine, install the CLI (and Node if needed):
- See: [Install](/install)
At this stage, its OK if onboarding creates a fresh `~/.openclaw/` — you will overwrite it in the next step.
### Step 2 - Copy the state dir + workspace to the new machine
Copy **both**:
- `$OPENCLAW_STATE_DIR` (default `~/.openclaw/`)
- your workspace (default `~/.openclaw/workspace/`)
Common approaches:
- `scp` the tarballs and extract
- `rsync -a` over SSH
- external drive
After copying, ensure:
- Hidden directories were included (e.g. `.openclaw/`)
- File ownership is correct for the user running the gateway
### Step 3 - Run Doctor (migrations + service repair)
On the **new** machine:
```bash
openclaw doctor
```
Doctor is the “safe boring” command. It repairs services, applies config migrations, and warns about mismatches.
Then:
```bash
openclaw gateway restart
openclaw status
```
## Common footguns (and how to avoid them)
### Footgun: profile / state-dir mismatch
If you ran the old gateway with a profile (or `OPENCLAW_STATE_DIR`), and the new gateway uses a different one, youll see symptoms like:
- config changes not taking effect
- channels missing / logged out
- empty session history
Fix: run the gateway/service using the **same** profile/state dir you migrated, then rerun:
```bash
openclaw doctor
```
### Footgun: copying only `openclaw.json`
`openclaw.json` is not enough. Many providers store state under:
- `$OPENCLAW_STATE_DIR/credentials/`
- `$OPENCLAW_STATE_DIR/agents/<agentId>/...`
Always migrate the entire `$OPENCLAW_STATE_DIR` folder.
### Footgun: permissions / ownership
If you copied as root or changed users, the gateway may fail to read credentials/sessions.
Fix: ensure the state dir + workspace are owned by the user running the gateway.
### Footgun: migrating between remote/local modes
- If your UI (WebUI/TUI) points at a **remote** gateway, the remote host owns the session store + workspace.
- Migrating your laptop wont move the remote gateways state.
If youre in remote mode, migrate the **gateway host**.
### Footgun: secrets in backups
`$OPENCLAW_STATE_DIR` contains secrets (API keys, OAuth tokens, WhatsApp creds). Treat backups like production secrets:
- store encrypted
- avoid sharing over insecure channels
- rotate keys if you suspect exposure
## Verification checklist
## Verification Checklist
On the new machine, confirm:
- `openclaw status` shows the gateway running
- Your channels are still connected (e.g. WhatsApp doesnt require re-pair)
- The dashboard opens and shows existing sessions
- Your workspace files (memory, configs) are present
## Related
- [Doctor](/gateway/doctor)
- [Gateway troubleshooting](/gateway/troubleshooting)
- [Where does OpenClaw store its data?](/help/faq#where-does-openclaw-store-its-data)
- [ ] `openclaw status` shows the gateway running
- [ ] Channels are still connected (no re-pairing needed)
- [ ] The dashboard opens and shows existing sessions
- [ ] Workspace files (memory, configs) are present

View File

@ -9,90 +9,81 @@ title: "Nix"
# Nix Installation
The recommended way to run OpenClaw with Nix is via **[nix-openclaw](https://github.com/openclaw/nix-openclaw)** — a batteries-included Home Manager module.
Install OpenClaw declaratively with **[nix-openclaw](https://github.com/openclaw/nix-openclaw)** -- a batteries-included Home Manager module.
## Quick Start
<Info>
The [nix-openclaw](https://github.com/openclaw/nix-openclaw) repo is the source of truth for Nix installation. This page is a quick overview.
</Info>
Paste this to your AI agent (Claude, Cursor, etc.):
## What You Get
```text
I want to set up nix-openclaw on my Mac.
Repository: github:openclaw/nix-openclaw
What I need you to do:
1. Check if Determinate Nix is installed (if not, install it)
2. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix
3. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot)
4. Set up secrets (bot token, model provider API key) - plain files at ~/.secrets/ is fine
5. Fill in the template placeholders and run home-manager switch
6. Verify: launchd running, bot responds to messages
Reference the nix-openclaw README for module options.
```
> **📦 Full guide: [github.com/openclaw/nix-openclaw](https://github.com/openclaw/nix-openclaw)**
>
> The nix-openclaw repo is the source of truth for Nix installation. This page is just a quick overview.
## What you get
- Gateway + macOS app + tools (whisper, spotify, cameras) — all pinned
- Gateway + macOS app + tools (whisper, spotify, cameras) -- all pinned
- Launchd service that survives reboots
- Plugin system with declarative config
- Instant rollback: `home-manager switch --rollback`
---
## Quick Start
<Steps>
<Step title="Install Determinate Nix">
If Nix is not already installed, follow the [Determinate Nix installer](https://github.com/DeterminateSystems/nix-installer) instructions.
</Step>
<Step title="Create a local flake">
Use the agent-first template from the nix-openclaw repo:
```bash
mkdir -p ~/code/openclaw-local
# Copy templates/agent-first/flake.nix from the nix-openclaw repo
```
</Step>
<Step title="Configure secrets">
Set up your messaging bot token and model provider API key. Plain files at `~/.secrets/` work fine.
</Step>
<Step title="Fill in template placeholders and switch">
```bash
home-manager switch
```
</Step>
<Step title="Verify">
Confirm the launchd service is running and your bot responds to messages.
</Step>
</Steps>
See the [nix-openclaw README](https://github.com/openclaw/nix-openclaw) for full module options and examples.
## Nix Mode Runtime Behavior
When `OPENCLAW_NIX_MODE=1` is set (automatic with nix-openclaw):
When `OPENCLAW_NIX_MODE=1` is set (automatic with nix-openclaw), OpenClaw enters a deterministic mode that disables auto-install flows.
OpenClaw supports a **Nix mode** that makes configuration deterministic and disables auto-install flows.
Enable it by exporting:
You can also set it manually:
```bash
OPENCLAW_NIX_MODE=1
export OPENCLAW_NIX_MODE=1
```
On macOS, the GUI app does not automatically inherit shell env vars. You can
also enable Nix mode via defaults:
On macOS, the GUI app does not automatically inherit shell environment variables. Enable Nix mode via defaults instead:
```bash
defaults write ai.openclaw.mac openclaw.nixMode -bool true
```
### Config + state paths
OpenClaw reads JSON5 config from `OPENCLAW_CONFIG_PATH` and stores mutable data in `OPENCLAW_STATE_DIR`.
When needed, you can also set `OPENCLAW_HOME` to control the base home directory used for internal path resolution.
- `OPENCLAW_HOME` (default precedence: `HOME` / `USERPROFILE` / `os.homedir()`)
- `OPENCLAW_STATE_DIR` (default: `~/.openclaw`)
- `OPENCLAW_CONFIG_PATH` (default: `$OPENCLAW_STATE_DIR/openclaw.json`)
When running under Nix, set these explicitly to Nix-managed locations so runtime state and config
stay out of the immutable store.
### Runtime behavior in Nix mode
### What changes in Nix mode
- Auto-install and self-mutation flows are disabled
- Missing dependencies surface Nix-specific remediation messages
- UI surfaces a read-only Nix mode banner when present
- UI surfaces a read-only Nix mode banner
## Packaging note (macOS)
### Config and state paths
The macOS packaging flow expects a stable Info.plist template at:
OpenClaw reads JSON5 config from `OPENCLAW_CONFIG_PATH` and stores mutable data in `OPENCLAW_STATE_DIR`. When running under Nix, set these explicitly to Nix-managed locations so runtime state and config stay out of the immutable store.
```
apps/macos/Sources/OpenClaw/Resources/Info.plist
```
[`scripts/package-mac-app.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/package-mac-app.sh) copies this template into the app bundle and patches dynamic fields
(bundle ID, version/build, Git SHA, Sparkle keys). This keeps the plist deterministic for SwiftPM
packaging and Nix builds (which do not rely on a full Xcode toolchain).
| Variable | Default |
| ---------------------- | --------------------------------------- |
| `OPENCLAW_HOME` | `HOME` / `USERPROFILE` / `os.homedir()` |
| `OPENCLAW_STATE_DIR` | `~/.openclaw` |
| `OPENCLAW_CONFIG_PATH` | `$OPENCLAW_STATE_DIR/openclaw.json` |
## Related
- [nix-openclaw](https://github.com/openclaw/nix-openclaw) full setup guide
- [Wizard](/start/wizard) non-Nix CLI setup
- [Docker](/install/docker) containerized setup
- [nix-openclaw](https://github.com/openclaw/nix-openclaw) -- full setup guide
- [Wizard](/start/wizard) -- non-Nix CLI setup
- [Docker](/install/docker) -- containerized setup

View File

@ -9,7 +9,7 @@ read_when:
# Node.js
OpenClaw requires **Node 22.16 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#install-methods) will detect and install Node automatically — this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
OpenClaw requires **Node 22.16 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically — this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
## Check your version

View File

@ -1,5 +1,9 @@
---
title: Deploy on Northflank
summary: "Deploy OpenClaw on Northflank with one-click template"
read_when:
- Deploying OpenClaw to Northflank
- You want a one-click cloud deploy with browser-based setup
title: "Northflank"
---
Deploy OpenClaw on Northflank with a one-click template and finish setup in your browser.
@ -34,20 +38,17 @@ and you configure everything via the `/setup` web wizard.
If Telegram DMs are set to pairing, web setup can approve the pairing code.
## Getting chat tokens
## Connect a channel
### Telegram bot token
Paste your Telegram or Discord token into the `/setup` wizard. For setup
instructions, see the channel docs:
1. Message `@BotFather` in Telegram
2. Run `/newbot`
3. Copy the token (looks like `123456789:AA...`)
4. Paste it into `/setup`
- [Telegram](/channels/telegram) (fastest — just a bot token)
- [Discord](/channels/discord)
- [All channels](/channels)
### Discord bot token
## Next steps
1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications)
2. **New Application** → choose a name
3. **Bot** → **Add Bot**
4. **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup)
5. Copy the **Bot Token** and paste into `/setup`
6. Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`)
- Set up messaging channels: [Channels](/channels)
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
- Keep OpenClaw up to date: [Updating](/install/updating)

156
docs/install/oracle.md Normal file
View File

@ -0,0 +1,156 @@
---
summary: "Host OpenClaw on Oracle Cloud's Always Free ARM tier"
read_when:
- Setting up OpenClaw on Oracle Cloud
- Looking for free VPS hosting for OpenClaw
- Want 24/7 OpenClaw on a small server
title: "Oracle Cloud"
---
# Oracle Cloud
Run a persistent OpenClaw Gateway on Oracle Cloud's **Always Free** ARM tier (up to 4 OCPU, 24 GB RAM, 200 GB storage) at no cost.
## Prerequisites
- Oracle Cloud account ([signup](https://www.oracle.com/cloud/free/)) -- see [community signup guide](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd) if you hit issues
- Tailscale account (free at [tailscale.com](https://tailscale.com))
- An SSH key pair
- About 30 minutes
## Setup
<Steps>
<Step title="Create an OCI instance">
1. Log into [Oracle Cloud Console](https://cloud.oracle.com/).
2. Navigate to **Compute > Instances > Create Instance**.
3. Configure:
- **Name:** `openclaw`
- **Image:** Ubuntu 24.04 (aarch64)
- **Shape:** `VM.Standard.A1.Flex` (Ampere ARM)
- **OCPUs:** 2 (or up to 4)
- **Memory:** 12 GB (or up to 24 GB)
- **Boot volume:** 50 GB (up to 200 GB free)
- **SSH key:** Add your public key
4. Click **Create** and note the public IP address.
<Tip>
If instance creation fails with "Out of capacity", try a different availability domain or retry later. Free tier capacity is limited.
</Tip>
</Step>
<Step title="Connect and update the system">
```bash
ssh ubuntu@YOUR_PUBLIC_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y build-essential
```
`build-essential` is required for ARM compilation of some dependencies.
</Step>
<Step title="Configure user and hostname">
```bash
sudo hostnamectl set-hostname openclaw
sudo passwd ubuntu
sudo loginctl enable-linger ubuntu
```
Enabling linger keeps user services running after logout.
</Step>
<Step title="Install Tailscale">
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --hostname=openclaw
```
From now on, connect via Tailscale: `ssh ubuntu@openclaw`.
</Step>
<Step title="Install OpenClaw">
```bash
curl -fsSL https://openclaw.ai/install.sh | bash
source ~/.bashrc
```
When prompted "How do you want to hatch your bot?", select **Do this later**.
</Step>
<Step title="Configure the gateway">
Use token auth with Tailscale Serve for secure remote access.
```bash
openclaw config set gateway.bind loopback
openclaw config set gateway.auth.mode token
openclaw doctor --generate-gateway-token
openclaw config set gateway.tailscale.mode serve
openclaw config set gateway.trustedProxies '["127.0.0.1"]'
systemctl --user restart openclaw-gateway
```
</Step>
<Step title="Lock down VCN security">
Block all traffic except Tailscale at the network edge:
1. Go to **Networking > Virtual Cloud Networks** in the OCI Console.
2. Click your VCN, then **Security Lists > Default Security List**.
3. **Remove** all ingress rules except `0.0.0.0/0 UDP 41641` (Tailscale).
4. Keep default egress rules (allow all outbound).
This blocks SSH on port 22, HTTP, HTTPS, and everything else at the network edge. You can only connect via Tailscale from this point on.
</Step>
<Step title="Verify">
```bash
openclaw --version
systemctl --user status openclaw-gateway
tailscale serve status
curl http://localhost:18789
```
Access the Control UI from any device on your tailnet:
```
https://openclaw.<tailnet-name>.ts.net/
```
Replace `<tailnet-name>` with your tailnet name (visible in `tailscale status`).
</Step>
</Steps>
## Fallback: SSH tunnel
If Tailscale Serve is not working, use an SSH tunnel from your local machine:
```bash
ssh -L 18789:127.0.0.1:18789 ubuntu@openclaw
```
Then open `http://localhost:18789`.
## Troubleshooting
**Instance creation fails ("Out of capacity")** -- Free tier ARM instances are popular. Try a different availability domain or retry during off-peak hours.
**Tailscale will not connect** -- Run `sudo tailscale up --ssh --hostname=openclaw --reset` to re-authenticate.
**Gateway will not start** -- Run `openclaw doctor --non-interactive` and check logs with `journalctl --user -u openclaw-gateway -n 50`.
**ARM binary issues** -- Most npm packages work on ARM64. For native binaries, look for `linux-arm64` or `aarch64` releases. Verify architecture with `uname -m`.
## Next steps
- [Channels](/channels) -- connect Telegram, WhatsApp, Discord, and more
- [Gateway configuration](/gateway/configuration) -- all config options
- [Updating](/install/updating) -- keep OpenClaw up to date

View File

@ -7,53 +7,64 @@ title: "Podman"
# Podman
Run the OpenClaw gateway in a **rootless** Podman container. Uses the same image as Docker (build from the repo [Dockerfile](https://github.com/openclaw/openclaw/blob/main/Dockerfile)).
Run the OpenClaw Gateway in a **rootless** Podman container. Uses the same image as Docker (built from the repo [Dockerfile](https://github.com/openclaw/openclaw/blob/main/Dockerfile)).
## Requirements
## Prerequisites
- Podman (rootless)
- Sudo for one-time setup (create user, build image)
- **Podman** (rootless mode)
- **sudo** access for one-time setup (creating the dedicated user and building the image)
## Quick start
**1. One-time setup** (from repo root; creates user, builds image, installs launch script):
<Steps>
<Step title="One-time setup">
From the repo root, run the setup script. It creates a dedicated `openclaw` user, builds the container image, and installs the launch script:
```bash
./setup-podman.sh
```
```bash
./scripts/podman/setup.sh
```
This also creates a minimal `~openclaw/.openclaw/openclaw.json` (sets `gateway.mode="local"`) so the gateway can start without running the wizard.
This also creates a minimal config at `~openclaw/.openclaw/openclaw.json` (sets `gateway.mode` to `"local"`) so the Gateway can start without running the wizard.
By default the container is **not** installed as a systemd service, you start it manually (see below). For a production-style setup with auto-start and restarts, install it as a systemd Quadlet user service instead:
By default the container is **not** installed as a systemd service -- you start it manually in the next step. For a production-style setup with auto-start and restarts, pass `--quadlet` instead:
```bash
./setup-podman.sh --quadlet
```
```bash
./scripts/podman/setup.sh --quadlet
```
(Or set `OPENCLAW_PODMAN_QUADLET=1`; use `--container` to install only the container and launch script.)
(Or set `OPENCLAW_PODMAN_QUADLET=1`. Use `--container` to install only the container and launch script.)
Optional build-time env vars (set before running `setup-podman.sh`):
**Optional build-time env vars** (set before running `scripts/podman/setup.sh`):
- `OPENCLAW_DOCKER_APT_PACKAGES` — install extra apt packages during image build
- `OPENCLAW_EXTENSIONS` pre-install extension dependencies (space-separated extension names, e.g. `diagnostics-otel matrix`)
- `OPENCLAW_DOCKER_APT_PACKAGES` -- install extra apt packages during image build.
- `OPENCLAW_EXTENSIONS` -- pre-install extension dependencies (space-separated names, e.g. `diagnostics-otel matrix`).
**2. Start gateway** (manual, for quick smoke testing):
</Step>
```bash
./scripts/run-openclaw-podman.sh launch
```
<Step title="Start the Gateway">
For a quick manual launch:
**3. Onboarding wizard** (e.g. to add channels or providers):
```bash
./scripts/run-openclaw-podman.sh launch
```
```bash
./scripts/run-openclaw-podman.sh launch setup
```
</Step>
Then open `http://127.0.0.1:18789/` and use the token from `~openclaw/.openclaw/.env` (or the value printed by setup).
<Step title="Run the onboarding wizard">
To add channels or providers interactively:
```bash
./scripts/run-openclaw-podman.sh launch setup
```
Then open `http://127.0.0.1:18789/` and use the token from `~openclaw/.openclaw/.env` (or the value printed by setup).
</Step>
</Steps>
## Systemd (Quadlet, optional)
If you ran `./setup-podman.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) unit is installed so the gateway runs as a systemd user service for the openclaw user. The service is enabled and started at the end of setup.
If you ran `./scripts/podman/setup.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) unit is installed so the gateway runs as a systemd user service for the openclaw user. The service is enabled and started at the end of setup.
- **Start:** `sudo systemctl --machine openclaw@ --user start openclaw.service`
- **Stop:** `sudo systemctl --machine openclaw@ --user stop openclaw.service`
@ -62,11 +73,11 @@ If you ran `./setup-podman.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Po
The quadlet file lives at `~openclaw/.config/containers/systemd/openclaw.container`. To change ports or env, edit that file (or the `.env` it sources), then `sudo systemctl --machine openclaw@ --user daemon-reload` and restart the service. On boot, the service starts automatically if lingering is enabled for openclaw (setup does this when loginctl is available).
To add quadlet **after** an initial setup that did not use it, re-run: `./setup-podman.sh --quadlet`.
To add quadlet **after** an initial setup that did not use it, re-run: `./scripts/podman/setup.sh --quadlet`.
## The openclaw user (non-login)
`setup-podman.sh` creates a dedicated system user `openclaw`:
`scripts/podman/setup.sh` creates a dedicated system user `openclaw`:
- **Shell:** `nologin` — no interactive login; reduces attack surface.
- **Home:** e.g. `/home/openclaw` — holds `~/.openclaw` (config, workspace) and the launch script `run-openclaw-podman.sh`.
@ -87,7 +98,7 @@ To add quadlet **after** an initial setup that did not use it, re-run: `./setup-
## Environment and config
- **Token:** Stored in `~openclaw/.openclaw/.env` as `OPENCLAW_GATEWAY_TOKEN`. `setup-podman.sh` and `run-openclaw-podman.sh` generate it if missing (uses `openssl`, `python3`, or `od`).
- **Token:** Stored in `~openclaw/.openclaw/.env` as `OPENCLAW_GATEWAY_TOKEN`. `scripts/podman/setup.sh` and `run-openclaw-podman.sh` generate it if missing (uses `openssl`, `python3`, or `od`).
- **Optional:** In that `.env` you can set provider keys (e.g. `GROQ_API_KEY`, `OLLAMA_API_KEY`) and other OpenClaw env vars.
- **Host ports:** By default the script maps `18789` (gateway) and `18790` (bridge). Override the **host** port mapping with `OPENCLAW_PODMAN_GATEWAY_HOST_PORT` and `OPENCLAW_PODMAN_BRIDGE_HOST_PORT` when launching.
- **Gateway bind:** By default, `run-openclaw-podman.sh` starts the gateway with `--bind loopback` for safe local access. To expose on LAN, set `OPENCLAW_GATEWAY_BIND=lan` and configure `gateway.controlUi.allowedOrigins` (or explicitly enable host-header fallback) in `openclaw.json`.
@ -99,7 +110,7 @@ To add quadlet **after** an initial setup that did not use it, re-run: `./setup-
- **Ephemeral sandbox tmpfs:** if you enable `agents.defaults.sandbox`, the tool sandbox containers mount `tmpfs` at `/tmp`, `/var/tmp`, and `/run`. Those paths are memory-backed and disappear with the sandbox container; the top-level Podman container setup does not add its own tmpfs mounts.
- **Disk growth hotspots:** the main paths to watch are `media/`, `agents/<agentId>/sessions/sessions.json`, transcript JSONL files, `cron/runs/*.jsonl`, and rolling file logs under `/tmp/openclaw/` (or your configured `logging.file`).
`setup-podman.sh` now stages the image tar in a private temp directory and prints the chosen base dir during setup. For non-root runs it accepts `TMPDIR` only when that base is safe to use; otherwise it falls back to `/var/tmp`, then `/tmp`. The saved tar stays owner-only and is streamed into the target users `podman load`, so private caller temp dirs do not block setup.
`scripts/podman/setup.sh` now stages the image tar in a private temp directory and prints the chosen base dir during setup. For non-root runs it accepts `TMPDIR` only when that base is safe to use; otherwise it falls back to `/var/tmp`, then `/tmp`. The saved tar stays owner-only and is streamed into the target users `podman load`, so private caller temp dirs do not block setup.
## Useful commands
@ -111,12 +122,12 @@ To add quadlet **after** an initial setup that did not use it, re-run: `./setup-
## Troubleshooting
- **Permission denied (EACCES) on config or auth-profiles:** The container defaults to `--userns=keep-id` and runs as the same uid/gid as the host user running the script. Ensure your host `OPENCLAW_CONFIG_DIR` and `OPENCLAW_WORKSPACE_DIR` are owned by that user.
- **Gateway start blocked (missing `gateway.mode=local`):** Ensure `~openclaw/.openclaw/openclaw.json` exists and sets `gateway.mode="local"`. `setup-podman.sh` creates this file if missing.
- **Gateway start blocked (missing `gateway.mode=local`):** Ensure `~openclaw/.openclaw/openclaw.json` exists and sets `gateway.mode="local"`. `scripts/podman/setup.sh` creates this file if missing.
- **Rootless Podman fails for user openclaw:** Check `/etc/subuid` and `/etc/subgid` contain a line for `openclaw` (e.g. `openclaw:100000:65536`). Add it if missing and restart.
- **Container name in use:** The launch script uses `podman run --replace`, so the existing container is replaced when you start again. To clean up manually: `podman rm -f openclaw`.
- **Script not found when running as openclaw:** Ensure `setup-podman.sh` was run so that `run-openclaw-podman.sh` is copied to openclaws home (e.g. `/home/openclaw/run-openclaw-podman.sh`).
- **Script not found when running as openclaw:** Ensure `scripts/podman/setup.sh` was run so that `run-openclaw-podman.sh` is copied to openclaws home (e.g. `/home/openclaw/run-openclaw-podman.sh`).
- **Quadlet service not found or fails to start:** Run `sudo systemctl --machine openclaw@ --user daemon-reload` after editing the `.container` file. Quadlet requires cgroups v2: `podman info --format '{{.Host.CgroupsVersion}}'` should show `2`.
## Optional: run as your own user
To run the gateway as your normal user (no dedicated openclaw user): build the image, create `~/.openclaw/.env` with `OPENCLAW_GATEWAY_TOKEN`, and run the container with `--userns=keep-id` and mounts to your `~/.openclaw`. The launch script is designed for the openclaw-user flow; for a single-user setup you can instead run the `podman run` command from the script manually, pointing config and workspace to your home. Recommended for most users: use `setup-podman.sh` and run as the openclaw user so config and process are isolated.
To run the gateway as your normal user (no dedicated openclaw user): build the image, create `~/.openclaw/.env` with `OPENCLAW_GATEWAY_TOKEN`, and run the container with `--userns=keep-id` and mounts to your `~/.openclaw`. The launch script is designed for the openclaw-user flow; for a single-user setup you can instead run the `podman run` command from the script manually, pointing config and workspace to your home. Recommended for most users: use `scripts/podman/setup.sh` and run as the openclaw user so config and process are isolated.

View File

@ -1,5 +1,9 @@
---
title: Deploy on Railway
summary: "Deploy OpenClaw on Railway with one-click template"
read_when:
- Deploying OpenClaw to Railway
- You want a one-click cloud deploy with browser-based setup
title: "Railway"
---
Deploy OpenClaw on Railway with a one-click template and finish setup in your browser.
@ -72,23 +76,14 @@ Set these variables on the service:
If Telegram DMs are set to pairing, web setup can approve the pairing code.
## Getting chat tokens
## Connect a channel
### Telegram bot token
Paste your Telegram or Discord token into the `/setup` wizard. For setup
instructions, see the channel docs:
1. Message `@BotFather` in Telegram
2. Run `/newbot`
3. Copy the token (looks like `123456789:AA...`)
4. Paste it into `/setup`
### Discord bot token
1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications)
2. **New Application** → choose a name
3. **Bot** → **Add Bot**
4. **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup)
5. Copy the **Bot Token** and paste into `/setup`
6. Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`)
- [Telegram](/channels/telegram) (fastest — just a bot token)
- [Discord](/channels/discord)
- [All channels](/channels)
## Backups & migration
@ -97,3 +92,9 @@ Download a backup at:
- `https://<your-railway-domain>/setup/export`
This exports your OpenClaw state + workspace so you can migrate to another host without losing config or memory.
## Next steps
- Set up messaging channels: [Channels](/channels)
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
- Keep OpenClaw up to date: [Updating](/install/updating)

View File

@ -0,0 +1,159 @@
---
summary: "Host OpenClaw on a Raspberry Pi for always-on self-hosting"
read_when:
- Setting up OpenClaw on a Raspberry Pi
- Running OpenClaw on ARM devices
- Building a cheap always-on personal AI
title: "Raspberry Pi"
---
# Raspberry Pi
Run a persistent, always-on OpenClaw Gateway on a Raspberry Pi. Since the Pi is just the gateway (models run in the cloud via API), even a modest Pi handles the workload well.
## Prerequisites
- Raspberry Pi 4 or 5 with 2 GB+ RAM (4 GB recommended)
- MicroSD card (16 GB+) or USB SSD (better performance)
- Official Pi power supply
- Network connection (Ethernet or WiFi)
- 64-bit Raspberry Pi OS (required -- do not use 32-bit)
- About 30 minutes
## Setup
<Steps>
<Step title="Flash the OS">
Use **Raspberry Pi OS Lite (64-bit)** -- no desktop needed for a headless server.
1. Download [Raspberry Pi Imager](https://www.raspberrypi.com/software/).
2. Choose OS: **Raspberry Pi OS Lite (64-bit)**.
3. In the settings dialog, pre-configure:
- Hostname: `gateway-host`
- Enable SSH
- Set username and password
- Configure WiFi (if not using Ethernet)
4. Flash to your SD card or USB drive, insert it, and boot the Pi.
</Step>
<Step title="Connect via SSH">
```bash
ssh user@gateway-host
```
</Step>
<Step title="Update the system">
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl build-essential
# Set timezone (important for cron and reminders)
sudo timedatectl set-timezone America/Chicago
```
</Step>
<Step title="Install Node.js 24">
```bash
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --version
```
</Step>
<Step title="Add swap (important for 2 GB or less)">
```bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# Reduce swappiness for low-RAM devices
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
```
</Step>
<Step title="Install OpenClaw">
```bash
curl -fsSL https://openclaw.ai/install.sh | bash
```
</Step>
<Step title="Run onboarding">
```bash
openclaw onboard --install-daemon
```
Follow the wizard. API keys are recommended over OAuth for headless devices. Telegram is the easiest channel to start with.
</Step>
<Step title="Verify">
```bash
openclaw status
sudo systemctl status openclaw
journalctl -u openclaw -f
```
</Step>
<Step title="Access the Control UI">
On your computer, get a dashboard URL from the Pi:
```bash
ssh user@gateway-host 'openclaw dashboard --no-open'
```
Then create an SSH tunnel in another terminal:
```bash
ssh -N -L 18789:127.0.0.1:18789 user@gateway-host
```
Open the printed URL in your local browser. For always-on remote access, see [Tailscale integration](/gateway/tailscale).
</Step>
</Steps>
## Performance tips
**Use a USB SSD** -- SD cards are slow and wear out. A USB SSD dramatically improves performance. See the [Pi USB boot guide](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-mass-storage-boot).
**Enable module compile cache** -- Speeds up repeated CLI invocations on lower-power Pi hosts:
```bash
grep -q 'NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache' ~/.bashrc || cat >> ~/.bashrc <<'EOF' # pragma: allowlist secret
export NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache
mkdir -p /var/tmp/openclaw-compile-cache
export OPENCLAW_NO_RESPAWN=1
EOF
source ~/.bashrc
```
**Reduce memory usage** -- For headless setups, free GPU memory and disable unused services:
```bash
echo 'gpu_mem=16' | sudo tee -a /boot/config.txt
sudo systemctl disable bluetooth
```
## Troubleshooting
**Out of memory** -- Verify swap is active with `free -h`. Disable unused services (`sudo systemctl disable cups bluetooth avahi-daemon`). Use API-based models only.
**Slow performance** -- Use a USB SSD instead of an SD card. Check for CPU throttling with `vcgencmd get_throttled` (should return `0x0`).
**Service will not start** -- Check logs with `journalctl -u openclaw --no-pager -n 100` and run `openclaw doctor --non-interactive`.
**ARM binary issues** -- If a skill fails with "exec format error", check whether the binary has an ARM64 build. Verify architecture with `uname -m` (should show `aarch64`).
**WiFi drops** -- Disable WiFi power management: `sudo iwconfig wlan0 power off`.
## Next steps
- [Channels](/channels) -- connect Telegram, WhatsApp, Discord, and more
- [Gateway configuration](/gateway/configuration) -- all config options
- [Updating](/install/updating) -- keep OpenClaw up to date

View File

@ -1,5 +1,9 @@
---
title: Deploy on Render
summary: "Deploy OpenClaw on Render with Infrastructure-as-Code"
read_when:
- Deploying OpenClaw to Render
- You want a declarative cloud deploy with Render Blueprints
title: "Render"
---
Deploy OpenClaw on Render using Infrastructure as Code. The included `render.yaml` Blueprint defines your entire stack declaratively, service, disk, environment variables, so you can deploy with a single click and version your infrastructure alongside your code.
@ -157,3 +161,9 @@ Render expects a 200 response from `/health` within 30 seconds. If builds succee
- Build logs for errors
- Whether the container runs locally with `docker build && docker run`
## Next steps
- Set up messaging channels: [Channels](/channels)
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
- Keep OpenClaw up to date: [Updating](/install/updating)

View File

@ -8,44 +8,35 @@ title: "Updating"
# Updating
OpenClaw is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart (or use `openclaw update`, which restarts) → verify.
Keep OpenClaw up to date.
## Recommended: re-run the website installer (upgrade in place)
## Recommended: `openclaw update`
The **preferred** update path is to re-run the installer from the website. It
detects existing installs, upgrades in place, and runs `openclaw doctor` when
needed.
The fastest way to update. It detects your install type (npm or git), fetches the latest version, runs `openclaw doctor`, and restarts the gateway.
```bash
openclaw update
```
To switch channels or target a specific version:
```bash
openclaw update --channel beta
openclaw update --tag main
openclaw update --dry-run # preview without applying
```
See [Development channels](/install/development-channels) for channel semantics.
## Alternative: re-run the installer
```bash
curl -fsSL https://openclaw.ai/install.sh | bash
```
Notes:
Add `--no-onboard` to skip onboarding. For source installs, pass `--install-method git --no-onboard`.
- Add `--no-onboard` if you dont want onboarding to run again.
- For **source installs**, use:
```bash
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboard
```
The installer will `git pull --rebase` **only** if the repo is clean.
- For **global installs**, the script uses `npm install -g openclaw@latest` under the hood.
- Legacy note: `clawdbot` remains available as a compatibility shim.
## Before you update
- Know how you installed: **global** (npm/pnpm) vs **from source** (git clone).
- Know how your Gateway is running: **foreground terminal** vs **supervised service** (launchd/systemd).
- Snapshot your tailoring:
- Config: `~/.openclaw/openclaw.json`
- Credentials: `~/.openclaw/credentials/`
- Workspace: `~/.openclaw/workspace`
## Update (global install)
Global install (pick one):
## Alternative: manual npm or pnpm
```bash
npm i -g openclaw@latest
@ -55,221 +46,83 @@ npm i -g openclaw@latest
pnpm add -g openclaw@latest
```
We do **not** recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs).
## Auto-updater
To switch update channels (git + npm installs):
The auto-updater is off by default. Enable it in `~/.openclaw/openclaw.json`:
```bash
openclaw update --channel beta
openclaw update --channel dev
openclaw update --channel stable
```
Use `--tag <dist-tag|version|spec>` for a one-off package target override.
For the current GitHub `main` head via a package-manager install:
```bash
openclaw update --tag main
```
Manual equivalents:
```bash
npm i -g github:openclaw/openclaw#main
```
```bash
pnpm add -g github:openclaw/openclaw#main
```
You can also pass an explicit package spec to `--tag` for one-off updates (for example a GitHub ref or tarball URL).
See [Development channels](/install/development-channels) for channel semantics and release notes.
Note: on npm installs, the gateway logs an update hint on startup (checks the current channel tag). Disable via `update.checkOnStart: false`.
### Core auto-updater (optional)
Auto-updater is **off by default** and is a core Gateway feature (not a plugin).
```json
```json5
{
"update": {
"channel": "stable",
"auto": {
"enabled": true,
"stableDelayHours": 6,
"stableJitterHours": 12,
"betaCheckIntervalHours": 1
}
}
update: {
channel: "stable",
auto: {
enabled: true,
stableDelayHours: 6,
stableJitterHours: 12,
betaCheckIntervalHours: 1,
},
},
}
```
Behavior:
| Channel | Behavior |
| -------- | ------------------------------------------------------------------------------------------------------------- |
| `stable` | Waits `stableDelayHours`, then applies with deterministic jitter across `stableJitterHours` (spread rollout). |
| `beta` | Checks every `betaCheckIntervalHours` (default: hourly) and applies immediately. |
| `dev` | No automatic apply. Use `openclaw update` manually. |
- `stable`: when a new version is seen, OpenClaw waits `stableDelayHours` and then applies a deterministic per-install jitter in `stableJitterHours` (spread rollout).
- `beta`: checks on `betaCheckIntervalHours` cadence (default: hourly) and applies when an update is available.
- `dev`: no automatic apply; use manual `openclaw update`.
The gateway also logs an update hint on startup (disable with `update.checkOnStart: false`).
Use `openclaw update --dry-run` to preview update actions before enabling automation.
## After updating
Then:
<Steps>
### Run doctor
```bash
openclaw doctor
```
Migrates config, audits DM policies, and checks gateway health. Details: [Doctor](/gateway/doctor)
### Restart the gateway
```bash
openclaw gateway restart
```
### Verify
```bash
openclaw health
```
Notes:
</Steps>
- If your Gateway runs as a service, `openclaw gateway restart` is preferred over killing PIDs.
- If youre pinned to a specific version, see “Rollback / pinning” below.
## Rollback
## Update (`openclaw update`)
For **source installs** (git checkout), prefer:
```bash
openclaw update
```
It runs a safe-ish update flow:
- Requires a clean worktree.
- Switches to the selected channel (tag or branch).
- Fetches + rebases against the configured upstream (dev channel).
- Installs deps, builds, builds the Control UI, and runs `openclaw doctor`.
- Restarts the gateway by default (use `--no-restart` to skip).
If you installed via **npm/pnpm** (no git metadata), `openclaw update` will try to update via your package manager. If it cant detect the install, use “Update (global install)” instead.
## Update (Control UI / RPC)
The Control UI has **Update & Restart** (RPC: `update.run`). It:
1. Runs the same source-update flow as `openclaw update` (git checkout only).
2. Writes a restart sentinel with a structured report (stdout/stderr tail).
3. Restarts the gateway and pings the last active session with the report.
If the rebase fails, the gateway aborts and restarts without applying the update.
## Update (from source)
From the repo checkout:
Preferred:
```bash
openclaw update
```
Manual (equivalent-ish):
```bash
git pull
pnpm install
pnpm build
pnpm ui:build # auto-installs UI deps on first run
openclaw doctor
openclaw health
```
Notes:
- `pnpm build` matters when you run the packaged `openclaw` binary ([`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs)) or use Node to run `dist/`.
- If you run from a repo checkout without a global install, use `pnpm openclaw ...` for CLI commands.
- If you run directly from TypeScript (`pnpm openclaw ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor.
- Switching between global and git installs is easy: install the other flavor, then run `openclaw doctor` so the gateway service entrypoint is rewritten to the current install.
## Always Run: `openclaw doctor`
Doctor is the “safe update” command. Its intentionally boring: repair + migrate + warn.
Note: if youre on a **source install** (git checkout), `openclaw doctor` will offer to run `openclaw update` first.
Typical things it does:
- Migrate deprecated config keys / legacy config file locations.
- Audit DM policies and warn on risky “open” settings.
- Check Gateway health and can offer to restart.
- Detect and migrate older gateway services (launchd/systemd; legacy schtasks) to current OpenClaw services.
- On Linux, ensure systemd user lingering (so the Gateway survives logout).
Details: [Doctor](/gateway/doctor)
## Start / stop / restart the Gateway
CLI (works regardless of OS):
```bash
openclaw gateway status
openclaw gateway stop
openclaw gateway restart
openclaw gateway --port 18789
openclaw logs --follow
```
If youre supervised:
- macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/ai.openclaw.gateway` (use `ai.openclaw.<profile>`; legacy `com.openclaw.*` still works)
- Linux systemd user service: `systemctl --user restart openclaw-gateway[-<profile>].service`
- Windows (WSL2): `systemctl --user restart openclaw-gateway[-<profile>].service`
- `launchctl`/`systemctl` only work if the service is installed; otherwise run `openclaw gateway install`.
Runbook + exact service labels: [Gateway runbook](/gateway)
## Rollback / pinning (when something breaks)
### Pin (global install)
Install a known-good version (replace `<version>` with the last working one):
### Pin a version (npm)
```bash
npm i -g openclaw@<version>
```
```bash
pnpm add -g openclaw@<version>
```
Tip: to see the current published version, run `npm view openclaw version`.
Then restart + re-run doctor:
```bash
openclaw doctor
openclaw gateway restart
```
### Pin (source) by date
Tip: `npm view openclaw version` shows the current published version.
Pick a commit from a date (example: “state of main as of 2026-01-01”):
### Pin a commit (source)
```bash
git fetch origin
git checkout "$(git rev-list -n 1 --before=\"2026-01-01\" origin/main)"
```
Then reinstall deps + restart:
```bash
pnpm install
pnpm build
pnpm install && pnpm build
openclaw gateway restart
```
If you want to go back to latest later:
```bash
git checkout main
git pull
```
To return to latest: `git checkout main && git pull`.
## If you are stuck
- Run `openclaw doctor` again and read the output carefully (it often tells you the fix).
- Run `openclaw doctor` again and read the output carefully.
- Check: [Troubleshooting](/gateway/troubleshooting)
- Ask in Discord: [https://discord.gg/clawd](https://discord.gg/clawd)

View File

@ -36,6 +36,10 @@ openclaw nodes status
openclaw nodes describe --node <idOrNameOrIp>
```
If a node retries with changed auth details (role/scopes/public key), the prior
pending request is superseded and a new `requestId` is created. Re-run
`openclaw devices list` before approving.
Notes:
- `nodes status` marks a node as **paired** when its device pairing role includes `node`.
@ -115,6 +119,9 @@ openclaw devices approve <requestId>
openclaw nodes status
```
If the node retries with changed auth details, re-run `openclaw devices list`
and approve the current `requestId`.
Naming options:
- `--display-name` on `openclaw node run` / `openclaw node install` (persists in `~/.openclaw/node.json` on the node).

View File

@ -29,6 +29,7 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
- Fly.io: [Fly.io](/install/fly)
- Hetzner (Docker): [Hetzner](/install/hetzner)
- GCP (Compute Engine): [GCP](/install/gcp)
- Azure (Linux VM): [Azure](/install/azure)
- exe.dev (VM + HTTPS proxy): [exe.dev](/install/exe-dev)
## Common links

View File

@ -42,6 +42,10 @@ openclaw devices list
openclaw devices approve <requestId>
```
If the app retries pairing with changed auth details (role/scopes/public key),
the previous pending request is superseded and a new `requestId` is created.
Run `openclaw devices list` again before approval.
4. Verify connection:
```bash

View File

@ -21,7 +21,7 @@ Native Linux companion apps are planned. Contributions are welcome if you want t
4. From your laptop: `ssh -N -L 18789:127.0.0.1:18789 <user>@<host>`
5. Open `http://127.0.0.1:18789/` and paste your token
Step-by-step VPS guide: [exe.dev](/install/exe-dev)
Full Linux server guide: [Linux Server](/vps). Step-by-step VPS example: [exe.dev](/install/exe-dev)
## Install

View File

@ -1,22 +1,22 @@
---
summary: "Windows (WSL2) support + companion app status"
summary: "Windows support: native and WSL2 install paths, daemon, and current caveats"
read_when:
- Installing OpenClaw on Windows
- Choosing between native Windows and WSL2
- Looking for Windows companion app status
title: "Windows (WSL2)"
title: "Windows"
---
# Windows (WSL2)
# Windows
OpenClaw on Windows is recommended **via WSL2** (Ubuntu recommended). The
CLI + Gateway run inside Linux, which keeps the runtime consistent and makes
tooling far more compatible (Node/Bun/pnpm, Linux binaries, skills). Native
Windows might be trickier. WSL2 gives you the full Linux experience — one command
to install: `wsl --install`.
OpenClaw supports both **native Windows** and **WSL2**. WSL2 is the more
stable path and recommended for the full experience — the CLI, Gateway, and
tooling run inside Linux with full compatibility. Native Windows works for
core CLI and Gateway use, with some caveats noted below.
Native Windows companion apps are planned.
## Install (WSL2)
## WSL2 (recommended)
- [Getting Started](/start/getting-started) (use inside WSL)
- [Install & updates](/install/updating)

View File

@ -57,7 +57,7 @@ OpenClaw's shared `/fast` toggle also supports direct Anthropic API-key traffic.
agents: {
defaults: {
models: {
"anthropic/claude-sonnet-4-5": {
"anthropic/claude-sonnet-4-6": {
params: { fastMode: true },
},
},
@ -228,7 +228,7 @@ openclaw onboard --auth-choice setup-token
## Notes
- Generate the setup-token with `claude setup-token` and paste it, or run `openclaw models auth setup-token` on the gateway host.
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription).
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token. See [/gateway/troubleshooting](/gateway/troubleshooting).
- Auth details + reuse rules are in [/concepts/oauth](/concepts/oauth).
## Troubleshooting

View File

@ -12,7 +12,7 @@ Cloudflare AI Gateway sits in front of provider APIs and lets you add analytics,
- Provider: `cloudflare-ai-gateway`
- Base URL: `https://gateway.ai.cloudflare.com/v1/<account_id>/<gateway_id>/anthropic`
- Default model: `cloudflare-ai-gateway/claude-sonnet-4-5`
- Default model: `cloudflare-ai-gateway/claude-sonnet-4-6`
- API key: `CLOUDFLARE_AI_GATEWAY_API_KEY` (your provider API key for requests through the Gateway)
For Anthropic models, use your Anthropic API key.
@ -31,7 +31,7 @@ openclaw onboard --auth-choice cloudflare-ai-gateway-api-key
{
agents: {
defaults: {
model: { primary: "cloudflare-ai-gateway/claude-sonnet-4-5" },
model: { primary: "cloudflare-ai-gateway/claude-sonnet-4-6" },
},
},
}

Some files were not shown because too many files have changed in this diff Show More