chore: extract 3 small fixes from closed #1664 - #1735
Merged
Conversation
…bypass + workspace-server Dockerfile GID entrypoint Three small, non-overlapping fixes extracted from closed PR #1664: 1. canvas/src/components/ContextMenu.tsx — Replace the useMemo-over-nodes pattern with a hashed-boolean selector (s.nodes.some(...)) so Zustand's useSyncExternalStore snapshot comparison is stable. Resolves React error #185 (infinite render loop). Moves the child-node list derivation into the delete handler via getState() so the render path no longer allocates a fresh array. 2. workspace-server/internal/handlers/a2a_proxy.go — Allow the Docker-bridge hostname path (ws-<id>:8000) to skip the SSRF guard in local-docker mode. Gated on !saasMode() so SaaS deployments keep the full private-IP blocklist (a remote workspace registration can't claim a ws-* hostname and reach a sensitive VPC IP). 3. workspace-server/Dockerfile — Add entrypoint.sh that discovers the docker.sock GID at boot and adds the platform user to that group, then exec's su-exec to drop privileges. Lets the platform container reach the host docker socket without running as root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| if tool_trace and hasattr(msg, "metadata"): | ||
| try: | ||
| msg.metadata = {"tool_trace": tool_trace} | ||
| except (AttributeError, TypeError): |
HongmingWang-Rabbit
pushed a commit
that referenced
this pull request
Jun 12, 2026
…ace (#1737) CTO-bypass merge 2026-05-24: all 5 CI sub-jobs verified success; umbrella stale due to status-propagation race; compensating success status posted. Persona acks in place.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three small, non-overlapping fixes extracted from closed PR #1664. Bundled because each fix is tiny, they touch independent files, and they share the same provenance (all unblocks or stability tweaks the original PR carried alongside bigger changes that won't ship).
Fix 1 — canvas
ContextMenu.tsx: React error #185 (infinite snapshot loop)ContextMenupreviously derived the "does this node have children?" flag via auseMemoover a new array returned by(s) => s.nodes. TheuseMemowas stable across renders, but the selector itself returned a fresh reference every time an unrelated field in the canvas store changed, which Zustand'suseSyncExternalStoresaw as "snapshot changed" and scheduled a re-render -> another selector call -> another fresh array -> loop -> React error #185 on some prod sessions.The new selector hashes down to a boolean (
s.nodes.some(...)). Booleans compare stably withObject.is, so the snapshot is steady. The delete handler still needs the actual child nodes to prompt the user, so it reads them once viauseCanvasStore.getState().nodes.filter(...)at click time, off the render path.Fix 2 — workspace-server a2a_proxy SSRF bypass for local-docker
resolveAgentURLrewriteshttp://127.0.0.1:<port>tohttp://ws-<id>:8000when the platform itself runs in Docker (Docker-bridge hostname). The rewritten URL then resolves to172.18.0.x, which the SSRF guard blocks as a private IP — stranding A2A in local-docker dev.The fix adds
isInternalDockerCalland skips the SSRF guard for that path. Two narrow cases trigger the bypass:http://ws-underplatformInDocker.SaaS safety: the
ws-*branch is gated on!saasMode(). In SaaS the workspace registry is remote and an attacker-controlled registration could claim aws-*hostname that resolves to a sensitive VPC IP; leaving the SSRF guard on there is correct. Local-docker is a trusted single-host setup wherews-*comes from our own compose/bridge network and the relaxation is safe.Fix 3 — workspace-server Dockerfile: dynamic Docker socket GID + gosu/su-exec entrypoint
The platform binary needs the host docker socket to provision workspaces, but the socket's group varies between hosts (Colima ~= 101, Linux ~= 999, etc.). Hard-coding a GID breaks on the other platform.
The new
entrypoint.sh:stat.dockergroup inside the container and addsplatformto it.exec su-exec platform /platform "$@"— drops privileges before running the binary.Non-root runtime is preserved. Only the entrypoint runs as root long enough to adjust group membership.
Test plan
cd workspace-server && go build ./...cleancd workspace-server && go test ./internal/handlers/ -count=1 -run "TestProxyA2A|TestResolveAgentURL|TestIsSafeURL|TestIsPrivateOrMetadataIP"— 14 failures, all pre-existing onmain(DNS-lookup + sqlmock regex drift, unrelated to this PR; same count and same names before/after the change)npx tsc --noEmiton canvas — no new errors in ContextMenu.tsxdocker psvia mounted socket🤖 Generated with Claude Code