Skip to content

Commit 2c9cd7d

Browse files
committed
fix: stabilize isolated host packaging and provisioning
1 parent f520d28 commit 2c9cd7d

12 files changed

Lines changed: 84 additions & 19 deletions

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.0.7] - 2026-07-24
11+
12+
### Fixed
13+
14+
- Linux fleet reconciliation no longer mistakes the intentional marker-less
15+
interval during a fresh LXC bootstrap for an ownership violation. The
16+
provisioning transaction retains strict post-bootstrap verification, while
17+
ordinary and post-crash ownership checks remain fail-closed.
18+
- macOS and Windows desktop packages now retain their required Mnemosyne and
19+
Windows WSL/removal runtime scripts. Electron Packager may inspect the
20+
parent `scripts` directory before its allowlisted children; the release
21+
filters now preserve that traversal without admitting unrelated build
22+
helpers or tests.
23+
1024
## [0.0.6] - 2026-07-24
1125

1226
### Added
@@ -187,7 +201,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
187201
notarization, stapled tickets, Gatekeeper verification, persistent
188202
Application Support, and isolated Apple container machines.
189203

190-
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.6...HEAD
204+
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.7...HEAD
205+
[0.0.7]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.7
191206
[0.0.6]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.6
192207
[0.0.5]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.5
193208
[0.0.4]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.4

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to
236236
| `PORT` | `8123` | HTTP/WebSocket control-plane port. |
237237
| `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and narrow workspace mirrors. |
238238
| `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `lxc` on Linux, `wsl` on Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. |
239-
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.6` | Versioned channel-machine image contract. |
239+
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.7` | Versioned channel-machine image contract. |
240240

241241
### Agent-first JSON CLI
242242

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "1helm",
33
"productName": "1Helm",
4-
"version": "0.0.6",
4+
"version": "0.0.7",
55
"private": true,
66
"type": "module",
77
"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.",

scripts/package-mac-dmg.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ const REQUIRE_NOTARIZATION = process.env.HELM_REQUIRE_NOTARIZATION === "1";
2323
const TEAM_ID = String(process.env.APPLE_TEAM_ID || "").trim().toUpperCase();
2424
const NOTARY_PROFILE = String(process.env.APPLE_NOTARY_PROFILE || "").trim();
2525
const CONFIGURED_IDENTITY = String(process.env.APPLE_SIGN_IDENTITY || "").trim();
26+
// Electron Packager evaluates directories before their children. Keep the
27+
// scripts directory itself traversable so the required Mnemosyne bridge can
28+
// survive the otherwise root-level release filter.
2629
const IGNORE_NON_RUNTIME_ROOTS =
27-
/^\/(?!package\.json$|LICENSE$|desktop(?:$|\/)|container(?:$|\/)|src(?:$|\/)|public(?:$|\/)|scripts\/mnemosyne-bridge\.py$|node_modules(?:$|\/))/;
30+
/^\/(?!package\.json$|LICENSE$|desktop(?:$|\/)|container(?:$|\/)|src(?:$|\/)|public(?:$|\/)|scripts(?:$|\/mnemosyne-bridge\.py$)|node_modules(?:$|\/))/;
2831

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

scripts/package-windows.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"))
1313
const VERSION = String(pkg.version || "").trim();
1414
const REQUIRE_SIGNATURE = process.env.HELM_REQUIRE_WINDOWS_SIGNATURE === "1";
1515
const CERT_SHA1 = String(process.env.WINDOWS_SIGN_CERT_SHA1 || "").replace(/\s+/g, "").toUpperCase();
16-
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(?:$|\/))/;
16+
// Electron Packager evaluates directories before their children. Keep the
17+
// scripts directory itself traversable, then retain only the three runtime
18+
// files below; otherwise the exact-file exceptions can never be reached.
19+
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(?:$|\/))/;
1720

1821
if (process.platform !== "win32" || process.arch !== "x64") throw new Error("Windows packaging must run on Windows x64.");
1922
if (!/^\d+\.\d+\.\d+$/.test(VERSION)) throw new Error("package.json must contain a release version.");

src/server/channel-computers.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const APPLE_RUNTIME_VERSION = "1.1.0";
6767
export const APPLE_RUNTIME_PACKAGE = `container-${APPLE_RUNTIME_VERSION}-installer-signed.pkg`;
6868
export const APPLE_RUNTIME_URL = `https://github.com/apple/container/releases/download/${APPLE_RUNTIME_VERSION}/${APPLE_RUNTIME_PACKAGE}`;
6969
export const APPLE_RUNTIME_SHA256 = "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714";
70-
export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.6";
70+
export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.7";
7171
const CONTAINER_CANDIDATES = [process.env.HELM_CONTAINER_CLI, "/usr/local/bin/container", "/opt/homebrew/bin/container", "container"].filter(Boolean) as string[];
7272
const LXC_RUNTIME_VERSION = "1helm-lxc-runtime-v1";
7373
const LXC_HELPER_CANDIDATES = [
@@ -106,6 +106,12 @@ const MAX_WORKSPACE_SYNC_ENTRIES = Math.max(10_000, Number(process.env.HELM_WORK
106106
const SCROLLBACK_CAP = 256 * 1024;
107107
const terminalSessions = new Map<string, MachineTerminal>();
108108
const channelLocks = new Map<number, Promise<unknown>>();
109+
// Provisioning is a long host operation (the first LXC boot installs its
110+
// guest toolchain). The reconciler must not interpret the intentionally
111+
// marker-less machine that exists during that transaction as an ownership
112+
// violation. This is process-local on purpose: after a crash/restart there is
113+
// no active transaction, so the normal inspection path can recover or retry.
114+
const activeProvisioning = new Set<number>();
109115
const syncTimers = new Map<number, NodeJS.Timeout>();
110116
let reconcileTimer: NodeJS.Timeout | null = null;
111117
let reconcileStartupTimer: NodeJS.Timeout | null = null;
@@ -534,14 +540,19 @@ async function ensureWslRootfs(): Promise<string> {
534540
async function ensureLxcProvisioned(computer: ChannelComputer): Promise<void> {
535541
let inspection = await inspectLxc(computer);
536542
if (!inspection) {
537-
run("UPDATE channel_computers SET provision_status='provisioning',last_error='',updated=? WHERE channel_id=?", now(), computer.channel_id);
538-
markWorkspaceDirty(computer.channel_id, "*", "full");
539-
const architecture = process.arch === "arm64" ? "arm64" : "amd64";
540-
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 });
541-
if (created.code !== 0) throw new Error(created.stderr.toString("utf8").trim() || created.stdout.toString("utf8").trim() || "LXC channel computer creation failed");
542-
inspection = await inspectLxc(computer);
543-
if (!inspection || inspection.homeMount !== "none") throw new Error("Provisioned LXC computer failed its ownership/isolation verification.");
544-
recordComputerActivity(computer.channel_id, "Provisioned a persistent unprivileged LXC computer for this resident.", "complete");
543+
activeProvisioning.add(computer.channel_id);
544+
try {
545+
run("UPDATE channel_computers SET provision_status='provisioning',last_error='',updated=? WHERE channel_id=?", now(), computer.channel_id);
546+
markWorkspaceDirty(computer.channel_id, "*", "full");
547+
const architecture = process.arch === "arm64" ? "arm64" : "amd64";
548+
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 });
549+
if (created.code !== 0) throw new Error(created.stderr.toString("utf8").trim() || created.stdout.toString("utf8").trim() || "LXC channel computer creation failed");
550+
inspection = await inspectLxc(computer);
551+
if (!inspection || inspection.homeMount !== "none") throw new Error("Provisioned LXC computer failed its ownership/isolation verification.");
552+
recordComputerActivity(computer.channel_id, "Provisioned a persistent unprivileged LXC computer for this resident.", "complete");
553+
} finally {
554+
activeProvisioning.delete(computer.channel_id);
555+
}
545556
}
546557
recordObserved(computer, inspection);
547558
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);
@@ -1316,6 +1327,10 @@ export async function resizeChannelComputer(channelId: number, targetCpus: numbe
13161327

13171328
async function reconcileOne(computer: ChannelComputer): Promise<void> {
13181329
if (computer.desired_state === "deleted") return;
1330+
// A newly created isolated world does not receive its owner marker until
1331+
// guest bootstrap completes. The provisioning transaction performs the
1332+
// authoritative post-bootstrap inspection itself.
1333+
if (activeProvisioning.has(computer.channel_id)) return;
13191334
const channel = q1("SELECT status FROM channels WHERE id=?", computer.channel_id);
13201335
if (!channel) return;
13211336
if (!isolatedBackend(computer)) { await ensureNativeProvisioned(computer); return; }

src/server/db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ export function migrate(): void {
924924
const platformBackend = process.platform === "darwin" ? "apple" : process.platform === "win32" ? "wsl" : "lxc";
925925
const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend);
926926
const backend = ["apple", "lxc", "wsl", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend;
927-
const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.6");
927+
const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.7");
928928
// Earlier Linux/Windows releases persisted the compatibility `native`
929929
// seam into every channel row. A production host update must actually
930930
// move those rows onto the platform isolation backend; changing the unit's

test/channel-computers-backend-child.mjs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,22 @@ if (backend === "wsl") {
5050
}
5151

5252
try {
53-
const provisioned = await computers.provisionChannelComputer(channelId);
53+
let provisioning;
54+
if (backend === "lxc") {
55+
process.env.FAKE_LXC_CREATE_DELAY_MS = "500";
56+
const pending = computers.provisionChannelComputer(channelId);
57+
const machineConfig = join(fakeState, "machines", record.machine_id, "config.json");
58+
for (let attempt = 0; !existsSync(machineConfig) && attempt < 100; attempt++) {
59+
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
60+
}
61+
assert(existsSync(machineConfig), "the fake LXC entered its marker-less provisioning window");
62+
const concurrent = await computers.reconcileChannelComputers([channelId]);
63+
assert.deepEqual(concurrent, { checked: 1, errors: 0 }, "fleet reconciliation leaves an active provisioning transaction alone");
64+
assert.equal(db.q1("SELECT last_error FROM channel_computers WHERE channel_id=?", channelId).last_error, "");
65+
provisioning = await pending;
66+
delete process.env.FAKE_LXC_CREATE_DELAY_MS;
67+
} else provisioning = await computers.provisionChannelComputer(channelId);
68+
const provisioned = provisioning;
5469
assert.equal(provisioned.backend, backend);
5570
assert.equal(provisioned.home_mount, "none");
5671
assert.equal(provisioned.observed_state, "running");

test/channel-computers.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ test("Apple channel-computer contract preserves isolation, files, wakes, archive
168168
test("runtime digest and packaged image recipe stay pinned", async () => {
169169
assert.equal(computers.APPLE_RUNTIME_SHA256, "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714");
170170
assert.match(computers.APPLE_RUNTIME_URL, /\/1\.1\.0\/container-1\.1\.0-installer-signed\.pkg$/);
171-
assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.6");
171+
assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.7");
172172
const packaging = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8");
173173
assert.match(packaging, /container\(\?:\$\|\\\/\)/, "release packaging includes container/ image assets");
174174
const image = await readFile(join(root, "container", "Containerfile"), "utf8");

0 commit comments

Comments
 (0)