fix(exec): allow uploaded files in active workspaces (#748)

Shell-aware command parsing, dynamic workspace exemptions, and symlink canonicalization for exec path denial. Fixes #739.
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-04-08 13:35:19 +07:00
1 parent e9155e0c5d
commit 8730a90acb
9 files changed
+990 -49

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
=== RUN TestSetupToolRegistryExecWorkspacePaths
2026/04/07 21:24:13 INFO memory + knowledge graph tools registered (PG-backed)
2026/04/07 21:24:13 INFO web_search tool enabled
2026/04/07 21:24:13 INFO web_fetch tool enabled policy="" blocked=0
2026/04/07 21:24:13 INFO tts enabled provider=edge auto=off
2026/04/07 21:24:13 INFO tool rate limiting enabled per_hour=150
2026/04/07 21:24:13 INFO exec approval enabled security=full ask=off
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
2026/04/07 21:24:13 WARN secure_cli.lookup: store is nil, skipping credentialed exec command="printf '%s' file=@\"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/001/personal/.uploads/Quarterly Report.png\""
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
2026/04/07 21:24:13 WARN secure_cli.lookup: store is nil, skipping credentialed exec command="printf '%s' --input=\"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/001/teams/team-123/Quarterly Report.png\""
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
2026/04/07 21:24:13 WARN secure_cli.lookup: store is nil, skipping credentialed exec command="cp \"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/001/.goclaw/goclaw-workspace/ws/system/uploads/Quarterly Report.png\" \"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/002/partner.png\""
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied
--- PASS: TestSetupToolRegistryExecWorkspacePaths (0.02s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed (0.01s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed (0.01s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied (0.00s)
PASS
ok github.com/nextlevelbuilder/goclaw/cmd 0.041s
+94
View File
@@ -0,0 +1,94 @@
go: downloading github.com/golang-migrate/migrate/v4 v4.19.1
go: downloading github.com/jackc/pgx/v5 v5.6.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
go: downloading github.com/charmbracelet/huh v0.8.0
go: downloading github.com/spf13/cobra v1.10.2
go: downloading github.com/bwmarrin/discordgo v0.29.0
go: downloading golang.org/x/sync v0.19.0
go: downloading github.com/slack-go/slack v0.19.0
go: downloading github.com/mattn/go-runewidth v0.0.16
go: downloading github.com/mymmrac/telego v1.6.0
go: downloading github.com/fsnotify/fsnotify v1.9.0
go: downloading github.com/titanous/json5 v1.0.0
go: downloading github.com/disintegration/imaging v1.6.2
go: downloading golang.org/x/image v0.27.0
go: downloading golang.org/x/time v0.14.0
go: downloading github.com/mark3labs/mcp-go v0.44.0
go: downloading github.com/adhocore/gronx v1.19.6
go: downloading github.com/go-rod/rod v0.116.2
go: downloading github.com/lib/pq v1.10.9
go: downloading github.com/mattn/go-shellwords v1.0.12
go: downloading golang.org/x/net v0.49.0
go: downloading golang.org/x/text v0.33.0
go: downloading github.com/rivo/uniseg v0.4.7
go: downloading github.com/catppuccin/go v0.3.0
go: downloading github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
go: downloading github.com/charmbracelet/bubbletea v1.3.6
go: downloading github.com/charmbracelet/lipgloss v1.1.0
go: downloading github.com/charmbracelet/x/cellbuf v0.0.13
go: downloading github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0
go: downloading github.com/mitchellh/hashstructure/v2 v2.0.2
go: downloading github.com/spf13/pflag v1.0.9
go: downloading golang.org/x/crypto v0.47.0
go: downloading golang.org/x/sys v0.42.0
go: downloading github.com/valyala/fasthttp v1.69.0
go: downloading github.com/invopop/jsonschema v0.13.0
go: downloading github.com/spf13/cast v1.7.1
go: downloading github.com/yosida95/uritemplate/v3 v3.0.2
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/charmbracelet/x/ansi v0.9.3
go: downloading github.com/charmbracelet/x/term v0.2.1
go: downloading github.com/atotto/clipboard v0.1.4
go: downloading github.com/grbit/go-json v0.11.0
go: downloading github.com/valyala/fastjson v1.6.7
go: downloading github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
go: downloading github.com/muesli/cancelreader v0.2.2
go: downloading github.com/muesli/termenv v0.16.0
go: downloading github.com/wk8/go-ordered-map/v2 v2.1.8
go: downloading github.com/ysmood/goob v0.4.0
go: downloading github.com/ysmood/got v0.40.0
go: downloading github.com/ysmood/gson v0.7.3
go: downloading github.com/ysmood/fetchup v0.2.3
go: downloading github.com/ysmood/leakless v0.9.0
go: downloading github.com/andybalholm/brotli v1.2.0
go: downloading github.com/klauspost/compress v1.18.2
go: downloading github.com/valyala/bytebufferpool v1.0.0
go: downloading github.com/jackc/pgpassfile v1.0.0
go: downloading github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761
go: downloading github.com/jackc/puddle/v2 v2.2.2
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading github.com/bahlo/generic-list-go v0.2.0
go: downloading github.com/buger/jsonparser v1.1.1
go: downloading github.com/mailru/easyjson v0.7.7
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/aymanbagabas/go-osc52/v2 v2.0.1
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc
go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
=== RUN TestSetupToolRegistryExecWorkspacePaths
2026/04/08 01:24:40 INFO memory + knowledge graph tools registered (PG-backed)
2026/04/08 01:24:40 INFO web_search tool enabled
2026/04/08 01:24:40 INFO web_fetch tool enabled policy="" blocked=0
2026/04/08 01:24:40 INFO tts enabled provider=edge auto=off
2026/04/08 01:24:40 INFO tool rate limiting enabled per_hour=150
2026/04/08 01:24:40 INFO exec approval enabled security=full ask=off
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths1657441769/001
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths1657441769/001
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths1657441769/001
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied
--- FAIL: TestSetupToolRegistryExecWorkspacePaths (0.00s)
--- FAIL: TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed (0.00s)
--- FAIL: TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed (0.00s)
--- FAIL: TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied (0.00s)
FAIL
FAIL github.com/nextlevelbuilder/goclaw/cmd 0.019s
FAIL
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

+228
View File
@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR #748 — fix(exec): allow uploaded files in active workspaces</title>
<style>
:root {
--bg: #0d0d0d;
--fg: #e8e4df;
--card: #1a1714;
--border: #2d2923;
--accent: #ef4444;
--good: #22c55e;
--muted: #8a837d;
--code: #151210;
}
.light {
--bg: #f8f6f3;
--fg: #1a1714;
--card: #ffffff;
--border: #e5e0da;
--accent: #dc2626;
--good: #15803d;
--muted: #6b6560;
--code: #f3f0eb;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--fg);
padding: 2rem;
line-height: 1.5;
}
.theme-toggle {
position: fixed;
top: 1rem;
right: 1rem;
background: var(--card);
border: 1px solid var(--border);
color: var(--fg);
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
z-index: 100;
}
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.subtitle { color: var(--muted); margin-bottom: 2rem; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.summary-card, .env, .comparison, .supporting {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
}
.summary-card { padding: 1rem; }
.summary-card h3 {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.5rem;
letter-spacing: 0.04em;
}
.env, .supporting, .comparison { padding: 1.25rem; margin-bottom: 1.5rem; }
.env { color: var(--muted); font-size: 0.92rem; }
.comparison h2, .supporting h2 {
font-size: 1.05rem;
margin-bottom: 1rem;
}
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 980px) { .grid { grid-template-columns: 1fr; } }
.col-header {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.before { color: var(--muted); }
.after { color: var(--accent); }
.panel {
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
background: var(--code);
}
.panel img {
display: block;
width: 100%;
height: auto;
}
.terminal {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.8rem;
line-height: 1.45;
white-space: pre-wrap;
overflow-wrap: anywhere;
padding: 1rem;
min-height: 320px;
}
.hl-bad {
display: inline-block;
background: rgba(239, 68, 68, 0.14);
border-left: 3px solid var(--accent);
padding: 0.05rem 0.35rem;
}
.hl-good {
display: inline-block;
background: rgba(34, 197, 94, 0.14);
border-left: 3px solid var(--good);
padding: 0.05rem 0.35rem;
}
.desc {
color: var(--muted);
font-size: 0.88rem;
margin-top: 0.65rem;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
background: var(--code);
padding: 0.12rem 0.32rem;
border-radius: 4px;
}
ul { margin-left: 1.1rem; color: var(--muted); }
</style>
</head>
<body>
<button class="theme-toggle" onclick="document.documentElement.classList.toggle('light'); this.textContent = document.documentElement.classList.contains('light') ? '● Dark' : '○ Light'">○ Light</button>
<h1>fix(exec): allow uploaded files in active workspaces</h1>
<p class="subtitle">PR #748 — visual proof pack for issue #739</p>
<div class="summary">
<div class="summary-card">
<h3>Bug</h3>
<p>Active workspace files under deny-roots were rejected by <code>exec</code> even when they belonged to the current upload/team scope.</p>
</div>
<div class="summary-card">
<h3>Root Cause</h3>
<p>Path exemptions were static and non-shell-aware, so quoted, prefixed, and legacy screenshot-era upload paths still matched the deny root.</p>
</div>
<div class="summary-card">
<h3>Fix</h3>
<p>Shell-aware parsing + canonical path checks now exempt the active personal <code>.uploads</code> subtree, the legacy <code>uploads</code> subtree, and the active team workspace.</p>
</div>
</div>
<div class="env">
Before source: stable <code>claw:3000</code> backed by the unfixed <code>/opt/goclaw</code> deployment (<code>v2.7.1</code>). After source: temporary PR deployment on <code>claw:3001</code>, backed by <code>goclaw-pr748:local</code> built from the fixed code commit <code>83fa1342</code>. Both UI captures use fresh chats with the same user and <code>GLM thục bô</code>. Callout border marks the reviewed <code>exec</code> row.
</div>
<div class="comparison">
<h2>UI Before / After</h2>
<div class="grid">
<div>
<div class="col-header before">Before</div>
<div class="panel">
<img src="pr748-before-absolute-goclaw-blocked.png" alt="Before: stable claw blocks absolute .goclaw upload path">
</div>
<p class="desc">Stable <code>claw</code> release, fresh chat with <code>GLM thục bô</code>: the agent runs the exact absolute <code>/app/.goclaw/.../.uploads/...jpg</code> copy command and the <code>exec</code> row fails with the safety-policy <code>.goclaw/</code> denial.</p>
</div>
<div>
<div class="col-header after">After</div>
<div class="panel">
<img src="pr748-after-absolute-goclaw-allowed.png" alt="After: fixed temporary claw deployment allows absolute .goclaw upload path">
</div>
<p class="desc">Temporary fixed deployment on <code>claw:3001</code>, fresh chat with the same agent: the same absolute <code>/app/.goclaw/.../.uploads/...jpg</code> copy command now completes successfully and returns <code>/tmp/partner-abs.png</code>.</p>
</div>
</div>
</div>
<div class="comparison">
<h2>Gateway Reproduction</h2>
<div class="grid">
<div>
<div class="col-header before">Before</div>
<div class="panel">
<pre class="terminal">$ ssh claw 'docker run --rm -v /tmp/goclaw-739-before:/src -w /src golang:1.26 sh -lc "/usr/local/go/bin/go test ./cmd -run TestSetupToolRegistryExecWorkspacePaths -count=1 -v"'
=== RUN TestSetupToolRegistryExecWorkspacePaths
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
<span class="hl-bad">gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001</span>
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
<span class="hl-bad">gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001</span>
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
<span class="hl-bad">gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001</span>
FAIL</pre>
</div>
<p class="desc">Baseline source on <code>claw</code>: the exact gateway wiring still rejects all three representative workspace-path forms.</p>
</div>
<div>
<div class="col-header after">After</div>
<div class="panel">
<pre class="terminal">$ go test ./cmd -run TestSetupToolRegistryExecWorkspacePaths -count=1 -v
=== RUN TestSetupToolRegistryExecWorkspacePaths
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
<span class="hl-good">--- PASS: TestSetupToolRegistryExecWorkspacePaths (0.01s)</span>
<span class="hl-good"> --- PASS: personal_workspace_prefixed_uploads_allowed</span>
<span class="hl-good"> --- PASS: team_workspace_quoted_input_allowed</span>
<span class="hl-good"> --- PASS: legacy_dotgoclaw_uploads_layout_allowed</span>
PASS</pre>
</div>
<p class="desc">Current worktree: the same gateway path handling now passes for personal uploads, team workspace paths, and the screenshot-era legacy <code>.goclaw/.../uploads</code> layout.</p>
</div>
</div>
</div>
<div class="supporting">
<h2>Supporting Live Recreation on <code>claw</code></h2>
<div class="panel">
<img src="claw-live-glm-thuc-bo-before.png" alt="Live claw recreation with GLM thục bô">
</div>
<p class="desc">
This earlier live run documents the same production agent and workspace context on stable <code>claw</code>. The report’s primary Before/After pair above is the clean isolated proof you asked for; this screenshot is kept as supporting context for the reproduction journey.
</p>
</div>
<div class="comparison">
<h2>What This Proves</h2>
<ul>
<li>Quoted/prefixed upload arguments now survive the deny matcher when they point into the active personal <code>.uploads</code> subtree, the legacy <code>.goclaw/.../uploads</code> subtree, or the active team workspace.</li>
<li>Symlink escapes, unrelated <code>dataDir</code> files, and workspace-local <code>.goclaw/*</code> remain denied.</li>
<li>The visual behavior difference is now shown on two real frontends: stable <code>claw:3000</code> Before and temporary fixed <code>claw:3001</code> After, both using fresh conversations.</li>
</ul>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB