claude-ceo-assistant 2309f137c1
publish-cli / publish (push) Skipped
meta-ci-advisory / meta (advisory) (push) Successful in 2s
CI / build (push) Successful in 13s
Merge pull request 'chore(rename): point repo-path references at enteros-workspace-cli' (#16) from chore/rename-refs-enteros-workspace-cli into main
2026-10-12 00:43:23 +00:00

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)

  1. Run the base loop — owns nothing of it; boots molecule_runtime standalone.
  2. Drive the selected runtime — --runtime claude-code|openclaw via the ADAPTER_MODULE + module-level Adapter indirection (no runtime→class table; the runtime string only cross-checks adapter.name()).
  3. Enroll + identity — one scoped, revocable key → first-class mesh peer.
  4. Wire mail transport — PLATFORM_URL (+ optional advertised A2A URL).
  5. Untrusted-box secrets — scoped key + BYOK vendor keys, fail-closed.
  6. 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 (--model only 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.app is fronted by a token-based Cloudflare tunnel (molecule-package-tunnel) that routes straight to Gitea — it bypasses the molecule-git-guard nginx — so the redirect cannot live in any local reverse proxy; it lives at the Cloudflare edge as one Single Redirect rule on zone moleculesai.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 every raw/** 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.

  1. install (above) — puts molecules on PATH.
  2. molecules login — one scoped mesh identity and the provider BYOK cred.
  3. 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 login may retry the configured OAuth-capable endpoint and says so — never a silent 404. Explicit CP selections do not redirect. MOLECULES_LOGIN_CP_URL overrides 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:

  1. Primary — GET {MOLECULE_CP_URL}/cp/runtimes + GET {MOLECULE_CP_URL}/cp/llm-providers?runtime=<name> (thin read-only CP projections; MOLECULE_CP_URL else derived from PLATFORM_URL).
  2. --offline only — the local workspace-configs-templates registry, 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 run the 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 (native codex 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 boots molecule_runtime.main.main_sync as a child (foreground or --detach).
  • --sandbox container: docker run the runtime image, workspace-only writable.
  • stop / status isolation-aware (PID or container) + durable state.
  • --no-launch dry-run to inspect the assembled environment (or docker argv).

Stubbed / TODO (needs a live endpoint or upstream change)

  • Enroll Step A via plain enroll is still printed-not-executed; use molecules 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 /workspace writes 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). A configs_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.

S
Description
Molecules Workspace CLI — run a Molecules workspace standalone on any runtime; thin wrapper over molecule_runtime
Readme
368 KiB
Languages
Python 95.7%
PowerShell 2.2%
Shell 2.1%