Why
PR #312 fixed a Windows cmd /c start &-truncation bug in the OAuth launcher — a bug that survived precisely because the launcher is copy-pasted three times and the copies had diverged: one was already fixed (mcp_browser.go used rundll32), while two others still used cmd /c start. The review's standing recommendation (finding 1, reiterated on the re-review inline at forge-ui/server.go) was to consolidate so this class of divergence cannot recur.
#312 closed the immediate bug and made the two copies that mattered identical + table-tested, but explicitly deferred the consolidation because it crosses module boundaries. This issue tracks that deferral.
The three copies today
All three now select rundll32 url.dll,FileProtocolHandler on Windows (post-#312), but each is a separately-maintained platform switch — nothing keeps them in sync:
| Site |
Module |
Symbol |
Shape |
forge-core/llm/oauth/flow.go |
forge-core |
browserCommand + openBrowser |
returns *exec.Cmd, errors on unsupported GOOS |
forge-ui/server.go |
forge-ui |
browserCommand + openBrowser |
returns *exec.Cmd, no-op on unsupported GOOS |
forge-cli/cmd/mcp_browser.go |
forge-cli |
openBrowserCLI |
builds cmd+args, .Start() inline, //nolint:gosec |
The first two are already byte-identical (from #312) and table-tested the same way; the third has the same behavior but a different signature.
The constraint that makes this non-trivial
The helper spans three modules (forge-core / forge-cli / forge-ui), so the only shared home is a package in forge-core (both forge-cli and forge-ui already depend on it via replace).
BUT mcp_browser.go carries a deliberate constraint (spec §4.6 / review B4):
it lives in the CLI package (not forge-core/mcp) because the runtime image must NOT link os/exec for MCP code. The CLI forge mcp login command is laptop-only and never runs inside the agent runtime.
So the shared package must be a forge-core package that is NOT in any import path the MCP runtime links. forge-core/llm/oauth already links os/exec and is not on the MCP runtime path, but a neutral, purpose-named package is cleaner.
Proposed shape
- New package e.g.
forge-core/browser (or forge-core/internal/browserlauncher) exporting:
Command(goos, url string) *exec.Cmd — pure, testable selection (nil for unsupported GOOS)
Open(url string) error — Command(runtime.GOOS, url).Start(), wrapping the unsupported-GOOS case
- Repoint all three call sites at it; delete the local copies.
- Keep the single table test (
darwin/linux/windows/plan9, asserting the Windows argv is rundll32 url.dll,FileProtocolHandler <url> and the URL rides as a single un-split trailing argument) in the shared package; drop the two duplicated tests.
- Verify the MCP-runtime-must-not-link-
os/exec invariant still holds after the move — the whole reason mcp_browser.go was placed where it is. A build/link assertion or an explicit note in the new package doc should call out that it must never be imported from MCP runtime code.
Acceptance
Context
Follow-up to #312 (merged/ready). Non-urgent — the bug is fixed and the two copies that mattered are identical + tested; this is the durable fix that removes the divergence itself.
Why
PR #312 fixed a Windows
cmd /c start&-truncation bug in the OAuth launcher — a bug that survived precisely because the launcher is copy-pasted three times and the copies had diverged: one was already fixed (mcp_browser.gousedrundll32), while two others still usedcmd /c start. The review's standing recommendation (finding 1, reiterated on the re-review inline atforge-ui/server.go) was to consolidate so this class of divergence cannot recur.#312 closed the immediate bug and made the two copies that mattered identical + table-tested, but explicitly deferred the consolidation because it crosses module boundaries. This issue tracks that deferral.
The three copies today
All three now select
rundll32 url.dll,FileProtocolHandleron Windows (post-#312), but each is a separately-maintained platform switch — nothing keeps them in sync:forge-core/llm/oauth/flow.gobrowserCommand+openBrowser*exec.Cmd, errors on unsupported GOOSforge-ui/server.gobrowserCommand+openBrowser*exec.Cmd, no-op on unsupported GOOSforge-cli/cmd/mcp_browser.goopenBrowserCLIcmd+args,.Start()inline,//nolint:gosecThe first two are already byte-identical (from #312) and table-tested the same way; the third has the same behavior but a different signature.
The constraint that makes this non-trivial
The helper spans three modules (forge-core / forge-cli / forge-ui), so the only shared home is a package in forge-core (both forge-cli and forge-ui already depend on it via
replace).BUT
mcp_browser.gocarries a deliberate constraint (spec §4.6 / review B4):So the shared package must be a forge-core package that is NOT in any import path the MCP runtime links.
forge-core/llm/oauthalready linksos/execand is not on the MCP runtime path, but a neutral, purpose-named package is cleaner.Proposed shape
forge-core/browser(orforge-core/internal/browserlauncher) exporting:Command(goos, url string) *exec.Cmd— pure, testable selection (nil for unsupported GOOS)Open(url string) error—Command(runtime.GOOS, url).Start(), wrapping the unsupported-GOOS casedarwin/linux/windows/plan9, asserting the Windows argv isrundll32 url.dll,FileProtocolHandler <url>and the URL rides as a single un-split trailing argument) in the shared package; drop the two duplicated tests.os/execinvariant still holds after the move — the whole reasonmcp_browser.gowas placed where it is. A build/link assertion or an explicit note in the new package doc should call out that it must never be imported from MCP runtime code.Acceptance
browser.Command/Openhelper in forge-core; zero platform-switch duplication.flow.go,server.go,mcp_browser.go) call it.os/execvia this package (the §4.6 constraint).gofmt+golangci-lintclean across forge-core / forge-cli / forge-ui; all three suites green.Context
Follow-up to #312 (merged/ready). Non-urgent — the bug is fixed and the two copies that mattered are identical + tested; this is the durable fix that removes the divergence itself.