Commit Graph

142 Commits

Author SHA1 Message Date
molecule-ai[bot]
fa2d4cde3e feat(platform): Temporal checkpoint DB persistence layer (closes #788)
feat(platform): Temporal checkpoint DB persistence layer (#788)
2026-04-17 19:05:48 +00:00
molecule-ai[bot]
bfc6e56aa5 fix(security): plugin supply chain hardening — SAFE-T1102 (closes #768)
fix(security): plugin supply chain hardening — SAFE-T1102 (issue #768)
2026-04-17 19:04:04 +00:00
Molecule AI Backend Engineer
b6d9af5fc2 feat(platform): Temporal checkpoint DB persistence layer (#788)
Adds step-level checkpoint storage so workflows can resume from the
last completed step after a crash or restart without replaying prior work.

- Migration: `workflow_checkpoints` table — workspace_id (FK + CASCADE),
  workflow_id, step_name, step_index, completed_at, payload JSONB.
  UNIQUE(workspace_id, workflow_id, step_name) + covering index on
  (workspace_id, workflow_id, completed_at DESC).

- Handlers (platform/internal/handlers/checkpoints.go):
  POST   /workspaces/:id/checkpoints        — upsert via ON CONFLICT DO UPDATE
  GET    /workspaces/:id/checkpoints/:wfid  — list steps ordered step_index DESC
  DELETE /workspaces/:id/checkpoints/:wfid  — clear on clean shutdown (404 if none)

- Router: all three routes on the wsAuth group (WorkspaceAuth middleware);
  workspace A's token cannot reach workspace B's checkpoints.

- Tests (11 cases, sqlmock + race-safe): upsert-insert, upsert-update,
  payload forwarding, list-ordered, list-not-found, rows.Err() → 500,
  delete-success, delete-not-found, callerMismatch 403 on all 3 endpoints.

Closes #788. Parent: #583-1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 18:36:12 +00:00
molecule-ai[bot]
0195308b73 feat: pgvector semantic search for agent memory recall (#576)
Rebase of feat/issue-576-pgvector-semantic-memory onto current main,
preserving the #767 security layer (globalMemoryDelimiter + GLOBAL audit
log) that predates this branch.

Changes layered on top of main:
- Migration 031: embedding vector(1536) column + ivfflat cosine-ops index
  (renumbered from 029 — 029/030 were taken by workspace-hibernation and
  audit-events)
- Commit: embed-on-write after INSERT, non-fatal on embedding failure
- Search: semantic cosine-distance path when EmbeddingFunc is wired up;
  falls back to FTS/ILIKE; GLOBAL delimiter wrapping applies on both paths
- EmbeddingFunc injection pattern; WithEmbedding chainable builder

All security invariants preserved:
- globalMemoryDelimiter wrapping on GLOBAL scope in both semantic + FTS
- GLOBAL write audit log (SHA-256 forensic trail) in Commit
- TestRecallMemory_GlobalScope_HasDelimiter passes
- TestMemoriesCommit_Global_AsRoot passes
- 3 new pgvector tests pass

Co-authored-by: molecule-ai[bot] <276602405+molecule-ai[bot]@users.noreply.github.com>
2026-04-17 17:19:45 +00:00
molecule-ai[bot]
d3a9bc841e fix(security): plugin supply chain hardening — SAFE-T1102 (#768)
Add two defenses against malicious plugins from uncontrolled sources:

1. **Pinned-ref enforcement** (resolveAndStage): github:// install/download
   specs without a #<tag/sha> suffix are now rejected with HTTP 422. A
   mutable default-branch tip could change between audit and install,
   silently swapping in untrusted code. Override via PLUGIN_ALLOW_UNPINNED=true.

2. **SHA-256 content integrity** (installRequest.sha256): callers may
   supply the expected hex SHA-256 of the fetched plugin.yaml. When present,
   resolveAndStage verifies the digest after staging; a mismatch aborts the
   install with HTTP 422 and cleans up the staging dir.

Updated TestPluginDownload_GithubSchemeStreamsTarball to use a pinned ref
(#v1.0.0) so it reflects the new security requirement.

Tests: 4 new (TestPluginInstall_SHA256Mismatch_AbortsInstall,
TestPluginInstall_SHA256Match_Succeeds, TestPluginInstall_UnpinnedRef_Rejected,
TestPluginInstall_PinnedRef_Accepted). All 15 packages green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:37:45 +00:00
molecule-ai[bot]
255c888ca1 Merge pull request #651 from Molecule-AI/feat/issue-594-audit-ledger
feat: molecule-audit-ledger — HMAC-SHA256 immutable agent event log (#594)
2026-04-17 16:37:01 +00:00
molecule-ai[bot]
8b59a1cb9a Merge pull request #724 from Molecule-AI/feat/issue-711-workspace-hibernation
feat(registry): workspace hibernation — auto-pause idle workspaces
2026-04-17 16:36:27 +00:00
molecule-ai[bot]
c47021c19e Merge pull request #769 from Molecule-AI/fix/issue-767-global-memory-injection
fix(security): GLOBAL memory prompt injection safeguards (#767)
2026-04-17 16:35:35 +00:00
molecule-ai[bot]
ccb9317a49 Merge pull request #766 from Molecule-AI/fix/issue-761-system-caller-header-forge
fix(security): reject X-Workspace-ID system-caller prefix forgery (#761)
2026-04-17 16:35:25 +00:00
molecule-ai[bot]
7e9e105029 fix(security): GLOBAL memory prompt injection safeguards (#767)
Two defenses against GLOBAL-scope agent memory injection attacks:

1. Recall delimiter: Search() wraps every GLOBAL-scope memory value
   with a non-instructable prefix before returning it to MCP clients:
     [MEMORY id=<uuid> scope=GLOBAL from=<workspace_id>]: <value>
   This prevents stored content (e.g. "IGNORE ALL PREVIOUS INSTRUCTIONS")
   from being parsed as instructions in the agent's context window.
   Raw DB content is unchanged — the wrapper is applied on read only.

2. Write audit log: Commit() writes an activity_log entry with
   activity_type='memory_write_global' whenever a GLOBAL memory is
   stored. The entry records a SHA-256 hash of the content (never
   plaintext) alongside memory_id and namespace for forensic replay.
   Audit failure is non-fatal — a logging error must not roll back
   a successful write.

Tests:
- TestRecallMemory_GlobalScope_HasDelimiter — verifies exact delimiter
  format [MEMORY id=... scope=GLOBAL from=...]: <value>
- TestCommitMemory_GlobalScope_AuditLogEntry — verifies activity_logs
  INSERT fires on every GLOBAL write (via mock.ExpectationsWereMet)
- TestMemoriesCommit_Global_AsRoot — updated to expect the audit INSERT

All 16 Go test packages pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:26:46 +00:00
molecule-ai[bot]
d76c56e648 fix(security): reject X-Workspace-ID system-caller prefix forgery (#761)
Added an early guard in ProxyA2A() that rejects HTTP requests whose
X-Workspace-ID header passes isSystemCaller() with 403 Forbidden.

Legitimate system callers (webhooks, scheduler, restart_context) call
proxyA2ARequest() directly via ProxyA2ARequest() and never send HTTP
headers with system-caller prefixes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:15:47 +00:00
Hongming Wang
865d825993 Merge pull request #743 from Molecule-AI/feat/issue-727-opus-4-7-default
feat: upgrade default workspace model to claude-opus-4-7
2026-04-17 08:47:27 -07:00
Molecule AI QA Engineer
489f8bfb16 test(hibernation): integration tests for workspace hibernation (#711)
Cover the full hibernation feature (PR #724) + scheduler interaction (#722):

handlers/hibernation_test.go (new, 6 tests):
- HibernateWorkspace_OnlineWorkspace_Success — container stop called (nil
  provisioner guard), DB status set to 'hibernated', Redis keys cleared
  (ws:{id}, ws:{id}:url, ws:{id}:internal_url), WORKSPACE_HIBERNATED broadcast
- HibernateWorkspace_NotEligible_NoOp — ErrNoRows → early return, no UPDATE,
  Redis keys untouched
- HibernateWorkspace_DBUpdateFails_NoCrash — UPDATE error → no panic, no broadcast
- HibernateHandler_Online_Returns200 — HTTP POST, online workspace → 200 {"status":"hibernated"}
- HibernateHandler_NotActive_Returns404 — not online/degraded → 404
- HibernateHandler_DBError_Returns500 — DB error → 500

a2a_proxy_test.go (2 new tests):
- ResolveAgentURL_HibernatedWorkspace_Returns503WithWaking — empty Redis + DB
  returns status=hibernated/url="" → 503 + Retry-After:15 + {waking:true,retry_after:15}
- ResolveAgentURL_HibernatedWorkspace_NullURLVariant — same with SQL NULL url

scheduler_test.go (1 new test):
- RepairNullNextRunAt_HibernatedWorkspace_ScheduleRepaired — repair query has
  no workspace status filter; hibernated workspace's schedule still gets
  next_run_at repaired so it fires on wake

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:44:41 +00:00
Molecule AI QA Engineer
a0a84b9d22 chore: merge main into test/issue-711-hibernation-integration (gets scheduler #722 fix) 2026-04-17 15:40:56 +00:00
Molecule AI Backend Engineer
ec4309138b feat: upgrade default workspace model to claude-opus-4-7 (#727)
Replace the anthropic:claude-sonnet-4-6 default across config, handlers,
env example, and litellm proxy config. All tests updated to match the new
default; sonnet-4-6 alias kept in litellm_config.yml for pinned workspaces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:30:57 +00:00
Molecule AI Backend Engineer
84584af2e6 fix(a2a): restore delivery_confirmed body-read logic removed by hibernation commit (#689)
The hibernation PR (5c1a9d0) accidentally removed the delivery_confirmed
fix that was introduced for issue #689. When io.ReadAll fails after the
target has already responded with headers (200-399), the message WAS
delivered — stripping delivery_confirmed from the error response caused
callers to treat a successful send as a hard failure.

Restore the full original body-read error block:
- deliveryConfirmed flag (true when status 200-399)
- log line with status/bytes_read context
- logA2ASuccess call when deliveryConfirmed (audit trail accuracy)
- proxyA2AError.Response includes "delivery_confirmed" field so callers
  can distinguish "not delivered" from "delivered, body lost"

The hibernation auto-wake feature (resolveAgentURL status='hibernated'
check) is orthogonal and untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:14:25 +00:00
molecule-ai[bot]
7cdbf8175e fix(scheduler): prevent NULL next_run_at from permanently dropping schedules (#722)
Three bugs caused enabled schedules to silently disappear from the fire query
(which requires next_run_at IS NOT NULL AND next_run_at <= now()):

Bug 1 - fireSchedule() and recordSkipped(): when ComputeNextRun returned an
error, nextRunPtr stayed nil and UPDATE SET next_run_at = $2 wrote NULL.
Fix: change to COALESCE($2, next_run_at) so the existing DB value is preserved
when $2 is NULL, and log the error explicitly.

Bug 2 - org importer (handlers/org.go): nextRun, _ := ComputeNextRun(...)
silently discarded the error. A bad cron expression would pass time.Time{}
(zero value) to the INSERT. Fix: surface the error, log it, and skip the
schedule INSERT via continue.

Bug 3 - no startup repair: schedules already NULL'd by the pre-fix binary
would never recover. Fix: Start() now calls repairNullNextRunAt() once on
boot, recomputing next_run_at for every enabled schedule with a NULL value.

Tests: TestFireSchedule_ComputeNextRunError, TestRecordSkipped_ComputeNextRunError,
TestRepairNullNextRunAt_RepairsRows, TestRepairNullNextRunAt_DBError_NoPanic,
TestOrgImport_ScheduleComputeError (all pass).

Fixes #722

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:34:28 +00:00
molecule-ai[bot]
5c1a9d0f24 feat(registry): workspace hibernation — auto-pause idle workspaces (#711)
Implements automatic workspace hibernation for workspaces that have been idle
longer than their configured hibernation_idle_minutes threshold.

Changes:
- migrations/029: Add hibernation_idle_minutes INT DEFAULT NULL column +
  partial index on workspaces table
- registry/hibernation.go: New StartHibernationMonitor goroutine that ticks
  every 2 min and calls hibernateIdleWorkspaces via the HibernateHandler
  callback (same import-cycle-prevention pattern as OfflineHandler)
- registry/hibernation_test.go: 5 unit tests covering handler calls, no-rows,
  DB error, tick behaviour, and context-cancel shutdown
- handlers/workspace_restart.go: New Hibernate() HTTP handler (POST
  /workspaces/:id/hibernate) + HibernateWorkspace(ctx, id) method — stops
  container, sets status='hibernated', clears Redis keys, broadcasts event
- handlers/a2a_proxy.go: Auto-wake in resolveAgentURL — when status='hibernated'
  and URL is empty, triggers async RestartByID and returns 503 + Retry-After: 15
  so callers can retry transparently
- registry/liveness.go: Exclude 'hibernated' workspaces from offline detection
- router.go: Register POST /workspaces/:id/hibernate under wsAuth group
- cmd/server/main.go: Wire hibernation monitor via supervised.RunWithRecover

Closes #711

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:27:39 +00:00
molecule-ai[bot]
37524ebe8f Merge pull request #719 from Molecule-AI/fix/issue-697-validate-token-removed-workspace
fix(wsauth): add removed-workspace JOIN to ValidateToken (#697)
2026-04-17 12:50:52 +00:00
Hongming Wang
3842d92ceb Merge pull request #696 from Molecule-AI/fix/issue-682-684-683-auth-token-fixes
fix(security): metrics auth, token revocation hardening, A2A false-negative (#682 #683 #689)
2026-04-17 05:47:08 -07:00
molecule-ai[bot]
523c0b9aa7 fix(wsauth): add removed-workspace JOIN to ValidateToken (#697)
Defense-in-depth: workspace-scoped ValidateToken now rejects tokens
belonging to workspaces with status='removed' at the DB layer, even
when revoked_at IS NULL. Mirrors the same guard added to ValidateAnyToken
in #696. Updated all test mock patterns (workspace_test, a2a_proxy_test,
secrets_test, admin_test_token_test, middleware) to match the new JOIN query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:46:27 +00:00
Molecule AI QA Engineer
5fd25dc0df test(security): regression suite for input validation fixes (#685 #686 #687 #688)
30 test cases covering all four security fixes from PR #701:

  #686 — AdminAuth gate on GET /templates and GET /org/templates:
    - NoAuth returns 401 when tokens are enrolled
    - FreshInstall fails open (bootstraps correctly)

  #687 — UUID path param validation:
    - URL-encoded traversal (..%2f..%2fetc%2fpasswd) → 400
    - Non-UUID strings (not-a-uuid, ws-123, XSS payloads) → 400
    - Valid UUIDs pass through (regression check)

  #688 — Field length limits:
    - name=256, role=1001, model=101 chars → 400
    - Exact-boundary values (255/1000/100) → pass (off-by-one guard)

  #685 — YAML injection via newline/CR:
    - Newline in name, CR in role → 400
    - YAML multi-field injection payload "agent\nrole: injected" → 400

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:37:13 +00:00
molecule-ai[bot]
bdd56b1489 fix(security): rebase #685-688 onto main — preserve wsAuth PATCH, add yamlSpecialChars
- Rebased onto 350288f1 (main HEAD, post-#692 IDOR fix)
- PATCH /workspaces/:id remains under wsAuth group (not open router)
- Added validateWorkspaceID (uuid.Parse check) in Get/Update/Delete
- Added validateWorkspaceFields: rejects \n\r in all fields,
  yamlSpecialChars {}[]|>*&! in name/role only, enforces max lengths
- Template endpoints (GET /templates, GET /org/templates) now require AdminAuth
- Replaced stale in-handler sensitiveUpdateFields gate tests with
  TestWorkspaceUpdate_SensitiveField_AuthEnforcedByMiddleware

Closes #685 #686 #687 #688
2026-04-17 12:13:44 +00:00
molecule-ai[bot]
112c17510c fix(security): revert #684 schema migration, restore /admin/schedules/health, add ADR-001
Required changes from security auditor before PR #696 can merge:

1. REVERT #684 (token_type schema migration):
   - Remove migration 029_token_type.{up,down}.sql
   - Revert wsauth/tokens.go — remove IssueAdminToken, token_type constants,
     restore HasAnyLiveTokenGlobal and ValidateAnyToken to pre-#684 behavior
   - Revert admin_test_token.go to use IssueToken (not IssueAdminToken)
   - Revert associated tests to pre-#684 patterns
   Path B: formal risk acceptance documented in ADR-001.

2. RESTORE /admin/schedules/health route (regression fix):
   - Add platform/internal/handlers/admin_schedules_health.go (from PR #671)
   - Add platform/internal/handlers/admin_schedules_health_test.go (from PR #671)
   - Wire GET /admin/schedules/health via AdminAuth in router.go

3. ADD ADR-001 (platform/docs/adr/ADR-001-admin-token-scope.md):
   - Documents #684 as known risk with Phase-H remediation plan
   - Phase-H tracking issue: Molecule-AI/molecule-core#710
2026-04-17 12:01:12 +00:00
molecule-ai[bot]
643ffc6648 fix(security): add token_type column — workspace tokens rejected by AdminAuth (#684)
Security Auditor confirmed: ValidateAnyToken accepted any live workspace
token, meaning a workspace agent bearer could satisfy AdminAuth and reach
/bundles/import, /events, /org/import, /settings/secrets, etc.

Fix: add token_type TEXT ('workspace' | 'admin') to workspace_auth_tokens.

Migration 029:
- ALTER workspace_id DROP NOT NULL (admin tokens have no workspace scope)
- ADD COLUMN token_type TEXT NOT NULL DEFAULT 'workspace'
- ADD CONSTRAINT token_type_check (IN 'workspace', 'admin')
- ADD CONSTRAINT scope_check (workspace tokens MUST have workspace_id;
  admin tokens MUST have workspace_id = NULL)

Code changes:
- IssueToken: explicitly inserts token_type = 'workspace'
- IssueAdminToken (new): inserts NULL workspace_id + token_type = 'admin'
- ValidateAnyToken: now filters WHERE token_type = 'admin' — workspace
  tokens unconditionally fail
- HasAnyLiveTokenGlobal: counts only admin tokens
- admin_test_token.go: GetTestToken calls IssueAdminToken (#684)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 11:47:31 +00:00
molecule-ai[bot]
108d257833 fix(a2a): surface delivery_confirmed + prevent 503-busy double-delivery (#689)
Two targeted fixes for the A2A false-negative (delivery succeeded but caller
receives A2A_ERROR):

Body-read failure: when Do() succeeds (target sent 2xx headers — delivery
confirmed) but io.ReadAll(resp.Body) fails, proxy now returns
{"delivery_confirmed": true} in the 502 body and logs the activity as
successful. Audit trail records true delivery, not a false failed entry.

isTransientProxyError fix: delegation retry loop now only retries 503s with
{restarting: true} (container died, message NOT delivered). 503 {busy: true}
signals the agent IS processing the delivered message — retrying causes
double-delivery. Fix prevents the double-delivery race.

All 16 packages pass: go test ./...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 11:26:28 +00:00
molecule-ai[bot]
1bbda53142 Merge pull request #692 from Molecule-AI/fix/issue-680-681-workspace-auth
fix(security): auth+ownership on PATCH /workspaces/:id (#680 #681)
2026-04-17 11:03:25 +00:00
molecule-ai[bot]
c825e44b50 Merge pull request #659 from Molecule-AI/infra/rebuild-runtime-images-script
infra: add rebuild-runtime-images.sh — patches all 6 adapter images with git credential helper (#658)
2026-04-17 10:59:33 +00:00
molecule-ai[bot]
627946528d fix(security): add auth+ownership to PATCH /workspaces/:id (#680 #681)
ISSUE #680 — IDOR on PATCH /workspaces/🆔
- Route was on the open router with no auth middleware. Any unauthenticated
  caller could rename, change role, or update any workspace field of any
  workspace ID without credentials (zero auth + no ownership check).
- Fix: register under wsAuth (WorkspaceAuth middleware) which (a) requires a
  valid bearer token and (b) validates the token belongs to the target
  workspace, providing auth + ownership in a single check.
- Remove the now-redundant in-handler field-level auth block — the middleware
  is a strictly stronger gate. Dead code gone.
- Remove unused `middleware` import from workspace.go.
- Update tests: two tests that asserted the old in-handler 401 are replaced
  by TestWorkspaceUpdate_SensitiveField_AuthEnforcedByMiddleware (documents
  that auth is now at the router layer); cosmetic-field test renamed.

ISSUE #681 — test-token endpoint auth:
- Confirmed: GET /admin/workspaces/:id/test-token already has
  middleware.AdminAuth(db.DB). No change needed — finding was from older state.

Build: `go build ./...` clean. All 15 test packages pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:55:23 +00:00
molecule-ai[bot]
470704416e fix(security): Ed25519 signature verification for Discord webhooks + strip token from error chain
HIGH (#659-1): POST /webhooks/discord had no signature verification, allowing
any attacker to POST forged Discord slash-command payloads. Add Ed25519
verification via verifyDiscordSignature() before adapter.ParseWebhook() is
called. The function reads r.Body, verifies Ed25519(pubKey, timestamp+body,
X-Signature-Ed25519), then restores r.Body with io.NopCloser so ParseWebhook
can still read the payload. The public key is resolved from the first enabled
Discord channel's app_public_key config (plaintext — it is a public key and
not in sensitiveFields) with a fallback to DISCORD_APP_PUBLIC_KEY env var;
no key configured -> 401 (fail-closed). discordPublicKey() is the DB helper.

MEDIUM (#659-2): discord.go SendMessage() wrapped http.Client.Do errors with
%w, propagating the *url.Error which includes the full webhook URL
(https://discord.com/api/webhooks/{id}/{token}) into logs and error responses.
Replace with a static "discord: HTTP request failed" string.

Tests added (11 new):
- TestVerifyDiscordSignature_Valid / _WrongKey / _TamperedBody /
  _MissingTimestamp / _MissingSignature / _InvalidHexSignature /
  _InvalidHexPubKey / _WrongLengthPubKey (real Ed25519 key pairs)
- TestChannelHandler_Webhook_Discord_NoKey_Returns401
- TestChannelHandler_Webhook_Discord_InvalidSig_Returns401
- TestChannelHandler_Webhook_Discord_ValidSig_PingAccepted
- TestDiscordAdapter_SendMessage_ErrorDoesNotLeakToken

go test ./... green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:36:51 +00:00
molecule-ai[bot]
6e4979954b feat(platform): add GET /admin/schedules/health for cross-workspace schedule monitoring (#618)
Operators and audit agents can now detect silent cron failures across all
workspaces with a single AdminAuth-gated request — no per-workspace bearer
tokens required. This closes the proactive detection gap that left issue #85
(cron died silently 10+ hours) undetectable until users noticed missing work.

Changes:
- platform/internal/handlers/admin_schedules_health.go: new AdminSchedulesHealthHandler
  - GET /admin/schedules/health joins workspace_schedules + workspaces (excluding
    removed workspaces), computes status (ok|stale|never_run) and
    stale_threshold_seconds (2 × cron interval via scheduler.ComputeNextRun)
  - computeStaleThreshold() and classifyScheduleStatus() extracted as
    package-level helpers for direct unit testing
- platform/internal/handlers/admin_schedules_health_test.go: 16 tests
  - Unit tests for computeStaleThreshold (5min/hourly/daily crons, invalid expr,
    invalid timezone) and classifyScheduleStatus (never_run/stale/ok/zero-threshold)
  - Integration tests via sqlmock: empty result, never_run classification,
    stale detection, ok status, DB error → 500, multi-workspace response,
    required JSON fields coverage
- platform/internal/router/router.go: register GET /admin/schedules/health
  behind middleware.AdminAuth(db.DB), mirroring the /admin/liveness gate

Closes #618

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:28:55 +00:00
Molecule AI Backend Engineer
3895e02e01 fix(security): address Security Auditor findings on audit-ledger (#651)
- Replace == HMAC comparisons with hmac.compare_digest (Python) and
  hmac.Equal (Go) in ledger.py, verify.py, and audit.go to prevent
  timing oracle attacks (Fixes 1-6)
- Increase PBKDF2 iterations from 100K to 210K in both ledger.py and
  audit.go — must match for cross-language verification (Fix 7)
- Return chain_valid: null when offset > 0 (paginated views cannot
  verify a truncated chain; null means "not computed") (Fix 8)
- Remove module-level AUDIT_LEDGER_SALT attribute from ledger.py; read
  the secret exclusively from os.environ inside _get_hmac_key() so the
  salt is not exposed in the module namespace (Fix 9)
- Update tests: use monkeypatch.setenv/delenv instead of setattr on the
  removed AUDIT_LEDGER_SALT attribute; update testAuditKey helper to
  use 210K iterations; add TestAuditQuery_PaginatedOffsetReturnsNullChainValid
- Fix migration 028: workspace_id column type TEXT → UUID to match
  workspaces.id UUID primary key

All tests pass: 1043 pytest + 0 Go test failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 07:30:10 +00:00
Molecule AI Backend Engineer
54737d58a2 feat(platform): merge stacked system messages for Hermes/vLLM (#499)
vLLM (and Nous Hermes portal) only accept a single system message.
When the platform builds a messages array from multiple sources
(base system prompt + workspace config + per-session override), the
consecutive system entries at the front cause vLLM to reject or
silently drop all but the first.

Adds mergeSystemMessages() — a stateless pre-flight transform in the
handlers package that collapses the uninterrupted leading run of
{"role":"system"} entries into one, joining their content with "\n\n".
Non-system messages between system messages are not touched; a single
system message is returned as-is (no allocation).

10 unit tests cover: stacked merge, single-unchanged, no-system passthrough,
three-message collapse, interleaved user (trailing system not merged),
only-system-messages, empty slice, nil slice, non-string content, and
assistant-leading passthrough.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 07:19:30 +00:00
Molecule AI Backend Engineer
fff063bd15 feat: molecule-audit-ledger — HMAC-SHA256 immutable agent event log (#594)
Implements EU AI Act Annex III compliance (Art. 12 record-keeping, Art. 13
transparency) via an append-only HMAC-SHA256-chained agent event log.

Python (workspace-template/molecule_audit/):
- ledger.py: SQLAlchemy 2.0 AuditEvent model + PBKDF2 key derivation +
  append_event() with prev_hmac chain linkage + verify_chain() CLI helper.
- hooks.py: LedgerHooks — on_task_start/on_llm_call/on_tool_call/on_task_end
  pipeline hooks; exception-safe (_safe_append); context manager support.
- verify.py: `python -m molecule_audit.verify --agent-id <id>` CLI;
  exits 0=valid, 1=broken, 2=missing SALT, 3=DB error.
- tests/test_audit_ledger.py: 46 tests covering HMAC determinism, field
  sensitivity, chain verification, LedgerHooks lifecycle, CLI.

Go (platform/):
- migrations/028_audit_events.up.sql: audit_events table with indexes.
- internal/handlers/audit.go: GET /workspaces/:id/audit — parameterized
  queries, inline chain verification (chain_valid: bool|null), PBKDF2
  key cached via sync.Once.
- internal/handlers/audit_test.go: 14 tests — HMAC, chain verify, handler
  query/filter/pagination/cap/error paths.
- internal/router/router.go: wire wsAuth.GET("/audit", audh.Query).
- .env.example: document AUDIT_LEDGER_SALT.
- requirements.txt: add sqlalchemy>=2.0.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:55:36 +00:00
molecule-ai[bot]
a6f4678b9e 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 dc89d8f verified. UNSTABLE = known App token scope gap.
2026-04-17 06:43:21 +00:00
Molecule AI Backend Engineer
dc89d8fd7b 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
57ff2eab78 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
7afd1cfc6d 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]
3a1c119fec 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 Backend Engineer
c1443bf52f 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
08be2ccb17 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
9223486756 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
72521866cc 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
47cb81f2bc 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
f99d8b0a1a 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
Hongming Wang
abb05c7ef9 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
19d863b052 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 Backend Engineer
cc660e5185 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[bot]
652019afcc 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]
b3963da815 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