diff --git a/docs/manage-sandboxes/runtime-controls.mdx b/docs/manage-sandboxes/runtime-controls.mdx index a55ccf862ae..a50ccdfd837 100644 --- a/docs/manage-sandboxes/runtime-controls.mdx +++ b/docs/manage-sandboxes/runtime-controls.mdx @@ -33,6 +33,7 @@ The table below maps each commonly changed item to the layer that owns it and th | Channel enable/disable (turn a configured channel off without removing the token) | Rebuild required (`openclaw.json` is the source of truth at runtime, see #3453) | `$$nemoclaw channels stop ` then rebuild | | Dashboard forward port | Runtime. Port is re-resolved on next `connect` | `NEMOCLAW_DASHBOARD_PORT= $$nemoclaw connect` | | Dashboard bind address (loopback compared to all interfaces) | Runtime. Applies on next `connect` | `NEMOCLAW_DASHBOARD_BIND=0.0.0.0 $$nemoclaw connect` (see #3259) | +| Default OpenClaw workspace template seed (`AGENTS.md`, `SOUL.md`, `IDENTITY.md`, `USER.md`, `TOOLS.md`, `HEARTBEAT.md`) | Locked at first sandbox boot. Re-onboard required to change the bake-time choice. | Set `NEMOCLAW_MINIMAL_BOOTSTRAP=1` before `$$nemoclaw onboard` to skip default template seeding for new/pristine workspaces. **Does not delete files already present.** Partial mitigation for #2598 (cuts ~3k tokens of project-context overhead off OpenClaw's per-turn bootstrap injection). | | Web search backend (Brave, Tavily, and so on) | Runtime through `web.backend` config flag; rebuild only if `web.fetchEnabled` flips | `$$nemoclaw config set --key web.backend --value tavily` | | Filesystem layout (Landlock zones, read-only mounts, container caps) | **Locked at creation**. No runtime change | Re-onboard with `$$nemoclaw onboard --recreate-sandbox` | | Sandbox name | **Locked at creation** | Re-onboard with a different `--name` | diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index b3387529318..57837b497cc 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -2746,6 +2746,14 @@ seed_default_workspace_templates() { local templates_dir="${2:-}" local config_file="${3:-/sandbox/.openclaw/openclaw.json}" + # #2598: opt-in flag that skips default workspace template seeding for + # new/pristine workspaces (does NOT delete files already present). Cuts + # ~3k tokens off OpenClaw's per-turn bootstrap context injection. + if [ "${NEMOCLAW_MINIMAL_BOOTSTRAP:-}" = "1" ]; then + echo "[setup] NEMOCLAW_MINIMAL_BOOTSTRAP=1; skipping default workspace template seed" >&2 + return 0 + fi + if [ ! -f "$config_file" ]; then return 0 fi diff --git a/src/lib/onboard/host-proxy-env.ts b/src/lib/onboard/host-proxy-env.ts index 8b0f242a751..c0185961011 100644 --- a/src/lib/onboard/host-proxy-env.ts +++ b/src/lib/onboard/host-proxy-env.ts @@ -29,6 +29,21 @@ export function appendHostProxyEnvArgs( } } + // #2598: NEMOCLAW_MINIMAL_BOOTSTRAP is a host-side opt-in flag (set to + // "1") that the sandbox's nemoclaw-start.sh:seed_default_workspace_templates + // reads to skip default workspace template seeding for new/pristine + // workspaces (does NOT delete files already present), knocking ~3k tokens + // off OpenClaw's per-turn bootstrap context injection. Partial #2598 + // mitigation: addresses the project-context contribution from NemoClaw's + // seeded templates; the remaining OpenClaw framework/non-project context + // is tracked upstream. Bundled here with the proxy propagation because + // both are env vars forwarded from the host into `openshell sandbox + // create -- env ... nemoclaw-start`, and the top-level onboard.ts + // entrypoint is line-budget-constrained per codebase-growth-guardrails. + if (env.NEMOCLAW_MINIMAL_BOOTSTRAP === "1") { + envArgs.push(formatEnvAssignment("NEMOCLAW_MINIMAL_BOOTSTRAP", "1")); + } + const hasProxy = proxyEnv.HTTP_PROXY || proxyEnv.HTTPS_PROXY || proxyEnv.http_proxy || proxyEnv.https_proxy; if (!hasProxy) return; diff --git a/test/nemoclaw-start.test.ts b/test/nemoclaw-start.test.ts index d00f2324069..78accada81d 100644 --- a/test/nemoclaw-start.test.ts +++ b/test/nemoclaw-start.test.ts @@ -2652,7 +2652,7 @@ describe("seed_default_workspace_templates (#3240)", () => { ); return spawnSync("bash", [scriptPath], { encoding: "utf-8", - env: { ...process.env, ...(options.env ?? {}) }, + env: { ...process.env, NEMOCLAW_MINIMAL_BOOTSTRAP: "", ...(options.env ?? {}) }, timeout: 5000, }); } @@ -2934,6 +2934,52 @@ describe("seed_default_workspace_templates (#3240)", () => { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); + + it("skips seeding when NEMOCLAW_MINIMAL_BOOTSTRAP=1 (#2598)", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-seed-minimal-")); + const workspaceDir = path.join(tmpDir, "workspace"); + const templatesDir = path.join(tmpDir, "templates"); + fs.mkdirSync(workspaceDir, { recursive: true }); + writeTemplates(templatesDir); + try { + const result = runSeed(workspaceDir, templatesDir, path.join(tmpDir, "seed.sh"), { + env: { NEMOCLAW_MINIMAL_BOOTSTRAP: "1" }, + }); + expect(result.status).toBe(0); + expect(result.stderr).toContain("NEMOCLAW_MINIMAL_BOOTSTRAP=1"); + expect(result.stderr).toContain("skipping default workspace template seed"); + for (const name of [ + "AGENTS.md", + "SOUL.md", + "IDENTITY.md", + "USER.md", + "TOOLS.md", + "HEARTBEAT.md", + ]) { + expect(fs.existsSync(path.join(workspaceDir, name))).toBe(false); + } + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it("still seeds when NEMOCLAW_MINIMAL_BOOTSTRAP is not '1' (#2598)", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-seed-noopt-")); + const workspaceDir = path.join(tmpDir, "workspace"); + const templatesDir = path.join(tmpDir, "templates"); + fs.mkdirSync(workspaceDir, { recursive: true }); + writeTemplates(templatesDir); + try { + const result = runSeed(workspaceDir, templatesDir, path.join(tmpDir, "seed.sh"), { + env: { NEMOCLAW_MINIMAL_BOOTSTRAP: "0" }, + }); + expect(result.status).toBe(0); + expect(result.stderr).not.toContain("skipping default workspace template seed"); + expect(fs.existsSync(path.join(workspaceDir, "SOUL.md"))).toBe(true); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); }); describe("Slack secrets-on-disk tripwire (#2085)", () => { diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 17a6ca861f5..761a9ad198e 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -173,6 +173,21 @@ describe("onboard helpers", () => { } }); + it("propagates NEMOCLAW_MINIMAL_BOOTSTRAP=1 from host into sandbox env (#2598)", () => { + const envArgs: string[] = []; + appendHostProxyEnvArgs(envArgs, { NEMOCLAW_MINIMAL_BOOTSTRAP: "1" }); + expect(envArgs).toContain("NEMOCLAW_MINIMAL_BOOTSTRAP=1"); + }); + + it("omits NEMOCLAW_MINIMAL_BOOTSTRAP when unset or not the literal '1' (#2598)", () => { + for (const value of [undefined, "", "0", "true", "yes"]) { + const envArgs: string[] = []; + const env: NodeJS.ProcessEnv = value === undefined ? {} : { NEMOCLAW_MINIMAL_BOOTSTRAP: value }; + appendHostProxyEnvArgs(envArgs, env); + expect(envArgs.some((e) => e.startsWith("NEMOCLAW_MINIMAL_BOOTSTRAP="))).toBe(false); + } + }); + it("prints doctor logs automatically when gateway fails to start (#1605)", testTimeoutOptions(20_000), () => { const repoRoot = path.join(import.meta.dirname, ".."); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gateway-diag-"));