openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
174 lines (173 loc) • 7.65 kB
JavaScript
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { l as readConfigFileSnapshotForWrite } from "./io.runtime-B9iJRs3w.js";
import { t as mutateConfigFile } from "./mutate-ZNN4iFCn.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import { t as createClackPrompter } from "./clack-prompter-BSpWH_ak.js";
import "./setup-runtime-BWfBnPr_.js";
import "./config-mutation-CUEqNwmX.js";
import "./policy-Cc4oN2HF.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region extensions/file-transfer/src/approvals-migration.ts
function listLegacyApprovalItems(pluginConfig) {
const config = asNullableRecord(pluginConfig);
if (!config || config.policyVersion === 2) return [];
const nodes = asNullableRecord(config.nodes);
if (!nodes) return [];
const items = [];
const seen = /* @__PURE__ */ new Set();
for (const [selector, rawNode] of Object.entries(nodes)) {
const node = asNullableRecord(rawNode);
if (!node) continue;
for (const [kind, field] of [["read", "allowReadPaths"], ["write", "allowWritePaths"]]) {
const paths = Array.isArray(node[field]) ? node[field] : [];
for (const value of paths) if (typeof value === "string" && value.length > 0) {
const key = `${selector}\0${kind}\0${value}`;
if (seen.has(key)) continue;
seen.add(key);
items.push({
selector,
kind,
path: value
});
}
}
}
return items;
}
function applyApprovalMigration(pluginConfig, decisions) {
const original = asNullableRecord(pluginConfig) ?? {};
const next = structuredClone(original);
const nodes = asNullableRecord(next.nodes) ?? {};
next.nodes = nodes;
const pendingReapprovals = [];
for (const decision of decisions) {
if (decision.action === "keep-glob") continue;
const node = asNullableRecord(nodes[decision.item.selector]);
if (!node) continue;
const field = decision.item.kind === "read" ? "allowReadPaths" : "allowWritePaths";
node[field] = (Array.isArray(node[field]) ? node[field] : []).filter((value) => value !== decision.item.path);
if (decision.action === "exact") pendingReapprovals.push(decision.item);
}
next.pendingReapprovals = pendingReapprovals;
next.policyVersion = 2;
return next;
}
//#endregion
//#region extensions/file-transfer/src/cli.ts
function readPluginConfig(config) {
const root = asNullableRecord(config);
const plugins = asNullableRecord(root?.plugins);
const entries = asNullableRecord(plugins?.entries);
const entry = asNullableRecord(entries?.["file-transfer"]);
return asNullableRecord(entry?.config);
}
function resolveMigrationBackupPath(prepared) {
const ownership = prepared.snapshot.includeProvenance?.findLast((entry) => entry.path.length <= 1 && entry.path[0] === "plugins");
const configPath = ownership?.path.length === 1 && ownership.kind === "single" && !ownership.hasSiblingOverrides && ownership.targetPath ? ownership.targetPath : prepared.snapshot.path;
return `${path.normalize(configPath)}.bak`;
}
async function runApprovalMigration(options) {
const prepared = await readConfigFileSnapshotForWrite();
if (!prepared.snapshot.valid) throw new Error("OpenClaw config is invalid; fix it before migrating file-transfer approvals");
const sourceRoot = asNullableRecord(prepared.snapshot.sourceConfig);
if (asNullableRecord(sourceRoot?.gateway)?.mode === "remote") throw new Error("This migration must run on the Gateway host because it updates that host's file-transfer policy.");
const pluginConfig = readPluginConfig(prepared.snapshot.sourceConfig);
const items = listLegacyApprovalItems(pluginConfig);
if (items.length === 0) {
const result = {
status: "ok",
changed: false,
message: "No legacy permissions need review."
};
process.stdout.write(options.json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
return;
}
if (options.json || !process.stdin.isTTY) {
const result = {
status: "needs-input",
changed: false,
items,
command: "openclaw file-transfer approvals migrate"
};
if (options.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
process.exitCode = 2;
return;
}
throw new Error("File-transfer permissions need interactive review. Run `openclaw file-transfer approvals migrate` in a terminal.");
}
const prompt = createClackPrompter();
await prompt.intro("Review file-transfer permissions");
await prompt.note("Older positive permissions remain inactive until this review finishes. Deny rules and transfer limits remain active.");
const decisions = [];
for (const item of items) {
const action = await prompt.select({
message: `${item.selector} · ${item.kind} · ${item.path}`,
options: [
{
value: "exact",
label: "Require exact reapproval",
hint: "Next use prompts once, then binds the actual node and command"
},
{
value: "keep-glob",
label: "Keep as an intentional wildcard",
hint: "Retains the current glob behavior"
},
{
value: "remove",
label: "Remove this permission"
}
]
});
decisions.push({
item,
action
});
}
const keepCount = decisions.filter((decision) => decision.action === "keep-glob").length;
const exactCount = decisions.filter((decision) => decision.action === "exact").length;
const removeCount = decisions.filter((decision) => decision.action === "remove").length;
await prompt.note(`Exact paths requiring one reapproval: ${exactCount}\nIntentional wildcards: ${keepCount}\nRemoved: ${removeCount}`, "Migration plan");
await prompt.note("Older OpenClaw versions cannot read the migrated format. To downgrade, restore the adjacent config backup shown after migration before starting the older version.", "Downgrade");
if (options.dryRun) {
await prompt.outro("Dry run complete. No config was changed.");
return;
}
if (!await prompt.confirm({
message: "Apply this migration?",
initialValue: true
})) {
await prompt.outro("Cancelled. No config was changed.");
return;
}
const migrated = applyApprovalMigration(pluginConfig, decisions);
const backupPath = resolveMigrationBackupPath(prepared);
const backupBefore = await fs.stat(backupPath).catch(() => null);
await mutateConfigFile({
base: "source",
baseHash: prepared.snapshot.hash,
writeOptions: prepared.writeOptions,
afterWrite: {
mode: "none",
reason: "file-transfer approval policy migration"
},
mutate: (draft) => {
const plugins = draft.plugins ??= {};
const entries = plugins.entries ??= {};
const entry = entries["file-transfer"] ??= {};
entry.config = migrated;
}
});
const backupAfter = await fs.stat(backupPath).catch(() => null);
const backupVerified = Boolean(backupAfter && (!backupBefore || backupAfter.ino !== backupBefore.ino || backupAfter.mtimeMs !== backupBefore.mtimeMs || backupAfter.size !== backupBefore.size));
await prompt.outro(backupVerified ? `File-transfer permissions updated. Exact paths will prompt once on next use. Config backup: ${backupPath}` : "File-transfer permissions updated. Exact paths will prompt once on next use. The standard config backup could not be verified.");
}
function registerFileTransferCli(program) {
program.command("file-transfer").description("Review file-transfer standing approvals").command("approvals").description("Manage standing approvals").command("migrate").description("Review and migrate older file-transfer permissions").option("--dry-run", "Review choices without changing config", false).option("--json", "Report unresolved legacy permissions as JSON", false).action(async (options) => {
await runApprovalMigration(options);
});
}
//#endregion
export { registerFileTransferCli };