Skip to content

feat(tools-pack): cross-platform WebUI (no-Electron) packaging type - #3509

Closed
SugarFatFree wants to merge 48 commits into
nexu-io:mainfrom
SugarFatFree:pr/webui-packaging-type
Closed

feat(tools-pack): cross-platform WebUI (no-Electron) packaging type#3509
SugarFatFree wants to merge 48 commits into
nexu-io:mainfrom
SugarFatFree:pr/webui-packaging-type

Conversation

@SugarFatFree

Copy link
Copy Markdown

Closes #3508

Why

Use case: I run Open Design on a headless Linux server and wanted an Electron-free, terminal-launched, browser-accessed deployment that's also easy to embed into another agent through the od / launcher CLI. Pain: the only packaged builds are the mac/Windows GUI installers, leaving headless/container setups with no supported path. Opened #3508 first to align on scope.

What users will see

  • A new tools-pack webui build --platform <mac|win|linux> [--arch ...] command that produces a terminal-launched distribution.
  • A launcher CLI open-design start|stop|status: runs in the background by default (--foreground to stay attached), prints the access URL, configurable port/host/token, auto-creates webui.config.json on first run, shows the LAN IP for 0.0.0.0 binds, and localizes its output (en/zh-CN).

Surface area

  • CLI / env vartools-pack webui subcommand + the open-design launcher CLI (--port / --daemon-port / --host / --token / --foreground / --lang)
  • No other surfaces: no UI, no new root dependency, no API/contract change, and no new web i18n keys (the launcher carries its own en/zh-CN strings).

Screenshots

N/A — CLI only, no UI.

Validation

  • pnpm guard, pnpm typecheck — green
  • pnpm --filter @open-design/tools-pack test (168), @open-design/packaged test (118; the 2 desktop suites fail only because this environment has no Electron binary, unrelated to this change), @open-design/daemon server-paths (13)
  • tools-pack and packaged builds pass
  • Notes: refactors the assembly core out of linux.ts into assemble.ts (shared by the Linux AppImage lane and the WebUI lane), and repoints upstream fix(pack): add missing download and host packages to Linux INTERNAL_PACKAGES #2837's internal-packages-coverage regression test at the relocated INTERNAL_PACKAGES definition.

`tools-pack webui build --platform <mac|win|linux> [--arch ...]` produces a
terminal-launched distribution (daemon + web in server output mode) that runs
on the user's system Node 24 — no Electron. Bundles the workspace node app, the
target's prebuilt better-sqlite3, the resource trees, and terminal launchers
(open-design start|stop|status) plus double-click wrappers.

- assemble.ts: extract the shared assembly core (workspace tarballs, node-app
  install, buildWorkspaceArtifacts, copyResourceTree) from linux.ts so the
  Linux AppImage lane and the WebUI lane share one path.
- webui.ts: per-platform archive (zip on mac/win, tar.gz on linux), target
  better-sqlite3 prebuild selection, and pruneBuildOnlyNativeModules to strip
  the build-only @next/swc compiler (~125MB) the server runtime never loads.
- apps/packaged: webui-launcher (start/stop/status) detaches into the
  background by default (--foreground to stay attached), auto-creates
  webui.config.json on first run, supports web + daemon port / host / token
  config, prints the LAN IP for 0.0.0.0 binds, persists an auto-generated
  remote token, and localizes output (en + zh-CN). startPackagedSidecars gains
  optional network injection; default callers are unchanged.
- apps/daemon: resolveProcessResourcesPath recognizes the packaged node-app
  layout (daemon under <appRoot>/node_modules) so OD_RESOURCE_ROOT validates
  for the system-Node runtime; waitForStatus embeds the daemon log tail so a
  startup crash surfaces in the terminal.

Both surfaces (build CLI + launcher CLI) are CLI-only by nature; no web UI
surface applies. Validated with pnpm guard, pnpm typecheck, and the tools-pack
/ packaged / daemon test suites.
@SugarFatFree
SugarFatFree requested a review from a team as a code owner June 2, 2026 11:03
@lefarcen
lefarcen requested a review from nettee June 2, 2026 11:52
@lefarcen lefarcen added size/XXL PR changes 1500+ lines risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps type/feature New feature labels Jun 2, 2026
@lefarcen
lefarcen requested a review from elihahah666 June 2, 2026 11:53

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking path/config regressions showed up in the new launcher flow: stop/status do not target the same runtime when config-driven namespace/data-dir overrides are used, and scoped dataDir values are normalized incorrectly. Details inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread apps/packaged/src/webui-launcher.ts Outdated
Comment thread apps/packaged/src/webui-launcher.ts Outdated
…malize scoped OD_DATA_DIR

Two path/config regressions in the WebUI launcher flagged in review:

1. stop/status ignored config-driven namespace. `start` derives its desktop
   IPC socket namespace from the resolved config (which may set `namespace`),
   but `commandStopOrStatus` always rebuilt the namespace from
   OD_PACKAGED_NAMESPACE/defaults. A config-driven instance was therefore
   reported as "not running" by a later `status`/`stop`. Both surfaces now
   derive the namespace through one shared `resolveRuntimeNamespace()` helper,
   and `main()` loads the same config (without scaffolding) for stop/status.

2. scoped OD_DATA_DIR got a doubled `namespaces` segment.
   `resolveNamespaceBaseRoot()` blindly appended `/namespaces`, so a scoped
   value (`<base>/namespaces/<ns>/data`, already supported by paths.ts) became
   `.../data/namespaces`, forking the launcher's runtime/log tree from the
   daemon data dir. Extracted `resolveWebuiNamespacesRoot()` in paths.ts which
   reuses the same scoped-vs-unscoped rule as `resolvePackagedDataRoot()`.

Adds unit specs for both helpers (paths.test.ts, webui-config.test.ts).
@SugarFatFree

Copy link
Copy Markdown
Author

Thanks for the careful review — both regressions confirmed and fixed in 4112f46.

1. stop/status ignored config-driven namespace
You were right: start derives the desktop IPC socket namespace from the resolved config, but commandStopOrStatus() rebuilt it from OD_PACKAGED_NAMESPACE/defaults, so a webui.config.json with namespace set made a later status/stop probe the wrong socket and report "not running". Both surfaces now resolve the namespace through a single shared resolveRuntimeNamespace(config, env) helper, and main() loads the same config for stop/status (without scaffolding — stopping must not create files). dataDir is applied for env parity too, though as you noted the socket itself is namespace-scoped so namespace is what governs discovery.

2. Scoped OD_DATA_DIR got a doubled namespaces segment
Confirmed. resolveNamespaceBaseRoot() blindly appended /namespaces, so a scoped value (<base>/namespaces/<ns>/data) became .../data/namespaces, forking the launcher tree from the daemon data dir. Extracted resolveWebuiNamespacesRoot() into paths.ts, which reuses the same scoped-vs-unscoped detection as resolvePackagedDataRoot() (via getScopedPackagedDataRootNamespace) and expandHomePrefix for ~/$HOME. A scoped value now resolves its namespaces/ parent two levels up instead of appending another segment.

Added unit specs for both helpers (paths.test.ts, webui-config.test.ts, including a "start and stop/status agree on the namespace" invariant test). pnpm guard, pnpm typecheck, and the packaged suite pass (the two desktop suites only fail here because this sandbox has no Electron binary).

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two correctness issues still need to be closed out in the new WebUI launcher flow: the explicit --config token-persistence path can change the runtime settings on the next restart, and the new --host surface still emits malformed URLs for concrete IPv6 binds. Details inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread apps/packaged/src/webui-launcher.ts Outdated
Comment thread apps/packaged/src/webui/launcher.ts
…en scaffolding

webui.config.example.json now annotates every field (including the optional
namespace/dataDir/lang keys) with `// <field>` sibling comment keys. These keep
the example a valid JSON document while explaining each option in place.

First-run scaffolding no longer copies the example verbatim. It seeds
webui.config.json from the example's real values but strips every `//`
documentation key, so the generated live config is pure data: it can never fail
JSON.parse and never carries a description string as a bogus value (e.g. an
optional field documented only by a `// namespace` key does not leak a
`namespace` value). A missing or unparseable example falls back to built-in
defaults so first run always yields a valid config.

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking launcher regressions are still present on the current head: the explicit --config remote-start path can persist a config that changes the next restart's runtime settings, and concrete IPv6 --host values still produce malformed URLs in the user-facing/banner path.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread apps/packaged/src/webui-launcher.ts Outdated
Comment thread apps/packaged/src/webui/launcher.ts
The upstream project is English-first. Translate the WebUI distribution's
user-facing resources (README, webui.config.example.json field docs,
open-design.sh/.cmd messages, launch-mac.command prompt) and the WebUI source
comments/error strings to English.

The zh-CN entries in webui-i18n.ts are intentionally kept — they are the
launcher's Chinese localization table (the launcher output stays bilingual,
selected by --lang / system locale), not comments.
Render the WebUI packaging-type design doc and implementation plan fully in
English to match the upstream English-first convention, including the
illustrative code/script samples and embedded README inside the plan. Structure,
code, paths, flags, and commands are preserved.
…plicit --config

Two correctness issues flagged in review:

1. Concrete IPv6 binds produced malformed URLs. resolveDisplayHost() passes a
   bare IPv6 literal (e.g. `fd00::10` from `--host fd00::10`) through unchanged,
   but browserUrl()/daemonDirectUrlFor() interpolated it as
   `http://${host}:${port}`, yielding the unparseable `http://fd00::10:7456`
   that was printed, copied, and handed to openBrowser(). Added
   formatHostForUrl()/composeHttpUrl() (a single bracketing chokepoint) and
   routed both URLs through it; added IPv6 regression specs.

2. Explicit `--config <path>` token persistence created a lossy config.
   discoverConfigFile() deliberately skips scaffolding for an explicit path, so
   a remote first start (`start --host 0.0.0.0 --config /missing.json`) wrote a
   `{ token }`-only file; the next `start --config` then fell back to defaults
   (host/port/namespace/dataDir all lost). persistTokenToConfig() now
   materializes the FULL resolved shape when the target file does not exist
   (dynamic daemonPort round-trips as 0), so a follow-up start reproduces the
   same runtime. Existing files keep their own keys + token as before.
@SugarFatFree

Copy link
Copy Markdown
Author

Both new issues confirmed and fixed in 747aa0f.

1. Malformed URLs for concrete IPv6 binds (webui-launcher.ts:151)
Confirmed. resolveDisplayHost() passes a bare IPv6 literal through unchanged, but browserUrl()/daemonDirectUrlFor() interpolated it as http://${host}:${port}, so --host fd00::10 produced the unparseable http://fd00::10:7456 that was printed, copied, and handed to openBrowser(). Added a single bracketing chokepoint — formatHostForUrl() (brackets a literal only when net.isIP() === 6) and composeHttpUrl() — and routed both URLs through it, so the output is now http://[fd00::10]:7456. Added regression specs covering a non-loopback IPv6 host end-to-end (resolveDisplayHost("fd00::10")composeHttpUrl(...) → asserts new URL() parses and hostname === "[fd00::10]"), plus IPv4/hostname/already-bracketed pass-through.

2. Explicit --config <path> token persistence created a lossy config (webui-launcher.ts:369)
Confirmed — and your trace is exact. discoverConfigFile() skips scaffolding for an explicit path, so start --host 0.0.0.0 --config /etc/open-design/webui.json (file absent) wrote a { "token": ... }-only file, and the next start --config fell back to defaults for host/port/namespace/dataDir.

I took the "write the resolved config shape" option (the remote flow this feature enables should just work). persistTokenToConfig() now: when the target file exists, preserves its keys and adds the token (unchanged); when it does not exist, it materializes the FULL resolved runtime shape (host, port, daemonPort, openBrowser, and namespace/dataDir when set) plus the token. Dynamic daemonPort (null) round-trips as 0 so it stays dynamic rather than snapping to the fixed 7457 default. Added a spec asserting the created file re-resolves to the same runtime (resolveWebuiConfig(written) reproduces host/port/daemonPort/namespace/dataDir).

pnpm guard, pnpm typecheck, and the packaged suite pass (the two desktop suites only fail in my sandbox for lack of an Electron binary).

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking regressions remain in the new WebUI packaging flow. Details inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread tools/pack/src/webui.ts Outdated
Comment thread apps/packaged/src/webui/launcher.ts
…rely

A second `start` on the same namespace (instance already running) used to spawn a
worker that collided on the IPC socket and the web/daemon ports; the worker died
and the user saw a confusing "failed to start, see log". Add a pre-flight desktop
IPC STATUS probe: if an instance is already serving the namespace, print its
address (+ the stop hint) and exit 0. The probe is fast when nothing is running
(a non-existent socket errors immediately) and never false-positives on a stale
socket. Extracted probeWebuiStatus() and reused it in waitForWebuiReady().
…ailures

1. The Linux double-click entry was never executable. Packaging chmod'd only
   open-design.sh and the macOS .command; open-design-webui.desktop (tracked
   100644) was copied as-is, so many file managers refuse to launch it — breaking
   the double-click contract in the WebUI README. Extracted
   stageWebuiLauncherResources() and chmod the staged .desktop to 0o755; added a
   tools/pack regression test that stats the staged mode (Linux + macOS).

2. stop/status collapsed every IPC failure into "not running". A 2s timeout,
   permission error, or protocol/ok:false rejection was reported as
   `{"state":"stopped"}` / "not running", so a wedged-but-live worker made
   `status` lie and `stop` silently orphan the service. Added
   isNotRunningIpcError() (only ENOENT / ECONNREFUSED mean not-running) and
   rethrow everything else so operators see the real error; added a classifier
   regression test covering the timeout/permission/protocol cases.
@SugarFatFree

Copy link
Copy Markdown
Author

Both confirmed and fixed in 6093d91.

1. Linux .desktop entry was not executable (tools/pack/src/webui.ts:214)
Confirmed — the source is tracked 100644 and packaging only chmod'd open-design.sh and the macOS .command, so the staged open-design-webui.desktop shipped non-executable and many file managers refuse to launch it, breaking the README's Linux double-click contract. Extracted stageWebuiLauncherResources() and now chmod the staged .desktop to 0o755. Added a focused tools/pack regression test that stages into a temp dir and stats the mode (asserts 755 for the Linux .desktop and the macOS .command, plus open-design.sh).

2. stop/status swallowed every IPC failure as "not running" (webui-launcher.ts)
Confirmed and agreed — a 2s timeout, permission error, or protocol/ok:false rejection was collapsed into {"state":"stopped"} / "not running", so a wedged-but-live worker made status lie and stop silently orphan the service. Added isNotRunningIpcError() which treats only ENOENT (missing socket) and ECONNREFUSED (nothing accepting) as not-running; everything else — IPC request timed out, EACCES/EPERM, malformed/ok:false replies — is rethrown so it surfaces via the top-level handler (exit 1) instead of lying. Added a classifier regression test covering the timeout/permission/protocol cases.

pnpm guard, both typechecks, the packaged suite (134) and the tools-pack suite (170) pass. (The two desktop suites only fail in my sandbox for lack of an Electron binary.)

Note: while here I also made a repeated start idempotent (commit e23f8a1) — an already-running namespace now prints its address and exits 0 instead of spawning a colliding second worker that failed obscurely.

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blocking launcher-path regression is still present on the current head: the new status-probe helper suppresses real IPC failures and can misclassify a wedged live instance as absent, so start can race into a duplicate launch and the readiness path can hide the underlying bug. Details inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread apps/packaged/src/webui-launcher.ts Outdated
probeWebuiStatus() caught every requestJsonIpc() failure and returned null, even
though isNotRunningIpcError() was just added to tell a dead socket from a real
IPC bug. So the pre-flight `start` path could treat a live-but-wedged worker
(timeout, malformed reply, permission error) as "not running" and spawn a second
instance on the same namespace/ports, and waitForWebuiReady() would poll to the
generic timeout instead of surfacing the actual failure.

Extracted probeWebuiStatus() into a side-effect-free, requester-injectable
webui-ipc module and gated its catch with isNotRunningIpcError(): only ENOENT /
ECONNREFUSED resolve to null (not running); every other failure is rethrown. The
`start` pre-flight now surfaces a wedged-worker error instead of racing into a
duplicate launch, and waitForWebuiReady() reports the real failure (with the log
tail) instead of polling to timeout — while a still-binding socket (ENOENT/
ECONNREFUSED) keeps polling as before. Added a launcher-level regression test
covering the timed-out / permission / protocol / not-running / running cases.
@SugarFatFree

Copy link
Copy Markdown
Author

Confirmed and fixed in c605e5b — and you're right that this is the same class of bug as the status/stop fix, still living in the new helper.

probeWebuiStatus() was swallowing every requestJsonIpc() failure into null. I extracted it into a side-effect-free, requester-injectable webui-ipc module and gated its catch with isNotRunningIpcError():

  • Not running (ENOENT / ECONNREFUSED) → null.
  • Everything else (timeout, EACCES/EPERM, malformed/ok:false) → rethrown.

Effects:

  • start pre-flight: a live-but-wedged worker now surfaces the real error and exits non-zero instead of treating the namespace as free and spawning a colliding second instance.
  • waitForWebuiReady(): a real probe failure (timeout once the socket is up, permission, malformed reply) is surfaced immediately with the log tail, instead of polling to the generic 60s timeout. A still-binding socket (ENOENT/ECONNREFUSED) keeps polling exactly as before, so normal slow startup is unaffected.

Added a launcher-level regression test (apps/packaged/tests/webui-ipc.test.ts) with an injected requester covering all five outcomes: running, not-running (ENOENT/ECONNREFUSED), url-less reply, timed-out probe (rethrows), and permission/protocol failures (rethrow).

pnpm guard, the packaged typecheck/build, and the packaged suite (139) pass. (The two desktop suites only fail in my sandbox for lack of an Electron binary.)

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blocking launcher correctness issue remains on the current head, and there is one release-workflow packaging issue worth tightening before this ships.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread apps/packaged/src/webui-ipc.ts Outdated
Comment thread .github/workflows/webui-build.yml Outdated
…h exactly one archive

1. probeWebuiStatus() returned null when a connected STATUS reply carried no
   url, conflating a live-but-wedged/regressed worker with a not-running socket.
   Callers use null to mean "namespace free" (start pre-flight → duplicate
   launch) and "keep polling" (waitForWebuiReady → hidden error). Once request()
   resolves the socket is live and the reply shape is the invariant, so a
   missing/empty url is now thrown as an `invalid STATUS reply` error; only
   ENOENT/ECONNREFUSED still map to not-running. Updated the launcher-level spec
   so the no-url case is a reject expectation.

2. The release workflow's artifact upload used a recursive glob over the
   tools-pack tree, which is broader than the one-archive-per-entry release
   contract and risks sweeping in internal staging files. Now reads the build
   JSON's archivePath and uploads exactly that file (validated with test -f), so
   each matrix entry publishes precisely one archive.
@SugarFatFree

Copy link
Copy Markdown
Author

Both addressed in 3c15c07.

1. probeWebuiStatus() — url-less STATUS reply is now a protocol error (webui-ipc.ts)
Agreed, and consistent with the invariant you named: once request() resolves, the socket is live, so the STATUS reply shape is the contract — a missing/empty url is a wedged/regressed worker, not a free namespace. It now throws invalid STATUS reply from <ipc>: missing url instead of returning null; only ENOENT/ECONNREFUSED still map to not-running. So the start pre-flight surfaces it (no duplicate launch) and waitForWebuiReady() fails with the log tail (no silent poll-to-timeout). Updated the launcher-level spec: the former "returns null for a reply that carries no url" case is now a rejects.toThrow("invalid STATUS reply") expectation.

2. Release workflow — publish exactly one archive per matrix entry (webui-build.yml)
Tightened. The build step now reads archivePath from the --json output (the build subprocess buffers its own stdout via execFile, so the teed JSON is clean) and uploads exactly that file, guarded by test -f. No more recursive ** glob over the tools-pack tree, so no staging file can ever be published as a release asset, and gh release upload dist/* gets precisely one archive per platform.

One factual note for the record: I checked the previous glob …/**/open-design-webui-* against the staged file open-design-webui.desktop — the trailing - in the pattern vs the . in .desktop means it would not actually have matched, so no stray .desktop was being published today. But the recursive glob was fragile and broader than the release contract, so switching to the explicit archivePath is the right hardening regardless.

pnpm guard, the packaged typecheck/build, and the packaged suite (139) pass; the workflow YAML parses. (The two desktop suites only fail in my sandbox for lack of an Electron binary.)

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SugarFatFree I re-checked the WebUI pack lane, the packaged launcher/runtime wiring, the daemon and web host/resource-root handling, and the added regression coverage on the current head. The changed ranges still line up with the runtime invariants around config-driven namespace and data-dir targeting, IPv6-safe URLs and proxying, detached start/status lifecycle, and namespace-scoped archive publishing, and I didn’t find a remaining actionable issue in this diff. I couldn’t rerun the Vitest/package slices locally because this reviewer worktree has no node_modules, but the implementation itself looks consistent and ready to merge. Thanks for pushing through a fairly tricky packaging surface.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@SugarFatFree

Copy link
Copy Markdown
Author

Friendly nudge 🙏 — head baa82e8b just merged the latest upstream/main (95 commits; the lone tools/pack/src/linux.ts import conflict was resolved as a union of upstream's new imports plus our assemble.ts re-export). Local gate is green: pnpm guard 55/55, tools-pack + packaged + web typecheck clean, tools-pack tests 226 passed.

State is MERGEABLE / reviewDecision: APPROVED (bots re-reviewed the merge commit). The only thing keeping it BLOCKED is the fork-PR Actions gate: ci sits at action_required and needs a maintainer to click "Approve and run workflows" on the Checks tab — I can't self-approve my own fork's runs.

@PerishCode @lefarcen whenever you have a moment, could you kick off CI for this head? Happy to rebase again if more upstream lands. Thanks!

@lefarcen

Copy link
Copy Markdown
Contributor

Hey @SugarFatFree — thanks for the clear handoff after the upstream merge.

I re-checked the changed files on baa82e8b; this still reads like packaging/runtime wiring rather than a user-facing visual UI delta, so there isn’t a separate design-routing step from my side. The remaining blocker is the maintainer-side workflow approval gate you called out.

@SugarFatFree

Copy link
Copy Markdown
Author

Gentle morning ping 🌅 — this is still green and ready on head baa82e8b: MERGEABLE, reviewDecision: APPROVED, conversations resolved. The only thing holding it at BLOCKED is the fork-PR Actions gate — ci sits at action_required waiting for a maintainer to click "Approve and run workflows" on the Checks tab (I can't self-approve my own fork's runs).

@PerishCode @lefarcen whenever you get a chance, a single click will let CI run and unblock the merge. Happy to re-sync if more upstream lands. Thanks!

@lefarcen

lefarcen commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Hey @SugarFatFree — saw the update.

I re-checked the changed-file set on baa82e8b; this still reads as packaging/runtime wiring rather than a visual UI delta, so there isn't a separate design-review step on this head. The remaining blocker is still the maintainer-side Approve and run workflows click you called out.

@lefarcen
lefarcen requested a review from 811634243-sketch July 2, 2026 08:54
@lefarcen lefarcen added the needs-product-review Feature PR awaiting product sign-off before merge (see roadmap) label Jul 2, 2026
@open-design-crew

Copy link
Copy Markdown
Contributor

@SugarFatFree Could you rebase this onto the latest main before we merge? The branch has diverged quite a bit from current main, and I'd rather validate/merge against an up-to-date base than risk a stale integration. Thanks!

@lefarcen lefarcen removed the needs-product-review Feature PR awaiting product sign-off before merge (see roadmap) label Jul 5, 2026
…tream merge

The upstream merge added win-manifest/win-nsis test files whose ToolPackConfig
fixtures omit the arch field. This branch makes arch a required field on
ToolPackConfig, so those fixtures fail typecheck. Set arch: "x64" to match the
idiom used across the existing tools-pack test fixtures.
@SugarFatFree

Copy link
Copy Markdown
Author

@open-design-crew Done — the branch is now updated to the latest main (b1dad3b0) and no longer diverged (0 commits behind). New head: 32a55924.

A note on merge vs. rebase: this branch already carries several Merge upstream/main commits from earlier syncs, so a literal git rebase would rewrite ~44 commits (including those merges) and force-push, with a high chance of replaying conflicts across the 114 upstream commits. To give you the up-to-date base you asked for without a risky history rewrite, I merged the latest main into the branch instead (non-destructive, no force-push). The merge itself was conflict-free. If you specifically need a linear/rebased history before merging, say the word and I'll do a true rebase.

One small integration fix was required and is included as 32a55924: the upstream merge brought in new win-manifest/win-nsis test files whose ToolPackConfig fixtures omit arch, which is a required field on this branch — I set arch: "x64" to match the idiom used across the existing tools-pack fixtures.

Local validation is green against the updated base:

  • pnpm guard — 71/71
  • pnpm --filter @open-design/packaged typecheck — clean
  • pnpm --filter @open-design/tools-pack typecheck — clean
  • pnpm --filter @open-design/web typecheck — clean
  • pnpm --filter @open-design/tools-pack test — 244 passed / 7 skipped

Ready for validation/merge on 32a55924. Thanks!

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One launcher correctness issue still needs to be closed out on the current head.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread apps/packaged/src/sidecars.ts
@lefarcen

lefarcen commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Hey @SugarFatFree — I re-checked the current head around @nettee's latest review (apps/packaged/src/webui/launcher.ts, apps/packaged/src/sidecars.ts, and the daemon origin-validation path).

The actionable blocker still looks like the same one he called out: the WebUI path now forwards OD_BIND_HOST and OD_API_TOKEN into the daemon env, but it still doesn't pass the browser-facing web port through as OD_WEB_PORT. With the default split-port setup (7456 web / 7457 daemon), that leaves the daemon validating /api requests against the daemon port it knows about instead of the browser port the request actually comes from, so this path can still 403 after a successful launch.

Once buildPackagedDaemonSpawnEnv() teaches the daemon the web port as well (or the launcher otherwise guarantees the daemon learns the browser-facing port before origin validation starts), this head should be ready for another review pass.

…es origin validation

In the WebUI split-port runtime the browser loads the web sidecar on the
browser-facing port (default 7456) and its /api requests are proxied to the
daemon (default 7457) carrying Origin: http://<host>:7456. The daemon's
origin-validation only allow-lists a browser origin whose port is OD_PORT or
OD_WEB_PORT (apps/daemon/src/origin-validation.ts:allowedBrowserPorts). But
buildPackagedDaemonSpawnEnv() forwarded the daemon bind host and token yet
never OD_WEB_PORT, so the daemon only knew 7457 and 403'd legitimate browser
/api traffic even though 'open-design start' succeeded.

Forward network.webPort as OD_WEB_PORT when it is a concrete port > 0. The
ephemeral --port 0 case cannot be validated up front (the daemon is spawned
before the web child binds and has no runtime origin-update channel), so
resolveWebuiConfig now rejects a browser-facing port 0 with an actionable
message; the internal daemonPort 0 (a loopback port the browser never sees)
stays valid.

Red specs: sidecars.test.ts pins OD_WEB_PORT forwarding (and its omission when
no concrete web port is given); config.test.ts pins the port-0 rejection.

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One launcher config correctness issue remains on the current head.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment thread apps/packaged/src/webui/config.ts
…pback

resolveWebuiConfig() documents daemonPort 0 as the single dynamic-loopback
escape hatch, but `daemonPortRaw > 0 ? daemonPortRaw : null` also folded every
negative value (from --daemon-port, webui.config.json, or OD_PORT) into that
same null path. A typo like `--daemon-port -1` would then silently launch the
daemon on a random loopback port instead of failing fast, so the config surface
lied about which values are valid.

Reject a negative (or non-integer) daemonPort with a clear error; 0 still means
dynamic and positive ports are honored. Mirrors the browser-facing port <= 0
rejection added just above. Red spec in config.test.ts covers flag/config/env.
@lefarcen

lefarcen commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Hey @SugarFatFree — the actionable blocker on the current head is @nettee's latest config follow-up.

Right now resolveWebuiConfig() still folds any daemonPortRaw <= 0 into the same null path, so --daemon-port -1 / OD_PORT=-1 / { "daemonPort": -1 } all silently behave like the documented 0 escape hatch and launch on a random loopback port. Once negative daemon ports fail fast with a clear error — and the regression covers the flag/config/env inputs so only 0 keeps the dynamic-port meaning — this head should be ready for another pass.

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SugarFatFree I re-checked the current head's launcher follow-up around OD_WEB_PORT forwarding, config-driven namespace/data-root resolution, IPv6-safe URL/proxy handling, and the final daemonPort validation tightening. The changed ranges and added regression coverage line up with those invariants, and I didn’t find a remaining actionable issue in this diff. I couldn’t rerun the Vitest/package slices locally because this reviewer worktree has no node_modules, but the implementation itself looks consistent and ready to merge. Thanks for closing out a fairly tricky packaging surface.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@SugarFatFree

Copy link
Copy Markdown
Author

@lefarcen — both of the follow-ups you called out are now closed on head 630f534b, and @nettee re-approved it (reviewDecision is back to APPROVED):

  1. OD_WEB_PORT (split-port origin validation)40ab3e0a: buildPackagedDaemonSpawnEnv() now forwards the browser-facing web port as OD_WEB_PORT when it's a concrete port > 0, so on the default 7456/7457 split the daemon allow-lists the browser origin instead of 403'ing /api. The ephemeral --port 0 case (which can't be validated up front, since the daemon is spawned before the web child binds and has no runtime origin-update channel) is now rejected in resolveWebuiConfig() with a clear message.
  2. Negative daemon port630f534b: resolveWebuiConfig() now rejects a non-integer/negative daemonPort (flag/webui.config.json/OD_PORT) with an actionable error, so only an explicit 0 keeps the documented dynamic-loopback meaning.

Both landed red-spec-first; local gate is green (pnpm guard 71/71, packaged/tools-pack/web typecheck clean, packaged tests 203 passed — only the 2 desktop Electron suites fail for lack of an Electron binary). Earlier I also updated the branch to the latest main (b1dad3b0) per @open-design-crew's request.

The only thing left is the maintainer-side Approve and run workflows on the fork PR (the ci check sits at action_required, which I can't self-approve) and then the merge. Thanks for the quick turnarounds!

@lefarcen

lefarcen commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the detailed write-up — the two launcher/config follow-ups are easy to trace from here, and this looks workflow-side rather than another author-side change at this point.

Once the maintainer-side fork workflow gate is cleared, the PR should be back in merge territory.

@SugarFatFree

Copy link
Copy Markdown
Author

Gentle daily nudge 🙏 — head 630f534b is still green (@nettee re-approved, mergeable, no conflicts). The only thing left is the maintainer-side fork-workflow gate that a contributor can't clear. Whenever you have a moment to hit Approve and run workflows, it should drop straight into merge territory. No rush — just keeping it on the radar. Thanks @lefarcen!

@lefarcen

lefarcen commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the nudge, @SugarFatFree — I re-checked the WebUI packaging/runtime changes on this head.

This still looks like a QA-needed runtime path rather than a visual design review case, so I've handed it to QA now. Once that validation clears, the remaining maintainer-side workflow step should be the last gate from the bot side.

@SugarFatFree

Copy link
Copy Markdown
Author

Perfect, thanks for routing it to QA @lefarcen 🙏 — that makes sense for a runtime/packaging path. I'm on standby: if QA turns up anything on the terminal-launched WebUI runtime (split-port --port/--daemon-port, origin validation, or the sidecar spawn env), I'll turn a fix around quickly on this branch. Happy to provide repro steps or a walkthrough of the packaging lane if that helps the validation.

@lefarcen

lefarcen commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Thanks — that’s helpful context. If QA hits the split-port/origin-validation path or wants a quick packaging-lane walkthrough, I’ll point them back to this thread so they can grab repro steps from you directly.

@AmyShang-alt

Copy link
Copy Markdown
Contributor

Hi @SugarFatFree, thank you again for the thoughtful proposal and the amount of care you put into this WebUI packaging lane. We really appreciate the detailed implementation, the follow-up fixes, and the patience throughout the review process.

After an internal assessment, we are going to close this PR for now. The packaging and distribution area touches our release pipeline, runtime layout, platform-specific assembly, and longer-term maintenance boundaries, and at this stage we prefer to keep this part of the system maintained by the core team. We are also actively working on packaging-related improvements internally, so we want to avoid creating parallel ownership or a public contribution surface that we may not be able to support well yet.

This is not a reflection on the quality of your contribution. The PR surfaced useful requirements and edge cases for headless/server-style WebUI usage, and we will keep that context in mind as we continue improving the packaging story internally.

Thanks again for the contribution and for all the iteration here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-validation Runtime change detected; needs human or /explore agent validation. risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps size/XXL PR changes 1500+ lines type/feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cross-platform "WebUI" packaging type (terminal-launched, no-Electron)

6 participants