From 1ccd92e0c8081e735e1671082ae1e3ae21127f83 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 30 Apr 2026 22:48:53 -0700 Subject: [PATCH] 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) --- .../docs/agent-runtime/workspace-runtime.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/content/docs/agent-runtime/workspace-runtime.md b/content/docs/agent-runtime/workspace-runtime.md index fa59694..8b87721 100644 --- a/content/docs/agent-runtime/workspace-runtime.md +++ b/content/docs/agent-runtime/workspace-runtime.md @@ -73,6 +73,58 @@ 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 + +### 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 |