Repository navigation
fix: ~/ paths in list-entry includes and placeholder values point under the env root after migrate - #983
Conversation
…nt under the env root after migrate
`ocm migrate` and `ocm adopt import` now also keep `~/...` values in `nodeHost.workerRuns.folders`, in includes placed at a list position and in values containing a `${VAR}` placeholder pointing at the location the plain install used. Home-relative trust roots in `skills.load.allowSymlinkTargets` and `security.installPolicy.exec.trustedDirs` stay as written, and migration now warns that they resolve under the new environment root.
Fixes #972.
…utside it after migrate
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
…lders, handle root paths
A `~/.openclaw/...` value with a `${VAR}` placeholder stays as written and is now named in a migration warning, because the placeholder may lead outside the state directory. A worker folder under `~/.openclaw` whose `..` does not lead to an existing directory stays as written, so OpenClaw keeps rejecting it instead of the copied target. A value resolving to the filesystem root no longer panics.
…missing directory A worker folder whose `..` leaves `~/.openclaw` is spelled out again, so it keeps naming the plain install's target instead of a directory under the env root. Only an entry the later relocation would move into the copied state although its source path does not resolve stays as written. Placeholder entries in trust roots are covered by tests, and a list-entry include shared by a path list and a trust root is pinned as refused.
Migration can change the copied state's links and directories, for example when it imports a linked workspace, so a worker folder whose `..` or placeholder leads into `~/.openclaw` cannot be spelled out to the same target. It now stays as written and migration prints a warning naming nodeHost.workerRuns.folders. Entries the state relocation leaves alone are still spelled out against the source home.
Migration can change the links and directories a worker folder resolves through, and OpenClaw uses that folder as a permission boundary, so `nodeHost.workerRuns.folders` entries are no longer spelled out or relocated. Every home-relative entry stays exactly as on main and migration prints a warning naming the setting. The limit is documented in USAGE and stays open in #972.
… link
A spelled-out placeholder value that leads into `~/.openclaw` through a symlink would be relocated into the env as if the placeholder were a directory name, while OpenClaw substitutes it before resolving `..`. Such values stay as written and are named in the placeholder warning. An escaped `$${VAR}` is no longer treated as a placeholder.
OpenClaw unescapes `$${X}` to the literal `${X}` before resolving a path, so a spelled-out value whose `$${X}` component is a link into `~/.openclaw` would be relocated to the wrong directory. The relocation guard now covers every value containing `${`, escaped or not. USAGE and CHANGELOG name placeholder values that reach `~/.openclaw` through a symlink.
|
Codex review: needs maintainer review before merge. What this changesThis PR preserves home-relative paths in list-entry includes and placeholder values during migration, and warns about settings deliberately kept unchanged. Example: Alice migrates a home where skills.load.extraDirs includes skills-dir.json containing "~/listdir".
Review scores
ProductKind: Bug fix · Worth it: Yes · Fix scope: Partial Merge readiness✅ Ready for maintainer review Keep this PR open: it provides a useful migration fix absent from current main, with no actionable introduced defect found. Priority: P2 Before mergeNone. FindingsNone. Tests
Agent review detailsHow this fits togetherOCM's shared migration writer receives copied OpenClaw configuration and include scopes, rewrites paths for the new environment, and returns warnings to the migrate and adopt import CLI handlers. flowchart LR
A[Plain OpenClaw home] --> B[Migration copy]
B --> C[Typed include traversal]
C --> D[Home path rewrite]
D --> E[Existing state relocation]
E --> F[Managed environment config]
D --> G[CLI warnings]
Technical reviewBest possible solution: Land the scoped migration repair through the existing include and rewrite owners, retaining the separate permission-preservation follow-up. Do we have a high-confidence way to reproduce the issue? Current-main source leaves list-entry include scopes untyped and skips placeholder paths; the PR supplies matching CLI before-and-after results, while this read-only review ran no target code. Is this the best way to solve the issue? Recording segment types during include traversal fixes the ambiguity at its owner, and placeholder expansion preserves substitution order while reusing existing relocation checks. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 779c5cc648f2. Provenance checked
TestingProof path: shipped entry point. SecurityNone. EvidenceWhat I checked:
LabelsLabel changes:
Label justifications:
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment HistoryReview history (1 earlier review cycle)
Reviewed November 2, 2026, 9:11 AM ET / 14:11 UTC (Revision 2). |
…d-cases # Conflicts: # CHANGELOG.md
|
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
|
Fixed: |
Refs #972
What Problem This Solves
Fixes:
~/...path values in includes placed at a list position and~/...path values containing a${VAR}placeholder point under the new environment root afterocm migrateorocm adopt import, and home-relative trust roots and worker folders change meaning without any warning.#946 made migration spell out
~/...config paths that point outside~/.openclaw, because a migrated environment runs withOPENCLAW_HOMEset to its root and OpenClaw expands a leading~againstOPENCLAW_HOMEbeforeHOME. It deliberately left four cases as written and tracked them in #972:skills.load.extraDirs: [{"$include": "./dir.json"}]. Include scopes were recorded as plain strings, so once arrays from several includes are concatenated a list index could not be told apart from a digit-only object key, and fix: ~/ config paths outside ~/.openclaw point at missing locations after migrate #946 treated every recorded segment as a key.${VAR}placeholder, such as~/${SKILLS_DIR}/shared. OpenClaw substitutes placeholders before it resolves the path, so resolving..before substitution could pick a different directory.skills.load.allowSymlinkTargetsandsecurity.installPolicy.exec.trustedDirs. After migration a~/...entry names a directory under the environment root, so it stops trusting what the plain install trusted.nodeHost.workerRuns.folders, the folders a node host lets workers use. OpenClaw's check (src/node-host/node-worker-device-folder.ts) replaces the leading~and then callsfs.realpathSync.native.On current
mainall four are copied verbatim and silently resolve under the environment root. For example, migrating a home withskills.load.extraDirs: [{"$include": "./skills-dir.json"}], where the include holds"~/listdir", leaves an environment whose skills directory is<env root>/listdir. Both commands exit 0 and nothing says so.User Impact
User impact: after
ocm migrateorocm adopt import, path values in list-entry includes and placeholder paths keep naming the directories the plain install used, and every home-relative value migration cannot safely carry over is named in a warning instead of changing meaning silently."0"underpathsis still not treated as a list entry.${VAR}placeholder, only the literal directories before the first placeholder are resolved; the placeholder and everything after it stay as written for OpenClaw to substitute and resolve.~/${SKILLS_DIR}/sharedbecomes<home>/${SKILLS_DIR}/shared, treated exactly like its absolute spelling, so a placeholder that itself leads into~/.openclawnames the source's state directory, as<home>/${X}already would. A placeholder value under~/.openclaw, such as~/.openclaw/${SUB}/skills, stays as written so it keeps naming the environment's copy, and migration warns, because a placeholder such asSUB=..could still lead outside~/.openclaw. The same applies to a placeholder value that reaches~/.openclawthrough a symlink, such as~/alias/${SUB}withaliaslinking to~/.openclaw: migration's later relocation of state paths would treat the placeholder as a directory name, while OpenClaw substitutes it before resolving... An escaped$${VAR}, which OpenClaw reads as the literal text${VAR}, does not split the value, but a spelled-out value containing it gets the same relocation check, because OpenClaw unescapes it before resolving the path.skills.load.allowSymlinkTargetsandsecurity.installPolicy.exec.trustedDirskeep their~/...entries as written, as before. Migration now warns about each such setting whose entries resolve differently.nodeHost.workerRuns.folderskeeps every~/...entry exactly as onmain, and migration now warns about the setting. These entries are a permission boundary resolved through symlinks, and migration itself can change the links and directories they resolve through: it copies the state directory into the environment, relocates absolute state paths into the copy, and imports a linked or external workspace asworkspaces/default. Earlier revisions of this PR spelled worker entries out and guarded the result, and each revision still had a layout in which the written entry admitted a folder the plain install rejected (a directory next to the environment, the imported workspace, a missing directory). Keeping the entry and saying so is the only choice that never makes migration grant access the plain install did not; the remaining gap stays open inmigratestill leaves~/innodeHost.workerRuns.foldersand in list-entry includes of concatenated arrays #972.The warnings go to stderr like the existing migration warnings, one per setting, with
[]standing for a list entry;--jsonoutput is unchanged:Entries under
~/.openclawand absolute entries of the trust roots produce no warning; absolute worker folders and worker entries that are not home-relative (OpenClaw does not trim them) produce none either. Environments that already exist are not touched: the rewrite runs only whilemigrateoradopt importcreates an environment.env clone,env exportandenv importare unchanged.A copied include used both at a position that is spelled out and at one that is kept, for example one file included as a
skills.load.extraDirsentry and as a trust-root entry, cannot be rewritten for one and kept for the other. Migration refuses it with the existing shared-include message, as it already does for includes shared between differently handled settings. Before this change both uses were left as written.Why This Change Was Made
~expansion relies on the type, and the Gateway port, plugin load path and agent directory rewrites compare each step by its spelling exactly as before, so their behavior does not change.nodeHost.workerRuns.foldersare marked as kept, and the expansion reports each kept setting whose entries may now resolve elsewhere, together with placeholder values it keeps under~/.openclaw. The CLI prints one warning per reported setting.${VAR}, then expands~and resolves the path. Replacing the leading~and resolving only the literal directories before the placeholder gives the same result after substitution, whatever the placeholder holds, as long as nothing later rewrites the value. Migration's relocation of absolute state paths runs afterwards and follows links, so a spelled-out placeholder value that relocation would move is kept as written and reported instead; the expansion asks that same relocation, on a scratch copy of the value, before writing. A value that resolves to the filesystem root, such as~/../..from a two-level home, is spelled/.docs/USAGE.mdreplaces the list of cases that were not rewritten with these rules, the warnings and the worker-folder limit, andCHANGELOG.mdhas an Unreleased entry.Evidence
Before and after. A Linux debug build (main at 31fc297, then this branch), isolated
HOMEandOCM_HOME, migrated a home whose config has worker folders["~/work", "~/hop/../work2", "~/.openclaw/wf"],hooksbuilt from two concatenated includes whereb.jsonplaces{"$include": "./p.json"}("~/bin") at a list entry,skills.load.extraDirs: [{"$include": "./skills-dir.json"}, "~/${SKILLS_SUB}/shared"],logging.file: "~/${LOG_NAME}.log", and the two trust roots as~/.... Each value was then resolved the way OpenClaw resolves it, for the plain install withHOMEand for the environment with theOPENCLAW_HOMEreported byocm env exec.ocm migrateandocm adopt importgave identical results:~/bin<home>/bin<env root>/bin<home>/bin~/listdir<home>/listdir<env root>/listdir<home>/listdir~/${SKILLS_SUB}/shared<home>/sk/shared<env root>/sk/shared<home>/sk/shared~/${LOG_NAME}.log<home>/gw.log<env root>/gw.log<home>/gw.log~/trusted,~/bin<home>/…<env root>/…, silent<env root>/…, warning names both settings~/work,~/hop/../work2<home>/…<env root>/…, silent<env root>/…, warning names the setting~/.openclaw/wfTests.
tests/migrate_command_tests.rs, each case through bothmigrateandadopt import:migrate_and_adopt_expand_home_relative_paths_in_includes_at_list_entries(replaces the test that asserted the old spelling): askills.load.extraDirslist-entry include, and the concatenatedhooksincludes where a digit-only"0"key underpathsstays as written while the list-entry include is expanded.migrate_and_adopt_expand_home_relative_paths_with_placeholders_before_substitution: placeholders after the home, after.., inside a file name, under~/.openclaw(kept, with exactly one warning namingskills.load.extraDirs[]), and in message text (kept, no warning).migrate_and_adopt_keep_home_relative_trust_roots_and_name_them: both trust roots kept, including an entry from a list-entry include with whitespace that OpenClaw trims, and exactly one warning per setting.migrate_and_adopt_keep_placeholder_paths_that_lead_into_the_state_dir_through_a_link:~/alias/${SUB}through a link to~/.openclawis kept and named, while~/shared/${SUB}is spelled out.migrate_and_adopt_name_trust_roots_with_placeholders_under_the_state_dir:~/.openclaw/${...}trust-root entries, one from a list-entry include, are kept and named.migrate_and_adopt_do_not_name_trust_roots_that_keep_their_meaning:~/.openclaw/...and absolute trust roots produce no warning.migrate_and_adopt_keep_worker_folders_and_name_them:~/work,~/hop/../work(through a symlink),~/.openclaw/wf, a placeholder entry, a whitespace-prefixed entry and an absolute entry all stay byte-for-byte as written, with exactly one warning.migrate_and_adopt_do_not_name_absolute_worker_folders: absolute and non-home-relative worker entries produce no warning.migrate_and_adopt_refuse_an_include_shared_by_a_path_list_and_a_trust_root: one list-entry include used as askills.load.extraDirsentry and as a trust root is refused with the shared-include message, and the source file is untouched.nodeHost.workerRuns.folders: ["~/work"]stays as written.Unit tests in
src/store/openclaw_config.rspin the placeholder split and the$${VAR}escape, values resolving to the filesystem root (no panic,/), the rule for each setting, the setting names used in warnings, and that include traversal records the list index forp.jsonin the concatenated layout.Review corrections. Review of earlier revisions found that
skills.load.extraDirs: ["~/../../../../../../../.."]made migration panic (exit 101; it now becomes/), that~/.openclaw/${SUB}/../../sharedwas kept silently (it now produces the placeholder warning), that~/alias/${SUB}through a link to~/.openclawwas relocated so thatSUB=../externalnamed<env root>/externalinstead of<home>/external(it is now kept and named), that~/$${X}/skillswith a$${X}link into~/.openclawwas relocated although OpenClaw reads it as~/${X}/skills(it is now kept and named), and several layouts in which a spelled-out worker folder admitted a folder the plain install rejected. Each was reproduced throughocm migrateagainst OpenClaw's own worker-folder check ata7fe06b17a9; the worker-folder cases led to keeping those entries as written, as described above.Upgrade compatibility. OCM v0.3.8 (the latest release) migrated a home with a list-entry include, a placeholder path, a trust root, a worker folder, a session store and a workspace file. This build then ran
env list,env status,env exec(reading the workspace file and the session store back), andenv cloneon that environment; all succeeded, and every file in the environment was byte-identical before and after. A newocm migrateof the same home with this build succeeded and printed the worker-folder and trust-root warnings. The rewrite runs only whilemigrateoradopt importcreates an environment, so existing environments are never changed.Checks on Linux for this commit:
git diff --check,cargo fmt --check,cargo check --workspace --all-targets --locked,cargo test --locked --lib store::, and every integration test file that uses config includes:migrate_command_tests,dev_command_tests,env_import_tests,env_export_plugin_path_tests,env_clone_tests,env_snapshot_tests,env_status_tests,launcher_help_tests,shell_init_tests,start_command_tests,upgrade_checkpoint_scope_testsandupgrade_command_tests. On the first revisionruntime_command_testshad 89 passed andruntime_build_local_preserves_native_companion_dependency_bindingsfailed withCannot find module 'external-bridge'; it fails the same way on main 31fc297 on that machine and does not involve config rewriting. The full Linux, macOS and Windows suites run in hosted CI on this PR.Limits. OpenClaw's resolution for the rewritten settings is applied from its source rules (
resolveConfigEnvVars,materializeRuntimeConfig,expandHomePrefix), not by starting a Gateway. A placeholder whose value leads into~/.openclawfrom outside it is not detected, and names the source's state directory, as the value's absolute spelling already does. Worker folders and trust roots keep theirmainbehavior and are only reported, so #972 stays open for them.