Commit Graph
8 Commits
Author SHA1 Message Date
Daniel Kim 76c671c3b2 OR-215 Use GitHub CLI for publishing (#254)
* OR-215 use GitHub CLI for publishing

* Isolate dev slot caches

* Color GitHub CLI connection states
2026-08-26 17:41:55 -07:00
Myles AndersonandClaude Opus 5 f27c858c78 Simplify the DMG installer window to a plain drag-to-install (#228)
Drop the brand mark from the DMG background so the window is just the app
icon, an arrow, and the /Applications alias. With the mark gone the window
shrinks 640x400 -> 640x320 and the icon row recenters (86px above the icons,
86px below the labels).

The arrow now sits level with the icon centers (ICON_Y) and is centered on
the icons' artwork gap rather than their 128px cells, since the two icons
inset their art by different amounts.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:47:58 -07:00
Myles AndersonandClaude Opus 5 45eb162e19 Focus the existing dashboard tab on a Dock click (#212)
The macOS app's Dock-icon handler called `open <url>` on every click. Once the
dashboard SPA has navigated off `/` the browser has no exact-URL match, so the
click opened a new tab instead of returning to the dashboard already open.

The handler now tries, best first: the exact tab via a JXA script over the
scriptable browsers; the browser window itself, when a tab still holds its
`/api/events` stream (the only way to infer an open dashboard in Firefox, which
exposes no tab API); and finally the plain open it did before. A repeat click
within five seconds takes the plain open, so a raise that did not surface the
dashboard is always recoverable — the app's port is ephemeral, and the user has
no other route back to it.

JXA rather than AppleScript because it resolves each browser's terminology at
run time: an AppleScript `using terms from` block fails to compile on a Mac
without Chrome installed.

Signing now passes macos/entitlements.plist, since the hardened runtime denies
the Apple event outright without it — a failure that appears only in signed
builds, never in a local one.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:28:06 -07:00
Daniel Kim e9d322e547 Add lightweight project briefs and refresh onboarding (#200)
* feat: add lightweight project briefs and onboarding refresh

* fix: refine onboarding and project workspace interactions

* fix: harden demo onboarding state

* fix: satisfy current clippy lint

* chore: bump version to 0.1.105
2026-08-17 12:46:35 -07:00
Myles AndersonandClaude Opus 5 46d359ff5e OR-173 Make the macOS app find harnesses, and stop it fighting a CLI install (#195)
* fix(app): detect harnesses in the macOS app by adopting the shell PATH

Finder launches OpenResearch.app through launchd, so the bundle starts with
PATH=/usr/bin:/bin:/usr/sbin:/sbin and no shell rc ever sourced. Harness
detection resolves `claude`/`codex`/`opencode` off that PATH, so a DMG user
with a Homebrew, nvm, or npm-global install saw every harness missing --
including an already-signed-in Claude Code session -- while a terminal
`orx up` on the same machine detected them all.

App mode now probes the user's shell once at startup and installs the result
via `local::search_path`, which harness lookup and harness children consult
instead of the process PATH. The probe is `$SHELL -ilc`: interactive, because
zsh reads .zshrc only for interactive shells and that is where PATH edits
live -- a login-only probe missed the ~/.local/bin holding `claude`. It runs
an inner `sh -c` so fish reports a colon-separated PATH rather than its own
list-valued $PATH, fences the value between per-probe nonce markers so rc-file
chatter cannot forge one, and requires an absolute directory in the result.
Best-effort throughout: a 5s cap, and every outcome logged.

The five detection probes that spawned a harness binary with no env prep
(`bin_version`, `claude_accepts_ultracode`, `claude_list_models`,
`codex_model_list`, `run_models`) now go through `prepare_env` like
`probe_auth` already did. Without that, a node-shebang install resolves via
the hydrated PATH but its `--version` fails for want of `node`, and
`gate_oauth_version` turns the missing version into `Unknown` -- telling a
signed-in user to sign in.

Verified end to end: a bundle run under launchd's PATH with an isolated
ORX_DATA_DIR reports Claude Code agentReady, and finds a `codex` reachable
only through the hydrated PATH -- codex has no fallback drop-location, so
that resolution can only have come from the override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(app): stop the bundle and a CLI install from conflicting

Three ways the DMG app and a curl-installed `orx` stepped on each other.

`orx delete` takes an exclusive lifecycle lock and refuses when another
OpenResearch process holds it -- "Close `orx up`, `orx serve`, and any active
runs before trying again." CLI `orx up` holds the matching read lock via
`dispatch`, but app mode returns from `main` before `dispatch` runs, so it held
nothing: `orx delete` from a CLI install would wipe the store out from under a
live app. App mode now takes the same read lock for the life of the process.

Agents shell out to `orx`, and `chat::prepare_env` prepends the running
executable's directory so they get THIS build. In the bundle that directory
held only `OpenResearch`, so `orx` fell through to whatever CLI the user had --
a different version, or nothing at all for a DMG-only user. The bundle now
ships `Contents/MacOS/orx` as a symlink to the executable. Verified the symlink
survives `codesign --options runtime` + `--verify --strict`.

That alias would otherwise open a second dashboard, since a bare `orx` reaches
app mode's no-arguments condition. `launched_as_app_bundle` now also requires
that the process was invoked under the bundle executable's own name, which
keeps the alias a plain CLI while leaving a direct run of the executable path
-- the documented way to watch the app's logs -- in GUI mode. macOS
`current_exe` reports the launch path symlink and all, so it is canonicalized
before the comparison.

Verified safe already, and left alone: the app binds an ephemeral port rather
than 4791; the store is WAL with a 5s busy timeout; and run supervisors hold a
per-run exclusive lock, so a second server recovering the same active run exits
instead of double-driving it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(app): address review of the PATH and coexistence work

Blocker: `launched_as_app_bundle` had grown real logic -- four distinct
outcomes turning on a subtle platform fact -- with no coverage, and being
macOS-gated it could not get any, since CI runs ubuntu only. The decision is
now the pure `is_bundle_exe_launch(exe, argv0)`, compiled everywhere and
table-tested for all four cases; only the process-state read stays gated. Same
seam `extract_path` uses.

The lifecycle lock moves ahead of the PATH probe. Taking it afterwards left the
store unprotected for however long the probe ran -- up to 5s -- which is exactly
when an `orx delete` racing app startup would land. A failed `read()` is now
logged too; it was the one outcome in this module that could pass silently, and
silently is how the app would then run unlocked.

`orx delete` blocks on a running app now, so its message says so instead of
naming three things the user isn't running.

Probe diagnostics split the timeout from the spawn failure and name the shell,
since "which shell, and did it hang or fail to start" is the whole question a
report needs to answer.

`bin_version` gets the `NO_COLOR` guard its siblings already carry: it feeds
`parse_version`, an escape-laden version line parses as `None`, and
`gate_oauth_version` turns that into `Unknown` -- reproducing the bug this
branch exists to fix, via the synced env the probe now inherits.

`find_opencode` skips empty PATH components, matching `find_on_path`.

Packaging asserts the `orx` symlink survived staging. Every step preserves it
today, but a dereferencing copy would fail silently -- the app builds, signs,
notarizes and runs, and agents just quietly fall back to the user's own CLI.

Docs: corrected the same overstatement in `search_path`'s module doc that
DISTRIBUTION.md already fixed, and recorded the known gap that the probe
imports only PATH, so `ORX_DATA_DIR`/`XDG_*` from a shell rc reach the CLI but
not a Finder-launched app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(app): import the shell's data and config dirs, not just PATH

`ORX_DATA_DIR`, `XDG_DATA_HOME`, and `XDG_CONFIG_HOME` exported from a shell rc
reached the CLI but never a Finder-launched app, so the two resolved different
directories: the dashboard opened the default database while `orx` on the same
machine used the user's, and the lifecycle lock added last commit guarded a file
neither of them shared. Same root cause as the PATH bug, one layer down.

`search_path` generalizes to `shell_env`: an allowlist (`IMPORTED`) the startup
probe reads, NUL-separated so a directory can contain anything but NUL, behind
`var()` -- process environment when no override is installed, so nothing changes
for a terminal `orx`. `store::env_path` and `config::config_dir` are the only
two places that resolve these, and both now go through it. The allowlist stays
short on purpose: these are the variables whose divergence makes the app and the
CLI act like separate installs, while credentials already reach harness children
via `prepare_env`.

This moves the lifecycle lock back after the probe, undoing last commit's
reordering. The reason is now structural rather than incidental: the lock path
is derived from `config_dir()`, so taking it before the probe would lock the
default path while the CLI locks the user's -- protecting nothing. A correct
lock a few hundred milliseconds late beats an immediate lock on the wrong file.

Verified with the app's process environment stripped of both variables while the
probe shell exported them: `orx.db` lands in the shell's data dir and
`orx.lifecycle.lock` in the shell's config dir, and the HOME-derived defaults
are never created.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(app): carry the imported environment into orx child processes

Importing the shell's directories fixed the app's own resolution but not its
children's, which made things worse rather than better for anyone exporting
`ORX_DATA_DIR`. `spawn_detached_supervise` and the harness adapters spawn plain
CLI processes; those never probe, so they re-resolved to the default store while
the app read the user's. A run launched from the app would have been supervised
against a different database than the dashboard was showing — before this
branch both landed on the default and at least agreed.

`shell_env::export_to` hands the imported variables to a child. `prepare_env`
applies it, so an agent's `orx exp run` shares the dashboard's store, and
`spawn_detached_supervise` applies it plus PATH, which also reaches the run
payload it launches (`localbox` spawns `bash run.sh` with an inherited env, so
it was losing the user's PATH entirely — no `python`, no `uv`, no `conda`).

`opencode::spawn_agent` had hand-rolled a copy of `prepare_env`; it now calls
it rather than growing a third copy of the same fix.

Verified with the app's process environment stripped of both variables while the
probe shell exported them: a harness child is handed ORX_DATA_DIR and
XDG_CONFIG_HOME, and an `orx` run with that environment creates no default
store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): print a runnable command in the hints the bundle breaks

A DMG user has no `orx` anywhere on PATH, so every hint telling them to run
`orx exp wait …` names a command their shell cannot find. Agents are already
covered -- `prepare_env` puts the bundle's `orx` first on their PATH -- but a
human driving the bundle's CLI is not.

`invocation::orx()` resolves the spelling once: plain `orx` when it is on PATH,
which is every CLI install and every agent, otherwise this binary's own path,
shell-quoted when it needs it. Nothing changes for anyone who installed the CLI.

Applied to the three hints the report named, each of which had been copied per
backend: the post-launch "Follow it with …" line (nine copies), the missing
run-command error (eight), and the outdated-version warning. Both hint bodies
move into `invocation`, so the eight backends now share one line each -- a net
deletion, and one place to fix the next time the wording moves.

`warning_for` takes the spelling as an argument rather than reading process
state, keeping its tests deterministic.

Not covered: the long tail of `orx login`-style hints in error strings, which
still print a literal `orx`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(app): close the environment gaps review found

A dashboard-synced `ORX_DATA_DIR`/`XDG_*` silently beat the shell-imported one
for harness children: `prepare_env` exported the imported vars, then the synced
loop overwrote any whose key was absent from the *process* env -- which in app
mode is exactly these. The app read one store while claude and opencode wrote
another. The guard now consults `shell_env::var`, so the imported value counts
as present. codex escaped this only because it re-pins `ORX_DATA_DIR` after
`prepare_env`; claude and opencode have no such pin.

Local run payloads were never actually fixed. The previous commit claimed the
supervisor passed its environment to `bash run.sh`, but the supervisor does not
spawn it -- `localbox::run_job` does, in the launching process, from
`LocalJobSpec::env`. A run started from the app still executed the user's script
under launchd's PATH, with no python, uv, or conda. The env now carries the
imported PATH and directories, and the supervisor comment drops the wrong claim.

Three more readers of these same variables were still going to the process env,
and two of them made harness detection itself diverge between app and terminal:
`harness::xdg_config_home` (whose doc claims to mirror `config::config_dir`) and
`opencode_auth_path`, which would report opencode signed out in the app while
the CLI saw it signed in. `invocation::resolves_on_path` was the third -- it
decides whether a hint can say plain `orx`, and the reader pastes that into the
shell whose PATH we imported.

Also: `orx login` -- the first hint a fresh DMG user meets -- now resolves like
the rest; `export_to` iterates in `IMPORTED` order so the startup log is
diffable; and the doc drift is repaired, including the octal-escape invariant
that makes the nonce's leading underscore load-bearing, the reopened
`orx delete` window, and the scope note listing what still inherits the process
environment (`git`, `gh`, `kubectl`, `ssh`, `publish-branch`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(app): allow dead_code for the launch check off macOS

`is_bundle_exe_launch` is deliberately un-gated so its tests run on CI's Linux
runner, but its only non-test caller is `#[cfg(target_os = "macos")]`, so the
Linux bin target sees it as dead. `mod local` carries a blanket
`#[allow(dead_code)]`, which is why `shell_env::parse_probe` -- same shape --
did not trip; `mod commands` does not.

Verified by rebuilding with every `target_os = "macos"` in `src/` swapped to
`"linux"`, which reproduces exactly what the runner cfgs out. Clean, and no
other item on this branch is dead off macOS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:27:10 -07:00
Myles AndersonandClaude Opus 4.8 8c17722d88 feat(macos): styled drag-to-Applications DMG installer window (#188)
Opening OpenResearch.dmg now shows OpenResearch.app and an /Applications
alias to drag it onto, over a branded background — instead of a bare app
in the DMG root.

- scripts/generate-dmg-background.mjs: pure-Node PNG background (no image
  deps, mirroring generate-icon.mjs) with the brand mark + a drag arrow,
  rendered at 1x and 2x and combined into a HiDPI background.tiff.
- scripts/package-macos-app.sh: stage app + Applications symlink +
  background, lay out the Finder window (size, icon positions, background)
  via AppleScript on a read-write image, then flatten to the compressed
  DMG. Styling is best-effort: if the Finder step fails (e.g. headless),
  a functional but unstyled DMG still ships, and the temp/mount cleanup
  runs regardless.
- release-macos-app.yml: bound the 10x-billed macOS job with a timeout.
- DISTRIBUTION.md: document the installer window and its constants.

Verified locally: styled DMG builds with the app + Applications alias +
branded background, 640x400 window; happy-path cleanup leaves no leaked
temp dirs or mounts.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 19:14:15 -07:00
Myles AndersonandClaude Opus 4.8 dccc37c4e8 feat: downloadable macOS app (OpenResearch.app) + signed release hosting (#180)
* feat: scaffold downloadable macOS app (OpenResearch.app)

Adds a native macOS `.app` bundle whose executable IS the `orx` binary.
Launched from the bundle (double-click, no args) it enters GUI "app mode":
an AppKit NSApplication + delegate run loop on the main thread — Dock icon
and "OpenResearch" name from the bundle's Info.plist + .icns, and a
Dock-icon click that reopens the dashboard in the browser — while the
`orx up` server runs on background tokio worker threads.

App mode is entered only when the executable lives in `.app/Contents/MacOS`
AND argv is empty, so the bundled binary is still usable as a CLI.

- src/commands/app.rs: bundle detection + AppKit delegate + background server
- macos/Info.plist: bundle metadata (name, icon, identifier, version)
- scripts/generate-icon.mjs: transparent-PNG rasterizer (from favicon.svg)
- scripts/build-macos-app.sh: builds release orx, generates .icns, assembles
  OpenResearch.app into dist/
- objc2/objc2-app-kit gated under cfg(target_os = "macos") so musl Linux is
  untouched

The bundle is unsigned; code-signing + notarization is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: sign, notarize, and host OpenResearch.app via GitHub Releases

Automates distribution of the macOS app as a signed, notarized DMG attached
to each GitHub Release:

- scripts/build-macos-app.sh: ORX_APP_UNIVERSAL=1 builds a universal
  (arm64 + x86_64) binary via lipo for distribution.
- scripts/package-macos-app.sh: codesigns (hardened runtime, inside-out),
  notarizes + staples the .app (so it launches offline once dragged out of
  the DMG), packages a DMG, then notarizes + staples the DMG. Env-gated:
  runs unsigned locally, fully signed in CI.
- .github/workflows/release-macos-app.yml: on the "Release" workflow
  completing (workflow_run — a GITHUB_TOKEN-created release: published event
  can't trigger workflows), a cheap ubuntu job gates on a real dispatched
  release + all signing secrets + the release existing, then a macOS job
  builds/signs/notarizes and uploads OpenResearch.dmg to that release.
- macos/DISTRIBUTION.md: the one-time Apple setup + the six repo secrets.

Inert until the signing secrets are configured, so it is safe to merge
before the Apple Developer account exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: gate macOS signing behind CODEOWNERS + a required-reviewer environment

Hardens the release-signing pipeline for a public repo:

- .github/CODEOWNERS: marks the release workflows and macOS signing scripts
  as owned so they can't change unreviewed (with branch protection's
  "Require review from Code Owners").
- release-macos-app.yml: the cert-using job now runs in the `release-signing`
  environment, so adding required reviewers to it pauses signing for human
  approval — the Developer ID cert is never used by an unreviewed change.
- DISTRIBUTION.md: documents creating the environment + branch protection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: add @sox8502 to CODEOWNERS

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* security: environment-scoped signing secrets + SHA-pinned actions

Hardens the release-signing pipeline:

- The 6 signing secrets move from repo secrets to the `release-signing`
  environment, so only the reviewed, environment-gated macos-app job can read
  them — the cert secrets never exist in the cheap ubuntu gate.
- The gate now keys on a non-secret repo variable MACOS_SIGNING_ENABLED
  instead of reading the secrets to detect configuration.
- actions/checkout and actions/setup-node are pinned to commit SHAs so a moved
  tag can't inject code into the job that holds the Developer ID cert.
- DISTRIBUTION.md updated: create the environment (required reviewers +
  main-only deployment branches), add environment secrets, set the variable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: trim DISTRIBUTION.md to repo-specific config

Drop the generic Apple-portal walkthrough (enrolment, cert creation,
export click-by-click) — Apple documents that. Keep only what's specific
to this repo: the environment/secrets/variable, the local build+sign
commands, and the download URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 15:25:33 -07:00
Daniel Kim 417828221b Add isolated dev slot helper (#166) 2026-08-07 17:12:29 -07:00