Compare commits

...

24 Commits

Author SHA1 Message Date
Hongming Wang
c93388441d docs(arch): rewrite staging DNS section (per-tenant CNAME, not wildcard)
The "Architecture" diagram and section 5 ("Cloudflare: staging
subdomain") both implied that `*.staging.moleculesai.app` is a wildcard
DNS record. It is not. The control plane writes a per-tenant CNAME at
provision time (see `internal/provisioner/ec2.go` ->
`Tunnel.CreateTunnelDNS`), and unknown slugs correctly return NXDOMAIN.

This rewrite:
- Replaces `*.staging.moleculesai.app` / `*.moleculesai.app` in the
  ASCII diagram with `<slug>.<env-domain>` plus a "no wildcard" note.
- Renames section 5 to "Cloudflare: per-tenant CNAMEs (no wildcard)"
  and explains that NXDOMAIN on unknown slugs is correct, that
  `getaddrinfo ENOTFOUND` in tests means the slug is unprovisioned
  (not an infra bug), and that the same model applies to both
  staging and production.
2026-05-01 19:17:57 -07:00
Hongming Wang
651fe00998
Merge pull request #113 from Molecule-AI/docs/runtime-mcp-claude-21-install
Claude Code 2.1+ install: modern -e flags + ~/.claude.json shape
2026-05-01 18:30:06 -07:00
Hongming Wang
1e8f9dc295 docs(runtime-mcp): Claude Code 2.1+ install — modern -e flags + ~/.claude.json shape
Older `claude mcp add ... -- env VAR=val molecule-mcp` snippets land as
`command: "env"` with positional args, which is fragile across 2.1.x
patch builds and unfamiliar to anyone reading `~/.claude.json` directly.
The post-2.1 CLI also rejects the form without a `--` between flags and
command (external feedback in #112).

Replace both snippets (basic install + identity-with-skills) with:
1. Modern CLI form using `-e KEY=VAL` and an explicit `--`.
2. Parallel `~/.claude.json` JSON shape under top-level `mcpServers` for
   user scope (or `.mcp.json` in project root for project scope), so
   users on any 2.1.x patch level have an authoritative reference if
   the CLI form misbehaves.

Add a Troubleshooting entry for the two common 2.1+ CLI rejections, and
fix the broken `[Install](#install)` cross-link in the dev-channels
section to point at `[Step 2](#claude-code)`.

Closes Molecule-AI/docs#112.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:22:40 -07:00
Hongming Wang
1408cbae94
Merge pull request #111 from Molecule-AI/docs/channel-envelope-attributes
docs(runtime-mcp): document <channel> envelope attributes table
2026-05-01 17:52:24 -07:00
Hongming Wang
9c28fcf22c docs(runtime-mcp): document channel envelope attributes table
Adds a per-attribute reference table for the <channel> tag (push
path) and the equivalent JSON shape (poll path) so the agent — and
operators reading the docs — know what metadata to expect for every
inbound message.

Covers the new peer_name, peer_role, agent_card_url fields landing
with the wheel-side enrichment in molecule-core#2471, including the
registry-lookup graceful-degrade rule (lookup failure → attrs
absent, push still delivers) and the deterministic agent_card_url
construction (present even on registry outage).

Includes a worked example of a peer_agent push so a reader can copy
the wire shape into their own host bridge if they need to validate
what they're seeing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:49:02 -07:00
Hongming Wang
a45a1c65c3
Merge pull request #110 from Molecule-AI/docs/dev-channels-tagged-server-form
docs(runtime-mcp): tagged server:molecule form for --dangerously-load-development-channels
2026-05-01 17:12:35 -07:00
Hongming Wang
49f95877f8 docs(runtime-mcp): tagged server:NAME form for --dangerously-load-development-channels
Claude Code 2.1.x changed the flag's signature to require an allowlist
of tagged entries — `server:<name>` for manually-configured MCP
servers, `plugin:<name>@<marketplace>` for plugin channels. The
previous bare-switch form was rejected with `argument missing`, and
an untagged value returns `entries must be tagged`.

Doc updates:
- Replace bare-flag references with tagged form throughout (env-var
  table, push-path narrative, capability matrix)
- Add a worked example showing `claude --dangerously-load-development-
  channels server:molecule` and the `Listening for channel messages
  from: server:molecule` confirmation header so operators have a
  visible signal that push is live
- Note the multi-server form (space-separated tagged entries)
- Three new troubleshooting entries:
  * `argument missing` → forgot the value
  * `entries must be tagged` → forgot the tag
  * `Control request timeout: initialize` → embedded the flag in an
    SDK options bundle as `{flag: None}` instead of `{flag:
    "server:molecule"}` — covers the workspace-side regression we
    caught live on dd40faf8 on 2026-05-01

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:10:39 -07:00
Hongming Wang
18866f6d7e
Merge pull request #98 from Molecule-AI/docs/claude-code-channel-plugin
docs: Claude Code Channel Plugin guide + cross-references
2026-05-01 16:31:47 -07:00
Hongming Wang
c6494de5ad docs: wrap <ws-id> placeholders in backticks to fix MDX build
Bare <ws-id> in headings emitted a `raw` HAST node that the renderer
couldn't handle ("Cannot handle unknown node `raw`"). Wrapping in
backticks renders as inline code instead of being parsed as JSX/HTML.

Verified locally with `npm run build` (106/106 static pages green).
2026-05-01 16:31:05 -07:00
Hongming Wang
c5421c61f5 docs: fix MDX build — escape <1 + drop Callout JSX
Two errors caught by the docs build CI on PR #98:

1. external-agents.mdx line 15 had `<1 min.` — the literal `<` made the
   MDX parser try to read it as a JSX tag opener. Replaced with prose
   "under a minute" — equivalent meaning, no escape gymnastics.
2. claude-code-channel-plugin.md used `<Callout type="info">` JSX, but
   the rest of /content/docs/guides/ is plain .md (no JSX). The .md
   loader can't resolve the Callout component → "Cannot handle unknown
   node `raw`". Replaced with a `> **Note:**` blockquote — same visual
   hierarchy, plain markdown.

Verified locally that the offending characters are gone; build CI on
push will re-run and should now pass.
2026-05-01 16:31:05 -07:00
Hongming Wang
2ccd4b99d0 docs: add Claude Code Channel Plugin guide + cross-references
Adds /docs/guides/claude-code-channel-plugin.md — the canonical guide
for connecting a Claude Code session as a Molecule external workspace
via the new Molecule-AI/molecule-mcp-claude-channel plugin. Polling-
based; no tunnel required.

Cross-references added:
- content/docs/external-agents.mdx — new "Pick the right path" table
  at top, distinguishing Claude Code (channel plugin) from generic
  HTTP-speaking agents (this page) at first glance.
- content/docs/guides/external-workspace-quickstart.md — short
  redirect callout near the top so laptop-Claude-Code users are
  routed to the channel plugin guide instead of the tunnel-required
  quickstart.

Per molecule-core's #2060 content-routing policy, public-facing docs
live in this repo. The plugin source + README live separately at
github.com/Molecule-AI/molecule-mcp-claude-channel; this guide
duplicates the operator-facing setup steps and adds Molecule-specific
context (how to get workspace_id + token from canvas, how /activity
shows up, troubleshooting against /activity endpoint shape).
2026-05-01 16:30:49 -07:00
Hongming Wang
5f0871707e
Merge pull request #99 from Molecule-AI/feat/marketplace-creator-docs
docs(marketplace): tier overview + creator listing guide
2026-05-01 16:24:48 -07:00
Hongming Wang
b80891b312
Merge branch 'main' into feat/marketplace-creator-docs 2026-05-01 16:24:11 -07:00
Hongming Wang
149c315dfa
Merge pull request #109 from Molecule-AI/feat/universal-push-poll-contract
docs(mcp): rewrite inbound-delivery section for dual push+poll contract
2026-05-01 15:38:17 -07:00
Hongming Wang
b26d7ee9b2 docs(mcp): rewrite inbound-delivery section for dual push+poll contract
Mirrors molecule-core feat/universal-push-via-instructions: documents
the universal poll path (instructions field → every MCP client) plus
the optional push path (notifications/claude/channel for Claude Code
with the dev-channels flag or a future allowlist entry).

Honesty pass on the prior text: previous version claimed push works
"automatically" and "there is no config flag to toggle." That was
true for the wire shape but false for live UX — standard `claude`
launches without --dangerously-load-development-channels silently
drop the notification, and non-Claude clients ignore the Claude-
namespaced method entirely. New text spells out exactly which clients
get push, which get poll, and the per-client capability matrix.

Adds MOLECULE_MCP_POLL_TIMEOUT_SECS to the env-var table — the new
operator knob landed in the wheel.

Successor to #44/#49 (which closed without the flag caveat).
2026-05-01 15:33:07 -07:00
Hongming Wang
1dd9cfaaf3
Merge pull request #108 from Molecule-AI/docs/smoke-gate-caveat-correction
docs(workspace-runtime): correct smoke-gate caveat factual errors
2026-05-01 00:01:09 -07:00
Hongming Wang
28600d7956 docs(workspace-runtime): correct smoke-gate caveat factual errors
Two errors in the merged caveat (#107):

1. Claimed the stub RequestContext "carries an empty user message"
   — actually carries "smoke test" text (smoke_mode.py:76 calls
   `new_text_message("smoke test")`, with the explicit comment
   that it's "enough that extract_message_text(context) returns
   non-empty input"). Adapter authors gating smoke-mode behavior
   on extract_message_text(ctx) == "" would have a logic that
   never fires.

2. Described only the timeout-pass path. The harness also returns
   0 on ANY non-import exception (smoke_mode.py:135-143) — the
   bare `except Exception` block treats RuntimeError, auth errors,
   validation errors etc. as "downstream of the import gate" and
   exits clean. Spelling out all three pass cases (clean return,
   timeout, non-import exception) is the honest description.

Caught while re-reading smoke_mode.py to verify claims for a
review pass — found I had asserted both behaviors from memory
without checking, exactly the failure mode my e2e-test memory
just got a worked-example update about.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:00:02 -07:00
Hongming Wang
36ab08129c
Merge pull request #107 from Molecule-AI/docs/smoke-gate-timeout-pass-caveat
docs(workspace-runtime): clarify what the boot-smoke gate does NOT prove
2026-04-30 23:56:05 -07:00
Hongming Wang
ec78c7637b docs(workspace-runtime): clarify what the boot-smoke gate does NOT prove
A green gate means imports are healthy enough that
executor.execute() reaches its body. It does NOT prove that
execute() produces the right output: adapters with real I/O
inside execute() (subprocess to a gateway, httpx call upstream)
time out under the 5s harness window, and the gate treats a clean
timeout as success.

Surfaced while running publish-image across all 8 templates: the
openclaw smoke "passed" with timing-out behavior in execute()
because OpenClawA2AExecutor proxies to a subprocess that doesn't
exist in the smoke env. Reading the green check, future operators
might over-trust it as a runtime-correctness signal — it isn't.

Add a "What the gate does NOT prove" subsection so readers don't
mistake the import-regression coverage for an integration test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:54:57 -07:00
Hongming Wang
aa710d9310
Merge pull request #105 from Molecule-AI/design/align-docs-to-landing
feat(docs): align doc.moleculesai.app chrome with landing's warm-paper design
2026-04-30 22:53:36 -07:00
Hongming Wang
050cd70060
Merge pull request #106 from Molecule-AI/docs/smoke-mode-adapter-contract
Document MOLECULE_SMOKE_MODE adapter contract
2026-04-30 22:51:42 -07:00
Hongming Wang
1ccd92e0c8 docs(workspace-runtime): document MOLECULE_SMOKE_MODE adapter contract
The publish-image boot-smoke gate (molecule-core#2275) invokes the
runtime with MOLECULE_SMOKE_MODE=1 + stub creds to catch lazy
imports inside executor.execute(). Adapters whose setup() does
real I/O (subprocess spawn, network calls, uid-sensitive writes)
need to opt out of that I/O when MOLECULE_SMOKE_MODE=1, otherwise
the gate fails before reaching the runtime's smoke short-circuit.

This documents:
- the contract (one-line opt-out for Python adapter.setup() and
  shell entrypoints that wrap molecule-runtime)
- which boot stages the gate exercises
- the stub env the harness sets so adapters can reason about what
  they can rely on under smoke mode

Surfaced when running publish-image across all 8 workspace
templates: openclaw and hermes hit the contract gap because both
spawn real gateway subprocesses in setup; six others passed
without any contract awareness because their setup is light.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 22:48:53 -07:00
Hongming Wang
403725b970 feat(docs): align doc.moleculesai.app chrome with landing's warm-paper design
Brings the docs site in visual parity with moleculesai.app so docs,
marketing, and the canvas read as one product. Five focused changes
inside the existing fumadocs shell — no MDX or content touched, no
runtime/build dep changes:

- global.css: override fumadocs @theme tokens with the warm-paper
  palette (#fafaf7 bg, #15181c ink, #3b5bdb governance blue,
  #efece4 muted, #e6e2d8 border). Dark mode keeps fumadocs' neutral
  defaults so dark-pref readers still get a readable docs site.

- layout.tsx: swap Inter → Geist (sans) + JetBrains Mono (code),
  matching the landing's font stack. Wired through @theme so
  Tailwind's font-sans / font-mono utilities pick them up.

- layout.config.tsx: brand the topbar — inline Molecule logo SVG +
  "Molecule AI · DOCS" lockup, plus three external links to the rest
  of the surface (Platform → app, Marketplace → market, Landing →
  www) and the org GitHub. Mirrors the landing's collapsed nav.

- (home)/page.tsx: replace the stock fumadocs landing with a
  hero-style page matching the landing — statusbar strip, "Phase 35
  Marketplace public beta" eyebrow, the same shimmering h1 copy,
  three quick-start lane cards (Build a workspace / Run an
  organisation / Publish to the Marketplace) pointing into the docs
  tree.

Build is clean (106 static pages still generate). Existing /docs/*
pages inherit the new tokens via fumadocs' DocsLayout, so the entire
site shifts to the warm-paper aesthetic without touching MDX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 22:42:42 -07:00
Hongming Wang
03a222d9b5 docs(marketplace): add tier overview + creator listing guide
Two additive pages tied to the new Marketplace surfaces on the
landing page and molecule-app workspace UI:

- content/docs/marketplace.mdx — L1 plugins / L2 agents / L3 bundles
  tier model, trust tiers (Verified / Partner / Community), install
  flow, and workspace.yaml pin examples.
- content/docs/marketplace/creators.mdx — three-step (Build · List ·
  Earn) builder workflow: SDK refs, Creator Portal submission, pricing
  options, policy/safety terms, and maintenance.

Wired into meta.json under a new ---Marketplace--- section between
Troubleshooting and Security.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:35:21 -07:00
13 changed files with 1092 additions and 69 deletions

View File

@ -1,29 +1,100 @@
import Link from 'next/link';
// Three quick-start lanes — keeps the home page from being a wall of text
// and lets builders, operators, and integrators each find their entry
// point in one click.
const lanes = [
{
kicker: '01',
title: 'Build a workspace',
body: 'Pick a runtime template (Claude Code, LangGraph, CrewAI, Hermes, …), wire your tools, and ship.',
href: '/docs/workspace',
cta: 'Workspace guide →',
},
{
kicker: '02',
title: 'Run an organisation',
body: 'Topology, A2A, three-tier memory, governance — the platform layer that ties multi-agent teams together.',
href: '/docs/platform',
cta: 'Platform reference →',
},
{
kicker: '03',
title: 'Publish to the Marketplace',
body: 'Plugins, agents, and org bundles ship as signed manifests. Authors keep 80%, paid via Stripe Connect.',
href: '/docs/marketplace',
cta: 'Author guide →',
},
];
export default function HomePage() {
return (
<main className="flex flex-1 flex-col items-center justify-center px-6 py-24 text-center">
<h1 className="mb-4 text-5xl font-bold tracking-tight sm:text-6xl">
Molecule AI
</h1>
<p className="mb-8 max-w-2xl text-lg text-fd-muted-foreground">
Build and run multi-agent organisations. Templates, plugins, channels,
and the runtime that ties them together documented end to end.
</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<Link
href="/docs"
className="rounded-md bg-fd-primary px-5 py-2.5 text-sm font-medium text-fd-primary-foreground transition-colors hover:opacity-90"
>
Read the docs
</Link>
<Link
href="https://github.com/Molecule-AI/molecule-monorepo"
className="rounded-md border border-fd-border px-5 py-2.5 text-sm font-medium transition-colors hover:bg-fd-muted"
>
View on GitHub
</Link>
<main className="flex flex-1 flex-col">
{/* Statusbar — mirrors the landing's "All systems · status.* · phase" strip */}
<div className="border-b border-fd-border bg-fd-muted px-6 py-1.5 text-[11px] font-mono text-fd-muted-foreground flex flex-wrap justify-between gap-4">
<span>
<span className="inline-block size-1.5 rounded-full bg-[#2f7a4d] align-middle mr-1.5" />
All systems · status.moleculesai.app
</span>
<span>Phase 33 shipped · Phase 35 Marketplace public beta</span>
</div>
{/* Hero */}
<section className="px-6 py-20 sm:py-28 max-w-6xl mx-auto w-full">
<div className="text-[11px] font-mono uppercase tracking-[0.08em] text-fd-muted-foreground mb-4 flex items-center gap-2">
<span className="inline-block size-1.5 rounded-full bg-[#c0532b]" />
Documentation
</div>
<h1 className="text-5xl sm:text-6xl font-semibold tracking-tight leading-[1.05] mb-5 max-w-3xl">
The operating system for{' '}
<span className="text-[#3b5bdb]">AI agent organizations.</span>
</h1>
<p className="text-lg text-fd-muted-foreground max-w-2xl leading-relaxed mb-8">
Build and run multi-agent organisations the way you'd staff a company.
Templates, plugins, channels, runtimes, governance documented end
to end.
</p>
<div className="flex flex-wrap items-center gap-3">
<Link
href="/docs"
className="rounded-md bg-fd-primary px-5 py-2.5 text-sm font-medium text-fd-primary-foreground transition hover:opacity-90"
>
Read the docs
</Link>
<Link
href="https://github.com/Molecule-AI"
target="_blank"
rel="noopener noreferrer"
className="rounded-md border border-fd-border px-5 py-2.5 text-sm font-medium transition hover:bg-fd-muted"
>
View on GitHub
</Link>
</div>
</section>
{/* Three lanes */}
<section className="px-6 pb-24 max-w-6xl mx-auto w-full">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{lanes.map((lane) => (
<Link
key={lane.kicker}
href={lane.href}
className="group rounded-lg border border-fd-border bg-fd-card p-6 transition hover:border-fd-foreground hover:-translate-y-0.5"
>
<div className="text-[11px] font-mono text-[#3b5bdb] mb-3 tracking-[0.08em]">
{lane.kicker}
</div>
<h3 className="text-base font-semibold mb-2">{lane.title}</h3>
<p className="text-sm text-fd-muted-foreground leading-relaxed mb-4">
{lane.body}
</p>
<div className="text-xs font-mono text-fd-foreground group-hover:text-[#3b5bdb] transition">
{lane.cta}
</div>
</Link>
))}
</div>
</section>
</main>
);
}

View File

@ -1,3 +1,32 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
/* Warm-paper light theme aligned with the landing page (moleculesai.app).
Tokens map fumadocs' @theme variables onto our brand palette so docs,
marketing, and the canvas read as one product. */
@theme {
--font-sans: var(--font-geist), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-mono), ui-monospace, SFMono-Regular, monospace;
--color-fd-background: #fafaf7;
--color-fd-foreground: #15181c;
--color-fd-muted: #f3f1ec;
--color-fd-muted-foreground: #5a5e66;
--color-fd-popover: #ffffff;
--color-fd-popover-foreground: #15181c;
--color-fd-card: #ffffff;
--color-fd-card-foreground: #15181c;
--color-fd-border: #e6e2d8;
--color-fd-primary: #3b5bdb;
--color-fd-primary-foreground: #ffffff;
--color-fd-secondary: #efece4;
--color-fd-secondary-foreground: #15181c;
--color-fd-accent: #efece4;
--color-fd-accent-foreground: #15181c;
--color-fd-ring: #3b5bdb;
--color-fd-overlay: hsla(0, 0%, 0%, 0.18);
}
/* Dark mode keeps fumadocs' neutral defaults readers expect docs sites
to honor their system preference, and our landing only ships light. */

View File

@ -1,7 +1,50 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
// Molecule logo — the same triangle-of-nodes mark used on moleculesai.app.
// Inlined as a JSX element so fumadocs renders it in the topbar without a
// separate asset request.
const MoleculeLogo = (
<svg
width="22"
height="22"
viewBox="0 0 28 28"
fill="none"
aria-hidden="true"
>
<circle cx="14" cy="6" r="2.5" fill="currentColor" />
<circle cx="6" cy="20" r="2.5" fill="currentColor" />
<circle cx="22" cy="20" r="2.5" fill="currentColor" />
<circle
cx="14"
cy="14"
r="1.6"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
/>
<line x1="14" y1="8.5" x2="14" y2="12.6" stroke="currentColor" strokeWidth="1.2" />
<line x1="8" y1="18.5" x2="12.7" y2="14.8" stroke="currentColor" strokeWidth="1.2" />
<line x1="20" y1="18.5" x2="15.3" y2="14.8" stroke="currentColor" strokeWidth="1.2" />
</svg>
);
export const baseOptions: BaseLayoutProps = {
nav: {
title: 'Molecule AI',
title: (
<span className="flex items-center gap-2 font-semibold tracking-tight">
{MoleculeLogo}
<span>Molecule AI</span>
<span className="text-xs uppercase tracking-[0.08em] text-fd-muted-foreground font-mono">
Docs
</span>
</span>
),
url: 'https://doc.moleculesai.app',
},
links: [
{ text: 'Platform', url: 'https://app.moleculesai.app', external: true },
{ text: 'Marketplace', url: 'https://market.moleculesai.app', external: true },
{ text: 'Landing', url: 'https://www.moleculesai.app', external: true },
],
githubUrl: 'https://github.com/Molecule-AI',
};

View File

@ -1,10 +1,16 @@
import './global.css';
import { RootProvider } from 'fumadocs-ui/provider/next';
import { Inter } from 'next/font/google';
import { Geist, JetBrains_Mono } from 'next/font/google';
import type { ReactNode } from 'react';
const inter = Inter({
const geist = Geist({
subsets: ['latin'],
variable: '--font-geist',
});
const jetbrains = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
});
export const metadata = {
@ -19,8 +25,12 @@ export const metadata = {
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<html
lang="en"
className={`${geist.variable} ${jetbrains.variable}`}
suppressHydrationWarning
>
<body className="flex flex-col min-h-screen font-sans">
<RootProvider>{children}</RootProvider>
</body>
</html>

View File

@ -73,6 +73,72 @@ At a high level, `workspace/main.py` does this:
10. Start the skill watcher when skills are configured.
11. Serve the A2A app through Uvicorn.
## Boot-Smoke Contract (`MOLECULE_SMOKE_MODE`)
The image-publish CI pipeline runs each template's image with `MOLECULE_SMOKE_MODE=1` to exercise lazy imports inside `executor.execute()` against stub credentials and no network. The runtime detects the env var, invokes `executor.execute()` once with a stubbed `RequestContext` and a short timeout, then exits — registration, heartbeats, and the A2A server are skipped.
This catches lazy imports that pure `python3 -c "import adapter"` smokes miss: imports nested inside `if`-branches, deferred until first call, or behind `importlib.import_module()`.
### What adapter authors need to do
**Most adapters need to do nothing.** If `setup()` only writes files, parses config, or instantiates Python objects, the smoke gate just works.
**Adapters whose `setup()` does real I/O must opt out of that I/O under smoke mode.** This applies to:
- spawning subprocesses that require valid credentials (e.g. a gateway daemon)
- making real network calls
- writing to filesystem locations that need a specific uid/gid the smoke harness can't guarantee
The contract:
```python
async def setup(self, config: AdapterConfig) -> None:
if os.environ.get("MOLECULE_SMOKE_MODE") == "1":
return # skip real I/O; runtime's smoke short-circuit handles the rest
# ... real setup ...
```
For shell entrypoints that wrap `molecule-runtime`:
```bash
if [ "${MOLECULE_SMOKE_MODE:-0}" = "1" ]; then
exec molecule-runtime
fi
```
### What gets exercised under smoke mode
- All `/app/*.py` modules import cleanly (covered by a separate static-import smoke step)
- `adapter.setup()` runs (with the opt-out above for I/O-heavy adapters)
- `adapter.create_executor()` runs
- `executor.execute()` is invoked once against a stub `RequestContext`/`EventQueue` with `MOLECULE_SMOKE_TIMEOUT_SECS` (default 5s); a clean timeout exits 0, an import error exits non-zero
### What the gate does NOT prove
A green gate means **"imports are healthy enough that `executor.execute()` reaches its body"** — that's the regression class the gate exists to catch (lazy `from x import y` inside an `if`-branch, or `importlib.import_module()` on a path that breaks after a wheel bump).
It does **not** prove that `execute()` produces the right output for real input. The harness reports PASS in three distinct cases:
1. **Clean return** — execute() ran to completion within the timeout.
2. **Timeout** — execute() was still running when the timer fired (typical for adapters that do real I/O inside execute(): subprocess to a gateway, httpx call to an upstream LLM).
3. **Any non-import exception** — execute() raised `RuntimeError`, auth errors, validation errors, etc. The harness only fails on `ImportError`/`ModuleNotFoundError`.
The stub `RequestContext` carries a non-empty `"smoke test"` text message (so adapters relying on `extract_message_text(ctx)` returning input still work), and the harness never drains the `EventQueue` — what `execute()` writes back is ignored.
If you need correctness coverage, write a separate integration test that runs the workspace against real or mocked infrastructure — the smoke gate is a strict subset.
### Stub env the smoke harness sets
| Var | Value |
|---|---|
| `MOLECULE_SMOKE_MODE` | `1` |
| `MOLECULE_SMOKE_TIMEOUT_SECS` | `10` (CI default) |
| `WORKSPACE_ID` | `fake-smoke` |
| `PYTHONPATH` | `/app` (mirrors the platform provisioner) |
| `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `OPENAI_API_KEY` | `sk-fake-smoke-*` |
A `config.yaml` from the template repo's root is mounted at `/configs/config.yaml`.
## Core Runtime Pieces
| File | Responsibility |

View File

@ -27,7 +27,8 @@ CP (Railway): staging service production service
staging.api.moleculesai.app api.moleculesai.app
Tenant EC2s: staging EC2 instances production EC2 instances
*.staging.moleculesai.app *.moleculesai.app
<slug>.staging.moleculesai.app <slug>.moleculesai.app
(per-tenant CNAME, no wildcard) (per-tenant CNAME, no wildcard)
App (Vercel): staging.app.moleculesai.app app.moleculesai.app
(Vercel preview) (Vercel production)
@ -38,8 +39,10 @@ DB (Neon): staging branch main branch
Docker images: platform-tenant:staging platform-tenant:latest
(GHCR) (GHCR)
Cloudflare: *.staging.moleculesai.app *.moleculesai.app
(separate tunnel/worker) (tunnel per tenant)
Cloudflare: per-tenant CNAMEs under per-tenant CNAMEs under
staging.moleculesai.app moleculesai.app
(one CNAME + one tunnel (one CNAME + one tunnel
per provisioned tenant) per provisioned tenant)
```
## Deploy flow
@ -115,15 +118,35 @@ platform-tenant:sha-xxxxx — immutable, pinned to specific commit
# pushes :latest only on manual promote
```
### 5. Cloudflare: staging subdomain
### 5. Cloudflare: per-tenant CNAMEs (no wildcard)
Option A (simple): `*.staging.moleculesai.app` with its own tunnel/worker
Option B (full): separate Cloudflare zone for staging (overkill)
There is **no `*.staging.moleculesai.app` wildcard record** and there is no
`*.moleculesai.app` wildcard either. The control plane writes a per-tenant
CNAME at provision time, pointing `<slug>.<env-domain>` at that tenant's
Cloudflare tunnel (`<tunnel-id>.cfargotunnel.com`).
Recommend Option A:
- Add `staging.moleculesai.app` DNS records
- Staging tenants get `slug.staging.moleculesai.app` subdomains
- Production tenants get `slug.moleculesai.app` (unchanged)
Verified in `molecule-controlplane/internal/provisioner/ec2.go` — the
provisioner calls `Tunnel.CreateTunnelDNS(ctx, slug, domain, tunnelID)`
during workspace provision, then records a `cf_dns` row in
`tenant_resources` with `type=CNAME` for symmetric create/delete audit.
Implications for staging:
- Staging tenants get `<slug>.staging.moleculesai.app` only **after** they
are provisioned through the staging control plane. The CNAME is
written as part of `Provision()`.
- Production tenants get `<slug>.moleculesai.app` the same way, against
the production CP.
- Pre-provision, an unknown slug returns **NXDOMAIN**. This is correct
behavior, not a regression — there is no wildcard to catch the lookup.
- Tests that hit a staging slug they have not provisioned themselves
will fail with `getaddrinfo ENOTFOUND` (Node) or `Name or service not
known` (curl). The fix is to provision your own slug against the
staging CP first; do not file this as an infrastructure bug.
The same model applies to both environments — the only difference is
the parent zone (`staging.moleculesai.app` vs `moleculesai.app`) and the
CP that writes the records.
### 6. EC2: staging tag

View File

@ -21,6 +21,17 @@ register and heartbeat by hand. Use it when your agent can't run an MCP
stdio server.
</Callout>
## Pick the right path
| Your agent runs as | Best path | Why |
|---|---|---|
| **An MCP-aware runtime** (Claude Code, Hermes, OpenCode, Cursor, Cline) | [Bring Your Own Runtime (MCP)](/docs/runtime-mcp) | Universal `molecule-mcp` wheel — no HTTP server, no tunnel. |
| **A Claude Code session on your laptop** | [Claude Code Channel Plugin](/docs/guides/claude-code-channel-plugin) | Polling-based; no tunnel/public URL needed. Set up in under a minute. |
| Any HTTP server with a public URL | The flow on this page (or the [Python SDK guide](/docs/guides/external-agent-registration)) | Push-based; lower latency; works for any A2A-compatible HTTP endpoint. |
| A custom A2A server you wrote yourself | The flow on this page | Direct register + heartbeat + handler. |
The rest of this doc covers the third + fourth rows. For Claude Code or other MCP runtimes, follow the linked guides.
## Prerequisites
- A running Molecule AI platform (default `http://localhost:8080`)

View File

@ -0,0 +1,222 @@
---
title: "Claude Code Channel Plugin — Connect a Claude Code Session as an External Workspace"
description: "Bridge Molecule A2A traffic into a running Claude Code session via MCP. Polling-based, no tunnel required. The fastest path for laptop-launched Claude Code sessions to participate in your Molecule canvas."
---
# Claude Code Channel Plugin
Run [Claude Code](https://claude.com/claude-code) on your laptop and have it appear on the Molecule AI canvas as a first-class external workspace. Inbound A2A messages from peer workspaces surface as conversation turns; replies route back through Molecule's A2A endpoints.
> **What this is:** [`Molecule-AI/molecule-mcp-claude-channel`](https://github.com/Molecule-AI/molecule-mcp-claude-channel) — an MCP-based "channel plugin" that turns a Claude Code session into a Molecule workspace.
> **What this is NOT:** the [Python SDK / curl register flow](/docs/guides/external-agent-registration) for arbitrary HTTP-speaking agents. That flow needs a public URL the platform can POST to. This one polls — runs on any laptop behind any NAT.
---
## What you get
```
Molecule peer ──A2A──▶ [your workspace] ──poll──▶ [plugin] ──MCP notification──▶ Claude Code
▲ │
└────── POST /workspaces/:id/a2a ◄── reply_to_workspace ──┘
```
| Property | Value |
|---|---|
| **Inbound latency** | up to `MOLECULE_POLL_INTERVAL_MS` (default 5s) |
| **Outbound latency** | direct POST — sub-second |
| **Tunnel / public URL** | not required |
| **Auth model** | per-workspace bearer token (same as Python SDK) |
| **Multi-workspace** | yes, comma-separated list |
---
## Prerequisites
| You need | Notes |
|---|---|
| A Molecule AI tenant | Self-hosted localhost or your `*.staging.moleculesai.app` SaaS tenant |
| One or more workspace IDs | Created via canvas or `POST /workspaces` (see [External Agent Registration](/docs/guides/external-agent-registration)) |
| The workspace bearer token | Shown once when the workspace is created — save it from the canvas modal |
| Claude Code | `claude` CLI ≥ the version that supports `--channels` |
| `bun` | The plugin runs under bun for fast startup; `bun install` is invoked automatically by `start` |
> **Note:** The platform must be running molecule-core ≥ PR #2300, which shipped the `?since_secs=` query parameter on `GET /workspaces/:id/activity`. Available on all staging-onward and self-hosted main builds after 2026-04-29.
---
## Step 1 — Create the workspace
In your Molecule canvas:
1. Click **+ New workspace**
2. Choose **External** runtime
3. Set tier as needed; click **Create**
4. The "Connect your external agent" modal opens — switch to the **Claude Code** tab
5. Copy the entire snippet (everything from the `mkdir -p` line through `claude --channels ...`)
Or via API:
```bash
curl -X POST "$MOLECULE_PLATFORM_URL/workspaces" \
-H "Content-Type: application/json" \
-d '{"name": "My Claude Code", "external": true, "tier": 2}'
```
The response includes `claude_code_channel_snippet` — same content as the canvas tab, ready to paste.
## Step 2 — Set up the channel config
Run the snippet from Step 1. It does two things:
```bash
mkdir -p ~/.claude/channels/molecule
cat > ~/.claude/channels/molecule/.env <<'EOF'
MOLECULE_PLATFORM_URL=https://your-tenant.staging.moleculesai.app
MOLECULE_WORKSPACE_IDS=ws-uuid-1
MOLECULE_WORKSPACE_TOKENS=<paste auth_token from create response>
EOF
chmod 600 ~/.claude/channels/molecule/.env
```
Replace the token placeholder with the workspace bearer from Step 1.
## Step 3 — Launch Claude Code
```bash
claude --channels plugin:molecule@Molecule-AI/molecule-mcp-claude-channel
```
You should see on stderr (use `--debug` to surface):
```
molecule channel: connected — watching 1 workspace(s) at https://your-tenant.staging.moleculesai.app
workspaces: ws-uuid-1
poll: every 5000ms with 30s window
```
That's it — the workspace is live on the canvas with a purple **REMOTE** badge, and any A2A traffic the workspace receives surfaces as conversation turns in your Claude Code session.
---
## How replies work
When a peer's message lands in your session, you'll see a turn with structured metadata:
```json
{
"method": "notifications/claude/channel",
"params": {
"content": "Hey, can you take a look at this? <issue body>",
"meta": {
"source": "molecule",
"workspace_id": "ws-uuid-1",
"peer_id": "ws-uuid-pm-coordinator",
"method": "user_message",
"activity_id": "act-...",
"ts": "2026-04-29T..."
}
}
}
```
Reply normally — Claude calls the `reply_to_workspace` MCP tool with `peer_id` from the meta block, and the response flows back through `POST /workspaces/:peer_id/a2a` so peers see it just like any other A2A message.
---
## Multi-workspace setup
Watch multiple workspaces from a single Claude Code session by comma-separating the lists. Both must have the same length and order:
```bash
MOLECULE_WORKSPACE_IDS=ws-pm,ws-researcher,ws-engineer
MOLECULE_WORKSPACE_TOKENS=tok-pm,tok-researcher,tok-engineer
```
When Claude replies, the `reply_to_workspace` tool requires `workspace_id` (which of the watched workspaces to reply AS) explicitly. With a single workspace it's implicit.
---
## Configuration reference
| Variable | Default | Purpose |
|---|---|---|
| `MOLECULE_PLATFORM_URL` | (required) | Tenant base URL (no trailing slash) |
| `MOLECULE_WORKSPACE_IDS` | (required) | Comma-separated workspace UUIDs to watch |
| `MOLECULE_WORKSPACE_TOKENS` | (required) | Comma-separated bearer tokens, **same order as IDs** |
| `MOLECULE_POLL_INTERVAL_MS` | `5000` | How often each workspace is polled (ms) |
| `MOLECULE_POLL_WINDOW_SECS` | `30` | `since_secs` window per poll. Wider than interval to recover from missed ticks |
| `MOLECULE_STATE_DIR` | `~/.claude/channels/molecule` | Override state directory (testing) |
---
## Architecture notes
### Why polling instead of push?
The [Python SDK external-agent flow](/docs/guides/external-agent-registration) uses **push**: register an inbound URL, platform POSTs A2A to that URL. Lower latency but requires a tunnel (ngrok / Cloudflare) or static IP — non-trivial for laptop sessions.
This plugin uses **polling** as the default because it works through every NAT/firewall with zero infra. Cost: up to `MOLECULE_POLL_INTERVAL_MS` of inbound latency. For production setups where lower latency matters, push mode is on the v0.2 roadmap.
### Why the 30s window over a 5s interval?
A single missed tick (transient network blip, GC pause, laptop sleep) shouldn't lose messages. The plugin re-fetches the last 30 seconds on every poll and dedups by `activity_id`, so 25 seconds of overlap is the recovery margin. Increase `MOLECULE_POLL_WINDOW_SECS` for noisier networks.
### Singleton lock
Only one channel server runs per host — multiple instances would race the dedup state and double-deliver. The plugin maintains a PID file at `~/.claude/channels/molecule/bot.pid` and on startup kills any stale predecessor. This mirrors the [`@claude-plugins-official/telegram`](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/telegram) pattern.
---
## Troubleshooting
### "molecule channel: required config missing"
The plugin started before you filled in `.env`. Re-run the snippet from Step 2, then re-launch Claude Code.
### "molecule channel: poll `<ws-id>` returned 401"
Bearer token mismatch. Two common causes:
- The token in `MOLECULE_WORKSPACE_TOKENS` doesn't match the workspace whose ID is in the corresponding position of `MOLECULE_WORKSPACE_IDS`. Verify same-order pairing.
- The workspace was rotated and the token was revoked. Generate a new token from the canvas Settings tab (or `POST /admin/workspaces/:id/tokens`).
### "molecule channel: poll `<ws-id>` returned 404"
Either the workspace doesn't exist or the `MOLECULE_PLATFORM_URL` is wrong. Confirm:
```bash
curl -fsS "$MOLECULE_PLATFORM_URL/workspaces/$WS_ID" \
-H "Authorization: Bearer $WS_TOKEN" | jq '.workspace.id'
```
### A2A messages aren't surfacing
Check that the watched workspace is actually receiving them — the plugin only pulls `activity_logs` rows whose `activity_type = a2a_receive`. If peers aren't sending to this workspace, there's nothing to surface. Verify with:
```bash
curl -fsS "$MOLECULE_PLATFORM_URL/workspaces/$WS_ID/activity?type=a2a_receive&limit=10" \
-H "Authorization: Bearer $WS_TOKEN" | jq
```
If that returns events but Claude doesn't see them, file an issue at [`Molecule-AI/molecule-mcp-claude-channel`](https://github.com/Molecule-AI/molecule-mcp-claude-channel/issues) with the workspace_id + sample event.
---
## Limitations (v0.1)
- **Polling-only inbound.** No push mode yet; latency floor is `MOLECULE_POLL_INTERVAL_MS`.
- **No pairing flow.** Tokens are configured manually via `.env`; no canvas-side approval handshake.
- **No file-attachment download.** URLs surface in the meta block; the host fetches on-demand.
- **No outbound channel-init.** The plugin only sends replies (in response to inbound A2A); starting a fresh A2A conversation initiated FROM the Claude Code side requires a future `start_workspace_chat` tool.
Track the v0.2 roadmap on the [plugin repo's README](https://github.com/Molecule-AI/molecule-mcp-claude-channel#limitations-v01).
---
## See also
- [External Agent Registration](/docs/guides/external-agent-registration) — full A2A wire-shape reference + Python SDK + curl flow
- [External Workspace Quickstart](/docs/guides/external-workspace-quickstart) — 5-min guide for any HTTP-speaking agent
- [Remote Workspaces FAQ](/docs/guides/remote-workspaces-faq) — production hardening notes
- [`Molecule-AI/molecule-mcp-claude-channel`](https://github.com/Molecule-AI/molecule-mcp-claude-channel) — plugin source code, issues, v0.2 roadmap

View File

@ -9,6 +9,8 @@ Run an agent on your laptop, a home server, a cloud VM, or any machine with inte
> **Looking for the operator-focused reference?** See [External Agent Registration](/docs/guides/external-agent-registration) for full capability + auth details, or [Remote Workspaces FAQ](/docs/guides/remote-workspaces-faq) for hardening + production notes. This doc is the fast path.
> **Running Claude Code on your laptop?** Skip this guide — use the [Claude Code Channel Plugin](/docs/guides/claude-code-channel-plugin) instead. It's polling-based and needs no tunnel, so your laptop session shows up on the canvas in under a minute.
---
## What is an "external workspace"?

View File

@ -0,0 +1,166 @@
---
title: Marketplace
description: A tiered library of plugins, agents, and bundles you can mount into any Molecule workspace.
---
## Overview
The Molecule **Marketplace** is the distribution surface for reusable agent
infrastructure. It surfaces three tiers of artifacts — from a single MCP
plugin to a full team topology — and the same governance, memory, and audit
substrate runs underneath each one.
You browse and install via the Marketplace UI at
[`https://moleculesai.app`](https://moleculesai.app), or pin entries from
your `workspace.yaml` for reproducible deployments.
---
## Three Tiers
| Tier | Name | Granularity | Mount as |
|------|------|-------------|----------|
| **L1** | Plugins | A single MCP server / tool pack | Tool capability on an agent or workspace |
| **L2** | Agents | A prebuilt single-agent skill (prompts + tools + policy) | Workspace member |
| **L3** | Bundles | A full team topology (root + children with their own scopes) | Workspace |
The tier model is intentionally additive — an L3 Bundle is composed of L2
Agents, which in turn use L1 Plugins. Forking a Bundle gives you the lineage
to swap any constituent piece without rewiring the operating model.
### L1 — Plugins
Plugins are MCP servers or agentskills.io packs. Examples:
- `postgres` — read/write Postgres with role-scoped credentials
- `slack` — post and search Slack with workspace-scoped tokens
- `linear` — create / triage / comment on Linear issues
- `gh-actions` — query and dispatch GitHub Actions runs
- `sentry` — read incident timeline, ack alerts
Plugins follow the [two-axis source/shape model](/docs/plugins) and install
from either a curated `local://` source or a pinned `github://owner/repo#tag`.
### L2 — Agents
Agents are single-purpose skills mounted as a workspace member. They ship with:
- A **system prompt** baked in
- A **tool manifest** specifying which L1 plugins they require
- A **policy** declaring scope reads/writes and approval requirements
Examples:
- `code-reviewer` — five-axis review, posts inline comments via `gh-actions`
- `oncall-triager` — reads Sentry, drafts a runbook step, requests approval before paging
- `churn-analyst` — periodic Postgres + Stripe rollup, posts a weekly Slack summary
Mount an agent via the workspace UI or `workspace.yaml`:
```yaml
members:
- kind: agent
source: marketplace://l2/code-reviewer
version: ^1.2.0
scopes:
- read: pull_requests
- write: pull_request_comments
```
### L3 — Bundles
Bundles are complete team topologies. A bundle ships:
- A **root agent** that coordinates the team
- One or more **child agents**, each with its own scope, memory, and tool
list
- A **policy graph** declaring which scopes the root can write through and
which approvals route to humans
Examples:
- `growth-team` — root strategist + content-writer + analytics-rollup +
experiment-designer
- `platform-ops` — root SRE + on-call triager + change-reviewer +
incident-scribe
- `revenue-pod` — root commercial lead + churn-analyst + cs-summarizer +
expansion-prospector
Mount a bundle as a workspace:
```yaml
workspace:
bundle: marketplace://l3/platform-ops
bundle_version: ^0.4.0
overrides:
members:
change-reviewer:
scopes:
- read: ["github:Molecule-AI/*", "linear:eng"]
```
Forking is encouraged — the bundle author publishes the operating model;
your team tunes it for your processes without rebuilding the substrate.
---
## Trust Tiers
Every Marketplace entry carries a **trust tier** that signals review depth
and supply-chain provenance:
| Trust | Vetting | Provenance |
|-------|---------|------------|
| **Verified** | Reviewed by Molecule for safety, prompt-injection resistance, and policy correctness | Published from a Molecule-controlled identity |
| **Partner** | Reviewed by a Marketplace partner; carries the partner's identity badge | Published from a verified partner account |
| **Community** | Self-published; static analysis + sandbox runtime; no human review | Pinned to a specific commit SHA |
The trust tier is shown on every listing card and gated by enterprise
policy: organizations on the Enterprise plan can restrict installs to
Verified-only via `policy.marketplace.min_trust = verified`.
---
## Installing from the Marketplace
Browse listings at [`https://moleculesai.app`](https://moleculesai.app).
Each card shows tier (L1/L2/L3), trust badge, runtime compatibility, and
required scopes. The "Install" flow:
1. Picks a workspace (or creates a new one) to mount into.
2. Surfaces required scopes for review and approval.
3. Pins to a specific version (semver range, exact tag, or commit SHA).
4. Writes the entry into your `workspace.yaml` and triggers a workspace
redeploy.
You can also install non-interactively:
```bash
curl -X POST https://app.moleculesai.app/cp/orgs/$ORG/marketplace/install \
-H "Authorization: Bearer $CP_ADMIN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tier": "l2",
"slug": "code-reviewer",
"version": "^1.2.0",
"workspace_id": "ws_abc123"
}'
```
---
## Listing on the Marketplace
If you have built reusable agent infrastructure — a plugin, agent, or
bundle — you can list it on the Marketplace and reach every Molecule
organization. See [Listing on the Marketplace](/docs/marketplace/creators)
for the full builder workflow.
---
## See also
- [Plugins](/docs/plugins) — L1 source/shape model and install mechanics
- [External Agents](/docs/external-agents) — bringing a non-Molecule agent runtime
- [Workspace Configuration](/docs/workspace-config) — `workspace.yaml` reference
- [Listing on the Marketplace](/docs/marketplace/creators) — builder workflow

View File

@ -0,0 +1,164 @@
---
title: Listing on the Marketplace
description: How builders ship plugins, agents, and bundles to every Molecule organization.
---
## Overview
The Marketplace is open to external builders. If you have authored reusable
agent infrastructure — an MCP plugin, a single-agent skill, or a full team
bundle — you can list it and reach every Molecule organization. We handle
distribution, billing, and policy; you keep the IP and the upgrade cadence.
This page walks through the three-step workflow: **Build · List · Earn**.
---
## 1. Build
You author your artifact against the open Molecule SDK. The same primitives
we use internally are available to you:
- **Workspace** — the durable boundary for memory, members, and policy
- **A2A** — agent-to-agent messaging, used to talk to runtimes you don't
own (LangGraph, CrewAI, etc.)
- **Memory scopes** — hierarchical, governance-aware persistence
- **Audit** — every action is captured at the orchestration layer
Pick the tier that matches your artifact's granularity:
### L1 — Plugins
A plugin is an MCP server (or an agentskills.io pack). The
[two-axis source/shape model](/docs/plugins) describes how the workspace
runtime loads it. Authoring requirements:
- A `plugin.yaml` manifest declaring tools, required scopes, and runtime
compatibility.
- A README documenting the tool surface and side effects.
- For MCP plugins: an HTTP or stdio MCP server pinned to a tagged commit.
A reference plugin lives at
[`Molecule-AI/molecule-ai-plugin-template`](https://github.com/Molecule-AI/molecule-ai-plugin-template).
### L2 — Agents
An agent is a single workspace member with a baked-in prompt + tools +
policy. Authoring requirements:
- An `agent.yaml` manifest declaring system prompt, required L1 plugins,
scope reads/writes, and approval triggers.
- A `prompts/` directory with the system prompt and any reusable templates.
- A `tests/` directory exercising the prompt against canned scenarios.
Reference: [`Molecule-AI/molecule-ai-agent-template`](https://github.com/Molecule-AI/molecule-ai-agent-template).
### L3 — Bundles
A bundle ships a complete team topology — a root agent plus children, each
with its own scope and memory. Authoring requirements:
- A `bundle.yaml` declaring members, their scopes, and the policy graph
(which scopes the root can write through, which approvals route to
humans).
- A `members/` directory containing member-specific overrides if any
member is a fork of an L2 agent.
- A `topology.svg` diagram (auto-rendered from `bundle.yaml`, but you can
override).
Reference: [`Molecule-AI/molecule-ai-bundle-template`](https://github.com/Molecule-AI/molecule-ai-bundle-template).
---
## 2. List
Submit through the **Creator Portal** at
[`https://moleculesai.app/creators`](https://moleculesai.app/creators).
The submission flow:
1. **Connect** — link the GitHub repository hosting your artifact. We pull
from tagged releases; we never re-tag or modify your code.
2. **Manifest check** — we validate `plugin.yaml` / `agent.yaml` /
`bundle.yaml` against the schema for your tier and surface any gaps.
3. **Static analysis** — credential-shape scan, prompt-injection-pattern
scan, and dependency vulnerability check on every tagged release.
4. **Sandbox boot** — your artifact is mounted into a throwaway workspace
to verify it boots, declares its scopes correctly, and surfaces a
reasonable error path.
5. **Trust tier** — every artifact starts at **Community**. Apply for
**Partner** or **Verified** review once you have a couple of releases
under your belt.
Pricing is configured at submission:
- **Free** — no charge to install.
- **Per-seat** — a flat monthly amount per workspace member that mounts
the artifact.
- **Per-use** — metered against a unit you define (token calls, runs,
alerts handled).
- **Hybrid** — base seat fee plus metered overages.
You can change pricing on subsequent releases; existing installs are
grandfathered to the version they pinned.
---
## 3. Earn
Once your listing is live, you receive:
- **Distribution** — every Molecule organization sees your listing in the
Marketplace UI, gated only by their policy (`min_trust`, region, etc.).
- **Billing** — Molecule handles the charge to the installing
organization, deducts the platform fee (15% as of writing; check the
current rate in the Creator Portal), and pays out monthly.
- **Audit visibility** — you see install counts, version distribution,
and aggregated usage metrics in the Creator Portal. You do **not** see
per-organization data.
- **Upgrade cadence** — semver: bump tags, organizations on a `^range`
pin pull updates on their next workspace redeploy. Major bumps require
re-approval of any new scopes.
---
## Policy & Safety
By listing, you agree to:
- **No exfiltration** — your code does not transmit organization data
outside the scopes it declares.
- **Pinned releases** — every version is pinned to an immutable commit;
retagging is not permitted.
- **Disclose model usage** — if your agent calls an LLM API, declare the
provider and model so enterprise plans can route through their own
keys.
- **Respect approval triggers** — if your `agent.yaml` declares a scope
that requires human approval (e.g. `write: pull_request_merge`), you
must call the approval API before acting.
Listings that violate these terms are de-listed; refunds for affected
installs are paid from your account.
---
## Maintenance
Once a listing is live, you can:
- Push new tagged releases — they enter the static-analysis + sandbox
flow automatically.
- Mark older versions as **deprecated** to nudge installs to upgrade.
- File **security advisories** that surface to every organization on a
vulnerable pinned version.
- Yank a release in the rare case of a critical bug; organizations
pinned to the yanked tag are notified and offered the next safe version.
---
## See also
- [Marketplace](/docs/marketplace) — tier model and installation overview
- [Plugins](/docs/plugins) — L1 plugin source/shape mechanics
- [Workspace Configuration](/docs/workspace-config) — pinning marketplace
entries in `workspace.yaml`
- [Security &raquo; OWASP Agentic Top 10](/docs/security/owasp-agentic-top-10) — supply-chain considerations relevant to bundle authors

View File

@ -20,6 +20,9 @@
"self-hosting/admin-token",
"observability",
"troubleshooting",
"---Marketplace---",
"marketplace",
"marketplace/creators",
"---Security---",
"security/index",
"security/safe-mcp-advisory",

View File

@ -52,14 +52,54 @@ set.
### Claude Code
Two equivalent paths — pick whichever your version supports.
**CLI (Claude Code 2.1+):** pass each env var with `-e`, scope with
`-s user` so the server is available in every project, and put the
command after `--`:
```bash
claude mcp add molecule -s user -- env \
WORKSPACE_ID=<your-workspace-uuid> \
PLATFORM_URL=https://<your-tenant>.moleculesai.app \
MOLECULE_WORKSPACE_TOKEN=<your-token> \
molecule-mcp
claude mcp add molecule -s user \
-e WORKSPACE_ID=<your-workspace-uuid> \
-e PLATFORM_URL=https://<your-tenant>.moleculesai.app \
-e MOLECULE_WORKSPACE_TOKEN=<your-token> \
-- molecule-mcp
```
<Callout type="info">
Older docs used a `-- env VAR=val ... molecule-mcp` shell trick (with
`env` as the command). It still works but produces a less idiomatic
`~/.claude.json` entry and trips up the post-2.1 flag parser if you
forget the `--`. Prefer the `-e` form above.
</Callout>
**Direct edit of `~/.claude.json`:** add the entry under the **top-level
`mcpServers` key** (this is the user-scope location — available in
every project). If you'd rather scope it to a single project, use a
`.mcp.json` file in that project's root with the same `mcpServers`
shape.
```json
{
"mcpServers": {
"molecule": {
"type": "stdio",
"command": "molecule-mcp",
"args": [],
"env": {
"WORKSPACE_ID": "<your-workspace-uuid>",
"PLATFORM_URL": "https://<your-tenant>.moleculesai.app",
"MOLECULE_WORKSPACE_TOKEN": "<your-token>"
}
}
}
}
```
If `molecule-mcp` isn't on the PATH that Claude Code sees (common on
macOS — see [Troubleshooting](#command-not-found-molecule-mcp-from-inside-the-runtime)),
replace `"command": "molecule-mcp"` with the absolute path from `which molecule-mcp`.
Reconnect with `/mcp` (or restart the Claude Code session) and the tools
appear in the next turn.
@ -104,31 +144,53 @@ Cline) and restart the client.
## Optional — declare your identity & capabilities
Three additional env vars control how your workspace appears on the
canvas and to peer agents calling `list_peers`:
Four additional env vars control how your workspace appears on the
canvas and how the wheel's inbound-delivery contract behaves:
| Env var | What it sets | Default |
|---|---|---|
| `MOLECULE_AGENT_NAME` | Display name on the canvas card | `molecule-mcp-{id[:8]}` |
| `MOLECULE_AGENT_DESCRIPTION` | One-line description in Details/Skills tabs | empty |
| `MOLECULE_AGENT_SKILLS` | Comma-separated skill names — e.g. `research,code-review,memory-curation` | `[]` |
| `MOLECULE_MCP_POLL_TIMEOUT_SECS` | How long the agent blocks on `wait_for_message` per turn (the universal poll path). `0` disables polling for push-only mode (Claude Code launched with `--dangerously-load-development-channels server:molecule`). Above 60 clamps to 60. | `2` |
Skills are surfaced two places:
1. **Canvas Skills tab** — each skill renders as a chip with the name
2. **Peer agents calling `list_peers`** — they see `{name, skills: [...]}` for each peer, so other agents can route delegations to the right specialist instead of guessing from name alone
Example with all three set:
Example with all three set (Claude Code 2.1+ CLI form):
```bash
claude mcp add molecule -s user -- env \
WORKSPACE_ID=<uuid> \
PLATFORM_URL=https://<tenant>.moleculesai.app \
MOLECULE_WORKSPACE_TOKEN=<token> \
MOLECULE_AGENT_NAME='Research Assistant' \
MOLECULE_AGENT_DESCRIPTION='Reads, summarises, cites.' \
MOLECULE_AGENT_SKILLS=research,summarisation,citations \
molecule-mcp
claude mcp add molecule -s user \
-e WORKSPACE_ID=<uuid> \
-e PLATFORM_URL=https://<tenant>.moleculesai.app \
-e MOLECULE_WORKSPACE_TOKEN=<token> \
-e MOLECULE_AGENT_NAME='Research Assistant' \
-e MOLECULE_AGENT_DESCRIPTION='Reads, summarises, cites.' \
-e MOLECULE_AGENT_SKILLS=research,summarisation,citations \
-- molecule-mcp
```
Or as the equivalent `~/.claude.json` entry:
```json
{
"mcpServers": {
"molecule": {
"type": "stdio",
"command": "molecule-mcp",
"env": {
"WORKSPACE_ID": "<uuid>",
"PLATFORM_URL": "https://<tenant>.moleculesai.app",
"MOLECULE_WORKSPACE_TOKEN": "<token>",
"MOLECULE_AGENT_NAME": "Research Assistant",
"MOLECULE_AGENT_DESCRIPTION": "Reads, summarises, cites.",
"MOLECULE_AGENT_SKILLS": "research,summarisation,citations"
}
}
}
}
```
A peer agent's `list_peers()` call would then surface this workspace
@ -158,7 +220,7 @@ status. If the workspace is still offline after ~30s, check
| `delegate_task` | Send a task to a peer and wait for the reply |
| `delegate_task_async` | Fire-and-forget delegation; result lands in inbox |
| `check_task_status` | Poll an async delegation |
| `wait_for_message` | Block until the next inbound A2A message arrives |
| `wait_for_message` | Block until the next inbound A2A message arrives — the universal inbound-delivery primitive (see [Inbound delivery](#inbound-delivery-universal-poll-optional-push)) |
| `inbox_peek` / `inbox_pop` | Inspect / acknowledge queued inbound messages |
| `send_message_to_user` | Push a chat bubble to the user's canvas |
| `commit_memory` / `recall_memory` | Persistent KV (local / team / global scope) |
@ -168,29 +230,125 @@ External runtimes can't accept inbound HTTP, so the wheel polls
through `wait_for_message` + `inbox_peek` / `inbox_pop`. Use those
instead of waiting for an HTTP webhook — there isn't one.
### Push-UX for notification-capable hosts
### Inbound delivery: universal poll, optional push
On top of the polling tools, the wheel emits a JSON-RPC notification
(`notifications/claude/channel`) on every new inbound message. Hosts
that recognise that method (Claude Code today; any compliant client
tomorrow) treat the notification as a conversation interrupt — the
message text becomes the next agent turn without the agent having to
call `wait_for_message` first.
Inbound messages reach the agent via one of two paths. The wheel
exposes both; which one fires depends on the host's capabilities.
Both paths converge on the same `inbox_pop` ack so dedup is automatic.
Hosts that don't recognise the method silently ignore it, so the same
wheel works for both push-capable and poll-only runtimes. There is no
config flag to toggle: pollers keep polling, notification-capable hosts
get push automatically.
**Poll path (universal default — works on every spec-compliant MCP
client).** The wheel's `initialize` handshake includes an `instructions`
field telling the agent: *"At the start of every turn, before producing
your final response, call `wait_for_message(timeout_secs=N)` to check
for inbound messages."* Every MCP client surfaces `instructions` to
the agent's system prompt automatically, so Claude Code, Cursor, Cline,
OpenCode, hermes-agent, and codex all receive the polling contract
without any per-client wiring. The 2-second default is tuned for the
"peer A2A landed seconds before my turn started" common case; tune
via the `MOLECULE_MCP_POLL_TIMEOUT_SECS` env var
(see "Optional — declare your identity & capabilities" above).
**Push path (Claude Code with channel push enabled — strictly
better when available).** On top of the poll path, the wheel emits a
JSON-RPC notification (`notifications/claude/channel`) on every new
inbound message and declares the matching `experimental.claude/channel`
capability in `initialize`. Claude Code with channel push enabled
turns the notification into an inline `<channel source="molecule"
...>` synthetic user turn — zero agent-side polling cost, zero
per-turn stall.
**Today (research preview), Claude Code's channel push requires
either the `--dangerously-load-development-channels` launch flag OR
an entry on Claude Code's approved channel-server allowlist.** The
wheel ships the wire shape correctly, but a standard `claude` launch
without the flag silently drops the notification — which is why the
poll path has to be the floor.
Since Claude Code 2.1.x the flag takes a tagged allowlist, not a bare
switch. Pass each MCP server you want to push from as `server:<name>`
(matching the name you registered the server under in Claude Code's
config — `molecule` if you followed [Step 2](#claude-code) above):
```bash
claude --dangerously-load-development-channels server:molecule
```
Multiple entries are space-separated:
`server:molecule server:telegram`. A bare
`--dangerously-load-development-channels` (no value) is rejected with
`argument missing`; an untagged value (`molecule`) is rejected with
`entries must be tagged`. Easy way to confirm push is live: the
session header prints `Listening for channel messages from:
server:molecule`, and inbound canvas messages render inline as
`← molecule: <text>` instead of arriving via `inbox_peek`.
Set `MOLECULE_MCP_POLL_TIMEOUT_SECS=0` to disable polling entirely
when you're running Claude Code with the dev-channels flag and don't
want the per-turn stall. The instructions adapt automatically: with
polling disabled, the agent is told push is the only delivery path.
#### `<channel>` envelope attributes
Every inbound message — push or poll — carries the same metadata
shape. On the push path, attributes render inline as XML-style attrs
on the `<channel>` tag; on the poll path, the same fields appear in
the JSON returned by `inbox_peek` / `wait_for_message`. Either way,
the agent sees a consistent view.
| Attribute | When present | Description |
|---|---|---|
| `source` | always | Always `molecule` — distinguishes our channel from other registered servers (`telegram`, etc.). |
| `kind` | always | `canvas_user` (a human in the canvas chat) or `peer_agent` (another workspace's agent). Drives reply routing. |
| `peer_id` | always | Empty for `canvas_user`; the sender's workspace UUID for `peer_agent`. Use as `workspace_id` when calling `delegate_task` to reply. |
| `peer_name` | `peer_agent` only | The peer's display name (e.g. `ops-agent`) resolved from the platform registry. Absent on registry-lookup failure — the push still delivers. |
| `peer_role` | `peer_agent` only | The peer's declared role (e.g. `sre`, `coordinator`). Same registry source as `peer_name`; same graceful-degrade rule. |
| `agent_card_url` | `peer_agent` only | URL of the platform's discover endpoint for this peer. Fetch it if you need the peer's full capability list (skills, runtime, etc.). |
| `activity_id` | always | The inbox row ID. **Pass it to `inbox_pop` after handling** so the message isn't re-delivered on the next push or poll cycle. |
| `ts` | always | ISO-8601 timestamp of when the message landed in the platform's activity log. |
`peer_name` and `peer_role` are added by the wheel via a TTL'd
registry lookup keyed on `peer_id`. Cache TTL is 5 minutes — long
enough that a busy multi-peer chat doesn't hit the registry on every
push, short enough that role/name renames propagate within a single
agent session. Lookup failure is silent: the attributes are simply
absent and the push delivers anyway, so a registry stall can never
block inbound messages.
`agent_card_url` is constructed deterministically from `peer_id`, so
it's present even if the registry is down. The agent can hit it
later to enumerate the sender's capabilities once the registry is
back up.
Worked push example for a `peer_agent` arrival:
```
<channel source="molecule" kind="peer_agent"
peer_id="11111111-2222-3333-4444-555555555555"
peer_name="ops-agent" peer_role="sre"
agent_card_url="https://platform.example.com/registry/discover/11111111-2222-3333-4444-555555555555"
activity_id="act-742" ts="2026-05-01T12:34:56Z">
Can you check the deploy status for the canary?
</channel>
```
| Client | Push path | Poll path |
|---|---|---|
| Claude Code with `--dangerously-load-development-channels server:molecule` | ✅ inline `← molecule:` tag | ✅ also works |
| Claude Code (standard launch) | ❌ silently dropped | ✅ via instructions |
| Cursor / Cline / OpenCode / codex | ❌ method ignored | ✅ via instructions |
| hermes-agent | ❌ method ignored | ✅ naturally polls every cycle |
### MCP spec compliance
The wheel speaks MCP protocol version **2024-11-05** over stdio
JSON-RPC, declaring only the `tools` capability. It implements the
standard request methods and nothing client-specific:
JSON-RPC. It declares the standard `tools` capability plus the
`experimental.claude/channel` capability for the optional push path
(see [Inbound delivery](#inbound-delivery-universal-poll-optional-push)).
It implements the standard request methods and nothing client-specific:
| MCP method | Behavior |
|---|---|
| `initialize` | Echoes `protocolVersion: "2024-11-05"`, `serverInfo`, declares `tools` capability |
| `initialize` | Echoes `protocolVersion: "2024-11-05"`, `serverInfo`, declares `tools` + `experimental.claude/channel` capabilities, returns the dual-path delivery `instructions` |
| `notifications/initialized` | No-op (no response — per spec) |
| `tools/list` | Returns all exposed tools in one response (no pagination cursor — surface is small) |
| `tools/call` | Dispatches by name, returns `content: [{ type: "text", text: ... }]` |
@ -198,8 +356,10 @@ standard request methods and nothing client-specific:
The push-UX notification (`notifications/claude/channel`) is the only
non-standard method emitted, and it's a one-way notification — clients
that don't handle it discard it per JSON-RPC semantics. No part of the
wheel's tool surface depends on a client recognizing it.
that don't handle it discard it per JSON-RPC semantics. The poll path
(via the standard `instructions` field) carries delivery for those
clients, so no part of the wheel's tool surface depends on a client
recognizing the notification.
This means **any spec-compliant MCP client** can drive the wheel:
Claude Code, Cursor, Cline, OpenCode, hermes-agent, or anything else
@ -305,6 +465,21 @@ A quick way to confirm: `ps aux | grep molecule-mcp` and check the
PID hasn't changed across `/mcp` reconnects. If the same PID stays
alive, the runtime is still using the old config.
### `claude mcp add` rejects the install command on Claude Code 2.1+
Two common shapes from older docs trip the 2.1+ parser:
- `claude mcp add molecule -s user -- env VAR=val molecule-mcp` — works
but lands as `command: "env"` with positional args, which surprises
some MCP clients on older 2.1.x patch builds.
- `claude mcp add molecule -e VAR=val molecule-mcp` (missing `--`) — the
CLI parses `molecule-mcp` as a flag value, not a command, and either
errors or silently registers nothing.
Use the `-e` form **with** `--` (see [Step 2](#claude-code)), or skip the
CLI entirely and write the JSON shape into `~/.claude.json` directly.
The on-disk shape is the source of truth and not version-sensitive.
### `command not found: molecule-mcp` from inside the runtime
The runtime's `PATH` may differ from your interactive shell — common
@ -319,6 +494,44 @@ which molecule-mcp
Then point `command` at that absolute path in `claude mcp add` /
`.cursor/mcp.json` / `mcp_servers.yaml`.
### `error: option '--dangerously-load-development-channels <servers...>' argument missing`
You're on Claude Code 2.1.x or later. The flag changed from a bare
switch to an allowlist that takes tagged entries. See
[Inbound delivery](#inbound-delivery-universal-poll-optional-push) for
the right form — short answer:
```bash
claude --dangerously-load-development-channels server:molecule
```
### `--dangerously-load-development-channels entries must be tagged: molecule`
The flag value needs the `server:` (or `plugin:`) prefix. Pass
`server:molecule` (the registered MCP server name), not bare
`molecule`.
### `Control request timeout: initialize` from the workspace agent
This is the symptom of forwarding the dev-channels flag to a nested
`claude` CLI through the `claude-agent-sdk` with the wrong shape. If
you embed the wheel inside an SDK-driven agent (e.g. the claude-code
workspace template's `claude_sdk_executor.py`), pass the tagged value
through `extra_args`:
```python
ClaudeAgentOptions(
...,
extra_args={"dangerously-load-development-channels": "server:molecule"},
)
```
The SDK forwards `extra_args` keys as `--<key> <value>` to the spawned
CLI. Passing `None` renders as a bare switch and the post-2.1.x CLI
rejects it with `argument missing`, which surfaces upstream as
`Control request timeout: initialize` (the SDK never gets a response
to its initialize control message).
## When to use this vs. the manual A2A path
| Scenario | Use |