Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions macos/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ let package = Package(
],
products: [
.library(name: "ForgeMenuCore", targets: ["ForgeMenuCore"]),
.executable(name: "ForgeMenuBar", targets: ["ForgeMenuBar"]),
.executable(name: "ForgeRuntimeSmokeHelper", targets: ["ForgeRuntimeSmokeHelper"]),
.executable(name: "ForgeRuntimeLeaseTestHelper", targets: ["ForgeRuntimeLeaseTestHelper"])
.executable(name: "ForgeMenuBar", targets: ["ForgeMenuBar"])
],
dependencies: [],
targets: [
Expand All @@ -35,16 +33,6 @@ let package = Package(
.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../Frameworks"])
]
),
.executableTarget(
name: "ForgeRuntimeSmokeHelper",
dependencies: ["ForgeMenuCore"],
path: "Sources/ForgeRuntimeSmokeHelper"
),
.executableTarget(
name: "ForgeRuntimeLeaseTestHelper",
dependencies: ["ForgeMenuCore"],
path: "Sources/ForgeRuntimeLeaseTestHelper"
),
.testTarget(
name: "ForgeMenuCoreTests",
dependencies: ["ForgeMenuCore"],
Expand Down
17 changes: 9 additions & 8 deletions macos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,25 +87,27 @@ The query value always reflects the port the helper actually bound, so a fallbac

## Service architecture

When the app launches and first requires the service, ForgeCode installs the runtime under `~/Library/Application Support/ForgeCode/runtime` if it is not already present. Before a newly downloaded runtime is activated, the installer validates the immutable HTTPS release version used for tag and artifact URL selection, checksum sidecar, archive structure, Mach-O architecture, embedded signature structure, signature class, pinned executable identity, and quarantine policy. Installed runtime directories, executables, and receipts use modes `0700`, `0700`, and `0600`. On later lookup or launch, the receipt is metadata and a candidate locator only: stale version/hash fields are allowed, and neither receipt hashes nor executable checksum, Mach-O, signature, inode, size, or original version are revalidated. The managed executable is accepted only through a safe no-symlink path with a private current-owner directory and a current-owner, regular, single-link, non-group/world-writable, owner-executable file. Parent-directory-pinned relative spawning preserves path-race protection while allowing legitimate self-replacement between launches. It then launches the installed runtime as:
When the app launches and first requires the service, ForgeCode installs the runtime into the cargo home (`$CARGO_HOME`, defaulting to `~/.cargo`, so the binary lands at `~/.cargo/bin/forge3`) if it is not already present by downloading and running forge3's upstream cargo-dist shell installer from `https://install.forgecode.dev/server` via `/bin/sh`. This is the same directory the script installs to on its own and the same one forge3's self-update writes back to, so the app, the installer, and the runtime always agree on the binary's location. The app is a thin wrapper: it sets `FORGE3_INSTALL_DIR` to that directory, `FORGE3_NO_MODIFY_PATH=1` so no shell rc files are touched, and `FORGE3_PRINT_QUIET=1`. The managed install still writes forge3's receipt at `~/.config/forge3/forge3-receipt.json` so forge3's own axoupdater-based self-update keeps working. The installer script is fully trusted: the app performs no checksum, signature, or Mach-O verification. As the only Gatekeeper workaround, `com.apple.quarantine` is best-effort removed from the installed binary. Set `FORGE_UPDATE_INSTALLER_URL` to override the installer script source (e.g. a local `http://127.0.0.1:9877/install.sh`). The app locates the binary by the exact path `<dir>/bin/forge3` and launches it as:

```text
forge3 --log-format json ws --addr 127.0.0.1:<first-free-port-from-9753>
```

### Running a locally built runtime (debug builds only)

Set `FORGE_RUNTIME_BINARY` to an absolute path to launch your own `forge3` instead of the managed runtime. No download, staging, or store validation runs, and the store is left untouched:
Set `FORGE_RUNTIME_BINARY` to an absolute path to launch your own `forge3` instead of running the upstream installer. No download or install step runs:

```text
FORGE_RUNTIME_BINARY=/path/to/forge3 .build/debug/ForgeMenuBar
```

This is compiled out of release builds — `DeveloperRuntimeOverride.resolve` returns `nil` unconditionally when `DEBUG` is not defined, so a shipped app cannot be redirected to an arbitrary binary. An unset variable is ignored; a variable that is set but does not point at an executable regular file fails loudly rather than silently falling back to the downloaded runtime. The launch-boundary checks still apply to the override: the binary must be a regular, single-link, current-owner, non-group/world-writable, owner-executable file reached through a no-symlink private path. The override reports version `0.0.0` so it never registers as an upgrade.
This is compiled out of release builds — `DeveloperRuntimeOverride.resolve` returns `nil` unconditionally when `DEBUG` is not defined, so a shipped app cannot be redirected to an arbitrary binary. An unset variable is ignored; a variable that is set but does not point at an executable regular file fails loudly rather than silently falling back to running the installer. The override must be an absolute path to an executable regular file and reports version `0.0.0` so it never registers as an upgrade.

Debug environment variables are forwarded from the app's environment to the forge3 child process: `FORGE_UPDATE_CURRENT_VERSION`, `FORGE_UPDATE_MANIFEST_URL`, `FORGE_UPDATE_INSTALLER_URL`, `FORGE_CONSOLE_ORIGIN`, and `FORGE3_API_KEY`.

Because a GUI launch from Finder or Dock does not inherit your shell environment, this variable — like `FORGE_CONSOLE_ORIGIN` — is only visible when the executable is launched directly from a terminal.

The ForgeCode installer does not poll for newer runtimes at app startup or in the background. The app's own lifetime is the desired state: the service is started at launch and stopped on termination. Unexpected exits and readiness failures use bounded exponential restart backoff. Stop and app termination use bounded `TERM`, `KILL`, and child reaping, with best-effort process-group cleanup. The runtime environment is allowlisted instead of copied wholesale, and JSON-aware/text fallback redaction is applied before bounded rotating logs are stored in `~/Library/Logs/ForgeMenuBar/`.
The app does not poll for newer runtimes at startup or in the background and performs no explicit update checks. forge3 self-updates by re-running its installer in place and exiting; the app simply restarts the binary whenever it exits (an exit status of `75` on the first attempt restarts immediately, which is how a self-update is picked up). The app's own lifetime is the desired state: the service is started at launch and stopped on termination. Unexpected exits and readiness failures use bounded exponential restart backoff. forge3 is spawned in its own process group; stop and app termination use bounded `TERM`, `KILL`, and child reaping against that group so no orphan processes survive the app. The runtime environment is allowlisted instead of copied wholesale, and JSON-aware/text fallback redaction is applied before bounded rotating logs are stored in `~/Library/Logs/ForgeMenuBar/`.

The active app does not poll usage, extensions, models, or providers. Readiness and health are probed with a single one-shot `rpc.discover` request per lifecycle: a successful round trip marks the service ready, and `result.data.complete["rpc.discover"].info.version` supplies the server version shown in the header. Failures inside the readiness window retry on a short poll interval; persistent failure fails the lifecycle and triggers a supervised restart. Process and probe generations prevent late responses from superseded work from updating current state. Stopping the service clears all service-derived state.

Expand All @@ -125,11 +127,10 @@ Useful individual commands are `scripts/assemble-app.sh`, `scripts/sign-app.sh`,

## Current limitations and security

- The native installer accepts only HTTPS runtime URLs at the configured release origin and limits redirects to that same host, with no credentials, explicit port, or fragment. This same-origin rule narrows redirect and credential-leak risk but does not independently authenticate every object served by a compromised origin; immutable version/tag URL selection, archive checksums, Mach-O validation, signature structure, and configured Developer ID Team ID authentication remain required controls.
- An embedded ad-hoc code signature, when present on a runtime, proves only self-consistency and is not an Apple- or publisher-backed identity. The installer retains the inspected signature class, Team Identifier, and signing identity rather than treating display-name text as provenance; checksum trust and the release origin remain security boundaries. A signed runtime is classified as Developer ID only when Security framework requirement evaluation validates the Apple generic anchor, the Developer ID intermediate certificate OID, and the Developer ID Application leaf certificate OID. Authentication repeats that supported code-signing requirement with the leaf certificate organizational unit bound to the exact explicitly configured Team Identifier. A self-signed or subject-name lookalike certificate is never Developer ID. An unset expected Team ID, missing signature Team ID, mismatch, requirement-construction failure, or requirement-evaluation failure stops before quarantine policy or installation. The production default leaves the expected Team Identifier unset, preserving the current ad-hoc artifact path while failing closed if the distribution changes to Developer ID until its Apple-issued Team Identifier is deliberately configured.
- The public runtime smoke on July 29, 2026 resolves `forge3 0.1.191` and validates installation plus cold cache recovery without an installer `--version` execution. The runtime contains its own update notifier, which is separate from the native installer. Tests assert that current-cache reuse, versioned-cache reuse, and self-updated cold recovery each issue exactly zero `RuntimeNetworkClient` requests. The desktop app neither invokes forge3's update commands nor renders its widgets, but no supported public-release environment variable or launch argument currently disables the launched runtime's own checks. Fully satisfying zero SDK/server update checks therefore requires upstream `forge3` support or a specially built runtime; this repository does not modify the SDK.
- The runtime is installed by downloading and executing forge3's upstream cargo-dist shell installer. The app performs no checksum, signature, Mach-O, or launch-security verification of the installer or the resulting binary — the installer script and the machine are fully trusted. Trust rests entirely on the HTTPS origin serving the installer script (`https://install.forgecode.dev/server`, overridable via `FORGE_UPDATE_INSTALLER_URL` for testing).
- As the only Gatekeeper workaround, the app best-effort removes `com.apple.quarantine` from the installed `forge3` binary. The installer script itself is executed via `/bin/sh` (interpreted, not a Gatekeeper-assessed Mach-O launch).
- The app performs no explicit runtime update checks. forge3 self-updates by re-running its own installer and replacing its binary in place, then exiting; the app relaunches on exit. This is the entire update mechanism from the app's perspective and is fully separate from the app's own Sparkle-based self-updates.
- A release cannot be notarized or assessed as Developer ID software without an installed Developer ID Application identity and a valid `notarytool` profile.
- **Temporary explicit ad-hoc runtime trust policy:** the current public `forge3` artifact is ad-hoc signed rather than Developer ID signed/notarized. After the installer validates the immutable HTTPS version/tag selection, checksum sidecar, archive safety, native Mach-O architecture, embedded signature structure, signature class, and stable single-link user-owned executable identity, it inspects quarantine. If and only if the validated signature class is ad-hoc and `com.apple.quarantine` is present, the injected pre-execution trust policy authorizes a fresh same-directory vnode. Descriptor-relative exclusive no-follow operations copy bytes from the pinned validated source vnode, preserve safe executable permissions and unrelated extended attributes, omit only quarantine, synchronize the file and directory, and atomically replace the staged basename. Before installation, the installer captures a new device/inode/size/hash identity and repeats hash, Mach-O, architecture, embedded-signature, ad-hoc-class, identity, and quarantine-absence checks; any failure prevents activation. Developer ID, other signed, unsigned, or already-unquarantined artifacts are not modified. The installer does not remove quarantine in response to runtime launch behavior because it performs no staged runtime execution. This deliberately bypasses Gatekeeper's quarantine-based first-launch assessment only for the exact checksum- and identity-validated ad-hoc artifact; HTTPS origin and checksum integrity are therefore temporary trust boundaries until upstream publishes a properly Developer ID signed and notarized runtime, at which point this exception should be removed.
- The installed runtime owns the selected port after a short bind-probe race; readiness supervision handles a lost race by stopping and retrying on the next available incremental port.
- Process-group cleanup is best effort on macOS. Descendants that deliberately escape the runtime's process group require a privileged service architecture for reliable discovery.
- The popover is intentionally fixed in size; overflowing content scrolls, and long text truncates while remaining available as tooltips.
7 changes: 2 additions & 5 deletions macos/Sources/ForgeMenuBar/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

do {
let paths = try RuntimePaths.resolve()
let runtimeRoot = RuntimeStore.defaultRoot()
let runtimeLease = RuntimeStoreLease(rootURL: runtimeRoot)
let runtimeInstaller = DeveloperOverrideRuntimeInstaller.wrapIfOverridden(
base: RuntimeInstaller(rootURL: runtimeRoot),
base: RuntimeInstaller(),
logger: logger
)
let processHost = ForgeProcessHost(
configuration: .init(logURL: paths.serviceLog),
logger: logger,
lease: runtimeLease
logger: logger
)
let supervisor = ServiceSupervisor(
processHost: processHost,
Expand Down
11 changes: 3 additions & 8 deletions macos/Sources/ForgeMenuCore/DeveloperRuntimeOverride.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import Foundation

/// Debug-only escape hatch that points the supervisor at a locally built
/// `forge3` instead of the managed runtime store.
/// `forge3` instead of running the upstream shell installer.
///
/// This exists so a developer can iterate on the runtime without publishing a
/// release and without hand-editing the private store (which is checksum- and
/// permission-validated, and silently re-downloaded when validation fails).
/// release and without running the installer at all.
///
/// The entire mechanism is compiled out of release builds: `resolve` returns
/// `nil` unconditionally when `DEBUG` is not defined, so a shipped app cannot be
Expand All @@ -15,11 +14,7 @@ import Foundation
///
/// FORGE_RUNTIME_BINARY=/path/to/forge3 .build/debug/ForgeMenuBar
///
/// Note that the launch-time safety checks in `RuntimePinnedExecutable` still
/// apply to the override: the binary must be a regular file owned by the current
/// user, with no hard links, no group/other write permission, the owner execute
/// bit set, and no symlink or group/other-writable directory anywhere along the
/// path below the user boundary.
/// The override must be an absolute path to an executable regular file.
public enum DeveloperRuntimeOverride {
public static let environmentKey = "FORGE_RUNTIME_BINARY"

Expand Down
Loading
Loading