Commit Graph

178 Commits

Author SHA1 Message Date
molecule-ai[bot]
2ab7054a26
Merge pull request #646 from Molecule-AI/fix/migration-025-fk-type
Merge gate passed. +2/-2 FK type fix: workspace_id TEXT→UUID in 025, org_id TEXT→UUID in 026 — matches workspaces.id (UUID PK). Schema migration — CEO explicit authorization in chat (boot-blocker/urgent). UNSTABLE = known App token scope gap.
2026-04-17 06:46:08 +00:00
molecule-ai[bot]
f6673b21a0
Merge pull request #641 from Molecule-AI/feat/issue-595-cloudflare-artifacts-demo
Merge gate passed (all 7 gates). Cloudflare Artifacts demo integration: 4 routes behind WorkspaceAuth, CF token from env only, import_url HTTPS enforced, CF 5xx errors sanitized, parameterized SQL throughout. Migration 028 uses CREATE TABLE IF NOT EXISTS. Schema migration — CEO explicit authorization in chat (urgent/first-mover). Tip SHA daf52da verified. UNSTABLE = known App token scope gap.
2026-04-17 06:43:21 +00:00
Hongming Wang
f7b04c0543 fix(migrations): TEXT→UUID FK type mismatch blocking all E2E runs
Migrations 025 + 026 declared workspace_id/org_id as TEXT but
workspaces.id is UUID — Postgres rejects the FK constraint, crashing
every E2E run on main since these migrations were merged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:40:22 -07:00
Molecule AI Backend Engineer
daf52daa1d fix(platform): address security review findings on CF Artifacts (#641)
Four findings from the security audit on PR #641:

FIX 1 (MEDIUM): import_url scheme validation
- Reject non-HTTPS import URLs with 400 before forwarding to CF API.
  Prevents SSRF via http://, git://, ssh://, file:// etc.

FIX 2 (MEDIUM): CF 5xx error leakage
- Add cfErrMessage() helper: returns "upstream service error" for CF 5xx
  responses and non-CF errors, passes through 4xx messages.
- Applied at all four CF-error response sites (Create, Get, Fork, Token).

FIX 3 (LOW): repo name validation
- Add package-level repoNameRE = ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$
- Validate in Create and Fork handlers when caller supplies an explicit name.
  Auto-generated names ("molecule-ws-<id>") are always safe and skip validation.

FIX 4 (LOW): response body size limit in CF client
- Wrap resp.Body with io.LimitReader(1 MB) before json.NewDecoder in do().
  Prevents memory exhaustion from a runaway/malicious CF response.

Tests: 16 new tests covering all four fixes (cfErrMessage 4xx/5xx/non-API,
import_url non-HTTPS cases, invalid repo names in Create and Fork).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:39:47 +00:00
Molecule AI Backend Engineer
3bcb2b21a5 feat(platform): Cloudflare Artifacts demo integration (#595)
Add a minimal but complete integration with the Cloudflare Artifacts API
(private beta Apr 2026, public beta May 2026) — "Git for agents" versioned
workspace-snapshot storage.

## What's included

**`platform/internal/artifacts/client.go`** — typed Go HTTP client for the
CF Artifacts REST API:
- CreateRepo, GetRepo, ForkRepo, ImportRepo, DeleteRepo
- CreateToken, RevokeToken
- CF v4 response-envelope decoding; *APIError with StatusCode + Message

**`platform/internal/handlers/artifacts.go`** — four workspace-scoped
Gin handlers (all behind WorkspaceAuth middleware):
- POST /workspaces/:id/artifacts — attach or import a CF Artifacts repo
- GET  /workspaces/:id/artifacts — get linked repo info (DB + live CF)
- POST /workspaces/:id/artifacts/fork — fork the workspace's repo
- POST /workspaces/:id/artifacts/token — mint a short-lived git credential

**`platform/migrations/028_workspace_artifacts.up.sql`** — `workspace_artifacts`
table: one-to-one link between a workspace and its CF Artifacts repo.
Credentials are never stored; only the credential-stripped remote URL.

**`platform/internal/router/router.go`** — wire the four routes into the
existing wsAuth group.

## Configuration
Two env vars gate the feature (returns 503 when either is absent):
- CF_ARTIFACTS_API_TOKEN — Cloudflare API token with Artifacts write perms
- CF_ARTIFACTS_NAMESPACE — Cloudflare Artifacts namespace name

## Tests
- 10 client-level tests (httptest.Server + CF v4 envelope mocks)
- 14 handler-level tests (sqlmock DB + mock CF server)
- Helper unit tests for stripCredentials, cfErrToHTTP

All 21 packages pass (go test ./...).

Closes #595

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:28:58 +00:00
77313434b1 fix(gate-1): resolve merge conflicts with main
Both conflicts were comment-only — identical logic on both sides:
- registry.go: kept main's wording ("accidentally clearing") for the
  monthly_spend comment in Heartbeat; logic is unchanged
- workspace.go: kept HEAD's comment (describes PR #634's clamping
  behaviour: [0, maxMonthlySpend]); logic is unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:27:14 +00:00
molecule-ai[bot]
7538b2a95c
Merge pull request #611 from Molecule-AI/feat/issue-541-budget-limit-backend
Merge gate passed (all 7 gates). Adds budget_limit + monthly_spend columns via 027_workspace_budget (ADD COLUMN IF NOT EXISTS — idempotent). A2A budget enforcement is fail-open on DB errors. WorkspaceAuth on all budget routes. Schema migration — CEO explicit authorization in chat. Merging before #634 which writes to monthly_spend.
2026-04-17 06:25:02 +00:00
molecule-ai[bot]
ea43256a87
Merge pull request #636 from Molecule-AI/fix/issue-631-migration-gap
Merge gate passed. Pure file renames (+0/-0): 026→025 (workspace_token_usage), 027→026 (org_plugin_allowlist). Closes migration numbering gap so sequential runners proceed past 024. Schema migration — CEO explicit authorization in chat. NOTE: if production DB recorded old filenames 026/027 as applied, verify runner idempotency before restart to avoid double-application.
2026-04-17 06:23:05 +00:00
Molecule AI Backend Engineer
f1fa92ad84 fix(migrations): renumber budget migration 025→027 to follow gap fix (#631)
Rebase on origin/fix/issue-631-migration-gap which inserts token_usage
(025) and org_plugin_allowlist (026); bump workspace_budget from 025 to
027 so the sequential runner applies all three in the correct order.
Update workspace_budget_test.go and workspace_test.go to match the
transaction-wrapped INSERT (BeginTx/Commit) introduced on main and the
resulting 10-arg WithArgs call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:22:09 +00:00
Molecule AI Backend Engineer
fce0be30fd fix(#611): remove budget_limit from PATCH /workspaces/:id and strip financial fields from GET
Security Auditor findings on PR #611:

Fix 1 (BLOCKING): Remove budget_limit handling from Update() entirely.
PATCH /workspaces/:id uses ValidateAnyToken — any enrolled workspace bearer
could self-clear its own spending ceiling. The dedicated AdminAuth-gated
PATCH /workspaces/:id/budget is the only authorised write path.

Fix 2 (MEDIUM): Strip budget_limit and monthly_spend from Get() response
before c.JSON(). GET /workspaces/:id is on the open router — any caller
with a valid UUID must not read billing data.

Also updates four existing tests in workspace_budget_test.go that encoded
the old (insecure) behaviour, and adds three new regression tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:18:41 +00:00
Molecule AI Backend Engineer
dd0b282c79 fix(issue-541): move PATCH /budget to adminAuth — workspace must not self-clear ceiling
Workspace agents could previously call PATCH /workspaces/:id/budget with their
own bearer token and set budget_limit=null, defeating the entire spend enforcement
feature. GET stays on wsAuth (reading own budget is legitimate); PATCH moves to
inline AdminAuth using the same pattern as /approvals/pending.

No existing tests needed updating — all budget PATCH tests call the handler
directly and are unaffected by router-level middleware changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:18:41 +00:00
Molecule AI Backend Engineer
4e6e3745f2 fix(issue-541): correct stale 429 comment to 402 in checkWorkspaceBudget
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:18:41 +00:00
Molecule AI Backend Engineer
2fb0aacd41 fix(#541): change budget enforcement status from 429 to 402
Budget limit exceeded on A2A proxy now returns HTTP 402 PaymentRequired
instead of 429 TooManyRequests, matching the issue spec and the FE amber
banner check. Updates a2a_proxy.go, workspace_budget_test.go (renamed
ExceededReturns429 → ExceededReturns402, AboveLimitReturns429 →
AboveLimitReturns402), and migration comment. All go test ./... pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:18:41 +00:00
Molecule AI Backend Engineer
22af070ef3 feat(#541): add dedicated GET/PATCH /workspaces/:id/budget endpoints
- New BudgetHandler with GetBudget and PatchBudget methods
- GET returns budget_limit (null or int64 USD cents), monthly_spend,
  and computed budget_remaining (null when no limit, can be negative
  when over-budget so callers can see the magnitude of the overage)
- PATCH accepts {budget_limit: int64|null}; null clears the ceiling;
  validates non-negative values; re-reads DB to echo final state
- Both handlers are wired in router.go under the WorkspaceAuth group
- 14 unit tests covering happy paths, 404, 400 validation, DB errors,
  over-budget state, zero limit, and clear-limit round-trip
- All 20 packages pass go test ./... and go build ./... is clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:18:41 +00:00
Molecule AI Backend Engineer
f8106b35be feat(platform): add per-workspace budget_limit field and A2A enforcement (#541)
- Migration 025: ADD COLUMN budget_limit BIGINT DEFAULT NULL and
  monthly_spend BIGINT NOT NULL DEFAULT 0 to workspaces table
- Models: BudgetLimit *int64 in CreateWorkspacePayload;
  MonthlySpend int64 in HeartbeatPayload
- workspace.go: scanWorkspaceRow, workspaceListQuery, Get, Create, and
  Update all handle budget_limit/monthly_spend; budget_limit is gated
  as a sensitiveUpdateField
- registry.go: heartbeat conditionally writes monthly_spend only when
  payload.MonthlySpend > 0 (avoids overwriting with zero)
- a2a_proxy.go: checkWorkspaceBudget() returns 429 when
  monthly_spend >= budget_limit (NULL = no limit; fail-open on DB error)
- Tests: 8 new workspace_budget_test.go tests + patched existing tests
  for the 20-column scanWorkspaceRow and 10-param CREATE INSERT

Field type: BIGINT (int64), units: USD cents (budget_limit=500 = $5.00/month)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:18:41 +00:00
molecule-ai[bot]
5b42bd76b5
Merge pull request #629 from Molecule-AI/fix/issue-614-security-headers
Merge gate passed (all 7 gates). Adds /orgs to apiPrefixes so PR #610's allowlist routes get nosniff + X-Frame-Options headers. One-line fix + 50 lines of regression tests. UNSTABLE = known App token scope gap.
2026-04-17 06:18:25 +00:00
Hongming Wang
44cef47763
Merge pull request #630 from Molecule-AI/fix/issue-615-cap-token-counts
fix(platform): cap token counts before upsert to prevent NUMERIC overflow (#615)
2026-04-16 23:17:37 -07:00
Molecule AI Backend Engineer
3329370b1c fix(migrations): close 024→026 gap — rename 026→025 token_usage, 027→026 allowlist (#631)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:17:36 +00:00
Molecule AI Backend Engineer
668c93e513 fix(platform): cap monthly_spend on heartbeat upsert (#615)
A malicious or buggy agent could report MonthlySpend = math.MaxInt64
causing NUMERIC overflow in the DB or incorrect budget-enforcement
comparisons downstream.

Changes:
- Add MonthlySpend int64 field to HeartbeatPayload (json:"monthly_spend")
- Clamp negative values to 0 and values above $10B (1_000_000_000_000
  cents) to the cap before any DB write
- The two-path UPDATE: when MonthlySpend > 0 after clamping, include
  monthly_spend = $7 in the UPDATE; otherwise skip to avoid accidentally
  clearing a previously-reported spend value
- 5 regression tests covering: within-bounds passthrough, negative
  clamp, math.MaxInt64 overflow clamp, exact-cap boundary, and
  zero/omitted no-update path

Note: this branch introduces MonthlySpend to HeartbeatPayload; it will
need trivial conflict resolution when feat/issue-541-budget-limit-backend
merges, as that branch also adds the field (without the cap). Keep this
branch's clamping logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:16:06 +00:00
molecule-ai[bot]
398c1e9f68
Merge pull request #628 from Molecule-AI/fix/issue-623-adminauth-origin-bypass
Merge gate passed (all 7 gates). Security fix: removes canvasOriginAllowed + isSameOriginCanvas Origin bypass from AdminAuth — bearer token is now the only accepted credential on admin routes. 3 regression tests cover forged-localhost, forged-tenant-domain, and bearer+Origin golden path. Auth PR — CEO explicit approval confirmed in chat. UNSTABLE = known GitHub App token scope gap.
2026-04-17 06:13:33 +00:00
Molecule AI Backend Engineer
13b8965c99 fix(platform): cap token counts before upsert to prevent NUMERIC overflow (#615)
Adversarial or buggy agents can report INT64_MAX token counts via A2A
responses. Without clamping, upsertTokenUsage would pass these directly to
Postgres NUMERIC(12,6), causing a silent upsert failure that corrupts the
workspace's cost accounting.

Fix: clamp input_tokens/output_tokens to [0, 10_000_000] before any
arithmetic or DB write. 10M tokens/call is well above any real LLM API
response; clamped values still produce valid cost rows.

Adds 4 regression tests:
- TestUpsertTokenUsage_615_CapsInt64Max      — INT64_MAX → maxTokensPerCall
- TestUpsertTokenUsage_615_CapsNegative      — negative → 0 (no DB call)
- TestUpsertTokenUsage_615_NormalValuesUnchanged — passthrough for normal counts
- TestUpsertTokenUsage_615_ExactlyAtCap      — at-cap value accepted unchanged

Closes #615

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:03:40 +00:00
Molecule AI Backend Engineer
67a9ec8fcb fix(platform): pin X-Content-Type-Options nosniff + add /orgs API prefix (#614)
SecurityHeaders() middleware already sets X-Content-Type-Options: nosniff and
X-Frame-Options: DENY globally on every response (issue #151 / PR ~securityheaders).
This commit adds the explicit acceptance test that #614 requires and extends
the apiPrefixes list to cover the new /orgs allowlist routes from PR #610.

Changes:
- securityheaders.go: add "/orgs" to apiPrefixes so allowlist routes get the
  strict CSP (no unsafe-inline) rather than the canvas-tier permissive policy
- securityheaders_test.go: TestSecurityHeaders_614_NosniffOnSSEAndAPIEndpoints
  verifies the header is present on SSE endpoint, /settings/secrets, /events,
  and /orgs paths; TestIsAPIPath gains /orgs cases

Closes #614

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:02:18 +00:00
Molecule AI Backend Engineer
cc45f0c0f6 fix(security): remove canvasOriginAllowed from AdminAuth middleware (#623)
The Origin header is trivially forgeable by any container on the Docker
network. Having canvasOriginAllowed() / isSameOriginCanvas() as auth
bypass paths in AdminAuth let any curl/container without a bearer token
reach /settings/secrets, /bundles/import, /bundles/export, /events, and
all other AdminAuth-gated routes by forging Origin: http://localhost:3000.

Fix: remove both Origin bypass branches from AdminAuth. Bearer token is
now the only accepted credential. Lazy-bootstrap fail-open (zero tokens →
pass-through) is preserved for fresh installs.

CanvasOrBearer retains the Origin bypass because it is scoped exclusively
to cosmetic routes (PUT /canvas/viewport) where a forged request has zero
security impact — worst case is viewport position corruption.

Added 3 regression tests:
- TestAdminAuth_623_ForgedOrigin_Returns401
- TestAdminAuth_623_ForgedCORSOrigin_Returns401
- TestAdminAuth_623_ValidBearer_WithOrigin_Passes

Closes #623, Closes #626

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:00:45 +00:00
molecule-ai[bot]
b948f0b140
Merge pull request #610 from Molecule-AI/feat/issue-591-org-plugin-allowlist
feat(platform): per-org plugin governance registry (allowlist)
2026-04-17 05:55:27 +00:00
molecule-ai[bot]
9f815e27a1
Merge pull request #602 from Molecule-AI/feat/issue-593-workspace-token-tracking
feat(platform): per-workspace token tracking + GET /workspaces/:id/metrics
2026-04-17 05:54:27 +00:00
molecule-ai[bot]
588190a92f
Merge pull request #612 from Molecule-AI/fix/test-token-adminauth
fix(security): gate test-token endpoint behind AdminAuth
2026-04-17 05:53:49 +00:00
molecule-ai[bot]
3ecdcf8c6b
Merge pull request #601 from Molecule-AI/feat/issue-590-agui-sse-endpoint
feat(platform): AG-UI compatible SSE endpoint for streaming agent events
2026-04-17 05:45:29 +00:00
Molecule AI Backend Engineer
53284c4626 feat(platform): per-org plugin governance registry (#591)
Add an org-scoped allowlist table so org admins can restrict which plugins
workspace agents are allowed to install.  An empty allowlist means
allow-all (backward-compatible with existing deployments).

• migrations/027_org_plugin_allowlist.{up,down}.sql — new table + unique
  index on (org_id, plugin_name)
• handlers/org_plugin_allowlist.go — resolveOrgID, checkOrgPluginAllowlist
  (fail-open on DB errors), GetAllowlist, PutAllowlist (atomic tx replace)
• handlers/org_plugin_allowlist_test.go — 23 unit tests covering all
  handler paths, resolveOrgID, and all checkOrgPluginAllowlist branches
• handlers/plugins_install.go — allowlist gate between resolveAndStage and
  deliverToContainer; returns 403 if plugin is blocked
• router/router.go — GET/PUT /orgs/:id/plugins/allowlist under AdminAuth

All tests pass; go build ./... clean; gosec Issues: 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 05:40:23 +00:00
Molecule AI Backend Engineer
f60c9df26f feat(platform): per-workspace token tracking + GET /workspaces/:id/metrics (#593)
Migration 026 adds workspace_token_usage table (uuid pk, workspace_id FK with
CASCADE, period_start TIMESTAMPTZ, input_tokens, output_tokens, call_count,
estimated_cost_usd NUMERIC(12,6), updated_at) with a UNIQUE index on
(workspace_id, period_start) for day-granularity upserts.

A2A proxy (proxyA2ARequest) now spawns a detached goroutine after each
successful call to extractAndUpsertTokenUsage, which:
  1. Parses usage.input_tokens / usage.output_tokens from result.usage
     (JSON-RPC wrapper) with fallback to top-level usage (direct Anthropic).
  2. Calls upsertTokenUsage — INSERT ... ON CONFLICT DO UPDATE so multi-
     call days accumulate correctly. Estimated cost = input×$0.000003 +
     output×$0.000015 (Claude Sonnet default; adjustable in a later phase).
  Token tracking never blocks the critical A2A path.

New endpoint: GET /workspaces/:id/metrics (wsAuth — WorkspaceAuth bearer
bound to :id). Returns:
  {"input_tokens":N,"output_tokens":N,"total_calls":N,
   "estimated_cost_usd":"0.000000","period_start":"...","period_end":"..."}
404 if workspace missing. Period is current UTC day.

11 new tests (4 handler + 7 parse-unit); 19/19 packages pass.

Closes #593

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 05:29:10 +00:00
Molecule AI Backend Engineer
c2891b5aba feat(platform): AG-UI compatible SSE endpoint for streaming agent events (#590)
- Add in-process SSE subscription mechanism to Broadcaster (SubscribeSSE,
  deliverToSSE) so both RecordAndBroadcast *and* BroadcastOnly fan out to
  SSE subscribers — critical because BroadcastOnly skips Redis pub/sub and
  would be invisible to a Redis-only subscriber (AGENT_MESSAGE, A2A_RESPONSE,
  TASK_UPDATED are all BroadcastOnly events).
- Add handlers/sse.go: SSEHandler.StreamEvents sets text/event-stream headers,
  checks workspace existence (404 if missing), subscribes via broadcaster, and
  wraps each WSMessage in an AG-UI envelope:
    data: {"type":"<event>","timestamp":<unix_ms>,"data":{...}}\n\n
- Register wsAuth.GET("/workspaces/:id/events/stream") behind existing
  WorkspaceAuth middleware — bearer token bound to :id.
- Add 6 tests: Content-Type, initial ping, AG-UI format, workspace filter
  (cross-workspace events not leaked), 404 on missing workspace, multiple
  sequential events.

All 19 packages pass. Build clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 05:16:51 +00:00
Molecule AI Backend Engineer
3e1e68004d fix(security): add AdminAuth to /admin/workspaces/:id/test-token route
Without middleware, any caller on a non-production instance could mint a
bearer token for any workspace UUID with no authentication. AdminAuth is
defence-in-depth: on a fresh install (no tokens yet) it is fail-open so
the bootstrap path still works; once the first workspace enrolls a token
all callers must present a valid bearer.

Adds two router-level tests confirming the gate:
- TestTestTokenRoute_RequiresAdminAuth_WhenTokensExist → 401 with no header
- TestTestTokenRoute_FailOpenOnFreshInstall → 200 (bootstrap path intact)

Env-var gating inside GetTestToken is retained as a second layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 02:48:00 +00:00
Hongming Wang
b0ec35e644 fix(auth): TenantGuard same-origin bypass for EC2 tenant Canvas
On EC2 tenant instances, Caddy serves Canvas (:3000) and API (:8080) under
the same domain. Canvas makes same-origin requests without X-Molecule-Org-Id
or Fly-Replay-Src headers, causing TenantGuard to 404 every API route.

- Add isSameOriginCanvas() as tertiary check in TenantGuard — when
  CANVAS_PROXY_URL is set and Referer/Origin matches Host, pass through.
- Enhance isSameOriginCanvas() to also check Origin header (WebSocket
  upgrade requests send Origin but may not send Referer).
- Add 3 new tests: Referer bypass, Origin bypass (WS), inactive without env.

Fixes all 404s on /workspaces, /templates, /org/templates, /approvals/pending,
/canvas/viewport, and /ws WebSocket on tenant EC2 instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:22:23 -07:00
molecule-ai[bot]
b1c976a54d
fix(github): refresh installation token when TTL < 10 min (#547) (#567)
Root cause: the github-app-auth plugin injects GH_TOKEN + GITHUB_TOKEN
into each workspace container's env at provision time (EnvMutator). Those
are GitHub App installation tokens with a fixed ~60 min TTL. The plugin
has an in-process cache that proactively refreshes 5 min before expiry —
but the workspace env is set once at container start and never updated.
Any workspace alive >60 min ends up with an expired token.

Fix (Option B — on-demand endpoint):

pkg/provisionhook:
  - Add TokenProvider interface: Token(ctx) (token, expiresAt, error)
    Lives in pkg/ (public) so the github-app-auth plugin can implement it.
  - Add Registry.FirstTokenProvider() — discovers the first mutator that
    also satisfies TokenProvider via interface assertion. Safe under
    concurrent reads (existing RWMutex).

platform/internal/handlers/github_token.go:
  - New GitHubTokenHandler serving GET /admin/github-installation-token
  - Delegates to the registered TokenProvider (plugin cache — always fresh)
  - 404 if no GitHub App configured, 500 + [github] prefix log on error
  - Never logs the token itself

platform/internal/handlers/workspace.go:
  - Add TokenRegistry() getter so the router can wire the handler without
    coupling to WorkspaceHandler internals

platform/internal/router/router.go:
  - Register GET /admin/github-installation-token under AdminAuth

workspace-template/:
  - scripts/molecule-git-token-helper.sh — git credential helper; calls
    the platform endpoint on every push/fetch; falls through to next
    helper (operator PAT) if platform unreachable
  - entrypoint.sh — configure the credential helper at startup

Why Option B over Option A (background goroutine):
  - The plugin already has its own cache refresh; nothing to refresh here.
  - Pushing env updates into running containers requires docker exec, which
    the architecture explicitly rejects (issue #547 "Alternatives").
  - Pull-based is stateless, trivially testable, zero extra goroutines.

Closes #547

Co-authored-by: Molecule AI DevOps Engineer <devops-engineer@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:47:03 +00:00
molecule-ai[bot]
d08f237de9
fix(platform): reject self-delegation to prevent _run_lock deadlock (#570)
When a workspace delegated a task to itself, it would acquire
_run_lock twice on the same goroutine mutex, blocking permanently.

Add an early-return guard in `DelegationHandler.Delegate` that
returns HTTP 400 {"error": "self-delegation not permitted"} as soon
as sourceID == body.TargetID, before any DB or A2A work is done.

Adds TestDelegate_SelfDelegation_Rejected to delegation_test.go.

Closes #548

Co-authored-by: Molecule AI Backend Engineer <backend-engineer@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:46:20 +00:00
molecule-ai[bot]
a360b64157
fix(platform): persist secrets envelope from POST /workspaces payload (#568)
`CreateWorkspacePayload` was missing a `Secrets` field, so any
`secrets: { KEY: value }` included in a POST /workspaces body was
silently dropped by ShouldBindJSON.

Changes:
- Add `Secrets map[string]string` field to `CreateWorkspacePayload`
- Wrap workspace INSERT in a DB transaction; iterate over secrets,
  encrypt each value via `crypto.Encrypt`, and upsert into
  `workspace_secrets` within the same tx — rollback both on any failure
- Add `mock.ExpectBegin()`/`mock.ExpectCommit()`/`mock.ExpectRollback()`
  to all existing Create tests that were missing transaction expectations
- Add 3 new tests: WithSecrets_Persists, SecretPersistFails_RollsBack,
  EmptySecrets_OK

Closes #545

Co-authored-by: Molecule AI Backend Engineer <backend-engineer@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:46:17 +00:00
Hongming Wang
737dd1999b fix: restore cp_provisioner.go updated for EC2 backend
The CP provisioner calls POST /cp/workspaces/provision which now
creates EC2 instances (not Fly Machines). The tenant platform
auto-activates this when MOLECULE_ORG_ID is set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:25:43 -07:00
Molecule AI Backend Engineer
a84a33523c fix(middleware): split CSP by route type — strict for API, permissive for canvas (#450)
API routes return JSON and never need 'unsafe-inline' or 'unsafe-eval'.
Serving those directives globally defeated the purpose of CSP and gave
false security assurance. Canvas-proxied routes (NoRoute → Next.js) keep
'unsafe-inline' because React hydration requires it; 'unsafe-eval' was
already absent and is confirmed unnecessary in production builds.

Implementation:
- Add isAPIPath() helper with an explicit prefix allowlist that mirrors
  the routes registered in router/router.go
- Strict "default-src 'self'" on all /workspaces, /registry, /health,
  /admin, /metrics, /settings, /bundles, /org, /templates, /plugins,
  /webhooks, /channels, /ws, /events, /approvals paths
- Permissive CSP (unsafe-inline, no unsafe-eval) on canvas/NoRoute paths
- 4 new test functions: TestCSPAPIRoutesGetStrictPolicy (covers every
  prefix + sub-path), TestCSPCanvasRoutesGetPermissivePolicy, and
  TestIsAPIPath unit test including substring-non-match guard

Resolves #450

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 20:26:17 +00:00
rabbitblood
3609b7ab8c feat(platform): wire github-app-auth plugin for per-installation tokens
Integrates github.com/Molecule-AI/molecule-ai-plugin-github-app-auth.
When GITHUB_APP_ID is set, the platform constructs a plugin
Authenticator at boot and registers it as an EnvMutator on the
WorkspaceHandler. Every workspace provision then gets a fresh
GITHUB_TOKEN / GH_TOKEN injected from the App's installation token
(rotates ~hourly, refresh 5 min before expiry).

Verified live this turn:
- Platform boot log: `github-app-auth: registered, 1 mutator(s) in chain`
- `docker exec ws-<id> gh auth status` → `Logged in as molecule-ai[bot] (GH_TOKEN)`
- `gh issue list --repo Molecule-AI/molecule-core` returns real data
  (Hermes #498/#499/#500 visible from inside a workspace container)

## Changes
- platform/go.mod + go.sum: new dep on the plugin
- platform/cmd/server/main.go: import + conditional registration
  (soft-skip when GITHUB_APP_ID is unset for self-hosted/dev)
- docker-compose.yml: pass GITHUB_APP_* env + bind-mount private key

## Drive-by
.gitignore: exclude /org-templates /plugins /workspace-configs-templates
— these dirs are populated locally by clone-manifest.sh from the
standalone repos, should never be committed to core. Without this rule
my previous git add -A staged 33 embedded git dirs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:52:20 -07:00
Hongming Wang
b6e039cb49 fix: code review findings — dead code, DRY, rate limit, docs
1. Delete fly_provisioner.go — superseded by control plane architecture.
   Direct Fly provisioning from tenant was intentionally removed.

2. Extract loadWorkspaceSecrets() — shared by Docker + CP provisioner
   paths. Eliminates 30-line secret-loading duplication.

3. Token rate limit — max 50 active tokens per workspace. Returns 429
   if exceeded. Prevents unbounded token creation by compromised client.

4. CLAUDE.md — add GET/POST/DELETE /workspaces/:id/tokens to route table.

5. .env.example — document MOLECULE_ORG_ID and CP_PROVISION_URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:04:37 -07:00
Hongming Wang
1ea615df4c feat(platform): auto-detect SaaS tenant → control plane provisioner
No env vars to configure. The platform auto-detects the backend:

  MOLECULE_ORG_ID set → SaaS tenant → control plane provisioner
  MOLECULE_ORG_ID empty → self-hosted → Docker provisioner

The control plane URL defaults to https://api.moleculesai.app (override
with CP_PROVISION_URL for testing). No FLY_API_TOKEN on the tenant.

Removed: direct Fly provisioner (FlyProvisioner) — all SaaS workspace
provisioning goes through the control plane which holds the Fly token
and manages billing, quotas, and cleanup.

Two backends: CPProvisioner (SaaS) and Docker Provisioner (self-hosted).

Closes #494

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:50:52 -07:00
Hongming Wang
1949846001 fix(auth): allow nesting + delete from tenant canvas (same-origin)
PATCH /workspaces/:id field-level auth for parent_id/tier/runtime
required a bearer token, blocking canvas nesting (drag-to-nest).
Added IsSameOriginCanvas check so the tenant canvas can update
sensitive fields without a bearer.

Exported IsSameOriginCanvas from middleware package so workspace.go
can call it for the field-level auth path.

DELETE /workspaces/:id is behind AdminAuth which already has the
same-origin check — if delete still fails, it's a different issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:22:45 -07:00
Hongming Wang
7160d1a1a8 feat(platform): Fly Machines provisioner for SaaS workspace deployment
When CONTAINER_BACKEND=flyio, workspaces are provisioned as Fly Machines
instead of local Docker containers. This enables workspace deployment
on SaaS tenants where no Docker daemon is available.

New files:
- provisioner/fly_provisioner.go: FlyProvisioner with Start/Stop/
  IsRunning/Restart/Close via Fly Machines API (api.machines.dev/v1)
- FlyRuntimeImages maps runtimes to GHCR image tags

Changes:
- main.go: select Docker vs Fly based on CONTAINER_BACKEND env var
- workspace.go: SetFlyProvisioner() setter, Create checks flyProv first
- workspace_provision.go: provisionWorkspaceFly() loads secrets, calls
  FlyProvisioner.Start, issues auth token for the new machine

Env vars for Fly backend:
- CONTAINER_BACKEND=flyio (activates Fly provisioner)
- FLY_API_TOKEN (Fly deploy token)
- FLY_WORKSPACE_APP (Fly app name for workspace machines)
- FLY_REGION (default: ord)

Closes #494

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:51:15 -07:00
Hongming Wang
96b909b8f3 fix: code review findings — token UI, auth hardening, WS dedup
1. Settings panel: wire TokensTab into "API Tokens" tab (was imported
   but not rendered). Rename "API Keys" → "Secrets", add "API Tokens"
   tab. Fix docs link → doc.moleculesai.app/docs/tokens.

2. Referer match hardening: require exact host match or trailing slash
   to prevent evil.com subdomain bypass. Cache CANVAS_PROXY_URL at
   init time instead of per-request os.Getenv.

3. Extract shared deriveWsBaseUrl() to lib/ws-url.ts — eliminates
   duplicate 12-line derivation in socket.ts and TerminalTab.tsx.

4. Token list pagination: add ?limit= and ?offset= params (default
   50, max 200) to GET /workspaces/:id/tokens.

507/507 canvas tests pass, Go build + vet clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:42:26 -07:00
Hongming Wang
c4b56c6c84 fix(auth): allow same-origin canvas requests through WorkspaceAuth on tenant
WorkspaceAuth only accepted bearer tokens, blocking the canvas from
calling per-workspace routes (restart, config, secrets, chat) on the
tenant image where canvas + API share the same origin.

Added isSameOriginCanvas() fallback (same check used by AdminAuth):
checks Referer matches request Host, gated behind CANVAS_PROXY_URL
so only tenant deployments are affected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:06:33 -07:00
Hongming Wang
25bd9241d1 fix(tenant): WebSocket URL derivation + AdminAuth same-origin for tenant image
Two bugs on the combined tenant image (canvas + API same-origin):

1. WebSocket URL: NEXT_PUBLIC_WS_URL="" (empty string for same-origin)
   was preserved by ?? operator, producing an invalid WS URL. Now derives
   from window.location when both env vars are empty. Same fix applied
   to TerminalTab.

2. AdminAuth blocking canvas: same-origin requests have no Origin header,
   so neither AdminAuth nor CanvasOrBearer could authenticate the canvas.
   Added isSameOriginCanvas() that checks Referer against request Host,
   gated behind CANVAS_PROXY_URL (only active on tenant image). This
   lets the canvas create/list workspaces, view events, etc. without a
   bearer token when served from the same Go process.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:43:01 -07:00
Hongming Wang
de9f3d179c feat(platform): token management API + MCP setup + external agent guide
1. Token Management API (closes production gap):
   - GET /workspaces/:id/tokens — list tokens (prefix + metadata, never plaintext)
   - POST /workspaces/:id/tokens — create new token (plaintext returned once)
   - DELETE /workspaces/:id/tokens/:tokenId — revoke specific token
   - Behind WorkspaceAuth middleware (need existing token to manage tokens)
   - Tests skip gracefully when no DB available

2. MCP Server Setup:
   - Fix .mcp.json to use npx @molecule-ai/mcp-server (was referencing
     non-existent local ./mcp-server/dist/index.js)
   - Add comprehensive tool→API mapping doc (87 tools across 15 categories)

3. External Agent Registration Guide:
   - Step-by-step: create workspace, register, heartbeat, A2A messaging
   - Python (Flask) and Node.js (Express) complete working examples
   - Communication rules, lifecycle, security, troubleshooting

4. Token Management Guide:
   - Bootstrap flow, rotation procedure, security properties

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:37:42 -07:00
Hongming Wang
206564c90b
Merge pull request #483 from Molecule-AI/fix/platform-modular-template-support
fix(platform): unblock org-template imports against modular workspace templates
2026-04-16 07:55:26 -07:00
rabbitblood
ff2394c085 fix(platform): unblock org-template imports against modular workspace templates
Two adjacent fixes that surfaced trying to bring the molecule-dev org
template back up against the new standalone workspace-template-* repos.

1) handlers/org.go — expand ${VAR} in workspace_dir before validation.
   The molecule-dev pm/workspace.yaml (and any operator's per-host
   binding) ships `workspace_dir: ${WORKSPACE_DIR}` so each operator
   can pick the host path PM bind-mounts. Without expansion the literal
   "${WORKSPACE_DIR}" string reaches validateWorkspaceDir and fails with
   "must be an absolute path", aborting the whole org import.
   Other fields (channel config, prompts) already go through expandWithEnv;
   workspace_dir was the last hold-out.

2) provisioner/provisioner.go — inject PYTHONPATH=/app for every
   workspace container. Standalone template Dockerfiles COPY adapter.py
   to /app and set ENV ADAPTER_MODULE=adapter, but molecule-runtime is
   a pip console_script entry point so cwd isn't on sys.path
   automatically. Setting PYTHONPATH here fixes every adapter image at
   once instead of needing 8 PRs against template repos. Operator
   override still wins (workspace EnvVars are appended after, so Docker
   takes the later duplicate).

   Note: this unblocks the import path but does NOT make claude-code /
   hermes / etc. boot. The runtime itself has a separate top-level
   `from adapters import` that breaks against modular templates —
   tracked at workspace-runtime#1.

Tests: TestBuildContainerEnv_InjectsPYTHONPATH +
TestBuildContainerEnv_WorkspaceEnvVarsCanOverridePYTHONPATH lock the
default + operator-override invariants. expandWithEnv is already covered
by TestExpandWithEnv_* — the workspace_dir use site is a one-line call
to that primitive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:49:45 -07:00
rabbitblood
e7710d2e6f feat(channels): Lark / Feishu adapter (outbound webhook + Events API inbound)
New ChannelAdapter implementation for Lark (international, open.larksuite.com)
and Feishu (China, open.feishu.cn). Both speak the same payload format —
only the host differs — so a single adapter covers both.

Outbound: POST text to a Custom Bot webhook URL with msg_type:"text".
Lark returns 200 OK even when delivery fails — the body's `code` field is
the truth. Adapter parses the response and returns a Go error when
code != 0 so callers don't think a revoked-webhook send succeeded.

Inbound: handles both v1 url_verification (handshake) and v2 event_callback
(im.message.receive_v1) shapes. Optional verify_token field — when set,
inbound payloads with mismatching tokens are rejected via constant-time
compare (#337 class — never raw == against a stored secret).

Sender ID resolution prefers user_id → falls back to open_id (open_id is
always present; user_id only when the bot has the contacts permission).
Non-text message types and non-message events return nil, nil so the
receiver responds 200 OK without dispatching.

Tests: 23 cases — identity, ValidateConfig (6 sub-cases incl. URL prefix
matrix), SendMessage (no URL / invalid prefix / happy-path body shape /
api-error-code surfacing), ParseWebhook (handshake + token mismatch +
text message + open_id fallback + non-message + non-text + token mismatch
+ malformed JSON + malformed content + empty text), StartPolling no-op,
registry presence.

Also: make migration 023 idempotent (ADD COLUMN IF NOT EXISTS) — the
platform's migration runner has no schema_migrations tracking table, so
every .up.sql replays on every boot. Without IF NOT EXISTS the second
boot against an existing volume crashes with "column already exists".
Followup issue to be filed for proper migration tracking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:10:58 -07:00
rabbitblood
e08f28c962 feat(platform): provision-time env mutator hook for plugins
Add `provisionhook.EnvMutator` extension point so out-of-tree plugins
(e.g. github-app-auth, vault-secrets) can inject or override env vars
right before container Start, without forking core or piling more
provider-specific code into the handlers package.

WorkspaceHandler gains an optional `envMutators *provisionhook.Registry`
wired in via SetEnvMutators during boot. The hook fires after built-in
secret loads + per-agent git identity, so plugins can both read what's
already there and override anything they own (GIT_AUTHOR_*, GITHUB_TOKEN).

A nil registry is a no-op via Registry.Run's nil-receiver branch — keeps
the hot path a single nil compare and means existing flows stay green
even with zero plugins registered.

Mutator failure aborts provisioning and marks the workspace failed with
the wrapped error in last_sample_error. Failing fast surfaces the cause
to the operator instead of letting an agent boot into opaque "git push
401" loops it can never recover from on its own.

Tests cover ordered execution, chained env visibility, first-error abort,
nil-receiver no-op, nil-mutator drop, registration order, and concurrent
register-vs-run safety (-race clean).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 06:47:09 -07:00