跳转到内容

产品哲学

PicoAide Harness 不是「一堆功能的集合」,而是一组有明确边界的设计取舍。这一页把七条原则写清楚,并且每条都给出本仓库里可核对的证据(配置项、代码路径、可观察行为),最后列出这些取舍各自的代价——没有代价的取舍通常意味着还没想清楚。

判断标准很简单:如果一条原则在代码里找不到落点,它就不该出现在这一页。

一、一切皆插件:桌面本身就是插件

Section titled “一、一切皆插件:桌面本身就是插件”

原则:整个产品与上游走同一条 Cordis 插件组合路径,不另起一套插件系统、不做整体分叉。

证据

  • 上游以固定版本放在 deepseek-harness/(git submodule),真源是仓库根的 upstream.json(repository / commit / sourceVersion)。
  • 桌面壳是一个普通插件行:packages/host/desktop/cordis.patch.yml 用 - insert: 声明 desktop-shell(dsh-plugin-desktop)以及 desktop-diagnostics、desktop-updates、desktop-loop-notify 等行;企业能力同样是行:packages/host/enterprise/cordis.patch.yml 声明 picoaide-enterprise、picoaide-session、picoaide-auth-gate 等。
  • 界面能力通过上游的 slot 机制注入,服务能力通过 service contract 提供;桌面对第三方公开的只有 desktopRuntime.registerTrayItem / desktopActions 这类明确契约(packages/host/desktop/docs/plugin-services.md),窗口、托盘、打包器内部不是第三方 API。
  • 因为桌面壳是插件,「替换上游某一行」这件事是配置级的:packages/host/desktop/cordis.patch.yml 里既有 disabled: true(关掉不需要的上游行),也有整行 config 覆盖。

代价

  • 公开契约(slot、service、子路径导出)成为兼容面:改它们等于改 API,需要走决策记录与判据。
  • 上游默认值的变化会直接进入产品组合。升级时必须逐条复核「哪些行默认启用了、默认值是否翻转」,而不是假定「我们没改就不受影响」(docs/decisions/ 下的升级决策记录就是为这件事存在的)。

原则:凡是「可被滥用」的能力——密钥、模型目录、计量计费、内容授权、审计——一律收口到服务端;客户端只负责把它们画出来。

证据

  • 命名空间把两侧职责写死:server/internal/router/router.go 集中声明 /api/server/*(管理面,AdminAuth + RBAC 逐路由申报)与 /api/client/v2/*(员工面,Bearer),/v1/* 是模型网关。
  • 上游密钥只在服务端:server/internal/util/crypto.go 用 AES-GCM 加密(密文前缀 enc:v1:),主密钥是独立文件 master.key(0600,永不入库);客户端拿到的只是它自己的会话令牌,模型端点是 <服务端>/v1。
  • 计量与余额是硬闸门:server/internal/llmgateway/balance_gate.go 在余额不足时返回 429 BALANCE_EXHAUSTED(管理员豁免;读设置失败 fail-closed),未定价模型默认 429 MODEL_NOT_PRICED,可用策略开关显式放行;usage 与余额扣减同事务。
  • 审计是防篡改的:server/internal/serverstore/audit.go 写入带 prev_hash 的哈希链,篡改任意一行会打断后续所有链节。
  • 授权在服务端判定,且不泄露存在性:市场/组织内容未授权一律 404(server/internal/marketplace/ 的分类用例把「依赖故障」与「不存在」分开,后者必须保持 404 同形)。
  • 客户端没有本地模型配置入口:企业组合里 ui-settings-models 被 disabled: true,上游 llm-deepseek 行被禁用并替换为自研的网关 provider 行(packages/host/enterprise/cordis.patch.yml),请求头由网关令牌决定。

代价

  • 服务端不可达就没有模型:未登录时客户端没有可用的模型路由。这是「密钥只在服务端」的直接结果,不是故障。
  • 服务端成为必须运维的一层:账号、余额、模型目录都靠它,客户需要承担它的部署、备份与升级(见私有化部署)。
  • 客户端侧的一切「聪明」都要压制:任何本地豁免、本地定价、本地授权判断都会与这条原则冲突。

三、客户端只信任它登录的那台服务端

Section titled “三、客户端只信任它登录的那台服务端”

原则:客户端的外部信息来源只有一个——它当前登录的那台服务端;渠道身份由服务端在结构上决定,客户端不携带渠道身份。

证据

  • 更新源是会话派生的:packages/host/desktop/src/updates.ts 监听 pico/session-changed,从会话里取 serverURL;serverURL === null(未登录)时没有更新源,不做任何外发检查。换服务端会让在飞的检查与下载整份作废(代际计数),「已下载待安装」的安装包被撤回。
  • 清单与安装包来自同一台服务端:GET /api/client/v2/updates/manifest 与 /updates/client/* 都在服务端(server/internal/clientrelease/),客户端不访问更新服务器、不访问 GitHub。
  • 渠道对账是硬判据:清单里的 channel_id 必须存在,且必须与服务端 /api/client/v2/channel 自报的渠道 id 精确相等;不一致直接判清单非法、不重试。
  • 模型链路同样绑定会话:packages/host/enterprise/src/gateway-model.ts 把 baseURL 设为 ${session.serverURL}/v1;登录页、渠道内容、模型目录、能力中心与连接器目录都来自这台服务端。
  • 客户端与它登录的服务端绑定还体现在数据面:连接器凭据按(账号 + 服务端地址)作用域存储,换服务端不会复用上一台的凭据。

代价

  • 没有「官网直接下载客户端」这条路:安装包由服务端下发(客户端分发与升级),这也是公开渠道镜像里必须带 client/ 的原因。
  • 客户端无法被指向别处的更新源:想换版本来源只能换服务端,这是有意的(防止「客户端被洗成另一个渠道」这类品牌与数据根错配事故)。
  • 换服务端或换账号是「换一整套上下文」:在飞状态整份丢弃,用户要重新登录并重新授权连接器。

原则:任何「顺手就放行」的默认值都被反过来——文件效果、进程、出站、本地写面、渲染进程权限,默认都是拒绝,要放行必须有人或显式配置点头。

证据

  • 权限是两个旋钮(沙箱 + 审批)捆成的预设,上游 packages/interaction/permission-presets 的默认表只有 workspace-write(+ 审批 ask)与 danger-full-access(+ 审批 never),新会话默认就是前者;审批是逐次的,没有「总是允许」。
  • 沙箱按平台落地:Linux 走 Landlock、Windows 走目录 ACL、macOS 走 Seatbelt;Windows 上 workspace-write 需要先给工作区根写 DACL,拿不到就 fail-closed(该工作区每条命令都起不来),报错明说这是目录 DACL 而不是命令语法问题。
  • 本地写面要求持有性证明:packages/host/desktop/src/write-proof.ts 对非 GET 请求校验上游 connection 的 dsh-auth-* cookie,证明不足返回 403,证明机制缺席返回 503——绝不放行。
  • 连接器出站是 fail-closed 的:packages/host/connectors/src/outbound.ts 只允许 https(或回环 http),拒绝私网/链路本地/元数据地址,redirect: 'manual' 不跟随 3xx;域名解析不可验证(解析失败、超时、空答案)与解析到非公网同等拒绝,并把已校验的地址钉给连接使用,杜绝「判完了再解析一次」的 TOCTOU。
  • 渲染进程默认没有任何权限:主窗口的权限处理器对非本应用顶层文档一律拒绝,只对本安装回环源的顶层文档放行剪贴板(内嵌浏览器与应用窗口各有自己的权限面)。
  • 出站代理默认禁止:packages/host/desktop/src/network-policy.ts 忽略系统代理、代理环境变量、PAC 与 --proxy-server;退路只有渠道字段 desktop.allow_system_proxy 或真实进程环境变量 PICOAI_ALLOW_SYSTEM_PROXY=1。
  • 服务端侧同样是默认拒绝:项目内容未授权不可见(404 不泄露存在性),管理面每条路由都要申报权限点,登录限流只对失败计数(成功即清空)。

代价

  • 交互上更啰嗦:写文件、跑命令、操作浏览器都要确认;审批没有「记住这次选择」。
  • 部分环境需要显式配置才能用:Windows 上要给工作区目录正确的 DACL;「只有认证代理能出公网」的网络要打开代理逃生门;某些边界情况会故意失败(例如域名解析无法验证时拒绝连接器出站),而不是降级放行。
  • 排障时需要区分「拒绝」与「故障」:拒绝都有明确错误码与文案(如 403 持有性证明、429 BALANCE_EXHAUSTED、连接器拒绝原因),而故障是另一类信号。

五、数据留在客户自己的机器上

Section titled “五、数据留在客户自己的机器上”

原则:敏感数据的默认位置是员工机器与客户自己的服务器,厂商不在数据路径上。

证据

  • 本机数据只有一个权威目录:packages/host/desktop/src/desktop-home.ts 定义 Harness home(~/.picoaide-harness,DSH_HOME 优先,渠道不同则目录不同),会话、设置、工作区、技能都落在它下面;平台会检查它是否落在系统关键目录上,拒绝把会话令牌写进攻击者可读的位置。
  • 连接器凭据落在按(账号 + 服务端)分隔的作用域目录(packages/host/connectors/src/user-scope.ts),目录 0700、文件 0600、原子写入,防符号链接与路径逃逸。
  • 本地 Harness 服务只监听 127.0.0.1;会话正文、工作区文件、工具执行、沙箱状态、语音音频都在员工机器上。
  • 服务端只保存它必须知道的:账号、模型用量与费用、余额账本、内容与授权、审计事件。它不接触员工本机文件与会话日志。
  • 模型请求走客户自己的服务端到客户自己配置的上游 provider:上游 API Key 加密存在客户服务器上,请求内容由客户选择发给哪家 provider——厂商既不托管也不中转。

代价

  • 没有跨设备同步:换一台机器不会自动带着会话与工作区走(这是数据主权的直接代价)。
  • 员工机器上的数据要自己负责:本机磁盘损坏不会由服务端兜底;服务端侧的备份口径见升级、备份与回滚。
  • 想用云端模型就必须由客户自己配置出网与密钥;离线部署时要自己准备模型出口。

原则:凡是会真实改变 AI 行为或对外可见的写入,先进入待确认 / 待审核队列,由人或管理员拍板后才生效。

证据

  • 工具调用逐次审批:写文件、跑命令、操作浏览器、上传都要确认,批准只对这一次有效;沙箱拒绝与审批拒绝都会作为工具错误回到模型,模型看得见「被谁拒了」。
  • 能力分发是审核 + 授权双门制:员工上传技能/智能体后进入 pending,管理员 approve 之后还要对被授权人可见可装;未授权一律不可见;归属 = 首个成功占名的发布者。
  • 记忆是确认制:vendored 插件 packages/vendor/memory-evolve 的默认 reviewMode: 'suggest'——AI 通过 memory_suggest 提建议进待确认队列,用户采纳才写入;待办同理。
  • 自动化任务由人创建(时间规则 + 提示词,绑定原会话);模型只能通过显式工具(schedule_create / schedule_list / schedule_update / schedule_delete)在当前账号可见的任务集合里操作,看不到的任务按「不存在」处理;目录与投递都按账号隔离。
  • 应用侧的授权记录是显式的:允许 AI 读取应用数据的同意记录按(用户 + 服务端 + 应用)作用域存储,拿不到作用域就 fail-closed 拒绝写入。
  • 浏览器控制权只能由同一个按钮双向交接:「我来操作」取得、「交给 AI」交回;蒙版空白处、活动面板、Esc 都不会改变控制权。

代价

  • 多一次点击:AI 不能「自己变得更聪明」,记忆与技能演进要用户确认。
  • 有意的能力上限:模型无法绕过审批去「先做了再说」,也无法自行扩张自己的行为输入。
  • 需要区分「AI 提议」与「用户意志」:审批卡、待确认队列、审核状态就是这条界线,界面上必须始终可见。

原则:上游 DeepSeek Harness 以固定版本原样运行(当前 pin dsh-v0.2.1-rc.2,真源为仓库根 upstream.json);产品能力通过插件组合与少量带判据的补丁实现,不做整体分叉。

证据

  • 上游是 git submodule(deepseek-harness/),不在此分支上编辑;升级入口是 scripts/upgrade-upstream.mjs。
  • 补丁数量是少数且逐个登记:patches/ 下每个文件都是 <包名>@<上游版本>.patch,并由守卫脚本盯着——scripts/verify-patches.mjs(补丁是否真的生效)、scripts/check-patch-pin.mjs(补丁与 pin 是否对得上)、scripts/verify-patch-resolutions.mjs(resolutions 是否登记齐全)、scripts/patch-targets.mjs(补丁目标是否仍然存在)。
  • 每次上游升级都留一份决策记录(docs/decisions/ 下的升级决策),写明「哪些补丁要重切、哪些上游默认值必须显式关掉、哪些行为变化需要用户可见的说明」。
  • 上游行为默认值会被显式改写而不是听任:企业组合里禁用上游的模型设置行与本地 provider 行、桌面组合里关闭产品分析/遥测行与不需要的上游行,都有判据钉住(例如 verify-profile-boot.mjs 的 mustStayDisabled 名单与 cordis.patch.yml 双向对拍)。

代价

  • 升级是有工作量的事件:补丁要重切(目标是哈希命名的产物文件)、默认值要逐条复核、打包与运行时门禁要重跑。
  • 不能「顺手拿上游新功能」:新能力要么等升级、要么以插件实现。
  • 补丁本身是长期负债:每条补丁都带一条「为什么必须存在」的判据;上游合并了同样的修复时,补丁要被删掉而不是留着。

下表把上面七条原则落成具体取舍。「代价」一列是这个选择必须付的成本,不是待办事项。

取舍我们选择代价
交付形态客户端只信任登录的那台服务端,安装包随镜像分发没有公开下载站;换版本来源只能换服务端
密钥与计费上游密钥只在服务端、AES-GCM 加密;余额是唯一计费闸门服务端不可达时没有可用模型;客户必须自建并运维服务端
数据位置会话、文件、沙箱、音频留在员工机器没有跨设备同步;本机数据损坏不由服务端兜底
客户端体积随包 Electron + 固定版本 DSH + Node/pnpm/Python 运行时安装包以百 MB 计,换取「装上即用、零外网依赖」
升级方式后台静默检查与下载,安装一律用户显式触发升级比「静默重启安装」慢一步,换来不打断本机未保存工作
出站策略默认忽略系统代理与代理环境变量「只有认证代理能出公网」的网络必须显式打开退路
插件管理固定 desktop profile,第三方插件走用户补丁层没有 profile 切换与终端入口;改动必须重启才进 Loader 组合
内容与品牌品牌来自构建期注入的渠道内容,不在管理端编辑改一个文案 = 重新构建该渠道镜像
白标隔离每个渠道的应用标识、数据根、单实例锁互相独立跨渠道安装是并存而不是覆盖,员工机器上可能出现两个客户端
应用形态应用只在客户端窗口内打开,服务端不为应用准备公网入口或证书应用不能被浏览器直链分享,只能通过深链在客户端打开

这些取舍的落地操作在部署总览、客户端分发与升级与渠道与白标;每条原则对应的具体界面行为在桌面客户端与企业管控面。

  • 欢迎 —— 三层结构与支持 / 不支持清单
  • 快速开始 —— 从安装到第一条消息
  • 系统架构 —— 启动顺序、Host/Client 边界与打包
  • 常见问题 —— 按主题的行为与边界