Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
73c58d2
refactor(cli): group remaining architecture modules
cv May 8, 2026
8f3ebc1
refactor(onboard): extract initial policy helpers
cv May 8, 2026
3d31299
refactor(onboard): extract compatible endpoint smoke helpers
cv May 8, 2026
05d1041
refactor(onboard): extract selection drift helpers
cv May 8, 2026
0055de0
refactor(onboard): extract config sync helpers
cv May 9, 2026
0b5c164
refactor(onboard): extract dockerfile patch helpers
cv May 9, 2026
57b3d61
Merge remote-tracking branch 'origin/main' into refactor/cli-architec…
cv May 9, 2026
1783c07
Merge branch 'refactor/cli-architecture-layout' into refactor/onboard…
cv May 9, 2026
66b0235
Merge branch 'refactor/onboard-initial-policy' into refactor/onboard-…
cv May 9, 2026
7ddddfb
Merge branch 'refactor/onboard-compatible-smoke' into refactor/onboar…
cv May 9, 2026
91d0e04
Merge branch 'refactor/onboard-selection-drift' into refactor/onboard…
cv May 9, 2026
af72a06
Merge branch 'refactor/onboard-config-sync' into refactor/onboard-doc…
cv May 9, 2026
bd0ffe5
test(inference): expect Kimi status thinking flag
cv May 9, 2026
5f51c03
Merge branch 'refactor/cli-architecture-layout' into refactor/onboard…
cv May 9, 2026
e3a0652
Merge branch 'refactor/onboard-initial-policy' into refactor/onboard-…
cv May 9, 2026
c525cbe
Merge branch 'refactor/onboard-compatible-smoke' into refactor/onboar…
cv May 9, 2026
9448574
Merge branch 'refactor/onboard-selection-drift' into refactor/onboard…
cv May 9, 2026
27c0246
Merge branch 'refactor/onboard-config-sync' into refactor/onboard-doc…
cv May 9, 2026
374abc5
Merge remote-tracking branch 'origin/main' into refactor/cli-architec…
cv May 9, 2026
76500de
Merge branch 'refactor/cli-architecture-layout' into refactor/onboard…
cv May 9, 2026
78bd330
Merge branch 'refactor/onboard-initial-policy' into refactor/onboard-…
cv May 9, 2026
31c7252
Merge branch 'refactor/onboard-compatible-smoke' into refactor/onboar…
cv May 9, 2026
5545769
Merge branch 'refactor/onboard-selection-drift' into refactor/onboard…
cv May 9, 2026
0f458e2
Merge branch 'refactor/onboard-config-sync' into refactor/onboard-doc…
cv May 9, 2026
955225f
Merge branch 'main' into refactor/cli-architecture-layout
cv May 9, 2026
18ee079
merge main into cli architecture layout
cv May 9, 2026
2cbb302
test(policy): update tier onboarding policy import
cv May 9, 2026
ed06048
Merge branch 'refactor/cli-architecture-layout' of https://github.com…
cv May 9, 2026
d775298
Merge branch 'refactor/cli-architecture-layout' into refactor/onboard…
cv May 9, 2026
600994d
Merge branch 'refactor/onboard-initial-policy' into refactor/onboard-…
cv May 9, 2026
aed4efa
Merge branch 'refactor/onboard-compatible-smoke' into refactor/onboar…
cv May 9, 2026
a2950bd
Merge branch 'refactor/onboard-selection-drift' into refactor/onboard…
cv May 9, 2026
515d8d3
Merge branch 'refactor/onboard-config-sync' into refactor/onboard-doc…
cv May 9, 2026
13bcc3a
Merge branch 'main' into refactor/cli-architecture-layout
cv May 10, 2026
96008e0
merge(main): update architecture layout stack base
cv May 11, 2026
32cb2b7
merge(stack): update initial policy helpers branch
cv May 11, 2026
43f8482
merge(stack): update compatible smoke branch
cv May 11, 2026
88eb34d
merge(stack): update selection drift branch
cv May 11, 2026
11d9014
merge(stack): update config sync branch
cv May 11, 2026
548a45a
merge(stack): update dockerfile patch branch
cv May 11, 2026
c86a8ed
merge(main): refresh architecture layout branch
cv May 11, 2026
31b255d
Merge branch 'main' into refactor/cli-architecture-layout
cv May 11, 2026
23efdf3
Merge branch 'main' into refactor/cli-architecture-layout
cv May 11, 2026
37d5704
merge(main): refresh architecture layout branch
cv May 11, 2026
5177213
Potential fix for pull request finding 'CodeQL / Unused variable, imp…
cv May 11, 2026
de52330
Merge branch 'refactor/cli-architecture-layout' into refactor/onboard…
cv May 11, 2026
3faec14
Merge branch 'refactor/onboard-initial-policy' into refactor/onboard-…
cv May 11, 2026
cf9c4ce
Merge branch 'refactor/onboard-compatible-smoke' into refactor/onboar…
cv May 11, 2026
a77f84b
Merge branch 'refactor/onboard-selection-drift' into refactor/onboard…
cv May 11, 2026
e716b66
Merge branch 'refactor/onboard-config-sync' into refactor/onboard-doc…
cv May 11, 2026
b7c3f43
Merge remote-tracking branch 'origin/main' into refactor/onboard-comp…
cv May 12, 2026
a89b0fa
Merge branch 'main' into refactor/onboard-compatible-smoke
cv May 12, 2026
e39f01b
Merge branch 'main' into refactor/onboard-compatible-smoke
cv May 12, 2026
0b5b7a9
Merge branch 'main' into refactor/onboard-compatible-smoke
cv May 12, 2026
a4397b0
Merge branch 'main' into refactor/onboard-compatible-smoke
jyaunches May 12, 2026
1fc9fc5
fix(onboard): fail fast in compatible endpoint smoke wrapper
ericksoa May 12, 2026
2b41313
Merge branch 'main' into refactor/onboard-compatible-smoke
cv May 12, 2026
a638e9e
Merge branch 'refactor/onboard-compatible-smoke' into refactor/onboar…
cv May 12, 2026
948f11e
Merge branch 'refactor/onboard-selection-drift' into refactor/onboard…
cv May 12, 2026
ff85a7c
Merge branch 'refactor/onboard-config-sync' into refactor/onboard-doc…
cv May 12, 2026
7203165
merge: origin/main into refactor/onboard-dockerfile-patch
cv May 12, 2026
0d41699
merge: origin/main into refactor/onboard-dockerfile-patch
cv May 13, 2026
b0347b1
fix(onboard): sanitize dockerfile arg replacements
cv May 13, 2026
2f58853
merge: origin/main into refactor/onboard-dockerfile-patch
ericksoa May 13, 2026
cd70819
fix(onboard): preserve falsy docker json args
ericksoa May 13, 2026
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
207 changes: 5 additions & 202 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ const {
buildSandboxConfigSyncScript,
writeSandboxConfigSyncFile,
}: typeof import("./onboard/config-sync") = require("./onboard/config-sync");
const {
isValidProxyHost,
isValidProxyPort,
patchStagedDockerfile,
}: typeof import("./onboard/dockerfile-patch") = require("./onboard/dockerfile-patch");
const {
buildDirectGpuPolicyYaml,
buildDirectSandboxGpuProofCommands,
Expand Down Expand Up @@ -2362,10 +2367,6 @@ function isOpenclawReady(sandboxName: string): boolean {
return Boolean(fetchGatewayAuthTokenFromSandbox(sandboxName));
}

function encodeDockerJsonArg(value: LooseValue): string {
return Buffer.from(JSON.stringify(value || {}), "utf8").toString("base64");
}

function isAffirmativeAnswer(value: string | null | undefined): boolean {
return ["y", "yes"].includes(
String(value || "")
Expand Down Expand Up @@ -2649,204 +2650,6 @@ function verifyWebSearchInsideSandbox(

// getSandboxInferenceConfig — moved to onboard-providers.ts

// Shared validators for NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT.
// Both `patchStagedDockerfile()` (build-time Dockerfile ARG override) and
// `createSandbox()` (runtime sandbox env whitelist) must reject the same
// inputs, otherwise the build and runtime paths can diverge — e.g. a
// build-time-accepted value silently no-ops at runtime, leaving the
// container running with the default proxy. Hostname regex deliberately
// excludes `:` so raw IPv6 literals are rejected: the runtime
// `http://${HOST}:${PORT}` template does not bracket them and would
// produce a malformed URL. Port is range-checked because a 5-digit
// length filter alone would accept out-of-range values like 70000.
const PROXY_HOST_RE = /^[A-Za-z0-9._-]+$/;
function isValidProxyHost(value: string): boolean {
return PROXY_HOST_RE.test(value);
}
function isValidProxyPort(value: string): boolean {
if (!/^[0-9]{1,5}$/.test(value)) return false;
const port = Number(value);
return port >= 1 && port <= 65535;
}

function patchStagedDockerfile(
dockerfilePath: string,
model: string,
chatUiUrl: string,
buildId = String(Date.now()),
provider: string | null = null,
preferredInferenceApi: string | null = null,
webSearchConfig: WebSearchConfig | null = null,
messagingChannels: string[] = [],
messagingAllowedIds: LooseObject = {},
discordGuilds: LooseObject = {},
baseImageRef: string | null = null,
telegramConfig: LooseObject = {},
darwinVmCompat = false,
) {
const { providerKey, primaryModelRef, inferenceBaseUrl, inferenceApi, inferenceCompat } =
getSandboxInferenceConfig(model, provider, preferredInferenceApi);
let dockerfile = fs.readFileSync(dockerfilePath, "utf8");
// Pin the base image to a specific digest when available (#1904).
// The ref must come from pullAndResolveBaseImageDigest() — never from
// blueprint.yaml, whose digest belongs to a different registry.
// Only rewrite when the current value already points at our sandbox-base
// image — custom --from Dockerfiles may use a different base.
if (baseImageRef) {
dockerfile = dockerfile.replace(
/^ARG BASE_IMAGE=(.*)$/m,
(line: string, currentValue: string) => {
const trimmed = String(currentValue).trim();
if (
trimmed.startsWith(`${SANDBOX_BASE_IMAGE}:`) ||
trimmed.startsWith(`${SANDBOX_BASE_IMAGE}@`)
) {
return `ARG BASE_IMAGE=${baseImageRef}`;
}
return line;
},
);
}
dockerfile = dockerfile.replace(/^ARG NEMOCLAW_MODEL=.*$/m, `ARG NEMOCLAW_MODEL=${model}`);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_PROVIDER_KEY=.*$/m,
`ARG NEMOCLAW_PROVIDER_KEY=${providerKey}`,
);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_PRIMARY_MODEL_REF=.*$/m,
`ARG NEMOCLAW_PRIMARY_MODEL_REF=${primaryModelRef}`,
);
dockerfile = dockerfile.replace(/^ARG CHAT_UI_URL=.*$/m, `ARG CHAT_UI_URL=${chatUiUrl}`);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_INFERENCE_BASE_URL=.*$/m,
`ARG NEMOCLAW_INFERENCE_BASE_URL=${inferenceBaseUrl}`,
);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_INFERENCE_API=.*$/m,
`ARG NEMOCLAW_INFERENCE_API=${inferenceApi}`,
);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_INFERENCE_COMPAT_B64=.*$/m,
`ARG NEMOCLAW_INFERENCE_COMPAT_B64=${encodeDockerJsonArg(inferenceCompat)}`,
);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_BUILD_ID=.*$/m,
`ARG NEMOCLAW_BUILD_ID=${buildId}`,
);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_DARWIN_VM_COMPAT=.*$/m,
`ARG NEMOCLAW_DARWIN_VM_COMPAT=${darwinVmCompat ? "1" : "0"}`,
);
// Honor NEMOCLAW_CONTEXT_WINDOW / NEMOCLAW_MAX_TOKENS / NEMOCLAW_REASONING
// so the user can tune model metadata without editing the Dockerfile.
const POSITIVE_INT_RE = /^[1-9][0-9]*$/;
const contextWindow = process.env.NEMOCLAW_CONTEXT_WINDOW;
if (contextWindow && POSITIVE_INT_RE.test(contextWindow)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_CONTEXT_WINDOW=.*$/m,
`ARG NEMOCLAW_CONTEXT_WINDOW=${contextWindow}`,
);
}
const maxTokens = process.env.NEMOCLAW_MAX_TOKENS;
if (maxTokens && POSITIVE_INT_RE.test(maxTokens)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_MAX_TOKENS=.*$/m,
`ARG NEMOCLAW_MAX_TOKENS=${maxTokens}`,
);
}
const reasoning = process.env.NEMOCLAW_REASONING;
if (reasoning === "true" || reasoning === "false") {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_REASONING=.*$/m,
`ARG NEMOCLAW_REASONING=${reasoning}`,
);
}
// Honor NEMOCLAW_INFERENCE_INPUTS for vision-capable models. OpenClaw's
// model schema currently accepts "text" and "image" only, so validate
// strictly against that vocabulary. Adding modalities to OpenClaw later
// only requires widening this regex. See #2421.
const inferenceInputs = process.env.NEMOCLAW_INFERENCE_INPUTS;
if (inferenceInputs && /^(text|image)(,(text|image))*$/.test(inferenceInputs)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_INFERENCE_INPUTS=.*$/m,
`ARG NEMOCLAW_INFERENCE_INPUTS=${inferenceInputs}`,
);
}
// NEMOCLAW_AGENT_TIMEOUT — override agents.defaults.timeoutSeconds at build
// time. Lets users increase the per-request inference timeout without
// editing the Dockerfile. Ref: issue #2281
const agentTimeout = process.env.NEMOCLAW_AGENT_TIMEOUT;
if (agentTimeout && POSITIVE_INT_RE.test(agentTimeout)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_AGENT_TIMEOUT=.*$/m,
`ARG NEMOCLAW_AGENT_TIMEOUT=${agentTimeout}`,
);
}
// NEMOCLAW_AGENT_HEARTBEAT_EVERY — override agents.defaults.heartbeat.every
// at build time. Accepts Go-style durations with a required s/m/h suffix
// ("30m", "1h"); "0m" disables heartbeat. Ref: issue #2880
const agentHeartbeat = process.env.NEMOCLAW_AGENT_HEARTBEAT_EVERY;
if (agentHeartbeat && /^\d+(s|m|h)$/.test(agentHeartbeat)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_AGENT_HEARTBEAT_EVERY=.*$/m,
`ARG NEMOCLAW_AGENT_HEARTBEAT_EVERY=${agentHeartbeat}`,
);
}
// Honor NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT exported in the host
// shell so the sandbox-side nemoclaw-start.sh sees them via $ENV at runtime.
// Without this, the host export is silently dropped at image build time and
// the sandbox falls back to the default 10.200.0.1:3128 proxy. See #1409.
const proxyHostEnv = process.env.NEMOCLAW_PROXY_HOST;
if (proxyHostEnv && isValidProxyHost(proxyHostEnv)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_PROXY_HOST=.*$/m,
`ARG NEMOCLAW_PROXY_HOST=${proxyHostEnv}`,
);
}
const proxyPortEnv = process.env.NEMOCLAW_PROXY_PORT;
if (proxyPortEnv && isValidProxyPort(proxyPortEnv)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_PROXY_PORT=.*$/m,
`ARG NEMOCLAW_PROXY_PORT=${proxyPortEnv}`,
);
}
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_WEB_SEARCH_ENABLED=.*$/m,
`ARG NEMOCLAW_WEB_SEARCH_ENABLED=${webSearchConfig ? "1" : "0"}`,
);
// Onboard flow expects immediate dashboard access without device pairing,
// so disable device auth for images built during onboard (see #1217).
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_DISABLE_DEVICE_AUTH=.*$/m,
`ARG NEMOCLAW_DISABLE_DEVICE_AUTH=1`,
);
if (messagingChannels.length > 0) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_MESSAGING_CHANNELS_B64=.*$/m,
`ARG NEMOCLAW_MESSAGING_CHANNELS_B64=${encodeDockerJsonArg(messagingChannels)}`,
);
}
if (Object.keys(messagingAllowedIds).length > 0) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=.*$/m,
`ARG NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=${encodeDockerJsonArg(messagingAllowedIds)}`,
);
}
if (Object.keys(discordGuilds).length > 0) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_DISCORD_GUILDS_B64=.*$/m,
`ARG NEMOCLAW_DISCORD_GUILDS_B64=${encodeDockerJsonArg(discordGuilds)}`,
);
}
if (telegramConfig && Object.keys(telegramConfig).length > 0) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_TELEGRAM_CONFIG_B64=.*$/m,
`ARG NEMOCLAW_TELEGRAM_CONFIG_B64=${encodeDockerJsonArg(telegramConfig)}`,
);
}
fs.writeFileSync(dockerfilePath, dockerfile);
}

// Inference probes — moved to inference/onboard-probes.ts
const {
hasResponsesToolCall,
Expand Down
Loading
Loading