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

## [Unreleased]

## [0.0.6] - 2026-07-24

### Added

- Linux systemd hosts now give every ordinary channel a persistent
unprivileged LXC computer with private networking, subordinate UID/GID maps,
exact owner markers, managed CPU/RAM, host-mirrored files, and no host-home
mount. The installer pins and verifies the Ubuntu Noble image payloads and
installs a narrow root-owned lifecycle boundary.
- The accepted Windows 11 implementation now has a native x64 desktop and one
private WSL 2 Ubuntu distribution per ordinary channel. Windows-drive
automount and process interop are disabled, Ubuntu root filesystems are
immutable-version and SHA-256 pinned for x64 and arm64, and setup/removal
verify exact ownership. Its public installer is withheld until Authenticode
signing is available.
- Durable feedback is always visible, accepts optional attachments and
diagnostics, retries central delivery, and provides a Captain feedback inbox.
- Residents can search their own authoritative raw channel transcripts by
meaning, exact text/date, or recency and hydrate a complete prior session on
demand. Each channel keeps a separate Mnemosyne index; guests and other
channel residents cannot access it.
- SkillsMD discovery now queries the open registry directly and displays every
returned result. Revision pinning, bounds, scanning, hashing, and runtime
wrapping remain enforced when the user chooses to install.

### Changed

- Linux and Windows are production isolation backends; the in-process native
computer remains an explicit development/test seam rather than the default.
- Linux update requests are executed entirely by the host. The verified root
updater migrates runtime files and systemd units transactionally, refuses
downgrades, health-checks for up to one minute, and reports rollback complete
only after the restored host answers its health endpoint.
- The website and documentation describe the macOS, Linux/LXC, and Windows/WSL
product contracts without claiming an unsigned Windows artifact is public.

### Fixed

- Fresh hosts now open the 1Helm server while the optional Mnemosyne Python
and embedding runtime is prepared in the background, instead of making
first launch and health checks wait on virtual-environment package installs.
- Newly created and newly assigned skills now appear immediately in already
open Arsenal and Channel Settings views without a page refresh.
- Linux upgrades migrate existing compatibility computer records to LXC while
retaining channels, workspaces, obligations, and durable application state.
- Fresh/repeat Linux installs reject unsafe rollback symlinks and install
Python venv support required by long-term Mnemosyne memory.
- App removal, fleet inspection, lifecycle commands, terminal execution,
workspace synchronization, and ownership refusal now cover Apple, LXC, and
WSL backends consistently.

## [0.0.5] - 2026-07-24

### Changed
Expand Down Expand Up @@ -133,7 +184,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.5...HEAD
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.6...HEAD
[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
[0.0.3]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.3
Expand Down
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ release acceptance tests.
<p align="center">
<code>Apple Silicon native</code>&nbsp;&nbsp;
<code>34 complete playbooks</code>&nbsp;&nbsp;
<code>Focused SkillsMD catalog</code>&nbsp;&nbsp;
<code>Open SkillsMD search</code>&nbsp;&nbsp;
<code>Signed + notarized</code>
</p>

Expand Down Expand Up @@ -102,7 +102,7 @@ The model receives a compact inventory of the arsenal—not all 34 procedures in
every prompt. It can inspect metadata and load one complete skill when useful.
It can also:

- search the focused SkillsMD catalog of ready GitHub-backed repositories;
- search SkillsMD directly without a 1Helm-curated subset, then inspect and install a selected GitHub-backed skill;
- install ready skills only after immutable revision pinning, bounds,
scanning, hashing, provenance storage, and runtime-authority wrapping;
- route sources without a ready repository-specific procedure through the
Expand Down Expand Up @@ -154,6 +154,11 @@ does not run on the laptop or phone viewing the web UI.
- Exactly one resident for every ordinary channel and one Skipper in `#main`.
- A persistent Apple `container machine` Linux VM per ordinary channel, with
`home-mount=none`, on supported Apple Silicon Macs.
- A persistent unprivileged LXC per ordinary channel on supported Linux
systemd hosts, with subordinate UID/GID mapping and exact ownership checks.
- A private WSL 2 Ubuntu distribution per ordinary channel in the accepted
Windows implementation, with Windows-drive mounts and interop disabled. Its
public installer remains withheld until Authenticode signing is available.
- Shared channel `/workspace` for the agent command surface and human Terminal.
- Durable files, threads, curated memory, Mnemosyne long-term recall,
corrections, follow-ups, and recurring workflows.
Expand All @@ -176,12 +181,12 @@ does not run on the laptop or phone viewing the web UI.
| Platform | Current contract |
|---|---|
| **Apple Silicon macOS 26** | Native desktop product and real isolated Linux computer per resident. |
| **Linux / CI** | Durable headless compatibility backend; not per-resident VM isolation. |
| **Windows + WSL** | Headless compatibility path; not a native Windows app. |
| **Linux / CI** | Supported headless systemd host with one unprivileged LXC per resident; CI may select an explicit test backend. |
| **Windows + WSL** | Native x64 Electron and private WSL 2 worlds have passed real-host acceptance; the public installer awaits Authenticode signing. |

Not yet shipped: native Windows and Linux desktop packages, mobile clients,
Linux resident VM isolation, a hosted control plane, rich Photon attachment
fidelity, or blind execution of community skills.
Not yet shipped: a signed public Windows installer, a native Linux desktop
shell, mobile clients, a hosted control plane, rich Photon attachment fidelity,
or blind execution of community skills.

## Install on Apple Silicon

Expand Down Expand Up @@ -212,8 +217,8 @@ checkouts remain operator-managed and never send a Mac installer to the browser.

## Run the source workspace

The native Mac app is the complete consumer product. For development and
headless compatibility deployments, use Node 22:
For development or a source deployment outside the verified platform
installers, use Node 22:

```bash
PUPPETEER_SKIP_DOWNLOAD=1 npm install
Expand All @@ -230,8 +235,8 @@ 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.5` | Versioned Apple channel-machine image. |
| `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. |

### Agent-first JSON CLI

Expand All @@ -249,20 +254,21 @@ npm run helm -- audit-verify

## Architecture

1Helm is a compact Node/TypeScript control plane hosted by Electron on macOS.
It does not need an external database or a server transpilation step.
1Helm is a compact Node/TypeScript control plane hosted by Electron on macOS
and in the accepted Windows implementation, or by systemd on Linux. It does not
need an external database or a server transpilation step.

| Layer | Implementation |
|---|---|
| Runtime | Official Node 22 with native TypeScript stripping. |
| Control plane | `node:http`, WebSocket, additive SQLite migrations. |
| Client | Vanilla TypeScript bundled with esbuild and Tailwind CSS. |
| Model routing | Embedded ReRouted headless engine, private internal gateway, account pools, retries, routes, quotas, and logs. |
| Computers | Defensive argv-only Apple `container machine` backend; explicit compatibility backend elsewhere. |
| Computers | Defensive argv-only Apple `container machine`, narrow root-owned unprivileged LXC, and private WSL 2 backends; explicit `native`/`mock` test seams. |
| Terminal | `node-pty`; ordinary terminals enter their channel VM while Skipper remains native. |
| Memory | Curated records with provenance plus an isolated Mnemosyne SQLite store per identity. |
| Scheduling | Durable obligations, wake reconciliation, lifecycle safety, repair, update, and pressure-aware sizing. |
| Desktop | Sandboxed Electron renderer, ephemeral loopback server, persistent Application Support, native wake agent. |
| Desktop | Sandboxed Electron renderer, ephemeral loopback server, persistent host data, and native wake/update integration on supported desktop hosts. |

Start with [`docs/VISION.md`](docs/VISION.md) for the product record and
[`docs/architecture`](https://1helm.com/docs/architecture) for the readable
Expand All @@ -286,7 +292,9 @@ or a complete security score.

## Security boundary

- Resident Macs use separate Linux VMs with no native Mac home mount.
- Residents use separate Linux worlds: Apple machines with no Mac home mount,
unprivileged LXC with subordinate host IDs, or private WSL 2 distributions
with Windows-drive mounts and interop disabled.
- Skipper's host tools require Captain-authorized provenance.
- Workspace file mirrors are channel-scoped, size-bounded, and symlink-contained.
- Provider and connection credentials stay in host-owned storage.
Expand Down
39 changes: 39 additions & 0 deletions cloudflare/migrations/0001_initial.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS workspaces (
slug TEXT PRIMARY KEY,
hostname TEXT NOT NULL UNIQUE,
installation_id TEXT NOT NULL UNIQUE,
workspace_name TEXT NOT NULL,
management_secret_hash TEXT NOT NULL,
tunnel_id TEXT NOT NULL DEFAULT '',
connector_secret_cipher TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'provisioning',
enabled INTEGER NOT NULL DEFAULT 1,
error TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_workspaces_installation ON workspaces(installation_id);

CREATE TABLE IF NOT EXISTS feedback_reports (
public_id TEXT PRIMARY KEY,
installation_id TEXT NOT NULL,
workspace_name TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL,
diagnostics TEXT NOT NULL DEFAULT '{}',
attachment_count INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
received_at INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_feedback_received ON feedback_reports(received_at DESC);

CREATE TABLE IF NOT EXISTS feedback_attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_id TEXT NOT NULL REFERENCES feedback_reports(public_id) ON DELETE CASCADE,
name TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL,
data TEXT NOT NULL,
created_at INTEGER NOT NULL
);
22 changes: 22 additions & 0 deletions cloudflare/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,25 @@ CREATE TABLE IF NOT EXISTS workspaces (
);

CREATE INDEX IF NOT EXISTS idx_workspaces_installation ON workspaces(installation_id);

CREATE TABLE IF NOT EXISTS feedback_reports (
public_id TEXT PRIMARY KEY,
installation_id TEXT NOT NULL,
workspace_name TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL,
diagnostics TEXT NOT NULL DEFAULT '{}',
attachment_count INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
received_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_feedback_received ON feedback_reports(received_at DESC);

CREATE TABLE IF NOT EXISTS feedback_attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_id TEXT NOT NULL REFERENCES feedback_reports(public_id) ON DELETE CASCADE,
name TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL,
data TEXT NOT NULL,
created_at INTEGER NOT NULL
);
67 changes: 67 additions & 0 deletions cloudflare/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ interface Env {
CLOUDFLARE_ZONE_ID: string;
CLOUDFLARE_RUNTIME_TOKEN: string;
PROVISION_LIMIT?: RateLimit;
FEEDBACK_LIMIT?: RateLimit;
FEEDBACK_ADMIN_TOKEN?: string;
}

type WorkspaceRow = {
Expand Down Expand Up @@ -151,11 +153,76 @@ async function workspaceAction(request: Request, env: Env, slug: string): Promis
return json({ workspace: { slug, hostname: workspace.hostname, status: workspace.status, enabled } });
}

async function feedbackIntake(request: Request, env: Env): Promise<Response> {
const address = request.headers.get("cf-connecting-ip") || "unknown";
if (env.FEEDBACK_LIMIT && !(await env.FEEDBACK_LIMIT.limit({ key: address })).success) {
return json({ error: "Too many feedback reports. Try again shortly." }, 429);
}
const body = await request.json().catch(() => ({})) as Record<string, unknown>;
const publicId = String(body.public_id || "");
const installationId = String(body.installation_id || "");
const workspaceName = String(body.workspace_name || "").trim().slice(0, 100);
const comment = String(body.comment || "").trim().slice(0, 10_000);
const diagnostics = body.diagnostics && typeof body.diagnostics === "object" && !Array.isArray(body.diagnostics) ? body.diagnostics : {};
const attachments = Array.isArray(body.attachments) ? body.attachments.slice(0, 3) as Array<Record<string, unknown>> : [];
if (!/^fb_[a-f0-9]{24}$/.test(publicId) || !/^[a-f0-9]{16}$/.test(installationId)) {
return json({ error: "Feedback source could not be verified." }, 400);
}
if (!comment && !attachments.length) return json({ error: "Feedback is empty." }, 400);
if (JSON.stringify(diagnostics).length > 64 * 1024) return json({ error: "Diagnostics are too large." }, 413);
let total = 0;
for (const attachment of attachments) {
const size = Number(attachment.size || 0);
const data = String(attachment.data || "");
const validBase64 = data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(data);
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
const decodedSize = data.length ? (data.length / 4) * 3 - padding : 0;
if (!Number.isSafeInteger(size) || !validBase64 || decodedSize !== size
|| size < 0 || size > 5 * 1024 * 1024 || data.length > 7 * 1024 * 1024) {
return json({ error: "A feedback attachment is too large." }, 413);
}
total += size;
}
if (total > 10 * 1024 * 1024) return json({ error: "Feedback attachments are too large." }, 413);
const timestamp = Date.now();
await env.REGISTRY.prepare(`INSERT OR IGNORE INTO feedback_reports
(public_id,installation_id,workspace_name,comment,diagnostics,attachment_count,created_at,received_at)
VALUES (?,?,?,?,?,?,?,?)`).bind(publicId, installationId, workspaceName, comment, JSON.stringify(diagnostics), attachments.length, timestamp, timestamp).run();
const exists = await env.REGISTRY.prepare("SELECT 1 FROM feedback_attachments WHERE report_id=? LIMIT 1").bind(publicId).first();
if (!exists) {
for (const attachment of attachments) await env.REGISTRY.prepare(`INSERT INTO feedback_attachments
(report_id,name,mime,size,data,created_at) VALUES (?,?,?,?,?,?)`).bind(
publicId,
String(attachment.name || "attachment").slice(0, 255),
String(attachment.mime || "application/octet-stream").slice(0, 120),
Number(attachment.size || 0),
String(attachment.data || ""),
timestamp,
).run();
}
return json({ id: publicId }, 202);
}

async function feedbackInbox(request: Request, env: Env): Promise<Response> {
const token = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "") || "";
if (!env.FEEDBACK_ADMIN_TOKEN || token !== env.FEEDBACK_ADMIN_TOKEN) return json({ error: "Not found" }, 404);
const results = await env.REGISTRY.prepare(`SELECT public_id,installation_id,workspace_name,comment,diagnostics,
attachment_count,created_at created,received_at FROM feedback_reports ORDER BY received_at DESC LIMIT 500`).all<Record<string, unknown>>();
return json({ reports: (results.results || []).map((report) => ({
...report,
diagnostics: JSON.parse(String(report.diagnostics || "{}")),
state: "delivered",
attachments: [],
})) });
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "OPTIONS") return json({ ok: true });
const url = new URL(request.url);
if (url.pathname === "/health") return json({ ok: true });
if (url.pathname === "/v1/feedback" && request.method === "POST") return feedbackIntake(request, env);
if (url.pathname === "/v1/feedback" && request.method === "GET") return feedbackInbox(request, env);
const availability = url.pathname.match(/^\/v1\/slugs\/([a-z0-9-]+)$/);
if (availability && request.method === "GET") {
const slug = availability[1].toLowerCase();
Expand Down
5 changes: 5 additions & 0 deletions cloudflare/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"name": "PROVISION_LIMIT",
"namespace_id": "1001",
"simple": { "limit": 20, "period": 60 }
},
{
"name": "FEEDBACK_LIMIT",
"namespace_id": "1002",
"simple": { "limit": 30, "period": 60 }
}
],
"d1_databases": [
Expand Down
14 changes: 14 additions & 0 deletions deploy/1helm-lxc-unprivileged.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 1Helm channel computers are user-namespaced Ubuntu containers. Root inside a
# channel maps to an unprivileged host subuid/subgid and each container receives
# only the private LXC bridge—not a host directory or device passthrough.
lxc.include = /usr/share/lxc/config/ubuntu.userns.conf
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536
lxc.apparmor.profile = generated
lxc.apparmor.allow_nesting = 0
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.name = eth0
lxc.start.auto = 0
Loading
Loading