Merge remote-tracking branch 'upstream/main' into fix/handle-sensitive-stop-reason
This commit is contained in:
commit
927331297c
71
.agents/skills/openclaw-test-heap-leaks/SKILL.md
Normal file
71
.agents/skills/openclaw-test-heap-leaks/SKILL.md
Normal 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.
|
||||
@ -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."
|
||||
@ -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();
|
||||
@ -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
3
.github/labeler.yml
vendored
@ -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"
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -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]
|
||||
|
||||
18
.github/workflows/install-smoke.yml
vendored
18
.github/workflows/install-smoke.yml
vendored
@ -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) {
|
||||
|
||||
11
AGENTS.md
11
AGENTS.md
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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`() {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +77,7 @@ struct ExecHostRequestEvaluatorTests {
|
||||
env: [:],
|
||||
resolution: nil,
|
||||
allowlistResolutions: [],
|
||||
allowAlwaysPatterns: [],
|
||||
allowlistMatches: [],
|
||||
allowlistSatisfied: allowlistSatisfied,
|
||||
allowlistMatch: nil,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
@ -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:
|
||||
|
||||
612
docker-setup.sh
612
docker-setup.sh
@ -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" "$@"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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" }],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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/`:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -7,8 +7,6 @@ title: "Gateway Architecture"
|
||||
|
||||
# Gateway architecture
|
||||
|
||||
Last updated: 2026-01-22
|
||||
|
||||
## Overview
|
||||
|
||||
- A single long‑lived **Gateway** owns all messaging surfaces (WhatsApp via
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 gateway’s `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 5 m).
|
||||
- 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 agent’s XDG dirs.
|
||||
|
||||
OpenClaw’s 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 can’t 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 agent’s 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).
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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`)
|
||||
|
||||
@ -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" },
|
||||
},
|
||||
},
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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).
|
||||
|
||||
|
||||
265
docs/docs.json
265
docs/docs.json
@ -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"]
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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" },
|
||||
},
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
4139
docs/help/faq.md
4139
docs/help/faq.md
File diff suppressed because it is too large
Load Diff
@ -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 you’re 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)
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
178
docs/install/azure.md
Normal 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 you’ll 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
|
||||
|
||||
You’ll 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 don’t 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)
|
||||
@ -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 there’s 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.
|
||||
|
||||
@ -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
|
||||
|
||||
129
docs/install/digitalocean.md
Normal file
129
docs/install/digitalocean.md
Normal 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
|
||||
@ -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
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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>"`.
|
||||
|
||||
@ -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 you’re 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 aren’t 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, it’s 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, you’ll 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 won’t move the remote gateway’s state.
|
||||
|
||||
If you’re 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 doesn’t 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
156
docs/install/oracle.md
Normal 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
|
||||
@ -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 user’s `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 user’s `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 openclaw’s 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 openclaw’s 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.
|
||||
|
||||
@ -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)
|
||||
|
||||
159
docs/install/raspberry-pi.md
Normal file
159
docs/install/raspberry-pi.md
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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 don’t 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 you’re 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 can’t 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. It’s intentionally boring: repair + migrate + warn.
|
||||
|
||||
Note: if you’re 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 you’re 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)
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user