Commit Graph

6 Commits

Author SHA1 Message Date
Hongming Wang
6488ba09e7 fix(preflight): downgrade required_env + auth_token failures to warnings
Preflight was hard-failing the workspace boot when required env vars or
legacy auth_token_files were missing, raising SystemExit(1) before
main.py's PR #2756 try/except could mount the not-configured handler.
Result: codex/openclaw workspaces launched without OPENAI_API_KEY were
INVISIBLE — `/.well-known/agent-card.json` never returned 200, the bench
timed out at 600s, canvas had no actionable signal. PR #2756 fixed half
the puzzle (decouple agent-card from adapter.setup() failure); this
fixes the other half (decouple from preflight failure).

Caught by bench-provision-time run 25335853189 on 2026-05-04: codex and
openclaw both timed_out at 609s while claude-code (whose default model
needs no env) hit 86.7s on the same AMI. Hermes hit 147s because hermes
config doesn't declare top-level required_env.

After this change:
- Missing required_env: WARN (operator sees it in boot logs); workspace
  proceeds to adapter.setup() which raises with the same env-name detail;
  PR #2756's try/except mounts the not-configured handler;
  /.well-known/agent-card.json serves 200; JSON-RPC POST / returns
  -32603 "agent not configured" with the env-name in `error.data`.
- Missing auth_token_file (legacy path): same treatment.
- Other preflight failures (runtime adapter not installable, invalid
  A2A port) STAY as fails — those are structural, the workspace truly
  can't run.

Updated 4 existing tests that asserted `report.ok is False` on
required_env / auth_token misses to assert `report.ok is True` and
check `report.warnings` instead. All 31 preflight tests pass; full
suite 1664 pass + 1 unrelated flake on staging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:20:34 -07:00
Hongming Wang
06240ab67b fix(preflight): skip required_env check in MOLECULE_SMOKE_MODE
Boot smoke (#2275) exercises executor.execute() against stub deps
and never hits the real provider, so missing auth env is not a real
blocker. Without this bypass, every adapter that introduces a new
auth env var must be mirrored into molecule-ci's fake-env list — a
maintenance treadmill that just bit hermes-template:

- 2026-05-03 09:47 UTC: hermes publish-image smoke fails on
  HERMES_API_KEY preflight (workflow injects CLAUDE_CODE_OAUTH_TOKEN,
  ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY but not
  HERMES_API_KEY or OPENROUTER_API_KEY). Failed for two cycles
  before being noticed.

The bypass demotes Required-env failures to warnings when
MOLECULE_SMOKE_MODE is truthy, so the unset env stays visible in
the boot log without blocking. Production paths are unchanged
(env unset → fail).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:44:05 -07:00
Hongming Wang
3e5955f04f fix(runtime): explicit empty per-model required_env means "no auth"
Two follow-ups from the independent review of #2538.

preflight.py
============
Today: `if per_model_env: required_env = list(per_model_env)` falls
through on `[]`, so a template entry that says "this model needs no
auth" (`required_env: []` — Ollama, llamafile, self-hosted OpenAI-
compat, anything where the SDK doesn't surface a key) is silently
overridden by the top-level fallback list. The template author cannot
express a zero-auth model without lying about its env requirements.

Fix: key off `"required_env" in entry` (key presence, not truthiness).
Missing key still falls back to top-level — that path is unchanged
and preserves "many templates list name/description per model without
enumerating env vars when auth is identical across the family". Empty
list now wins outright. Comment updated to call out the distinction.

test_preflight.py
=================
Renamed `test_per_model_match_with_no_required_env_falls_back_to_top_level`
to `…_no_required_env_KEY_…` and tightened its docstring to reflect
that it's the missing-KEY case only. Added new
`test_per_model_explicit_empty_required_env_means_no_auth` to pin the
new explicit-empty semantic.

test_config.py
==============
New `test_runtime_config_model_env_wins_over_explicit_yaml`. Pins the
intentional precedence inversion shipped in #2538 with both
MODEL_PROVIDER and runtime_config.model in YAML set — MODEL_PROVIDER
wins. Without this pin a future refactor could quietly restore the
old YAML-wins order and re-introduce Bug B.

77/77 targeted tests pass locally.

Closes #250 (review follow-up). Builds on merged #2538.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:51:01 -07:00
Hongming Wang
97ebd1910a fix(runtime): canvas-picked model wins universally + per-model required_env
Two surgical edits to the molecule-runtime workspace package that fix
Bug B (canvas-picked model silently dropped for templated workspaces)
and Bug D (preflight rejects valid auth for non-default models),
universally for every adapter.

Bug B — canvas-picked model dropped (config.py)
================================================
Before: load_config resolved runtime_config.model as
  runtime_raw.get("model") or model
which means a template's `runtime_config.model: sonnet` always wins
over the canvas-picked MODEL_PROVIDER env var. Surfaced 2026-05-02
during MiniMax E2E — picking MiniMax-M2.7 in canvas, server plumbed
MODEL_PROVIDER=MiniMax-M2.7 correctly, but the workspace booted with
sonnet because the template's verbatim config.yaml won.

After:
  os.environ.get("MODEL_PROVIDER") or runtime_raw.get("model") or model

Centralising in load_config means EVERY adapter (claude-code, hermes,
codex, langgraph, future ones) gets canvas-picked-model passthrough
for free — no per-adapter env-reading code required.

Bug D — preflight per-model required_env (preflight.py)
========================================================
Before: preflight read the top-level required_env list, which
declares the auth needed by the *default* model. A template like
claude-code-default declares CLAUDE_CODE_OAUTH_TOKEN at the top
level. When a user picked MiniMax instead and only set
MINIMAX_API_KEY, preflight rejected the workspace with
"missing CLAUDE_CODE_OAUTH_TOKEN" and the workspace crash-looped
despite the user having satisfied the picked model's actual auth.

After: when runtime_config.models[] declares per-entry required_env,
preflight matches the picked model id (case-insensitive) and uses
that entry's required_env outright instead of the top-level list.
REPLACE semantics, not union — different models have *different*
auth paths (OAuth vs API key vs third-party provider key); unioning
would re-introduce the very crash-loop this fix closes.

Surface enabling both fixes (config.py)
========================================
RuntimeConfig now carries `models: list[dict]` so the canvas Model
dropdown source flows through to preflight without forcing the
parser schema to grow. Malformed entries are silently dropped to
match the rest of the lenient parser.

Tests
=====
- workspace/tests/test_preflight.py: 9 new tests covering the
  per-model lookup (case-insensitive, REPLACE not union, fallback
  to top-level when no models[] or no match, multi-entry, malformed
  entries dropped, etc.)
- workspace/tests/test_config.py: existing 48 pass; field
  initialisation already covered by parser tests.
- All 75 targeted tests pass locally; CI runs the full suite
  including coverage gate.

Closes part of #246. Sibling PR opens against
molecule-ai-workspace-template-claude-code for per-template
defensive fixes + boot debug logging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:36:24 -07:00
Hongming Wang
7dba700ac3 feat(preflight): replace SUPPORTED_RUNTIMES static list with adapter discovery
Closes task #123 — last piece of #87 cleanup.

Pre-fix: workspace/preflight.py:11 hardcoded a tuple of "supported"
runtime names (claude-code, codex, ollama, langgraph, etc.). Every
new template repo required a code change in molecule-runtime to be
recognized — direct violation of the universal-runtime principle
(#87) where adapters declare themselves and the runtime stays generic.

Post-fix: discovery-based validation via the same ADAPTER_MODULE env
var that production load paths already consult
(workspace/adapters/__init__.py:get_adapter). Distinguished failure
modes so operator messages are concrete:

  - ADAPTER_MODULE unset → "no adapter installed; set the env var"
  - ADAPTER_MODULE set but module won't import → import error type +
    message
  - module imports but no Adapter class → "convention violation, add
    `Adapter = YourClass`"
  - Adapter.name() raises → caught with operator message
  - Adapter.name() returns non-string → contract violation message
  - Adapter.name() doesn't match config.runtime → drift WARNING (not
    fatal; the adapter wins in production, config.yaml is just
    documentation)

The drift case is the one behavioral change worth calling out: the
prior static-list path would have hard-failed config.runtime values
not in the allowlist. With discovery, an unknown runtime in
config.yaml is just a documentation drift — the adapter that's
actually installed runs regardless. Operator gets a warning naming
both the configured and installed names so they can fix whichever
is stale.

Tests:
  - Replaces the obsolete "static list pass/fail" tests with 6 new
    cases covering each distinguished failure mode, plus a positive
    test for the adapter-matches-config happy path
  - Adds an autouse `_default_langgraph_adapter` fixture that
    pre-installs a fake adapter via sys.modules monkey-patching, so
    existing tests building default WorkspaceConfig (runtime="langgraph")
    inherit a valid adapter without each test setting ADAPTER_MODULE
  - Failure-mode tests opt out of the default fixture via
    @pytest.mark.no_default_adapter (registered in pytest.ini)
  - Sentinel pattern (`_UNSET = object()`) for `name_returns` so None
    is a passable test value (otherwise `is not None` would skip the
    None branch — exact bug the sentinel avoids)

Verification:
  - 22/22 preflight tests pass (was 16; +6 new failure-path tests)
  - 1256/1256 workspace pytest pass (was 1251; +5 net)
  - No production code path other than preflight changed

Source: 2026-04-27 #87 cleanup audit after PR #2154 (wedge extraction).
This change is independent of the cli_executor.py template moves
(task #122) — completes one of the two remaining cleanup items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:44:51 -07:00
Hongming Wang
479a027e4b chore: open-source restructure — rename dirs, remove internal files, scrub secrets
Renames:
- platform/ → workspace-server/ (Go module path stays as "platform" for
  external dep compat — will update after plugin module republish)
- workspace-template/ → workspace/

Removed (moved to separate repos or deleted):
- PLAN.md — internal roadmap (move to private project board)
- HANDOFF.md, AGENTS.md — one-time internal session docs
- .claude/ — gitignored entirely (local agent config)
- infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy
- org-templates/molecule-dev/ → standalone template repo
- .mcp-eval/ → molecule-mcp-server repo
- test-results/ — ephemeral, gitignored

Security scrubbing:
- Cloudflare account/zone/KV IDs → placeholders
- Real EC2 IPs → <EC2_IP> in all docs
- CF token prefix, Neon project ID, Fly app names → redacted
- Langfuse dev credentials → parameterized
- Personal runner username/machine name → generic

Community files:
- CONTRIBUTING.md — build, test, branch conventions
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1

All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml,
README, CLAUDE.md updated for new directory names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:24:44 -07:00