Commit Graph

3663 Commits

Author SHA1 Message Date
Hongming Wang
c2c80fd269 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
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
bb44b1f03b 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
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
54bb543ff7 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
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
5d4ee18c72 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
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
807b4c1b45 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
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
6ea559079e 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
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
c5ef9a71fc 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
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
f9cb6396f8 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
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
0c9fda559a 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
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
a06a9baca5 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
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
8451af0683 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
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
6f3c16eb78 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
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
071fb0da88 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
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
3892e4dee1 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
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
15abfca106 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
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
f93ec926cb 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
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
08a8c06cc3 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
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
40c825b8ed 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
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
54e1500e6a 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
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
bf17209d6c 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
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
aeb9308c7d 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
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
a4b1886c35 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
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
ac6d6ce5bd 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
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
d7161f5877 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
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
a394ae55c3 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
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