- TestRecordSkipped_AdvancesNextRunAt: call recordSkipped directly instead
of going through fireSchedule, which now has a 2-min deferral loop (#969)
that makes sqlmock-based end-to-end testing impractical.
- TestFireSchedule_NormalSuccess_AdvancesNextRunAt: add missing expectation
for the consecutive_empty_runs reset query (#795) that fires on non-empty
successful responses.
- TestFireSchedule_ComputeNextRunError: same consecutive_empty_runs fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add production fix and three new test cases verifying that workspace
deletion cascade-disables all workspace_schedules for the deleted
workspace and its descendants, preventing zombie schedule firings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When fireSchedule panics before reaching the next_run_at UPDATE,
the deferred recover catches the panic but never advances next_run_at,
leaving it stuck in the past forever. The schedule then fires every
tick (30s) in an infinite retry loop.
Add next_run_at advancement to both panic recovery defers (the
per-goroutine one in tick() and the inner one in fireSchedule()) so
the schedule always moves forward regardless of how the fire exits.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The peers endpoint requires a workspace-scoped bearer token (see
validateDiscoveryCaller in handlers/discovery.go — designed for
agent-to-agent calls). The canvas session doesn't hold that token, so
every Details-tab open for a provisioning / failed / offline workspace
fired a 401 that cluttered devtools and lit up the error banner even
though the real UX here is "no peers — the workspace hasn't booted."
Gate the fetch on status ∈ {online, degraded} and render an empty
Peers list for everything else.
Follow-up: give the canvas a way to see peers for any workspace (admin
session should be enough). Tracked separately — this fix just quiets
the noise on the common case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document where to post (Reddit r/LocalLlama, r/ML, dev.to), required
credentials, and current status. All committed to staging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chrome DevTools MCP:
- mcp-bridge-diagram.svg: AI Agent → MCP → CDP → Chrome architecture
- comparison-table-card.svg: 3-approach comparison with cost/cred isolation
Fly.io Deploy Anywhere:
- backend-comparison-card.svg: 3 backend comparison with env vars
Social copy docs updated to reference generated assets.
Social Media Brand can use SVGs directly or screenshot for PNG export.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Draft X thread (5 posts) + LinkedIn post + visual recs for the
2026-04-17 published post. Ready for Social Media Brand review.
Coordination note: avoid same-day publish as Chrome DevTools MCP post.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All actions 1-5 complete. Action 6 outreach targets prepped.
Status updated: Marketing Lead review required before outreach.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Action 6 prep — outreach target list (Tier 1-3), email template,
priority order, monitoring plan. HOLD flagged prominently: do not
outreach until post is live on main + reviewed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Internal linking (Action 3):
- Chrome DevTools MCP post: added MCP spec + CDP docs as external links
- Chrome DevTools MCP post: cross-linked to fly-machines-provisioner tutorial + deploy-anywhere post
- docs/index.md: added blog section with both posts
- deploy-anywhere post: added "See also" cross-link to new browser post
No sitemap.xml found — likely auto-generated by site build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PMM feedback applied:
- Stronger outcome-first headline: "Give Your AI Agent a Real Browser"
- MCP defined within first 100 words for non-MCP-literate readers
- Infrastructure comparison table added (custom, SaaS, Molecule AI)
- "Zero-config" claim now proven with concrete workspace YAML config
- LangChain/CrewAI differentiation added to comparison section
- n8n contrast added to use cases: agents reason, workflows are manually wired
- Meta description and tags updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Brief: keywords, audience, outline, SEO requirements (Content Marketer authored)
- Blog post: "How to Add Browser Automation to AI Agents with MCP"
- CDP + MCP bridge explanation
- Full Python code example (end-to-end competitor research agent)
- Chrome remote debugging setup guide
- Minimal MCP-to-CDP server implementation
- Real-world use cases (4 production scenarios)
- CTAs linking to Molecule AI docs + GitHub
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CP-QA approved. seedInitialMemories() now truncates mem.Content at 100,000 bytes before INSERT. Oversized content is logged with byte count before/after so operators can detect truncation. Fixes#1066 (CWE-400). NOTE: no unit tests in this commit — follow-up issue recommended.
CP-QA approved. Panic recovery in fireSchedule now advances next_run_at via ComputeNextRun + ExecContext, preventing a panicking cron from indefinitely starving all other schedules. 3 new tests: TestPanicRecovery_AdvancesNextRunAt, TestFireSchedule_NormalSuccess, TestRecordSkipped_AdvancesNextRunAt. Fixes#1029.
Security fixes for the memory backup/restore endpoints merged in PR #1051.
## F1084 / #1131: Memory export exposes all workspaces
GET /admin/memories/export now applies redactSecrets() to each content
field before including it in the JSON response. Pre-SAFE-T1201 memories
(stored before redactSecrets was mandatory on writes) no longer leak
credential patterns in the admin export.
## F1085 / #1132: Memory import does not call redactSecrets
POST /admin/memories/import now calls redactSecrets() on content before
BOTH the deduplication check and the INSERT. This ensures:
- Imported memories with embedded credentials cannot land unredacted in
agent_memories (SAFE-T1201 / #838 parity with the commit_memory path).
- Dedup is performed against the redacted value so two backups with
the same original secret both get [REDACTED:*] as their content and
are correctly treated as duplicates.
## New tests
admin_memories_test.go: 6 tests covering redactSecrets parity on
both Export and Import endpoints.
Closes#1131.
Closes#1132.
Co-authored-by: Molecule AI Core-DevOps <core-devops@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Molecule AI Infra-Runtime-BE <infra-runtime-be@agents.moleculesai.app>
URLs returned from DB and Redis cache (db.GetCachedURL, workspaces.url column)
are now validated via validateAgentURL() before any HTTP request is made:
- mcpResolveURL (mcp.go): added validateAgentURL() calls on all three return
paths (internal cache, Redis cache, DB fallback).
- resolveAgentURL (a2a_proxy.go): added validateAgentURL() call before
returning agentURL to the A2A dispatcher.
validateAgentURL() was extended (registry.go) to resolve DNS hostnames and
check each returned IP against the blocklist (private ranges, loopback,
cloud-metadata 169.254.0.0/16). "localhost" is allowed by name for local dev.
GET /admin/memories/export now applies redactSecrets() to each content field
before including it in the JSON response. Pre-SAFE-T1201 memories (stored
before redactSecrets was mandatory on writes) no longer leak credentials.
POST /admin/memories/import now calls redactSecrets() on content before both
the deduplication check and the INSERT. Imported memories with embedded
credentials cannot bypass SAFE-T1201 (#838).
- admin_memories.go: GET /admin/memories/export + POST /admin/memories/import
handler (from PR #1051, with security fixes applied).
- admin_memories_test.go: 6 tests covering redactSecrets parity on both endpoints.
- registry_test.go: added DNS-lookup test cases for validateAgentURL (F1083).
"localhost" allowed by name (preserves existing test); nxdomain blocked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove duplicate-line ExecContext call that caused syntax error at mcp.go:784
- Update redactSecrets signature from 1-arg to 2-arg (workspaceID, content)
to match the canonical form established in PR #1017
- Update toolCommitMemory call site to use 2-arg form
- Add reserved workspaceID param note in docstring for future audit logging
Fixes PR #1036 compile-blocking issues (Platform Go job).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Part 3 of 3 for the "fail fast + comprehensive logs" UX. Platform PR
#1168 and controlplane #181 ship the server-side; this PR surfaces the
data in the canvas.
Two changes:
1. DetailsTab renders `last_sample_error` in a dedicated Error section
when the workspace is failed (or degraded with an error). Before,
the only trace of why a workspace failed was a generic banner —
users had to click "View Logs", which opened the terminal tab (the
post-boot log, empty on a runtime crash). Now the actual Python
traceback is inline. A "View console output" button in the same
section opens the full serial console in a modal.
2. New ConsoleModal component. Fetches GET /workspaces/:id/console
(platform → CP → ec2:GetConsoleOutput). Portal-rendered above the
canvas with Copy / Close / Esc handlers. Renders a friendly message
on 501 (self-hosted deploys without CP) and 404 (instance
terminated).
3. ProvisioningTimeout's "View Logs" button now opens the console
modal instead of the (usually empty) terminal tab — when a
workspace is stuck in provisioning, the cloud-init + user-data
trace is what the user actually needs.
Tests cover the closed-state no-fetch, happy-path fetch, 501/404
messaging, and Close/Escape wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CP-QA approved. golangci-lint fixes in bundle/exporter.go + bundle/importer.go, redactSecrets in admin_memories.go, plus 489-line admin_memories_test.go.
Workspaces stuck in provisioning used to sit in "starting" for 10min
until the sweeper flipped them. The real signal — a runtime crash at
EC2 boot — lands on the serial console within seconds but nothing
listened. These endpoints close the loop.
1. POST /admin/workspaces/:id/bootstrap-failed
The control plane's bootstrap watcher posts here when it spots
"RUNTIME CRASHED" in ec2:GetConsoleOutput. Handler:
- UPDATEs workspaces SET status='failed' only when status was
'provisioning' (idempotent — a raced online/failed stays put)
- Stores the error + log_tail in last_sample_error so the canvas
can render the real stack trace, not a generic "timeout" string
- Broadcasts WORKSPACE_PROVISION_FAILED with source='bootstrap_watcher'
2. GET /workspaces/:id/console
Proxies to CP's new /cp/admin/workspaces/:id/console endpoint so
the tenant platform can surface EC2 serial console output without
holding AWS credentials. CPProvisioner.GetConsoleOutput is the
client; returns 501 in non-CP deployments (docker-compose dev).
Both gated by AdminAuth — CP holds the tenant ADMIN_TOKEN that the
middleware accepts on its tier 2b branch.
Tests cover: happy-path fail, already-transitioned no-op, empty id,
log_tail truncation, and the 501 fallback when no CP is wired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes: #177 (CRITICAL — Dockerfile runs as root)
Dockerfiles changed:
- workspace-server/Dockerfile (platform-only): addgroup/adduser + USER platform
- workspace-server/Dockerfile.tenant (combined Go+Canvas): addgroup/adduser + USER canvas
+ chown canvas:canvas on canvas dir so non-root node process can read it
- canvas/Dockerfile (canvas standalone): addgroup/adduser + USER canvas
- workspace-server/entrypoint-tenant.sh: update header comment (no longer starts
as root; both processes now start non-root)
The entrypoint no longer needs a root→non-root handoff since both the Go
platform and Canvas node run as non-root by default. The 'canvas' user owns
/app and /platform, so volume mounts owned by the host's canvas user work
without needing a root init step.
Co-authored-by: Molecule AI CP-BE <cp-be@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>