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
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.4] - 2026-07-24

### Added

- Every workspace member can connect private OAuth or API-key providers,
explicitly share owned providers and routes with the workspace, select a
personal model, create isolated revocable endpoint keys, and use a dedicated
loopback endpoint port on the 1Helm host. Personal keys also scope the shared
`/v1` URL to that member's own-plus-shared provider pool and usage history.
- Residents and Skipper can search current web/news sources, inspect selected
pages, and attach real sourced images with captions and article links.
- Native Mac releases now include a post-notarization updater ZIP, and the
standard Linux installer provisions a root-owned atomic systemd updater with
digest verification, health checks, and rollback.

### Fixed

- Update controls now operate on the machine hosting the 1Helm instance. The
browser never receives a DMG or Linux artifact as the update action; native
macOS downloads and verifies in place, while Linux accepts only a fixed
host-side update request.
- Recent-event questions must research first and answer once with dated source
links. Ordinary uncertainty no longer opens an immediate interview, and a
real-photo request cannot be satisfied with generated artwork.
- Provider, route, OAuth, key, model, usage, and endpoint operations enforce
signed-in member ownership server-side. Teammates—including the Captain—cannot
mutate another member's shared credential, forge a private provider into a
route, or observe another member's OAuth session.
- Terminal panes send heartbeats and silently reconnect after brief
backgrounding or transport loss while retaining the same server session,
shell state, working directory, and scrollback. Disconnect text is no longer
written into the terminal.

## [0.0.3] - 2026-07-24

### Fixed
Expand Down Expand Up @@ -71,7 +104,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
notarization, stapled tickets, Gatekeeper verification, persistent
Application Support, and isolated Apple container machines.

[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.3...HEAD
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.4...HEAD
[0.0.4]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.4
[0.0.3]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.3
[0.0.2]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.2
[0.0.1]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.1
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,18 @@ Connect multiple ChatGPT, Claude, Gemini/Antigravity, and xAI OAuth accounts;
OpenRouter, NVIDIA NIM, Cloudflare, GLM, and custom API keys; then enable exact
models and assemble fallback or round-robin routes.

Each signed-in workspace member connects their own OAuth accounts and API keys.
New accounts and routes are private to that member unless their owner explicitly
shares them with the workspace; shared accounts are usable but remain editable
only by their owner. Each member may inherit the workspace model or choose a
personal model from their own-plus-shared pool.

Changing a route never replaces the resident or discards its computer, memory,
skills, files, obligations, or thread history. The same fabric also exposes an
authenticated OpenAI- and Anthropic-compatible `/v1` endpoint for external
tools.
tools. Every member receives separate revocable keys whose identity selects that
same personal pool, plus a dedicated loopback port on the 1Helm host. The port
does not run on the laptop or phone viewing the web UI.

## What ships now

Expand All @@ -155,6 +163,9 @@ tools.
SHA-256 chain for new operational events.
- Local-first collaboration through an optional workspace domain routed to the
Captain's Mac; workspace state and provider credentials remain on that Mac.
- Host-owned updates: a signed native Mac updater plus an atomic, digest-verified Linux system service with health-check rollback.
- Automatic terminal heartbeat and silent same-session reconnection after
backgrounding, focus changes, or brief network interruptions.
- Signed, Apple-notarized, stapled Apple Silicon DMG releases.

### Platform truth
Expand Down Expand Up @@ -184,8 +195,17 @@ Application state lives under:
~/Library/Application Support/1Helm
```

Replacing the app with a newer signed DMG preserves that directory, including
credentials, databases, resident state, files, and workspaces.
After the first install, Profile → Check for updates asks the Mac running
1Helm—not the device displaying the web UI—to download and verify the signed
update. **Restart & install** replaces the app while preserving that directory,
including credentials, databases, resident state, files, and workspaces.

The standard Linux installer similarly provisions a root-owned systemd updater.
The unprivileged web service may write only a fixed request file; systemd then
downloads the exact release artifact on the Linux host, verifies GitHub's
SHA-256 asset digest, stages a versioned release, switches atomically, restarts,
health-checks, and restores the previous release on failure. Arbitrary source
checkouts remain operator-managed and never send a Mac installer to the browser.

## Run the source workspace

Expand All @@ -208,7 +228,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to
| `PORT` | `8123` | HTTP/WebSocket control-plane port. |
| `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and narrow workspace mirrors. |
| `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `native` elsewhere | Explicit development/test backend override. |
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.3` | Versioned Apple channel-machine image. |
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.4` | Versioned Apple channel-machine image. |

### Agent-first JSON CLI

Expand Down
14 changes: 13 additions & 1 deletion desktop/main.cjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
"use strict";

const { app, BrowserWindow, dialog, shell, session } = require("electron");
const { app, autoUpdater, BrowserWindow, dialog, shell, session } = require("electron");
const { createServer } = require("node:net");
const { pathToFileURL } = require("node:url");
const path = require("node:path");
const crypto = require("node:crypto");
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const { createNativeUpdateService } = require("./updater.cjs");

const LOOPBACK = "127.0.0.1";
let mainWindow = null;
let authWindow = null;
let localOrigin = "";
let quitting = false;
let hostUpdateService = null;
const remoteWorkspacePath = () => path.join(app.getPath("userData"), "remote-workspace");

function preferredWorkspaceOrigin() {
Expand Down Expand Up @@ -209,7 +211,16 @@ if (!app.requestSingleInstanceLock()) {
try {
removeLegacyWakeLaunchAgent();
keepSkipperAvailable();
hostUpdateService = createNativeUpdateService({ app, autoUpdater });
hostUpdateService.initialize();
globalThis[Symbol.for("1helm.nativeUpdater")] = {
state: hostUpdateService.state,
check: hostUpdateService.check,
install: hostUpdateService.install,
};
process.on("1helm-native-update-ready", () => { hostUpdateService?.commitInstall(); });
await startLocalRuntime();
hostUpdateService.schedule();
const login = app.getLoginItemSettings({ type: "mainAppService" });
createWindow(!login.wasOpenedAtLogin && !process.argv.includes("--1helm-background"));
} catch (error) {
Expand All @@ -229,6 +240,7 @@ if (!app.requestSingleInstanceLock()) {
if (process.platform !== "darwin") app.quit();
});
app.on("before-quit", () => {
hostUpdateService?.stop();
if (quitting) return;
quitting = true;
// Explicit Quit is respected; the signed main-app login service starts the
Expand Down
152 changes: 152 additions & 0 deletions desktop/updater.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"use strict";

const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;

function publicError(error) {
const message = String(error?.message || error || "Update failed")
.replace(/https?:\/\/\S+/g, "the update service")
.replace(/\s+/g, " ")
.trim();
return message.slice(0, 220) || "Update failed";
}

function releaseVersion(name) {
const match = String(name || "").match(/v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
return match ? match[1] : null;
}

function createNativeUpdateService({ app, autoUpdater, platform = process.platform, arch = process.arch } = {}) {
let initialized = false;
let active = false;
let busy = false;
let initialTimer = null;
let intervalTimer = null;
let state = {
mode: "native-macos",
status: "idle",
current_version: app.getVersion(),
version: null,
checked_at: null,
error: null,
message: "Check for a signed 1Helm update on this Mac.",
};

let inApplications = true;
if (platform === "darwin" && typeof app.isInApplicationsFolder === "function") {
try { inApplications = app.isInApplicationsFolder(); } catch { inApplications = false; }
}
const supported = platform === "darwin" && arch === "arm64" && app.isPackaged === true && inApplications;
const feedUrl = `https://update.electronjs.org/gitcommit90/1Helm/darwin-arm64/${encodeURIComponent(app.getVersion())}`;

const snapshot = () => ({ ...state });
const setState = (patch) => { state = { ...state, ...patch }; };

function initialize() {
if (initialized) return active;
initialized = true;
if (!supported) {
setState({
status: "unsupported",
error: app.isPackaged && platform === "darwin" && arch === "arm64" && !inApplications
? "Move 1Helm to Applications to enable host updates."
: null,
message: app.isPackaged
? "Signed automatic updates are available for Apple Silicon macOS hosts."
: "Development builds are updated from their source checkout.",
});
return false;
}
autoUpdater.on("checking-for-update", () => {
busy = true;
setState({ status: "checking", error: null, message: "The 1Helm host is checking for a signed update…" });
});
autoUpdater.on("update-available", () => {
busy = true;
setState({ status: "downloading", error: null, message: "The 1Helm host is downloading and verifying the update…" });
});
autoUpdater.on("update-not-available", () => {
busy = false;
setState({ status: "current", checked_at: Date.now(), error: null, message: "This 1Helm host is up to date." });
});
autoUpdater.on("update-downloaded", (_event, notes, name) => {
busy = false;
const version = releaseVersion(name) || releaseVersion(notes);
setState({
status: "ready",
version,
checked_at: Date.now(),
error: null,
message: `1Helm${version ? ` v${version}` : ""} is verified and ready. Restart the host app to install it.`,
});
});
autoUpdater.on("error", (error) => {
busy = false;
const message = publicError(error);
console.error(`1Helm host update failed: ${message}`);
setState({ status: "error", checked_at: Date.now(), error: message, message });
});
try {
autoUpdater.setFeedURL({ url: feedUrl });
active = true;
return true;
} catch (error) {
const message = publicError(error);
setState({ status: "error", error: message, message });
return false;
}
}

function check() {
if (!initialize()) return snapshot();
if (busy || state.status === "ready") return snapshot();
busy = true;
setState({ status: "checking", error: null, message: "The 1Helm host is checking for a signed update…" });
try {
const pending = autoUpdater.checkForUpdates();
pending?.catch?.((error) => {
busy = false;
const message = publicError(error);
setState({ status: "error", checked_at: Date.now(), error: message, message });
});
} catch (error) {
busy = false;
const message = publicError(error);
setState({ status: "error", checked_at: Date.now(), error: message, message });
}
return snapshot();
}

function install() {
if (state.status !== "ready") {
return { ...snapshot(), error: "No downloaded host update is ready." };
}
setState({ status: "installing", error: null, message: "1Helm is restarting this Mac host to install the verified update…" });
process.env.HELM_UPDATE_INSTALLING = "1";
return snapshot();
}

function commitInstall() {
if (state.status !== "installing") return false;
autoUpdater.quitAndInstall(false, true);
return true;
}

function schedule({ initialDelayMs = 20_000, intervalMs = CHECK_INTERVAL_MS } = {}) {
if (!initialize()) return;
initialTimer ||= setTimeout(() => check(), initialDelayMs);
initialTimer.unref?.();
intervalTimer ||= setInterval(() => check(), intervalMs);
intervalTimer.unref?.();
}

function stop() {
if (initialTimer) clearTimeout(initialTimer);
if (intervalTimer) clearInterval(intervalTimer);
initialTimer = null;
intervalTimer = null;
}

return { initialize, check, install, commitInstall, schedule, stop, state: snapshot, feedUrl };
}

module.exports = { createNativeUpdateService, publicError, releaseVersion };
56 changes: 49 additions & 7 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ Files created through either path appear in Files after reconciliation.
![The channel Files surface with explicit Open and Download](assets/guide/files.png)

The terminal prompt displays the live current path and changes after `cd`.
1Helm sends a terminal heartbeat while the pane is open and automatically
reconnects a dropped browser transport to the same live shell. Briefly
backgrounding the app or changing networks does not print disconnect noise or
discard the shell's working directory, exported variables, running process, or
scrollback. If the host confirms that the underlying terminal session itself no
longer exists, 1Helm opens a fresh one.
Ordinary residents cannot select or enter the Captain's native Mac. On supported
Apple Silicon Macs, each resident runs inside its own Apple `container machine`
with `home-mount=none`; source/CI compatibility backends do not claim equivalent
Expand Down Expand Up @@ -150,6 +156,14 @@ Cloudflare, GLM, or custom OpenAI-compatible endpoints. Enable accounts and
models independently, then use a direct model or named fallback/round-robin
route.

Providers are member-owned. Any signed-in member may connect their own OAuth
accounts or API keys. A new provider or route starts private: only its owner can
see and use it. The owner may explicitly choose **Share with workspace**, which
makes it available to teammates without letting them reconnect, disable,
reconfigure, or delete the owner's credential. A shared route can reference
only providers that are also shared. Each member can choose **My model** from
their own-plus-shared pool or return to the Captain's workspace default.

![The live Requests in flight → 1Helm → Providers visualization](assets/guide/providers.png)

The visualization reflects live requests and their selected destination.
Expand All @@ -158,9 +172,24 @@ account does not prevent a healthy account in the same family from serving its
shared model. Quotas and Logs provide account-aware evidence, and disabled
accounts must not appear in a request's attempts or log record.

The Endpoint section manages separate external gateway keys. 1Helm agents use a
private internal credential that is not exposed, disabled, or revoked with
external keys.
The Endpoint section manages separate external gateway keys for the signed-in
member. A personal key sent to the workspace `/v1` URL is also the routing
identity: requests see only that member's providers plus accounts explicitly
shared with the workspace. Each member additionally receives a distinct
loopback port on the 1Helm host for host-local tools. That port is a convenience,
not an unauthenticated trust boundary; clients still use a personal revocable
key. 1Helm agents use a separate private internal credential that is not
exposed, disabled, or revoked with external keys.

## Current events and real images

Recent-event questions trigger public web or news research immediately. The
agent should search before asking for ordinary identifying details, inspect the
useful source, and answer once with publication dates and clickable source
links. When the user asks to see a real event, the agent attaches an image
returned by that research with its article source and caption. Generated art is
reserved for requests to create or illustrate something and cannot silently
stand in for a news photograph.

## Skills and Learn a new skill

Expand Down Expand Up @@ -248,15 +277,28 @@ messages, and WebSocket fan-out.

## Updates, removal, and recovery

Signed desktop releases are unique patch versions. Installing a newer 1Helm app
preserves:
Signed desktop releases are unique patch versions. Profile → Check for updates
always operates on the machine hosting the active 1Helm instance. In the native
Mac app, Electron downloads and verifies a notarized update ZIP on that Mac and
offers **Restart & install** only when it is ready. It does not navigate the
browsing device to a DMG.

The standard Linux systemd install uses a root-owned updater. 1Helm can request
that one fixed operation, but cannot choose an arbitrary URL, command, or target
path. The host updater requires a stable GitHub release and its SHA-256 asset
digest, installs into a versioned directory, switches the current symlink
atomically, restarts, health-checks, and restores the prior release if needed.
Source/developer deployments report that their host operator owns updates.

Every host update preserves:

```text
~/Library/Application Support/1Helm
```

That directory contains databases, credentials, workspaces, resident state,
and narrow mirrors. Do not delete it during an app replacement.
On macOS that directory contains databases, credentials, workspaces, resident
state, and narrow mirrors. Linux preserves the equivalent host state under
`/var/lib/1helm`. Do not delete either data root during replacement.

Before removing 1Helm, use its removal preparation flow. It is Captain-only,
requires typed confirmation, reports backend-owned resident machines, and
Expand Down
Loading
Loading