Files
openclaw/src/config/patch-replace-paths.ts
Yuval Dinodia 83705fba04 fix(config): stop config.patch replacePaths index suffix from widening array consent (#91966)
* fix(config): stop config.patch replacePaths index suffix from widening array consent

normalizeConfigPatchReplacePath stripped a trailing array bracket, so an entry/index-scoped token like bindings[0] or bindings[] collapsed onto the bare whole-array token (bindings). That bare token is both the merge replaceArrayPaths key and the destructive-array gate's exact-path token, so an index-scoped consent silently authorized a full-array replacement and dropped unrelated base entries on the gateway config.patch path, and the same collapse let the agent self-edit tool truncate id-keyed arrays whenever no protected path happened to be involved.

Keep the interior index normalization (agents.list[0].skills -> agents.list[].skills) but no longer collapse a trailing bracket, so a bracket/index-suffixed token never matches the bare whole-array token and the destructive-array gate stays fail-closed unless the documented exact path is passed. Update the agent-tool test whose expectation depended on the old collapse: agents.list[0] now does a non-destructive id-keyed merge that only changes model and is correctly allowed.

* fix(config): distinguish indexed and array replace consent

* test(config): cover replace consent syntax

* fix(config): make replace path normalization idempotent

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-11 20:35:52 +09:00

23 lines
690 B
TypeScript

// Normalizes config.patch replacePaths shared by Gateway and agent preflight checks.
export function normalizeConfigPatchReplacePath(value: string): string {
const trimmed = value.trim();
if (trimmed.endsWith("[]")) {
return trimmed.slice(0, -2).replace(/\[\d+\](?=\.)/g, "[]");
}
return trimmed.replace(/\[\d+\](?=\.)/g, "[]");
}
export function normalizeConfigPatchReplacePaths(
values: readonly unknown[] | undefined,
): Set<string> {
if (!values) {
return new Set();
}
return new Set(
values
.filter((value): value is string => typeof value === "string")
.map(normalizeConfigPatchReplacePath)
.filter((value) => value.length > 0),
);
}