Release: credential resolver, WhatsApp native, exec hardening, traces UI (#754)

* fix(ci): skip CI condition in semantic-release for main branch

go-semantic-release auto-detects the default branch from GitHub API
(which is dev), but releases are cut from main. The CI condition
rejects runs on non-default branches. Use --no-ci to bypass this
check since the workflow already gates on push to main.

* docs: document CI/CD pipelines, release flow, and v2.66.0 changelog

- CLAUDE.md: add CI/CD & Releases section with workflow table, tag
  patterns, Docker variants, beta/desktop release commands
- CONTRIBUTING.md: expand Releases section with standard (auto),
  beta (manual tag), and desktop release workflows
- docs/17-changelog.md: add v2.66.0 entry covering IDOR fix, BytePlus
  provider, per-agent grants, beta pipeline, and CI fixes

* fix(telegram): handle group-to-supergroup migration seamlessly

When a Telegram group upgrades to a supergroup, the chat ID changes and
all existing references become stale. This caused send failures (400),
orphaned sessions, and required manual re-pairing.

Add dual-path migration handling:
- Proactive: intercept inbound MigrateToChatID before isServiceMessage
- Reactive: detect 400 + MigrateToChatID on send, migrate DB, retry

DB migration updates in a single transaction (scoped by tenant + channel):
- paired_devices: sender_id, chat_id
- sessions: session_key, user_id
- channel_contacts: sender_id
- channel_pending_messages: history_key

Also invalidates in-memory caches (approvedGroups, pairingReplySent,
groupHistory) and handles media sends via migration retry in Send().

* fix(tools): quote-aware shell operator detection in credentialed exec (#700) (#702)

* fix(tools): quote-aware shell operator detection in credentialed exec (#700)

- Replace detectShellOperators with detectUnquotedShellOperators in
  credentialed exec path — respects single/double quoting so that
  characters like | inside argument values (e.g. --jq '.[0] | .name')
  are not falsely flagged as shell operators
- Pass raw command string (preserving quotes) to executeCredentialed
  instead of reconstructing from parsed args
- Downgrade "no credential found" log from Warn to Debug (fires for
  every non-credentialed command, too noisy at Warn)
- Add extractUnquotedSegments() helper with comprehensive tests

* fix(tools): handle backslash escape outside quotes in shell operator detection

extractUnquotedSegments did not handle \ as an escape character outside
of quotes, causing \" to incorrectly enter double-quote mode. This hid
subsequent shell operators from detection (e.g. gh \"arg\" | env would
not detect the unquoted pipe).

Add backslash escape handling in the unquoted state to match
go-shellwords parsing behavior. Both \ and the escaped character are
emitted as unquoted content so operator detection still catches them.

---------

Co-authored-by: viettranx <viettranx@gmail.com>

* feat(infra): tracing recovery, browser cleanup, CLI fixes, UI workspace split (#709)

- Tracing: recover stale running traces/spans on startup (PG + SQLite)
- Browser: Chrome orphan cleanup via launcher PID, timeouts, Leakless
- Claude CLI: WaitDelay 5s + context-cancel early exit
- Agent loop: safety-net defer to finalize orphan root traces
- UI: split workspace sharing into separate Memory and KG toggles
- Minor: for-range idiom, min() builtin

* fix(prompt): skip credentialed CLI context when exec tool is denied

Agents with exec in their deny list cannot run CLI commands, so
injecting wrangler/gh credential context is misleading — the LLM
sees instructions for tools it cannot use. Gate the section on
exec being present in the filtered tool list.

* fix(ui): improve traces table layout and readability

Compact columns: status as icon-only, merge time+duration into one column.
Truncate long user IDs, clean <media:*> tags from preview, move badges to second line.

* fix(security): harden exec path exemption matching

- Add absolute path exemption for dataDir/skills-store/ (fixes skill
  scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
  if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
  would exempt the entire command including non-exempt paths

Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.

* feat(whatsapp): add native WhatsApp channel with whatsmeow (#720)

Replace Node.js Baileys bridge with native go.mau.fi/whatsmeow — zero
external dependencies. QR auth, media support, markdown formatting,
typing indicators, dual JID/LID identity, group policies, pairing.

Resolves #703

* fix(chat): load message history on first conversation click (#730)

* fix(chat): load message history when selecting existing conversation from clean state

The skipNextHistoryRef was unconditionally set when sessionKey transitioned
from empty to non-empty. This prevented loadHistory() from running when
clicking an existing conversation from the initial /chat page. The skip
was only intended for the new-chat send flow where the optimistic message
is already displayed.

Guard the skip with expectingRunRef so it only activates when a message
send is in flight.

Closes #729

* docs: add UI diff evidence for PR #730

Before/after screenshots and HTML comparison report showing
first conversation click behavior fix.

* fix(permissions): use cron-specific permission check for cron tool (#725)

* fix(security): harden exec path exemption matching (#721)

- Add absolute path exemption for dataDir/skills-store/ (fixes skill
  scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
  if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
  would exempt the entire command including non-exempt paths

Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.

* fix(permissions): use cron-specific permission check for cron tool

Cron tool was hardcoded to check `file_writer` configType via
CheckFileWriterPermission(), ignoring the `cron` configType that
the UI actually saves when granting cron permissions. This caused
agents in group chats to be denied cron access even with correct
permission configured.

Add ConfigTypeCron constant and CheckCronPermission() that checks
`cron` configType first, falling back to `file_writer`.

---------

Co-authored-by: Viet Tran <viettranx@gmail.com>

* 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.

* refactor(exec): extract path exemption logic to separate file

Move shell-aware parsing, dynamic workspace exemptions, path alias
variants, and canonicalization functions from shell.go (688 LOC) to
shell_path_exemption.go (284 LOC) for maintainability.

* feat(agent): centralized tenant user identity resolution for credentials

Add CredentialUserID context key that resolves channel contacts to merged
tenant users for credential lookups (SecureCLI, MCP). Keeps UserID
unchanged for session/workspace scoping. Resolves group senders, group
contacts, and unresolved DMs via ContactStore with 60s TTL cache.

* fix(ui): improve traces table column layout

Add width constraints and whitespace-nowrap to prevent column wrapping
on narrow viewports. Cherry-picked from dev-v3.

* fix(ui): enable search filtering on knowledge graph view (#758)

Search box on /knowledge-graph only filtered table view. Add useMemo client-side filtering of graph entities by name/type/description, only show relations where both endpoints match.

Closes #759

* feat: add zoom controls to knowledge graph (#757)

Add +/- buttons and percentage display to the knowledge graph stats bar
so users can zoom without relying solely on mouse wheel. Uses existing
react-force-graph-2D zoom() API with 1.5x step and 300ms animation.

Closes #755

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): use DB name for ClaudeCLI, ACP, and Anthropic registration

Provider Name() methods returned hardcoded strings, so DB-registered
providers with custom names got wrong registry key — causing "provider
not found" fallback. Add WithClaudeCLIName/WithACPName/WithAnthropicName
options, pass p.Name from DB registration paths. Config-based paths
keep hardcoded defaults.

Closes #742

* fix(agent): correct soft-trim head/tail budget allocation when tail is important (#723)

Co-authored-by: quxy5 <quxy5@outlook.com>

---------

Co-authored-by: Duy /zuey/ <duy@wearetopgroup.com>
Co-authored-by: Duc Nguyen <me@vanducng.dev>
Co-authored-by: Kai (Tam Nhu) Tran <61256810+kaitranntt@users.noreply.github.com>
Co-authored-by: Plateau Nguyen <nguyennlt.ncc@gmail.com>
Co-authored-by: Reski Rukmantiyo <reski.rukmantio@lintasarta.co.id>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: QuXiangyu <771744189@qq.com>
Co-authored-by: quxy5 <quxy5@outlook.com>
This commit is contained in:
85 files changed
+4076 -605

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR #730 — fix(chat): first conversation click</title>
<style>
:root { --bg: #0d0d0d; --fg: #e8e4df; --card: #1a1714; --border: #2d2923; --accent: #ef4444; --muted: #6b6560; }
.light { --bg: #f8f6f3; --fg: #1a1714; --card: #ffffff; --border: #e5e0da; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); padding: 2rem; }
.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(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.summary-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; }
.summary-card h3 { font-size: 0.75rem; text-transform: uppercase; color: var(--muted); margin-bottom: 0.5rem; }
.env { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin-bottom: 2rem; font-size: 0.85rem; color: var(--muted); }
.comparison { margin-bottom: 3rem; }
.comparison h2 { font-size: 1.1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
.col-header { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
.col-header.before { color: var(--muted); }
.col-header.after { color: var(--accent); }
.grid img { width: 100%; border: 1px solid var(--border); border-radius: 6px; }
.desc { font-size: 0.85rem; color: var(--muted); margin-top: 0.5rem; }
</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(chat): load message history on first conversation click</h1>
<p class="subtitle">PR #730 — Closes #729</p>
<div class="summary">
<div class="summary-card">
<h3>Bug</h3>
<p>First conversation click from clean state shows empty placeholder instead of messages</p>
</div>
<div class="summary-card">
<h3>Root Cause</h3>
<p><code>skipNextHistoryRef</code> unconditionally set on empty→non-empty transition, skipping <code>loadHistory()</code></p>
</div>
<div class="summary-card">
<h3>Fix</h3>
<p>Guard skip with <code>expectingRunRef.current</code> — only active during new-chat send flow</p>
</div>
</div>
<div class="env">
Same instance (claw:3000 backend), same user (kai/Master), same agent (glm thục bô), same conversation (Chat a95c8ffa), light theme. Callout borders mark the review area.
</div>
<div class="comparison">
<h2>Chat — First Click from Clean State</h2>
<div class="grid">
<div>
<div class="col-header before">BEFORE</div>
<img src="before-chat-first-click.png" alt="Before: empty placeholder on first click">
<p class="desc">Before: Clicking "Chat a95c8ffa" from initial state — conversation highlighted but main area shows "Start a conversation" placeholder. Messages never load.</p>
</div>
<div>
<div class="col-header after">AFTER</div>
<img src="after-chat-first-click.png" alt="After: messages load on first click">
<p class="desc">After: Same click — conversation messages load immediately. The 5-message history is displayed correctly on first click.</p>
</div>
</div>
</div>
</body>
</html>
+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