diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7474110..601533f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -175,6 +175,17 @@ and run CI manually. - Type hints on public functions - pytest for all tests +## External integrations + +Code in this repo lands in molecule-core. Some related runtime artifacts +live in their own repos: + +- [`Molecule-AI/molecule-ai-workspace-runtime`](https://github.com/Molecule-AI/molecule-ai-workspace-runtime) — Python adapter SDK (`molecule_runtime`) that runs inside containerized Molecule workspaces. Bridges Claude Code SDK / hermes / langgraph / etc. → A2A queue. +- [`Molecule-AI/molecule-sdk-python`](https://github.com/Molecule-AI/molecule-sdk-python) — `A2AServer` + `RemoteAgentClient` for external agents that register over the public `/registry/register` flow. +- [`Molecule-AI/molecule-mcp-claude-channel`](https://github.com/Molecule-AI/molecule-mcp-claude-channel) — Claude Code channel plugin. Bridges A2A traffic into a running Claude Code session via MCP `notifications/claude/channel`. Polling-based (no tunnel required); install with `claude --channels plugin:molecule@Molecule-AI/molecule-mcp-claude-channel`. + +When extending the **A2A surface** in molecule-core (`workspace-server/internal/handlers/a2a_proxy.go` etc.), consider whether the change has a downstream impact on the runtime SDK or the channel plugin — they're versioned independently but share the wire shape. + ## Architecture Overview See `CLAUDE.md` for detailed architecture documentation, including: diff --git a/canvas/src/components/ExternalConnectModal.tsx b/canvas/src/components/ExternalConnectModal.tsx index 10fc4f85..a10fb54c 100644 --- a/canvas/src/components/ExternalConnectModal.tsx +++ b/canvas/src/components/ExternalConnectModal.tsx @@ -1,3 +1,5 @@ +'use client'; + // ExternalConnectModal — shown once after creating a runtime="external" // workspace. Surfaces the workspace_auth_token + ready-to-paste snippets // so the operator can hand them to whoever runs their off-host agent @@ -24,6 +26,12 @@ export interface ExternalConnectionInfo { heartbeat_endpoint: string; curl_register_template: string; python_snippet: string; + // Claude Code channel plugin snippet — for operators whose external + // agent IS a Claude Code session. Polling-based; no tunnel required. + // Optional in the type for backward compat with platforms that + // haven't shipped molecule-core PR #2304 yet (older response payload + // omits the field; tab is hidden if empty). + claude_code_channel_snippet?: string; } interface Props { @@ -31,10 +39,14 @@ interface Props { onClose: () => void; } -type Tab = "python" | "curl" | "fields"; +type Tab = "python" | "curl" | "claude" | "fields"; export function ExternalConnectModal({ info, onClose }: Props) { - const [tab, setTab] = useState("python"); + // Default to Claude Code when the platform offers it — that's the + // newest + simplest path (no tunnel needed). Falls back to Python + // for older platform builds that don't ship the snippet. + const initialTab: Tab = info?.claude_code_channel_snippet ? "claude" : "python"; + const [tab, setTab] = useState(initialTab); const [copiedKey, setCopiedKey] = useState(null); const copy = useCallback(async (value: string, key: string) => { @@ -70,6 +82,13 @@ export function ExternalConnectModal({ info, onClose }: Props) { 'WORKSPACE_AUTH_TOKEN=""', `WORKSPACE_AUTH_TOKEN="${info.auth_token}"`, ); + // The channel snippet asks the operator to paste the auth_token into + // the .env file's MOLECULE_WORKSPACE_TOKENS field. Stamp it server-side + // here so the copy-paste-block is truly ready-to-run. + const filledChannel = info.claude_code_channel_snippet?.replace( + 'MOLECULE_WORKSPACE_TOKENS=', + `MOLECULE_WORKSPACE_TOKENS=${info.auth_token}`, + ); return ( !o && onClose()}> @@ -91,7 +110,11 @@ export function ExternalConnectModal({ info, onClose }: Props) { aria-label="Connection snippet format" className="mt-4 flex gap-1 border-b border-zinc-800" > - {(["python", "curl", "fields"] as Tab[]).map((t) => ( + {( + filledChannel + ? (["claude", "python", "curl", "fields"] as Tab[]) + : (["python", "curl", "fields"] as Tab[]) + ).map((t) => ( ))} {/* Snippet area */}
+ {tab === "claude" && filledChannel && ( + copy(filledChannel, "claude")} + /> + )} {tab === "python" && ( copy(filledPython, "python")} diff --git a/workspace-server/internal/handlers/external_connection.go b/workspace-server/internal/handlers/external_connection.go index 7a73a37b..12a3abfb 100644 --- a/workspace-server/internal/handlers/external_connection.go +++ b/workspace-server/internal/handlers/external_connection.go @@ -67,6 +67,37 @@ curl -fsS -X POST "{{PLATFORM_URL}}/registry/register" \ }' ` +// externalChannelTemplate — Claude Code channel plugin install + .env. For +// operators whose external agent IS a Claude Code session (laptop or +// remote dev VM); routes the workspace's A2A traffic into the running +// Claude Code session as conversation turns via MCP. The plugin source +// lives at github.com/Molecule-AI/molecule-mcp-claude-channel — polling +// based, no tunnel required (uses /workspaces/:id/activity?since_secs=, +// platform-side support shipped in #2300). +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: +mkdir -p ~/.claude/channels/molecule +cat > ~/.claude/channels/molecule/.env <<'EOF' +MOLECULE_PLATFORM_URL={{PLATFORM_URL}} +MOLECULE_WORKSPACE_IDS={{WORKSPACE_ID}} +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 + +# 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. +# +# 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. +` + // externalPythonTemplate uses molecule-sdk-python's RemoteAgentClient + // A2AServer (PR #13 in that repo). Until the SDK cuts a v0.y release // to PyPI the snippet pins git+main. diff --git a/workspace-server/internal/handlers/workspace.go b/workspace-server/internal/handlers/workspace.go index 723b85b6..00d86b7e 100644 --- a/workspace-server/internal/handlers/workspace.go +++ b/workspace-server/internal/handlers/workspace.go @@ -370,6 +370,17 @@ func (h *WorkspaceHandler) Create(c *gin.Context) { "{{PLATFORM_URL}}", platformURL), "{{WORKSPACE_ID}}", id, ), + // Claude Code channel plugin snippet. For operators + // whose external agent IS a Claude Code session — + // the snippet sets up ~/.claude/channels/molecule/.env + // and points at the canonical first-party plugin at + // github.com/Molecule-AI/molecule-mcp-claude-channel. + // Polling-based; no tunnel needed. + "claude_code_channel_snippet": strings.ReplaceAll( + strings.ReplaceAll(externalChannelTemplate, + "{{PLATFORM_URL}}", platformURL), + "{{WORKSPACE_ID}}", id, + ), } } c.JSON(http.StatusCreated, resp)