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
7 changes: 3 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,11 @@ MUTEKI_DEEPSEEK_API_KEY=sk-your-deepseek-key-here
# MUTEKI_MAX_UPLOAD_FILES=20

# ── Optional: run retention policy ───────────────────────────────────────────
# The web backend auto-archives idle unpinned runs, then deletes old archived
# runs. Disable only if you want to keep every local run forever.
# CTF and pentest default to archive after 15 idle days and delete archived
# runs after 30 idle days. Each mode's settings page stores its own policy;
# this flag disables all automatic actions globally.
# MUTEKI_RETENTION_ENABLED=1
# MUTEKI_RETENTION_INTERVAL=3600 # sweep interval, seconds
# MUTEKI_ARCHIVE_DAYS=3
# MUTEKI_DELETE_DAYS=7

# ── Optional: containerized worker image ─────────────────────────────────────
# The single generic worker image used in CONTAINER mode (one image for all
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

All notable public release changes are tracked here.

## 0.4.2 - 2026-10-09

### Fixed

- Restored installed Agent CLI and native login discovery in regular desktop workspaces, including user-local, NVM, and Bun installations. Candidate startup checks retain isolated homes and credentials.
- Fixed desktop candidate identity checks to hash the physical ASAR archive and report recovery failures clearly.
- Reused the Agent credential view and fixed Switch CSS and narrow cards in the settings interface.

### Changed

- Moved version updates out of the CTF and penetration-testing settings into global settings. Existing update links redirect to the new page.

### Added

- Added global desktop update settings with official macOS release checks, GitHub SHA-256 verification, strict signature checks, isolated candidate startup, data backups, and restart-based app replacement. Service updates remain available in Web settings.
- Added separate CTF and penetration-testing retention settings with saved configuration and independent archive and delete switches. Defaults archive after 15 idle days and delete after 30 idle days. Idle time starts at the run's last activity, and deletion applies only to already archived tasks.

### Upgrade

- Use `muteki upgrade v0.4.2` or `./run.sh upgrade v0.4.2` for a managed Web installation, and add `--compose` for a container deployment. Existing `.env`, `state/`, credentials, and sessions remain outside the release bundle.
- Download the macOS `.dmg` or `.zip` matching your Mac from the release page. Windows and Linux desktop packages are not included.
- Retention thresholds now come from each mode's saved settings. `MUTEKI_ARCHIVE_DAYS` and `MUTEKI_DELETE_DAYS` no longer set these thresholds; `MUTEKI_RETENTION_ENABLED=0` still disables all automatic retention actions.
- Published 0.4.1 desktop installs need one manual install of 0.4.2 because 0.4.1 has no in-app update screen. Once on 0.4.2, official updates are available in global settings and verify the release SHA-256 and strict signatures before preparing a candidate.
- Local candidate updates require a stable Developer ID signature. The published macOS packages remain ad-hoc signed and are not notarized. There is no unsigned update bypass.

## 0.4.1 - 2026-10-09

### Added
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ After a month of engineering work, tuning, and fixes, Muteki is open source with

## Release notes

The current version is **0.4.1**. See [CHANGELOG.md](CHANGELOG.md) for new features, behavior changes, removals, and upgrade notes.
The current version is **0.4.2**. See [CHANGELOG.md](CHANGELOG.md) for new features, behavior changes, removals, and upgrade notes.

The [0.4.1 release](https://github.com/FishCodeTech/muteki/releases/tag/v0.4.1) includes a macOS desktop app for Apple silicon and Intel, with a bundled local service and support for remote Muteki services. Windows and Linux desktop packages are not included in this release. Web and container deployments remain available.
The [0.4.2 release](https://github.com/FishCodeTech/muteki/releases/tag/v0.4.2) includes a macOS desktop app for Apple silicon and Intel, with a bundled local service and support for remote Muteki services. Windows and Linux desktop packages are not included. Web and container deployments remain available.

The macOS packages are ad-hoc signed and are not notarized. Install desktop updates manually; the in-app signed-candidate update flow requires a stable Developer ID signature.
The macOS packages are ad-hoc signed and are not notarized. Published 0.4.1 desktop installs need a manual first install of 0.4.2 because they do not include the update screen. In 0.4.2, global settings can check and install official releases after GitHub SHA-256 and strict signature verification. Local candidate updates require a stable Developer ID signature.

---

Expand Down Expand Up @@ -295,7 +295,7 @@ The UI remains at `http://localhost:3001` by default. `MUTEKI_HOST_DATA_ROOT` mu
```bash
./run.sh upgrade --check # Check the latest stable release
./run.sh install # Set up a managed installation
muteki upgrade v0.4.1 # Install this release
muteki upgrade v0.4.2 # Install this release
muteki rollback # Return to the previous installed release
muteki version # Show version and installation kind
```
Expand Down
8 changes: 4 additions & 4 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ tsecbench 托管模式下+deepseek-flash,排名13

## 版本更新

当前版本为 **0.4.1**。新增功能、行为变化、移除内容及升级说明见 [CHANGELOG.md](CHANGELOG.md)。
当前版本为 **0.4.2**。新增功能、行为变化、移除内容及升级说明见 [CHANGELOG.md](CHANGELOG.md)。

[0.4.1 发布页](https://github.com/FishCodeTech/muteki/releases/tag/v0.4.1) 提供 Apple 芯片和 Intel Mac 的 macOS 客户端,内置本地服务,也可连接远程 Muteki 服务。本次暂不提供 Windows、Linux 桌面安装包,Web 和容器部署继续支持。
[0.4.2 发布页](https://github.com/FishCodeTech/muteki/releases/tag/v0.4.2) 提供 Apple 芯片和 Intel Mac 的 macOS 客户端,内置本地服务,也可连接远程 Muteki 服务。本次不提供 Windows、Linux 桌面安装包,Web 和容器部署继续支持。

本次 macOS 安装包使用临时签名,尚未公证。桌面更新请手动安装;应用内候选版本更新流程需要稳定的 Developer ID 签名。
本次 macOS 安装包使用临时签名,尚未公证。已发布的 0.4.1 桌面版没有应用内更新界面,需要先手动安装一次 0.4.2。0.4.2 可在全局设置中检查并安装官方版本,安装前会校验 GitHub SHA-256 和严格签名。本地候选版本更新需要稳定的 Developer ID 签名。

---

Expand Down Expand Up @@ -319,7 +319,7 @@ MUTEKI_WEB_PASSWORD='请替换为强密码' \
```bash
./run.sh upgrade --check # 检查稳定版
./run.sh install # 安装托管版本
muteki upgrade v0.4.1 # 更新到本次版本
muteki upgrade v0.4.2 # 更新到本次版本
muteki rollback # 回滚到上一个已安装版本
muteki version # 查看版本与安装形态
```
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/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 apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "muteki-desktop",
"productName": "Muteki",
"version": "0.4.1",
"version": "0.4.2",
"private": true,
"type": "module",
"description": "Muteki desktop with an isolated local workspace and remote service connections",
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/renderer/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export type DesktopCommand = 'back' | 'forward' | 'reload' | 'sidebar' | 'search' | 'theme' | 'new-chat' | 'settings' | 'shortcuts';
export type DesktopWindowAction = 'minimize' | 'maximize' | 'close';
export interface DesktopUpdateStatus {
state: 'idle' | 'checking' | 'current' | 'available' | 'downloading' | 'validating' | 'ready' | 'installing' | 'error';
currentVersion: string; latestVersion?: string; progress?: number; message?: string;
error?: {code: string; message: string}; canInstall?: boolean; installReason?: string;
checkedAt?: string; releaseUrl?: string;
lastInstall?: {state: 'complete' | 'failed'; version: string; at: string; message: string; code?: string};
}
import type { DesktopSpeechInput, DesktopSpeechEvent } from '../../web/ui/lib/desktopChatBridge';
export interface DesktopNotificationScope { connectionVersion: number; serviceId: string; identityId: string }
export interface DesktopNotificationEvent extends DesktopNotificationScope {
Expand Down Expand Up @@ -31,6 +38,7 @@ export type DesktopState = {
capabilities?: {version: number; host: string; entries?: Record<string, {supported: boolean; host: 'desktop-client' | 'service'; code?: string; reason?: string}>; [key: string]: unknown};
};
export interface DesktopBridge {
updates: {getStatus(): Promise<DesktopUpdateStatus>; check(): Promise<DesktopUpdateStatus>; install(): Promise<DesktopUpdateStatus>; onStatus(callback: (status: DesktopUpdateStatus) => void): () => void};
connectLocal(): Promise<DesktopState>;
consumeAnchor(id: number): Promise<void>;
getState(): Promise<DesktopState>; connect(origin: string): Promise<DesktopState>;
Expand Down
44 changes: 37 additions & 7 deletions apps/desktop/src/environment.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { randomUUID, createHash } = require('node:crypto');

function atomicJson(file, value) {
Expand Down Expand Up @@ -37,33 +38,62 @@ function desktopEnvironment({ appData, packaged, channel, root, workspace }) {
}

function serviceEnvironment(context, parent = process.env) {
// No inherited Python/Node injection, provider secrets, MUTEKI_* settings or
// native CLI homes. Tool binaries are discovered only in explicit locations.
const candidate = context.channel === 'candidate';
const hostHome = path.resolve(parent.HOME || parent.USERPROFILE || os.homedir());
// Candidate acceptance must not observe host PATH, HOME, or native logins.
// Regular desktop launches behave like a local app and discover host tools.
const env = {};
for (const key of ['SYSTEMROOT', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'DISPLAY', 'WAYLAND_DISPLAY', 'XDG_RUNTIME_DIR', 'DBUS_SESSION_BUS_ADDRESS', 'SSL_CERT_FILE', 'SSL_CERT_DIR']) {
if (parent[key]) env[key] = parent[key];
}
const hostPath = candidate ? [] : [
...(parent.PATH || '').split(path.delimiter).filter(Boolean),
path.join(hostHome, 'bin'), path.join(hostHome, '.local', 'bin'),
path.join(hostHome, '.bun', 'bin'), path.join(hostHome, '.kimi-code', 'bin'), path.join(hostHome, '.grok', 'bin'),
path.join(hostHome, '.volta', 'bin'), path.join(hostHome, '.npm-global', 'bin'),
...nodeVersionBinPaths(hostHome),
];
const standardPath = [path.join(context.paths.tools, 'bin'), '/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', ...(process.platform === 'win32' ? [path.join(parent.SystemRoot || 'C:\\Windows', 'System32')] : [])];
const runtimeHome = candidate ? context.paths.home : hostHome;
Object.assign(env, {
HOME: context.paths.home, USERPROFILE: context.paths.home,
XDG_CONFIG_HOME: path.join(context.paths.home, '.config'), XDG_CACHE_HOME: path.join(context.paths.home, '.cache'), XDG_DATA_HOME: path.join(context.paths.home, '.local', 'share'),
HOME: runtimeHome, USERPROFILE: runtimeHome,
XDG_CONFIG_HOME: candidate ? path.join(runtimeHome, '.config') : parent.XDG_CONFIG_HOME || path.join(hostHome, '.config'),
XDG_CACHE_HOME: candidate ? path.join(runtimeHome, '.cache') : parent.XDG_CACHE_HOME || path.join(hostHome, '.cache'),
XDG_DATA_HOME: candidate ? path.join(runtimeHome, '.local', 'share') : parent.XDG_DATA_HOME || path.join(hostHome, '.local', 'share'),
TMPDIR: context.paths.tmp, TMP: context.paths.tmp, TEMP: context.paths.tmp,
PATH: [path.join(context.paths.tools, 'bin'), '/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', ...(process.platform === 'win32' ? [path.join(parent.SystemRoot || 'C:\\Windows', 'System32')] : [])].join(path.delimiter),
PATH: [...new Set([...hostPath, ...standardPath])].join(path.delimiter),
MUTEKI_ENVIRONMENT_ROOT: context.root, MUTEKI_ENVIRONMENT_ID: context.id,
MUTEKI_ENVIRONMENT_CHANNEL: context.channel, MUTEKI_MANAGED_DESKTOP: '1',
MUTEKI_SESSIONS_ROOT: context.paths.sessions, MUTEKI_STATE_ROOT: context.paths.state,
MUTEKI_COORDINATOR_CONTROL_ROOT: path.join(context.paths.state, 'control'),
MUTEKI_COORDINATOR_GRAPH_ROOT: path.join(context.paths.state, 'control', 'graphs'),
MUTEKI_ENV_FILE: path.join(context.root, 'config', '.env'), MUTEKI_HOST_DISCOVERY: '0',
MUTEKI_ENV_FILE: path.join(context.root, 'config', '.env'), MUTEKI_HOST_DISCOVERY: candidate ? '0' : '1',
MUTEKI_WEB_BIND: '127.0.0.1', MUTEKI_UI_HOST: '127.0.0.1', MUTEKI_CONTROL_BIND: '127.0.0.1', MUTEKI_CONTROL_PORT: '0',
PYTHONDONTWRITEBYTECODE: '1', PYTHONNOUSERSITE: '1', PYTHONUNBUFFERED: '1',
NODE_ENV: 'production', NEXT_TELEMETRY_DISABLED: '1',
NPM_CONFIG_PREFIX: context.paths.tools, NPM_CONFIG_CACHE: path.join(context.paths.home, '.npm'),
});
if (context.channel !== 'candidate') env.MUTEKI_SHARED_CREDENTIALS_ROOT = context.sharedCredentials;
if (!candidate) env.MUTEKI_SHARED_CREDENTIALS_ROOT = context.sharedCredentials;
if (context.maintenance) env.MUTEKI_DESKTOP_MAINTENANCE = '1';
return env;
}

function nodeVersionBinPaths(home) {
const roots = [path.join(home, '.nvm', 'versions', 'node'), path.join(home, '.fnm', 'node-versions')];
const result = [];
for (const root of roots) {
let versions;
try { versions = fs.readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name).sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); }
catch { continue; }
for (const version of versions) {
const base = path.join(root, version);
result.push(path.join(base, 'bin'));
result.push(path.join(base, 'installation', 'bin'));
}
}
return result;
}

function runtimePaths({ packaged, resources, workspace, node = process.env.MUTEKI_DESKTOP_NODE || process.env.npm_node_execpath || process.execPath }) {
if (!packaged) {
return { code: workspace, python: path.join(workspace, '.venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python'), node,
Expand Down
72 changes: 56 additions & 16 deletions apps/desktop/src/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ if (updateJournal) {
const patchUpdate = patch => writeUpdateJournal(updateJournal, { ...JSON.parse(fs.readFileSync(updateJournal, 'utf8')), ...patch });
let desktopUpdates, applyUpdate = false, quitApproved = false;
let managedService;
function updatesController() {
if (!desktopUpdates) {
const {DesktopUpdates} = require('./updates.cjs');
desktopUpdates = new DesktopUpdates(app, environment, status => {
for (const record of windows.values()) if (!record.window.isDestroyed()) record.window.webContents.send('desktop:update-status', status);
});
}
return desktopUpdates;
}
function managedRuntime() {
if (!managedService) {
managedService = new ManagedService(environment, runtimePaths({ packaged: app.isPackaged && buildChannel !== 'dev', resources: process.resourcesPath, workspace }));
Expand Down Expand Up @@ -989,6 +998,30 @@ function installIPC() {
catch (error) { return { ok: false, error: transportError(error) }; }
});
handle('desktop:state', record => ({ ...record.state })); handle('desktop:connect', connect);
handle('desktop:update-status', () => updatesController().getStatus());
handle('desktop:update-check', () => updatesController().check());
handle('desktop:update-install', async () => {
const controller = updatesController();
if (preparingUpdate) throw new DesktopError('update.busy', '更新流程正在进行中。');
preparingUpdate = true;
try {
const ready = await controller.download();
if (ready.state !== 'ready') return ready;
const { response } = await dialog.showMessageBox({
type: 'question',
message: text(`版本 ${ready.latestVersion} 已验证,可以安装`, `Version ${ready.latestVersion} is verified and ready to install`),
detail: text('安装会等待当前任务结束、保存草稿并重启。日常数据将先备份。', 'Installation saves drafts, waits for active work to finish, backs up daily data, and restarts.'),
buttons: [text('稍后', 'Later'), text('应用更新', 'Install update')],
defaultId: 0,
cancelId: 0,
});
if (response !== 1) return controller.publish({ state: 'ready', message: '更新包已验证。' });
await applyPreparedUpdate();
return controller.publish({ state: 'installing', message: '正在退出并安装更新。' });
} finally {
preparingUpdate = false;
}
});
handle('desktop:connect-local', connectLocal);
handle('desktop:configure', record => { disposePreview(record); record.remote?.setVisible(false); emit(record, { configuring: true }); });
handle('desktop:resume', record => { if (record.scope && (!record.scope.closed || record.state.draftRecovery)) { record.remote?.setVisible(true); emit(record, { configuring: false }); } });
Expand Down Expand Up @@ -1187,28 +1220,35 @@ async function installCandidate() {
try {
const picked = await dialog.showOpenDialog({title: text('选择已构建的 Muteki 候选应用', 'Select a built Muteki candidate'), properties: ['openFile'], filters: [{name: 'Muteki', extensions: ['app']}]});
if (picked.canceled || picked.filePaths.length !== 1) return;
const { DesktopUpdates } = require('./updates.cjs');
desktopUpdates = new DesktopUpdates(app, environment);
desktopUpdates = updatesController();
const prepared = await desktopUpdates.prepare(picked.filePaths[0]);
app.setAsDefaultProtocolClient('muteki');
const {response} = await dialog.showMessageBox({type: 'question', message: text(`候选版本 ${prepared.version} 已通过启动验收`, `Candidate ${prepared.version} passed startup acceptance`), detail: text('应用更新会等待当前任务结束、保存草稿并重启。日常数据将先备份,开发数据不会导入。', 'The update saves drafts and restarts after current tasks finish. Daily data is backed up; development data is not imported.'), buttons: [text('稍后', 'Later'), text('应用更新', 'Apply update')], defaultId: 0, cancelId: 0});
if (response !== 1) return;
const records = [...windows.values()];
try {
for (const record of records) {
record.window.hide();
const saved = await persistenceBarrier(record, 'update');
if (!saved.persisted) throw new Error(saved.error || '草稿未保存,更新已暂停。');
}
if (managedService?.ready) await managedService.drain();
for (const record of records) record.allowClose = true;
applyUpdate = true; quitApproved = true; app.quit();
} catch (error) {
for (const record of records) if (!record.window.isDestroyed()) record.window.show();
throw error;
if (response !== 1) {
desktopUpdates.discardPreparedLocal();
return;
}
await applyPreparedUpdate();
} finally { preparingUpdate = false; }
}
async function applyPreparedUpdate() {
const records = [...windows.values()];
try {
for (const record of records) {
record.window.hide();
const saved = await persistenceBarrier(record, 'update');
if (!saved.persisted) throw new Error(saved.error || '草稿未保存,更新已暂停。');
}
if (managedService?.ready) await managedService.drain();
for (const record of records) record.allowClose = true;
applyUpdate = true;
quitApproved = true;
app.quit();
} catch (error) {
for (const record of records) if (!record.window.isDestroyed()) record.window.show();
throw error;
}
}
async function deepLink(value) {
let url;
try { url = new URL(value); } catch { throw new DesktopError('desktop.deep_link_invalid', '聊天链接无效。'); }
Expand Down
Loading
Loading