forked from molecule-ai/molecule-core
The runtime persists per-workspace state (`.auth_token`,
`.platform_inbound_secret`, `.mcp_inbox_cursor`) under `/configs` —
the workspace-EC2 mount path. Inside a container that's writable,
agent-owned. Outside a container, `/configs` either doesn't exist or
isn't writable by an unprivileged user.
The default broke the external-runtime path (`pip install
molecule-ai-workspace-runtime` + `molecule-mcp` on a Mac/Linux
laptop). First heartbeat tries to persist `.platform_inbound_secret`
and crashes:
[Errno 30] Read-only file system: '/configs'
The heartbeat thread logs and dies. Workspace flips offline within
a minute. Operator sees no actionable error.
Adds workspace/configs_dir.py — single resolution point with a tiered
fallback:
1. CONFIGS_DIR env var, if set — explicit operator override
(preserves existing tests + custom deployments verbatim).
2. /configs — if it exists AND is writable. In-container default;
unchanged behavior for every prod workspace.
3. ~/.molecule-workspace — created with mode 0700 so per-file 0600
perms aren't undermined by a world-readable parent.
Migrates the four readers (platform_auth, platform_inbound_auth,
mcp_cli, inbox) to call configs_dir.resolve() instead of
inlining `Path(os.environ.get("CONFIGS_DIR", "/configs"))`.
Existing tests that assert the old `/configs`-as-default contract
updated to assert the new contract: when CONFIGS_DIR is unset, path
resolves to a writable location — `/configs` if present, fallback
otherwise. Tests skip the fallback branch on hosts that DO have a
writable `/configs` (CI containers).
Verified the original repro is fixed: with no CONFIGS_DIR set on
macOS, configs_dir.resolve() returns ~/.molecule-workspace, the dir
exists, and writes succeed.
Test suite: 1454 passed, 3 skipped, 2 xfailed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
62 lines
2.4 KiB
Python
62 lines
2.4 KiB
Python
"""Resolve the configs directory used by the workspace runtime.
|
|
|
|
The runtime persists per-workspace state to a single directory:
|
|
``.auth_token`` (platform_auth), ``.platform_inbound_secret``
|
|
(platform_inbound_auth), ``.mcp_inbox_cursor`` (inbox). Inside a
|
|
workspace EC2 container that directory is ``/configs`` — a tmpfs/EBS
|
|
mount owned by the agent user, populated by the provisioner before
|
|
runtime boot.
|
|
|
|
Outside a container — operators running ``molecule-mcp`` on a laptop
|
|
for the external-runtime path — ``/configs`` doesn't exist (or, if it
|
|
does, isn't writable by an unprivileged user). The default would
|
|
silently fail on the first heartbeat: ``.platform_inbound_secret``
|
|
write hits ``Read-only file system: '/configs'``, the heartbeat thread
|
|
logs and dies, the workspace flips offline within a minute. The
|
|
operator sees no actionable error.
|
|
|
|
This module is the single resolution point. Resolution order:
|
|
|
|
1. ``CONFIGS_DIR`` env var, if set — explicit operator override.
|
|
2. ``/configs`` — used iff the path exists AND is writable. This
|
|
preserves the in-container default for every existing deployment.
|
|
3. ``$HOME/.molecule-workspace`` — the non-container fallback,
|
|
created with mode 0700 so per-file 0600 perms aren't undermined
|
|
by a world-readable parent.
|
|
|
|
Not cached: callers (heartbeat thread, MCP tools) hit this at most a
|
|
few times per second; reading the env var + one ``stat()`` call is
|
|
cheap, and the existing call sites read ``os.environ`` live so tests
|
|
that monkeypatch ``CONFIGS_DIR`` between cases keep working.
|
|
|
|
Issue: Molecule-AI/molecule-core#2458.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
def resolve() -> Path:
|
|
"""Return the configs directory, creating the home fallback if needed."""
|
|
explicit = os.environ.get("CONFIGS_DIR", "").strip()
|
|
if explicit:
|
|
path = Path(explicit)
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
in_container = Path("/configs")
|
|
if in_container.exists() and os.access(str(in_container), os.W_OK):
|
|
return in_container
|
|
|
|
home_path = Path.home() / ".molecule-workspace"
|
|
home_path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
return home_path
|
|
|
|
|
|
def reset_cache() -> None:
|
|
"""No-op kept for API stability; this module is stateless. Tests
|
|
that called reset_cache when the cached prototype was in tree
|
|
keep working without modification."""
|
|
return
|