Maka 核心技术解读:本地优先 Agent 工作台的 Runtime 内核、权限门控与崩溃恢复架构
【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka
本文基于 docs/archive/maka-core-tech-walkthrough.md(2026-06-25 生成、2026-07-13 归档的技术通读笔记)展开,并结合当前仓库源码、配置与测试用例进行验证与补充。面向工程师,帮助你理解 Maka 的 runtime 内核分层、四档权限模型、ledger 持久化恢复以及 Electron 集成边界,并掌握"崩溃可恢复、权限可控、诊断不影响主路径"这三条核心设计如何在代码中落地。
整体定位
Maka 是一个本地优先(local-first)的 Electron 桌面 AI agent 工作台。它的技术追求可以概括为一句话:让用户在自己的电脑上跑一个可观察、可控、可恢复的 agent——所有数据本地落盘,敏感值按各自边界保存在本地,工具调用走权限策略,单次对话崩溃后能从 ledger 恢复。
当前官方 README 将其定位为 "a high-performance agent workspace that keeps a complete record of everything it did"(高性能 agent 工作区,完整记录它所做的一切),这与下面要讲的 ledger + projection 架构完全对应:"完整记录"正是靠 run 事件日志(ledger)实现的。
仓库分层
monorepo 使用 npm workspaces,分层清晰:
| 包 | 定位 | 说明 |
|---|---|---|
packages/core | 纯契约层 | 零运行时依赖,只定义类型、纯函数与策略表(如 packages/core/src/permission.ts) |
packages/storage | 文件持久化层 | run ledger、原子写、session 消息 JSONL |
packages/runtime | 内核实现 | 最核心的部分,SessionManager / AgentRun / backend / ToolRuntime |
packages/ui | 共享渲染组件 | React 组件与 stories |
apps/desktop | Electron 壳子 | 把 runtime 装进窗口,提供 IPC、preload、OAuth、bot、gateway |
核心设计原则:契约与实现分离。runtime 内核再拆成可独立理解的边界(session → run → backend → model/tool),公共 API 保持稳定,内部可演化。例如契约层的permission.ts只声明类型与纯策略,而真正的运行时状态(requestId、parked Promise)由 runtime 层管理。
Runtime 内核架构
这是整个项目最核心的部分。分层关系如下:
SessionManager ← 对外公共 API(桌面 / bot / gateway 都走它) -> AgentRun ← 单次 turn 的生命周期 + 启动恢复 -> AiSdkBackend ← 流式 + 工具循环引擎 -> ModelAdapter ← provider stream / usage / error 归一化 -> ToolRuntime ← 工具输入校验 / 权限 / watchdog / abort / telemetry -> RunTrace ← best-effort 诊断 trace,写失败不影响对话 -> AgentRunStore ← durable run ledger关键代码入口(已按当前仓库核实):
| 模块 | 文件 |
|---|---|
SessionManager类 | packages/runtime/src/session-manager.ts(export class SessionManager,位于 905 行附近) |
AgentRun类 | packages/runtime/src/agent-run.ts(export class AgentRun,242 行) |
AiSdkBackend.send()流式泵 | packages/runtime/src/ai-sdk-backend.ts |
ToolRuntime权限门控 | packages/runtime/src/tool-runtime.ts(export class ToolRuntime,477 行) |
ModelAdapterprovider 适配 | packages/runtime/src/model-adapter.ts(export class ModelAdapter,145 行) |
RunTrace诊断 | packages/runtime/src/run-trace.ts(export class RunTrace,107 行) |
SessionManager:公共 runtime API
SessionManager是对外(桌面 IPC、bot adapter、open-gateway 三类入口)暴露的唯一 runtime 门面,职责包括:
- session CRUD
- backend registry 编排
- active run 查找
- 恢复入口(
recoverInterruptedSessions(),参见 packages/runtime/src/session-manager.ts 及测试 packages/runtime/src/tests/runtime-continuation-crash.test.ts)
真正干活的 turn 生命周期被委派给AgentRun。
AgentRun:单 turn 的 durable 编排
理解恢复机制的关键。一次sendMessage会创建一个AgentRun,它负责:
- 生成 runId,写
run_created事件到 ledger; - 追加用户消息、写初始 turn 状态;
- 锁定连接快照(
connectionLocked),防止 turn 跑到一半连接被改; - 构建 prior runtime context(从历史 run 的
RuntimeEvent投影成模型历史); - 驱动 backend 流事件,投影 session 状态;
- 写 turn 完成 / 失败 / abort / permission-wait 状态;
finalize()收尾:注销 active run、更新 header、写run_completed / run_failed / run_cancelled。
execute()是 async generator,核心结构(packages/runtime/src/agent-run.ts):
async *execute(): AsyncIterable<SessionEvent> { try { const begin = await this.begin(); for await (const ev of begin.backend.send(begin.backendInput)) { await this.recordSessionEvent(ev); yield ev; } } catch (error) { await this.recordFailure(error); throw error; } finally { await this.finalize(); } }每个 backend 事件都会被recordSessionEvent投影成 session 状态变更(running / blocked / aborted),并记录到 run ledger。即使进程崩溃,重启时也能从 ledger 重建。AgentRun还定义了AgentRunDurability = 'best_effort' | 'required'(packages/runtime/src/agent-run.ts 159 行),区分"尽力持久化"与"必须持久化"两种耐用性级别。
AiSdkBackend:流式 + 多步工具循环
agent loop 的引擎,用 Vercel AI SDK 的streamText配合stopWhen: stepCountIs(N)驱动多步工具调用循环——循环本身交给 AI SDK,但所有"自己的机器"(权限、持久化、materializer、watchdog)都保留在 Maka 这边。
send()的当前实现(packages/runtime/src/ai-sdk-backend.ts):
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> { const turn = this.openTurnScope(input); try { yield* turn.run(); } finally { this.activeTurns.delete(turn); await turn.close(); } }相比归档笔记中"单 turn 单后台泵"的描述,当前实现已演化为per-turn scope 模型:每次send()打开一个AiSdkTurn(openTurnScope),该 turn 拥有自己的 abort controller、自己的ToolRuntime、自己的 watchdog 与 run trace。这带来两个重要特性:
- 并发 turn 互不串扰:
stop()以广播方式停止所有 active turn,但每个 turn 以"自身"身份收尾,不再共享一个currentTurnId导致重叠 turn 被错误标记; - 清理顺序保证:teardown 对每个 scope 全部 settle 后才抛错——因为
endTurn是唯一能 reject 停在askUserQuestion上的工具的东西,abort signal 不会唤醒 registry,若中途 bail 会导致兄弟 turn 永远 parked(代码注释对此有详细说明)。
stop()支持mode: 'immediate' | 'after_step'两种停止策略,且会先abortHistoryCompact()中止手动历史压缩。
backend 层的精巧点还包括:
- StreamWatchdog:流式连接有 connect timeout 和 idle timeout,超时会 abort controller 并推 error 事件,防止挂死;工具执行期间 watchdog 会被暂停(见下文 ToolRuntime)。
- step cap grace:当
finishReason === 'tool-calls'(撞了 step 上限)且没有 assistant 文本时,注入一条确定性提示,告诉用户已达本轮工具上限、可发"继续"。 - prepareStep 动态工具加载:同 turn 内可以逐步激活更多工具(deferred load),active 工具集按 step 重算;
snapshotToolAvailability()(packages/runtime/src/ai-sdk-backend.ts)负责冻结 host tools 并注入 memory trigger 工具,且保留MEMORY_REMEMBER_TOOL_NAME/MEMORY_EXTRACT_TOOL_NAME两个保留名,防止用户工具冲突。
ToolRuntime:权限门控 seam
整个安全模型的执行点。每个send()会createToolRuntime(...)创建一个与 turn 身份绑定的ToolRuntime(packages/runtime/src/ai-sdk-backend.ts),其 scope 闭包保证"工具在 step 之后很久才 settle 时,仍能解析到本 turn 的 watchdog、trace 和 run"。工具调用经过的链:
- loop-gate:同一个 tool + 同一组 args 连续失败 N 次,直接 block,防止 agent 死循环。block 本身不记 outcome,streak 停在阈值,后续相同调用继续被拦。
- 权限评估三态:
allow→ 跑真实实现,写ToolResult;block→ 合成isError: true的工具结果返回给模型,不执行真实逻辑;prompt→ 推PermissionRequestEvent给 UI,await 一个 parked Promise,等用户决定。allow 则跑,deny 则合成"用户拒绝"。
- 工具执行期间暂停 stream watchdog(避免长任务被判超时),
finally里恢复。 - abort signal 透传进工具,stop 按钮能中断。
- 记录 telemetry、artifact candidate、错误分类。
此外当前 ToolRuntime 还接入了sandbox boundary 请求(AwaitRegistry<SandboxBoundarySettlement>),支持沙箱边界升级的异步等待与respondToSandboxBoundary()路由。
ModelAdapter:provider 归一化
把 provider / AI SDK 的细节挡在外面:stream chunk 类型、provider setup、usage 归一化、provider error mapping。
startStream统一调streamText;handleStreamChunk把各种 chunk 翻译成 Maka 的SessionEvent(text delta、thinking delta、complete 等);ModelAdapter还维护OpenAiChatReasoningTransportState,处理 OpenAI chat 推理通道的状态(packages/runtime/src/model-adapter.ts)。
这样未来加 provider 不需要重复权限 / 工具 / run / session 逻辑。
RunTrace:best-effort 诊断
记录 turn 的里程碑事件(RunTracePhase,见 packages/runtime/src/run-trace.ts 79 行附近):turn started、model resolved / stream started / completed / failed、tool started / completed / failed、permission requested / decided、usage recorded、abort requested。
关键约束:trace 写失败绝不影响用户对话。错误消息被截断到 2048 字符(REDACTED_ERROR_MESSAGE_MAX_CHARS),且它与 session 消息 JSONL 是分开的两套持久化。
权限系统
这是 Maka 区别于普通 chat demo 的核心安全设计,定义在 packages/core/src/permission.ts。注意:当前仓库中的权限模型相比归档笔记已有所演化,下面以当前源码为准说明。
权限模式(PermissionMode)
当前PERMISSION_MODES = ['explore', 'ask', 'bypass'](packages/core/src/permission.ts)。归档笔记中提到的execute模式已被退役:RETIRED_PERMISSION_MODES将execute折叠为ask——因为它从未有独立行为,编译结果与ask相同、显示为ask、产生相同的执行边界。为了兼容历史记录,decodePersistedPermissionMode()允许旧记录中的execute读回为ask,但新输入与线上值使用严格的isPermissionMode()检查。
| 模式 | 语义 |
|---|---|
explore | 只读探索:读工具放行,写与危险操作被 block,网络读取需要 prompt |
ask | 默认交互模式:读放行,写/危险/网络/浏览器均需用户确认 |
bypass | 全自动:全部 allow,仅用户显式选择时进入 |
工具类别(ToolCategory)
TOOL_CATEGORIES共 14 类(比归档笔记的 12 类多了computer_use与client_capability两类):
read, web_read, file_write, fs_destructive, shell_safe, shell_unsafe, git_destructive, network_send, privileged, browser, computer_use, client_capability, custom_tool, subagent类别名沿用 Claude SDK 术语,Pi adapter 必须把 Pi 原生工具名翻译成这些类别后再进入 runtime(见 packages/core/src/permission.ts 注释)。
BUILTIN_TOOL_CATEGORY把常见工具名映射到类别(Read/search_files/Grep/Glob→read;WebFetch/WebSearch→web_read;Write/Edit/apply_patch/patch→file_write;Bash/WriteStdin→shell_unsafe)。
mode × category 策略矩阵
契约层用PolicyDecision = 'allow' | 'prompt' | 'block'表示决策三态。虽然当前源码中策略矩阵的实现已迁移到运行时(runtime 层根据readExecutionBoundary/readPermissionMode计算,而不是 core 里一份静态PERMISSION_POLICY表),归档笔记总结的行为模型仍然有效,可帮助理解各模式的意图:
| 模式 | 读 | 写 | 危险操作 | 网络 | 浏览器 |
|---|---|---|---|---|---|
explore | allow | block | block | web_read=prompt | block |
ask | allow | prompt | prompt | prompt | prompt |
bypass | allow | allow | allow | allow | allow |
核心规则依然成立:不可逆操作(fs_destructive、git_destructive、privileged、browser)在任何非 bypass 模式下都强制 prompt——浏览器操作可能发帖/下单,视为不可逆。bypass全 allow,仅用户显式选择时进入。
Shell 命令分类:从"安全降级"到"fail-closed"
这是当前源码相对归档笔记最重要的演化。归档笔记描述categorizeBash()可根据命令前缀动态降级/升级(ls/pwd→shell_safe,rm→fs_destructive,git push --force→git_destructive)。当前源码明确移除了shell_safe的产出路径:
There is no SAFE_SHELL_PREFIXES allowlist: a shell command cannot be proven safe from its string... Eight review rounds of enumerating dangerous shapes proved the futility of the inverse (deciding a Turing-complete shell's runtime effect from a static string is undecidable). So
categorizeBashnever returnsshell_safe; read-only needs go through typed tools (Read/Glob/Grep — fixed argv, no shell), and every shell command is at leastshell_unsafe→ prompt.
也就是说:从静态字符串证明 shell 命令安全是不可判定的(echo "$(rm x)"、PowerShell 的$(...)、反引号、iex都能在参数里藏执行),因此不再允许任何 shell 命令自动放行——只读需求走固定 argv 的 typed tools(Read/Glob/Grep),其余 shell 命令至少shell_unsafe→ prompt。类别划分仍然保留,但作用从"安全边界"降级为"让确认原因(REASON)更准确"(delete vs elevate vs generic),漏配只是措辞问题而非绕过漏洞。
相应的分类器以纯函数 + 正则表实现(packages/core/src/permission.ts):
PRIVILEGED_SHELL_PREFIXES(sudo、su、chmod、chown、mount、kill、systemctl、shutdown、reboot 等)与PRIVILEGED_SHELL_PATTERNS(PowerShell/cmd 等价物:stop-process、-verb runas、service 控制、icacls/takeown等,大小写不敏感);FS_DESTRUCTIVE_PATTERNS(dd、truncate、shred、mkfs、find -delete、rm家族、remove-item/clear-content等),锚定在每个语句段的开头(commandSegments按|;&\n(){}切分),而不是整个命令开头;PIPE_DESTRUCTIVE_PATTERNS(| xargs rm/shred/truncate/dd、| sh/bash/zsh);DESTRUCTIVE_GIT_PATTERNS(git reset --hard、git push --force/-f、git branch -D、git clean -fd?、git checkout .、git rebase -i)。
切分策略刻意"quote-naive"(引号感知切分会更糟):$( )与反引号在双引号内也会展开,所以 naive 切分永不丢内容、只会切碎,多出的边界只增加扫描候选,不会隐藏危险片段。
纯函数原则
preToolUse()给定输入必然返回相同结果,不生成 UUID(requestId 由 runtime 层的权限引擎生成),便于测试。
parked Promise 机制
权限引擎为每个 outstanding 权限请求维护一个 parked Promise,UI 的决定通过respondToSandboxBoundary()/respondToUserQuestion()(packages/runtime/src/ai-sdk-backend.ts)resolve 回等待中的 adapter。turn 结束时未回答的请求会被 reject 成user_stop。如前述,endTurn是唯一能 reject 停在askUserQuestion上的工具的地方,abort signal 不会唤醒 registry——这保证了 stop 时不会留下永久悬挂的 parked 请求。
持久化与恢复
文件布局
工作数据放 ElectronuserData下的 workspace 目录:
<Electron userData>/workspaces/default/ llm-connections.json credentials.json settings.json sessions/<sessionId>/ messages.jsonl runs/<runId>/ run.json ← run header(原子写) events.jsonl ← append-only run 事件AgentRun ledger
存储层在 packages/storage/src/agent-run-store.ts,对应测试见 packages/storage/src/tests/atomic-file-write.test.ts。核心保障:
run.json原子写(writeAtomicFile先写临时文件再 rename,测试验证"写出精确字节且不残留临时文件");events.jsonlappend-only;- 同 run 写串行化(per sessionId+runId 的 Promise 链),避免并发写竞争;
- ID 校验
SAFE_ID_PATTERN,防路径穿越; - 读事件时容忍损坏行(
event_corrupt)和未终止尾部。
这套 ledger 和用户可见的 session 消息 JSONL 分开,诊断 / 恢复状态不污染对话历史。此外还配套了agent-run-inspect.ts/execution-inspect.ts(core 层)提供 run 审计文档(schemaVersion、source health、projection summary、compaction checkpoint 等)。
启动恢复
recoverInterruptedSessions()优先用 run ledger:扫描持久化的 run header 和事件,分类非终态 run(created/running/waiting_permission),修复后收敛 session / turn 投影。恢复入口的严格版本recoverInterruptedSessionsStrict()在 packages/runtime/src/tests/runtime-ledger-repair.test.ts 中有覆盖(对 pending invocation 进行 run ledger 修复断言)。
覆盖的恢复场景:
- stale 的
created/runningrun; - 停在
run_started/model_stream_started的 run; tool_started后的残留;permission_requested后的等待;- 缺终态事件的
model_stream_completed; - 损坏事件行。
没有 run ledger 的老 session 走旧的 message + turn-state 恢复路径。相关回归测试还包括 packages/runtime/src/tests/runtime-continuation-crash.test.ts、packages/runtime/src/tests/sandbox-boundary-restart-recovery.test.ts 与 packages/runtime/src/tests/runtime-handoff.test.ts。
Electron 集成层
进程边界
| 进程 | 文件 | 职责 |
|---|---|---|
| main | apps/desktop/src/main/main.ts | 装配SessionManager、注册 IPC、管理窗口、OAuth、bot、gateway |
| preload | apps/desktop/src/preload/preload.ts | contextBridge暴露window.maka.*白名单 API |
| renderer | apps/desktop/src/renderer/ | React UI + Settings |
main 进程装配runtime = new SessionManager({...})并启动 gateway 服务;IPC 用ipcMain.handle注册大量通道(memory、artifacts、settings、connection、session 等)。
密钥安全边界
- Provider/API key、bot token、proxy password、gateway token 等写入本机凭据文件,依赖 OS 账号边界和文件权限;subscription OAuth token 使用独立的系统安全存储;
- Renderer 永远拿不到明文密钥,Settings 只显示 masked 状态和测试结果。
多入口
除了桌面 UI,runtime 还服务两类外部入口:
- Bot adapter(packages/runtime/src/bots/):Telegram、飞书、企业微信、微信 iLink、Discord、钉钉、QQ 等,统一对接 SessionManager;
- Open Gateway:本地 HTTP / SSE API,用 token 保护,让外部读取会话状态 / 事件 / 能力 / 健康摘要。
数据流:一次对话的完整路径
- 用户在 renderer 输入消息 →
window.maka.session.send→ IPC →SessionManager.sendMessage; - 创建
AgentRun,写run_created到 ledger,append 用户消息到messages.jsonl; - 从历史 run 的 RuntimeEvent 投影模型上下文;
AiSdkBackend.send():打开 turn scope → 构建权限包裹的工具 →streamText(stepCountIs(N))→ 后台泵 fullStream;- 每个流 chunk 经
ModelAdapter.handleStreamChunk归一化成SessionEvent→ 推 queue → 前台 yield → IPC 流到 renderer; - 模型调用工具 → 权限门控链 → loop-gate → 权限评估三态(allow / block / prompt)→ 执行、合成错误或等待用户决定;
- assistant 文本落
messages.jsonl,usage 记 telemetry,trace 写 run ledger; finalize()收尾,写run_completed,更新 session header。
技术亮点小结
- ledger + projection 模式:run 事件是 source of truth,session 状态是投影,崩溃可重建——整个恢复能力的根基;
- 纯函数策略 + runtime 包装:权限决策表是纯函数好测试,requestId / parking 状态由 runtime 管,职责分明;
- 权限门控作为工具 execute 的 seam:不动 AI SDK 的循环,在 execute 回调里插入 allow / block / prompt,最小侵入;
- fail-closed 的 shell 分类:承认"静态判定 shell 安全性不可判定",取消
shell_safe自动放行,只读走 typed tools,类别只用于精确确认原因; - best-effort trace 不影响主路径:诊断信息尽力写,失败静默,对话绝不受拖累;
- 同 run 写串行化 + 原子写:文件持久化的并发安全靠 Promise 链和 rename 保证;
- per-turn scope 模型:每次 send 绑定独立的 watchdog / ToolRuntime / trace,并发 turn 互不串扰,stop 与 teardown 顺序有严格保证;
- 安全分层:模式 × 类别矩阵 + 命令分类器 + 凭据文件权限 / subscription token 安全存储 + preload 白名单,多层防御。
延伸阅读:本笔记已归档(2026-07-13),当前 backend 体系的最新指引以仓库根目录 ARCHITECTURE.md 及其架构章节、源码和测试为准。
【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考