Commit Graph

538 Commits

Author SHA1 Message Date
Hongming Wang
b1e971e4ff
Merge pull request #503 from Molecule-AI/feat/controlplane-provisioner
feat(platform): control plane provisioner (CONTAINER_BACKEND=controlplane)
2026-04-16 11:54:07 -07:00
Hongming Wang
1ea615df4c feat(platform): auto-detect SaaS tenant → control plane provisioner
No env vars to configure. The platform auto-detects the backend:

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

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

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

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

Closes #494

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

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

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

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

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

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

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

Closes #494

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:06:33 -07:00
Hongming Wang
f8bc303985
Merge pull request #489 from Molecule-AI/fix/tenant-dockerfile-in-publish
fix(ci): use Dockerfile.tenant for Fly registry (Go + Canvas)
2026-04-16 09:34:44 -07:00
Hongming Wang
feec130685 fix(ci): use Dockerfile.tenant for Fly registry image (Go + Canvas)
The publish workflow was pushing platform/Dockerfile (Go-only) to the
Fly registry, but tenant machines run the combined image (Go + Canvas
reverse proxy). This caused "canvas unavailable" after machine update.

Changes:
- Fly registry build: platform/Dockerfile → platform/Dockerfile.tenant
- GHCR: keeps Go-only image (for self-hosted/dev use)
- Path triggers: add canvas/** and manifest.json (tenant image includes both)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:31:51 -07:00
Hongming Wang
0adf707eb5
Merge pull request #487 from Molecule-AI/fix/ci-publish-skip-docker-login-v2
fix(ci): bypass docker login + macOS Keychain (real fix)
2026-04-16 09:30:45 -07:00
Hongming Wang
ca1d5741d5 fix(ci): bypass docker login + macOS Keychain for image publish
Six prior PRs (#273, #319, #322, #341, #484, #486) all kept calling
`docker login` and tried to coerce credsStore via increasingly elaborate
config tricks. None worked. The latest publish-canvas-image and
publish-platform-image runs on main are still failing with:

    error storing credentials - err: exit status 1,
    out: `User interaction is not allowed. (-25308)`

Verified locally on the runner host (2026-04-16): `docker login` on
macOS unconditionally writes credentials to osxkeychain after a
successful login, regardless of the config presented to it.

    # I wrote this:
    { "auths": {}, "credsStore": "", "credHelpers": {} }
    # After `docker login --config <dir> ghcr.io ...` succeeded:
    {
      "auths": { "ghcr.io": {} },        # empty — auth is in Keychain
      "credsStore": "osxkeychain"        # Docker rewrote it back
    }

So `--config` flag, DOCKER_CONFIG env var, credsStore="" etc. all share
the same fate: Docker re-enables osxkeychain after every successful
login. The Mac mini runner is a launchd user agent with a locked
Keychain, so storage fails with -25308.

This PR replaces the `docker login` invocation entirely. We write
`base64(user:pat)` directly into the disposable DOCKER_CONFIG's `auths`
map. `docker/build-push-action@v5` and the daemon honor the auths map
for push without ever calling `docker login`, so the Keychain is never
involved.

Same shape in both workflows:
- publish-canvas-image.yml — single registry (ghcr.io)
- publish-platform-image.yml — two registries (ghcr.io + registry.fly.io)
  Fly username remains literal "x".

Security:
- Token env vars never echoed. Heredoc writes the auth blob via
  `umask 077` (file mode 600). The temp config dir lives under
  RUNNER_TEMP and is reaped at job end.
- Diagnostics preserved (docker version + binary ls + registry keys
  only, no values) so future runner permission regressions remain
  visible without leaking secrets.

Equivalent to closed PR #464 — re-opening because main is still
broken (verified by inspecting the most recent failure). The closing
comment on #464 stated the issue was already addressed by #341, but
it isn't.
2026-04-16 09:25:20 -07:00
Hongming Wang
ed3e8eed3c
Merge pull request #485 from Molecule-AI/feat/mcp-docs-tokens-external-agent
feat(platform): token management API + MCP setup + external agent guide
2026-04-16 09:00:04 -07:00
Hongming Wang
8fe3fd5aa0 docs: update remote-workspaces-readiness for Phase 30.1 shipped status
- Mark Phase 30.1 (auth tokens) as shipped
- Update hard-problem A (spoofing) from blocker → resolved
- Cross-reference new guides: external-agent-registration, token-management, mcp-server-setup
- Update last-reviewed date

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:49:07 -07:00
Hongming Wang
83a1a28b3f fix(ci): use docker login CLI instead of login-action to bypass macOS Keychain
docker/login-action@v3 ignores DOCKER_CONFIG and still tries the
macOS system keychain on the self-hosted runner, producing:
  error storing credentials: User interaction is not allowed. (-25308)

Switch to `docker login ... --password-stdin` which respects
DOCKER_CONFIG and writes credentials to the per-run config.json
we created in the isolate step. Applied to both GHCR and Fly
registry logins in both publish workflows.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:37:42 -07:00
Hongming Wang
c9e44ec8f7
Merge pull request #484 from Molecule-AI/fix/publish-workflow-yaml
fix(ci): fix YAML parse error in publish workflows
2026-04-16 08:22:37 -07:00
Hongming Wang
dbe96ca11d fix(ci): replace heredoc JSON with printf in publish workflows
The heredoc block writing Docker config.json had unindented `{` at
column 1, which GitHub Actions' YAML parser interpreted as a flow
mapping start — causing every publish-platform-image and
publish-canvas-image run to fail with 0 jobs (startup_failure).

Replace `cat <<'JSON' ... JSON` with a single `printf` call that
produces identical config.json content without confusing the parser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:20:43 -07:00
Hongming Wang
a3748ec090
Merge pull request #481 from Molecule-AI/feat/fly-deploy-step
feat(ci): deploy to Fly after image push
2026-04-16 08:15:46 -07:00
Hongming Wang
206564c90b
Merge pull request #483 from Molecule-AI/fix/platform-modular-template-support
fix(platform): unblock org-template imports against modular workspace templates
2026-04-16 07:55:26 -07:00
Hongming Wang
8025fb2f09
Merge pull request #482 from Molecule-AI/fix/canvas-ux-improvements
fix(canvas): UX improvements — tokens, focus, loading, a11y
2026-04-16 07:54:48 -07:00
Hongming Wang
9d39fa53f5
Merge pull request #480 from Molecule-AI/feat/lark-channel-adapter
feat(channels): Lark / Feishu channel adapter + idempotent migration 023
2026-04-16 07:54:45 -07:00
rabbitblood
ff2394c085 fix(platform): unblock org-template imports against modular workspace templates
Two adjacent fixes that surfaced trying to bring the molecule-dev org
template back up against the new standalone workspace-template-* repos.

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:49:45 -07:00
Hongming Wang
2049870057 fix(canvas): address all code review findings on PR #482
- Reconcile TIER_CONFIG/TIER_COLORS into single TIER_CONFIG with both
  `color` (pill style) and `border` (bordered badge style) fields
- Remove TemplatePalette alias indirection (TIER_LABELS_SHARED → direct import)
- Extract inline spinner SVGs to shared Spinner component (3 copies → 1)
- Migrate status dot colors from 6 remaining files to shared tokens:
  SearchDialog, StatusDot, Legend, ContextMenu, Toolbar + add statusDotClass()
- Add COMM_TYPE_LABELS to design-tokens, used by CommunicationOverlay sr-only
- Update reduced-motion tests: components that delegate to design-tokens
  pass the guard check via import detection; add design-tokens.ts own test
- 507/507 tests pass, build clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:48:47 -07:00
Hongming Wang
cd30430979 fix(canvas): UX improvements — shared tokens, focus rings, loading spinners, a11y
- Extract STATUS_CONFIG, TIER_CONFIG, TIER_COLORS to shared design-tokens.ts
  (eliminates 3 duplicate definitions across WorkspaceNode, EmptyState, TemplatePalette)
- Add focus-visible:ring-2 ring-blue-500 to WorkspaceNode, SidePanel tabs,
  EmptyState buttons, TemplatePalette buttons (keyboard navigation now visible)
- Replace "Loading..." text with animated spinner SVG in EmptyState,
  TemplatePalette sidebar, and OrgTemplatesSection
- Add disabled:cursor-not-allowed + suppress hover styling when disabled
  on EmptyState template buttons and TemplatePalette deploy buttons
- Brighten SidePanel tab hover from bg-zinc-800/20 to bg-zinc-800/40
  and text from zinc-300 to zinc-200
- Add screen reader labels to CommunicationOverlay directional arrows
  and status icons (sr-only text for "sent", "received", "to", status)

Fixes #422, #424, #427

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:35:44 -07:00
Hongming Wang
0064e61881 feat(ci): add Fly deploy step to publish-platform-image workflow
After pushing the tenant image to registry.fly.io, the workflow now
lists all running/stopped molecule-tenant machines and updates each
to the newly pushed image tag. Gracefully skips if no machines exist
(control plane provisions on demand).

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 06:47:09 -07:00
Hongming Wang
1a85ca7656 fix(canvas): template layout + org card styling
- Wider modal (max-w-2xl), 3-col grid, no max-height clipping
- Org template cards: violet→blue, consistent rounded-xl styling
- Container scrolls vertically instead of cutting off
2026-04-16 06:42:41 -07:00
Hongming Wang
295b1fa8d9 fix(e2e): clear ADMIN_TOKEN after last workspace delete so AdminAuth fail-opens 2026-04-16 06:34:17 -07:00
Hongming Wang
3231560fcf fix(e2e): fall back to test-token when register doesn't return a new token
On re-registration (workspace already has tokens), the register endpoint
doesn't issue a new token — it returns the existing one in the response
or omits it. The e2e_extract_token helper returns empty in that case.
Fall back to the per-workspace token we already minted via test-token.
2026-04-16 06:29:44 -07:00
Hongming Wang
f4462a24df fix(e2e): use per-workspace tokens for register + heartbeat + discover
AdminAuth (admin token) gates workspace CRUD operations.
WorkspaceAuth (per-workspace token) gates register, heartbeat, discover.
The test now mints a workspace-specific token via test-token endpoint
for each workspace before calling register.
2026-04-16 06:22:16 -07:00
Hongming Wang
a661e1bf55 fix(e2e): use acurl for registry/register + re-register calls (C18 auth) 2026-04-16 06:15:39 -07:00
Hongming Wang
edd17cecaa fix(e2e): read auth_token not token from test-token response 2026-04-16 06:11:32 -07:00
Hongming Wang
b1def4a933 debug: add test-token response logging to e2e 2026-04-16 06:08:58 -07:00
Hongming Wang
dacc7425ef fix(e2e): use admin bearer token for AdminAuth-gated API calls
After the first workspace is created and the test-token endpoint mints
a bearer, HasAnyLiveTokenGlobal returns true. All subsequent calls to
AdminAuth-gated routes (workspace CRUD, events, bundles, etc.) need the
token. Added acurl() helper that attaches the token when available.
2026-04-16 06:05:13 -07:00
Hongming Wang
0071b66a59 fix(ci): heredoc indentation in publish workflows + add dev-start.sh
Two fixes:
1. publish-canvas-image.yml + publish-platform-image.yml: the JSON
   heredoc for config.json had leading whitespace from YAML indentation,
   producing invalid JSON. Docker fell back to osxkeychain → -25308.
   Fixed by removing indentation inside the heredoc body.

2. Added scripts/dev-start.sh — one-command local dev environment.
   Starts infra (docker-compose), platform (Go), and canvas (Next.js)
   with proper health checks and cleanup on Ctrl-C.
2026-04-16 05:56:25 -07:00
Hongming Wang
d10067697e
Merge pull request #470 from Molecule-AI/fix/aria-time-sensitive-components
fix(a11y): WCAG ARIA fixes for time-sensitive components
2026-04-16 05:52:23 -07:00
Hongming Wang
fd719f4d36 fix: use /bin/sh not bash in clone-manifest (Alpine has no bash) 2026-04-16 05:42:49 -07:00
Hongming Wang
dc895bb17e
Merge pull request #462 from Molecule-AI/fix/security-460-461-yaml-injection-error-disclosure
fix(security): YAML-quote skill/prompt names in generateDefaultConfig + opaque file-write errors
2026-04-16 05:40:49 -07:00
Security Auditor
284fb26558 fix(security): YAML-quote skill/prompt names in generateDefaultConfig + opaque file-write errors
Closes #460, #461.

**#460 — YAML injection via unquoted skill/prompt filenames**
`generateDefaultConfig` extracted skill directory names and prompt file
names from user-supplied `body.Files` keys and wrote them directly into
YAML list items without quoting:

  cfg.WriteString("  - " + s + "\n")

`validateRelPath` only blocks path traversal (`../`); it does NOT block
YAML control characters including newlines. On Linux, filenames can
contain newlines, so an attacker with any live workspace bearer token
could submit:

  {"files": {"skills/legit\nruntime: malicious/SKILL.md": "# skill"}}

The generated config.yaml would then contain `runtime: malicious` as a
top-level YAML key, overriding the runtime for workspaces provisioned
from the template.

Fix: extract `yamlEscape` as a reusable local from the same
`strings.NewReplacer` already used for the `name` field (#221) and apply
it to both the `skills:` and `prompt_files:` list items, wrapping each
in double-quotes.

**#461 — Docker error details in ReplaceFiles 500 responses**
`ReplaceFiles` returned `fmt.Sprintf("failed to write files: %v", err)`
in two 500 paths, where `err` comes from Docker API calls and may include
internal container names, volume names, and daemon error messages.

Fix: log the full error server-side and return a static opaque string to
the caller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:45 -07:00
Canvas Agent
66fd7c3ccf fix(a11y): WCAG ARIA fixes for time-sensitive components (Fixes #Fix1/#Fix2/#Fix3)
- ApprovalBanner: add role="alert" aria-live="assertive" aria-atomic="true" to
  each pending approval card; aria-hidden="true" on decorative ⚠ icon span
- TerminalTab: add role="status" aria-live="polite" to connection status bar;
  add role="alert" to inline error message div
- BundleDropZone: extract shared processFile(); add hidden <input type="file">
  with id/accept/aria-label; add sr-only focus:not-sr-only keyboard trigger
  button; add role="status" aria-live="polite" to result toast

Tests: 7 new assertions in aria-time-sensitive.test.tsx covering all 3 fixes
(496/496 pass, build clean)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Canvas Agent
477b6c06b7 fix(canvas): fitView on new workspace provision — respects user zoom level (#426)
Replace setCenter(x, y, {zoom:1}) with fitView({nodes:[{id}]}) in the
molecule:pan-to-node handler (Canvas.tsx). The old implementation forced
zoom=1 regardless of the user's current zoom level, which was jarring when
panned/zoomed away. fitView adapts to whatever zoom the user had and
gracefully fits the new node in view.

Tests:
- Canvas.pan-to-node.test.tsx: fitView called with correct nodeId after
  100ms debounce; debounce coalesces rapid successive events.
- canvas-events-pan.test.ts: molecule:pan-to-node dispatched for new
  provisions only, NOT on restart of an existing node.

Fixes #426.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Hongming Wang
16b6e8b53e
Merge pull request #477 from Molecule-AI/fix/canvas-proxy-test-closenotify
fix(test): canvas proxy test CloseNotify panic
2026-04-16 05:40:36 -07:00
Hongming Wang
8b13fff355 fix(test): wrap httptest.ResponseRecorder with CloseNotify for canvas proxy tests
httputil.ReverseProxy calls CloseNotify() which httptest.ResponseRecorder
doesn't implement. Gin casts the writer, causing a panic. Added a
closeNotifyRecorder wrapper with a no-op channel.
2026-04-16 05:40:17 -07:00
rabbitblood
57870abe98 chore(gitignore): exclude .secrets/ + *.pem from tracking
Local-only secrets (GitHub App private keys, future per-tenant
credentials) live in .secrets/ on the host. Belt-and-braces with the
existing .env exclusion so a stray copy / rename can't leak.

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