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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,34 @@ jobs:
- name: Verify current documentation
run: node scripts/check-docs.mjs

desktop-preview:
name: Desktop protocol journey
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm --filter @deepcode/desktop exec playwright install --with-deps chromium
- name: Exercise the desktop protocol fixture
run: pnpm --filter @deepcode/desktop test:e2e
- name: Upload browser diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: desktop-playwright-report
path: |
apps/desktop/playwright-report
apps/desktop/test-results
if-no-files-found: ignore
retention-days: 7

desktop-rust:
name: Desktop Rust check + test
runs-on: macos-latest
Expand All @@ -88,6 +116,17 @@ jobs:
- uses: actions/checkout@v6
- name: Show Rust toolchain
run: rustc --version && cargo --version
# Tauri validates every externalBin path in its build script. This job
# only compiles/tests Rust and never executes or packages the sidecar, so
# use a target-correct placeholder instead of copying a 100+ MB runtime.
- name: Prepare Tauri sidecar placeholder
run: |
target="$(rustc -vV | sed -n 's/^host: //p')"
runtime="apps/desktop/src-tauri/binaries/deepcode-runtime-${target}"
mkdir -p "$(dirname "$runtime")"
touch "$runtime"
mkdir -p apps/server/dist-sidecar
touch apps/server/dist-sidecar/app-server.cjs
- name: Check and test Tauri backend
run: |
cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml --locked
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ jobs:
- name: pnpm install
run: pnpm install --frozen-lockfile

- name: Prepare pinned Node sidecar runtime
env:
NODE_SIDECAR_VERSION: 22.23.1
NODE_SIDECAR_SHA256: ef28d8fab2c0e4314522d4bb1b7173270aa3937e93b92cb7de79c112ac1fa953
run: |
archive="node-v${NODE_SIDECAR_VERSION}-darwin-arm64.tar.xz"
curl --fail --location --retry 3 \
"https://nodejs.org/dist/v${NODE_SIDECAR_VERSION}/${archive}" \
--output "$RUNNER_TEMP/$archive"
echo "${NODE_SIDECAR_SHA256} $RUNNER_TEMP/$archive" | shasum -a 256 --check
tar -xJf "$RUNNER_TEMP/$archive" -C "$RUNNER_TEMP"
echo "DEEPCODE_NODE_RUNTIME=$RUNNER_TEMP/node-v${NODE_SIDECAR_VERSION}-darwin-arm64/bin/node" >> "$GITHUB_ENV"

- name: Set version
run: |
cd apps/desktop
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ yarn-error.log
# Test outputs
coverage/
.nyc_output/
apps/desktop/playwright-report/
apps/desktop/test-results/

# Electron build outputs (pre-Tauri pivot — kept for historical artifacts)
apps/desktop/release/
Expand All @@ -39,6 +41,8 @@ apps/desktop/dist-electron/
# Tauri build outputs (Rust target dir is large)
apps/desktop/src-tauri/target/
apps/desktop/src-tauri/gen/
apps/desktop/src-tauri/binaries/
apps/server/dist-sidecar/
# Note: Cargo.lock IS committed (best practice for applications)

# Release artifacts — too large for git; CI uploads to GitHub Releases instead
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Mac 客户端(v1 即将发布):拖入 Applications → 首启完成 onboar
| [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md) | 整体开发方案 v0.5(1500+ 行 / §3 模块 / §6 里程碑) |
| [docs/VISUAL_DESIGN.html](docs/VISUAL_DESIGN.html) | 视觉设计 v0.4(11 屏 mockup) |
| [docs/security-model.md](docs/security-model.md) | 威胁模型 + 防御层 + 攻击向量测试 + 已知缺口 |
| [docs/design/session-format-v1.md](docs/design/session-format-v1.md) | 统一 session JSONL、旧格式迁移与 writer ownership |
| [docs/design/sandbox-plan-worktree.md](docs/design/sandbox-plan-worktree.md) | sandbox × plan mode × worktree 关系矩阵 |
| [docs/design/plugin-security.md](docs/design/plugin-security.md) | plugin 信任 ladder + sandbox 子进程 |
| [docs/design/effort-levels.md](docs/design/effort-levels.md) | 5 档 effort 到 DeepSeek API 参数映射 |
Expand Down
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"start": "node ./dist/cli.js"
},
"dependencies": {
"@deepcode/app-server": "workspace:*",
"@deepcode/core": "workspace:*",
"@deepcode/shared-ui": "workspace:*"
},
Expand Down
9 changes: 9 additions & 0 deletions apps/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// M2: onboarding + REPL + slash commands + settings + permissions matcher.

import { CredentialsStore, VERSION, redact } from '@deepcode/core';
import { runAppServer } from '@deepcode/app-server';
import { homedir } from 'node:os';
import { resolve } from 'node:path';
import { runHeadless } from './headless.js';
Expand Down Expand Up @@ -80,6 +81,14 @@ async function main(): Promise<number> {
errOutput: process.stderr,
});
}
if (args.positional[0] === 'app-server') {
await runAppServer({
input: process.stdin,
output: process.stdout,
home: process.env.DEEPCODE_HOME ?? resolve(homedir(), '.deepcode'),
});
return 0;
}
if (args.positional[0] === 'trust') {
return runTrustCommand(args.positional.slice(1), {
cwd: process.cwd(),
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const SUBCOMMANDS = [
'doctor',
'upgrade',
'mcp',
'app-server',
'trust',
'plugins',
'skills',
Expand Down
22 changes: 13 additions & 9 deletions apps/cli/src/headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
EFFORT_PARAMS,
HookDispatcher,
ReadTool,
RuntimeHost,
SessionManager,
ToolRegistry,
WebFetchTool,
Expand All @@ -39,7 +40,6 @@ import {
loadSkills,
makeSkillTool,
resolveCredentials,
runAgent,
wirePlugins,
collectPluginContributions,
type AgentEvent,
Expand Down Expand Up @@ -271,25 +271,28 @@ export async function runHeadless(opts: HeadlessOpts): Promise<number> {
}
let exitCode = 0;
try {
const result = await runAgent({
const runtime = new RuntimeHost({
provider,
tools,
cwd,
mode,
permissions: settings.permissions,
hooks,
pluginDirs: pluginContrib.dirs,
autoMode: settings.autoMode,
sandboxConfig: settings.sandbox,
});
const result = await runtime.run({
systemPrompt,
userMessage,
history: [],
model,
maxTokens,
temperature,
maxTurns,
cwd,
signal: ctrl.signal,
session: { manager: sessions, id: session.id },
mode,
permissions: settings.permissions,
hooks,
pluginDirs: pluginContrib.dirs,
autoCompact: { contextWindow: contextWindowFor(model), threshold: 0.8 },
autoMode: settings.autoMode,
sandboxConfig: settings.sandbox,
// In headless mode there's no human to ask: auto-deny anything that
// would normally need approval. Users wanting auto-yes should pass
// --mode dontAsk or --mode bypassPermissions (gated by trust).
Expand Down Expand Up @@ -441,6 +444,7 @@ function formatEventText(out: Writable, e: AgentEvent): void {
return;
case 'usage':
case 'thinking_delta':
case 'model_step_complete':
case 'turn_complete':
return;
}
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/parse-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ USAGE
deepcode cron <cmd> Scheduled tasks: install/uninstall/list/status
deepcode scheduler run Run due scheduled jobs (invoked by launchd)
deepcode mcp serve Expose DeepCode tools as an MCP server (stdio)
deepcode app-server Run the experimental lifecycle server (JSONL stdio)
deepcode trust [--plan-only] Trust this directory's project config (hooks/MCP/...)
deepcode plugins list [--json] List installed plugins
deepcode plugins install <spec> Install a plugin (gh:owner/repo | name@npm | ./path)
Expand Down
58 changes: 29 additions & 29 deletions apps/cli/src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EFFORT_PARAMS,
HookDispatcher,
ReadTool,
RuntimeHost,
SessionManager,
TaskManager,
ToolRegistry,
Expand Down Expand Up @@ -37,7 +38,6 @@ import {
contextWindowFor,
makeSkillTool,
resolveCredentials,
runAgent,
settingsPaths,
wirePlugins,
collectPluginContributions,
Expand Down Expand Up @@ -429,6 +429,17 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
}

let history: StoredMessage[] = resolved.seededHistory;
const runtime = new RuntimeHost({
provider,
tools,
cwd,
mode,
permissions: settings.permissions,
hooks,
pluginDirs: pluginContrib.dirs,
autoMode: settings.autoMode,
sandboxConfig: settings.sandbox,
});
const ctx: SessionContext = {
cwd,
model,
Expand Down Expand Up @@ -471,25 +482,20 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
// reading ctx.model/ctx.mode live so /model and /mode switches are honored.
const tasks = new TaskManager((spec) => {
const ac = new AbortController();
const done = runAgent({
provider,
tools,
systemPrompt,
userMessage: spec.prompt,
model: ctx.model,
maxTokens,
temperature,
cwd: ctx.cwd,
signal: ac.signal,
mode: ctx.mode as Mode,
permissions: settings.permissions,
hooks,
pluginDirs: pluginContrib.dirs,
sandboxConfig: settings.sandbox,
autoMode: settings.autoMode,
subAgentDepth: 1,
systemReminders: false,
}).then((r) => assistantText(r.history));
const done = runtime
.run({
systemPrompt,
userMessage: spec.prompt,
model: ctx.model,
maxTokens,
temperature,
cwd: ctx.cwd,
signal: ac.signal,
modeOverride: ctx.mode as Mode,
subAgentDepth: 1,
systemReminders: false,
})
.then((r) => assistantText(r.history));
return { done, abort: () => ac.abort() };
});
ctx.tasks = tasks;
Expand Down Expand Up @@ -649,9 +655,7 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
}

// Otherwise: send to agent (with mode/permission/hooks gating from M3b)
const result = await runAgent({
provider,
tools,
const result = await runtime.run({
systemPrompt,
userMessage: userInput,
history,
Expand All @@ -663,13 +667,8 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
// ctx.sessionId (not the launch `session.id`) so a live `/resume <id>`
// switch redirects new messages to the resumed session.
session: { manager: sessions, id: ctx.sessionId },
mode: ctx.mode as Mode,
permissions: settings.permissions,
hooks,
pluginDirs: pluginContrib.dirs,
modeOverride: ctx.mode as Mode,
autoCompact: { contextWindow: contextWindowFor(ctx.model), threshold: 0.8 },
autoMode: settings.autoMode,
sandboxConfig: settings.sandbox,
// Session-scoped manager: the agent's TaskCreate calls land here too, so
// background tasks persist across turns and show up in /tasks.
taskManager: tasks,
Expand Down Expand Up @@ -762,6 +761,7 @@ function formatEvent(out: Writable, e: AgentEvent): void {
else out.write(` ✓ ${truncate(e.result.content, 200)}\n`);
return;
case 'usage':
case 'model_step_complete':
return;
case 'error':
out.write(`\n ✕ ${e.error}\n`);
Expand Down
6 changes: 5 additions & 1 deletion apps/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "../../packages/core" }, { "path": "../../packages/shared-ui" }]
"references": [
{ "path": "../../packages/core" },
{ "path": "../../packages/shared-ui" },
{ "path": "../server" }
]
}
23 changes: 16 additions & 7 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ src/ renderer(React + Vite,无 Tailwind,手写设计系统
screens/ About / MCPManager / Onboarding / Permissions /
Plugins / Repl / Sessions / Settings / Skills
components/ Sidebar / InspectorRail / ToolCard / UpdateBanner …
lib/ tauri-api(renderer↔Rust IPC 封装)· mac-agent ·
mac-tools · repl-stream · updater …
lib/ tauri-api(renderer↔Rust IPC 封装)· protocol-client ·
protocol-agent · repl-stream · updater …
src-tauri/ Rust 主进程
src/app_server.rs bundled runtime 启停、stdio 与 crash event
src/commands.rs #[tauri::command] —— renderer 通过 invoke() 调用
src/credentials.rs 凭据读写(原子写入)
src/credentials.rs 凭据保存与无密钥状态查询
src/settings.rs 设置持久化
src/tools.rs 工具实现
src/tools.rs legacy native helpers(renderer 仅暴露只读 file read)
src/lib.rs Tauri builder / 插件注册
tauri.conf.json 窗口 + 构建 + 打包配置
capabilities/ 权限能力声明
Expand All @@ -31,6 +32,11 @@ src-tauri/ Rust 主进程
renderer ↔ Rust 的 IPC 边界由 `src/lib/tauri-api.ts` 封装,契约测试见
`src/lib/tauri-api.test.ts`(#84)。

app-server 由 Tauri 作为 target-specific sidecar 监督。`apps/server` 会被打成单个
`app-server.cjs` resource,Node runtime 通过 `bundle.externalBin` 进入 `.app`;renderer 只能通过
Rust commands 与版本化协议通信,不能直接使用 shell plugin。provider、agent loop、tools、权限、
session materialization 和凭证明文都只存在于 sidecar;renderer 不再带有第二套运行时。

## 开发

依赖在 monorepo 根 `pnpm install` 一次装好;Rust 工具链 + Tauri CLI 见下。
Expand All @@ -40,25 +46,28 @@ renderer ↔ Rust 的 IPC 边界由 `src/lib/tauri-api.ts` 封装,契约测试
| `pnpm dev` | 仅 Vite dev server(5173)—— 一般由 Tauri 自动拉起 |
| `pnpm tauri:dev` | 完整 app:Tauri 启 dev server + 原生窗口,热重载 |
| `pnpm build` | `tsc -b` + `vite build` → `dist/`(renderer 产物) |
| `pnpm tauri:build` | 当前架构的 .app / .dmg |
| `pnpm tauri:build` | 构建包含 runtime + app-server 的 `.app` |
| `pnpm tauri:build:universal` | universal-apple-darwin 通用二进制 |
| `pnpm typecheck` | `tsc -b` |
| `pnpm test` | `vitest run`(lib 单测 + IPC 契约测试) |

`tauri.conf.json` 里 `beforeDevCommand` / `beforeBuildCommand` 分别接
`pnpm dev` / `pnpm build`,所以平时只跑 `pnpm tauri:dev` 即可。
`tauri.conf.json` 的 dev/build hooks 会先生成 app-server bundle 和目标 runtime,再启动 Vite 或
Tauri release build,所以平时只跑 `pnpm tauri:dev` 即可。

### 前置工具

- Node ≥ 22、pnpm
- Rust 工具链(`rustup`)—— Tauri 主进程是 Rust
- 通用构建需 `rustup target add aarch64-apple-darwin x86_64-apple-darwin`
- 通用构建还要求 `DEEPCODE_NODE_RUNTIME` 指向同时含 arm64/x86_64 的通用 Node binary

## 打包 / 签名

- 产物配置在 `src-tauri/tauri.conf.json`,公证 entitlements 在
`src-tauri/Entitlements.plist`。
- 签名 + 公证需要 Apple Developer ID 证书,以及 `APPLE_ID` /
`APPLE_APP_SPECIFIC_PASSWORD` 等环境变量(CI 走 secrets)。
- release CI 固定 Node 22.23.1,校验官方 SHA256 后才进入 Tauri 打包;nested runtime 先签,outer
`.app` 后签,再做 strict deep verification 与 notarization。

详见 `docs/DEVELOPMENT_PLAN.md` §4 / §4a / §4b。
Loading
Loading