molecule-core/workspace-server/internal/middleware/devmode.go
Hongming Wang 3f11df031c fix: six UX bugs (peers auth, scroll, chat tabs, config persist, + visibility)
Six bugs reported from a live session — all shippable in one commit:

1. Peers tab 401 on local Docker. The /registry/:id/peers endpoint
   demands a workspace-scoped bearer token (validateDiscoveryCaller)
   which the canvas session doesn't hold. Added the same Tier-1b
   dev-mode fail-open hatch that AdminAuth and WorkspaceAuth already
   use — gated by MOLECULE_ENV=development + empty ADMIN_TOKEN, so
   SaaS production stays strict. Exported IsDevModeFailOpen from the
   middleware package for the handler layer to reuse.

2. Org Templates list unscrollable. OrgTemplatesSection was rendered
   in the TemplatePalette footer — a div without overflow — so when
   it expanded to 15+ entries the list extended past the viewport
   with no scroll. Moved it to the top of the flex-1 overflow-y-auto
   container. Tall lists now scroll naturally.

3. Chat tab: "My Chat" and "Agent Comms" rendered stacked instead
   of switching. HTML `hidden` attribute was being overridden by
   Tailwind's `flex` class (display: flex beats the attribute),
   so both tabpanels rendered concurrently. Swapped to a conditional
   Tailwind `hidden`/`flex` class so the inactive panel is
   display:none with proper CSS specificity.

4. Hermes Config form never persists. handleSave wrote config.yaml
   but name / tier / runtime / model all live on the workspace row
   (or the dedicated /workspaces/:id/model endpoint) — the form
   edited in-memory, the request returned 200, the next reload
   wiped everything back. Hermes + external runtimes manage their
   own config inside the container anyway, so writing config.yaml
   is a no-op for them; skip it. Always diff and PATCH the DB-backed
   fields that actually changed.

5. Channels "+ Connect" dropdown empty on first open. ChannelsTab's
   load() used Promise.all with a silent catch — if EITHER the
   channels or adapters fetch failed, both setters were skipped
   with no error visible. Switched to Promise.allSettled so each
   endpoint settles independently, and the adapters failure now
   surfaces via the top-level error state.

6. Plugin registry always "No plugins in registry". Same silent
   catch pattern in SkillsTab.tsx — load errors for /plugins,
   /plugins/sources, and /workspaces/:id/plugins swallowed without
   logging. Replaced the empty catches with console.warn so future
   failures are at least visible in devtools.

Tests: 923 passing (unchanged). Go handler tests pass. Server
rebuilt and running with the peers-auth + collapsed-persistence
fixes (pid 15875).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:18:30 -07:00

66 lines
2.6 KiB
Go

package middleware
import (
"os"
"strings"
)
// Dev-mode escape hatch — factored out of AdminAuth + WorkspaceAuth so a
// future third caller (or a change to what "dev mode" means) touches one
// place. Narrowing the exposed seam also makes it grep-able from security
// reviews: every `isDevModeFailOpen()` call is an intentional fail-open.
//
// Why the helper exists at all: on `go run ./cmd/server` the Canvas (at
// localhost:3000) calls the platform (at localhost:8080) cross-port. Both
// `isSameOriginCanvas` (Referer==Host) and the AdminAuth Tier-1 fail-open
// (no tokens in DB) close the moment the user creates their first
// workspace. Without this hatch the Canvas 401s on every /workspaces
// enumeration and every /workspaces/:id/* read until the operator sets
// `ADMIN_TOKEN` and rebuilds the Canvas bundle with a matching
// `NEXT_PUBLIC_ADMIN_TOKEN`. That's too much friction for a local smoke
// test — hence the hatch.
//
// Why it's safe for SaaS: hosted tenants are provisioned with both
// `ADMIN_TOKEN` (a random secret, checked by Tier-2 above) and
// `MOLECULE_ENV=production`. Either one being set makes this helper
// return false, so the fail-open branch is unreachable in production.
// The convention matches `handlers/admin_test_token.go`, which gates
// the e2e test-token mint on `MOLECULE_ENV != "production"`.
// devModeEnvValues is the set of MOLECULE_ENV values that count as
// "explicit dev mode". Production callers don't set any of these.
// Case-insensitive compare via strings.ToLower below.
var devModeEnvValues = map[string]struct{}{
"development": {},
"dev": {},
}
// isDevModeFailOpen reports whether the AdminAuth / WorkspaceAuth
// middleware should let a bearer-less request through despite live
// workspace tokens existing in the DB.
//
// True only when BOTH:
// - `ADMIN_TOKEN` is empty (operator has not opted in to the #684
// closure), AND
// - `MOLECULE_ENV` is explicitly a dev value ("development" / "dev").
//
// Either condition failing returns false — that's the SaaS safety
// guarantee. Tests: `devmode_test.go` covers every branch.
func isDevModeFailOpen() bool {
if os.Getenv("ADMIN_TOKEN") != "" {
return false
}
env := strings.ToLower(strings.TrimSpace(os.Getenv("MOLECULE_ENV")))
_, ok := devModeEnvValues[env]
return ok
}
// IsDevModeFailOpen exposes isDevModeFailOpen to packages outside the
// middleware module (handlers, discovery, etc.) so they can apply the
// same Tier-1b escape hatch their sibling AdminAuth / WorkspaceAuth
// already do. Keep every call site audit-tagged so security review can
// grep them.
func IsDevModeFailOpen() bool {
return isDevModeFailOpen()
}