diff --git a/.env.example b/.env.example index 247b4a434..9c013dbfc 100644 --- a/.env.example +++ b/.env.example @@ -51,7 +51,7 @@ MOLECULE_ENV=development # Environment label (development/ # MOLECULE_IN_DOCKER= # Set when running the platform inside Docker (accepts 1/0, true/false). Triggers A2A proxy to rewrite 127.0.0.1: agent URLs to Docker bridge hostnames. Auto-detected via /.dockerenv; only set if detection fails or to force off. # GitHub -# GITHUB_REPO=owner/repo # Target repo for agent initial_prompt clone (e.g. Molecule-AI/molecule-monorepo). Read inside workspace containers. +# GITHUB_REPO=owner/repo # Target repo for agent initial_prompt clone (e.g. Molecule-AI/molecule-core). Read inside workspace containers. # GITHUB_TOKEN= # Personal access token / installation token used by agents that clone private repos. Register as a global secret via POST /admin/secrets for propagation to workspace env. Token is used in-URL during clone and then scrubbed from .git/config via `git remote set-url`. # Webhooks diff --git a/README.md b/README.md index 7b1407e82..17ce607bc 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ ## Quick Start ```bash -git clone https://git.moleculesai.app/molecule-ai/molecule-monorepo.git -cd molecule-monorepo +git clone https://git.moleculesai.app/molecule-ai/molecule-core.git +cd molecule-core ./scripts/dev-start.sh ``` diff --git a/canvas/src/app/pricing/page.tsx b/canvas/src/app/pricing/page.tsx index 4f0e53ce4..63cd44165 100644 --- a/canvas/src/app/pricing/page.tsx +++ b/canvas/src/app/pricing/page.tsx @@ -41,7 +41,7 @@ export default function PricingPage() {

We publish the{" "} full source on GitHub diff --git a/docs/architecture/molecule-technical-doc.md b/docs/architecture/molecule-technical-doc.md index 5120c2e00..775c18a7f 100644 --- a/docs/architecture/molecule-technical-doc.md +++ b/docs/architecture/molecule-technical-doc.md @@ -1,7 +1,7 @@ # Molecule AI — Comprehensive Technical Documentation > Definitive technical reference for the Molecule AI Agent Team platform. -> Based on a full non-invasive scan of the [molecule-monorepo](https://git.moleculesai.app/molecule-ai/molecule-monorepo) repository. +> Based on a full non-invasive scan of the [molecule-core](https://git.moleculesai.app/molecule-ai/molecule-core) repository. --- @@ -1131,11 +1131,11 @@ Molecule AI's workspace abstraction is **runtime-agnostic by design**. A workspa ## Links -- **GitHub**: https://git.moleculesai.app/molecule-ai/molecule-monorepo -- **Architecture Docs**: https://git.moleculesai.app/molecule-ai/molecule-monorepo/src/branch/main/docs/architecture -- **API Protocol**: https://git.moleculesai.app/molecule-ai/molecule-monorepo/src/branch/main/docs/api-protocol -- **Agent Runtime**: https://git.moleculesai.app/molecule-ai/molecule-monorepo/src/branch/main/docs/agent-runtime -- **Product Docs**: https://git.moleculesai.app/molecule-ai/molecule-monorepo/src/branch/main/docs/product +- **GitHub**: https://git.moleculesai.app/molecule-ai/molecule-core +- **Architecture Docs**: https://git.moleculesai.app/molecule-ai/molecule-core/src/branch/main/docs/architecture +- **API Protocol**: https://git.moleculesai.app/molecule-ai/molecule-core/src/branch/main/docs/api-protocol +- **Agent Runtime**: https://git.moleculesai.app/molecule-ai/molecule-core/src/branch/main/docs/agent-runtime +- **Product Docs**: https://git.moleculesai.app/molecule-ai/molecule-core/src/branch/main/docs/product --- diff --git a/docs/development/local-development.md b/docs/development/local-development.md index d5bd116b7..95c5d5d6d 100644 --- a/docs/development/local-development.md +++ b/docs/development/local-development.md @@ -82,7 +82,7 @@ DATABASE_URL=postgres://dev:dev@postgres:5432/molecule?sslmode=prefer REDIS_URL=redis://redis:6379 PORT=8080 SECRETS_ENCRYPTION_KEY=dev-key-change-in-production -WORKSPACE_DIR=/path/to/molecule-monorepo # Optional global fallback; prefer per-workspace workspace_dir in org.yaml or API +WORKSPACE_DIR=/path/to/molecule-core # Optional global fallback; prefer per-workspace workspace_dir in org.yaml or API ``` ### Canvas (Next.js) diff --git a/docs/infra/workspace-terminal.md b/docs/infra/workspace-terminal.md index 84e120e3b..43c60440e 100644 --- a/docs/infra/workspace-terminal.md +++ b/docs/infra/workspace-terminal.md @@ -16,11 +16,9 @@ workspace container running on it) over an [EC2 Instance Connect Endpoint](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-setup-ec2-instance-connect-endpoint.html). End users see a terminal; no direct public SSH ingress is required. -Tracking: originally `molecule-core#1528` (resolved 2026-04-22). The -`molecule-core` repo has since been renamed to `molecule-monorepo` and no -longer accepts new issues under the old name; future terminal work is -tracked in `molecule-monorepo` issues (workspace-server scope) and in -`molecule-controlplane` issues for the EIC / per-tenant SG path. +Tracking: originally `molecule-core#1528` (resolved 2026-04-22). Future +terminal work is tracked in `molecule-core` issues (workspace-server scope) +and in `molecule-controlplane` issues for the EIC / per-tenant SG path. ## Where things are diff --git a/docs/integrations/opencode.md b/docs/integrations/opencode.md index 4d69ef729..ce2df6fce 100644 --- a/docs/integrations/opencode.md +++ b/docs/integrations/opencode.md @@ -64,7 +64,7 @@ When opencode connects to the Molecule MCP endpoint, the agent gains access to: "tool": "delegate_task", "arguments": { "target": "research-lead", - "task": "Summarise the last 7 days of commits in Molecule-AI/molecule-monorepo" + "task": "Summarise the last 7 days of commits in Molecule-AI/molecule-core" } } ``` diff --git a/docs/internal-content-policy.md b/docs/internal-content-policy.md index b0c5e165f..d1dcccd80 100644 --- a/docs/internal-content-policy.md +++ b/docs/internal-content-policy.md @@ -1,6 +1,6 @@ # Internal content policy -The `Molecule-AI/molecule-monorepo` repo is **public**. Anything internal +The `Molecule-AI/molecule-core` repo is **public**. Anything internal (positioning, competitive briefs, sales playbooks, PMM/press drip, draft campaigns, raw research notes, ops runbooks, retrospectives) lives in **`Molecule-AI/internal`**. @@ -18,14 +18,14 @@ This page is the canonical decision tree. | Draft campaign asset (still iterating, not yet customer-visible) | `Molecule-AI/internal/marketing/campaigns/` | | Roadmap discussion, planning doc, retrospective | `Molecule-AI/internal/PLAN.md` or `Molecule-AI/internal/retrospectives/` | | Runbook, ops procedure, incident postmortem | `Molecule-AI/internal/runbooks/` | -| **Public-ready** blog post (final draft, ready to ship to docs site) | `Molecule-AI/molecule-monorepo/docs/blog/` | -| **Public-ready** tutorial / quickstart | `Molecule-AI/molecule-monorepo/docs/tutorials/` | -| Public DevRel content (code samples, demos for users) | `Molecule-AI/molecule-monorepo/docs/devrel/` | -| API reference, architecture docs for external developers | `Molecule-AI/molecule-monorepo/docs/api/` | +| **Public-ready** blog post (final draft, ready to ship to docs site) | `Molecule-AI/molecule-core/docs/blog/` | +| **Public-ready** tutorial / quickstart | `Molecule-AI/molecule-core/docs/tutorials/` | +| Public DevRel content (code samples, demos for users) | `Molecule-AI/molecule-core/docs/devrel/` | +| API reference, architecture docs for external developers | `Molecule-AI/molecule-core/docs/api/` | | Code, tests, infrastructure | wherever is appropriate inside this repo | **Rule of thumb:** *"Would I be comfortable if a competitor / journalist / customer -read this verbatim today?"* — yes → `monorepo/docs/`. No / not yet → `internal/`. +read this verbatim today?"* — yes → `molecule-core/docs/`. No / not yet → `internal/`. ## Why @@ -82,7 +82,7 @@ git push -u origin HEAD gh pr create --base main --fill ``` -Yes, this is more steps than `cd molecule-monorepo && git add research/foo.md`. +Yes, this is more steps than `cd molecule-core && git add research/foo.md`. That cost is intentional: the friction is the point. Public space and internal space are different products with different audiences and different durability guarantees. diff --git a/docs/quickstart.md b/docs/quickstart.md index ff2a0457d..283a1023b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -17,8 +17,8 @@ This path is aligned to the current repository and current UI. It gets you from ## The one-command path ```bash -git clone https://git.moleculesai.app/molecule-ai/molecule-monorepo.git -cd molecule-monorepo +git clone https://git.moleculesai.app/molecule-ai/molecule-core.git +cd molecule-core ./scripts/dev-start.sh ``` @@ -42,8 +42,8 @@ If you'd rather run each component yourself — useful when you're iterating on ### Step 1: Clone the repository ```bash -git clone https://git.moleculesai.app/molecule-ai/molecule-monorepo.git -cd molecule-monorepo +git clone https://git.moleculesai.app/molecule-ai/molecule-core.git +cd molecule-core ``` ### Step 2: Start the shared infrastructure diff --git a/tools/check-template-parity.sh b/tools/check-template-parity.sh index a164ba92e..819dda9f0 100755 --- a/tools/check-template-parity.sh +++ b/tools/check-template-parity.sh @@ -13,7 +13,7 @@ # # Invocation (from template-hermes repo's CI): # -# bash /path/to/molecule-monorepo/tools/check-template-parity.sh \ +# bash /path/to/molecule-core/tools/check-template-parity.sh \ # install.sh start.sh # # Or inline via curl: diff --git a/workspace-server/internal/handlers/delegation_test.go b/workspace-server/internal/handlers/delegation_test.go index 223b8de77..c4f0a7341 100644 --- a/workspace-server/internal/handlers/delegation_test.go +++ b/workspace-server/internal/handlers/delegation_test.go @@ -1059,13 +1059,13 @@ func expectExecuteDelegationBase(mock sqlmock.Sqlmock) { WillReturnResult(sqlmock.NewResult(0, 1)) // CanCommunicate: getWorkspaceRef(source) + getWorkspaceRef(target). - // Both are root-level workspaces (parent_id=NULL) → root-level siblings → allowed. + // Source is root-level, target is its child → parent→child communication allowed. mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id = "). WithArgs(testDeliverySourceID). WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow(testDeliverySourceID, nil)) mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id = "). WithArgs(testDeliveryTargetID). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow(testDeliveryTargetID, nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow(testDeliveryTargetID, testDeliverySourceID)) // resolveAgentURL: test callers always set the URL in Redis (mr.Set ws:{id}:url), // so resolveAgentURL gets a cache hit and never falls back to DB. diff --git a/workspace-server/internal/handlers/discovery.go b/workspace-server/internal/handlers/discovery.go index f97408ae7..6a12bb5fe 100644 --- a/workspace-server/internal/handlers/discovery.go +++ b/workspace-server/internal/handlers/discovery.go @@ -246,15 +246,11 @@ func (h *DiscoveryHandler) Peers(c *gin.Context) { FROM workspaces w WHERE w.parent_id = $1 AND w.id != $2 AND w.status != 'removed'`, parentID.String, workspaceID) peers = append(peers, siblings...) - } else { - siblings, _ := queryPeerMaps(` - SELECT w.id, w.name, COALESCE(w.role, ''), w.tier, w.status, - COALESCE(w.agent_card, 'null'::jsonb), COALESCE(w.url, ''), - w.parent_id, w.active_tasks - FROM workspaces w WHERE w.parent_id IS NULL AND w.id != $1 AND w.status != 'removed'`, - workspaceID) - peers = append(peers, siblings...) } + // Root workspaces (parent_id IS NULL) have no siblings: each org has + // exactly one root, so a distinct workspace with parent_id IS NULL + // belongs to a different tenant. Querying `parent_id IS NULL` would + // leak every org root across tenants (security #1953). // Children — exclude self defensively. A child row whose parent_id // equals the requesting workspaceID can never legitimately be the diff --git a/workspace-server/internal/handlers/discovery_test.go b/workspace-server/internal/handlers/discovery_test.go index d803226a6..340e92efd 100644 --- a/workspace-server/internal/handlers/discovery_test.go +++ b/workspace-server/internal/handlers/discovery_test.go @@ -223,10 +223,8 @@ func TestPeers_RootWorkspace_NoPeers(t *testing.T) { peerCols := []string{"id", "name", "role", "tier", "status", "agent_card", "url", "parent_id", "active_tasks"} - // Siblings (other root-level workspaces) — none - mock.ExpectQuery("SELECT w.id, w.name.*WHERE w.parent_id IS NULL AND w.id != \\$1"). - WithArgs("ws-root-alone"). - WillReturnRows(sqlmock.NewRows(peerCols)) + // Siblings — root workspaces have none (each org has exactly one root; + // querying `parent_id IS NULL` would leak all org roots cross-tenant). // Children — none. #383 added explicit `w.id != $2` self-filter. mock.ExpectQuery("SELECT w.id, w.name.*WHERE w.parent_id = \\$1 AND w.id != \\$2"). @@ -959,15 +957,12 @@ func TestPeers_DevModeFailOpen_AllowsBearerlessRequest(t *testing.T) { peersAuthFixtureHasLiveToken(mock, "ws-dev") - // Root workspace → children+parent queries still fire but the - // parent_id lookup comes first. + // Root workspace → no sibling query (security #1953), children+parent + // queries still fire, and the parent_id lookup comes first. mock.ExpectQuery("SELECT parent_id FROM workspaces WHERE id ="). WithArgs("ws-dev"). WillReturnRows(sqlmock.NewRows([]string{"parent_id"}).AddRow(nil)) peerCols := []string{"id", "name", "role", "tier", "status", "agent_card", "url", "parent_id", "active_tasks"} - mock.ExpectQuery("SELECT w.id.+WHERE w.parent_id IS NULL AND w.id"). - WithArgs("ws-dev"). - WillReturnRows(sqlmock.NewRows(peerCols)) // #383 — children query gained explicit `w.id != $2` self-filter. mock.ExpectQuery("SELECT w.id.+WHERE w.parent_id = \\$1 AND w.id != \\$2 AND w.status"). WithArgs("ws-dev", "ws-dev"). diff --git a/workspace-server/internal/handlers/handlers_additional_test.go b/workspace-server/internal/handlers/handlers_additional_test.go index 9916f0469..0180d5fb7 100644 --- a/workspace-server/internal/handlers/handlers_additional_test.go +++ b/workspace-server/internal/handlers/handlers_additional_test.go @@ -576,13 +576,13 @@ func TestDiscover_TargetOffline(t *testing.T) { setupTestRedis(t) handler := NewDiscoveryHandler() - // Both root-level, access allowed + // Same-parent siblings, access allowed mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-caller"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-caller", nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-caller", "ws-parent")) mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-off"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-off", nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-off", "ws-parent")) // Name + runtime lookup (discovery now queries both) mock.ExpectQuery("SELECT COALESCE"). @@ -622,13 +622,13 @@ func TestCheckAccess_SiblingsAllowed(t *testing.T) { setupTestRedis(t) handler := NewDiscoveryHandler() - // Both root-level siblings → allowed + // Siblings under a shared parent → allowed mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-a"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-a", nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-a", "ws-parent")) mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-b"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-b", nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-b", "ws-parent")) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/workspace-server/internal/handlers/handlers_extended_test.go b/workspace-server/internal/handlers/handlers_extended_test.go index 5b82e7fc6..832f29337 100644 --- a/workspace-server/internal/handlers/handlers_extended_test.go +++ b/workspace-server/internal/handlers/handlers_extended_test.go @@ -376,14 +376,13 @@ func TestExtended_DiscoverWithCallerID(t *testing.T) { handler := NewDiscoveryHandler() // CanCommunicate needs to look up both workspaces - // Caller: root-level (no parent) + // Caller and target are siblings under a shared parent mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-caller"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-caller", nil)) - // Target: also root-level (no parent) — root-level siblings are allowed + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-caller", "ws-parent")) mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-target"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-target", nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-target", "ws-parent")) // Discover handler looks up workspace name + runtime mock.ExpectQuery("SELECT COALESCE"). @@ -458,14 +457,14 @@ func TestExtended_Peers(t *testing.T) { setupTestRedis(t) handler := NewDiscoveryHandler() - // Expect parent_id lookup for requesting workspace (root-level, no parent) + // Expect parent_id lookup for requesting workspace (has a parent) mock.ExpectQuery("SELECT parent_id FROM workspaces WHERE id ="). WithArgs("ws-peer"). - WillReturnRows(sqlmock.NewRows([]string{"parent_id"}).AddRow(nil)) + WillReturnRows(sqlmock.NewRows([]string{"parent_id"}).AddRow("ws-parent")) - // Expect root-level siblings query (parent IS NULL, excluding self) + // Expect siblings query (same parent, excluding self) mock.ExpectQuery("SELECT w.id, w.name"). - WithArgs("ws-peer"). + WithArgs("ws-parent", "ws-peer"). WillReturnRows(sqlmock.NewRows([]string{"id", "name", "role", "tier", "status", "agent_card", "url", "parent_id", "active_tasks"}). AddRow("ws-sibling", "Sibling Agent", "worker", 1, "online", []byte("null"), "http://localhost:9001", nil, 0)) @@ -512,13 +511,13 @@ func TestExtended_CheckAccess(t *testing.T) { handler := NewDiscoveryHandler() // CanCommunicate will look up both workspaces - // Both root-level — should be allowed + // Siblings under a shared parent — should be allowed mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-a"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-a", nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-a", "ws-parent")) mock.ExpectQuery("SELECT id, parent_id FROM workspaces WHERE id ="). WithArgs("ws-b"). - WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-b", nil)) + WillReturnRows(sqlmock.NewRows([]string{"id", "parent_id"}).AddRow("ws-b", "ws-parent")) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/workspace-server/internal/handlers/mcp_tools.go b/workspace-server/internal/handlers/mcp_tools.go index 21d53633f..40a2f8d7c 100644 --- a/workspace-server/internal/handlers/mcp_tools.go +++ b/workspace-server/internal/handlers/mcp_tools.go @@ -107,16 +107,10 @@ func (h *MCPHandler) toolListPeers(ctx context.Context, workspaceID string) (str log.Printf("MCP toolListPeers: sibling scan error: %v", scanErr) } } - } else { - rows, err := h.database.QueryContext(ctx, - cols+` FROM workspaces w WHERE w.parent_id IS NULL AND w.id != $1 AND w.status != 'removed'`, - workspaceID) - if err == nil { - if scanErr := scanPeers(rows); scanErr != nil { - log.Printf("MCP toolListPeers: sibling scan error: %v", scanErr) - } - } } + // Root workspaces (parent_id IS NULL) have no siblings: each org has + // exactly one root. Querying `parent_id IS NULL` would leak every org + // root across tenants (security #1953). // Children { diff --git a/workspace-server/internal/registry/access.go b/workspace-server/internal/registry/access.go index 56a9c419e..2672db0e4 100644 --- a/workspace-server/internal/registry/access.go +++ b/workspace-server/internal/registry/access.go @@ -94,15 +94,13 @@ func CanCommunicate(callerID, targetID string) bool { return false } - // Siblings — same parent (including root-level where both have no parent) + // Siblings — same parent (excluding root-level; each org has exactly one + // root workspace, so two distinct root-level workspaces are in different + // orgs and must not communicate cross-tenant). if caller.ParentID != nil && target.ParentID != nil && *caller.ParentID == *target.ParentID { return true } - // Root-level siblings — both have no parent - if caller.ParentID == nil && target.ParentID == nil { - return true - } // Direct parent → child (fast path; avoids the ancestor walk) if target.ParentID != nil && caller.ID == *target.ParentID { diff --git a/workspace-server/internal/registry/access_test.go b/workspace-server/internal/registry/access_test.go index b01a14691..3fbc56f92 100644 --- a/workspace-server/internal/registry/access_test.go +++ b/workspace-server/internal/registry/access_test.go @@ -63,14 +63,16 @@ func TestCanCommunicate_Siblings(t *testing.T) { } } -func TestCanCommunicate_RootSiblings(t *testing.T) { +func TestCanCommunicate_Denied_RootCrossTenant(t *testing.T) { mock := setupMockDB(t) - // Both at root level (no parent) + // Both at root level (no parent) but in different orgs. + // parent_id = NULL means org root; each org has exactly one root, + // so two distinct root workspaces must NOT communicate. expectLookup(mock, "ws-a", nil) expectLookup(mock, "ws-b", nil) - if !CanCommunicate("ws-a", "ws-b") { - t.Error("root-level siblings should communicate") + if CanCommunicate("ws-a", "ws-b") { + t.Error("distinct root-level workspaces should NOT communicate cross-tenant") } }