Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
047c3ad
Integrate local model setup and gated workflow qualification
BunsDev Nov 4, 2026
d99687d
Record pinned local model qualification results and remaining gates
BunsDev Nov 4, 2026
bdd5dd2
Fence local model metadata replies and preserve qualification receipts
BunsDev Nov 4, 2026
861100d
Record metadata ordering repair validation at bdd5dd2
BunsDev Nov 4, 2026
ac0f3b4
Bind offline qualification to the owned runtime model digest
BunsDev Nov 4, 2026
4bbdd46
Report managed configuration failures with correct recovery guidance
BunsDev Nov 5, 2026
f6763d0
Document direct daemon recovery for native qualification
BunsDev Nov 6, 2026
d5dac05
Add foreground local-model qualification independent of shared daemon
BunsDev Nov 6, 2026
433ecc3
Redact local credentials before tool and stderr output limits
BunsDev Nov 6, 2026
bf7c915
Qualify local workflow in recovered macOS context and assert current …
BunsDev Nov 6, 2026
368235b
Scope the unadmitted local-model refusal assertion to its visible alert
BunsDev Nov 6, 2026
93fd820
Use an escape-free exact approved edit in native qualification
BunsDev Nov 6, 2026
5348074
Verify the host sandbox before offline native qualification
BunsDev Nov 7, 2026
e5a0b0d
Record complete local-model workflow qualification and recovery
BunsDev Nov 7, 2026
f424950
Record local-model requalification after runtime upgrades
BunsDev Nov 8, 2026
c72a956
Qualify local-model recovery and preserve acknowledged image drafts
BunsDev Nov 8, 2026
ff99bba
Record signed local-model qualification and recovery
BunsDev Nov 8, 2026
13ce3e4
fix(ios): preserve drafts during command picker recovery
BunsDev Nov 8, 2026
d3d2eee
docs: distinguish expected UI self-test failure from ordinary passes
BunsDev Nov 8, 2026
e6b7fd3
Integrate prepared local model workflow and native recovery qualifica…
BunsDev Nov 11, 2026
7d6094e
Keep packaged credential storage on OS encryption and name follow-up …
BunsDev Nov 11, 2026
0b77a13
Record closeout qualification and restore CF-8 evidence
BunsDev Nov 11, 2026
19d418d
Capture native stacks when test application shutdown stays pending
BunsDev Nov 11, 2026
e068ef1
Integrate published Companion recovery and secure-answer keyboard han…
BunsDev Nov 11, 2026
47d9e9e
Observe paired TLS phases and qualify native closeout delivery
BunsDev Nov 11, 2026
be5a4c8
Integrate protected Companion recovery delivery on main
BunsDev Nov 11, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ modex/
explicit; connection tests identify the saved configuration, and failed saves retain
your draft. CLI-only routing never falls back to HTTPS, existing sessions stay on their
backend, and Auto stops if it cannot honor your reasoning-effort ceiling.
- **Local model connections (experimental).** Settings → Local models manages loopback connections and reads bounded model metadata without loading or downloading models. Local coding execution remains disabled while native qualification and product admission are open. See [setup and current limits](docs/local-models.md#local-models-v1-workflow-candidate).
- **Launch from anywhere.** Opened from Finder or the Dock, Modex reads your login shell's
PATH once at startup (interactive `zsh -ilc`, falling back to `-lc`), so `claude`, `codex`,
and `jev` installed under nvm, Homebrew, or `~/.local/bin` are found exactly as in a terminal.
Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/e2e/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ test("⌘N → type → ⌘⏎ → approve: the agent edits the repo and the UI
await expect(followUp).toHaveAttribute("data-source", "heuristic");
const suggested = (await tid(followUp, "followup-text").innerText()).trim();
expect(suggested).toMatch(/test|verif|review/i);
await expect(followUp).toHaveAccessibleName(`Use suggested follow-up: ${suggested}`);
const input = tid(page, "composer-input");
await input.focus();
await input.press("Tab");
Expand Down Expand Up @@ -310,12 +311,18 @@ test("a key typed into Settings is kept encrypted outside state.json, reported m
const status = tid(page, "routing-status");
await expect(status).toContainText("Jev transport configured");
await expect(status).toContainText("key ****4321 from Modex keychain");
await expect(keyBox.locator("label > span").first()).toContainText("saved in test cipher (not secure)");
const storageBackend = process.env.MODEX_PACKAGED_APP
? process.platform === "darwin" ? "macOS Keychain" : process.platform === "win32" ? "Windows DPAPI" : "OS keyring"
: "test cipher (not secure)";
await expect(keyBox.locator("label > span").first()).toContainText(`saved in ${storageBackend}`);
await expect(keyBox.locator("input[type=password]")).toHaveValue("");
// On disk: never in state.json, never in plaintext, and the secrets file is owner-only.
// The unpackaged fixture is reversible; a packaged artifact must use OS encryption.
// Neither stores credentials in state.json, and the secrets file remains owner-only.
expect(fs.readFileSync(path.join(home, "app", "state.json"), "utf8")).not.toContain("4321");
const secretsFile = path.join(home, "app", "secrets.json");
expect(fs.readFileSync(secretsFile, "utf8")).not.toContain("sk-e2e-typed-key-4321");
const savedKey = JSON.parse(fs.readFileSync(secretsFile, "utf8")).entries.typesafe_api_key.enc;
expect(Buffer.from(savedKey, "base64").includes(Buffer.from("sk-e2e-typed-key-4321"))).toBe(!process.env.MODEX_PACKAGED_APP);
if (process.platform !== "win32") expect(fs.statSync(secretsFile).mode & 0o777).toBe(0o600);
await tid(page, "settings").getByRole("button", { name: "Cancel" }).click();
await expect(tid(page, "settings")).toHaveCount(0);
Expand Down
7 changes: 4 additions & 3 deletions apps/desktop/e2e/code-review-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from "node:fs";
import path from "node:path";
import { appDir, resizeWindow, seedHome, tid } from "./support.js";
import { WORLD_GH, type GhWorld } from "../test/fixtures/code-review-gh-world.js";
import { closeWithDiagnostics } from "../test/support/electron-close.js";

export { baseWorld, FIELD_GUIDE, FIELD_GUIDE_73, FIELD_GUIDE_91, LENDING, LENDING_73 } from "../test/fixtures/code-review-gh-world.js";

Expand Down Expand Up @@ -93,17 +94,17 @@ export async function launchWithGh(initial: GhWorld, options: { showCodeReview?:
// SIGKILL: no quit handlers run, as when the Mac loses power or Modex crashes mid-send.
const pid = app.process().pid;
if (pid) process.kill(pid, "SIGKILL");
await app.close().catch(() => {});
await closeWithDiagnostics(app, test.info().outputDir).catch(() => {});
({ app, page } = await start());
harness.app = app; harness.page = page;
},
restart: async () => {
await app.close().catch(() => {});
await closeWithDiagnostics(app, test.info().outputDir).catch(() => {});
({ app, page } = await start());
harness.app = app; harness.page = page;
},
close: async () => {
await app.close().catch(() => {});
await closeWithDiagnostics(app, test.info().outputDir).catch(() => {});
for (const dir of [home, repo]) fs.rmSync(dir, { recursive: true, force: true });
},
};
Expand Down
75 changes: 75 additions & 0 deletions apps/desktop/e2e/fixtures/local-model-native.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Real transport for the existing opt-in disposable-home Electron bootstrap. Not packaged.
import fs from "node:fs";
import path from "node:path";
import { app } from "electron";
import { LocalExecution } from "../../dist/src/main/engine/local-models/execution.js";
import { cliSpawn } from "../../dist/src/main/engine/backends/health.js";
import { resolveCli } from "../../dist/src/main/engine/cli-path.js";
import { ALLOWED, MODEL, CONTEXT } from "./local-native-spec.mjs";
import { nativeQualification } from "./native-qualification-guard.mjs";

export function createNativeExecution(home, connections, runner) {
const { cwd, origin } = nativeQualification({ home, enabled: process.env.MODEX_LM_NATIVE_E2E, e2e: process.env.MODEX_E2E, packaged: app.isPackaged });
const bin = resolveCli("codex", "codex");
const children = new Map();
const events = [];
let cloudCalls = 0;
const record = event => {
const value = { at: Date.now(), ...event }; events.push(value);
fs.appendFileSync(path.join(home, "app/local-native-events.jsonl"), JSON.stringify(value) + "\n", { mode: 0o600 });
};
const observeLines = (direction, child) => {
let buffer = "";
return bytes => {
buffer += bytes.toString();
if (buffer.length > 1024 * 1024) throw new Error("Native qualification observer exceeded its bound.");
let end;
while ((end = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
try {
const frame = JSON.parse(line);
if (frame.method) record({ type: "rpc", direction, pid: child.pid, method: frame.method, ...(frame.params?.threadId ? { threadId: frame.params.threadId } : {}), ...(frame.params?.turn?.status ? { turnStatus: frame.params.turn.status } : {}), ...(frame.method === "error" ? { willRetry: frame.params?.willRetry, error: frame.params?.error } : {}) });
} catch { record({ type: "observer-error", direction, pid: child.pid }); }
}
};
};
const spawn = (executable, args, options) => {
if (executable !== bin || args[0] !== "app-server" || !options.env?.HOME?.startsWith(path.join(home, "app/local-codex") + path.sep)) throw new Error("Refusing a non-owned native child.");
const child = cliSpawn(executable, args, options);
const row = { child, home: options.env.HOME, running: true }; children.set(child.pid, row);
record({ type: "spawn", pid: child.pid, executable, home: row.home });
const input = observeLines("out", child), write = child.stdin.write;
child.stdin.write = function (bytes, ...rest) { input(bytes); return write.call(this, bytes, ...rest); };
child.stdout.on("data", observeLines("in", child));
child.once("exit", (code, signal) => { row.running = false; record({ type: "exit", pid: child.pid, code, signal }); });
return child;
};
const execution = new LocalExecution(home, connections, bin, spawn);
const validate = execution.validate.bind(execution);
execution.validate = thread => {
const target = validate(thread), connection = connections.resolve(target.model.connection);
if (target.model.id !== MODEL || connection?.endpoint.origin !== origin || thread.localContextTokens !== CONTEXT || fs.realpathSync(thread.cwd) !== cwd) throw new Error("Native qualification refused an unapproved target.");
return target;
};
const pending = () => [...runner.live].flatMap(([threadId, live]) => Object.values(live.requests.requests).filter(r => r.state === "pending" && r.request.kind === "approval" && live.pending.has(r.request.id)).map(r => r.request).map(r => ({ threadId, itemId: r.id,
command: r.action?.tool === "command" ? String(r.action.input?.command ?? "") : "", cwd: r.action?.cwd,
allowed: r.action?.tool === "command" && ALLOWED.includes(String(r.action.input?.command ?? "")) && r.action.cwd && fs.realpathSync(r.action.cwd) === cwd })));
globalThis.__modexLocalNative = {
snapshot: () => ({ kind: "real Codex and Ollama; test-only admission", executable: bin, cloudCalls, events: [...events], children: [...children].map(([pid, row]) => ({ pid, home: row.home, running: row.running })), pending: pending(),
provenance: Object.fromEntries(runner.o.store.snapshot().threads.filter(t => t.localTarget).map(t => [t.id, execution.provenance(t.id)])) }),
killCodex: pid => {
const row = children.get(pid);
if (!row || !row.running || row.child.exitCode !== null || row.child.signalCode) throw new Error("Native PID is no longer owned and running.");
record({ type: "injected-codex-loss", pid }); return row.child.kill("SIGKILL");
},
cloudCalled: () => { cloudCalls++; record({ type: "forbidden-cloud-call" }); },
};
const answer = runner.answer.bind(runner);
runner.answer = (threadId, itemId, decision, by) => {
const request = pending().find(r => r.threadId === threadId && r.itemId === itemId);
if (decision !== "no" && !request?.allowed) throw new Error("Native qualification refuses approval outside its exact command allowlist.");
record({ type: "approval", threadId, itemId, decision, command: request?.command ?? "" });
return answer(threadId, itemId, decision, by);
};
return execution;
}
36 changes: 36 additions & 0 deletions apps/desktop/e2e/fixtures/local-model-workflow.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Inject a scripted app-server only in a disposable Electron acceptance home.
// The production index, preload, IPC, runner, admission policy and renderer stay in use.
// This fixture is never packaged and does not establish real-model qualification.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { ThreadRunner } from "../../dist/src/main/engine/runner.js";
import { LocalExecution } from "../../dist/src/main/engine/local-models/execution.js";
import { LocalConnectionStore } from "../../dist/src/main/engine/local-models/connections.js";
import { SecretStore, testCipher } from "../../dist/src/main/engine/secrets.js";
import { localWorkflowSpawn } from "../../dist/test/local-workflow-fixture.js";

const home = fs.realpathSync(process.env.MODEX_HOME ?? "");
if (process.env.MODEX_E2E !== "1" || !home.startsWith(fs.realpathSync(os.tmpdir()) + path.sep)
|| !path.basename(home).startsWith("modex-e2e-home-")
|| JSON.parse(fs.readFileSync(path.join(home, "app/state.json"), "utf8")).settings.default_backend !== "mock") {
throw new Error("Local workflow acceptance requires a disposable mock home.");
}
const installed = new WeakSet();
// Only this unpackaged, disposable-home entry point can select the real test transport.
const native = process.env.MODEX_LM_NATIVE_E2E === "1" ? await import("./local-model-native.mjs") : null;
function install(runner) {
if (installed.has(runner)) return;
const connections = new LocalConnectionStore(home, new SecretStore(home, testCipher));

runner.o.localExecution = native ? native.createNativeExecution(home, connections, runner) : new LocalExecution(home, connections, "fixture-codex", localWorkflowSpawn);
runner.backends.codex = { id: "codex", listModels: async () => [{ id: "fixture-cloud", label: "Fixture Cloud", isDefault: true }],
health: async () => ({ executable: "available", authentication: "authenticated", access: "available", detail: "Offline fixture" }),
dispose: async () => {}, runTurn: async () => { globalThis.__modexLocalNative?.cloudCalled(); throw new Error("A local fixture reached the cloud backend"); } };
installed.add(runner);
}
for (const method of ["updateThread", "send", "sendDurably", "retry", "listModels", "backend"]) {
const original = ThreadRunner.prototype[method];
ThreadRunner.prototype[method] = function (...args) { install(this); return original.apply(this, args); };
}
await import("../../dist/src/main/index.js");
22 changes: 22 additions & 0 deletions apps/desktop/e2e/fixtures/local-native-spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Fixed inputs for the opt-in native lifecycle qualification. Never imported by product main.
export const MODEL = "qwen3-coder:30b";
export const DIGEST = "06c1097efce0431c2045fe7b2e5108366e43bee1b4603a7aded8f21689e90bca";
export const CONTEXT = 65536;
export const SENTINEL = "LM3-FIXTURE-SENTINEL-7f3a";
export const EDITED = "LM3-APPROVED-EDIT-8c42\n";
export const WRITE = "echo LM3-APPROVED-EDIT-8c42 > result.txt";
export const CHECK_COMMAND = "node check.cjs";
export const DELAY_MS = 10000;
export const GRACE_MS = 1500;
export const DELAYED_CASES = Object.freeze(["stop", "codex-loss", "server-loss"]);
export const FRESH_CASES = Object.freeze(["after-stop", "after-codex-loss", "after-server-loss"]);
export const commandFor = id => `node delayed-write.cjs ${id}`;
export const ALLOWED = Object.freeze(["cat fixture.txt", WRITE, CHECK_COMMAND, ...DELAYED_CASES.map(commandFor), ...FRESH_CASES.map(commandFor)]);
export const CHECK_SOURCE = `const fs=require('node:fs');const ok=fs.readFileSync('result.txt','utf8')===${JSON.stringify(EDITED)};fs.appendFileSync('checks.jsonl',JSON.stringify({ok,exitCode:ok?0:1})+'\\n');console.log(ok?'independent check passed':'independent check failed');process.exit(ok?0:1);\n`;
export const DELAY_SOURCE = `const fs=require('node:fs');const id=process.argv[2];const delayed=${JSON.stringify(DELAYED_CASES)}.includes(id);if(![...${JSON.stringify(DELAYED_CASES)},...${JSON.stringify(FRESH_CASES)}].includes(id))throw Error('Unknown qualification case');const startedAt=Date.now(),deadline=startedAt+(delayed?${DELAY_MS}:0);fs.appendFileSync('lifecycle-runs.jsonl',JSON.stringify({id,pid:process.pid,startedAt,deadline})+'\\n');fs.writeFileSync(id+'-started.json',JSON.stringify({id,pid:process.pid,startedAt,deadline}),{flag:'wx'});const finish=()=>{fs.appendFileSync('lifecycle-effects.jsonl',JSON.stringify({id,pid:process.pid,at:Date.now()})+'\\n');fs.writeFileSync(id+'-effect.txt',id+':one\\n',{flag:'wx'});console.log(id+' effect completed');};if(delayed)setTimeout(finish,${DELAY_MS});else finish();\n`;
export const SCENARIOS = Object.freeze({
golden: { barrier: "approved exact edit and separately seeded check", result: EDITED, checks: 1 },
stop: { barrier: "stop-started.json exists before deadline", dispatches: 1, effectsAfterDeadline: 0, freshAction: "after-stop", freshEffects: 1 },
"codex-loss": { barrier: "codex-loss-started.json exists before deadline; exact owned native PID", dispatches: 1, effectsAfterDeadline: "zero or one from original command; never replayed", freshAction: "after-codex-loss", freshEffects: 1 },
"server-loss": { barrier: "server-loss-started.json exists before deadline; exact owned inference-server PID", dispatches: 1, effectsAfterDeadline: "zero or one from original command; never replayed", freshAction: "after-server-loss", freshEffects: 1 },
});
21 changes: 21 additions & 0 deletions apps/desktop/e2e/fixtures/native-qualification-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Validation for the opt-in, unpackaged native fixture. This module starts no process or service.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { MODEL, DIGEST, CONTEXT } from "./local-native-spec.mjs";

export function nativeQualification({ home, enabled, e2e, packaged }) {
if (enabled !== "1" || e2e !== "1" || packaged !== false) throw new Error("Native qualification requires an opt-in unpackaged test app.");
const temporary = fs.realpathSync(os.tmpdir()), root = fs.realpathSync(home);
if (!root.startsWith(temporary + path.sep) || !path.basename(root).startsWith("modex-e2e-home-")
|| JSON.parse(fs.readFileSync(path.join(root, "app/state.json"), "utf8")).settings.default_backend !== "mock") throw new Error("Native qualification requires a disposable mock home.");
const file = path.join(root, "local-native-qualification.json"), stat = fs.lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 8192) throw new Error("Invalid native qualification manifest.");
const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
const cwd = fs.realpathSync(manifest.cwd), origin = new URL(manifest.origin);
if (manifest.version !== 1 || manifest.authorization !== "local-native-lifecycle" || manifest.model !== MODEL || manifest.digest !== DIGEST || manifest.contextTokens !== CONTEXT
|| !cwd.startsWith(temporary + path.sep) || !path.basename(cwd).startsWith("modex-e2e-repo-")
|| origin.protocol !== "http:" || origin.hostname !== "127.0.0.1" || !origin.port || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash
|| Object.keys(manifest).some(key => !["version", "authorization", "model", "digest", "contextTokens", "cwd", "origin"].includes(key))) throw new Error("Native qualification is limited to the approved disposable project and model.");
return { cwd, origin: origin.origin };
}
Loading
Loading