Golden paths + safer workspace + mac app Nix mode

- Add golden paths doc and link from README\n- Default workspaceDir to stateDir/workspace and pin agents.defaults.workspace when unset\n- Fix macOS app defaults domain and add openclaw.nixMode toggle
This commit is contained in:
Josh Palmer 2026-02-08 10:33:47 -08:00
parent 90ff91f3be
commit 2cb22671dc
5 changed files with 103 additions and 5 deletions

View File

@ -16,6 +16,8 @@ If youre **not listed as a maintainer** (see [AGENTS.md#maintainers](AGENTS.m
## Table of Contents
- [Golden Paths](#golden-paths)
- [Contributions (read this first)](#contributions-read-this-first)
- [What You Get](#what-you-get)
- [Requirements](#requirements)
@ -31,6 +33,16 @@ If youre **not listed as a maintainer** (see [AGENTS.md#maintainers](AGENTS.m
---
## Golden Paths
**There should be one — and preferably only one — obvious way to deploy.**
Pick a Golden Path, then follow the docs:
- [docs/golden-paths.md](docs/golden-paths.md)
---
## What You Get
```

63
docs/golden-paths.md Normal file
View File

@ -0,0 +1,63 @@
# Golden Paths
nix-openclaw is opinionated: **there should be one obvious way to deploy**.
A **Golden Path** is a supported topology + defaults + docs that:
- is secure by default
- is reproducible (pinned inputs)
- avoids manual state drift
- has a clear boundary between **Nix-managed config** and **runtime state**
If your setup doesnt match a Golden Path, it may still work — but youre on your own.
## GP1: Single Mac (laptop or Mac mini)
**Who its for:** simplest “it just works” install; macOS-only capabilities available locally.
- Gateway: macOS (launchd)
- OpenClaw.app: same machine
- Networking: localhost (default)
## GP2: VPS Gateway + Mac Node (recommended for reliability)
**Who its for:** always-on Gateway (Telegram/Discord/etc) with macOS-only capabilities bridged from your Mac.
- Gateway: Linux VPS (systemd user service)
- Node: OpenClaw.app on macOS (connects over WebSocket)
- Networking: **Tailscale tailnet** (private), no public exposure
Key idea: the Gateway routes tool calls to the node when `host=node` is selected.
### Why Tailscale?
- private-by-default connectivity
- MagicDNS stable hostnames (no IP chasing)
- easy to lock down with ACLs
### Nix mode on macOS app
OpenClaw.app supports **Nix mode** (`OPENCLAW_NIX_MODE=1` or `defaults write ai.openclaw.mac openclaw.nixMode -bool true`).
In Nix mode the app disables auto-mutation flows and treats config as read-only.
If something is missing for a fully declarative deployment, its a bug — fix it upstream.
## GP3: Laptop-only dev
**Who its for:** local experimentation.
- Gateway: macOS/Linux laptop
- Node: optional
- Expect downtime / sleep / network changes
## Runtime state vs pinned config
Pinned / Nix-managed:
- `openclaw.json` (gateway config)
- documents (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, etc.)
- workspace path selection
Runtime:
- sessions, caches
- pairing state (devices/nodes)
- exec approvals

View File

@ -73,7 +73,13 @@ let
inst.package;
pluginPackages = plugins.pluginPackagesFor name;
pluginEnvAll = plugins.pluginEnvAllFor name;
mergedConfig = stripNulls (lib.recursiveUpdate (lib.recursiveUpdate baseConfig cfg.config) inst.config);
mergedConfig0 = stripNulls (lib.recursiveUpdate (lib.recursiveUpdate baseConfig cfg.config) inst.config);
existingWorkspace = (((mergedConfig0.agents or {}).defaults or {}).workspace or null);
mergedConfig =
if (cfg.workspace.pinAgentDefaults or true) && existingWorkspace == null then
lib.recursiveUpdate mergedConfig0 { agents = { defaults = { workspace = inst.workspaceDir; }; }; }
else
mergedConfig0;
configJson = builtins.toJSON mergedConfig;
configFile = pkgs.writeText "openclaw-${name}.json" configJson;
gatewayWrapper = pkgs.writeShellScriptBin "openclaw-gateway-${name}" ''
@ -108,6 +114,7 @@ let
appDefaults = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.appDefaults.enable) {
attachExistingOnly = inst.appDefaults.attachExistingOnly;
gatewayPort = inst.gatewayPort;
nixMode = inst.appDefaults.nixMode;
};
appInstall = if !(pkgs.stdenv.hostPlatform.isDarwin && inst.app.install.enable && appPackage != null) then
@ -250,8 +257,10 @@ in {
home.activation.openclawAppDefaults = lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != {}) (
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
/usr/bin/defaults write com.steipete.Openclaw openclaw.gateway.attachExistingOnly -bool ${lib.boolToString (appDefaults.attachExistingOnly or true)}
/usr/bin/defaults write com.steipete.Openclaw gatewayPort -int ${toString (appDefaults.gatewayPort or 18789)}
# Nix mode + app defaults (OpenClaw.app)
/usr/bin/defaults write ai.openclaw.mac openclaw.nixMode -bool ${lib.boolToString (appDefaults.nixMode or true)}
/usr/bin/defaults write ai.openclaw.mac openclaw.gateway.attachExistingOnly -bool ${lib.boolToString (appDefaults.attachExistingOnly or true)}
/usr/bin/defaults write ai.openclaw.mac gatewayPort -int ${toString (appDefaults.gatewayPort or 18789)}
''
);

View File

@ -137,6 +137,12 @@
default = true;
description = "Attach existing gateway only (macOS).";
};
nixMode = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable OpenClaw Nix mode in the macOS app via defaults (openclaw.nixMode).";
};
};
};
}

View File

@ -84,8 +84,16 @@ in {
workspaceDir = lib.mkOption {
type = lib.types.str;
default = "${openclawLib.homeDir}/.openclaw/workspace";
description = "Workspace directory for OpenClaw agent skills.";
default = "${config.programs.openclaw.stateDir}/workspace";
description = "Workspace directory for Openclaw agent skills (defaults to stateDir/workspace).";
};
workspace = {
pinAgentDefaults = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Pin agents.defaults.workspace to each instance workspaceDir when unset (prevents falling back to template ~/.openclaw/workspace).";
};
};
documents = lib.mkOption {