Go to file
rabbitblood 0a0f11b41f feat(runtime): auto-detect LLM token type, normalise env on boot
Platform stores per-workspace LLM credentials under a single key
(ANTHROPIC_AUTH_TOKEN in workspace_secrets). But downstream tools
expect different env var names depending on the token type:

  sk-ant-oat01-*  → CLAUDE_CODE_OAUTH_TOKEN  (Claude Code OAuth session)
  sk-ant-api03-*  → ANTHROPIC_API_KEY        (direct Anthropic API)
  sk-cp-*         → ANTHROPIC_AUTH_TOKEN     (proxy: MiniMax, gateways)

Without normalisation, an OAuth token under ANTHROPIC_AUTH_TOKEN gets
sent as a bearer to api.anthropic.com, which responds:

    401 authentication_error: OAuth authentication is currently not
    supported.

This was a platform-wide footgun: anyone rotating LLM keys had to
know the exact env var for each token type, AND make sure stale
overrides were cleared, AND set ANTHROPIC_BASE_URL correctly for
proxies (or NOT set for native Claude). Nothing downstream could
help — the SDK just saw the wrong var.

Fix:

- New molecule_runtime/llm_auth.py — normalise_llm_env() mutates
  os.environ (or any dict) to the correct shape based on token
  prefix. Returns a NormalisationResult for logging.
- main.py calls it as step 0, before any adapter/executor import.
  Every adapter (claude-code, langgraph, crewai, autogen, hermes,
  …) benefits automatically — no per-adapter branching needed.
- 11 unit tests covering all prefix paths, edge cases, and the
  "operator deliberately set CLAUDE_CODE_OAUTH_TOKEN" precedence
  rule.

Operationally: this means operators can keep using one
ANTHROPIC_AUTH_TOKEN slot in platform settings and just paste
whatever token the agent needs. No env-var-name awareness required.

Tested locally: 11/11 new tests pass. 83 other tests unchanged
(pre-existing failures on staging are all unrelated:
test_workspace_id_validation, test_a2a_mcp_server RBAC, the
test_imports.main module-walker — same signature as on staging
HEAD before this PR).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:41:47 -07:00
.github/workflows fix(CI): remove conflicting bandit flags from security linter step 2026-04-21 00:58:43 +00:00
molecule_runtime feat(runtime): auto-detect LLM token type, normalise env on boot 2026-04-23 10:41:47 -07:00
tests feat(runtime): auto-detect LLM token type, normalise env on boot 2026-04-23 10:41:47 -07:00
.gitignore chore: gitignore credentials for molecule-ai-workspace-runtime 2026-04-16 09:18:48 -07:00
pyproject.toml chore: bump 0.1.8 — executor_helpers phantom-busy fix confirmed in tree 2026-04-21 07:16:47 -07:00
README.md feat: initial release of molecule-ai-workspace-runtime 0.1.0 2026-04-16 04:26:06 -07:00

molecule-ai-workspace-runtime

Shared Python runtime infrastructure for all Molecule AI agent adapters.

This package provides the core machinery that every Molecule AI workspace container needs:

  • A2A server — Registers with the platform, heartbeats, serves A2A JSON-RPC
  • Adapter interfaceBaseAdapter / AdapterConfig / SetupResult
  • Built-in tools — delegation, memory, approvals, sandbox, telemetry
  • Skill loader — loads and hot-reloads skill modules from /configs/skills/
  • Plugin system — per-workspace + shared plugin discovery and install
  • Config / preflight — YAML config loading with validation

Installation

pip install molecule-ai-workspace-runtime

Adapter Discovery

The runtime discovers adapters in two ways:

  1. ADAPTER_MODULE env var (standalone adapter repos):

    ADAPTER_MODULE=my_adapter molecule-runtime
    

    The module must export an Adapter class extending BaseAdapter.

  2. Built-in subdirectory scan (monorepo local dev): Scans molecule_runtime/adapters/ subdirectories for Adapter classes.

Writing an Adapter

from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig
from a2a.server.agent_execution import AgentExecutor

class Adapter(BaseAdapter):
    @staticmethod
    def name() -> str:
        return "my-runtime"

    @staticmethod
    def display_name() -> str:
        return "My Runtime"

    @staticmethod
    def description() -> str:
        return "My custom agent runtime"

    async def setup(self, config: AdapterConfig) -> None:
        result = await self._common_setup(config)
        # Store result attributes for create_executor

    async def create_executor(self, config: AdapterConfig) -> AgentExecutor:
        # Return an AgentExecutor instance
        ...

Set ADAPTER_MODULE=my_package.adapter and run molecule-runtime.

License

BSL-1.1 — see LICENSE for details.