Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55b7d8c26e | |||
| d0c5611e8b | |||
| 6265ce5ec1 |
@@ -7,7 +7,6 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -8,6 +8,27 @@ Entries are published daily at 23:50 UTC.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-13
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- **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))
|
||||
|
||||
### 🔧 Fixes
|
||||
|
||||
- **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))
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **CWE-22: Path traversal regression in org template import fixed**: a regression removed the path-traversal guard from `createWorkspaceTree` in `org_import.go`, which could allow a malicious org YAML with `filesDir: "../../../etc"` to read arbitrary server files. The fix replaces the unprotected `parseEnvFile` calls with `loadWorkspaceEnv` which applies `resolveInsideRoot` validation before accessing any path. (`molecule-core` [#810](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/810))
|
||||
|
||||
### 🧹 Internal
|
||||
|
||||
- **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. (`molecule-core` [#781](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/781))
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-12
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
@@ -25,7 +25,7 @@ npx @molecule-ai/mcp-server@1.0.0
|
||||
"command": "npx",
|
||||
"args": ["@molecule-ai/mcp-server@1.0.0"],
|
||||
"env": {
|
||||
"MOLECULE_URL": "http://localhost:8080"
|
||||
"MOLECULE_API_URL": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,10 @@ npx @molecule-ai/mcp-server@1.0.0
|
||||
**Pin the package version.** The examples above use `@1.0.0` — always specify an exact version and omit the `-y` flag. An unpinned `npx -y @molecule-ai/mcp-server` (no version) silently installs whatever npm serves on the next restart; if the package is ever compromised, it runs with your full MCP client permissions. Check [npm](https://www.npmjs.com/package/@molecule-ai/mcp-server) for the latest stable release before upgrading.
|
||||
</Callout>
|
||||
|
||||
For SaaS deployments, set `MOLECULE_URL` to your tenant URL:
|
||||
For SaaS deployments, set `MOLECULE_API_URL` to your tenant URL:
|
||||
|
||||
```json
|
||||
"MOLECULE_URL": "https://your-org.moleculesai.app"
|
||||
"MOLECULE_API_URL": "https://your-org.moleculesai.app"
|
||||
```
|
||||
|
||||
### Verify
|
||||
@@ -151,12 +151,14 @@ The MCP server exposes tools across these categories:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `MOLECULE_URL` | `http://localhost:8080` | Platform API URL |
|
||||
| `MOLECULE_API_URL` | `http://localhost:8080` | Platform API base URL |
|
||||
| `MOLECULE_API_KEY` | — | API key for platform authentication |
|
||||
| `MCP_SERVER_PORT` | `3000` | Port (for HTTP/SSE transport) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Fix |
|
||||
|---|---|
|
||||
| Connection refused | Check `MOLECULE_URL` points to running platform |
|
||||
| Connection refused | Check `MOLECULE_API_URL` points to running platform |
|
||||
| 401 Unauthorized | Token expired or revoked — create a new one |
|
||||
| Tools not showing | Run `npx @molecule-ai/mcp-server@1.0.0` standalone to check errors |
|
||||
|
||||
@@ -9,6 +9,28 @@ This page documents security fixes shipped in the Molecule AI platform. Each ent
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-13 — CWE-22: Path Traversal Regression in `org_import.go` (Resolved)
|
||||
|
||||
**Severity:** Critical (CWE-22)
|
||||
**PR:** [#810](https://git.moleculesai.app/molecule-ai/molecule-core/pull/810)
|
||||
**Affected:** `workspace-server/internal/handlers/org_import.go` — `createWorkspaceTree`
|
||||
|
||||
### Vulnerability
|
||||
|
||||
A regression removed the `resolveInsideRoot` path-traversal guard from `createWorkspaceTree` at `org_import.go:494`. The function called `parseEnvFile(filepath.Join(orgBaseDir, ws.FilesDir, ".env"))` without validating that `ws.FilesDir` resolved inside `orgBaseDir`.
|
||||
|
||||
An attacker who could submit a malicious org YAML with `filesDir: "../../../etc"` could cause the platform to read arbitrary files accessible to the server process via the `.env` loading path.
|
||||
|
||||
### Fix
|
||||
|
||||
Replaced the two raw `parseEnvFile` calls with `loadWorkspaceEnv(orgBaseDir, ws.FilesDir)`, which applies `resolveInsideRoot` internally before joining paths. This restores the guard that was present before the regression was introduced.
|
||||
|
||||
### User-facing summary
|
||||
|
||||
The org template import endpoint now validates all workspace file paths before accessing them. Attempts to access files outside the designated org directory return an error and are never processed.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-20 — CWE-22: Path Traversal in `copyFilesToContainer`
|
||||
|
||||
**Severity:** High (CWE-22)
|
||||
|
||||
@@ -7,3 +7,19 @@ description: Security guides, advisories, and coverage reports for the Molecule
|
||||
|
||||
- [SAFE-MCP Security Advisory (2026-04-17)](/docs/security/safe-mcp-advisory) —
|
||||
Three HIGH-severity findings for self-hosted operators
|
||||
- [OWASP Agentic Top 10 (2026-04-28)](/docs/security/owasp-agentic-top-10) —
|
||||
Risk framework for LLM-agent systems; covers goal misalignment, data exfiltration,
|
||||
privilege escalation, and six additional agent-specific threats
|
||||
- [Security Changelog](/docs/security/changelog) —
|
||||
Record of all security findings, fixes, and advisory publications
|
||||
|
||||
## Severity levels
|
||||
|
||||
All advisories follow this classification:
|
||||
|
||||
| Level | Meaning |
|
||||
|---|---|
|
||||
| **CRITICAL** | Active exploitation confirmed; patch immediately |
|
||||
| **HIGH** | Proof-of-concept or significant attack path; remediate within 48h |
|
||||
| **MEDIUM** | Moderate risk; remediate within 30 days |
|
||||
| **LOW** | Minor risk; address in next release cycle |
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
title: Self-Hosted Workspace Deployment with Docker
|
||||
---
|
||||
|
||||
# Self-Hosted Workspace Deployment with Docker
|
||||
|
||||
This guide covers running a Molecule AI workspace agent as a Docker container on a self-hosted server or VM. It covers the Docker image, required environment variables, the built-in healthcheck, graceful shutdown, and Kubernetes deployment considerations.
|
||||
|
||||
> **Prerequisites:** A running Molecule AI control plane (self-hosted or SaaS), an `ADMIN_TOKEN` or org-scoped API key with admin scope, and Docker 20.10+ on the host.
|
||||
|
||||
## How the workspace container works
|
||||
|
||||
The Molecule AI workspace Dockerfile includes:
|
||||
|
||||
- A uvicorn server on port 8000 (configurable via `PORT`)
|
||||
- A healthcheck endpoint at `/.well-known/agent-card.json` (used by Docker and Kubernetes probes)
|
||||
- Graceful SIGTERM handling via uvicorn — the heartbeat loop and adapter tasks shut down cleanly
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Docker host (your VM / bare metal) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ workspace container │ │
|
||||
│ │ │ │
|
||||
│ │ uvicorn (port 8000) │ │
|
||||
│ │ └─ /.well-known/agent-card.json ← HEALTHCHECK │ │
|
||||
│ │ │ │
|
||||
│ │ heartbeat loop + A2A agent │ │
|
||||
│ └──────────────┬──────────────────────┘ │
|
||||
│ │ │
|
||||
│ host.docker.internal:8080 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ Molecule AI control plane │ │
|
||||
│ │ (platform on port 8080) │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Step 1: Create an external workspace
|
||||
|
||||
First register the workspace as an external (self-managed) agent on the platform.
|
||||
|
||||
```bash
|
||||
ADMIN_TOKEN="your-admin-token"
|
||||
PLATFORM_URL="https://platform.moleculesai.app" # or http://localhost:8080 for local dev
|
||||
WORKSPACE=$(curl -s -X POST "${PLATFORM_URL}/workspaces" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "self-hosted-agent", "runtime": "external"}')
|
||||
|
||||
WORKSPACE_ID=$(echo "$WORKSPACE" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
||||
echo "Workspace ID: $WORKSPACE_ID"
|
||||
```
|
||||
|
||||
Save the returned `WORKSPACE_ID`. The workspace agent obtains its bearer token automatically during its first registration with the platform.
|
||||
|
||||
## Step 2: Pull the workspace image
|
||||
|
||||
The workspace image is published to the Molecule AI ECR registry. Contact your platform administrator for the registry prefix and credentials, then log in:
|
||||
|
||||
```bash
|
||||
aws ecr get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin "${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com"
|
||||
|
||||
docker pull "${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com/molecule-workspace:latest"
|
||||
```
|
||||
|
||||
## Step 3: Configure environment variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PLATFORM_URL` | `http://localhost:8080` | Platform API URL. Inside a Docker container, use `http://host.docker.internal:8080` to reach the platform on the host machine. |
|
||||
| `WORKSPACE_ID` | — | Workspace ID from Step 1 (required; no default) |
|
||||
| `PORT` | `8000` | Agent server port. Must match `containerPort` in Kubernetes and the port mapped with `-p` in Docker. |
|
||||
|
||||
## Step 4: Run the container
|
||||
|
||||
### Docker (standalone)
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name molecule-workspace \
|
||||
-p 8000:8000 \
|
||||
-e PLATFORM_URL="http://host.docker.internal:8080" \
|
||||
-e WORKSPACE_ID="your-workspace-id" \
|
||||
-e PORT=8000 \
|
||||
"${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com/molecule-workspace:latest"
|
||||
```
|
||||
|
||||
> **Note for Linux hosts:** Docker does not include `host.docker.internal` by default. On Linux, either add `--add-host=host.docker.internal:host-gateway` to the `docker run` command, or use the host machine's IP address directly (e.g. `http://192.168.1.100:8080`).
|
||||
|
||||
### Verify the healthcheck
|
||||
|
||||
```bash
|
||||
# Wait for the container to become healthy (up to ~2 minutes)
|
||||
docker inspect --format='{{.State.Health.Status}}' molecule-workspace
|
||||
|
||||
# Expected output: healthy
|
||||
# Once healthy, the agent card is reachable:
|
||||
curl -s http://localhost:8000/.well-known/agent-card.json | python3 -m json.tool
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
molecule-workspace:
|
||||
image: "${REGISTRY_PREFIX}.dkr.ecr.us-east-1.amazonaws.com/molecule-workspace:latest"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
PLATFORM_URL: "http://host.docker.internal:8080"
|
||||
WORKSPACE_ID: "your-workspace-id"
|
||||
PORT: "8000"
|
||||
# Linux hosts: add host.docker.internal resolution
|
||||
# extra_hosts:
|
||||
# - "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/.well-known/agent-card.json"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
## Step 5: Graceful shutdown
|
||||
|
||||
When the container receives SIGTERM (e.g. from `docker stop` or Kubernetes pod deletion), the workspace's uvicorn server initiates graceful shutdown: the heartbeat loop stops, active A2A tasks are given a grace period to complete, and any snapshotable state is persisted before the process exits.
|
||||
|
||||
To integrate the heartbeat loop into custom agent code:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os, signal
|
||||
from heartbeat import HeartbeatLoop
|
||||
|
||||
# SIGTERM is handled by the Docker runtime, which sends the signal to the
|
||||
# workspace process. The workspace (via uvicorn) initiates graceful shutdown:
|
||||
# the heartbeat loop is stopped, any active adapter tasks are cancelled, and
|
||||
# in-flight A2A requests are given a grace period to complete.
|
||||
#
|
||||
# For custom integration with the heartbeat loop directly:
|
||||
async def main():
|
||||
heartbeat = HeartbeatLoop(
|
||||
platform_url=os.environ["PLATFORM_URL"],
|
||||
workspace_id=os.environ["WORKSPACE_ID"],
|
||||
)
|
||||
heartbeat.start()
|
||||
try:
|
||||
await asyncio.Event().wait() # keep running
|
||||
finally:
|
||||
await heartbeat.stop()
|
||||
print("Heartbeat loop stopped.")
|
||||
```
|
||||
|
||||
The Docker `stop` command sends SIGTERM and waits up to 10 seconds by default before sending SIGKILL. The healthcheck ensures orchestrators detect an unhealthy container before the SIGTERM timeout.
|
||||
|
||||
## Kubernetes deployment
|
||||
|
||||
For Kubernetes deployments, use the native liveness/readiness probe configuration instead of the Docker HEALTHCHECK:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /.well-known/agent-card.json
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /.well-known/agent-card.json
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
terminationGracePeriodSeconds: 120
|
||||
```
|
||||
|
||||
> **Note:** The Kubernetes `terminationGracePeriodSeconds` should exceed the liveness probe failure threshold so that the probe can register a failure before the pod is killed. With `periodSeconds: 30` and `failureThreshold: 3`, the probe does not register a failure until approximately 120–150s after the container becomes unhealthy. Set `terminationGracePeriodSeconds: 120` or higher.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Container shows `unhealthy` after startup | Platform unreachable from container | Verify `PLATFORM_URL` uses `host.docker.internal` (Docker) or the correct host IP |
|
||||
| `curl: (7) Failed to connect` on healthcheck | Container not fully started | Wait up to 30s; increase `start_period` |
|
||||
| Agent not appearing on canvas | Wrong `WORKSPACE_ID` or expired token | Re-run registration; check platform logs |
|
||||
| `host.docker.internal` not resolved | Linux host without the Docker flag | Use `--add-host=host.docker.internal:host-gateway` or the host's LAN IP |
|
||||
Reference in New Issue
Block a user