Merge pull request #1735 from Molecule-AI/chore/extract-1664-small-fixes
chore: extract 3 small fixes from closed #1664
This commit is contained in:
commit
0d820bd869
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
|
||||
import { api } from "@/lib/api";
|
||||
import { showToast } from "./Toaster";
|
||||
@ -23,17 +23,9 @@ export function ContextMenu() {
|
||||
const setPanelTab = useCanvasStore((s) => s.setPanelTab);
|
||||
const nestNode = useCanvasStore((s) => s.nestNode);
|
||||
const contextNodeId = contextMenu?.nodeId ?? null;
|
||||
// Select the full nodes array (stable reference across unrelated store
|
||||
// updates) and derive children via useMemo. Filtering inside the
|
||||
// selector returned a new array every call, which Zustand's
|
||||
// useSyncExternalStore saw as "snapshot changed" → schedule
|
||||
// re-render → loop → React error #185. See canvas-store-snapshots.
|
||||
const nodes = useCanvasStore((s) => s.nodes);
|
||||
const children = useMemo(
|
||||
() => (contextNodeId ? nodes.filter((n) => n.data.parentId === contextNodeId) : []),
|
||||
[nodes, contextNodeId],
|
||||
const hasChildren = useCanvasStore((s) =>
|
||||
contextNodeId ? s.nodes.some((n) => n.data.parentId === contextNodeId) : false
|
||||
);
|
||||
const hasChildren = children.length > 0;
|
||||
const setPendingDelete = useCanvasStore((s) => s.setPendingDelete);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
@ -174,7 +166,8 @@ export function ContextMenu() {
|
||||
// it survives ContextMenu unmount. Closing the menu here avoids the
|
||||
// prior race where the portal dialog's Confirm click was treated as
|
||||
// "outside" by the menu's outside-click handler.
|
||||
setPendingDelete({ id: contextMenu.nodeId, name: contextMenu.nodeData.name, hasChildren, children: children.map(c => ({ id: c.id, name: c.data.name })) });
|
||||
const childNodes = useCanvasStore.getState().nodes.filter((n) => n.data.parentId === contextMenu.nodeId);
|
||||
setPendingDelete({ id: contextMenu.nodeId, name: contextMenu.nodeData.name, hasChildren, children: childNodes.map(c => ({ id: c.id, name: c.data.name })) });
|
||||
closeContextMenu();
|
||||
}, [contextMenu, setPendingDelete, closeContextMenu]);
|
||||
|
||||
|
||||
@ -32,9 +32,21 @@ COPY workspace-server/migrations /migrations
|
||||
COPY --from=templates /workspace-configs-templates /workspace-configs-templates
|
||||
COPY --from=templates /org-templates /org-templates
|
||||
COPY --from=templates /plugins /plugins
|
||||
# Non-root runtime — platform binary doesn't need root; dropping privileges
|
||||
# prevents container escape attacks from reaching host UID 0.
|
||||
# Non-root runtime with Docker socket access for workspace provisioning.
|
||||
RUN addgroup -g 1000 platform && adduser -u 1000 -G platform -s /bin/sh -D platform
|
||||
EXPOSE 8080
|
||||
USER platform
|
||||
CMD ["/platform"]
|
||||
COPY <<'ENTRY' /entrypoint.sh
|
||||
#!/bin/sh
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock 2>/dev/null)
|
||||
if [ -n "$SOCK_GID" ] && [ "$SOCK_GID" != "0" ]; then
|
||||
addgroup -g "$SOCK_GID" docker 2>/dev/null || true
|
||||
addgroup platform docker 2>/dev/null || true
|
||||
else
|
||||
addgroup platform root 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
exec su-exec platform /platform "$@"
|
||||
ENTRY
|
||||
RUN chmod +x /entrypoint.sh && apk add --no-cache su-exec
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@ -386,15 +386,29 @@ func (h *WorkspaceHandler) resolveAgentURL(ctx context.Context, workspaceID stri
|
||||
// When the platform runs inside Docker, 127.0.0.1:{host_port} is
|
||||
// unreachable (it's the platform container's own localhost, not the
|
||||
// Docker host). Rewrite to the container's Docker-bridge hostname.
|
||||
isInternalDockerCall := false
|
||||
if strings.HasPrefix(agentURL, "http://127.0.0.1:") && h.provisioner != nil && platformInDocker {
|
||||
agentURL = provisioner.InternalURL(workspaceID)
|
||||
isInternalDockerCall = true
|
||||
}
|
||||
// Also detect URLs already pointing to Docker-bridge hostnames (ws-<id>:8000).
|
||||
// Only trust the ws-* prefix in local-docker mode — in SaaS the workspace
|
||||
// registry is remote and an attacker-controlled registration could claim a
|
||||
// ws-* hostname that resolves to a sensitive internal VPC IP.
|
||||
if platformInDocker && !saasMode() && strings.HasPrefix(agentURL, "http://ws-") {
|
||||
isInternalDockerCall = true
|
||||
}
|
||||
// SSRF defence: reject private/metadata URLs before making outbound call.
|
||||
if err := isSafeURL(agentURL); err != nil {
|
||||
log.Printf("ProxyA2A: unsafe URL for workspace %s: %v", workspaceID, err)
|
||||
return "", &proxyA2AError{
|
||||
Status: http.StatusBadGateway,
|
||||
Response: gin.H{"error": "workspace URL is not publicly routable"},
|
||||
// Skip for Docker-internal workspace URLs — these always resolve to private
|
||||
// IPs (172.18.0.x) on the bridge network, which is expected and safe when
|
||||
// the platform itself runs in the same Docker network.
|
||||
if !isInternalDockerCall {
|
||||
if err := isSafeURL(agentURL); err != nil {
|
||||
log.Printf("ProxyA2A: unsafe URL for workspace %s: %v", workspaceID, err)
|
||||
return "", &proxyA2AError{
|
||||
Status: http.StatusBadGateway,
|
||||
Response: gin.H{"error": "workspace URL is not publicly routable"},
|
||||
}
|
||||
}
|
||||
}
|
||||
return agentURL, nil
|
||||
|
||||
Loading…
Reference in New Issue
Block a user