Skip to content

Commit 578c2ea

Browse files
committed
fix(sandbox): escape C1 controls in visible session label + cover both paths
The prior change extended hasUnsafeControlChars (which gates the explain command to an agent-only fallback) through the C1 range. But the parallel visible Session-label path went through escapeControlCharsVisible, which only escaped C0/DEL, so a C1 byte (e.g. U+009B CSI) in a redacted session key still reached the single-line Session output raw. Extend the shared escapeControlCharsVisible contract through C1 (0x80-0x9f) so the visible label escapes those bytes too, and add an end-to-end regression in sandbox-explain.test.ts asserting a C1-bearing session key both escapes in the Session line and triggers the agent-only explain fallback. Signed-off-by: lsr911 <[email protected]>
1 parent ed5d522 commit 578c2ea

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

src/agents/sandbox-explain.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,39 @@ describe("sandbox explain helpers", () => {
167167
expect(msg).toContain('Tool "browser" blocked by sandbox tool policy');
168168
expect(toolPolicyAuditInfo).not.toHaveBeenCalled();
169169
});
170+
171+
it("escapes C1 controls in the Session line and falls back to agent-only explain", () => {
172+
// U+009B is the C1 CSI introducer, an alternative ANSI escape prefix (ESC [).
173+
const CSI = String.fromCharCode(0x9b);
174+
const cfg: OpenClawConfig = {
175+
agents: {
176+
defaults: {
177+
sandbox: { mode: "non-main", scope: "agent" },
178+
},
179+
},
180+
tools: {
181+
sandbox: {
182+
tools: {
183+
deny: ["browser"],
184+
},
185+
},
186+
},
187+
};
188+
189+
// Session key carries a C1 byte within its trailing (visible) 6 chars.
190+
const sessionKey = `agent:main:mobilechat:group:g1${CSI}`;
191+
const msg = formatSandboxToolPolicyBlockedMessage({
192+
cfg,
193+
sessionKey,
194+
toolName: "browser",
195+
});
196+
197+
// Visible Session label escapes the C1 byte instead of emitting it raw.
198+
expect(msg).toContain("\\x9b");
199+
expect(msg).not.toContain(CSI);
200+
// Unsafe (C1-bearing) keys use the agent-scoped explain command and never
201+
// embed the raw key in a --session argument.
202+
expect(msg).toContain("openclaw sandbox explain --agent");
203+
expect(msg).not.toContain("--session");
204+
});
170205
});

src/agents/tool-policy-audit.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ export function escapeControlCharsVisible(value: string): string {
131131
return "\\t";
132132
}
133133
const codePoint = char.codePointAt(0) ?? 0;
134-
if (codePoint < 0x20 || codePoint === 0x7f) {
134+
if (codePoint < 0x20 || codePoint === 0x7f || (codePoint >= 0x80 && codePoint <= 0x9f)) {
135+
// Escape C0 (0x00-0x1f), DEL (0x7f), and C1 (0x80-0x9f) controls. The C1
136+
// range includes the CSI introducer U+009B (an alt ANSI escape prefix),
137+
// which must not reach single-line audit/session-label output raw.
135138
return `\\x${codePoint.toString(16).padStart(2, "0")}`;
136139
}
137140
return char;

0 commit comments

Comments
 (0)