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

## [Unreleased]

## [0.0.7] - 2026-07-24

### Fixed

- Linux fleet reconciliation no longer mistakes the intentional marker-less
interval during a fresh LXC bootstrap for an ownership violation. The
provisioning transaction retains strict post-bootstrap verification, while
ordinary and post-crash ownership checks remain fail-closed.
- macOS and Windows desktop packages now retain their required Mnemosyne and
Windows WSL/removal runtime scripts. Electron Packager may inspect the
parent `scripts` directory before its allowlisted children; the release
filters now preserve that traversal without admitting unrelated build
helpers or tests.

## [0.0.6] - 2026-07-24

### Added
Expand Down Expand Up @@ -187,7 +201,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.6...HEAD
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.7...HEAD
[0.0.7]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.7
[0.0.6]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.6
[0.0.5]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.5
[0.0.4]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.4
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,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, `lxc` on Linux, `wsl` on Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. |
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.6` | Versioned channel-machine image contract. |
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.7` | Versioned channel-machine image contract. |

### Agent-first JSON CLI

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "1helm",
"productName": "1Helm",
"version": "0.0.6",
"version": "0.0.7",
"private": true,
"type": "module",
"description": "1Helm is the self-hosted home for durable AI employees: one resident, one private computer, compounding memory and skills, and Skipper for every boundary.",
Expand Down
5 changes: 4 additions & 1 deletion scripts/package-mac-dmg.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ const REQUIRE_NOTARIZATION = process.env.HELM_REQUIRE_NOTARIZATION === "1";
const TEAM_ID = String(process.env.APPLE_TEAM_ID || "").trim().toUpperCase();
const NOTARY_PROFILE = String(process.env.APPLE_NOTARY_PROFILE || "").trim();
const CONFIGURED_IDENTITY = String(process.env.APPLE_SIGN_IDENTITY || "").trim();
// Electron Packager evaluates directories before their children. Keep the
// scripts directory itself traversable so the required Mnemosyne bridge can
// survive the otherwise root-level release filter.
const IGNORE_NON_RUNTIME_ROOTS =
/^\/(?!package\.json$|LICENSE$|desktop(?:$|\/)|container(?:$|\/)|src(?:$|\/)|public(?:$|\/)|scripts\/mnemosyne-bridge\.py$|node_modules(?:$|\/))/;
/^\/(?!package\.json$|LICENSE$|desktop(?:$|\/)|container(?:$|\/)|src(?:$|\/)|public(?:$|\/)|scripts(?:$|\/mnemosyne-bridge\.py$)|node_modules(?:$|\/))/;

if (!VERSION) throw new Error("package.json must define a version");

Expand Down
5 changes: 4 additions & 1 deletion scripts/package-windows.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"))
const VERSION = String(pkg.version || "").trim();
const REQUIRE_SIGNATURE = process.env.HELM_REQUIRE_WINDOWS_SIGNATURE === "1";
const CERT_SHA1 = String(process.env.WINDOWS_SIGN_CERT_SHA1 || "").replace(/\s+/g, "").toUpperCase();
const IGNORE_NON_RUNTIME_ROOTS = /^\/(?!package\.json$|LICENSE$|desktop(?:$|\/)|container(?:$|\/)|deploy(?:$|\/)|src(?:$|\/)|public(?:$|\/)|scripts\/(?:mnemosyne-bridge\.py|install-wsl-runtime\.ps1|windows-removal\.cjs)$|node_modules(?:$|\/))/;
// Electron Packager evaluates directories before their children. Keep the
// scripts directory itself traversable, then retain only the three runtime
// files below; otherwise the exact-file exceptions can never be reached.
const IGNORE_NON_RUNTIME_ROOTS = /^\/(?!package\.json$|LICENSE$|desktop(?:$|\/)|container(?:$|\/)|deploy(?:$|\/)|src(?:$|\/)|public(?:$|\/)|scripts(?:$|\/(?:mnemosyne-bridge\.py|install-wsl-runtime\.ps1|windows-removal\.cjs)$)|node_modules(?:$|\/))/;

if (process.platform !== "win32" || process.arch !== "x64") throw new Error("Windows packaging must run on Windows x64.");
if (!/^\d+\.\d+\.\d+$/.test(VERSION)) throw new Error("package.json must contain a release version.");
Expand Down
33 changes: 24 additions & 9 deletions src/server/channel-computers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const APPLE_RUNTIME_VERSION = "1.1.0";
export const APPLE_RUNTIME_PACKAGE = `container-${APPLE_RUNTIME_VERSION}-installer-signed.pkg`;
export const APPLE_RUNTIME_URL = `https://github.com/apple/container/releases/download/${APPLE_RUNTIME_VERSION}/${APPLE_RUNTIME_PACKAGE}`;
export const APPLE_RUNTIME_SHA256 = "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714";
export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.6";
export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.7";
const CONTAINER_CANDIDATES = [process.env.HELM_CONTAINER_CLI, "/usr/local/bin/container", "/opt/homebrew/bin/container", "container"].filter(Boolean) as string[];
const LXC_RUNTIME_VERSION = "1helm-lxc-runtime-v1";
const LXC_HELPER_CANDIDATES = [
Expand Down Expand Up @@ -106,6 +106,12 @@ const MAX_WORKSPACE_SYNC_ENTRIES = Math.max(10_000, Number(process.env.HELM_WORK
const SCROLLBACK_CAP = 256 * 1024;
const terminalSessions = new Map<string, MachineTerminal>();
const channelLocks = new Map<number, Promise<unknown>>();
// Provisioning is a long host operation (the first LXC boot installs its
// guest toolchain). The reconciler must not interpret the intentionally
// marker-less machine that exists during that transaction as an ownership
// violation. This is process-local on purpose: after a crash/restart there is
// no active transaction, so the normal inspection path can recover or retry.
const activeProvisioning = new Set<number>();
const syncTimers = new Map<number, NodeJS.Timeout>();
let reconcileTimer: NodeJS.Timeout | null = null;
let reconcileStartupTimer: NodeJS.Timeout | null = null;
Expand Down Expand Up @@ -534,14 +540,19 @@ async function ensureWslRootfs(): Promise<string> {
async function ensureLxcProvisioned(computer: ChannelComputer): Promise<void> {
let inspection = await inspectLxc(computer);
if (!inspection) {
run("UPDATE channel_computers SET provision_status='provisioning',last_error='',updated=? WHERE channel_id=?", now(), computer.channel_id);
markWorkspaceDirty(computer.channel_id, "*", "full");
const architecture = process.arch === "arm64" ? "arm64" : "amd64";
const created = await lxc(["create", computer.machine_id, ownerMarker(computer), String(computer.cpus), String(Math.round(computer.memory_bytes / 1024 ** 2)), architecture], { timeoutMs: 30 * 60_000 });
if (created.code !== 0) throw new Error(created.stderr.toString("utf8").trim() || created.stdout.toString("utf8").trim() || "LXC channel computer creation failed");
inspection = await inspectLxc(computer);
if (!inspection || inspection.homeMount !== "none") throw new Error("Provisioned LXC computer failed its ownership/isolation verification.");
recordComputerActivity(computer.channel_id, "Provisioned a persistent unprivileged LXC computer for this resident.", "complete");
activeProvisioning.add(computer.channel_id);
try {
run("UPDATE channel_computers SET provision_status='provisioning',last_error='',updated=? WHERE channel_id=?", now(), computer.channel_id);
markWorkspaceDirty(computer.channel_id, "*", "full");
const architecture = process.arch === "arm64" ? "arm64" : "amd64";
const created = await lxc(["create", computer.machine_id, ownerMarker(computer), String(computer.cpus), String(Math.round(computer.memory_bytes / 1024 ** 2)), architecture], { timeoutMs: 30 * 60_000 });
if (created.code !== 0) throw new Error(created.stderr.toString("utf8").trim() || created.stdout.toString("utf8").trim() || "LXC channel computer creation failed");
inspection = await inspectLxc(computer);
if (!inspection || inspection.homeMount !== "none") throw new Error("Provisioned LXC computer failed its ownership/isolation verification.");
recordComputerActivity(computer.channel_id, "Provisioned a persistent unprivileged LXC computer for this resident.", "complete");
} finally {
activeProvisioning.delete(computer.channel_id);
}
}
recordObserved(computer, inspection);
run("UPDATE channel_computers SET provision_status='ready',desired_state='auto',last_update=?,last_update_attempt=?,last_error='',updated=? WHERE channel_id=?", now(), now(), now(), computer.channel_id);
Expand Down Expand Up @@ -1316,6 +1327,10 @@ export async function resizeChannelComputer(channelId: number, targetCpus: numbe

async function reconcileOne(computer: ChannelComputer): Promise<void> {
if (computer.desired_state === "deleted") return;
// A newly created isolated world does not receive its owner marker until
// guest bootstrap completes. The provisioning transaction performs the
// authoritative post-bootstrap inspection itself.
if (activeProvisioning.has(computer.channel_id)) return;
const channel = q1("SELECT status FROM channels WHERE id=?", computer.channel_id);
if (!channel) return;
if (!isolatedBackend(computer)) { await ensureNativeProvisioned(computer); return; }
Expand Down
2 changes: 1 addition & 1 deletion src/server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ export function migrate(): void {
const platformBackend = process.platform === "darwin" ? "apple" : process.platform === "win32" ? "wsl" : "lxc";
const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend);
const backend = ["apple", "lxc", "wsl", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend;
const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.6");
const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.7");
// Earlier Linux/Windows releases persisted the compatibility `native`
// seam into every channel row. A production host update must actually
// move those rows onto the platform isolation backend; changing the unit's
Expand Down
17 changes: 16 additions & 1 deletion test/channel-computers-backend-child.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,22 @@ if (backend === "wsl") {
}

try {
const provisioned = await computers.provisionChannelComputer(channelId);
let provisioning;
if (backend === "lxc") {
process.env.FAKE_LXC_CREATE_DELAY_MS = "500";
const pending = computers.provisionChannelComputer(channelId);
const machineConfig = join(fakeState, "machines", record.machine_id, "config.json");
for (let attempt = 0; !existsSync(machineConfig) && attempt < 100; attempt++) {
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
}
assert(existsSync(machineConfig), "the fake LXC entered its marker-less provisioning window");
const concurrent = await computers.reconcileChannelComputers([channelId]);
assert.deepEqual(concurrent, { checked: 1, errors: 0 }, "fleet reconciliation leaves an active provisioning transaction alone");
assert.equal(db.q1("SELECT last_error FROM channel_computers WHERE channel_id=?", channelId).last_error, "");
provisioning = await pending;
delete process.env.FAKE_LXC_CREATE_DELAY_MS;
} else provisioning = await computers.provisionChannelComputer(channelId);
const provisioned = provisioning;
assert.equal(provisioned.backend, backend);
assert.equal(provisioned.home_mount, "none");
assert.equal(provisioned.observed_state, "running");
Expand Down
2 changes: 1 addition & 1 deletion test/channel-computers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ test("Apple channel-computer contract preserves isolation, files, wakes, archive
test("runtime digest and packaged image recipe stay pinned", async () => {
assert.equal(computers.APPLE_RUNTIME_SHA256, "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714");
assert.match(computers.APPLE_RUNTIME_URL, /\/1\.1\.0\/container-1\.1\.0-installer-signed\.pkg$/);
assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.6");
assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.7");
const packaging = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8");
assert.match(packaging, /container\(\?:\$\|\\\/\)/, "release packaging includes container/ image assets");
const image = await readFile(join(root, "container", "Containerfile"), "utf8");
Expand Down
12 changes: 12 additions & 0 deletions test/desktop.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn
assert.match(channelComputers, /! findmnt -rn \/mnt\/c[\s\S]*rmdir \/mnt\/c \/mnt\/d[\s\S]*test ! -e \/mnt\/c/, "WSL removes only inert drive mountpoint directories after proving they are not mounted");
assert.match(channelComputers, /test ! -e \/mnt\/c/, "WSL provisioning verifies the host C drive is not visible");
const windowsPackager = await readFile(join(root, "scripts", "package-windows.cjs"), "utf8");
const macPackager = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8");
const windowsRemoval = await readFile(join(root, "scripts", "windows-removal.cjs"), "utf8");
const windowsRuntime = await readFile(join(root, "scripts", "install-wsl-runtime.ps1"), "utf8");
assert.match(windowsPackager, /HELM_REQUIRE_WINDOWS_SIGNATURE/);
Expand All @@ -117,6 +118,17 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn
assert.match(windowsPackager, /path\.join\(DIST, "RELEASES"\)/, "the GitHub asset keeps Squirrel's required literal RELEASES name");
assert.match(windowsPackager, /path\.join\(DIST, path\.basename\(nupkg\)\)/, "the uploaded package keeps the exact basename referenced by RELEASES");
assert.match(windowsPackager, /signPackagedExecutables\(appDir\)/, "release signing covers nested Windows executables before packaging");
for (const [source, requiredPaths] of [
[windowsPackager, ["/scripts", "/scripts/mnemosyne-bridge.py", "/scripts/install-wsl-runtime.ps1", "/scripts/windows-removal.cjs"]],
[macPackager, ["/scripts", "/scripts/mnemosyne-bridge.py"]],
]) {
const literal = source.match(/const IGNORE_NON_RUNTIME_ROOTS\s*=\s*(\/\^[^;]+\/);/)?.[1];
assert.ok(literal, "desktop packager exposes a testable runtime-root filter");
const filter = Function(`"use strict"; return (${literal})`)();
for (const requiredPath of requiredPaths) assert.equal(filter.test(requiredPath), false, `${requiredPath} survives desktop packaging`);
assert.equal(filter.test("/scripts/release-only-helper.cjs"), true, "non-runtime release helpers stay outside the desktop app");
assert.equal(filter.test("/test"), true, "tests stay outside the desktop app");
}
assert.match(windowsRemoval, /installation_id/);
assert.match(windowsRemoval, /ctrl-pane\.db/, "Windows removal reads the real durable 1Helm database");
assert.doesNotMatch(windowsRemoval, /helm\.sqlite/);
Expand Down
2 changes: 2 additions & 0 deletions test/fake-lxc-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ if (operation === "create") {
const [, name, owner, cpus, memoryMb] = args;
const result = spawnSync(process.execPath, [fakeContainer, "machine", "create", "--name", name, "--cpus", cpus, "--memory", `${memoryMb}M`, "--home-mount", "none", "fake"], { env: process.env, encoding: null });
if (result.status !== 0) fail(String(result.stderr || "create failed"));
const createDelay = Math.max(0, Math.min(5_000, Number(process.env.FAKE_LXC_CREATE_DELAY_MS || 0)));
if (createDelay) await new Promise((resolveDelay) => setTimeout(resolveDelay, createDelay));
mkdirSync(join(machineDir(name), "var", "lib", "1helm"), { recursive: true });
writeFileSync(ownerPath(name), `${owner}\n`);
const config = JSON.parse(readFileSync(configPath(name), "utf8"));
Expand Down
Loading