diff --git a/.gitattributes b/.gitattributes index 90236b24..52f9f1f5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 24d9c2cb..f7ed589f 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07199201..3fe9ff74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.gitignore b/.gitignore index 1c43e845..293dbe4b 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/docs/architecture/wildcard-dns-proxy.md b/docs/architecture/wildcard-dns-proxy.md index e9a43fe8..123baa79 100644 --- a/docs/architecture/wildcard-dns-proxy.md +++ b/docs/architecture/wildcard-dns-proxy.md @@ -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 | diff --git a/docs/edit-history/2026-04-14.md b/docs/edit-history/2026-04-14.md index 5d111011..b90e7d0a 100644 --- a/docs/edit-history/2026-04-14.md +++ b/docs/edit-history/2026-04-14.md @@ -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=;instance=` header, diff --git a/docs/edit-history/2026-04-15.md b/docs/edit-history/2026-04-15.md index 0e739caf..27711cd5 100644 --- a/docs/edit-history/2026-04-15.md +++ b/docs/edit-history/2026-04-15.md @@ -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:` from env and spawns each tenant Fly Machine from this image. diff --git a/docs/retrospectives/2026-04-17-saas-buildout.md b/docs/retrospectives/2026-04-17-saas-buildout.md index 318cfc08..e9efae2d 100644 --- a/docs/retrospectives/2026-04-17-saas-buildout.md +++ b/docs/retrospectives/2026-04-17-saas-buildout.md @@ -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 | diff --git a/docs/runbooks/gdpr-erasure.md b/docs/runbooks/gdpr-erasure.md index ea1f090b..43171fe8 100644 --- a/docs/runbooks/gdpr-erasure.md +++ b/docs/runbooks/gdpr-erasure.md @@ -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 diff --git a/manifest.json b/manifest.json index 166dce91..55790ca2 100644 --- a/manifest.json +++ b/manifest.json @@ -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"}, diff --git a/tests/README.md b/tests/README.md index 9ce9e050..6521cdc9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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** | diff --git a/tests/e2e/test_claude_code_e2e.sh b/tests/e2e/test_claude_code_e2e.sh index 6ed6a061..3635869d 100755 --- a/tests/e2e/test_claude_code_e2e.sh +++ b/tests/e2e/test_claude_code_e2e.sh @@ -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 diff --git a/tests/e2e/test_saas_tenant.sh b/tests/e2e/test_saas_tenant.sh index 1faa33ac..19dd0e87 100755 --- a/tests/e2e/test_saas_tenant.sh +++ b/tests/e2e/test_saas_tenant.sh @@ -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= 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. diff --git a/workspace-server/.gitignore b/workspace-server/.gitignore new file mode 100644 index 00000000..254defdd --- /dev/null +++ b/workspace-server/.gitignore @@ -0,0 +1 @@ +server diff --git a/workspace-server/internal/handlers/github_token.go b/workspace-server/internal/handlers/github_token.go index c6f5c9c2..1091b967 100644 --- a/workspace-server/internal/handlers/github_token.go +++ b/workspace-server/internal/handlers/github_token.go @@ -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. diff --git a/workspace-server/internal/handlers/mcp.go b/workspace-server/internal/handlers/mcp.go index f036f534..c10976ed 100644 --- a/workspace-server/internal/handlers/mcp.go +++ b/workspace-server/internal/handlers/mcp.go @@ -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{ diff --git a/workspace-server/internal/handlers/org.go b/workspace-server/internal/handlers/org.go index 7d2c54d0..f51c3321 100644 --- a/workspace-server/internal/handlers/org.go +++ b/workspace-server/internal/handlers/org.go @@ -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) diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index b5d5ef97..7290c56c 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -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": {}, diff --git a/workspace-server/internal/provisioner/provisioner.go b/workspace-server/internal/provisioner/provisioner.go index e7943ce5..d409080c 100644 --- a/workspace-server/internal/provisioner/provisioner.go +++ b/workspace-server/internal/provisioner/provisioner.go @@ -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, ) } diff --git a/workspace-server/internal/provisioner/provisioner_test.go b/workspace-server/internal/provisioner/provisioner_test.go index bededcd3..9330f494 100644 --- a/workspace-server/internal/provisioner/provisioner_test.go +++ b/workspace-server/internal/provisioner/provisioner_test.go @@ -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) { diff --git a/workspace-server/server b/workspace-server/server deleted file mode 100755 index 41aed4ab..00000000 Binary files a/workspace-server/server and /dev/null differ diff --git a/workspace/adapter_base.py b/workspace/adapter_base.py index a1820e74..43996415 100644 --- a/workspace/adapter_base.py +++ b/workspace/adapter_base.py @@ -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// + 1. Create workspace/adapters// 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 diff --git a/workspace/adapters/google-adk/test_adapter.py b/workspace/adapters/google-adk/test_adapter.py index 773a001d..7a185c1a 100644 --- a/workspace/adapters/google-adk/test_adapter.py +++ b/workspace/adapters/google-adk/test_adapter.py @@ -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. """ diff --git a/workspace/plugins_registry/__init__.py b/workspace/plugins_registry/__init__.py index ef1c5b3b..363f26fe 100644 --- a/workspace/plugins_registry/__init__.py +++ b/workspace/plugins_registry/__init__.py @@ -2,7 +2,7 @@ Resolution order for ``(plugin_name, runtime)``: - 1. Platform registry → ``workspace-template/plugins_registry//.py`` + 1. Platform registry → ``workspace/plugins_registry//.py`` 2. Plugin-shipped → ``/adapters/.py`` 3. Raw filesystem → :class:`RawDropAdaptor` (warns, drops files only) diff --git a/workspace/tests/test_platform_auth.py b/workspace/tests/test_platform_auth.py index cefdf837..ca08bdf7 100644 --- a/workspace/tests/test_platform_auth.py +++ b/workspace/tests/test_platform_auth.py @@ -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 diff --git a/workspace/tests/test_plugins_registry.py b/workspace/tests/test_plugins_registry.py index d4e021f7..44531eb4 100644 --- a/workspace/tests/test_plugins_registry.py +++ b/workspace/tests/test_plugins_registry.py @@ -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: