Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@ RUN mkdir -p /sandbox/.openclaw-data/agents/main/agent \
/sandbox/.openclaw-data/workspace \
/sandbox/.openclaw-data/skills \
/sandbox/.openclaw-data/hooks \
/sandbox/.openclaw-data/identity \
/sandbox/.openclaw-data/devices \
/sandbox/.openclaw-data/canvas \
/sandbox/.openclaw-data/cron \
&& mkdir -p /sandbox/.openclaw \
&& ln -s /sandbox/.openclaw-data/agents /sandbox/.openclaw/agents \
&& ln -s /sandbox/.openclaw-data/extensions /sandbox/.openclaw/extensions \
&& ln -s /sandbox/.openclaw-data/workspace /sandbox/.openclaw/workspace \
&& ln -s /sandbox/.openclaw-data/skills /sandbox/.openclaw/skills \
&& ln -s /sandbox/.openclaw-data/hooks /sandbox/.openclaw/hooks \
&& ln -s /sandbox/.openclaw-data/identity /sandbox/.openclaw/identity \
&& ln -s /sandbox/.openclaw-data/devices /sandbox/.openclaw/devices \
&& ln -s /sandbox/.openclaw-data/canvas /sandbox/.openclaw/canvas \
&& ln -s /sandbox/.openclaw-data/cron /sandbox/.openclaw/cron \
&& touch /sandbox/.openclaw-data/update-check.json \
&& ln -s /sandbox/.openclaw-data/update-check.json /sandbox/.openclaw/update-check.json \
&& chown -R sandbox:sandbox /sandbox/.openclaw /sandbox/.openclaw-data

# Install OpenClaw CLI
Expand Down Expand Up @@ -91,13 +101,21 @@ chat_origin = f'{parsed.scheme}://{parsed.netloc}' if parsed.scheme and parsed.n
origins = ['http://127.0.0.1:18789']; \
origins = list(dict.fromkeys(origins + [chat_origin])); \
config = { \
'agents': {'defaults': {'model': {'primary': model}}}, \
'models': {'mode': 'merge', 'providers': {'nvidia': { \
'baseUrl': 'https://inference.local/v1', \
'apiKey': 'openshell-managed', \
'api': 'openai-completions', \
'models': [{'id': model.split('/')[-1], 'name': model, 'reasoning': False, 'input': ['text'], 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': 131072, 'maxTokens': 4096}] \
}}}, \
'agents': {'defaults': {'model': {'primary': f'inference/{model}'}}}, \
'models': {'mode': 'merge', 'providers': { \
'nvidia': { \
'baseUrl': 'https://inference.local/v1', \
'apiKey': 'openshell-managed', # pragma: allowlist secret \
'api': 'openai-completions', \
'models': [{'id': model.split('/')[-1], 'name': model, 'reasoning': False, 'input': ['text'], 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': 131072, 'maxTokens': 4096}] \
}, \
'inference': { \
'baseUrl': 'https://inference.local/v1', \
'apiKey': 'unused', # pragma: allowlist secret \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was testing this out because onboarding is broken for me (see #580), but the # pragma: allowlist secret comment here (and also above on line 108) seemed to break this for me. It resulted in:

SyntaxError: '{' was never closed

'api': 'openai-completions', \
'models': [{'id': model, 'name': model, 'reasoning': False, 'input': ['text'], 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': 131072, 'maxTokens': 4096}] \
} \
}}, \
'gateway': { \
'mode': 'local', \
'controlUi': { \
Expand Down
45 changes: 6 additions & 39 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,50 +103,17 @@ function buildSandboxConfigSyncScript(selectionConfig) {
? "vllm-local"
: "nvidia-nim";
const primaryModel = getOpenClawPrimaryModel(providerType, selectionConfig.model);
const providerKey = "inference";
const providerConfig = {
baseUrl: selectionConfig.endpointUrl,
apiKey: "unused",
api: "openai-completions",
models: [
{
id: selectionConfig.model,
name: selectionConfig.model,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 131072,
maxTokens: 4096,
},
],
};
// openclaw.json is immutable (root:root 444, Landlock read-only) — never
// write to it at runtime. The inference provider and default model are baked
// at image build time. We only write the NemoClaw selection config (writable
// ~/.nemoclaw/) and override the active model via `openclaw models set`,
// which writes to the agent-level config in ~/.openclaw-data/ (writable).
return `
set -euo pipefail
mkdir -p ~/.nemoclaw ~/.openclaw
mkdir -p ~/.nemoclaw
cat > ~/.nemoclaw/config.json <<'EOF_NEMOCLAW_CFG'
${JSON.stringify(selectionConfig, null, 2)}
EOF_NEMOCLAW_CFG
python3 - <<'PYCFG'
import json
import os

cfg_path = os.path.expanduser('~/.openclaw/openclaw.json')
cfg = {}
if os.path.exists(cfg_path):
with open(cfg_path) as f:
cfg = json.load(f)

cfg.setdefault('agents', {}).setdefault('defaults', {}).setdefault('model', {})['primary'] = ${JSON.stringify(primaryModel)}
models_cfg = cfg.setdefault('models', {})
models_cfg.setdefault('mode', 'merge')
providers_cfg = models_cfg.setdefault('providers', {})
providers_cfg[${JSON.stringify(providerKey)}] = json.loads(${pythonLiteralJson(providerConfig)})

with open(cfg_path, 'w') as f:
json.dump(cfg, f, indent=2)

os.chmod(cfg_path, 0o600)
PYCFG
openclaw models set ${shellQuote(primaryModel)} > /dev/null 2>&1 || true
exit
`.trim();
Expand Down
5 changes: 3 additions & 2 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ PYAUTOPAIR
}

echo 'Setting up NemoClaw...'
openclaw doctor --fix > /dev/null 2>&1 || true
# openclaw doctor --fix and openclaw plugins install already ran at build time
# (Dockerfile Step 28). At runtime they fail with EPERM against the locked
# /sandbox/.openclaw directory and accomplish nothing.
write_auth_profile
openclaw plugins install /opt/nemoclaw > /dev/null 2>&1 || true

if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then
exec "${NEMOCLAW_CMD[@]}"
Expand Down
13 changes: 8 additions & 5 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const assert = require("node:assert/strict");
const { buildSandboxConfigSyncScript } = require("../bin/lib/onboard");

describe("onboard helpers", () => {
it("builds a sandbox sync script that writes config and updates the selected model", () => {
it("builds a sandbox sync script that writes config and sets the model", () => {
const script = buildSandboxConfigSyncScript({
endpointType: "custom",
endpointUrl: "https://inference.local/v1",
Expand All @@ -18,14 +18,17 @@ describe("onboard helpers", () => {
onboardedAt: "2026-03-18T12:00:00.000Z",
});

// Writes NemoClaw selection config to writable ~/.nemoclaw/
assert.match(script, /cat > ~\/\.nemoclaw\/config\.json/);
assert.match(script, /"model": "nemotron-3-nano:30b"/);
assert.match(script, /"credentialEnv": "OPENAI_API_KEY"/);

// Sets the active model via openclaw CLI (writes to agent config, not openclaw.json)
assert.match(script, /openclaw models set 'inference\/nemotron-3-nano:30b'/);
assert.match(script, /cfg\.setdefault\('agents', \{\}\)\.setdefault\('defaults', \{\}\)\.setdefault\('model', \{\}\)\['primary'\]/);
assert.match(script, /providers_cfg\["inference"\]/);
assert.match(script, /json\.loads\("\{\\\"baseUrl\\\":\\\"https:\/\/inference\.local\/v1\\\",\\\"apiKey\\\":\\\"unused\\\"/);
assert.match(script, /inference\/nemotron-3-nano:30b/);

// Must NOT write to openclaw.json — it is immutable (root:root 444)
assert.doesNotMatch(script, /openclaw\.json/);

assert.match(script, /^exit$/m);
});
});