molecules-workspace
A thin CLI that hosts a Molecules workspace:
workspace = (molecule_runtime base loop) + (a selected runtime) + (a scoped identity) --connected to the mesh
The OSS molecule-core base is the product. This CLI is a wrapper — it
assembles and runs; it does not own the substance. The base loop, mail/inbox,
heartbeat, memory, A2A server, prompt assembly and MCP rendering all live in
molecule_runtime and the selected runtime adapter. The CLI's whole job is to
put the right pieces around one call into the runtime's own entrypoint.
The invariant: in-container == CLI-in-container
The in-container image's entrypoint.sh ends with exec molecule-runtime,
which is the console script for molecule_runtime.main.main_sync. This CLI's
run command boots the loop with the exact same call:
from molecule_runtime.main import main_sync; main_sync()
There is one workspace-runner, internal and external. The CLI does not fork
the loop or maintain a parallel path — it imports molecule_runtime as a
library and reaches its entrypoint, assembling items 1–8 (below) around it,
which is byte-for-byte what the container assembles via Dockerfile + entrypoint.
Responsibilities (thin)
- Run the base loop — owns nothing of it; boots
molecule_runtimestandalone. - Drive the selected runtime —
--runtime claude-code|openclawvia theADAPTER_MODULE+ module-levelAdapterindirection (no runtime→class table; the runtime string only cross-checksadapter.name()). - Enroll + identity — one scoped, revocable key → first-class mesh peer.
- Wire mail transport —
PLATFORM_URL(+ optional advertised A2A URL). - Untrusted-box secrets — scoped key + BYOK vendor keys, fail-closed.
- Lifecycle —
run/stop/status, durable local state.
MUST NOT: reimplement the loop/mail/memory (base+runtime own it), be runtime-specific, hold platform secrets beyond the scoped key, or fork from the in-container path.
Three orthogonal axes
- runtime —
claude-code(native adapter) ·openclaw(generic adapter) · (future) MCP escape-hatch. Selected with--runtime. - compute — wherever the CLI runs (this box, an EC2, the container).
- LLM provider — resolved by the adapter from
config.yaml's provider registry SSOT (--modelonly picks the model id; the CLI never hardcodes base_urls or auth routing).
Architecture (what runs where)
molecules-workspace run --runtime claude-code
|
| (1) install pinned backend CLI (npm i -g @anthropic-ai/claude-code)
| (2) stage template adapter -> ~/.molecules-workspace/adapters/claude-code/
| adapter.py + claude_sdk_executor.py + config.yaml + __init__.py
| (3) render config.yaml (model override only) -> <configs_dir>/config.yaml
| (4) assemble env: WORKSPACE_ID, PLATFORM_URL, WORKSPACE_CONFIG_PATH,
| CONFIGS_DIR, ADAPTER_MODULE=adapter, PYTHONPATH(+runtime src),
| PLUGINS_DIR, MOLECULE_WORKSPACE_TOKEN + BYOK keys
v
python -c "from molecule_runtime.main import main_sync; main_sync()" <-- the base loop
|
+-- preflight (adapter importable, name() == config.runtime)
+-- get_adapter() -> import $ADAPTER_MODULE -> Adapter
+-- adapter.setup() (provider/auth resolution, plugin install)
+-- adapter.create_executor() (ClaudeSDKExecutor; SDK spawns `claude`)
+-- register_with_platform / heartbeat loop (mesh enroll + liveness)
+-- uvicorn A2A server + idle self-fire loop
config.yaml is the SSOT for providers/models/prompt_files — staged verbatim
(only model: is patched when --model is given). The adapter derives
ANTHROPIC_BASE_URL, vendor-key projection, etc. from it at boot.
Install
From the Molecules registry (tokenless, any machine)
The CLI and its runtime are published to the Molecules package registry. They are
not on public PyPI, so install with PyPI as the primary resolver and our
registry as an extra index (this resolves the two private packages while all
public deps still come from PyPI — do not use --index-url alone, our
registry is a partial mirror, not a PyPI proxy):
pip install "molecules-workspace[runtime]" \
--extra-index-url https://package.moleculesai.app/api/packages/molecule-ai/pypi/simple/
molecules-workspace --help
No credentials required — anonymous read is enabled for the molecule-ai
registry. The [runtime] extra pulls molecules-workspace-runtime (the OSS
base loop this CLI boots), whose own wheel dependencies cover everything needed
to boot the loop for the openclaw runtime. For the claude-code runtime you
additionally need the Python SDK backend (the claude CLI itself is
auto-installed by run via npm):
pip install "claude-agent-sdk>=0.1.58"
One-line installer (PATH-correct, no manual editing). Prefers pipx; falls
back to an isolated venv and, either way, ensures both molecules and
molecules-workspace resolve by name from a clean shell. The advertised entry
points are the short vanity URLs (identical to the installer script headers):
# Unix / macOS
curl -fsSL https://package.moleculesai.app/install.sh | sh
# Windows (persists PATH via the .NET API, not setx; broadcasts WM_SETTINGCHANGE)
irm https://package.moleculesai.app/install.ps1 | iex
Those short forms resolve via a one-line Cloudflare edge redirect (302) onto
the non-rotting raw script on main (deliberately not pinned to a tag, so it
never goes stale):
package.moleculesai.app/install.sh -> .../molecule-workspace-cli/raw/branch/main/install.sh
package.moleculesai.app/install.ps1 -> .../molecule-workspace-cli/raw/branch/main/install.ps1
Edge rule (reference — deployed and serving).
package.moleculesai.appis fronted by a token-based Cloudflare tunnel (molecule-package-tunnel) that routes straight to Gitea — it bypasses themolecule-git-guardnginx — so the redirect cannot live in any local reverse proxy; it lives at the Cloudflare edge as one Single Redirect rule on zonemoleculesai.app(recorded here so it can be re-created exactly if the zone is ever rebuilt):
- When (custom filter expression):
(http.host eq "package.moleculesai.app" and (http.request.uri.path eq "/install.sh" or http.request.uri.path eq "/install.ps1"))- Then: URL redirect -> Dynamic, status 302, target URL expression:
concat("https://package.moleculesai.app/molecule-ai/molecule-workspace-cli/raw/branch/main", http.request.uri.path)The one rule serves both files. It matches only those two exact paths on the package host, so
/api/packages/**(the PyPI registry) and everyraw/**path are untouched. Cloudflare access policy is managed outside this repository; use the dashboard or the current domain-authenticated runbook rather than a retired operator-host procedure.
Alternatives — the same non-rotting branch/main raw URL the redirect
points at, an immutable tag pin, or the in-repo copies:
curl -fsSL https://package.moleculesai.app/molecule-ai/molecule-workspace-cli/raw/branch/main/install.sh | sh
# immutable pin: .../raw/tag/cli-v0.3.0/install.sh # in-repo: sh ./install.sh / pwsh ./install.ps1
Both console scripts are created by pyproject (molecules is primary;
molecules-workspace is a back-compat alias).
Re-running the installer is safe (idempotent upgrade). Piping the one-liner
again over an existing install cleanly UPGRADES it in place — it detects the
existing pipx app and runs pipx upgrade (never pipx install --force, which
under the uv backend fails with "a virtual environment already exists … not
created in this session"). The venv fallback likewise reuses and upgrades an
existing venv instead of erroring.
Updating
Once installed, self-update from the registry without re-fetching the installer:
molecules update # detect the install method (pipx / venv / pip) and
# upgrade to the latest registry version; prints
# before -> after (or "already at the latest version")
molecules update --check # report the available version without upgrading
update uses the same robust upgrade path as the installer, so it never hits
the pipx/uv "venv already exists" failure. The latest version is read from the
same package index the installer uses (MOLECULES_REGISTRY_SIMPLE, default
package.moleculesai.app) — nothing is hardcoded.
On the common commands (molecules, run, login, whoami, status) the CLI
also prints a one-line "molecules <new> available — run molecules update"
nudge when a newer version exists. It is best-effort (never blocks or slows
the command), cached (probes the registry at most once/day), and
opt-out via --no-update-check or MOLECULES_NO_UPDATE_CHECK=1 (--offline
disables it too).
From a local checkout (dev / editable)
# 1. the CLI
pip install -e D:/MoleculesAI/molecule-workspace-cli
# 2. the runtime it wraps (ONE source shared with the container). Editable:
pip install -e D:/MoleculesAI/molecule-ai-workspace-runtime
# ...or set MOLECULE_RUNTIME_SRC=<checkout> and the CLI adds it to PYTHONPATH.
# 3. deps needed to actually BOOT the loop for claude-code (the backend `claude`
# CLI itself is auto-installed by `run` via npm; these are the Python deps).
# A partial ambient install surfaces as a boot crash (e.g. sse_starlette
# missing) or a degraded adapter (claude_agent_sdk missing).
pip install -r D:/MoleculesAI/molecule-workspace-cli/requirements-runtime.txt
The CLI discovers the runtime checkout + the template repo automatically under
D:/MoleculesAI (or ~/MoleculesAI / cwd). Override with MOLECULE_RUNTIME_SRC
and MOLECULE_TEMPLATES_DIR.
Usage — the codex-simple path
The whole flow is three steps: install → molecules login → molecules.
install(above) — putsmoleculesonPATH.molecules login— one scoped mesh identity and the provider BYOK cred.molecules— bare; boots the workspace (LIVE SSOT picker on first run).
Endpoint (zero-config). The CLI's default CP/registry endpoint is
https://api.moleculesai.app (the prod public API — live, serving both the
/cp/runtimes catalog and the /cp/cli/auth/* browser-OAuth login flow), so
molecules login / molecules resolve the runtime & provider catalog with
no --platform-url needed. Env still wins: set MOLECULE_CP_URL /
PLATFORM_URL or pass --platform-url to override.
Compatibility safety net. Production serves the CLI login endpoints. If an older resolved default lacks them, a bare
molecules loginmay retry the configured OAuth-capable endpoint and says so — never a silent 404. Explicit CP selections do not redirect.MOLECULES_LOGIN_CP_URLoverrides the retry target; set it to""to disable the retry and get an actionable error.
# 1. one command for BOTH the scoped mesh identity AND the provider BYOK cred.
# Delegates to native runtime auth where it exists (`codex login`, `claude`
# /login); else prompts (hidden) for the provider's key. Creds land 0600
# (+ icacls on Windows).
molecules login --platform-url https://<tenant>
# 2. just run. Bare `molecules` resolves a selection in this order:
# CLI flag > saved selection (state.json) > LIVE interactive picker
# The picker is sourced FRESH from the SSOT every time (never a baked-in
# list): runtime -> provider (READY vs "needs <VAR>") -> model -> sandbox.
molecules
# subsequent bare `molecules` reuses the saved selection. Re-pick any time:
molecules config
molecules config --reset # clear the saved selection
# non-interactive scripting overrides (persist unless --once):
molecules --runtime codex --provider minimax-token-plan --model gpt-5.5
molecules --sandbox container # docker-isolate: only the workspace is writable
molecules --once --runtime claude-code # one-shot, don't save
# 3. lifecycle (isolation-aware: pid OR container)
molecules status
molecules stop
# 4. keep current — self-update to the latest registry version (see "Updating").
molecules update # upgrade in place; prints before -> after
molecules update --check # just report the available version, do not upgrade
The old explicit verbs are unchanged and still work:
molecules enroll --platform-url https://<tenant> --workspace-id <id> --token <one-time>
molecules run --runtime claude-code --platform-url https://<tenant> \
--model claude-sonnet-4-6 # else template default
# --sandbox {none,container} --detach --no-launch --skip-install
The SSOT (nothing hardcoded)
The runtime/provider/model catalog is fetched live on every picker invocation:
- Primary —
GET {MOLECULE_CP_URL}/cp/runtimes+GET {MOLECULE_CP_URL}/cp/llm-providers?runtime=<name>(thin read-only CP projections;MOLECULE_CP_URLelse derived fromPLATFORM_URL). --offlineonly — the localworkspace-configs-templatesregistry, projected to the same shape; if that directory is unavailable, the last-known cache from a prior successful live fetch. Offline mode accepts that cache regardless of age, so it is a recovery snapshot rather than a freshness guarantee. The local templates directory is the same source CP embeds; it is not a list baked into the CLI.
If none is reachable, the picker raises a clear typed error naming the CP URL and stops — it never falls back to a hardcoded runtime/provider list.
Sandbox (--sandbox)
none(default) — in-process child; full host access. Byte-for-byte the behavior that shipped before this flag existed.container—docker runthe runtime's own image (workspace-template-<runtime>): only the workspace dir is writable, the rendered config is read-only, secrets arrive via a locked--env-file(never argv), and native-OAuth stores (~/.codex) are bind-mounted read-only rather than copied. Needs Docker; the image ref comes from the pin SSOT, not a hardcoded tag.
enroll performs the real POST /registry/register (Step B) and persists
.auth_token + .platform_inbound_secret into the configs dir using the
runtime's own helpers. Creating the external workspace row (Step A,
POST /workspaces with an org-admin credential) is STUBBED — it needs a
live tenant platform + an admin credential the CLI does not hold; enroll
prints the exact curl to run.
Durable state
Everything lives under ~/.molecules-workspace/ (override MOLECULES_WS_HOME):
state.json # current workspace: id, runtime, pid, urls, paths
secrets.env # untrusted-box keys (gitignored, 0600)
workspaces/<id>/configs/ # CONFIGS_DIR: config.yaml, .auth_token, .platform_inbound_secret, .mcp_inbox_cursor
workspaces/<id>/workspace/ # runtime working dir (executor cwd)
workspaces/<id>/run.log # --detach logs
adapters/<runtime>/ # staged template adapter (PYTHONPATH, ADAPTER_MODULE=adapter)
What works / what's stubbed
Works now
molecules login: mesh identity (Step A create-workspace when an org-admin bearer is supplied, + real Step B register + 0600/icacls credential persist) and provider BYOK cred (nativecodex login/claude/login delegation, else a hidden key prompt) in one command.- Bare
molecules+molecules config: LIVE SSOT-sourced picker (runtime → provider → model → sandbox), saved selection reuse, flag overrides. run --runtime claude-code|codex|openclaw: installs the pinned backend, stages the adapter from the template SSOT, renders config, assembles items 1–8, and bootsmolecule_runtime.main.main_syncas a child (foreground or--detach).--sandbox container:docker runthe runtime image, workspace-only writable.stop/statusisolation-aware (PID or container) + durable state.--no-launchdry-run to inspect the assembled environment (or docker argv).
Stubbed / TODO (needs a live endpoint or upstream change)
- Enroll Step A via plain
enrollis still printed-not-executed; usemolecules login --admin-token <bearer>to actually create the workspace. - Advertised A2A URL: without
--workspace-url(a tunnel), push-mode inbound is undialable off-VPC; poll mode is the portable default (url:""). - Residual hardcoded
/workspacewrites in a few runtime call-sites have no env override yet; the CLI best-effort creates the path (writes are fail-soft in the runtime). Aconfigs_dir-style resolver upstream would remove this. - Runtime backend version pin: SSOT today is the npm dist-tag (
latest); pass--runtime-pin <version>to pin (claude-code/openclaw images use latest).
Why a subprocess, when the brief says "import as a library"
The child literally does from molecule_runtime.main import main_sync; main_sync()
— it imports the runtime and calls its entrypoint; it does not re-implement
the loop. A child process is used so stop/status get a clean PID handle and
so the invocation is identical to the container's exec molecule-runtime. Set
MOLECULE_RUNTIME_SRC (or pip-install the runtime) and the child resolves the
same package the container ships.