Skip to content
Merged
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}'}}}, \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this part is where I'm concerned. We need to have all the inference choices in BEFORE we configure the immutable file OR we have to have a way to update the config from outside the sandbox (this is more likely what we need)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, the model inference choices have to be handled from the outside the sandbox via openshell with initial versions baked in before we configure immutability - going to add additional tests and affordances for that.

@EltronAI EltronAI Mar 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi @ericksoa and @kjw3, since openclaw.json is now immutable at runtime in the sandbox, what is the recommended way to update config settings and override skill configurations if defaults are baked in at build time? openclaw config set commands can no longer update in sandbox. Clarifying this would help users safely manage OpenClaw config settings and skills under the new sandbox model.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @EltronAI we are looking at ways to implement this in a way that is consistent with the policy engine in openshell, specifically allowing more granular control of what can change. For example, there maybe configurations where the policy engine will forbid changes to skills unless certain guidelines are met, or forbid changes to system prompt, and others where those changes are 100% fine.

Some of these are design details we are still working out, and hope to support very soon.

Appreciate the feedback!

'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}] \
}, \
'inference': { \
'baseUrl': 'https://inference.local/v1', \
'apiKey': 'unused', \
'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
60 changes: 5 additions & 55 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ const {
const {
CLOUD_MODEL_OPTIONS,
DEFAULT_CLOUD_MODEL,
DEFAULT_OLLAMA_MODEL,
getOpenClawPrimaryModel,
getProviderSelectionConfig,
} = require("./inference-config");
const {
Expand Down Expand Up @@ -102,65 +100,17 @@ function getStableGatewayImageRef(versionOutput = null) {
return `ghcr.io/nvidia/openshell/cluster:${version}`;
}

function pythonLiteralJson(value) {
return JSON.stringify(JSON.stringify(value));
}

function buildSandboxConfigSyncScript(selectionConfig) {
const providerType =
selectionConfig.profile === "inference-local"
? selectionConfig.model === DEFAULT_OLLAMA_MODEL
? "ollama-local"
: "nvidia-nim"
: selectionConfig.endpointType === "vllm"
? "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. Model routing is handled by the host-side
// gateway (`openshell inference set` in Step 5), not from inside the
// sandbox. We only write the NemoClaw selection config (~/.nemoclaw/).
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
14 changes: 8 additions & 6 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const {
} = 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 only writes nemoclaw config", () => {
const script = buildSandboxConfigSyncScript({
endpointType: "custom",
endpointUrl: "https://inference.local/v1",
Expand All @@ -22,14 +22,16 @@ 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"/);
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 modify openclaw config from inside the sandbox — model routing
// is handled by the host-side gateway (openshell inference set)
assert.doesNotMatch(script, /openclaw\.json/);
assert.doesNotMatch(script, /openclaw models set/);

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

Expand Down
Loading