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>
Closes: #177 (CRITICAL — Dockerfile runs as root)
Dockerfiles changed:
- workspace-server/Dockerfile (platform-only): addgroup/adduser + USER platform
- workspace-server/Dockerfile.tenant (combined Go+Canvas): addgroup/adduser + USER canvas
+ chown canvas:canvas on canvas dir so non-root node process can read it
- canvas/Dockerfile (canvas standalone): addgroup/adduser + USER canvas
- workspace-server/entrypoint-tenant.sh: update header comment (no longer starts
as root; both processes now start non-root)
The entrypoint no longer needs a root→non-root handoff since both the Go
platform and Canvas node run as non-root by default. The 'canvas' user owns
/app and /platform, so volume mounts owned by the host's canvas user work
without needing a root init step.
Co-authored-by: Molecule AI CP-BE <cp-be@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The github-app-auth plugin's go.mod had a relative replace directive
(../molecule-monorepo/platform) that didn't resolve in Docker where
the plugin is at /plugin/ and the platform at /app/. This caused the
plugin's provisionhook.TokenProvider interface to come from a different
package path than the platform's, so the type assertion in
FirstTokenProvider() failed — "no token provider registered".
Fix: sed the plugin's go.mod replace to point at /app during Docker build.
Also added debug logging to GetInstallationToken for future diagnosis.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>