Merge pull request #944 from Molecule-AI/chore/open-source-final-fixes

chore: final open-source cleanup — binary, stale paths, private refs
This commit is contained in:
Hongming Wang 2026-04-18 00:39:12 -07:00 committed by GitHub
commit 2959bde0b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 48 additions and 47 deletions

4
.gitattributes vendored
View File

@ -1,11 +1,11 @@
# Shell scripts must stay LF so they execute inside Linux containers
# when cloned on Windows with core.autocrlf=true.
*.sh text eol=lf
workspace-template/entrypoint.sh text eol=lf
workspace/entrypoint.sh text eol=lf
# Python hook files are invoked by .sh hooks with path substitution;
# CRLF in either the .sh OR the .py file breaks the hook dispatch.
# See Molecule-AI/molecule-core#507 — SessionStart hook failed silently,
# See #507 — SessionStart hook failed silently,
# agents returned "(no response generated)" on every A2A call.
*.py text eol=lf

View File

@ -64,7 +64,7 @@ fi
# 3. Python: No bare except pass (silent swallowing)
# ──────────────────────────────────────────────────────────
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$' | grep 'workspace-template/' || true)
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$' | grep 'workspace/' || true)
if [ -n "$STAGED_PY" ]; then
for f in $STAGED_PY; do
@ -82,7 +82,7 @@ fi
# 4. Go: No string-concatenated SQL
# ──────────────────────────────────────────────────────────
STAGED_GO=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' | grep 'platform/' || true)
STAGED_GO=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' | grep 'workspace-server/' || true)
if [ -n "$STAGED_GO" ]; then
for f in $STAGED_GO; do

View File

@ -42,7 +42,7 @@ jobs:
DIFF=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo ".github/workflows/ci.yml")
echo "platform=$(echo "$DIFF" | grep -qE '^workspace-server/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "canvas=$(echo "$DIFF" | grep -qE '^canvas/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "python=$(echo "$DIFF" | grep -qE '^workspace-template/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "python=$(echo "$DIFF" | grep -qE '^workspace/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "scripts=$(echo "$DIFF" | grep -qE '^tests/e2e/|^scripts/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
platform-build:

6
.gitignore vendored
View File

@ -1,6 +1,6 @@
# Binaries
platform/server
platform/molecli
workspace-server/server
workspace-server/molecli
*.exe
*.out
*.bin
@ -87,7 +87,7 @@ redis_data/
workspace-configs-templates/ws-*
# Local dev cruft — provisioner writes here at runtime; templates live at repo root
platform/workspace-configs-templates/
workspace-server/workspace-configs-templates/
# Codex/Gemini agent skill cache (local only, not authoritative)
.agents/

View File

@ -218,10 +218,10 @@ continues to work as the primary flow (redirect after org creation).
| File | Change |
|------|--------|
| `molecule-controlplane/internal/provisioner/ec2.go` | Remove Cloudflare DNS creation, remove Caddy from user-data |
| `molecule-controlplane/internal/cloudflareapi/dns.go` | Eventually removable (Worker replaces it) |
| `molecule-controlplane/internal/handlers/orgs.go` | Add `GET /cp/orgs/:slug/instance` endpoint |
| New: `infra/cloudflare-worker/` | Worker source + wrangler.toml |
| `the private control-plane repo/internal/provisioner/ec2.go` | Remove Cloudflare DNS creation, remove Caddy from user-data |
| `the private control-plane repo/internal/cloudflareapi/dns.go` | Eventually removable (Worker replaces it) |
| `the private control-plane repo/internal/handlers/orgs.go` | Add `GET /cp/orgs/:slug/instance` endpoint |
| New: `Molecule-AI/molecule-tenant-proxy (separate repo)` | Worker source + wrangler.toml |
| `docs/runbooks/saas-secrets.md` | Add Worker secrets (CF account ID, API token) |
| `.github/workflows/deploy-worker.yml` | CI/CD for Worker deploys |

View File

@ -485,10 +485,10 @@ Merge commit `57a05686`. Noteworthy: saas-foundation / auth-adjacent.
allowlist-bypass, allowlist-is-exact-match.
- CLAUDE.md: test count 740 → 746; new `MOLECULE_ORG_ID` env var documented.
### Paired work — private `molecule-controlplane` repo scaffolded
### Paired work — private `the private control-plane repo` repo scaffolded
(Outside this monorepo; logged here because it anchors the open-core split.)
- Initial commit `1bab493` on new private repo `Molecule-AI/molecule-controlplane`.
- Initial commit `1bab493` on new private repo `Molecule-AI/the private control-plane repo`.
- Migrations 001 (organizations), 002 (org_instances), 003 (org_members).
- HTTP server: `/health`, `/cp/orgs` CRUD, subdomain + `X-Molecule-Org-Slug`
header fallback → `fly-replay: app=<tenant>;instance=<machine_id>` header,

View File

@ -21,7 +21,7 @@ Adds `.github/workflows/publish-platform-image.yml`:
using the built-in `GITHUB_TOKEN`, no extra secrets.
- OCI labels propagate source URL + commit SHA for provenance.
Purpose: pairs with the private `molecule-controlplane` Fly + Neon
Purpose: pairs with the private `the private control-plane repo` Fly + Neon
provisioner (PR #3 there, merged `2e85d5ad`) which reads
`TENANT_IMAGE=ghcr.io/molecule-ai/platform:<tag>` from env and spawns
each tenant Fly Machine from this image.

View File

@ -13,14 +13,14 @@
| Change | Repo | Status |
|--------|------|--------|
| Railway deployment for control plane | molecule-controlplane | Deployed, auto-deploy on push |
| EC2 provisioner for tenants (Postgres + Redis + Platform in Docker) | molecule-controlplane | Deployed |
| EC2 provisioner for workspaces (pip install runtime at boot) | molecule-controlplane | Deployed, 9 min cold start |
| Railway deployment for control plane | the private control-plane repo | Deployed, auto-deploy on push |
| EC2 provisioner for tenants (Postgres + Redis + Platform in Docker) | the private control-plane repo | Deployed |
| EC2 provisioner for workspaces (pip install runtime at boot) | the private control-plane repo | Deployed, 9 min cold start |
| Cloudflare Worker for wildcard subdomain routing | molecule-tenant-proxy (new repo) | Deployed |
| Wildcard DNS `*.moleculesai.app` → Worker | Cloudflare dashboard | Done |
| Per-tenant ADMIN_TOKEN for Worker auth injection | molecule-controlplane | Deployed |
| Auto-updater cron on tenant EC2s (Option B) | molecule-controlplane | Deployed |
| Phase 33.2: stop creating per-tenant DNS records | molecule-controlplane | Deployed |
| Per-tenant ADMIN_TOKEN for Worker auth injection | the private control-plane repo | Deployed |
| Auto-updater cron on tenant EC2s (Option B) | the private control-plane repo | Deployed |
| Phase 33.2: stop creating per-tenant DNS records | the private control-plane repo | Deployed |
| Provisioning status page (progress bar + ETA) | molecule-app | Deployed to Vercel |
| Delete org button with type-to-confirm | molecule-app | Deployed to Vercel |
| Remove admin section from SaaS app | molecule-app | Deployed to Vercel |

View File

@ -1,6 +1,6 @@
# GDPR Art. 17 hard-delete cascade
Operational reference for the "delete my org" flow in `molecule-controlplane`.
Operational reference for the "delete my org" flow in `the private control-plane repo`.
Skim this before replying to an erasure request, answering a DPA (Data
Processing Addendum) audit, or debugging a failed purge.
@ -89,7 +89,7 @@ any purge row is older than 48h without hitting `completed`.
## Testing the cascade
See the test plan in [PR #29](https://github.com/Molecule-AI/molecule-controlplane/pull/29)
See the test plan in [PR #29](https://github.com/Molecule-AI/the private control-plane repo/pull/29)
for the staging checklist. The unit tests cover the orchestrator logic
(happy path, resume-from-step, Stripe failure, no-customer); end-to-end
proof requires a real Stripe test-mode customer + provisioned Fly Machine
@ -102,5 +102,5 @@ because the failure modes that matter are transport errors, not logic.
- `docs/runbooks/admin-auth.md``DELETE /cp/orgs/:slug` is behind
session-cookie auth in controlplane, not the workspace bearer-token
middleware documented there
- `molecule-controlplane/internal/handlers/purge.go` — the orchestrator
- `molecule-controlplane/migrations/006_org_purges.*.sql` — audit schema
- `the private control-plane repo/internal/handlers/purge.go` — the orchestrator
- `the private control-plane repo/migrations/006_org_purges.*.sql` — audit schema

View File

@ -1,5 +1,5 @@
{
"_comment": "TODO: pin refs to release tags (v1.0.0) before first customer deploy for reproducible builds. 'main' is OK while all repos are internal.",
"_comment": "Pin refs to release tags for reproducible builds. 'main' is OK while all repos are internal.",
"version": 1,
"plugins": [
{"name": "browser-automation", "repo": "Molecule-AI/molecule-ai-plugin-browser-automation", "ref": "main"},

View File

@ -9,7 +9,7 @@ This repo uses the standard monorepo testing convention: **unit tests live with
| Go unit + integration (platform, CLI, handlers) | `workspace-server/**/*_test.go` — run with `cd workspace-server && go test -race ./...` |
| TypeScript unit (canvas components, hooks, store) | `canvas/src/**/__tests__/` — run with `cd canvas && npm test -- --run` |
| TypeScript unit (MCP server handlers) | `mcp-server/src/__tests__/` — run with `cd mcp-server && npx jest` |
| Python unit (workspace runtime, adapters) | `workspace-template/tests/` — run with `cd workspace-template && python3 -m pytest` |
| Python unit (workspace runtime, adapters) | `workspace/tests/` — run with `cd workspace && python3 -m pytest` |
| Python unit (SDK: plugin + remote agent) | `sdk/python/tests/` — run with `cd sdk/python && python3 -m pytest` |
| **Cross-component E2E** (spans platform + runtime + HTTP) | `tests/e2e/`**you are here** |

View File

@ -1,10 +1,10 @@
#!/bin/bash
# Full E2E test for Claude Code workspace runtime
# Run from repo root after: docker compose up -d && docker build -t workspace-template:latest workspace-template/
# Run from repo root after: docker compose up -d && docker build -t workspace:latest workspace/
#
# Prerequisites:
# - Platform running on localhost:8080
# - workspace-template:latest image built
# - workspace:latest image built
# - .auth-token in workspace-configs-templates/claude-code-default/
set -euo pipefail

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash
# test_saas_tenant.sh — smoke test a live SaaS tenant through the Cloudflare Worker
#
# Usage: TENANT_SLUG=hongming2 bash tests/e2e/test_saas_tenant.sh
# TENANT_SLUG=hongming2 DIRECT_IP=3.144.193.40 bash tests/e2e/test_saas_tenant.sh
# Usage: TENANT_SLUG=example-org bash tests/e2e/test_saas_tenant.sh
# TENANT_SLUG=example-org DIRECT_IP=<EC2_IP> bash tests/e2e/test_saas_tenant.sh
#
# Tests both Worker-proxied routes and (optionally) direct EC2 access.
# Exits 0 if all critical tests pass, 1 otherwise.

1
workspace-server/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
server

View File

@ -23,7 +23,7 @@
// AdminAuth — any valid workspace bearer token can call it.
//
// 2. Workspace side: a shell credential helper
// (workspace-template/scripts/molecule-git-token-helper.sh) configured
// (workspace/scripts/molecule-git-token-helper.sh) configured
// as the git credential helper. git calls it on every push/fetch;
// it hits this endpoint and emits the fresh token to stdout. A 30-min
// cron also runs `gh auth login --with-token` using the same helper.

View File

@ -2,7 +2,7 @@ package handlers
// Package handlers — MCP bridge for opencode integration (#800, #809, #810).
//
// Exposes the same 8 A2A tools as workspace-template/a2a_mcp_server.py but
// Exposes the same 8 A2A tools as workspace/a2a_mcp_server.py but
// served directly from the platform over HTTP so CLI runtimes running
// OUTSIDE workspace containers (opencode, Claude Code on the developer's
// machine) can participate in the A2A mesh.
@ -96,7 +96,7 @@ func NewMCPHandler(database *sql.DB, broadcaster *events.Broadcaster) *MCPHandle
}
// ─────────────────────────────────────────────────────────────────────────────
// Tool definitions (mirrors workspace-template/a2a_mcp_server.py TOOLS list)
// Tool definitions (mirrors workspace/a2a_mcp_server.py TOOLS list)
// ─────────────────────────────────────────────────────────────────────────────
var mcpAllTools = []mcpTool{

View File

@ -93,7 +93,7 @@ type OrgDefaults struct {
// when both are set.
InitialPromptFile string `yaml:"initial_prompt_file" json:"initial_prompt_file"`
// IdlePrompt / IdleIntervalSeconds are the workspace-default idle-loop
// body and cadence (see workspace-template/heartbeat.py). They were
// body and cadence (see workspace/heartbeat.py). They were
// previously dropped by the org importer because the struct didn't
// declare them — causing live configs to boot without idle_prompts
// even when org.yaml had them. Phase 1 scalability work adds both
@ -539,7 +539,7 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa
// Resolve idle_prompt — same precedence (ws inline → ws file → defaults).
// Inject into config.yaml alongside idle_interval_seconds so the
// workspace's heartbeat loop picks up the idle-reflection cadence on
// boot (see workspace-template/heartbeat.py + config.py).
// boot (see workspace/heartbeat.py + config.py).
idlePrompt, err := resolvePromptRef(ws.IdlePrompt, ws.IdlePromptFile, orgBaseDir, ws.FilesDir)
if err != nil {
log.Printf("Org import: failed to resolve idle_prompt for %s: %v", ws.Name, err)
@ -569,7 +569,7 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa
// means the idle loop never fires regardless of interval, so we
// only emit interval when there's a body to go with it.
if idleInterval <= 0 {
idleInterval = 600 // same default as workspace-template/config.py
idleInterval = 600 // same default as workspace/config.py
}
block := fmt.Sprintf("idle_interval_seconds: %d\nidle_prompt: |\n %s\n", idleInterval, indented)
configFiles["config.yaml"] = appendYAMLBlock(configFiles["config.yaml"], block)

View File

@ -358,7 +358,7 @@ func configDirName(workspaceID string) string {
// string, and the path-traversal oracle where `runtime: ../../sensitive`
// probed host directories for existence.
//
// Keep in sync with workspace-template/build-all.sh — adding a new
// Keep in sync with workspace/build-all.sh — adding a new
// runtime means bumping both this list and the Docker image tags.
var knownRuntimes = map[string]struct{}{
"langgraph": {},

View File

@ -24,7 +24,7 @@ import (
// RuntimeImages maps runtime names to their Docker image tags.
// Each adapter has its own pre-built image extending workspace-template:base,
// with runtime-specific deps pre-installed for fast startup.
// Build all: workspace-template/Dockerfile (base), then each adapters/*/Dockerfile.
// Build all: workspace/Dockerfile (base), then each adapters/*/Dockerfile.
var RuntimeImages = map[string]string{
"langgraph": "workspace-template:langgraph",
"claude-code": "workspace-template:claude-code",
@ -244,7 +244,7 @@ func (p *Provisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string, e
if err != nil {
if isImageNotFoundErr(err) {
return "", fmt.Errorf(
"docker image %q not found — run 'bash workspace-template/build-all.sh %s' to build it (underlying error: %w)",
"docker image %q not found — run 'bash workspace/build-all.sh %s' to build it (underlying error: %w)",
image, runtimeTagFromImage(image), err,
)
}

View File

@ -741,14 +741,14 @@ func TestImageNotFoundErrorIncludesBuildHint(t *testing.T) {
tag := runtimeTagFromImage("workspace-template:openclaw")
wrapped := testErr(
`docker image "workspace-template:openclaw" not found — run 'bash workspace-template/build-all.sh ` +
`docker image "workspace-template:openclaw" not found — run 'bash workspace/build-all.sh ` +
tag + `' to build it (underlying error: ` + underlying.Error() + `)`,
)
s := wrapped.Error()
for _, want := range []string{
`"workspace-template:openclaw"`,
`bash workspace-template/build-all.sh openclaw`,
`bash workspace/build-all.sh openclaw`,
`No such image: workspace-template:openclaw`,
} {
if !strings.Contains(s, want) {

Binary file not shown.

View File

@ -39,7 +39,7 @@ class BaseAdapter(ABC):
"""Interface every agent infrastructure adapter must implement.
To add a new agent infra:
1. Create workspace-template/adapters/<your_infra>/
1. Create workspace/adapters/<your_infra>/
2. Implement adapter.py with a class extending BaseAdapter
3. Add requirements.txt with your infra's dependencies
4. Export as Adapter in __init__.py

View File

@ -37,7 +37,7 @@ import pytest
def _make_a2a_stubs() -> None:
"""Register minimal a2a SDK stubs in sys.modules.
Mirrors what workspace-template/tests/conftest.py does; needed because
Mirrors what workspace/tests/conftest.py does; needed because
this test file lives outside the ``tests/`` directory and conftest.py
is not automatically loaded for it.
"""

View File

@ -2,7 +2,7 @@
Resolution order for ``(plugin_name, runtime)``:
1. Platform registry ``workspace-template/plugins_registry/<plugin>/<runtime>.py``
1. Platform registry ``workspace/plugins_registry/<plugin>/<runtime>.py``
2. Plugin-shipped ``<plugin_root>/adapters/<runtime>.py``
3. Raw filesystem :class:`RawDropAdaptor` (warns, drops files only)

View File

@ -1,4 +1,4 @@
"""Tests for workspace-template/platform_auth.py (Phase 30.1)."""
"""Tests for workspace/platform_auth.py (Phase 30.1)."""
from __future__ import annotations
import os

View File

@ -16,7 +16,7 @@ from pathlib import Path
import pytest
# Resolve workspace-template/ so `import plugins_registry` works in CI without
# Resolve workspace/ so `import plugins_registry` works in CI without
# requiring an installed package.
_WS_TEMPLATE = Path(__file__).resolve().parents[1]
if str(_WS_TEMPLATE) not in sys.path: