From d5eb58af56a1109da929c65dd1bc4a1a7496652d Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 17:46:55 -0700 Subject: [PATCH 1/3] =?UTF-8?q?feat(external-connect):=20comprehensive=20s?= =?UTF-8?q?etup=20=E2=80=94=20fix=20Claude=20Code=20channel=20snippet=20+?= =?UTF-8?q?=20add=20per-tab=20Help=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: handing the modal's Claude Code channel snippet to an agent fails immediately with two errors that the snippet doesn't tell the operator how to resolve: plugin:molecule@Molecule-AI/molecule-mcp-claude-channel · plugin not installed plugin:molecule@Molecule-AI/molecule-mcp-claude-channel · not on the approved channels allowlist Root cause: the snippet's `claude --channels plugin:...` line assumes the plugin is pre-installed AND that the channel is on Anthropic's default allowlist. Both assumptions are wrong for a custom Molecule plugin in a public repo. Two changes: 1. Rewrite externalChannelTemplate (Go) with full setup chain: - Bun prereq check (channel plugins are Bun scripts) - `/plugin marketplace add Molecule-AI/molecule-mcp-claude-channel` + `/plugin install molecule@molecule-mcp-claude-channel` BEFORE the launch — otherwise "plugin not installed" - `--dangerously-load-development-channels` flag on launch — required for non-Anthropic-allowlisted channels, otherwise "not on approved channels allowlist" - Common-errors block at the bottom mapping each error string to which numbered step recovers it - Team/Enterprise managed-settings caveat (the dev-channels flag is blocked there; admin must use channelsEnabled + allowedChannelPlugins) Plugin install info verified by reading `Molecule-AI/molecule-mcp-claude-channel` plugin.json (`name: "molecule"`) and the Claude Code channels + plugin-discovery docs at code.claude.com/docs/en/{channels,discover-plugins}. 2. Add per-tab HelpBlock to the modal (canvas): - Collapsible
below each snippet, closed by default so the snippet stays the visual focus - "Where to install" link (PyPI for runtime, claude.com for Claude Code, github.com/openai/codex for Codex, NousResearch/hermes-agent for Hermes) - "Documentation" link (docs.molecule.ai/docs/guides/*; hostname confirmed by existing blog post canonical metadata; paths map 1:1 to docs/guides/*.md files in this repo) - "Common errors" list with concrete recovery steps for each tab (e.g. Codex tab calls out the codex≥0.57 requirement and TOML duplicate-table parse error; OpenClaw calls out the :18789 port conflict check) URL discipline: every URL is either (a) verified against a file path in this repo's docs/, (b) the canonical repo of an existing snippet reference, or (c) a well-known third-party canonical URL. No guessed URLs — broken links would defeat the purpose of "more comprehensive instructions." Verification: - `go build ./...` clean in workspace-server - `go test ./internal/handlers/...` passes (4.3s) - Bash syntax check on test_staging_full_saas.sh (no edits there) clean - TS brace/paren/bracket counts balanced; no full tsc run because the worktree's node_modules isn't installed — counterpart Canvas tabs E2E on the PR will exercise the full type-check + render path Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/ExternalConnectModal.tsx | 218 +++++++++++++++++- .../internal/handlers/external_connection.go | 38 ++- 2 files changed, 251 insertions(+), 5 deletions(-) diff --git a/canvas/src/components/ExternalConnectModal.tsx b/canvas/src/components/ExternalConnectModal.tsx index af51c447..a9b1f360 100644 --- a/canvas/src/components/ExternalConnectModal.tsx +++ b/canvas/src/components/ExternalConnectModal.tsx @@ -18,6 +18,157 @@ import { useCallback, useState } from "react"; import * as Dialog from "@radix-ui/react-dialog"; +type Tab = "python" | "curl" | "claude" | "mcp" | "hermes" | "codex" | "openclaw" | "fields"; + +// Per-tab help metadata: docs link, where-to-install link, common errors. +// All URLs verified against repo content (docs/guides/* file paths map to +// docs.molecule.ai/docs/guides/*; canonical hostname confirmed by existing +// blog post canonical metadata) or against the snippet text the operator +// just copied. Never linking to a URL that wasn't already in product — +// dead links here defeat the purpose of "more comprehensive instructions." +const TAB_HELP: Record< + Tab, + { + docsUrl?: string; + docsLabel?: string; + downloadUrl?: string; + downloadLabel?: string; + commonIssues?: { symptom: string; check: string }[]; + } +> = { + mcp: { + docsUrl: "https://docs.molecule.ai/docs/guides/mcp-server-setup", + docsLabel: "MCP server setup guide", + downloadUrl: "https://pypi.org/project/molecule-ai-workspace-runtime/", + downloadLabel: "molecule-ai-workspace-runtime on PyPI", + commonIssues: [ + { + symptom: "Tools not appearing in your agent", + check: + "Run `claude mcp list` (or your runtime's equivalent) — the molecule entry should be listed. If missing, re-run the `claude mcp add` line.", + }, + { + symptom: "ConnectionRefused / DNS error on first call", + check: + "PLATFORM_URL must include the scheme (https://) and have no trailing slash. Verify with `curl $PLATFORM_URL/healthz`.", + }, + ], + }, + python: { + docsUrl: + "https://docs.molecule.ai/docs/guides/external-agent-registration", + docsLabel: "External agent registration guide", + downloadUrl: "https://pypi.org/project/molecule-ai-workspace-runtime/", + downloadLabel: "molecule-ai-workspace-runtime on PyPI", + commonIssues: [ + { + symptom: "401 from /heartbeat", + check: + "AUTH_TOKEN expired or wrong workspace_id. Tokens are shown only once at create time — re-create the workspace to get a fresh token.", + }, + { + symptom: "AGENT_URL not reachable from platform", + check: + "Public HTTPS URL required for inbound A2A. Use ngrok or Cloudflare Tunnel if your agent is behind NAT.", + }, + ], + }, + claude: { + docsUrl: + "https://docs.molecule.ai/docs/guides/external-agent-registration", + docsLabel: "External agent registration guide", + downloadUrl: "https://claude.com/claude-code", + downloadLabel: "Claude Code (claude.com)", + commonIssues: [ + { + symptom: "plugin not installed", + check: + "Run `/plugin marketplace add Molecule-AI/molecule-mcp-claude-channel` then `/plugin install molecule@molecule-mcp-claude-channel` inside Claude Code, then `/reload-plugins`.", + }, + { + symptom: "not on the approved channels allowlist", + check: + "Custom channels need `--dangerously-load-development-channels` on the launch command. Team/Enterprise orgs need admin to set `channelsEnabled` + `allowedChannelPlugins` in claude.ai admin settings.", + }, + { + symptom: "Inbound messages not arriving", + check: + "Check stderr for `molecule channel: connected — watching N workspace(s)`. Verify ~/.claude/channels/molecule/.env has the right PLATFORM_URL + token.", + }, + ], + }, + hermes: { + docsUrl: + "https://docs.molecule.ai/docs/guides/external-agent-registration", + docsLabel: "External agent registration guide", + downloadUrl: "https://github.com/NousResearch/hermes-agent", + downloadLabel: "hermes-agent (NousResearch)", + commonIssues: [ + { + symptom: "Gateway start failure", + check: + "Tail ~/.hermes/gateway.log. YAML duplicate-key in config.yaml is the most common cause — `gateway:` block must appear exactly once.", + }, + { + symptom: "Plugin not discovered after install", + check: + "Run `pip show hermes-channel-molecule` to confirm install. Some hermes builds need `hermes plugin reload` before the new platform_plugins entry takes effect.", + }, + ], + }, + codex: { + docsUrl: "https://docs.molecule.ai/docs/guides/mcp-server-setup", + docsLabel: "MCP server setup guide", + downloadUrl: "https://github.com/openai/codex", + downloadLabel: "openai/codex", + commonIssues: [ + { + symptom: "[mcp_servers.molecule] not loaded", + check: + "Codex must be ≥ 0.57. Check with `codex --version`; upgrade via `npm install -g @openai/codex@latest`.", + }, + { + symptom: "TOML parse error after re-running setup", + check: + "TOML rejects duplicate `[mcp_servers.molecule]` tables. Open ~/.codex/config.toml and remove the old block before pasting the new one.", + }, + ], + }, + openclaw: { + docsUrl: "https://docs.molecule.ai/docs/guides/mcp-server-setup", + docsLabel: "MCP server setup guide", + commonIssues: [ + { + symptom: "Gateway not starting", + check: + "Tail ~/.openclaw/gateway.log. The loopback bind requires :18789 to be free — check with `lsof -iTCP:18789`.", + }, + { + symptom: "openclaw mcp set rejected", + check: + "The heredoc generates JSON; verify it parsed by running `jq < ~/.openclaw/mcp/molecule.json`. Re-run `openclaw mcp set` if the file is malformed.", + }, + ], + }, + curl: { + docsUrl: + "https://docs.molecule.ai/docs/guides/external-agent-registration", + docsLabel: "External agent registration guide", + commonIssues: [ + { + symptom: "401 / 403 on register", + check: + "WORKSPACE_AUTH_TOKEN must be the value shown at workspace create. Tokens are shown only once.", + }, + ], + }, + fields: { + docsUrl: + "https://docs.molecule.ai/docs/guides/external-agent-registration", + docsLabel: "External agent registration guide", + }, +}; + export interface ExternalConnectionInfo { workspace_id: string; platform_url: string; @@ -63,8 +214,6 @@ interface Props { onClose: () => void; } -type Tab = "python" | "curl" | "claude" | "mcp" | "hermes" | "codex" | "openclaw" | "fields"; - export function ExternalConnectModal({ info, onClose }: Props) { // Default to Universal MCP when the platform offers it — runtime- // agnostic outbound tool path that works for any MCP-aware runtime @@ -303,6 +452,7 @@ export function ExternalConnectModal({ info, onClose }: Props) { copy(info.heartbeat_endpoint, "hb")} copied={copiedKey === "hb"} /> )} +
@@ -351,6 +501,70 @@ function SnippetBlock({ ); } +// HelpBlock — collapsible "Need help?" section under each tab's snippet. +// Renders only the keys present in the per-tab help metadata (no empty +// sections). Closed by default so the snippet stays the visual focus; +// operators with a working setup never see this. Uses native
+// for keyboard accessibility (Tab + Enter) without extra ARIA wiring. +function HelpBlock({ + help, +}: { + help: (typeof TAB_HELP)[Tab] | undefined; +}) { + if (!help) return null; + const { docsUrl, docsLabel, downloadUrl, downloadLabel, commonIssues } = help; + if (!docsUrl && !downloadUrl && !commonIssues?.length) return null; + + return ( +
+ + Need help? — install link, docs, common errors + +
+ {downloadUrl && ( +
+ Where to install: + + {downloadLabel || downloadUrl} + +
+ )} + {docsUrl && ( +
+ Documentation: + + {docsLabel || docsUrl} + +
+ )} + {commonIssues && commonIssues.length > 0 && ( +
+
Common errors:
+
    + {commonIssues.map((issue, i) => ( +
  • + {issue.symptom} + — {issue.check} +
  • + ))} +
+
+ )} +
+
+ ); +} + function Field({ label, value, diff --git a/workspace-server/internal/handlers/external_connection.go b/workspace-server/internal/handlers/external_connection.go index f3ff3553..847ebee6 100644 --- a/workspace-server/internal/handlers/external_connection.go +++ b/workspace-server/internal/handlers/external_connection.go @@ -83,7 +83,20 @@ curl -fsS -X POST "{{PLATFORM_URL}}/registry/register" \ const externalChannelTemplate = `# Claude Code channel — bridges this workspace's A2A traffic into your # Claude Code session. No tunnel/public URL needed (polling-based). # -# 1. Save this token + workspace_id, then create ~/.claude/channels/molecule/.env: +# Prereq: Bun installed (channel plugins are Bun scripts). +# bun --version # must print a version number +# +# 1. Inside Claude Code, install the channel plugin from its GitHub repo. +# The plugin is NOT on Anthropic's default allowlist, so a one-time +# marketplace-add is needed before install: +# +# /plugin marketplace add Molecule-AI/molecule-mcp-claude-channel +# /plugin install molecule@molecule-mcp-claude-channel +# +# Then either run /reload-plugins or restart Claude Code so the +# plugin is registered. +# +# 2. Create the per-watched-workspace config file: mkdir -p ~/.claude/channels/molecule cat > ~/.claude/channels/molecule/.env <<'EOF' MOLECULE_PLATFORM_URL={{PLATFORM_URL}} @@ -92,13 +105,32 @@ MOLECULE_WORKSPACE_TOKENS= EOF chmod 600 ~/.claude/channels/molecule/.env -# 2. Launch Claude Code with the channel enabled: -claude --channels plugin:molecule@Molecule-AI/molecule-mcp-claude-channel +# 3. Launch Claude Code with the channel enabled. Custom (non-Anthropic- +# allowlisted) channels need the --dangerously-load-development-channels +# flag to opt in — without it, you'll see "not on the approved channels +# allowlist" on startup. +claude --dangerously-load-development-channels \ + --channels plugin:molecule@molecule-mcp-claude-channel +# You should see on stderr: +# molecule channel: connected — watching 1 workspace(s) at {{PLATFORM_URL}} +# # Inbound A2A messages now surface as conversation turns. Claude's # replies route back via the reply_to_workspace MCP tool — no extra # wiring on your side. # +# Common errors: +# "plugin not installed" → Step 1 didn't run; run /plugin install +# inside Claude Code, then /reload-plugins. +# "not on approved channels allowlist" → Add --dangerously-load-development-channels +# to the launch command (Step 3). +# "config-missing" → ~/.claude/channels/molecule/.env not +# readable; re-run Step 2 and check chmod. +# +# Team/Enterprise orgs: the --dangerously-load-development-channels flag is +# blocked by managed settings. Your admin must set channelsEnabled=true and +# add the plugin to allowedChannelPlugins in claude.ai admin settings. +# # Multi-workspace: comma-separate IDs and tokens (same order). See # https://github.com/Molecule-AI/molecule-mcp-claude-channel for # pairing flow, push-mode upgrade, and v0.2 roadmap. From 71fb499dee2fcfd32320063925895dd5b91563da Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 17:53:29 -0700 Subject: [PATCH 2/3] canvas/DeleteCascadeConfirmDialog: fix Cancel no-op hover + Delete light hover + focus rings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four fixes for the cascade-delete confirmation modal: 1. Cancel button hover was a no-op: bg-surface-card on top of the same base — clicking did something but the button looked dead. Lifted to surface-elevated, matching the ConfirmDialog Cancel pattern. 2. Delete button hovered LIGHTER (bg-red-500 over bg-red-600). On white text that drops contrast below AA — same trap fixed in ConfirmDialog and ApprovalBanner. Flipped to bg-red-700 so hover stays readable in both themes. 3. Checkbox ring-offset color was zinc-900 — but the dialog actually sits on bg-surface-sunken, so the offset showed the wrong color through the ring gap. Corrected to ring-offset-surface-sunken. Also moved focus → focus-visible so the ring only shows on keyboard nav, not mouse clicks. 4. Cancel + Delete had no focus-visible rings. Added accent ring on Cancel, danger ring on Delete, both with the correct ring-offset-surface-sunken. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/DeleteCascadeConfirmDialog.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/canvas/src/components/DeleteCascadeConfirmDialog.tsx b/canvas/src/components/DeleteCascadeConfirmDialog.tsx index 949437c3..ba68bb83 100644 --- a/canvas/src/components/DeleteCascadeConfirmDialog.tsx +++ b/canvas/src/components/DeleteCascadeConfirmDialog.tsx @@ -127,13 +127,16 @@ export function DeleteCascadeConfirmDialog({

- {/* Checkbox guard */} + {/* Checkbox guard. Ring-offset color was zinc-900 — the dialog + actually sits on bg-surface-sunken, so the offset showed + the wrong color through the ring gap. Switched to the + real bg + a danger-tinted ring. */}