Merge branch 'main' into fix/issue10-runtime-aware-plugin-install
Some checks failed
CI / Detect changes (pull_request) Successful in 13s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 14s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
E2E API Smoke Test / detect-changes (pull_request) Successful in 13s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 10s
pr-guards / disable-auto-merge-on-push (pull_request) Failing after 6s
Harness Replays / detect-changes (pull_request) Successful in 11s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 12s
CI / Canvas (Next.js) (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 8s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 9s
CodeQL / Analyze (${{ matrix.language }}) (go) (pull_request) Failing after 1m6s
CodeQL / Analyze (${{ matrix.language }}) (javascript-typescript) (pull_request) Failing after 1m41s
CodeQL / Analyze (${{ matrix.language }}) (python) (pull_request) Failing after 1m44s
Harness Replays / Harness Replays (pull_request) Failing after 55s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 1m13s
CI / Platform (Go) (pull_request) Successful in 5m42s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 5m44s
Some checks failed
CI / Detect changes (pull_request) Successful in 13s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 14s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
E2E API Smoke Test / detect-changes (pull_request) Successful in 13s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 10s
pr-guards / disable-auto-merge-on-push (pull_request) Failing after 6s
Harness Replays / detect-changes (pull_request) Successful in 11s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 12s
CI / Canvas (Next.js) (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 8s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 9s
CodeQL / Analyze (${{ matrix.language }}) (go) (pull_request) Failing after 1m6s
CodeQL / Analyze (${{ matrix.language }}) (javascript-typescript) (pull_request) Failing after 1m41s
CodeQL / Analyze (${{ matrix.language }}) (python) (pull_request) Failing after 1m44s
Harness Replays / Harness Replays (pull_request) Failing after 55s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 1m13s
CI / Platform (Go) (pull_request) Successful in 5m42s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 5m44s
This commit is contained in:
commit
f51722411b
89
workspace-server/cmd/server/bind_test.go
Normal file
89
workspace-server/cmd/server/bind_test.go
Normal file
@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestResolveBindHost pins the precedence: BIND_ADDR explicit > dev-mode
|
||||
// fail-open default of 127.0.0.1 > production-shape empty (all interfaces).
|
||||
//
|
||||
// Mutation-test invariant: removing the IsDevModeFailOpen() branch makes
|
||||
// "no_bindaddr_devmode_unset_admin" fail (returns "" instead of "127.0.0.1").
|
||||
// Removing the BIND_ADDR branch makes "explicit_bindaddr_*" cases fail.
|
||||
func TestResolveBindHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
bindAddr string
|
||||
adminToken string
|
||||
molEnv string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no_bindaddr_devmode_unset_admin",
|
||||
bindAddr: "",
|
||||
adminToken: "",
|
||||
molEnv: "dev",
|
||||
want: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "no_bindaddr_devmode_unset_admin_full_word",
|
||||
bindAddr: "",
|
||||
adminToken: "",
|
||||
molEnv: "development",
|
||||
want: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "no_bindaddr_admin_set_in_dev_env",
|
||||
bindAddr: "",
|
||||
adminToken: "secret",
|
||||
molEnv: "dev",
|
||||
want: "", // ADMIN_TOKEN flips IsDevModeFailOpen to false → all interfaces
|
||||
},
|
||||
{
|
||||
name: "no_bindaddr_production_env",
|
||||
bindAddr: "",
|
||||
adminToken: "",
|
||||
molEnv: "production",
|
||||
want: "", // production is not a dev value → all interfaces
|
||||
},
|
||||
{
|
||||
name: "no_bindaddr_unset_env",
|
||||
bindAddr: "",
|
||||
adminToken: "",
|
||||
molEnv: "",
|
||||
want: "", // unset MOLECULE_ENV → not dev → all interfaces
|
||||
},
|
||||
{
|
||||
name: "explicit_bindaddr_loopback_overrides_devmode",
|
||||
bindAddr: "127.0.0.1",
|
||||
adminToken: "",
|
||||
molEnv: "dev",
|
||||
want: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "explicit_bindaddr_wildcard_overrides_devmode_default",
|
||||
bindAddr: "0.0.0.0",
|
||||
adminToken: "",
|
||||
molEnv: "dev",
|
||||
want: "0.0.0.0",
|
||||
},
|
||||
{
|
||||
name: "explicit_bindaddr_in_production",
|
||||
bindAddr: "10.0.5.7",
|
||||
adminToken: "secret",
|
||||
molEnv: "production",
|
||||
want: "10.0.5.7",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("BIND_ADDR", tc.bindAddr)
|
||||
t.Setenv("ADMIN_TOKEN", tc.adminToken)
|
||||
t.Setenv("MOLECULE_ENV", tc.molEnv)
|
||||
got := resolveBindHost()
|
||||
if got != tc.want {
|
||||
t.Errorf("resolveBindHost() = %q, want %q (BIND_ADDR=%q ADMIN_TOKEN=%q MOLECULE_ENV=%q)",
|
||||
got, tc.want, tc.bindAddr, tc.adminToken, tc.molEnv)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@ import (
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/handlers"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/imagewatch"
|
||||
memwiring "github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/wiring"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/middleware"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/pendinguploads"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/provisioner"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/registry"
|
||||
@ -337,15 +338,23 @@ func main() {
|
||||
// Router
|
||||
r := router.Setup(hub, broadcaster, prov, platformURL, configsDir, wh, channelMgr, memBundle)
|
||||
|
||||
// HTTP server with graceful shutdown
|
||||
// HTTP server with graceful shutdown.
|
||||
//
|
||||
// Bind host: in dev-mode (no ADMIN_TOKEN, MOLECULE_ENV=dev|development)
|
||||
// the AdminAuth chain fails open by design; pairing that with a wildcard
|
||||
// bind would expose unauth /workspaces to any same-LAN peer. Default to
|
||||
// loopback when fail-open is active. Operators who need LAN exposure set
|
||||
// BIND_ADDR=0.0.0.0 explicitly. Production (ADMIN_TOKEN set) is unchanged.
|
||||
// See molecule-core#7.
|
||||
bindHost := resolveBindHost()
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", port),
|
||||
Addr: fmt.Sprintf("%s:%s", bindHost, port),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// Start server in goroutine
|
||||
go func() {
|
||||
log.Printf("Platform starting on :%s", port)
|
||||
log.Printf("Platform starting on %s:%s (dev-mode-fail-open=%v)", bindHost, port, middleware.IsDevModeFailOpen())
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Server failed: %v", err)
|
||||
}
|
||||
@ -380,6 +389,29 @@ func envOr(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// resolveBindHost picks the listener interface for the HTTP server.
|
||||
//
|
||||
// Precedence:
|
||||
// 1. BIND_ADDR — explicit operator override (any value, including "0.0.0.0").
|
||||
// 2. dev-mode fail-open active → "127.0.0.1" (loopback only).
|
||||
// 3. otherwise → "" (Go binds every interface; existing prod/self-host shape).
|
||||
//
|
||||
// Coupling the loopback default to middleware.IsDevModeFailOpen() means the
|
||||
// two safety levers — bind narrowness and auth strength — move together. A
|
||||
// production deploy (ADMIN_TOKEN set) keeps binding to all interfaces because
|
||||
// the auth chain is doing its job; a dev Mac (no ADMIN_TOKEN, MOLECULE_ENV=dev)
|
||||
// is reachable only via loopback because the auth chain is fail-open. See
|
||||
// molecule-core#7 for the original LAN exposure finding.
|
||||
func resolveBindHost() string {
|
||||
if v := os.Getenv("BIND_ADDR"); v != "" {
|
||||
return v
|
||||
}
|
||||
if middleware.IsDevModeFailOpen() {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findConfigsDir() string {
|
||||
candidates := []string{
|
||||
"workspace-configs-templates",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user