mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix: allow trusted exec approvals home symlinks (#72377)
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Exec approvals: accept a symlinked `OPENCLAW_HOME` as the trusted approvals root while still rejecting symlinked `.openclaw` path components below it. (#64663) Thanks @FunJim.
|
||||
- ACP: route server logs to stderr before Gateway config/bootstrap work so ACP stdout remains JSON-RPC only for IDE integrations. Fixes #49060. Thanks @Hollychou924.
|
||||
- Logging: propagate internal request trace scopes through Gateway HTTP requests and WebSocket frames so file logs, diagnostic events, agent run traces, model-call traces, OTEL spans, and trusted provider `traceparent` headers share a correlatable `traceId` without logging raw request or model content. Fixes #40353. Thanks @liangruochong44-ui.
|
||||
- Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.
|
||||
|
||||
@@ -187,17 +187,34 @@ describe("exec approvals store helpers", () => {
|
||||
expect(fs.readFileSync(targetPath, "utf8")).toBe('{"sentinel":true}\n');
|
||||
});
|
||||
|
||||
it("refuses to traverse a symlinked parent component in the approvals path", () => {
|
||||
it("accepts a symlinked OPENCLAW_HOME as the trusted approvals root", () => {
|
||||
const realHome = makeTempDir();
|
||||
const linkedHome = `${realHome}-link`;
|
||||
tempDirs.push(realHome);
|
||||
fs.symlinkSync(realHome, linkedHome);
|
||||
tempDirs.push(realHome, linkedHome);
|
||||
fs.symlinkSync(realHome, linkedHome, "dir");
|
||||
process.env.OPENCLAW_HOME = linkedHome;
|
||||
|
||||
saveExecApprovals({ version: 1, defaults: { security: "full" }, agents: {} });
|
||||
|
||||
expect(
|
||||
fs.readFileSync(path.join(realHome, ".openclaw", "exec-approvals.json"), "utf8"),
|
||||
).toContain('"security": "full"');
|
||||
});
|
||||
|
||||
it("refuses to traverse symlinked approvals components below a symlinked home", () => {
|
||||
const realHome = makeTempDir();
|
||||
const linkedHome = `${realHome}-link`;
|
||||
const linkedStateTarget = path.join(realHome, "state-target");
|
||||
tempDirs.push(realHome, linkedHome);
|
||||
fs.mkdirSync(linkedStateTarget, { recursive: true });
|
||||
fs.symlinkSync(realHome, linkedHome, "dir");
|
||||
fs.symlinkSync(linkedStateTarget, path.join(realHome, ".openclaw"), "dir");
|
||||
process.env.OPENCLAW_HOME = linkedHome;
|
||||
|
||||
expect(() =>
|
||||
saveExecApprovals({ version: 1, defaults: { security: "full" }, agents: {} }),
|
||||
).toThrow(/Refusing to traverse symlink in exec approvals path/);
|
||||
expect(fs.existsSync(path.join(realHome, ".openclaw"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(linkedStateTarget, "exec-approvals.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("adds trimmed allowlist entries once and persists generated ids", () => {
|
||||
|
||||
@@ -248,10 +248,8 @@ function assertNoSymlinkPathComponents(targetPath: string, trustedRoot: string):
|
||||
const relative = path.relative(resolvedRoot, resolvedTarget);
|
||||
const segments = relative && relative !== "." ? relative.split(path.sep) : [];
|
||||
let current = resolvedRoot;
|
||||
for (const segment of [".", ...segments]) {
|
||||
if (segment !== ".") {
|
||||
current = path.join(current, segment);
|
||||
}
|
||||
for (const segment of segments) {
|
||||
current = path.join(current, segment);
|
||||
try {
|
||||
const stat = fs.lstatSync(current);
|
||||
if (stat.isSymbolicLink()) {
|
||||
|
||||
Reference in New Issue
Block a user