Repository navigation
fix(cli): #891 -- headless sys.openUrl logs the setup URL instead of launching a host GUI browser - #895
Merged
Conversation
…ing a host GUI browser
Legacy Frontier launched the owner's browser at the setupFrontier page to
collect config from potentially non-technical server owners. That onboarding
intent is a required feature and is preserved. The only wrong part is a
headless/server/CI process reaching into a host GUI session: there is no human
at that machine's desktop, and the daemon host is frequently not the operator's
workstation.
Root cause: webBrowser.openURL already branched on system.environment.isHeadless,
but that branch was not a no-exec branch -- it called sys.openUrl, the kernel
verb that double-forks and execs open(1). It succeeded, so the msg() fallback
never ran and a browser opened. Headless was the MORE aggressive path, not the
safer one.
The guard goes in the kernel verb, the single choke point every caller funnels
through (webBrowser.openURL, trialVersionCheck, ftpTable, Frontier.tools.commands
.openUrl, and the live trigger startupScript.ut:252 -> system.temp.installer
.urlToOpen set by finishInstall). Guarding here cannot be bypassed by a script
calling sys.openUrl directly, and needs no ODB edit.
Headless behavior by --browser value:
absent / "default" -> no exec; log the URL and record it at
system.temp.Frontier.pendingBrowserUrl
"agent-browser" -> unchanged (automated verification path)
"allow-gui" -> NEW; restores the real launch for a local desktop session
anything else -> rejected, as before
The marker records the last URL that wanted a browser (overwrite per call, no URL
sniffing), so first-run flows assert the onboarding step by URL equality rather
than a boolean. GUI builds are unaffected: the headless block is inside
FRONTIER_HEADLESS.
Note on file layout: Common/source/shellsysverbs.c is NOT linked into frontier-cli
(absent from frontier-cli/Makefile); tests/headless_sys_verbs.c is the production
headless implementation despite living under tests/. Both carry the logic and are
changed in lockstep, each with a TWIN comment naming the other. Also corrects a
latent wrong Pascal length byte in the shellsysverbs.c rejection message.
Tests: tests/openurl_headless_guard_test.sh proves the default performs NO exec
and the opt-in DOES, using PATH-shadowing stub launchers so no real browser is
ever invoked. Registered as test-openurl-guard in test-all. Four integration cases
added and two stale "not implemented" skips cleared.
Unit: exit 0. Integration: 2491 total, 0 failed (baseline 2488/0; +3 tests,
-2 skips). Pre-existing and unchanged: 3 sys.appisrunning failures and the
load-dependent test_callback_infrastructure segfault (#828), both reproduced on
pristine 2b56ce8.
Owner
Author
Gate Review: unit-891-openurl (PR #895)Verdict: PASS WITH NOTES — fix the P1 pair before merge (house completionist rule: P2s too)Tests (orchestrator-run merge record at 1e776a7)
Reviewers
All three converge: the only blocking-tier defects are error-path memory-ownership bugs in the new P1 — fix before merge (deduplicated)
Shared root cause: hand-rolled disposal conflicting with the setheapvalue/tmp-stack ownership contracts. ~4 lines total, one function. P2 — fix in the same round (completionist)
P2 — noted, no change required
Clean dimensions (explicitly verified, not assumed)
Stats
Next stepImplementer fixes P1 #1-2 and P2 #3-6 in one round; re-run guard test + unit + integration at the new SHA; orchestrator re-runs the merge record and delta-reviews the changed function; then merge. |
…rurl (P1-1, P1-2) + 4 P2s P1-1 (headless_sys_verbs.c): setheapvalue disposes the handle itself on failure (langvalue.c:594 -- "the caller isn't managing the memory any more"), so the disposehandle on that branch was a double free. Removed. P1-2 (headless_sys_verbs.c): on hashtableassign failure the value is still on the tmp stack, so a bare disposehandle left a dangling tmp-stack entry. Replaced with disposetmpvalue, which unstacks AND disposes. Documented that the assign->exempt pair must stay yield-free. P2-3 (shellsysverbs.c): allow_gui_browser scoped inside the FRONTIER_HEADLESS block; it would be set-but-unused in a GUI compile. The fl_no_exec assignment moved inside the same block so it can still read it. P2-4 (openurl_headless_guard_test.sh): the test copies Frontier.root into its scratch dir and points --system-root at the copy. The CLI opens the system root read-write; these runs must not touch the shared working DB. P2-5 (headless_sys_verbs.c): the setup-URL log_info now runs only after the marker write succeeds, so the operator-facing line cannot claim onboarding was reached when the marker was never recorded. P2-6 (both TUs): TWIN comments no longer claim "identical logic". The classic path suppresses the exec but does not log or set the marker, so the observable contract diverges. Both sides now say so and reference #893. Test note on P1-2: the review suggested pre-creating the marker path as a table to force the assign-failure branch from script. Verified directly that this does NOT fail in this build -- hashassign's protect block is gated on fllanghashassignprotect, which headless_threadglobals.c:60 initializes to false and whose only toggles (tableverbs.c:288/294) are commented out. The assign succeeds and overwrites the table. Added a test asserting that actual observable behavior instead of a failure that cannot occur; the P1-2 cleanup path is covered by inspection against the langvalue.c/lang.h contracts. Guard test 15/15. Integration 2492 total, 0 failed. Unit failure set = {#828} only (test_callback_infrastructure 2/6 and test_window_bridge 1/6 clean standalone runs; both nondeterministic, both unlinked from either file changed here -- verified test_window_bridge segfaults 4/5 on pristine 2b56ce8 too).
…s not reachable headless
The comment claimed the assign-failure branch was reachable from script by
pre-creating the marker path as a table, so hashassign would refuse a
non-external value over an external one. That refusal does not happen in this
build: it is gated on fllanghashassignprotect, which headless_threadglobals.c:60
initializes false and whose only toggles (tableverbs.c:288/294) are commented
out. Verified live -- the assign succeeds and overwrites the table.
The real headless trigger for that branch is allocation failure inside
hashassign (newhashnode). Comment corrected to say so, and to point at the
integration case covering the observed overwrite behavior.
Comment-only; no behavior change. Guard test 15/15; unit failure set = {#828}.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #891.
Headless Frontier no longer launches a host GUI browser. The first-run onboarding intent is preserved — only the surface moves.
Why this is not "suppress the browser launch"
Legacy Frontier launched the owner's browser at the setupFrontier page to collect configuration (site name, admin credentials) from potentially non-technical server owners. That is a required product feature, and it is what the current goal means by "completes first-run."
The wrong part is narrow: a headless/server/CI process reaching into a host GUI session. There is no human at that machine's desktop, and the daemon host is frequently not the operator's workstation. So the headless default now logs the URL for a remote operator and records it for flows and tests; the launch itself is preserved behind an explicit opt-in and is unchanged in GUI builds.
Root cause: headless was the MORE aggressive path
webBrowser.openURLalready branched onsystem.environment.isHeadless— but that branch was never a no-exec branch:sys.openUrlis the kernel verb that double-forks and callsexeclp("open", ...). It succeeds, so themsg(...)fallback never runs and a browser opens. Headless routed throughopen(1)while interactive routed through an AppleEvent — the headless path was the more aggressive of the two. That inversion is the bug.Trigger chain (this is how a browser actually launched during test cycles):
finishInstall.ut:227setssystem.temp.installer.urlToOpen = setupUrl(its ownopenUrlcall at:228is commented out)startupScript.ut:252callswebBrowser.openUrl (system.temp.installer.urlToOpen)at the next startup phasesys.openUrl→execlp("open", ...)→ browser opens on the host desktopWhere the guard goes, and a near-miss worth recording
The guard is at the
sys.openUrlkernel verb — the single choke point every caller funnels through (webBrowser.openURL,trialVersionCheck,ftpTable,Frontier.tools.commands.openUrl,startupScript). A guard here cannot be bypassed by a script callingsys.openUrldirectly, and it requires zero ODB edits, so no Virgin.root binary edit and no re-run of the #881/#892 repair risk.The near-miss: the exec path was first traced to
Common/source/shellsysverbs.cby reading the code. That file is not linked into frontier-cli (grep shellsysverbs frontier-cli/Makefile→ no matches). The CLI's real implementation istests/headless_sys_verbs.c(case sysv_openurl), pulled in viatests/headless_verbs.mk:47→frontier-cli/Makefile:240→:255→:266. A fix landing only inshellsysverbs.cwould have compiled, reviewed cleanly, and done nothing.What caught it: the red test ran against the built binary and captured a real exec —
That line is the TU-identity proof. Both copies are now changed in lockstep, each carrying a
TWIN:comment naming the other and stating which one is linked. Duplication tracked in #893.Behavior (headless build)
--browsersystem.temp.Frontier.pendingBrowserUrl, return true.defaultagent-browseragent-browser(automated verification path).allow-guiopen(1)/xdg-openlaunch for a local desktop-style session.The opt-in is a new value on the existing
--browserflag rather than a separate--allow-gui-browserflag: one flag gives one answer to "what does openUrl do here," where two flags could contradict each other. Usage text infrontier-cli/main.cdocuments all three modes.GUI builds are unaffected by construction — the whole block sits inside
FRONTIER_HEADLESS.Marker semantics
system.temp.Frontier.pendingBrowserUrlholds the last URL that wanted a browser: written on every no-exec call, overwriting, with no URL sniffing. Unit B (thefirstRootRunflag flip) asserts this equals the setup URL, which is a stronger assertion than a boolean would be. It is stored as a heap string rather than via the bigstring assign helpers, since a URL can exceed the 255-byte Pascal-string limit. A rejected URL (for example the empty string) never writes a marker.Also fixed: a latent wrong Pascal length byte
shellsysverbs.cdeclared\x45for a 76-character rejection message. Because that TU is not compiled by the CLI build, the headlessstatic_assertnever saw it. My twin edit initially repeated the mistake; the headless build caught mine, and I then verified the twin arithmetically. Both are now\x4c. The pre-commit hook confirms 1026/1026 Pascal lengths correct across 50 files.Tests
tests/openurl_headless_guard_test.sh(new, wired in astest-openurl-guardintest-alland.PHONY).Hard rule: no test may launch a host GUI browser. Exec assertions use PATH-shadowing stub launchers named
open,xdg-open, andagent-browserthat record their argv to a file. Sinceexeclpresolves throughPATH, the stub intercepts and the real browser is never reached — this is what lets the suite assert positively that the opt-in does exec.Coverage: default returns true / performs no exec / sets the marker; explicit
defaultbehaves identically; marker overwrites per call;allow-guiexecs with the URL and leaves the marker unset;agent-browserstill execs; unknown values rejected with no exec; empty URL errors before any fork.Exec recorded: open ...capture above.Four integration cases added to
sys_processor_tests.yaml, and two staleskip: "sys.openUrl not implemented"entries cleared — the verb is implemented, and those skips were hiding this path. One marker test explicitly clears the marker first, since sequential tests share one session (#852).Suite evidence
2b56ce868Delta is exactly this change: +3 net tests, −2 stale skips.
Every anomaly was attributed against a purpose-built pristine baseline worktree (since removed) rather than assumed:
sys.appisrunningfailures — reproduce identically on pristine2b56ce868. Pre-existing, untouched.test_callback_infrastructureSIGSEGV on the first unit run — this is Latent: in-memory hash node takes on-disk resolution path -> NULL deref (dbgeteof via hashresolvevalue) #828 verbatim, which names this exact binary at a 30–80% load-dependent rate. Passed standalone 14/14 and on a clean re-run (exit 0). Not introduced here, but recorded since it fired during this window.Follow-ups filed
openUrlplus theunixShellCommandtwin); the link-time ambiguity that produced the near-miss above. TheTWIN:comments added here are the stopgap and should be removed when that lands.sys.unixShellCommand(popen→/bin/sh, in the CLI build) can exec a launcher past this guard. Deliberately not guarded here: a general-purpose shell verb has no narrow contract to enforce, so that is its own decision. Flagging it explicitly so the boundary of this guard is not read as stronger than it is.Not in scope
No ODB edits —
Virgin.rootis byte-identical. No UserTalk script changes. GUI/Mac build behavior unchanged.