Commit Graph

3996 Commits

Author SHA1 Message Date
Hongming Wang
ae9bf50ad3 docs: sync CLAUDE.md with current architecture (2026-04-16)
Measured test counts (not guessed):
- Platform Go: 12 packages (was claiming 818 individual tests — now
  reports package-level which is the go test output format)
- Canvas: 490 Vitest tests (33 files)
- workspace-template: 955 pytest tests (down from 1179 — 224 adapter-
  specific tests moved to standalone template repos)
- molecule-app: 76 unit + 22 e2e (separate repo)

Architecture updates:
- CI section: documents manifest-driven Docker builds + reusable CI
  workflows from molecule-ci repo for all 33 plugin/template repos
- Workspace Images section: already updated by prior PR (adapter repos)
- Test commands: accurate counts, standalone repo URLs with test counts
2026-04-16 05:09:19 -07:00
Hongming Wang
4b6f08833e Merge pull request #474 from Molecule-AI/fix/code-review-issues
fix: code review findings + remove exposed secrets
2026-04-16 05:06:11 -07:00
Hongming Wang
14f1af1b1b
Merge pull request #474 from Molecule-AI/fix/code-review-issues
fix: code review findings + remove exposed secrets
2026-04-16 05:06:11 -07:00
Hongming Wang
510c40089f fix: address all code review findings + remove exposed secrets
Code review fixes:
- 🟡 #1: Replace python3 with jq in Dockerfile template stages (~50MB → ~2MB)
- 🟡 #2: Add clone count verification to scripts/clone-manifest.sh
  (set -e + expected vs actual count check — fails build if any clone fails)
- 🟡 #3: Drop 'unsafe-eval' from CSP (not needed for Next.js production
  standalone builds, only dev mode). Updated test assertion.
- 🟡 #4: Remove broken pyproject.toml from workspace-template/ (it claimed
  to package as molecule-ai-workspace-runtime but the directory structure
  didn't match — the real package ships from the standalone repo)
- 🔵 #1: Add version-pinning TODO comment to manifest.json
- 🔵 #3: Add full repo URLs + test counts for SDK/MCP/CLI/runtime in CLAUDE.md

Security (GitGuardian alert):
- Removed Telegram bot token (8633739353:AA...) from template-molecule-dev
  pm/.env — replaced with ${TELEGRAM_BOT_TOKEN} placeholder
- Removed Claude OAuth token (sk-ant-oat01-...) from template-molecule-dev
  root .env — replaced with ${CLAUDE_CODE_OAUTH_TOKEN} placeholder
- Both tokens need immediate rotation by the operator

Tests: Platform middleware tests updated + all pass.
2026-04-16 05:05:49 -07:00
Hongming Wang
74e4f30216 fix: address all code review findings + remove exposed secrets
Code review fixes:
- 🟡 #1: Replace python3 with jq in Dockerfile template stages (~50MB → ~2MB)
- 🟡 #2: Add clone count verification to scripts/clone-manifest.sh
  (set -e + expected vs actual count check — fails build if any clone fails)
- 🟡 #3: Drop 'unsafe-eval' from CSP (not needed for Next.js production
  standalone builds, only dev mode). Updated test assertion.
- 🟡 #4: Remove broken pyproject.toml from workspace-template/ (it claimed
  to package as molecule-ai-workspace-runtime but the directory structure
  didn't match — the real package ships from the standalone repo)
- 🔵 #1: Add version-pinning TODO comment to manifest.json
- 🔵 #3: Add full repo URLs + test counts for SDK/MCP/CLI/runtime in CLAUDE.md

Security (GitGuardian alert):
- Removed Telegram bot token (8633739353:AA...) from template-molecule-dev
  pm/.env — replaced with ${TELEGRAM_BOT_TOKEN} placeholder
- Removed Claude OAuth token (sk-ant-oat01-...) from template-molecule-dev
  root .env — replaced with ${CLAUDE_CODE_OAUTH_TOKEN} placeholder
- Both tokens need immediate rotation by the operator

Tests: Platform middleware tests updated + all pass.
2026-04-16 05:05:49 -07:00
Hongming Wang
73865ee164 Merge pull request #473 from Molecule-AI/fix/remove-adapters-dir
fix: remove adapter subdirectories from workspace-template
2026-04-16 04:59:34 -07:00
Hongming Wang
045e477cd8
Merge pull request #473 from Molecule-AI/fix/remove-adapters-dir
fix: remove adapter subdirectories from workspace-template
2026-04-16 04:59:34 -07:00
Hongming Wang
2347d6a80b fix: properly remove adapter subdirectories + move shared code to root
PR #471 removed Dockerfiles/requirements from adapters/ but left the
Python source files. This commit finishes the extraction:

1. Moved shared_runtime.py → workspace-template/shared_runtime.py
   (used by prompt.py, a2a_executor.py, coordinator.py — not adapter-specific)
2. Moved base.py → workspace-template/adapter_base.py
   (BaseAdapter + AdapterConfig — the interface adapters implement)
3. Updated imports in prompt.py, a2a_executor.py, coordinator.py
4. Rewritten adapters/__init__.py as a thin shim that:
   - Reads ADAPTER_MODULE env var (production: standalone repos set this)
   - Re-exports BaseAdapter/AdapterConfig for backward compat
5. adapters/base.py + adapters/shared_runtime.py remain as re-export shims
6. Deleted all 8 adapter subdirectories (autogen, claude_code, crewai,
   deepagents, gemini_cli, hermes, langgraph, openclaw)
7. Removed 11 test files that imported adapter-specific code

Tests: 955 passed, 0 failed (down from 1216 — the difference is
adapter-specific tests that moved to standalone repos).
2026-04-16 04:59:13 -07:00
Hongming Wang
55a2ee0153 fix: properly remove adapter subdirectories + move shared code to root
PR #471 removed Dockerfiles/requirements from adapters/ but left the
Python source files. This commit finishes the extraction:

1. Moved shared_runtime.py → workspace-template/shared_runtime.py
   (used by prompt.py, a2a_executor.py, coordinator.py — not adapter-specific)
2. Moved base.py → workspace-template/adapter_base.py
   (BaseAdapter + AdapterConfig — the interface adapters implement)
3. Updated imports in prompt.py, a2a_executor.py, coordinator.py
4. Rewritten adapters/__init__.py as a thin shim that:
   - Reads ADAPTER_MODULE env var (production: standalone repos set this)
   - Re-exports BaseAdapter/AdapterConfig for backward compat
5. adapters/base.py + adapters/shared_runtime.py remain as re-export shims
6. Deleted all 8 adapter subdirectories (autogen, claude_code, crewai,
   deepagents, gemini_cli, hermes, langgraph, openclaw)
7. Removed 11 test files that imported adapter-specific code

Tests: 955 passed, 0 failed (down from 1216 — the difference is
adapter-specific tests that moved to standalone repos).
2026-04-16 04:59:13 -07:00
Hongming Wang
12db4e9342 Merge pull request #472 from Molecule-AI/fix/remove-orphaned-plugin-tests
fix: remove orphaned plugin/adapter tests
2026-04-16 04:39:44 -07:00
Hongming Wang
3534aa0b5b
Merge pull request #472 from Molecule-AI/fix/remove-orphaned-plugin-tests
fix: remove orphaned plugin/adapter tests
2026-04-16 04:39:44 -07:00
Hongming Wang
c0af9cbde2 fix: remove tests that referenced removed plugins/ directory
test_first_party_plugins.py, test_plugins_builtins_drift.py, and
test_hermes_adapter.py all referenced files under plugins/ and
adapters/ which were extracted to standalone repos. These tests
belong in those repos now, not in the core workspace-template.

1216 passed, 0 failed after removal.
2026-04-16 04:39:31 -07:00
Hongming Wang
8ea8c1d7af fix: remove tests that referenced removed plugins/ directory
test_first_party_plugins.py, test_plugins_builtins_drift.py, and
test_hermes_adapter.py all referenced files under plugins/ and
adapters/ which were extracted to standalone repos. These tests
belong in those repos now, not in the core workspace-template.

1216 passed, 0 failed after removal.
2026-04-16 04:39:31 -07:00
Hongming Wang
bf2208a49d Merge pull request #471 from Molecule-AI/chore/extract-workspace-runtime-to-pypi
chore: extract workspace runtime to PyPI package + standalone adapter repos
2026-04-16 04:34:30 -07:00
Hongming Wang
d17c242016
Merge pull request #471 from Molecule-AI/chore/extract-workspace-runtime-to-pypi
chore: extract workspace runtime to PyPI package + standalone adapter repos
2026-04-16 04:34:30 -07:00
Hongming Wang
ab1562f3fe chore: remove adapter Dockerfiles and requirements.txt from monorepo
These files have moved to the standalone template repos:
  https://github.com/Molecule-AI/molecule-ai-workspace-template-<runtime>

Each adapter repo now has its own Dockerfile (FROM python:3.11-slim + pip install
molecule-ai-workspace-runtime) and requirements.txt. The adapter Python source
files (.py) stay in the monorepo for local development and testing.

Adapters removed from workspace-template/adapters/*/: Dockerfile, requirements.txt
Adapters retained: adapter.py, __init__.py (+ hermes extras: escalation.py, executor.py, providers.py)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 04:33:22 -07:00
Hongming Wang
57ad7b5fe5 chore: remove adapter Dockerfiles and requirements.txt from monorepo
These files have moved to the standalone template repos:
  https://github.com/Molecule-AI/molecule-ai-workspace-template-<runtime>

Each adapter repo now has its own Dockerfile (FROM python:3.11-slim + pip install
molecule-ai-workspace-runtime) and requirements.txt. The adapter Python source
files (.py) stay in the monorepo for local development and testing.

Adapters removed from workspace-template/adapters/*/: Dockerfile, requirements.txt
Adapters retained: adapter.py, __init__.py (+ hermes extras: escalation.py, executor.py, providers.py)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 04:33:22 -07:00
Hongming Wang
03f6fc81dd chore: extract workspace runtime to PyPI + move adapter Dockerfiles to template repos
Published `molecule-ai-workspace-runtime==0.1.0` to PyPI:
  https://pypi.org/project/molecule-ai-workspace-runtime/0.1.0/

Source repo: https://github.com/Molecule-AI/molecule-ai-workspace-runtime

Each adapter's Dockerfile and requirements.txt have moved to the corresponding
standalone template repo (molecule-ai-workspace-template-<runtime>). The adapter
Python code (.py files) stays in the monorepo for local dev and testing.

Changes:
- workspace-template/pyproject.toml — new, packages the shared runtime as a PyPI package
- workspace-template/adapters/*/Dockerfile — removed (now in template repos)
- workspace-template/adapters/*/requirements.txt — removed (now in template repos)
- workspace-template/Dockerfile — drop COPY adapters/ (still copies .py files via *.py glob)
- workspace-template/build-all.sh — simplified to base-image-only build
- workspace-template/entrypoint.sh — remove adapter requirements.txt install step
- workspace-template/tests/test_hermes_adapter.py — skip Dockerfile/requirements.txt checks
- CLAUDE.md — update architecture description + workspace image table
- docs/workspace-runtime-package.md — new, explains the package + adapter repo layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 04:33:10 -07:00
Hongming Wang
cb74f0d6ae chore: extract workspace runtime to PyPI + move adapter Dockerfiles to template repos
Published `molecule-ai-workspace-runtime==0.1.0` to PyPI:
  https://pypi.org/project/molecule-ai-workspace-runtime/0.1.0/

Source repo: https://github.com/Molecule-AI/molecule-ai-workspace-runtime

Each adapter's Dockerfile and requirements.txt have moved to the corresponding
standalone template repo (molecule-ai-workspace-template-<runtime>). The adapter
Python code (.py files) stays in the monorepo for local dev and testing.

Changes:
- workspace-template/pyproject.toml — new, packages the shared runtime as a PyPI package
- workspace-template/adapters/*/Dockerfile — removed (now in template repos)
- workspace-template/adapters/*/requirements.txt — removed (now in template repos)
- workspace-template/Dockerfile — drop COPY adapters/ (still copies .py files via *.py glob)
- workspace-template/build-all.sh — simplified to base-image-only build
- workspace-template/entrypoint.sh — remove adapter requirements.txt install step
- workspace-template/tests/test_hermes_adapter.py — skip Dockerfile/requirements.txt checks
- CLAUDE.md — update architecture description + workspace image table
- docs/workspace-runtime-package.md — new, explains the package + adapter repo layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 04:33:10 -07:00
Hongming Wang
0e60b94a1c Merge pull request #459 from Molecule-AI/chore/remove-extracted-dirs
chore: remove extracted dirs (templates, SDK, MCP, CLI)
2026-04-16 04:18:05 -07:00
Hongming Wang
49782c9a51
Merge pull request #459 from Molecule-AI/chore/remove-extracted-dirs
chore: remove extracted dirs (templates, SDK, MCP, CLI)
2026-04-16 04:18:05 -07:00
Molecule AI Backend Engineer
c35dfaf143 feat(channels): per-channel message budget with 429 enforcement (#368)
Add an optional channel_budget (INTEGER, nullable) to workspace_channels
via migration 024. When channel_budget IS NOT NULL and message_count has
reached the budget, the Send handler returns 429 {"error":"channel budget
exceeded"} and aborts before calling SendOutbound.

Implementation details:
- Single SELECT query reads both message_count and channel_budget in one
  round-trip (avoids TOCTOU window between read and write)
- Fail-open on DB error: transient failures log but don't block sends
- Early-return on budget hit is before SendOutbound so message_count
  cannot be incremented past the limit by a concurrent send that slips
  through the window (best-effort; atomic enforcement requires DB-level CAS)
- NULL channel_budget = unlimited (default, backward-compatible)

Migration is idempotent (ADD COLUMN IF NOT EXISTS). Down migration drops
the column cleanly.

Four sqlmock tests cover: at-limit → 429, above-limit → 429, NULL budget
passes through, under-limit passes through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:17:14 +00:00
Molecule AI Backend Engineer
b021f85af9 feat(channels): per-channel message budget with 429 enforcement (#368)
Add an optional channel_budget (INTEGER, nullable) to workspace_channels
via migration 024. When channel_budget IS NOT NULL and message_count has
reached the budget, the Send handler returns 429 {"error":"channel budget
exceeded"} and aborts before calling SendOutbound.

Implementation details:
- Single SELECT query reads both message_count and channel_budget in one
  round-trip (avoids TOCTOU window between read and write)
- Fail-open on DB error: transient failures log but don't block sends
- Early-return on budget hit is before SendOutbound so message_count
  cannot be incremented past the limit by a concurrent send that slips
  through the window (best-effort; atomic enforcement requires DB-level CAS)
- NULL channel_budget = unlimited (default, backward-compatible)

Migration is idempotent (ADD COLUMN IF NOT EXISTS). Down migration drops
the column cleanly.

Four sqlmock tests cover: at-limit → 429, above-limit → 429, NULL budget
passes through, under-limit passes through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:17:14 +00:00
DevOps Engineer
8ba6e18c0a ci: extract e2e-api into dedicated workflow with run-level cancel protection (#458)
Job-level `concurrency.cancel-in-progress: false` only prevents sibling jobs
from killing each other — it does not protect the parent workflow run from
being cancelled when a new push arrives. Every PR push was cancelling the
in-progress E2E run, forcing manual `gh run rerun` across 7+ active PRs.

Fix: move e2e-api into `.github/workflows/e2e-api.yml` with a workflow-level
concurrency group (`e2e-api-${{ github.ref }}`, cancel-in-progress: false).
New pushes now queue behind the running E2E job instead of cancelling it.

Fast jobs (platform-build, canvas-build, shellcheck, python-lint) stay in
ci.yml and retain normal run-level cancellation for quick iteration feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:15:13 +00:00
DevOps Engineer
9b72be75f6 ci: extract e2e-api into dedicated workflow with run-level cancel protection (#458)
Job-level `concurrency.cancel-in-progress: false` only prevents sibling jobs
from killing each other — it does not protect the parent workflow run from
being cancelled when a new push arrives. Every PR push was cancelling the
in-progress E2E run, forcing manual `gh run rerun` across 7+ active PRs.

Fix: move e2e-api into `.github/workflows/e2e-api.yml` with a workflow-level
concurrency group (`e2e-api-${{ github.ref }}`, cancel-in-progress: false).
New pushes now queue behind the running E2E job instead of cancelling it.

Fast jobs (platform-build, canvas-build, shellcheck, python-lint) stay in
ci.yml and retain normal run-level cancellation for quick iteration feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:15:13 +00:00
Molecule AI Backend Engineer
6358f26932 feat(channels): add Slack adapter with webhook URL validation (#384)
Implement SlackAdapter satisfying the ChannelAdapter interface:
- ValidateConfig: rejects any webhook_url that doesn't start with
  https://hooks.slack.com/ — returns "invalid Slack webhook URL" so
  the handler surfaces 400 {"error":"invalid config: invalid Slack webhook URL"}
- SendMessage: HTTP POST JSON {"text":"..."} to the webhook URL with a
  10s timeout; rejects invalid-prefix URLs at send time too (defence in depth)
- ParseWebhook: handles both slash-command (form-encoded) and Events API
  (JSON) payloads; no-ops on url_verification and non-message events
- StartPolling: returns nil immediately (Slack doesn't support polling via
  Incoming Webhooks)

Register "slack" in the adapter registry. Twelve unit tests cover
Type/DisplayName, happy-path validation, every bad-URL variant (wrong scheme,
wrong host, SSRF lookalike, empty string), empty webhook in SendMessage,
StartPolling nil return, and registry lookup/listing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:14:31 +00:00
Molecule AI Backend Engineer
68c9b37048 feat(channels): add Slack adapter with webhook URL validation (#384)
Implement SlackAdapter satisfying the ChannelAdapter interface:
- ValidateConfig: rejects any webhook_url that doesn't start with
  https://hooks.slack.com/ — returns "invalid Slack webhook URL" so
  the handler surfaces 400 {"error":"invalid config: invalid Slack webhook URL"}
- SendMessage: HTTP POST JSON {"text":"..."} to the webhook URL with a
  10s timeout; rejects invalid-prefix URLs at send time too (defence in depth)
- ParseWebhook: handles both slash-command (form-encoded) and Events API
  (JSON) payloads; no-ops on url_verification and non-message events
- StartPolling: returns nil immediately (Slack doesn't support polling via
  Incoming Webhooks)

Register "slack" in the adapter registry. Twelve unit tests cover
Type/DisplayName, happy-path validation, every bad-URL variant (wrong scheme,
wrong host, SSRF lookalike, empty string), empty webhook in SendMessage,
StartPolling nil return, and registry lookup/listing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:14:31 +00:00
Hongming Wang
d424bd947f chore: remove extracted directories, add manifest-driven Docker builds
Remove plugins/, workspace-configs-templates/, org-templates/ dirs (now
in standalone repos). Add manifest.json listing all 33 repos and
scripts/clone-manifest.sh to clone them. Both Dockerfiles now use the
manifest script instead of 33 hardcoded git-clone lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 04:13:29 -07:00
Hongming Wang
8e304e69e8 chore: remove extracted directories, add manifest-driven Docker builds
Remove plugins/, workspace-configs-templates/, org-templates/ dirs (now
in standalone repos). Add manifest.json listing all 33 repos and
scripts/clone-manifest.sh to clone them. Both Dockerfiles now use the
manifest script instead of 33 hardcoded git-clone lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 04:13:29 -07:00
Molecule AI Backend Engineer
9b2539a042 fix(memories): add hard cap of 50 on recall results (#377)
Introduce `memoryRecallMaxLimit = 50` constant and honour the `?limit=N`
query parameter in Search. Values above 50 are silently clamped to 50;
absent or invalid values default to 50. The LIMIT clause is now a
parameterised argument (nextArg pattern) instead of a hardcoded literal.
Three sqlmock tests verify the cap, the explicit limit, and the default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:12:35 +00:00
Molecule AI Backend Engineer
6fb4b7b282 fix(memories): add hard cap of 50 on recall results (#377)
Introduce `memoryRecallMaxLimit = 50` constant and honour the `?limit=N`
query parameter in Search. Values above 50 are silently clamped to 50;
absent or invalid values default to 50. The LIMIT clause is now a
parameterised argument (nextArg pattern) instead of a hardcoded literal.
Three sqlmock tests verify the cap, the explicit limit, and the default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:12:35 +00:00
Molecule AI Backend Engineer
b5dc285080 fix(security): strip Authorization + Cookie headers in canvas reverse proxy (closes #451)
The canvas proxy was forwarding all headers verbatim to the Next.js process.
Workspace bearer tokens sent by agents (e.g. during an A2A call that hit a
canvas-side route) could reach unvalidated Next.js handlers and be echoed back
to an attacker via an error page or a debug endpoint.

Fix: Director now calls Header.Del("Authorization") + Header.Del("Cookie")
before forwarding. Non-credential headers (Accept, X-Request-Id, etc.) are
unaffected — the strip is surgical.

Four unit tests added (strips Authorization, strips Cookie, forwards other
headers, strips both simultaneously).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:00:43 +00:00
Molecule AI Backend Engineer
479b172b25 fix(security): strip Authorization + Cookie headers in canvas reverse proxy (closes #451)
The canvas proxy was forwarding all headers verbatim to the Next.js process.
Workspace bearer tokens sent by agents (e.g. during an A2A call that hit a
canvas-side route) could reach unvalidated Next.js handlers and be echoed back
to an attacker via an error page or a debug endpoint.

Fix: Director now calls Header.Del("Authorization") + Header.Del("Cookie")
before forwarding. Non-credential headers (Accept, X-Request-Id, etc.) are
unaffected — the strip is surgical.

Four unit tests added (strips Authorization, strips Cookie, forwards other
headers, strips both simultaneously).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:00:43 +00:00
Canvas Agent
43c99e0af7 fix(canvas): QA blockers — ChatTab aria-controls, AuthGate test, CommunicationOverlay status icons
BLOCKER 1 (ChatTab.tsx): Replace ternary rendering with always-in-DOM panels
using `hidden` attribute so `aria-controls` targets always exist (WCAG 4.1.2).
Add `id` to tab buttons for `aria-labelledby` back-reference. Non-blocking:
change `key={i}` → `key={line + i}` on activity log items.

BLOCKER 2 (AuthGate.test.tsx): Create test file asserting the loading state
renders a `.bg-zinc-950.fixed.inset-0` overlay with `aria-hidden="true"` —
covers the zinc-950 flash-prevention overlay added in the prior commit.

BLOCKER 3 (CommunicationOverlay.tsx): Add `aria-hidden="true"` to the status
icon span so decorative glyphs (✓ ✕ ⏱) are not announced by screen readers.

Tests: 490/490 passing. Build: clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:53:52 +00:00
Canvas Agent
c33b59a93a fix(canvas): QA blockers — ChatTab aria-controls, AuthGate test, CommunicationOverlay status icons
BLOCKER 1 (ChatTab.tsx): Replace ternary rendering with always-in-DOM panels
using `hidden` attribute so `aria-controls` targets always exist (WCAG 4.1.2).
Add `id` to tab buttons for `aria-labelledby` back-reference. Non-blocking:
change `key={i}` → `key={line + i}` on activity log items.

BLOCKER 2 (AuthGate.test.tsx): Create test file asserting the loading state
renders a `.bg-zinc-950.fixed.inset-0` overlay with `aria-hidden="true"` —
covers the zinc-950 flash-prevention overlay added in the prior commit.

BLOCKER 3 (CommunicationOverlay.tsx): Add `aria-hidden="true"` to the status
icon span so decorative glyphs (✓ ✕ ⏱) are not announced by screen readers.

Tests: 490/490 passing. Build: clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:53:52 +00:00
Hongming Wang
055efc535a Merge pull request #449 from Molecule-AI/fix/issue-425-sidepanel-width-persist
fix(canvas): persist SidePanel width to localStorage (closes #425)
2026-04-16 03:49:05 -07:00
Hongming Wang
520c993baa
Merge pull request #449 from Molecule-AI/fix/issue-425-sidepanel-width-persist
fix(canvas): persist SidePanel width to localStorage (closes #425)
2026-04-16 03:49:05 -07:00
Hongming Wang
a8c0bc059e Merge pull request #440 from Molecule-AI/fix/docker-compose-platform-build-context
fix(compose): platform build context must be repo root
2026-04-16 03:48:30 -07:00
Hongming Wang
e0b83d170d
Merge pull request #440 from Molecule-AI/fix/docker-compose-platform-build-context
fix(compose): platform build context must be repo root
2026-04-16 03:48:30 -07:00
Canvas Agent
4d0f2d4c79 fix(canvas): C1/C2/C3/C5 dark-theme CSS and ReactFlow colorMode 2026-04-16 10:45:16 +00:00
Canvas Agent
c936b451a9 fix(canvas): C1/C2/C3/C5 dark-theme CSS and ReactFlow colorMode 2026-04-16 10:45:16 +00:00
Canvas Agent
026921ae62 fix(canvas): persist SidePanel width to localStorage (issue #425)
Width was initialized to 480px on every render, so clicking a different
workspace node (which re-mounts SidePanel) discarded any resize the user
had done.

Fix:
- localStorage-backed useState initializer (SSR-safe typeof window guard)
- Validates the stored value: must be a finite integer ≥ 320px
- Persists the width in the mouseUp handler via a widthRef that stays in
  sync with the live drag value — avoids spamming localStorage on every
  pixel during the drag
- Extra guard: onMouseUp bails early if not actually dragging (prevents
  spurious saves on unrelated window mouseup events)
- Named constants replace magic numbers 480 / 320

Tests: 5 new cases in SidePanel.tabs.test.tsx — default fallback, valid
saved value, too-small saved value, NaN saved value, drag-persist roundtrip.

Closes #425

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:40:08 +00:00
Canvas Agent
966920355a fix(canvas): persist SidePanel width to localStorage (issue #425)
Width was initialized to 480px on every render, so clicking a different
workspace node (which re-mounts SidePanel) discarded any resize the user
had done.

Fix:
- localStorage-backed useState initializer (SSR-safe typeof window guard)
- Validates the stored value: must be a finite integer ≥ 320px
- Persists the width in the mouseUp handler via a widthRef that stays in
  sync with the live drag value — avoids spamming localStorage on every
  pixel during the drag
- Extra guard: onMouseUp bails early if not actually dragging (prevents
  spurious saves on unrelated window mouseup events)
- Named constants replace magic numbers 480 / 320

Tests: 5 new cases in SidePanel.tabs.test.tsx — default fallback, valid
saved value, too-small saved value, NaN saved value, drag-persist roundtrip.

Closes #425

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:40:08 +00:00
Canvas Agent
3daa9083b8 fix(canvas): UIUX Cycle 15 dark-theme & a11y sweep (C1-C5, A1-A4, F1, M1)
- C4: OnboardingWizard skip button — aria-label + text-zinc-400 (was zinc-600)
- A1+M1: CommunicationOverlay — aria-label on both icon buttons, aria-hidden
  on decorative arrow glyphs (↗↙ toggle, ✕ close, → comms rows)
- A2: ChatTab sub-tab bar — ARIA roving tabIndex + ArrowLeft/ArrowRight
  keyboard navigation (role=tablist/tab already present)
- A4: SearchDialog search input — focus-visible:ring-2 ring-blue-500 replaces
  bare focus:outline-none so keyboard focus is visible
- F1: AuthGate loading state — zinc-950 full-screen backdrop instead of null
  (prevents white flash on SaaS tenant load)
- A3: SidePanel tab bar — wrap in relative container + right-edge fade
  gradient so truncated tabs are visually signalled

C2 (settings-panel.css input backgrounds) and C3 (Canvas.tsx colorMode="dark")
were already in place; verified by code audit before this commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:35:32 +00:00
Canvas Agent
28f3e33581 fix(canvas): UIUX Cycle 15 dark-theme & a11y sweep (C1-C5, A1-A4, F1, M1)
- C4: OnboardingWizard skip button — aria-label + text-zinc-400 (was zinc-600)
- A1+M1: CommunicationOverlay — aria-label on both icon buttons, aria-hidden
  on decorative arrow glyphs (↗↙ toggle, ✕ close, → comms rows)
- A2: ChatTab sub-tab bar — ARIA roving tabIndex + ArrowLeft/ArrowRight
  keyboard navigation (role=tablist/tab already present)
- A4: SearchDialog search input — focus-visible:ring-2 ring-blue-500 replaces
  bare focus:outline-none so keyboard focus is visible
- F1: AuthGate loading state — zinc-950 full-screen backdrop instead of null
  (prevents white flash on SaaS tenant load)
- A3: SidePanel tab bar — wrap in relative container + right-edge fade
  gradient so truncated tabs are visually signalled

C2 (settings-panel.css input backgrounds) and C3 (Canvas.tsx colorMode="dark")
were already in place; verified by code audit before this commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:35:32 +00:00
Backend Engineer
f2b7357b60 fix(security): registry DB errors must not leak raw driver messages (closes #435)
The Register handler was serialising the raw Go error into the HTTP response:
  c.JSON(500, gin.H{"error": fmt.Sprintf("failed to register: %v", err)})

PostgreSQL errors wrapped by lib/pq contain table names, constraint names, and
driver-version strings — enough for a caller to fingerprint the schema and craft
targeted attacks. The error is already logged at full detail with Printf before
this line, so callers only need the generic message.

Fix: replace the Sprintf with a static "registration failed" string (same pattern
the heartbeat and update-card handlers already used).

New test: TestRegister_DBErrorResponseIsOpaque verifies the response body is the
opaque string and that "sql:", "pq:", and "connection" substrings are absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:34:35 +00:00
Backend Engineer
b0381d656c fix(security): registry DB errors must not leak raw driver messages (closes #435)
The Register handler was serialising the raw Go error into the HTTP response:
  c.JSON(500, gin.H{"error": fmt.Sprintf("failed to register: %v", err)})

PostgreSQL errors wrapped by lib/pq contain table names, constraint names, and
driver-version strings — enough for a caller to fingerprint the schema and craft
targeted attacks. The error is already logged at full detail with Printf before
this line, so callers only need the generic message.

Fix: replace the Sprintf with a static "registration failed" string (same pattern
the heartbeat and update-card handlers already used).

New test: TestRegister_DBErrorResponseIsOpaque verifies the response body is the
opaque string and that "sql:", "pq:", and "connection" substrings are absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:34:35 +00:00
Backend Engineer
7e971222f4 fix(provisioner): rebuild_config flag on restart recovers from destroyed config volume (closes #239)
When a workspace container AND its /configs Docker volume are both destroyed,
the restart handler previously had no recovery path — findTemplateByName searched
only the top-level configsDir, which holds workspace-instance dirs (ws-{id[:12]}/),
not the role-named org-template source directories.

Fix: add `rebuild_config: true` to the POST /workspaces/:id/restart body struct.
When set, the handler falls back to searching configsDir/org-templates/ via the
existing findTemplateByName logic (which already handles name normalisation and
config.yaml name-field matching). The workspace can then self-recover with its own
bearer token — no admin intervention required.

New helper: resolveOrgTemplate(configsDir, wsName) — pure function, independently
tested (4 cases: hit-by-dir, hit-by-config-yaml, no org-templates dir, no match).

Usage:
  curl -X POST -H "Authorization: Bearer $(cat /configs/.auth_token)" \
       -d '{"rebuild_config": true}' \
       http://platform:8080/workspaces/$WORKSPACE_ID/restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:34:25 +00:00
Backend Engineer
2451b1acc0 fix(provisioner): rebuild_config flag on restart recovers from destroyed config volume (closes #239)
When a workspace container AND its /configs Docker volume are both destroyed,
the restart handler previously had no recovery path — findTemplateByName searched
only the top-level configsDir, which holds workspace-instance dirs (ws-{id[:12]}/),
not the role-named org-template source directories.

Fix: add `rebuild_config: true` to the POST /workspaces/:id/restart body struct.
When set, the handler falls back to searching configsDir/org-templates/ via the
existing findTemplateByName logic (which already handles name normalisation and
config.yaml name-field matching). The workspace can then self-recover with its own
bearer token — no admin intervention required.

New helper: resolveOrgTemplate(configsDir, wsName) — pure function, independently
tested (4 cases: hit-by-dir, hit-by-config-yaml, no org-templates dir, no match).

Usage:
  curl -X POST -H "Authorization: Bearer $(cat /configs/.auth_token)" \
       -d '{"rebuild_config": true}' \
       http://platform:8080/workspaces/$WORKSPACE_ID/restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:34:25 +00:00
Canvas Agent
29d8f18952 fix(canvas): replace AuthGate null loading state with zinc-950 backdrop
Closes #430.

During the session fetch on SaaS deployments, AuthGate returned null —
causing a white/blank screen flash for 200–500ms before the zinc-950
canvas background appeared.

Replace with a fixed zinc-950 div so the browser always paints the
correct dark background from the first frame. The canvas loading UI
renders on top once the session resolves, with no visible transition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:30:24 +00:00