Adds Z as a keyboard equivalent for the existing double-click zoom-to-team
gesture (WCAG 2.1.1). When a team node is selected, pressing Z dispatches
molecule:zoom-to-team, which fitBounds to the parent and all children.
Input elements are guarded so Z still types normally in text fields.
Adds a 6th help panel entry documenting the Dbl-click / Z gesture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
globals.css: append @media (prefers-reduced-motion: reduce) block that zeroes
animation/transition durations, disables .animate-in/.slide-in-from-* entry
animations (Toaster, ApprovalBanner, SidePanel slide), strips dashdraw and
node-appear keyframes from React Flow elements.
Components: replace all bare animate-pulse (13 occurrences across WorkspaceNode,
StatusDot, Toolbar, SidePanel, Legend, SearchDialog, TerminalTab, TemplatePalette)
with motion-safe:animate-pulse so status indicator pulsing stops for users with
vestibular disorders. Replace 3 animate-bounce occurrences in ChatTab typing
indicator with motion-safe:animate-bounce.
Tests: new canvas/src/__tests__/reduced-motion.test.ts (12 tests) verifies the
@media block is present in globals.css and that every component file uses the
motion-safe: variant rather than bare animation classes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
H3 (compliance.py): GitHub fine-grained PATs use the github_pat_ prefix
with an 82-character alphanumeric+underscore suffix — different from
classic tokens (36 chars). Add the missing pattern to _PII_PATTERNS so
fine-grained PATs are redacted in compliance logs alongside classic tokens.
M4 (platform_auth.py): Replace write_text()+chmod() in save_token() with
os.open(O_WRONLY|O_CREAT|O_TRUNC, 0o600) + os.write(). The old approach
had a TOCTOU window where a concurrent reader could access the token file
before chmod restricted permissions. os.open with explicit mode creates the
file with 0600 permissions atomically in a single syscall.
H2 (a2a_client.py): Already fixed in commit 6c78962 (Cycle 5); no-op.
Tests: 1136 passed, 2 skipped (workspace-template pytest suite)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
IMPACT WITHOUT THIS FIX: deploying PR #31 (WorkspaceAuth middleware on
/workspaces/*) without this patch causes EVERY delegation cycle to silently
break — the heartbeat poll returns 401, the self-message A2A POST returns
401, agents never wake up after task completion, and memory consolidation
stops. The entire multi-agent coordination system degrades to single-shot
interactions with no result delivery.
Changes (all using the existing platform_auth.auth_headers() pattern
already used for POST /registry/heartbeat):
heartbeat.py — 5 calls fixed:
- GET /workspaces/:id/delegations (delegation poll)
- GET /workspaces/:id (self workspace info for parent lookup)
- GET /workspaces/{parent_id} (parent workspace name lookup)
- POST /workspaces/:id/a2a (self-message to wake agent on results)
- POST /workspaces/:id/notify (canvas delegation result notification)
Also moved `from platform_auth import auth_headers` from inline (per-call)
to module-level import so _check_delegations() can use it without re-importing.
consolidation.py — 4 calls fixed:
- GET /workspaces/:id/memories (fetch memories for consolidation)
- POST /workspaces/:id/memories (write consolidated summary — agent path)
- DELETE /workspaces/:id/memories/:id (delete original memories post-consolidation)
- POST /workspaces/:id/memories (write consolidated summary — fallback path)
a2a_client.py — 1 call fixed:
- GET /workspaces/:id (get_workspace_info())
⚠️ DEPLOYMENT NOTE: This PR MUST be merged and deployed at the same time as
PR #31 (WorkspaceAuth middleware). Deploying #31 without this fix will
immediately break all delegation result delivery.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a `canvas-deploy-reminder` job to ci.yml that fires on every
push to main once `canvas-build` passes. It posts a commit comment via
the built-in GITHUB_TOKEN (no new secrets needed) reminding whoever
monitors CI to run:
cd /g/personal_programs/molecule-monorepo
git pull origin main
docker compose build canvas && docker compose up -d canvas
The comment includes the commit SHA and a direct link to the build log.
Rationale: 5 consecutive merge cycles (PRs #21, #25, #30, #32, #34)
went undeployed because there is no auto-deploy hook and the manual
step was silently forgotten. A commit comment on the merge commit is
the lowest-friction reminder that requires no external secrets or infra.
Does NOT run on PRs — only on direct pushes to main (i.e. post-merge).
Uses `needs: canvas-build` so the reminder only fires after build+tests
pass; a failing build produces no comment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gap 1 — WS_URL now derives from NEXT_PUBLIC_PLATFORM_URL when
NEXT_PUBLIC_WS_URL is not set (http→ws, appends /ws; https→wss).
Operators need only one env var. NEXT_PUBLIC_WS_URL remains an explicit
override escape hatch.
Gap 2 — Add canvas/.env.example documenting NEXT_PUBLIC_PLATFORM_URL
(required) and NEXT_PUBLIC_WS_URL (optional override, commented out).
Gap 3 — Toolbar fires showToast("Live updates restored", "success")
when wsStatus transitions connecting→connected. mountedRef (set after
2 s) suppresses the toast on the very first page-load connection so
only genuine reconnects notify the user.
Gap 4 — New canvas/src/store/__tests__/socket.url.test.ts (6 tests):
· fallback to ws://localhost:8080/ws when no env set
· http→ws derivation from NEXT_PUBLIC_PLATFORM_URL
· https→wss derivation
· NEXT_PUBLIC_WS_URL override takes precedence
· api.ts PLATFORM_URL fallback
· api.ts reads NEXT_PUBLIC_PLATFORM_URL
375/375 tests passing, production build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This PR gates DELETE /workspaces/:id behind AdminAuth. The E2E smoke
test's three DELETE calls (cleanup of echo, summarizer, re-imported
bundle) need to send Authorization: Bearer <token>. Any valid live
token is accepted — use the token issued to each workspace at
/registry/register.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a live/reconnecting/offline pill to the Toolbar so users can see
at a glance whether the canvas is receiving real-time updates.
Changes:
- canvas/src/store/canvas.ts: add wsStatus ('connected'|'connecting'|
'disconnected') field + setWsStatus action to CanvasState (initial:
'connecting')
- canvas/src/store/socket.ts: wire setWsStatus into ReconnectingSocket —
'connecting' on connect() call, 'connected' in onopen, 'connecting'
in onclose (will reconnect), 'disconnected' in disconnect()
- canvas/src/components/Toolbar.tsx: subscribe to wsStatus; render
WsStatusPill (green "Live" / amber pulsing "Reconnecting" / red
"Offline") after the workspace count section
- canvas/src/store/__tests__/socket.test.ts: add setWsStatus: vi.fn()
to the canvas store mock (global factory, beforeEach reset, and the
mid-test override in the onmessage test)
369/369 canvas tests passing, production build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both watcher.py (ConfigWatcher) and skill_loader/watcher.py
(SkillsWatcher) used hashlib.md5() for file-integrity change detection.
MD5 is collision-prone: a crafted config file could produce the same
hash as a benign one, silently suppressing the hot-reload callback and
preventing agents from picking up legitimate config changes.
Replace hashlib.md5 → hashlib.sha256 in both _hash_file() methods.
Update docstrings, comments, and the type-annotation comment
(rel_path → md5 hex → sha256 hex).
Test update: test_skills_watcher.py — rename helper _md5 → _sha256,
update the hash-length assertion from 32 (MD5) to 64 (SHA-256), and
rename the test from test_hash_file_returns_md5_for_existing_file to
test_hash_file_returns_sha256_for_existing_file. All 25 watcher tests
pass.
Note: H2 (a2a_client.py timeout=None) was already fixed in Cycle 5
(timeout=httpx.Timeout(connect=30.0, read=300.0, ...)) — confirmed by
code review before opening this PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C18 — Workspace URL hijacking (CRITICAL, CONFIRMED LIVE):
POST /registry/register now calls requireWorkspaceToken() before
persisting anything. If the workspace has any live auth tokens, the
caller must supply a valid Bearer token matching that workspace ID.
First registration (no tokens yet) passes through — token is issued
at end of this function (unchanged bootstrap contract). Mirrors the
same pattern already applied to /registry/heartbeat and
/registry/update-card. Attacker POC — overwriting Backend Engineer URL
to http://attacker.example.com:9999/steal — now returns 401.
C20 — Unauthenticated workspace deletion (CRITICAL, CONFIRMED LIVE):
DELETE /workspaces/:id moved from bare router into AdminAuth group.
Any valid workspace bearer token grants access (same fail-open
bootstrap contract as /settings/secrets). Mass-deletion attack chain
(C19 list → C20 delete all) requires auth for the DELETE step.
POST /workspaces (create) also moved to AdminAuth to prevent
unauthenticated workspace creation.
C19 (GET /workspaces topology exposure) deferred — canvas browser
has no bearer token; fix requires canvas service-token refactor.
Tests: 2 new registry tests — C18 bootstrap (no tokens, passes
through and issues token), C18 hijack blocked (has tokens, no
bearer → 401).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw Parent Workspace ID text input with a <select> populated
from GET /workspaces (T{tier} · {name} format, graceful fallback on
fetch error). Raise all interactive button text from text-[8px]/[9px]
to text-[11px] across SkillsTab, ScheduleTab, secrets-section,
ActivityTab, SidePanel, ChatTab; non-interactive labels/badges to
text-[10px]. Adds 7 CreateWorkspaceDialog unit tests (372/372 passing).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /registry/register accepted any URL string and persisted it as
the workspace's A2A endpoint — an attacker could register a workspace
with url=http://169.254.169.254/latest/meta-data/ and cause the platform
to proxy requests to the cloud metadata service when proxying A2A traffic.
Fix: validateAgentURL() helper rejects:
- empty URL
- non-http/https schemes (file://, ftp://, etc.)
- 169.254.0.0/16 link-local IPs (AWS/GCP/Azure IMDS endpoints)
Allows RFC-1918 private ranges (Docker networking uses 172.16-31.x.x).
Adds 12 unit tests covering valid Docker-internal URLs and all SSRF vectors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three unauthenticated routes allowed arbitrary read/write/delete of all
global platform secrets (API keys, provider credentials) with zero auth:
- GET/PUT/POST /settings/secrets
- DELETE /settings/secrets/:key
- GET/POST/DELETE /admin/secrets (legacy aliases)
Fix: new AdminAuth middleware with same lazy-bootstrap contract as
WorkspaceAuth — fail-open when no tokens exist (fresh install / pre-Phase-30
upgrade), enforce once any workspace has a live token. Any valid workspace
bearer token grants access (platform-wide scope, no workspace binding needed).
Changes:
wsauth/tokens.go — HasAnyLiveTokenGlobal + ValidateAnyToken functions
wsauth/tokens_test.go — 5 new tests covering both new functions
middleware/wsauth_middleware.go — AdminAuth middleware
router/router.go — global secrets routes now registered under adminAuth group
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap CanvasInner return in React Fragment to host skip-nav link as sibling of <main>
- Add <a href="#canvas-main"> skip link (sr-only, revealed on focus) before <main>
- Add id="canvas-main" to <main> element
- Add aria-label="Molecule AI workspace canvas" to ReactFlow wrapper
- Add Canvas.a11y.test.tsx: 4 jsdom tests covering all three a11y landmarks
369/369 tests pass; next build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The WorkspaceAuth middleware (PR #31) now requires bearer tokens on all
/workspaces/:id/* sub-routes. The E2E test_api.sh already captured ECHO_TOKEN
and SUM_TOKEN from /registry/register but was not passing them to the ten
/activity curl calls, causing 10 FAIL assertions in CI.
Add -H "Authorization: Bearer $ECHO_TOKEN" (or $SUM_TOKEN) to every
GET and POST /workspaces/:id/activity call in the Activity Log Tests section.
PATCH /workspaces/:id and DELETE /workspaces/:id remain unauthenticated (they
are on the root router, not the wsAuth group).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UX Audit Run 6 critical finding: Legend panel and workspace node cards used 8px and 9px
text (6–7pt), which is physically unreadable and fails WCAG minimum guidelines.
- Legend.tsx: raise all text-[8px]/[9px]/[10px] → text-[11px] across every sub-component
(StatusItem labels, TierItem badge+label, CommItem icon+label, section headers)
- WorkspaceNode.tsx: raise text-[8px]/[9px] → text-[10px] for all readable labels in
the main card (status text, skill badges, task/error banners, tier badge, sub count,
Team Members header) and TeamMemberChip primary name/role text
Compact 7px elements inside TeamMemberChip (tier/sub badges, status micropills) retained
to preserve dense canvas layout — only human-readable labels were upgraded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up to root-cause analysis in #17 (see 2026-04-14 02:14 UTC comment).
The Security Auditor's hourly DAST was creating test workspaces, secrets,
and plugins to probe auth/validation logic — but only secrets and plugins
had teardown in the prompt. Workspace-create probes leaked rows into
`workspaces` with sequential IDs aaaaaaaa- bbbbbbbb- cccccccc- dddddddd-,
each trapped in a restart loop on missing config.yaml. Four hourly runs,
four leaked workspaces.
Adds explicit step 4a: DAST TEARDOWN. Maintains three lists (workspaces,
secrets, plugins) populated as probes run, and iterates them at the end
with DELETE calls. Uses `|| true` so partial teardown failures don't
break the audit, but every created artifact gets a cleanup attempt.
Doesn't remove the cleanup the cron was already doing for secrets/plugins
— just formalises the pattern so workspace-create (and any future probe
surface) is covered by the same contract.
Related:
- #17 — rogue workspace restart loop (root cause was this)
- #26 — audit cron routing (this PR sits alongside that structure)
Fix A — platform/internal/middleware/wsauth_middleware.go (NEW):
WorkspaceAuth() gin middleware enforces per-workspace bearer-token auth on
ALL /workspaces/:id/* sub-routes. Same lazy-bootstrap contract as
secrets.Values: workspaces with no live token are grandfathered through.
Blocks C2, C3, C4, C5, C7, C8, C9, C12, C13 simultaneously.
Fix A — platform/internal/router/router.go:
Reorganised route registration: bare CRUD (/workspaces, /workspaces/:id)
and /a2a remain on root router; all other /workspaces/:id/* sub-routes
moved into wsAuth = r.Group("/workspaces/:id", middleware.WorkspaceAuth(db.DB)).
CORS AllowHeaders updated to include Authorization so browser/agent callers
can send the bearer token cross-origin.
Fix B — workspace-template/heartbeat.py:
_check_delegations(): validate source_id == self.workspace_id before
accepting a delegation result. Attacker-crafted records with a foreign
source_id are silently skipped with a WARNING log (injection attempt).
trigger_msg no longer embeds raw response_preview text; references
delegation_id + status only — removes the prompt-injection vector.
Fix C — workspace-template/skill_loader/loader.py:
load_skill_tools(): before exec_module(), verify script is within
scripts_dir (path traversal guard) and temporarily scrub sensitive env
vars (CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, OPENAI_API_KEY,
WORKSPACE_AUTH_TOKEN, GITHUB_TOKEN, GH_TOKEN) from os.environ; restore
in finally block. Defence-in-depth even if /plugins auth gate is bypassed.
Fix D — platform/internal/handlers/socket.go:
HandleConnect(): agent connections (X-Workspace-ID present) validated via
wsauth.HasAnyLiveToken + wsauth.ValidateToken before WebSocket upgrade.
Canvas clients (no X-Workspace-ID) remain unauthenticated.
Fix D — workspace-template/events.py:
PlatformEventSubscriber._connect(): include platform_auth bearer token in
WebSocket upgrade headers alongside X-Workspace-ID.
Fix E — workspace-template/executor_helpers.py:
recall_memories() and commit_memory() now pass platform_auth bearer token
in Authorization header so WorkspaceAuth middleware allows access.
Fix F — workspace-template/a2a_client.py:
send_a2a_message(): timeout=None → httpx.Timeout(connect=30, read=300,
write=30, pool=30). Resolves H2 flagged across 5 consecutive audits.
Tests: 149/149 Python tests pass (test_heartbeat + test_events updated to
assert new source_id validation behaviour and allow Authorization header).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Currently no workspace in the molecule-dev template installs any of the
four available plugins (browser-automation, ecc, molecule-dev, superpowers).
Agents run without coding guardrails, codebase conventions, or debugging
discipline unless a plugin is installed per-workspace via the runtime
POST /workspaces/:id/plugins endpoint — which isn't happening.
Changes:
1. defaults.plugins: [ecc, molecule-dev, superpowers]
- ecc: "Everything Claude Code" — coding standards, API design,
deep research, security review, TDD workflow, node guardrails
- molecule-dev: project-specific conventions, past bugs, review-loop skill
- superpowers: systematic debugging, TDD, plan writing/execution,
verification-before-completion
All three target runtime claude_code (matches our default).
2. plugins override on Research Lead + its 3 children + UIUX Designer:
[ecc, molecule-dev, superpowers, browser-automation]
- Research agents need live web access for scraping/trending/docs,
which is core to their role.
- UIUX Designer gets Puppeteer via CDP; this may work around the
libglib/X11 gap that breaks Playwright today (#23 — the image-level
fix remains the right long-term solution, but browser-automation
uses puppeteer-core + a Chrome CDP proxy and may bypass the deps
issue entirely).
Note: platform/internal/handlers/org.go:345 treats per-workspace
`plugins:` as a REPLACEMENT of defaults (not a union), which is why
each opt-in workspace re-lists the full set. Documented inline in the
template so future editors don't accidentally drop defaults.
No other roles take browser-automation — Dev Lead, BE, FE, DevOps,
Security, QA, PM all get the default set only. If they need web access
they can install ad-hoc via the runtime plugin API.
Addresses the gap surfaced by CEO 2026-04-13: audit agents (Security
Auditor, QA Engineer, UIUX Designer) were running their crons successfully
but findings stayed in agent memory and didn't consistently flow to
GitHub issues or to developers with build ability. BE noticed Security
findings once via a manual escalation; subsequent hourly audits
accumulated 13 criticals (including an unauthenticated-plugin-install
RCE) with no durable tracking.
Changes:
1. Security Auditor schedule: replace 12h (7 6,18 * * *) with hourly
(17 * * * *) to match what's actually running in the platform DB.
Rewrite the prompt with the full body of the runtime cron — git diff
scoping, gosec/bandit, manual checklist, live API DAST, secrets scan,
open-PR review.
2. QA Engineer schedule: keep 12h cadence, tighten post-audit routing.
3. UIUX Designer: add a schedule (was previously runtime-only — see #24).
Uses hourly cadence to match runtime. Accepts Playwright may be
unavailable (see #23) and falls back to HTML analysis with the
limitation noted in the deliverable.
All three audit crons now end with an identical FINAL STEP — DELIVERABLE
ROUTING block that makes the post-audit flow MANDATORY:
a. File a GitHub issue for each CRITICAL / HIGH finding (dedupe first)
b. delegate_task to PM with a structured summary listing issue numbers;
PM decides which dev agent picks up which issue
c. Even on clean cycles, send PM a one-line "clean on SHA X" so audits
are observable
d. Memory write becomes a secondary record, not the primary deliverable
Rationale: findings need to flow into the issue tracker (durable, visible
to CEO, part of the PR/issue review feedback loop already in place) and
through PM (who owns cross-team orchestration). Memory-only output is
invisible to everyone except the auditor itself.
Related:
- #23 — UIUX Designer container missing libglib/X11 for Playwright.
This PR accepts the current limitation; #23 tracks the image fix.
- #24 — template-vs-runtime schedule drift. This PR backfills the template;
#24 tracks the platform-layer fix for preventing future drift.
- 13 open criticals in Security Auditor memory are out of scope for this
PR (that's team work once the routing is in place).
- computeAutoLayout() BFS tree layout seeds from anchored nodes; assigns
distinct x/y to workspaces returned at 0,0 by the API and persists via PATCH
- buildNodesAndEdges() accepts layoutOverrides map so hydration uses computed
positions instead of raw 0,0 coordinates
- canvas-events WORKSPACE_PROVISIONING grid layout replaces offset===offset
assignment that caused position:{x:t,y:t} in the minified bundle
- 8 new vitest tests cover computeAutoLayout and override behaviour (365 pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses FLAG 1 and FLAG 2 from the 7-Gate review on PR #20.
FLAG 1 (token persisted on disk):
Previous: `git clone https://x-access-token:${GITHUB_TOKEN}@github.com/...` wrote
the full tokenized URL into /workspace/repo/.git/config as `[remote "origin"] url = …`.
Token survived container restarts on any bind-mounted workspace_dir.
Fix: after clone, `git remote set-url origin https://github.com/${GITHUB_REPO}.git`
scrubs the token from the remote URL. Token is only in the clone command's argv
(transient) and not persisted on disk. Falls back to anonymous for public repos.
FLAG 2 (docs not updated):
Added GITHUB_REPO and GITHUB_TOKEN entries under a new 'GitHub' section in
.env.example with notes about (a) what they're read for, (b) that GITHUB_TOKEN
should be registered as a global secret via POST /admin/secrets, (c) how it's
handled to avoid on-disk persistence.
FLAG 3 (per-workspace gating) is deferred to a separate issue — it's a platform
design question about secret scope/ACLs, not a template fix.