mcpField
A
Appcrane

Appcrane

远程服务
github.com/gitayg开发工具7今天更新
代码仓库

关于 Appcrane

AppCrane 是部署在自有服务器上的可自托管 PaaS,面向需要运行 AI 构建应用并审计其操作的团队。它让 Agent 通过 MCP 管理部署生命周期,同时提供 SSO、按应用角色和区分 Agent 与人工的审计记录。

主要功能

  1. 01通过 MCP 创建、部署和回滚应用
  2. 02管理应用密钥并查看日志
  3. 03SSO 与按应用角色控制
  4. 04区分 Agent 和人工的审计记录
  5. 05单机部署应用

适用场景

  1. 01让编码 Agent 部署内部应用
  2. 02追踪部署操作的执行者
  3. 03在自有服务器运行团队应用

MCP 相关

AppCrane 通过 MCP 暴露应用管理与部署工具,供 Claude Code、Cursor、Codex 等 MCP 客户端操作。服务端还可提供 onboarding 和 operations 操作指南。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://YOUR-SERVER/api/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

全部服务地址
Streamable HTTPhttps://YOUR-SERVER/api/mcp需要授权

工具

详细介绍

原文在翻译后有更新,译文可能不是最新。

AppCrane

可自托管的 PaaS,由 Agent 负责操作,平台留存操作记录。

GitHub stars License: AGPL v3 Platform: Ubuntu 22.04+ npm: appcrane-mcp

appcrane.dev · 比较优势 · 从 Claude 部署 · MCP 连接器 · 更新日志

AppCrane 可在你拥有的服务器上运行团队使用 Claude Code 或 Cursor 构建的内部应用。Agent 可通过 62 个 MCP 工具创建应用、部署应用、读取日志和回滚,无需浏览器或 curl;平台则会强制实施 SSO 和按应用划分的角色权限,并将每项操作记录到执行者名下(标记为 agent 或 human),同时确保管理服务器的人员无法访问应用密钥。

它面向必须自托管的团队,例如出于数据驻留要求、客户合同规定或仅限内部访问的网络环境;这些团队仍需要回答:是谁部署了这个应用、里面有什么,以及能否撤销这次部署?

这里真正与众不同之处

自托管 PaaS 在 2026 年补齐了治理能力。Coolify 推出了结构化审计日志和原生 OIDC;Dokploy 推出了 SSO、SCIM、自定义角色和审计日志。Komodo 提供细粒度的逐资源权限和完整审计追踪的时间,比这两者都长。过去在这方面的许多差异化优势如今已不复存在,以下说法都是经过核对其文档后仍然成立的。还有四点值得关注:

1. 治理能力包含在开源版本中,无需许可证密钥。 SAML 2.0、OIDC、SCIM 配置、逐应用角色和审计日志都包含在 AGPL-3.0 版本中,无需启用任何功能。Dokploy 提供同类能力,但属于企业版,需要许可证密钥才能使用。Coolify 的相关功能也是免费的,但其变更日志列出了 OIDC 和审计日志,没有 SAML 或 SCIM。Komodo 的相关功能同样免费——采用 GPL-3.0,内置逐资源权限和审计追踪——但其文档所述的登录方式是用户名/密码和 OAuth(GitHub、Google、通用 OIDC),文档中没有 SAML 或 SCIM。因此,免费与付费的区别实际上只存在于 Dokploy;与 Coolify 和 Komodo 相比,差异在于涵盖了哪些企业目录协议,而不是启用这些功能是否需要付费。

2. 内置 Agent 接口可以执行变更。 Coolify 的实例级 MCP 服务刻意设计为只读——只有 10 个 list/get 工具。AppCrane 的 59 个工具中包括 appcrane_deploy、appcrane_rollback、appcrane_promote、appcrane_set_secret 和 appcrane_grant_app_access。Dokploy 的官方 MCP 软件包也支持写入,规模大得多(49 个类别,共 508 个工具)——AppCrane 的工具范围较小是有意为之,而非能力不足;它还搭配了 appcrane_get_guide(topic="onboarding"|"operations"),可从服务端提供当前操作手册,让 Agent 阅读操作流程,而不是从工具列表中自行推断。Komodo 没有自带 MCP 服务;其代码库和文档中都没有相关内容,现有的几个服务都是基于其 REST API 的第三方封装。

3. 审计日志能区分 Agent 和人员。 每条记录都有 actor_kind 字段,因此查询一次就能知道“上周 Agent 在这台机器上做了什么”。其他产品会记录用户身份——Komodo 的审计追踪会记录“由谁在何时创建”(简介)——但它们的文档都没有说明如何区分自动化执行者和人工用户。

4. 操作员无法读取应用密钥。 环境变量的访问权限遵循应用分配规则,platform_admin 也不例外——负责安装和更新平台的角色,无法读取其未获分配应用的明文内容。通过 HTTP 和 MCP 这两种入口查看密钥时,都会受到频率限制并记录审计日志,因此切换传输方式也不会获得新的查看额度。Komodo 文档明确说明了相反的机制:将变量标记为密钥后,非管理员用户无法访问其值,也就是说,管理员可以读取该值。

还有四点虽然不常见,但值得根据自身需求评估,而非只看标题:已部署应用的按租户隔离数据;无仓库上传通过对接收字节在服务端计算 SHA-256 来识别,而不是依赖自行报告的提交 SHA;按收件人发送的每日漏洞摘要——平台管理员收到全平台摘要,应用所有者只收到自己应用的摘要,因此摘要不会泄露其他哪些应用存在风险;以及托管仓库,让 Agent 能为完全没有 GitHub 账户的用户创建并发布应用。

与厂商托管的受治理平台(Replit、Lovable、Retool、Superblocks)相比,取舍一如既往:它们的治理能力更加成熟,而你的应用数据、数据库连接和 API 密钥都存放在它们的基础设施上。

与自托管 PaaS 对比
AppCraneCoolifyDokployKomodoCapRover / Dokku
多主机 / 集群部署不支持 — 单主机支持(实验性)支持,远程服务支持,每台主机运行一个 AgentSwarm 集群(CapRover)
可用于部署的内置 MCP62 个工具,包括回滚10 个工具,只读508 个工具(官方软件包),包括回滚仅社区项目
SAML 2.0支持更新日志中未提及Enterprise未记录不支持
OIDC支持支持(v4.4-rc.1)Enterprise支持,通用 OIDC不支持
SCIM 配置支持更新日志中未提及Enterprise未记录不支持
审计日志支持,可区分 Agent 操作和人工操作支持,结构化日志(v4.1.0)Enterprise支持,完整变更记录不支持
治理功能是否需要付费不需要不需要需要不需要不适用
操作员无法读取应用密钥支持未记录未记录不支持 — 管理员可以读取不支持
应用的租户间数据隔离支持未记录未记录未记录不支持
无仓库上传的部署标识服务端 SHA-256未记录未记录未记录不支持
核心许可证AGPL-3.0Apache-2.0Apache-2.0 + 付费 EnterpriseGPL-3.0开源

第一行是 AppCrane 完全不具备的能力。Komodo 的核心设计是集群管理:一个 Core Web 服务,加上每台已连接机器上运行的无状态 Periphery Agent,并且“可连接的服务器数量没有限制”,还支持 Docker Swarm 管理,以及从 git 仓库声明式同步资源。AppCrane 没有 Agent、主机注册表或远程执行路径——它只会在安装所在的机器上部署容器,这就是它的完整设计。Coolify 和 Dokploy 也都支持管理其他服务器,CapRover 则通过 Docker Swarm 加入节点。

CapRover 和 Dokku 放在同一列,是因为它们的访问控制模式本质相同:CapRover 使用单个管理员账号;Dokku 则通过 SSH 密钥管理,密钥名称中含有 admin 一词即可获得密钥管理权限。前者将多用户访问明确列为不在范围内,后者则依赖一个未经审计的社区插件。对于由一名操作员管理的单台服务器来说,这种设计合理;但不适合在它前面接入身份提供商(IdP)。

根据各项目在 2026 年 9 月的官方文档和更新日志核对。**“未记录”**表示文档中没有提到该功能——这不能证明该功能不存在;供应商页面只是声明,并非测试结果。对于任何关键能力,请在自己的安装环境中验证。

坦诚说明适用范围。 Coolify 的模板市场大得多,社区规模也大得多,而且支持多服务器编排;如果你想一键部署 Postgres 和数百种应用模板,请使用 Coolify。Dokploy 的 API 覆盖范围比 AppCrane 更广,并且提供带 SLA 的支持。只要需求涉及多台机器,Komodo 就是更好的选择——无论是管理一组主机、Swarm 集群、将构建任务分配给按需实例,还是从 git 声明式同步配置;AppCrane 只能部署到自身运行的服务器,除此之外无处可部署。因此,多主机环境不是这个产品的简化版,而是另一种产品。如果只有一个人操作服务器,Dokku 和 CapRover 更简单、更轻量。当应用由 Agent 构建、Agent 应负责部署、所有内容都部署到你拥有的一台服务器上,并且之后有人可能会要求你证明是谁做了什么时,就选择 AppCrane。

为何此刻很重要。 2026 年发生了三件事:

  • 瓶颈已从编写软件转向运行软件。 在 Anthropic 的 Claude Code 研究(约 40 万次会话)中,“运行软件”——部署、配置、运行流水线——所占比例从 14% 升至 21%,而修复损坏代码的比例则从 33% 降至 19%。非工程师如今交付可部署代码的能力与专业工程师相差仅 7 个百分点。稀缺的已不再是应用,而是安全运行应用的场所。
  • 影子 AI 已变得可衡量。 2026 年 Verizon DBIR 报告称,影子 AI 检测量增加了 4 倍;一年内,公司设备上的 AI 使用率从 15% 升至 45%,其中 67% 通过非公司账户进行;源代码则是最常提交的数据类型。禁令只会让情况更糟;有效的解决方案是获批的平台。
  • 以控制台实施治理是失败之道。 如果平台要求每个应用都由人工点击审批界面才能通过,那么当应用数量达到数百个时,流程就会停滞。AppCrane 的解决方案截然不同:由 Agent 通过 MCP 推进受治理的生命周期,平台负责记录并约束这一流程,而不是由人员介入每个步骤。

功能

  • Docker 容器隔离 — 每个应用都在自己的容器中运行;没有共享依赖,也不会出现失控进程
  • 托管数据库 — 为每个应用配置 PostgreSQL、MariaDB、MongoDB 和 Redis,并将凭据注入应用实际读取的任意环境变量名下。Postgres、MariaDB 和 Mongo 的每种引擎由所有应用共用一个服务器,并在引擎内部实施隔离——Mongo 用户仅限访问自己的数据库,listDatabases 只返回该数据库;这比 Postgres 更严格,因为 Postgres 的 pg_database 会泄露所有数据库名称。Redis 则为每个作用域分配一个容器,因为它的 ACL 无法限制对某个编号数据库的访问:固定到 db 1 的用户仍可通过 COPY ... DB 和 MOVE 访问 db 2。Mongo 以单节点副本集运行,因此变更流和事务都能正常工作
  • 每个应用各自的容器命令和卷 — 应用可以声明镜像所需的 argv(["start-dev"])以及实际需要持久化的路径。命令采用 argv 数组,绝不使用 shell 字符串,因此已存储命令中的内容无法变成第二个参数。每次重新部署都会停止并重建容器,但已声明的路径会保留下来;目录中的 23 个条目都带有从各自镜像实测出的路径,因此已安装的应用无需任何人编辑配置,就会开始保留其状态。新建挂载会先用镜像中的内容填充,因为 bind mount(与命名卷不同)会遮蔽镜像在该路径提供的内容:如果不先填充,声明 /var/www/html 就会给应用一个空目录,而原来的 7,747 个文件都会消失
  • 托管应用的仓库位于 AppCrane 主机上 — 新托管应用的代码是 DATA_DIR/repos 下的 bare git 仓库,而不是 GitHub 仓库:无需服务账号,代码也永远不会离开这台机器。仓库通过 git plumbing 写入(不使用 worktree),与主机的 git 配置和钩子隔离;如果两个推送发生竞争,则以最新推送为准——先到达的推送会存放在 refs/dropped/ ref 下,向推送者报告,并包含在备份中,因此可以恢复。推送也可以删除文件:appcrane_push_to_managed_app 接受 deletions 列表,其中包含相对于仓库的路径,这些路径会与写入内容一起应用到同一提交,因此一次调用仍只产生一个提交(纯删除时传入 files: [],并从响应的 deleted 中读取已删除路径)。如果分支没有某条待删除路径,则会按路径名拒绝,而不是提交一个空操作——在 git plumbing 层面,这种情况不会报错;同一推送中不得同时写入和删除同一路径;如果推送会导致仓库中一个文件都不剩,也会被拒绝。下文的 .env* 规则同样适用于删除和写入。GitHub 托管应用不支持此功能:GitHub 的 contents API 无法表示删除,因此删除会被拒绝,而不会悄悄忽略。部署会从本地仓库克隆,部署前提交检查也会直接读取本地仓库。如果该环境已为应用开启自动部署(使用 GitHub webhook 的相同标志和分支筛选器),推送会自动部署;检查更新会与本地分支进行比较。Ask Claude 通过只读文件列表、读取和搜索功能,基于仓库本身作答,并固定到单个提交——不会启动容器;它需要服务器的 ANTHROPIC_API_KEY(这些应用不会使用每个应用的订阅凭据)。AppStudio 会分析仓库,并将结果作为新请求提交,而不是编写代码。编码会话仅适用于这些应用:工作区会从本地仓库克隆,不带任何凭据;如果应用由 GitHub 托管,则会给出明确原因并拒绝,而不是创建一个无法发布其成果的会话。AppCrane 不再将编码工作区推送到 GitHub 远程仓库。会话带有 UI:Sandbox 标签页上应用的顶部栏会出现 Coder 按钮(编码 Agent 的工作在此审核和发布;切换到 Production 会关闭会话),点击后会在运行中的应用旁边打开聊天面板,而不是覆盖应用;元素选择器会将你指向的内容传入提示词。对于无法运行的应用,按钮会变灰并说明所需条件。Changes 标签页会逐个文件列出 Agent 修改的内容及差异,你只发布勾选的内容。右键单击环境标签会显示 Redeploy;在 sandbox 环境中还会显示 Promote to Production——两者都会先要求确认,并且只对服务器会接受的用户显示:应用所有者或平台管理员。每条消息都可以从服务器提供的列表中单独选择模型(因此选择器不会与允许使用的模型不一致),你可以在某一轮运行时继续输入:后续消息会在服务器端排队,显示为待处理状态,刷新页面后仍会保留,也可以取消——停止某一轮会清除排队的消息,而不会应用为已放弃的工作编写的指令。发布是一项选择,而不是全部扫入——GET /api/coder/:slug/session/:id/changes 会列出 Agent 修改的内容,每个文件都附有差异;POST .../release 只接受你指定的路径,将其作为单个提交推送到本地仓库,并由常规自动部署流程部署到 sandbox。Agent 删除的文件会作为删除操作发布,因此重命名只需一个提交,不会留下过时文件。发布要求应用管理员权限,与对托管仓库的其他写入操作相同;可以与 Agent 对话的人不一定能发布其工作。当空闲容器被清除时,所有尚未发布的内容都会先提交到 agent/<userId>/<sessionId>——这是安全网,而不是发布:如果该分支与推送时将要部署的分支相同,则会拒绝创建,因此不能因为用户离开而让未经审核的工作进入 sandbox。Agent 的对话也会保留,因此再次打开会话时可以接着继续,不必从头开始;即使空闲容器被清除或 AppCrane 重启,也一样。现有 GitHub 托管应用会自动迁移:升级后首次启动时,在 AppCrane 开始提供服务之前,每个应用都会被获取(仅分支和标签)到暂存副本中;只有每个分支和标签都与 GitHub 完全一致时,才会切换到本地仓库;否则应用会继续使用 GitHub,并记录原因。应用会逐个迁移;每个应用的 git 操作都有单独的时限,整个迁移过程也有总时限(未轮到的应用会在下次启动时重试);失败不会阻止启动;GitHub 令牌通过环境传给 git,绝不会放进 URL、配置文件或命令行。迁移期间,托管应用无法登录。平台管理员可以通过 GET /api/github-service/repo-migration 查看每个应用的迁移结果;设置 APPCRANE_REPO_MIGRATION=off 可跳过迁移。
  • 上传的应用会自动转为 Crane 托管 — 升级后首次启动时,在托管仓库迁移之后、AppCrane 开始提供服务之前,每个通过上传包部署的应用都会逐个转换为 Crane 托管应用,不会触发部署,也不会碰触容器。其仓库会有一个包含 Production 当前运行版本的提交;如果 Sandbox 运行的是不同版本,则再在其上添加第二个提交。随后,每个环境的在线部署都会指向相应提交(上传包的 SHA-256 会保留在提交说明中),因此检查更新和升级都可以基于仓库进行。任意层级的 node_modules 和 .git 都不会放入仓库;符号链接会作为符号链接提交(绝不会跟随;指向发布目录外部的链接会被排除),每个应用排除的路径都会列出。所有 .env* 文件,无论位于根目录还是嵌套目录,都会保留,但绝不会放入 git:文件内容会加密存储在 AppCrane 数据库中(因此会包含在数据备份里),并在每次部署时写回对应环境的发布目录,保留原始路径和文件模式,并在构建前完成,因此 VITE_* 等构建时密钥会像从上传包读取时一样传给构建过程。存储的文件是基础文件,每次写入时都会叠加该环境的 AppCrane 环境变量:在构建 Production 时会加载的文件(根目录或嵌套目录中的 .env、.env.local、.env.production、.env.production.local、.env.sandbox、.env.sandbox.local)中,如果某个键同时也是环境变量,则使用环境变量的值,只重写该行,并保留注释、顺序和格式;如果根目录中没有任何可加载文件定义某个环境变量,则将其追加到根目录的 .env(如果应用没有该文件,则以模式 0600 创建),绝不会追加到嵌套文件;示例、样例和模板文件会按存储内容写回。部署绝不会更改存储的副本,因此编辑或删除变量后,只有在下次部署构建新镜像时才会反映到文件中(提交未更改时会复用缓存镜像)。由于该文件属于构建上下文,其值(包括追加的变量)都会进入构建的镜像。部署日志会列出被覆盖和追加的键,但绝不记录其值。Production 只会获取 Production 的文件,Sandbox 只会获取 Sandbox 的文件;指向发布目录外部的 .env 符号链接(升级到 Production 时会保留)不会被保留;转换状态会向平台管理员显示已存储的路径,但绝不显示文件内容。应用所有者(或被分配到该应用的平台管理员)可以在应用页面的 Stored .env files 下管理已存储的文件,也可以使用 GET /api/apps/:slug/env-files(路径、环境、模式、大小和时间,不含内容)、GET /api/apps/:slug/env-files/content?env=&path=(文件内容;会按 env_file.reveal 记录审计、实施限流,并像环境变量揭示一样通知,且共用限额)、PUT /api/apps/:slug/env-files,传入 { env, path, content }(创建或替换;原始字节请使用 encoding: "base64";新文件模式为 0600,替换文件保留原模式;最多 1 MiB 的有效 UTF-8,且必须能解析为 dotenv;解析错误会报告行号,但不会回显内容),以及 DELETE /api/apps/:slug/env-files?env=&path=。路径遵循与还原时相同的规则(相对路径,不得包含 .. 或 .git,文件名必须符合 .env*)。更改会以 env_file.replace / env_file.delete 记录审计,仅记录路径,并在该环境下次部署时生效。其他应用成员会收到 403;非 Crane 托管应用会收到 409。向 Crane 托管应用的仓库推送时,如果其中包含任何 .env* 文件(仅依据文件名判断,任意层级、任意大小写,包含 .env.example),整个推送都会以 ENV_FILE_IN_PUSH 拒绝,不会提交任何内容;appcrane_push_to_managed_app 和 appcrane_managed_patch 会在碰触仓库前拒绝;appcrane_managed_push_chunk 会在收到第一部分时拒绝;appcrane_managed_assemble 会在提交前拒绝。错误会列出路径,但绝不包含文件内容,并提示使用环境变量(appcrane_set_secret)或已存储文件;被拒绝文件的内容也不会写入 MCP 审计记录。GitHub 托管应用不受影响。打包文件根目录 .env 中的值会成为应用的加密环境变量;Production 使用 .env.production 覆盖,Sandbox 使用 .env.sandbox 覆盖。AppCrane 中已设置的变量不会被覆盖;不会导入示例、样例、模板、开发环境和嵌套文件;绝不会记录或记录任何值,只记录键名。如果构建依赖打包的 node_modules、缺少发布版本、排除内容之外没有任何内容、要导入的 .env 格式错误、.env 文件超过 1 MiB,或应用超过大小上限(512 MiB,APPCRANE_UPLOAD_CONVERSION_MAX_BYTES),则应用会保持上传应用状态并记录原因。转换完成后,该应用的上传包会以 409 拒绝;请改用 appcrane_push_to_managed_app 推送。上传发布目录会保留在磁盘上:如需还原,将应用的 source_type 设回 upload、将 repo_backend 设为 NULL,并保留仓库原位(之后的下一次启动就不会再处理它)。平台管理员可以通过 GET /api/github-service/upload-conversion 查看每个应用的转换结果;设置 APPCRANE_UPLOAD_CONVERSION=off 可跳过转换。如果 Sandbox 运行的是不同代码,其提交会是最新提交,因此之后的每次部署(包括 Production)都会构建 Sandbox 的代码;Production 无法从自己较早的提交重新部署,因为部署前检查只接受最新推送的提交。旧版上传应用(source_type='managed_legacy',由 v2.3.1 重命名)也会转换,但带有 github_url 的应用除外;状态路由中的 original_source_type / revert.source_type 会指出转换后的应用还原时使用的类型。如果只有一个环境的上传发布仍在磁盘上(例如,Sandbox 最近的上传都失败了),应用会基于该环境进行转换;另一个环境下次部署时会构建仓库的最新提交。
  • 在嵌入式框架内登录 — 如果某个网站将应用嵌入平台自己的域名下(或应用列出的来源),用户无需离开框架即可登录:登录链中的每一跳(身份验证检查的重定向、/login、/launch)都会移除 X-Frame-Options 并发送应用的 frame-ancestors 策略,而且仅当重定向指向该应用时才会这样做。现在,登录后会保留深层链接,因此已退出登录的访客会进入自己请求的应用,而不是仪表板。密码登录会在框架内完成;OIDC 和 SAML 会跳转到身份提供商自己的页面,而提供商可能拒绝将该页面嵌入框架。AppCrane 的嵌入策略也优先于应用自己的 Content-Security-Policy:应用为自身发送的 frame-ancestors 会在边缘处重写为已配置的策略(应用 CSP 的其他内容——script-src 及其 nonce、object-src、form-action——则原样传递),因此,即使应用的 frame-ancestors 'none' 是硬编码的,只要有人允许它,仍可嵌入。当登录页面检测到自身处于框架中时,OIDC 和 SAML 会改为显示 Sign in 按钮,在顶层小型弹出窗口中打开身份提供商;登录成功后弹出窗口会自行关闭,同时框架会重新加载并显示应用。如果登录失败(提供商拒绝、没有账户,或尝试超时),弹出窗口会保留,并显示简短原因及 Close 按钮;框架会立即停止等待,并再次提供 Sign in 按钮。由于提供商的 Cross-Origin-Opener-Policy,单纯关闭弹出窗口后无法检测到,因此框架等待期间按钮仍可使用。登录页面会先向服务器确认已存储的会话,再将框架重定向回应用,因此已失效的会话会显示登录表单;没有应用角色的用户会看到“You don't have access”;如果登录链反复跳转超过三次,则会显示“Sign-in couldn't complete”,而不会无限重新加载。
  • 还原到没有镜像仓库的裸主机 — 备份由一组归档组成,每个归档都会直接写入磁盘,绝不会整个放在内存中:一个数据归档(数据库、密钥、图标、每个应用的 /data 和所有已声明的卷)、每个托管应用代码仓库各一个归档,以及一个以流式方式保存容器镜像本身的归档,并按摘要保存。数据归档会记录其依赖的仓库和提交,因此如果在旁边还原来自不同备份的仓库归档,会报告不匹配,而不是悄悄混用。导出会在主线程之外运行,因此备份时托管应用仍会响应登录检查;还原会将上传内容流式写入磁盘,并检查可用空间,而不会受固定大小上限限制。符号链接会作为链接归档,绝不会跟随,因此应用不能通过自己的 /data 将主机文件带进备份。计划任务的异地(S3)备份只上传数据归档;在输入存储桶和凭据之前不会执行任何操作——因此默认启用的是本地每晚备份,而不是默认什么都不备份:每天 04:00 将 deployhub.db 和 .env 写入 DATA_DIR/backups/local,并保留最新的七份(Settings → Backup 可修改时间、数量或关闭备份)。这只能应对数据库损坏、删除或还原错误,不包括应用图标、每个应用的 /data、已声明的卷、仓库或镜像,因为这些内容会让无人值守的每晚任务在其试图保护的同一磁盘上变得没有上限。它不能替代异地副本,因此,在实际完成一次上传之前,所有备份界面——设置 API、appcrane_get_backup_status、仪表板和启动日志——都会显示 "No off-site copy — everything AppCrane knows lives on this host."。已存储存储桶但关闭了计划任务,以及已开启计划任务但从未成功完成,都视为没有副本。这一点很重要,因为镜像仓库不是备份:镜像仓库变更后,bitnami/* 镜像会返回 404;目前 medusajs/medusa、vendureio/vendure 和 crater/crater 也都会返回 404。拉取失败时,部署现在会改用主机上已有的镜像数据,而不是拒绝启动
  • 会销毁数据的重新部署会先询问 — 每次部署都会重建容器,因此 /data 和应用已声明卷以外的所有内容都会丢失。AppCrane 会检查正在运行的容器,并且只在确实存在状态丢失风险时发出警告,同时说明哪些内容会保留、哪些不会保留。它通过两种方式查找状态:镜像声明的 VOLUME 路径中没有任何挂载覆盖的路径,以及应用实际写入过(docker diff)且没有挂载覆盖的路径——第二种方式很重要,因为 VOLUME 只是最低保障,而且所有将数据持久化到 storage/ 的 Laravel 应用都没有声明 VOLUME。对八个真实镜像进行实测后,只有确实存在未受保护状态的应用会收到警告;对安全应用也发出警告只会让人习惯性地点击确认。仪表板要求二次确认;API 和 MCP 则要求明确提供 acknowledge_data_loss,否则会拒绝操作——这里的大多数部署都由 Agent 发起,仅在 UI 中显示警告无法覆盖这些情况
  • 全局 admin 角色已退役 — 具有平台级 admin 角色的用户会自动成为所有应用的管理员,无需任何人分配;这与应用访问权限应按应用授予的规则相矛盾。现有持有者现已变为普通 user:他们只保留明确分配给自己的应用,并使用相应应用角色,同时失去对其他所有应用的隐式访问权限。不会有人被提升。现在无法再设置该角色:PUT /api/users/:id/role 和 POST /api/users 会返回 400,数据库也会拒绝该角色(迁移 105)。平台管理员不受影响,每个应用的 admin 角色也不受影响——应用管理员会保留原有权限。过去允许“admin 或平台管理员”执行的操作,现在仅平台管理员可以执行。
  • 选择用户是否能看到 Sandbox — 每个应用都有“Show Sandbox to users”设置(默认开启,因此只有在有人关闭时行为才会改变)。关闭后,成员和查看者将无法在启动器、应用框架的 Production/Sandbox 切换器或 Applications 页面链接中看到 Sandbox;所有者、应用管理员和平台管理员仍可访问,并会看到“hidden from users”标记。这只影响可见性:sandbox 仍会部署,而且任何有权访问的人仍可使用 sandbox URL。只有应用所有者或平台管理员可以在应用页面上更改设置,或者通过 PUT /api/apps/:slug 和 appcrane_update_app 中的 show_sandbox 更改;应用列表和详情响应会包含 show_sandbox,以及由服务器计算出的、针对当前调用者的 sandbox_visible。
  • 请求完成邮件会说明完成了什么 — 将请求标记为已发布、已完成或不予处理的人可以添加简短备注(可选,纯文本,最多 2000 个字符):Requests 中的 Mark shipped 按钮会提示输入备注,PUT /api/enhancements/:id/bucket 和 POST /api/enhancements/:id/set-status 接受 note,appcrane_set_request_status 也有 note 参数。备注会存储在请求上(迁移 106,done_note),添加到请求者的邮件中的“What was done:”(不予处理时显示“Note:”),也会添加到平台管理员收到的副本中,并显示给 My Requests 中的请求者。除换行符以外的控制字符都会被清除;备注绝不会进入邮件主题。没有备注时,邮件内容与之前相同。
  • 基于 Anthropic 开源 Defending Code Reference Harness 扫描的安全修复 — 编码工作区中的主机 git 不再运行由 Agent 植入的命令:工作区(包括其中的 .git/)可以由 Agent 重写,而 safe.directory 会导致主机遵循 core.fsmonitor、过滤器驱动程序、include.path 和 .git/commondir。现在,每次在该工作区中运行主机 git 命令前,都会先根据允许列表检查工作区配置;不符合的配置会被拒绝,并且运行时会关闭钩子、fsmonitor 和网络传输;救援推送会按路径推送到托管仓库,而不是推送到 Agent 可写入的 origin。编码会话只接受所有者发送的消息、附件、恢复请求和后续编辑:每一轮都使用所有者的个人 Claude 令牌运行,任何应用成员都可能消耗该令牌。应用管理员仍可停止其他成员的运行。应用 slug 如果会与平台路径冲突(如 a、api、login、mcp 等),或以 -sandbox 结尾,则在创建和重命名时都会被拒绝:Caddy 会将平台的兜底路由排在最后,因此名为 a 的应用会接收到所有 /api/... 请求,包括 Cookie 和 API 密钥。现有应用不会重新检查。为其他用户签发 MCP 密钥仅限平台管理员操作,与重置其他用户的密码一样;该密钥会以该用户身份进行身份验证,因此全局管理员可以借此成为平台管理员。
  • 删除应用和部署失败后不再积累旧镜像 — 以前只在部署通过健康检查后,才清理仍存在的应用的镜像。现在删除应用时也会删除其镜像:包括该应用构建的镜像,以及仅由该应用部署过的拉取摘要(不使用 -f;如果某个摘要也被其他应用部署或命名,就会保留;因此像 odoo:19 这样的共享仓库会保留其他应用用于回滚的镜像——v2.97.7 的第一版会清理整个仓库;v2.97.8 按镜像 ID 而不是 ref 文本进行比较,因为 odoo@sha256:x 和 docker.io/library/odoo@sha256:x 是同一镜像;并会通过 appcrane-<slug>-<env> 仓库和标签查找构建的镜像,因为任何拉取的镜像都可能带有这些标签;同一版本还将管理端 SPA 的构建时 source-map-js 更新到 1.2.2,修复 GHSA-68fv-2mgg-jv7q)。部署失败时也会删除其构建的镜像,不使用 -f,因此如果仍有容器在使用,Docker 会拒绝删除。尝试中复用的缓存镜像(例如回滚目标)绝不会被删除。之前,失败的镜像会成为最新镜像,因此下一次保留最近 N 个镜像的清理可能会保留它并删除最后一个正常镜像。
  • 在应用页面保存资源限制现在有效 — Resource Limits 卡片将值嵌套发送(resource_limits: {…}),而 PUT /api/apps/:slug 只读取顶层的 max_ram_mb / max_cpu_percent,因此设置实际上并未保存,卡片却显示“saved”,且非平台管理员也不会获知只有平台管理员可以设置限制。现在,卡片会发送服务器读取的字段;服务器也接受嵌套形式;按钮会显示 Save(与之前一样,限制会在下次部署或重启时生效)。
  • 登录、Ask Claude、编码器消息和文件上传都会写入审计日志 — 之前任何形式的登录都不会写入审计日志。现在密码、OIDC 和 SAML 登录都会记录 login 或 login-failed,包括所用方法、原因(unknown_user、bad_password、deactivated、no_account、SSO 链接被拒绝)和客户端 IP,绝不会记录密码或令牌。Ask Claude(ask)、编码派发(coder-dispatch)和文件暂存(file-stage)也会记录审计;问题或提示词只记录其长度,因为内容可能包含任何信息。proxy-addr 更新到 2.0.8(GHSA-jqcg-44mw-7w3h:若信任子网使用较短的 IPv4 映射 IPv6 前缀,就会信任所有客户端的 X-Forwarded-For)。
  • 安全审计修复:密码、Production 数据、日志、租户清除和 SSO 链接 — 同一轮审计发现的中危问题。重置其他用户的密码(PUT /api/users/:id/password)现在仅限平台管理员操作;之前全局管理员可以重置平台管理员的密码并冒充其登录。破坏性的 Production 操作必须由应用所有者或平台管理员执行:删除 Production 环境变量、删除由 Sandbox 和 Production 共用的应用托管数据库、还原 Production 备份,以及将 Production 数据复制到 Sandbox。运行时日志(REST /api/<slug>/logs/<env> 和此前完全跳过权限检查的 MCP appcrane_get_logs)现在仅对应用所有者或平台管理员可见。租户清除不再会被某个应用在其 tenants/ 中植入的符号链接重定向到另一个应用:如果应用有容器,删除操作会在临时辅助容器中运行(无网络、no-new-privileges),该容器只挂载该应用的数据,因此链接只会在辅助容器内部解析;如果没有容器,则会在主机上通过 lstat 遍历路径,只要任意部分是链接就不会删除任何内容(v2.97.3:第一版修复会先在主机上检查,再在主机上删除,而正在运行的应用可以在两者之间插入操作)。SSO 链接(OIDC 和 SAML 共用 services/ssoLink.js 中的一条规则):已经关联一个 SSO 身份的账户绝不会转移到另一个身份;OIDC 只有在身份提供商发送 email_verified: true 时,才会通过电子邮件地址关联现有账户,或更新其存储的电子邮件地址。如果身份提供商从不发送该声明(例如 Microsoft Entra),则设置 OIDC_ALLOW_UNVERIFIED_EMAIL_LINK=1,允许首次通过电子邮件地址关联。
  • 安全审计修复:Production 部署、成员列表和写入 /data — 来自对 v2.97.0 的全面审计(squash 加上 unit515 审查)。Production 部署的每条路径现在都要求 deploy.production 权限,而不仅仅是 POST /deploy/:env:上传部署、appcrane_deploy_artifact、appcrane_push_staged_file,以及 webhook 的 auto_deploy_prod 开关(开启它,或在开启时更改决定 Production 跟随哪个代码的 branch_filter)以前允许任何应用成员将代码部署到 Production。关闭开关仍对成员开放。替换应用成员列表(PUT /api/apps/:slug/users)现在需要所有者或平台管理员权限,与 PUT /:slug/roles 相同,并且拒绝移除所有所有者;之前任何成员都可以将所有所有者移除。appcrane_cp 不再跟随应用在其 /data 中植入的符号链接:只要应用有容器,无论运行还是已停止,都会通过 docker cp 将文件流式写入(链接会在容器内解析);只有完全没有容器的应用才会在主机上写入,遍历路径并拒绝任何链接;如果 Docker 无法确定容器状态,则不会写入任何内容。之前,应用管理员可以将文件写入 AppCrane 进程有权写入的任何位置。以 - 开头的路径段会被拒绝,并且 tar 会在路径前加上 --。
  • 重启会保留多租户应用的租户设置 — POST /api/apps/:slug/restart/:env 会从头重建容器环境变量,但不会重新添加 APPCRANE_TENANT_ROOT 或 APPCRANE_TENANT_QUOTA_BYTES;部署会从 deployhub.json 派生这些设置,因此重启后的多租户应用会丢失每个用户的配额,直到下次部署。现在,重启会从被替换的容器中原样保留这两个变量(如果不存在,则为标记为多租户的应用使用租户根目录),并像部署一样向应用提供其身份签名密钥。
  • 身份标头会签名,应用因此可以拒绝并非通过 AppCrane 发送的身份信息 — 代理会移除客户端发送的 X-AppCrane-* 标头,但有两种路由会绕过代理:原始 tcp/dual 端口;以及在 Docker Desktop 上(实测可行;Linux 上会被阻止)通过 host.docker.internal 访问应用 loopback 端口的同级容器,这会让一个被入侵的应用冒充另一个应用的任何用户。现在,/api/identity/verify 会添加 X-AppCrane-Identity-Ts 和 X-AppCrane-Identity-Sig(HMAC-SHA256,每个应用和环境各有一个密钥,加密存储,并在每次部署和重启时注入为 APPCRANE_IDENTITY_SECRET,因此 sandbox 无法签发其 Production 会信任的身份),而 appcrane-tenant 1.2.0 新增了 verifyIdentity(req);设置密钥后,tenantKey()/tenantDb() 会自动验证。应用会在下次部署时获取密钥;不执行验证的应用行为不变。
  • UDP 数据平面经过 http 后往返仍会保留 — 应用从 dual 切换到其他类型后,会保留其 UDP 设置;读取时也会报告该设置(data_plane_protocol: 'udp'),而不是报告实际生效的 tcp。之前,客户端将应用切回 dual 并把读到的值发回时,会悄悄将 UDP 中继变成 TCP 中继。非 dual 应用发布的内容保持不变:TCP。
  • dual 应用可以通过 UDP 发布数据平面 — data_plane_protocol: 'udp'(REST PUT /api/apps/:slug 或 appcrane_set_app_ingress,仅限平台管理员)会将 dual 应用的原始数据平面发布为 <host>:<port>/udp,例如 WireGuard 中继。HTTP 控制平面及其健康检查仍通过 Caddy 使用 TCP。只有 dual 可以使用 UDP:tcp 应用发布的端口是其 HTTP 源站。每个应用都会报告 data_plane_protocol,并且发送回所报告的值时会被接受,而不是拒绝。对于 UDP 数据平面,暴露警告和入门指南会提供 -p udp 的 DOCKER-USER 规则,因为 TCP 规则不涵盖 UDP。入口漂移检测也会比较协议。
  • 依赖更新,修复六个 CVE — multer 2.4.0(CVE-2026-88932)和 nodemailer 10(修复 9.1.1 中的五个 CVE),以及 chalk 6、better-sqlite3 13(SQLite 3.53.4)和 CI 中 OSV-Scanner、SBOM 与 SARIF 上传 action 的更新固定版本。nodemailer 10 需要 Node 20+,而 AppCrane 本来就要求该版本;它的 ES-module 构建保留了 AppCrane 调用的默认导出。better-sqlite3 13 打开现有 WAL 数据库时,synchronous = NORMAL 行为与 11 完全相同,因此 v2.93.2 中记录的持久性行为不变。npm audit --omit=dev:0 个漏洞。
  • 每租户数据库会逐个文件自动升级 — 每个用户一个 SQLite 文件时,最容易踩的陷阱是数千个文件间的架构漂移。appcrane-tenant 1.1.0 支持 tenantDb(req, { migrations: [step1, step2, ...] }),可在每个用户文件于部署后首次打开时按需升级,并以 PRAGMA user_version 为依据:每个待执行步骤都会在一个 IMMEDIATE 事务中运行,因此步骤失败时文件会保留原版本;同时打开同一文件的两个请求只会执行一次升级。如果文件版本高于迁移列表(代码已回滚),则会原样打开。multitenant-notes 示例使用了该功能。
  • 主机繁忙时,干净的应用不再要求确认数据丢失风险 — 重新部署保护机制曾为 docker diff 设置 10 秒时限。在使用 containerd snapshotter、运行二十多个容器的主机上,即使容器没有任何变更路径,docker diff 也要花 4–10 秒,因此运行较慢的命令会被终止并报告为“Command failed”,导致每次重新部署都要求确认。现在时限提高到 60 秒,真正超时时会显示“docker diff did not finish within 60 s”。仍无法读取的容器会继续报告为存在风险。
  • 作为应用所有者就一定有权访问该应用 — 过去访问权限记录在两处:app_users 中的成员身份,以及 app_user_roles 中的角色等级。两者会发生不一致。较早的升级步骤(迁移 042/048)和启动时的所有者回填会在没有成员记录的情况下将应用创建者设为所有者,因此,用户虽然在 Users 对话框中显示为所有者,却无法访问自己应用的环境变量和数据,并看到“Admin access does not include app data/env”。现在,访问权限只保存在一处:app_user_roles。迁移 101 会为每个没有角色的成员设置 user 角色,删除遗留的 none 行,并将 app_users 变成角色表的只读视图,以便所有读取操作继续正常工作,而对它的任何写入都会失败。MCP 权限检查不再将已存储的 none 计作访问权限;替换应用成员列表(PUT /api/apps/:slug/users)时会保留剩余每个成员的权限级别,并删除被移除成员的租户数据。平台管理员仍须被分配到应用后才能读取其密钥。
  • 每个应用都显示 Request 按钮 — 应用打开时的两个入口都显示该按钮:Launch 或侧边栏标签,以及 Manage → “open”(所有者和管理员可在此打开应用;此前 Jobs 和 Bug 也被隐藏)。按钮以前只会在带有 GitHub URL 的应用上显示,这是每个请求都同时创建 GitHub issue 时留下的遗留限制。AppCrane 自行保存代码的应用没有 GitHub URL,因此按钮恰好在带有 Coder 的应用上缺失。无论如何,请求都会由 AppCrane 存储;只有应用在 GitHub 上有仓库时,才会同步到 GitHub。
  • AppCrane 自身的 What's New 会再次列出所有版本 — 升级预览和登录后对话框曾从 CHANGELOG.md 读取更新说明,而该文件停在 2.78.1,因此之后的任何版本都只会显示“A new version of AppCrane is available.”。现在,更新说明会在安装自身的 git checkout 中,从版本提交说明(vX.Y.Z: ...)读取;先执行 git fetch origin,再读取远程自更新已使用的提交,无需调用 api.github.com,也不限制回溯范围。不是 git checkout 的安装会回退到 GitHub commits API。不再读取 CHANGELOG.md。
  • 从多租户应用中移除用户时,无论使用何种方式,都会删除其数据 — 以前只有通过 MCP 工具撤销访问权限时,才会清除每租户数据(该用户的 SQLite 数据库和存储文件夹,包括 Production 和 Sandbox)。在仪表板中移除用户、删除用户,以及通过 SCIM 组移除用户,都会撤销访问权限但将数据留在磁盘上,尽管本 README 承诺撤权时会清除数据。现在这四种路径都会清除数据(server/services/tenants.js 中的 purgeRevokedTenants),并在 test/tenant-purge-on-revoke.test.js 中为每种路径分别测试。托管数据库条目也不再声称会为每个租户配置数据库:架构包含该维度,但目前尚未配置。
  • 从 Launch 打开应用时会显示 What's New — 现在,从 Launch 或侧边栏(标签式视图)打开应用时,系统会询问 Production 是否发布了你尚未查看的版本,并在 What's New 对话框中显示这些版本,适用于所有有权访问该应用的用户,而不仅限于所有者。此前只有较旧的全屏框架会提示,因此大多数用户从未看到该信息。首次访问应用时仍会静默记录当前版本,并且只统计 Production 版本。
  • Coder 发布后会持续跟踪部署结果 — 发布后,Changes 标签页和聊天会报告由此次发布触发的 sandbox 部署最终结果:已上线(附版本号)、已回滚或失败。部署失败时,系统会自动将失败日志行交还给 Coder,要求它重现并修复构建问题,但不要发布;你可以审核修复并发布。每个已发布提交只会执行一次,因此无法修复的构建不会陷入循环。此前部署失败时不会显示任何提示,面板会一直显示“Deploying”。
  • 匿名每日安装签到(可选择退出) — 每个 AppCrane 服务器每天会向 https://ping.appcrane.dev/v1/checkin 发送一个小型 JSON 文档:随机安装 ID(首次启动时生成,存储在 DATA_DIR/install-id,与任何信息都无关)、AppCrane 版本、Node 版本、操作系统和 CPU 架构,以及三个计数:注册应用数、有在线部署的应用数和活跃用户数。不包含名称、域名、电子邮件或应用内容,接收方也不会存储 IP 地址。此功能旨在了解安装数量以及它们运行的版本和运行时。设置 APPCRANE_CHECKIN=off(或 DO_NOT_TRACK=1)即可关闭;每次启动都会记录该功能是否启用以及将要发送的内容。APPCRANE_CHECKIN_URL 可将数据发送到其他位置。
  • 磁盘繁忙时,数据库不再阻塞 AppCrane — SQLite 的 WAL 检查点过去会在提交操作触及阈值时,由对应请求在事件循环中执行,包括 fsync。在磁盘受限且竞争激烈时,这会导致每隔几秒就卡顿 3.5–5.9 秒;改用 synchronous = NORMAL 并无帮助。现在,服务器会每隔 2 秒在工作线程中执行检查点(使用 PASSIVE 模式,因此绝不会阻塞写入),实测最慢的提交耗时为 1.9–2.4 毫秒。持久性设置没有变化:AppCrane 使用 better-sqlite3 WAL 的默认设置 synchronous = NORMAL,实测与 FULL 没有区别。crane CLI 仍使用 SQLite 默认设置。MariaDB 安装也会在使用已记录的 root 密码前检查其是否能登录服务器,并会拒绝使用一个并非由它启动的服务器,同时说明容器名称、可能原因和解决方法,而不是等到之后才显示一个单独的“Access denied”。
  • Coder 按钮位于 Sandbox 标签页 — 审核 sandbox 时会显示该按钮,在 production 上不会显示;切换到 Production 会关闭面板(正在运行的一轮会继续,并在你返回时继续显示)。CI 上偶尔失败的五项测试已查明原因并直接修复。不得与并行测试套件共用磁盘的计时测试位于 test/serial/,并作为单独的 CI 步骤运行(npm run test:serial)。
  • 无法安装 Node 时,自我更新会自带 Node — 低于 Node 最低版本要求的主机过去会拒绝所有更新,除非 AppCrane 以 root 身份运行,因为升级 Node 需要使用 apt-get。现在,如果无法访问系统软件包管理器(不是 root、没有 apt,或使用 nvm/asdf 提供的 Node),自我更新会从 nodejs.org 下载官方 Node 构建版本到 <appcrane>/.runtime/;使用 gpgv 和 Node 发布密钥(随附于 infra/node-release-keys.gpg,由 scripts/update-node-release-keys.sh 从 nodejs/release-keys 更新)检查发布版 SHASUMS256.txt 的 GPG 签名;根据该签名列表检查压缩包的 SHA-256;运行 Node 以确认主版本及其能否在当前主机的 libc 上工作;然后在剩余更新过程中使用该版本。没有 gpgv 的主机(它随 apt 提供)会被拒绝,除非设置 APPCRANE_NODE_ALLOW_UNSIGNED=1,允许仅验证校验和。只要系统 Node 低于最低版本要求,scripts/safe-boot.sh 就会在每次启动时将该 Node 放到 PATH 首位;系统 Node 达到最低版本要求后,脚本会停止使用它。已经因 Node 版本过低而无法启动的主机,也会在下次启动时通过相同方式修复,无需 root。服务必须通过 scripts/safe-boot.sh 启动(install.sh 安装的 unit 会这样做),并且主机必须运行 x64 或 arm64 Linux;否则更新仍会被拒绝,并指出缺少上述哪一项。
  • Coder 模式和聊天中的文件 — 每条消息都可以在模型旁边选择模式:Auto(读取、编辑和运行命令,无需询问——即 Coder 一直以来的行为)、Edits only(编辑文件,拒绝 shell 命令)或 Plan(读取代码并提出计划,不做任何更改)。每种模式都对应一个 Claude Code --permission-mode;模式列表由 GET /api/coder/models 提供,并在路由层和构造命令时再次按完全匹配的字符串进行强制检查。消息也可以附带文件:粘贴屏幕截图或复制的文件,将文件拖放到撰写框中,或使用 Attach(每个文件 10 MB,每条消息最多 10 个文件)。文件会上传到 POST /api/coder/:slug/session/:id/attachments,并在主机上按会话存储在不透明 ID 下——绝不会放入工作区,因此不会列为修改内容或被发布;运行该轮时,文件会复制到容器中,并通过路径告知 Agent,Claude Code 的 Read 工具可读取文本、代码、图片和 PDF。排队的后续消息会保留自己的模式和文件。面板中尚未实现按操作审批(Manual 模式)。
  • Coder 可以运行 shell 命令并保留对话 — 在 Coder 容器中,Agent 以 studio 用户身份运行,但 Docker 以 root 身份创建了 ~/.claude(对话记录会挂载在该目录下),而每次启动容器时,已保存的对话都会交给另一个用户。Claude 无法创建 Bash 工具所需的状态,因此 Agent 会报告“Bash is blocked by an environment permission issue”,重启后也无法读取或继续自己的对话。现在,studio 镜像会为 studio 创建 ~/.claude,并固定 uid/gid;对话记录由该用户所有,并会在下次启动时修复权限。镜像版本已升级,因此每台主机都会在下次启动 Coder 时重建一次镜像。
  • 元素选择器发送的消息现在可以到达 Coder — 所有选择器消息都以破折号开头(--- Pointed element ---),因此以破折号开头的消息会被 Claude CLI 解析为命令行选项,并失败并显示 unknown option。现在提示词会在 -- 后传递,因此消息不会被解释为标志。现在,在首条消息之前生成的代码库摘要会读取 Coder 工作区(此前主机 git 会因工作区属于其他用户而拒绝读取,导致摘要缺少提交信息且文件树为空),会使用与你的消息相同的凭据运行;如果失败,会显示 Claude 返回的信息,而不只是退出码。
  • 发送给已暂停 Coder 的消息会正常运行,不会消失 — 如果 Coder 容器已停止(例如被清除,或发生了打开页面未获知的 AppCrane 重启),过去发送消息会报错,而面板仍显示空闲。现在,面板会恢复会话并运行消息;如果恢复失败,会说明消息未运行及原因。Claude 拒绝凭据时,运行会在第一个 401 处停止,而不会在重试中等待约三分钟;错误会指出需要更换哪项凭据:你的订阅令牌、应用上传的凭据,还是平台的 ANTHROPIC_API_KEY。现在,在首条消息之前生成的代码库摘要会显示正在处理中;如果失败,也会说明;如果只是恢复对话,则会跳过摘要。
  • Coder 启动时会说明状态,失败时会说明原因 — 恢复已暂停会话时需要重新创建容器;升级后首次恢复时还要重建 Agent 镜像,耗时至少一分钟。以前这里完全不显示任何信息,因此缓慢的恢复看起来和按钮失效一模一样。现在会显示运行计时器和当前操作,十分钟后会明确放弃,而不是无限等待;如果镜像构建失败,错误消息会包含最后几行构建输出——npm 仓库无法访问和磁盘已满不会再都显示成 exit 1
  • Coder 按钮会告诉你需要哪些条件 — 该按钮会显示在每个应用上,而不仅限于它能够运行的应用。暂时无法运行时,按钮会变灰;点击后会一次性列出所有原因及其解决方法:应用代码位于 GitHub、应用仍是上传的归档、应用从镜像运行、仓库尚未迁移,或你还没有关联 Claude 账户(并提供相关页面的链接)。该列表来自会话拒绝条件的同一套检查,因此不会承诺服务器随后会拒绝的功能,也不会为了拒绝而先启动会话
  • 你教给一个 Agent 的内容,下一个 Agent 也会知道 — 应用可以保存操作人员备注,并在每次编码派发时将其置于提示词开头。本地 Claude Code 现在可以通过 MCP(appcrane_append_app_context)写入备注,因此你在自己机器上发现的限制或注意事项可以传递给平台内的 Coder,而不是随着会话结束而消失。默认采用追加,因为这样不会删除人工编写的内容;替换工具会报告丢弃了多少内容,备注也有长度上限,因为每次派发时都会置于提示词开头的内容会悄悄占用它原本要帮助的上下文窗口
  • Coder 实际上有 shell 可用 — Agent 镜像基于 Alpine,内置 busybox sh 而没有 bash;Claude Code 的 Bash 工具会专门启动 bash。Agent 发起的每次 shell 调用都会在启动时失败,因此它虽然可以读取和编辑文件,却会报告“no shell”,并让用户手动运行命令——会话看起来正常,构建、测试和 git 却都无法使用。现在,镜像会安装 bash,且版本已升级,以便现有主机重建镜像
  • 没有 buildx 的 Docker 安装也能正常构建 — BuildKit 需要 docker buildx 插件,而 Ubuntu 的 docker.io 软件包只提供守护进程,不含该插件,因此强制启用 BuildKit 会导致每次部署都在构建步骤失败,并显示指向 Docker 文档而非解决方法的消息。AppCrane 现在会探测一次;有 BuildKit 时使用 BuildKit,没有时使用传统构建器,并在日志中说明。APPCRANE_DOCKER_BUILDKIT 可固定选择任一方式
  • 为 Node 和 PHP 自动生成 Dockerfile — 没有 Dockerfile 的应用会基于 node:*-alpine 构建;如果应用提供了 composer.json,则会基于 php:8.3-apache 构建。两者都以非 root 身份运行,并遵循 AppCrane 分配的端口。PHP 构建会在其前端控制器进行健康检查,并将要求设为“返回 200”,而不是要求返回 AppCrane 的 {status, version} 响应体——因为 Dockerfile 是由 AppCrane 生成的,而不是应用本身生成的;Laravel 和 Symfony 都不会提供 /api/health。其他应用仍由 Nixpacks 处理
  • 企业 SSO — SAML 2.0、OIDC 和 SCIM 配置;可连接 Okta、Azure AD、Google Workspace
  • 通过标头向应用传递身份信息 — X-AppCrane-User-Role、X-AppCrane-App-Role 等标头会在 forward_auth 验证用户后由代理注入;已部署应用无需回调,直接从请求中读取身份信息(oauth2-proxy / IAP 模式)
  • /api/me 端点 — 通过代理的应用可以使用规范的“调用者身份”查询;接受 cc_token Cookie、Bearer 或 X-API-Key;返回全局角色和每个应用的角色(使用 ?app=<slug> 或从 Referer 推断)
  • 无头应用类型 — 设置 auth_mode: 'headless',即可在应用上完全绕过 forward_auth;适用于遥测接收、公开 webhook、状态页面以及用途单一的未认证服务
  • TCP(第 4 层)入口 — 适用于完全不是 HTTP 的应用(转发/CONNECT 代理会返回任何反向代理都无法表示的原始隧道);平台管理员可以直接在主机上发布容器端口,绕过 Caddy。不提供 SSO、身份标头或 AppCrane TLS——身份验证完全由应用负责
  • 双平面应用 — 对同时具有两种平面的应用设置 ingress_type: 'dual':HTTP 控制平面仍通过 Caddy 在容器端口 3000 上提供服务,所有控制功能保持不变;同一容器内另一个端口上提供原始数据平面,并发布到 0.0.0.0:<public_port>。数据平面端口不能是 3000——否则会将控制平面未经身份验证就重新发布;健康检查仍会探测控制平面,因为只有它能正常响应
  • AppStudio — 将功能请求转换为待你批准的计划。对于 GitHub 托管应用,它随后会编写代码并将代码部署到 sandbox,供你审核后再进入 Production;对于 Crane 托管应用,它会分析仓库并将结果作为新请求提交,由 Coder 完成更改
  • 实时在线状态 — 查看每个应用的活跃用户、所在环境以及最近部署时间
  • 每个应用具有双环境:Production + Sandbox,始终运行,端口分离
  • 通过 Caddy 反向代理自动启用 HTTPS,并使用 Let's Encrypt
  • GitHub webhook 推送后自动部署(经 HMAC 验证)
  • 连接仓库使用 GitHub App 身份验证 — 无需长期有效的个人访问令牌,平台管理员可以通过 Settings → GitHub 为此实例创建专属 GitHub App(使用 GitHub 的 App 清单流程;不同安装之间绝不共享 App);应用构建者只需在自己选择的仓库上安装该 App,然后在 Applications → gh app 中将应用关联到该安装。之后,AppCrane 会使用有效期一小时、范围仅限于该仓库的安装令牌来克隆和固定代码、运行部署前提交检查、读取拉取请求以及检查更新;令牌只缓存在内存中,并通过环境传递给 git——绝不放入 URL、配置文件、日志行或命令行。该 App 只请求只读的 Contents、Metadata 和 Pull requests 权限,因此无法推送、创建拉取请求或注册 webhook(已关联 App 时会拒绝“Register on GitHub”;请改为在 GitHub 上粘贴 payload URL)。如果无法为已关联 App 签发令牌,该操作会说明原因并失败;绝不会回退到应用存储的 PAT。未关联安装的应用会像之前一样继续使用 PAT。**Webhooks:**App 会向 POST /api/github-app/webhook 发送事件;只使用 App 的 webhook 密钥对原始请求体的 GitHub X-Hub-Signature-256 进行验证(重复投递的相同 X-GitHub-Delivery 会在不重复执行操作的情况下得到响应)。向已关联 App 的部署分支推送会触发与每应用 webhook 相同的自动部署(使用相同的自动部署开关和分支筛选器,并记录为 github-app-push-deploy);标签推送和分支删除不会触发部署。若安装被卸载或暂停,或仓库被从安装中移除,系统会将其标记,而不是解除关联,因此已关联应用会说明原因并失败,而不会悄悄回退到 PAT;取消暂停或重新添加仓库后即可恢复。设置了 CRANE_DOMAIN 创建的 App 从一开始就会启用 webhook 并订阅 Push;未设置时 webhook 会保持关闭,因为 GitHub 没有可访问的公开 URL。对于此前创建的 App,请在 Settings → GitHub 中点击 Send webhook settings to GitHub(此操作会通过 GitHub API 设置 URL、JSON 内容类型和密钥),随后在 GitHub 上打开 Settings → Developer settings → GitHub Apps → Edit 下的 App,勾选 Webhook 下的 Active、在 Subscribe to events 下勾选 Push,然后点击 Save changes——GitHub API 无法设置这两项。无论哪种情况,5 分钟一次的 PR 轮询器都会继续运行。尚不支持:AppStudio 编码/PR 工作流、Ask Claude、发布说明和 issue 镜像仍使用 PAT;实例配置导出也不会包含 App 的私钥。
  • 已存储的 GitHub 凭据不会出现在 URL、文件或响应中 — 部署会通过环境而非克隆 URL,将已存储的个人访问令牌、托管应用的服务账号令牌或安装令牌传给 git,因此令牌不会出现在进程列表、错误文本或 git 写入每个发布目录的 .git/config 中。早期版本会将令牌放入 URL,因此每个发布目录都留下了明文令牌;启动时,AppCrane 会从现有发布目录、AppStudio 作业目录和构建器工作区中删除这些凭据(只删除远程 URL 中的凭据部分,不影响应用数据或卷)。应用响应不再包含任何加密凭据列;仪表板只读取 has_* 标志。应用列表中的每个应用都会标明代码来源:Crane 托管、Managed (GitHub)、GitHub App、GitHub token 或 Public GitHub。
  • 零停机部署(启动新版本、健康检查、切换、排空旧版本)
  • 数秒内回滚(基于符号链接,保留最近 5 个发布版本)——前一个发布版本的容器镜像也会保留,因此回滚一个版本时会重启该镜像,而不是重新构建;使用 image_retention(默认值为 1,可通过 appcrane_update_app 或应用设置修改)更改应用要保留的旧镜像数量;0 表示只保留正在运行的镜像,每次回滚都会重新构建
  • 加密环境变量(AES-256-GCM)——按设计,管理员无法读取这些变量
  • 健康检查,支持自动重启和电子邮件通知
  • 每项操作都有审计日志
  • MCP 服务位于 /api/mcp,提供 59 个 appcrane_* 工具——Agent 无需使用 curl、gh 或 shell 即可操作平台

快速开始

在全新的 Ubuntu 服务器上,运行一条命令即可安装并配置好所有内容——Node、 Caddy(自动配置 HTTPS)、Docker、systemd 服务、加密密钥和管理员用户:

curl -fsSL https://raw.githubusercontent.com/gitayg/appCrane/main/install.sh | sudo bash

它只会提示你输入两项内容——域名和管理员邮箱——并且可以安全地重复运行。运行完成后,将域名的 DNS 指向服务器即可上线。

**前提条件:**一台全新的 Ubuntu 服务器(root / sudo 权限),以及 DNS A 记录指向该服务器的域名——Caddy 会在首次请求时自动配置 TLS。

非交互模式(CI / 自动化)——不会出现提示:

sudo CRANE_DOMAIN=crane.example.com [email protected] bash install.sh
# flags also work: --domain / --admin-email / --admin-name / --tls-cert / --tls-key
安装程序会配置什么,以及为什么不建议手动安装

上面的一条命令会为你幂等地完成以下所有设置:

  • Node.js 22 + AppCrane,并将 crane CLI 链接到全局
  • Caddy——反向代理,将 <domain>/<slug> 路由到各个应用,运行 SSO 身份验证,注入 X-AppCrane-* 身份标头,并自动配置 TLS——此外还会配置组、文件权限和 sudoers 规则,以便 AppCrane 在每次部署时重新加载 Caddy
  • Docker + systemd appcrane 服务(Restart=always——可在崩溃和重启后继续运行,并支持一键自我更新)
  • 一个包含新生成的 ENCRYPTION_KEY 的 .env 文件——请备份此密钥;丢失后,所有已存储的密钥都将无法恢复——以及你的管理员用户(crane init)

手动安装意味着要重新完成以上所有设置——尤其是安装 Caddy、配置权限和 sudoers,这是最容易遗漏的步骤,之后会导致权限错误,或应用无法收到身份标头。如果必须手动安装,请以 install.sh 为准,而不要依赖精简版清单。

AI 会话(可选)。 编码会话会按以下顺序使用可用凭据:已登录用户自己的 Claude 订阅,然后是应用存储的凭据,最后是平台 API 密钥。每次只会向容器发送一种凭据——Anthropic 对 API 密钥的优先级高于订阅令牌,因此同时发送两者会悄悄地将费用计入错误的账户。

对于第一种方式,每个人都要使用 claude setup-token 为自己生成一个有效期一年的令牌(Pro、Max、Team 或 Enterprise 计划),并将其保存到“设置”→“账户”中。此操作会移除从终端复制时带入的空白字符,并说明移除了哪些字符;包括平台管理员在内的其他任何人都无法读取、替换或清除此令牌。令牌不会自动刷新——它会在生成一年后过期,重新生成需要使用浏览器,因此 AppCrane 会保存并显示过期时间。

如需使用平台级密钥:运行 systemctl edit appcrane --force,在 [Service] 下添加 Environment="ANTHROPIC_API_KEY=sk-ant-...",然后运行 systemctl daemon-reload && systemctl restart appcrane。

### 部署你的第一个应用

应用由 Agent 通过 MCP 创建和部署,而不是通过 CLI。 安装程序运行了 crane init,输出你的 dhk_admin_* 密钥并将其写入 CLI 配置(运行 crane config --show 可查看;如果密钥丢失,可在服务器上运行 crane regenerate-key)。将 Claude Code 指向此实例一次:

claude mcp add --transport http appcrane https://<your-domain>/api/mcp \
  --header "X-API-Key: dhk_admin_xxxxxxxxxxxxx" \
  --header "X-Github-Token: ghp_your_github_pat"

然后在 Claude Code 会话中请求创建应用:

在 AppCrane 上接入一个新应用。先调用 appcrane_get_guide,并设置 topic="onboarding" 获取操作指南。该应用名为 MyApp,slug 为 myapp,代码来自 https://github.com/yourorg/myapp。将其部署到 sandbox,并授予 [email protected] 访问权限。

Agent 会依次调用 appcrane_create_app、appcrane_deploy 和 appcrane_grant_app_access,随后即可通过 https://<your-domain>/myapp 访问该应用。仪表板上的 Add Application 按钮会为你提供相同的提示词,并预先填入此实例的 URL 和你的密钥。

想自己操作?仪表板 https://<your-domain> 会通过相同的路由创建和部署应用,其底层 REST API (POST /api/apps、POST /api/apps/:slug/deploy/:env)也使用相同的 X-API-Key。无代码仓库的部署方式请参阅不通过 GitHub 部署。

CLI 参考

crane 是平台运维人员使用的工具,不是应用所有者使用的工具。 它用于安装服务器、终止 TLS、迁移实例配置、找回丢失的密钥、重新加载 Caddy 并修复配置漂移——共九条命令,详见下文。应用操作(创建、部署、回滚、晋级、密钥、日志、访问)不在 CLI 中:面向 Agent 的操作接口已在 v2.6.0 中弃用,现由 MCP 服务和 REST API 提供。请参阅下文的应用操作(MCP 或 REST)。

安装和首次运行
crane init --email [email protected]          # First run: create the admin directly in the DB
                                              # (--name defaults to "admin"); prints the dhk_admin_* key
crane setup-https --domain crane.example.com  # Install Caddy, configure HTTPS, set up the firewall
crane update                                  # Pull latest code from GitHub and restart AppCrane
连接和身份
crane status                              # Server health and all apps
crane me                                  # Show current user info
crane config --show                       # Show CLI config
crane config --url http://localhost:5001  # Set API URL
crane config --key dhk_admin_xxx          # Set API key
找回丢失的 API 密钥

在服务器上运行——它会直接写入数据库。默认操作平台管理员;如需指定账户,可进行覆盖:

crane regenerate-key                      # Regenerate the platform owner's key
crane regenerate-key --email [email protected]   # ...for a specific user by email
crane regenerate-key --user-id 1          # ...or by user id
代理和配置漂移修复
crane caddy --show                        # Show the current generated Caddyfile
crane caddy --reload                      # Regenerate and reload Caddy config
crane reconcile --dry-run                 # Preview orphaned filesystem apps
crane reconcile                           # Register them into the DB and reload Caddy

regenerate-key 和 reconcile 打开数据库时不会执行迁移——迁移由服务器负责,并在启动时应用。如果磁盘上的代码版本比数据库架构新,这两个命令都会拒绝操作,并报告待执行的迁移数量,而不会在服务器运行时修改数据库。crane init 是唯一会执行迁移的命令:它负责引导初始化,此时服务器尚未运行。

在实例之间迁移配置

将平台的 settings(包括加密密钥)迁移到另一个 AppCrane,无需共享加密密钥。导出时会保留密钥密文;导入时会使用目标实例自己的密钥重新加密。

# On the SOURCE instance:
crane config export --out config.json

# Copy config.json to the TARGET, then on the TARGET:
OLD_ENCRYPTION_KEY=<source ENCRYPTION_KEY> crane config import config.json

导入时仅需使用源实例的 ENCRYPTION_KEY(来自源实例的 .env)解密密钥;它只会临时使用,不会保存。单向值(例如以哈希形式存储的 SCIM 令牌)无法迁移——导入过程会列出这些值,供你在目标实例上重新生成。之后删除 config.json。

应用操作(MCP 或 REST)

应用所有者执行的所有操作都通过 MCP(主要入口,详见 MCP(面向 AI Agent))或底层的 REST 路由进行, 两者都使用相同的 X-API-Key。两者的审计方式完全相同,并且都执行 相同的应用级角色权限。

操作MCP 工具REST
创建应用appcrane_create_appPOST /api/apps
列出 / 检查appcrane_list_apps、appcrane_get_appGET /api/apps、GET /api/apps/:slug
部署appcrane_deployPOST /api/apps/:slug/deploy/:env
部署历史 / 日志appcrane_list_releases、appcrane_get_deploy_logGET /api/apps/:slug/deployments/:env、…/:id/log
回滚appcrane_rollbackPOST /api/apps/:slug/rollback/:env
将沙箱提升至生产环境appcrane_promotePOST /api/apps/:slug/promote
环境变量 / 密钥appcrane_set_secret、appcrane_get_secret、appcrane_reveal_secretGET / PUT /api/apps/:slug/env/:env、DELETE …/:key
授予访问权限appcrane_grant_app_accessPUT /api/apps/:slug/users
健康状态appcrane_get_healthGET / PUT /api/apps/:slug/health/:env
备份appcrane_run_backup_now、appcrane_get_backup_statusPOST /api/apps/:slug/backup/:env、GET /api/apps/:slug/backups
运行时日志appcrane_get_logsGET /api/:slug/logs/:env
审计日志—(在控制面板中查看)GET /api/audit、GET /api/:slug/audit

向生产环境部署需要在应用上拥有 deploy.production 权限; 沙箱是所有地方的默认环境。可能会销毁未挂载状态的部署会被拒绝, 除非传入 acknowledge_data_loss——参见 功能中的重新部署警告。

不通过 GitHub 部署

应用不需要代码仓库。创建应用时将 source_type 设为 "upload",并以资源包(.zip、.tar.gz、.tgz)的形式发布版本:

curl -F [email protected] -F env=sandbox \
     -H "X-API-Key: $CRANE_KEY" \
     https://<your-domain>/api/apps/myapp/deploy/upload

响应中包含 artifact.sha256——AppCrane 会在解压前对收到的字节计算哈希,并将其记录为版本标识 (commit_hash = sha256:<digest>)。将其与本地计算的摘要进行比较,以确认部署内容与发送内容一致。你传入的任何 commit_sha 都会作为上下文一并存储,但明确不会被视为可信的标识。

Agent 使用个人 MCP 密钥(dhk_mcp_*),这些密钥仅允许访问 /api/mcp 和 /api/files/staged,因此它们通过两个步骤走相同的路径:先用 POST /api/files/staged 上传字节,再调用 appcrane_deploy_artifact(slug, env, token)。这是基于代码仓库的路径 发生故障时仍然可用的部署路由——服务账户 PAT 过期会导致所有托管代码仓库写入操作返回 401,而此路由不会联系 GitHub。

MCP(面向 AI Agent)

AppCrane 以 MCP 为先。执行一次 claude mcp add,Agent 就能使用 59 个 appcrane_* 工具——列出应用、部署、回滚、设置 / 获取密钥、读取 日志、管理访问权限、扫描易受攻击的依赖项等。工具 名称遵循 AWS 的命名方式(stage、set_secret/get_secret、cp)。

claude mcp add --transport http appcrane https://crane.example.com/api/mcp \
  --header "X-API-Key: dhk_admin_or_user_xxxxxxxxxxxxx" \
  --header "X-Github-Token: ghp_your_github_pat"

然后在任意 Claude Code 会话中:

为新应用完成接入。先调用 appcrane_get_guide,并设置 topic="onboarding",获取操作手册。

Agent 会从服务端获取最新指南,因此修改内容会立即生效,无需重新部署工具。topic="operations" 会返回接入后的参考指南(部署生命周期、排查快速失败、访问权限管理等)。

架构

Ubuntu Server
├── Caddy (reverse proxy, auto-HTTPS)
│   ├── myapp.example.com          → production app
│   └── myapp-sandbox.example.com  → sandbox app
├── Docker (container isolation)
│   ├── myapp-production           ← isolated container per env
│   └── myapp-sandbox
├── AppCrane API (:5001)
│   ├── Express 5 + SQLite
│   ├── Health checker (cron)
│   ├── SSO (SAML / OIDC / SCIM)
│   ├── AppStudio (request → plan → code) + coder
│   └── Presence (WebSocket)
└── /data/apps/myapp/
    ├── production/releases/       (symlink-based, last 5)
    └── sandbox/releases/

每个容器都只发布到 loopback(127.0.0.1:<port>:3000),因此只能通过 Caddy 访问。例外情况是 ingress_type 为 tcp 或 dual 的应用,它还会将端口发布到 0.0.0.0:<public_port>——这不经过 Caddy,也不受 Caddy 提供的任何控制。tcp 应用会直接发布容器端口 3000;dual 应用会发布另一个容器端口,并将 3000 留在 Caddy 后面,仅对 loopback 开放。请参阅下文 §6。 AppCrane 只信任 TRUST_PROXY 中指定代理的 X-Forwarded-For(默认值为 loopback,即同一主机上的 Caddy),因此每客户端速率限制、登录节流和记录的地址都能获取真实客户端信息;如果代理运行在其他地址,请将 TRUST_PROXY 设置为该地址——不接受 true 和跳数,因为它们会让任何客户端自行选择其地址。

安全

  • 初始化仅限本地主机——只能从服务器本机进行管理员设置
  • API 密钥身份验证——所有请求都必须包含 X-API-Key 标头
  • 管理员隔离——管理员无法读取环境变量或 /data/;在中间件层强制执行
  • 静态环境变量使用 AES-256-GCM 加密
  • GitHub Webhook HMAC 验证
  • SCIM 取消配置——从 IdP 中移除用户后,会自动撤销其 AppCrane 访问权限
  • 审计所有操作——记录操作人、操作内容和时间
  • 扫描你托管的应用中的漏洞,而不只是平台本身——每次部署都会扫描刚刚发布的内容,并将结果记录到相应部署中。源代码应用会从其锁文件中读取依赖,并查询 OSV:package-lock.json、yarn.lock(classic 和 Berry)、pnpm-lock.yaml(9.x)、composer.lock、go.sum、Cargo.lock、Gemfile.lock、poetry.lock、Pipfile.lock。使用镜像部署的应用会通过固定版本的 Trivy 扫描解析后的摘要——name@sha256:…,即实际运行的字节,而不是标签当前指向的内容。扫描只会报告,从不阻止部署:扫描器缺失、运行缓慢或出错都不会导致部署失败。每项发现都会附带修复该问题的版本,该版本从完整的 OSV 公告中读取——批量端点只返回公告 ID;如果无法获取公告,则会将扫描记录为错误,而不会声称不存在修复版本。覆盖情况以数字报告,而不是用一个词概括:每份报告开头都会显示 COVERAGE: <covered> of <rows> app/stage rows have a usable scan result (<percent>%),说明跳过、出错或从未扫描的数量,并根据每行记录的原因列出跳过行的主要原因。assurance 仍会显示 none / partial / complete,含义也完全不变——但 partial 的范围是 1% 到 99%,因此始终会附带具体数字。

提交验证与构建并行运行,而不是在构建前执行——GitHub head-SHA 交叉检查会在克隆完成后立即启动,并在容器关卡处等待结果,因此其耗时(GitHub 状态正常时约为 27 毫秒,触发重试退避时为 1.5–4.5 秒)会隐藏在 docker build 期间,而不会延迟构建;验证失败仍会以相同消息中止部署,且不会启动或停止任何容器,相关日志行也仍会出现在部署日志中——只是现在位于构建输出之后,而不是之前。

镜像构建固定使用 BuildKit(DOCKER_BUILDKIT=1),而不是继承主机 Docker CLI 的默认设置;在此处使用约 126 MB 的 Node 镜像进行冷构建时,测得耗时约为 18.6 秒,而经典构建器耗时约为 29.0 秒,且输出完全相同。经典构建器现在每次使用时都会打印移除通知——如需覆盖此设置,请在 AppCrane 主机上设置 APPCRANE_DOCKER_BUILDKIT=0。

供应链 — SBOM + 构建来源证明

部署会直接从 git 自动更新(/api/self-update 会运行 git fetch

  • git reset --hard origin/main),因此审查者会问:“我怎么知道拉取的源代码就是你们发布的源代码?”每个带标签的版本都会附上四个制品来回答这个问题:
制品说明
appcrane-<tag>-source.tar.gz带标签目录树的可复现 git archive(仅包含已跟踪文件)
appcrane-sbom.cdx.json生产环境依赖树的 CycloneDX SBOM
appcrane-sbom.spdx.json相同内容,采用 SPDX 格式
SHA256SUMS.txt上述所有文件的校验和

源代码归档附带通过 sigstore 无密钥签名的构建来源证明和 SBOM 证明(GitHub 制品证明)——没有可被盗取的长期签名密钥。使用以下命令验证下载的归档:

gh attestation verify appcrane-<tag>-source.tar.gz --repo gitayg/appCrane

有意将开发依赖排除在 SBOM 之外——它们不会部署,因此将其纳入会夸大实际攻击面。

上传的版本也会获得相当于提交 SHA 的标识,而不会被排除在这个问题之外。AppCrane 会在解压前于服务器端对软件包进行哈希,并将该摘要存储为版本标识——因此,即使应用没有代码仓库,也能回答“当前运行的内容是否经过审查?”这个问题。在 v2.53.0 之前并非如此:commit_hash 保存的是上传者输入的任何内容,或者字面字符串 unknown,两个毫不相关的软件包也可能声称拥有同一个 SHA。

已部署应用的身份契约

在 AppCrane 上部署的应用无需自行实现身份验证。Caddy 代理会在将每个请求转发到容器之前,针对 /api/identity/verify 验证请求,并通过三种互补方式将结果传递给应用。应用应按以下优先顺序使用这些方式:

维护现有开源应用,并想了解更深入的集成能带来什么?SKIFF.md 是面向维护者的指南——逐级介绍身份、应用定义的角色、托管数据库、电子邮件、运行状况和租户数据,并说明各项契约。

1. 请求标头(零次请求,推荐)

Caddy 会通过 copy_headers 将已验证的身份复制到上游代理请求中。应用可直接读取这些标头:

标头值说明
X-AppCrane-Auth-Modeauthenticated | headless | bypass每个代理请求中始终存在,包括没有身份的请求。请先读取此标头。
X-AppCrane-User电子邮件向后兼容的单一标识符。在 authenticated 请求中设置。
X-AppCrane-User-Id数字 ID(字符串)在 authenticated 请求中设置。
X-AppCrane-User-Email电子邮件细分标识符。如果用户没有电子邮件,可能不存在。
X-AppCrane-User-Name显示名称,经 encodeURIComponent 编码读取时使用 decodeURIComponent。可能不存在。
X-AppCrane-User-Roleplatform_admin | user平台级别的权限层级,原始令牌。不是应用级权限。(全局 admin 层级已弃用。)
X-AppCrane-App-Roleowner | admin | user | viewer应用级角色——应以此作为权限判断依据。显式的 app_user_roles 行优先于平台管理员回退规则,因此拥有该应用的平台管理员会以 owner 而非 admin 的身份到达。
X-AppCrane-Is-Admin1 | 0当应用级角色为 admin 或 owner 时为 1。请使用此标头,而不要比较角色字符串。
X-AppCrane-App-Roles逗号分隔的键,例如 approver,auditor应用自行定义的角色——与上面的 X-AppCrane-App-Role 属于不同系统。AppCrane 负责存储和颁发这些角色;应用负责执行这些角色,AppCrane 的授权检查不会读取它们。用户可持有多个角色(取并集,而非层级)。用户不持有任何角色时会完全省略此标头,因此 split(',') 不会生成虚假的 '' 角色。见下文第 5 节。
信任模型:Caddy 生成器会将 request_header -X-AppCrane-* 清除规则和 forward_auth 块包在 route { … } 中,使它们按书写顺序执行——否则,Caddy 自身的指令排序会在 forward_auth 之后执行清除规则,删除刚刚复制的身份信息。Caddy 会先清除客户端设置的所有 X-AppCrane-* 请求头,然后 copy_headers 只重新注入 /verify 返回的内容。清除规则会应用于每条代理应用的路由,包括无头应用,以及不运行 forward_auth 的 auth_bypass_paths 前缀——不验证任何人的路由不得接受调用方自己的 X-AppCrane-Is-Admin。因此,经由代理到达的内容由平台签发。有两种路由会绕过代理:原始 tcp/dual 数据平面端口,以及仅限 Docker Desktop(实测可用;在 Linux 上受阻)时,其他应用容器通过 host.docker.internal 访问本应用的回环端口。对于这些情况,v2.97.0 会对身份信息签名:/verify 会添加 X-AppCrane-Identity-Ts 和 X-AppCrane-Identity-Sig,对身份请求头使用每个应用和环境各自的密钥进行 HMAC-SHA256 签名(沙盒和生产环境使用不同密钥),并通过 APPCRANE_IDENTITY_SECRET 注入应用。应用可从 appcrane-tenant(1.2.0+)调用 verifyIdentity(req),拒绝任何未经 AppCrane 签名的内容;设置了密钥后,tenantKey()/tenantDb() 会自动执行此操作。被入侵的应用只能持有自己的密钥,因此既无法伪造其他应用的身份,也无法在其他位置重放自己的身份。Caddy 还会在平台的 cc_token 会话 Cookie 到达任何容器之前将其从 Cookie 中清除(v2.39.0),因此应用无法读取访客的平台会话并冒充访客——应用必须从这些请求头获取身份信息,绝不能从 Cookie 获取。

身份信息不需要 SSO。/api/identity/verify 会根据 identity_sessions,通过 X-API-Key 或 Authorization: Bearer / cc_token Cookie 解析会话。SSO 是创建此类会话的一种方式;本地密码登录和 API 密钥也是。没有 IdP 的实例仍会为已登录用户注入完整的请求头集合。

缺失语义:对于 authenticated 应用,未经验证的访客根本无法到达容器(Caddy 会在 forward_auth 处默认拒绝并重定向到 /login),因此存在即可信。身份信息确实缺失,意味着 X-AppCrane-Auth-Mode 为 headless(整个应用选择不使用身份验证)或 bypass(此路径位于 auth_bypass_paths 中,或应用通过自己的自定义域名提供服务)——在每种情况下,请求都在没有已验证身份的情况下提供服务,应用自行负责身份验证。完全没有 X-AppCrane-Auth-Mode 意味着请求并非经由 AppCrane 的代理——也就是直接访问容器。自定义域名应用会经过代理,且确实会收到 X-AppCrane-Auth-Mode: bypass。

角色顺序:none < viewer < user < admin < owner。appRole === 'admin' 是一个错误——它会拒绝所有者。

// Express example
const RANK = { none: 0, viewer: 1, user: 2, admin: 3, owner: 4 }
const atLeast = (appRole, min) => (RANK[appRole] ?? 0) >= RANK[min]

app.use((req, res, next) => {
  const mode    = req.get('X-AppCrane-Auth-Mode')   // 'authenticated' | 'headless' | 'bypass'
  const role    = req.get('X-AppCrane-User-Role')   // platform tier
  const appRole = req.get('X-AppCrane-App-Role')    // 'owner' | 'admin' | 'user' | 'viewer'
  const email   = req.get('X-AppCrane-User-Email') || req.get('X-AppCrane-User')
  req.user = (mode === 'authenticated' && role)
    ? { id: req.get('X-AppCrane-User-Id'), email, role, appRole, isAppAdmin: atLeast(appRole, 'admin') }
    : null
  next()
})
2. GET /api/me(需要获取超出基本信息的内容时)

返回完整的用户对象——姓名、电子邮件、用户名、全局角色——以及调用方所查询应用的角色。与应用同源,因此浏览器会自动发送 cc_token;无需 SDK 或令牌处理:

const r = await fetch('/api/me')        // ?app=<slug> optional; Referer-inferred otherwise
if (r.status === 401) { location.href = '/login?redirect=' + encodeURIComponent(location.href); return }
const { user, app_role } = await r.json()

/api/me 内部的身份验证优先级:

  1. cc_token Cookie(代理应用的默认方式——httpOnly,由浏览器管理)。
  2. Authorization: Bearer <session>(CLI / 程序化调用)。
  3. X-API-Key: dhk_*(管理员 / Agent 密钥)。

应用标识解析:

  1. 显式指定的 ?app=<slug> 查询参数。
  2. 根据 Referer 推断(第一个路径段;重试时附加沙盒后缀)。
  3. 若两者都无法解析,则返回精简的全局信息载荷。
3. 无头应用 — 完全选择退出

对于整个应用都不需要身份验证的服务(遥测接收端、公共 Webhook、状态页面、squash CLI 的 ping/stats),请将应用的 auth_mode 设为 headless(在 Launcher 中由所有者专属开关控制,或通过 MCP 调用 appcrane_set_app_meta slug=<…> auth_mode=headless)。此时 Caddy 配置块会跳过 forward_auth 和 copy_headers:不会添加身份标头、不会提供 /api/me,也不会提供 cc_token(该 Cookie 对所有应用都会被剥离)。剥离传入的 X-AppCrane-* 标头不会跳过——无头路由不会验证任何人,因此也不能允许调用方自行提供身份标头。X-AppCrane-Auth-Mode: headless 仍会到达应用,因此应用可以区分“按设计关闭身份验证”和代理配置错误。应用自身的服务器负责所需的任何负载级身份验证(HMAC、安装 ID、IP 允许列表等)。

根据应用形态选择:

  • 整个应用都是未经身份验证的接收端 → 使用无头应用(隔离清晰,影响范围更小)。
  • 应用大多需要身份验证,但有少数公共端点 → 保持 authenticated,在应用自身的路由器中限制公共路径。
4. 按租户使用数据库(多租户)— 选择启用

在 deployhub.json 中设置 "multitenant": true 即可选择启用,AppCrane 会为应用的每位用户在持久化 /data 卷上提供独立的 SQLite 数据库——无需自行实现租户隔离。租户是 (组织、用户),其中组织是用户的电子邮件域名。这是完全可选的:未设置此标志的应用完全不受影响。

启用后,AppCrane 会注入 APPCRANE_TENANT_ROOT=/data/tenants。使用 appcrane-tenant 辅助工具,根据上文(第 1 节)的身份标头派生租户数据库——无需手动构造路径:

import { tenantDb } from 'appcrane-tenant'

app.get('/api/notes', (req, res) => {
  const db = tenantDb(req)   // opens /data/tenants/<org>/u<userId>/db.sqlite
  res.json({ notes: db.prepare('SELECT * FROM notes').all() })
})

如果使用其他 SQLite 驱动,tenantDbPath(req) 只会返回路径。传入 tenantDb(req, { migrations: [step1, step2, ...] }),每个租户的数据库文件会在部署后首次打开时延迟升级,依据 PRAGMA user_version 判断,并在单个事务中完成(因此即使在并发请求下,也不会重复升级文件或留下升级到一半的文件)。请追加迁移步骤,切勿修改已发布的步骤。每个租户还会获得一个用于存放文件的 storage/ 目录(tenantStorageDir(req) / tenantFile(req, name))。在 deployhub.json 中设置 "tenant_quota_mb": <n>,即可限制每个租户的用量——AppCrane 会注入此值,租户空间用满后,assertTenantQuota(req) 会抛出异常(配额涵盖数据库和存储)。

始终通过辅助工具构造租户路径(绝不使用未经处理的用户输入)——身份标头由平台签名,组织标识也经过清理,可防止路径遍历。无论用户通过哪种方式(仪表板、删除用户、SCIM 组或 MCP 撤销工具)被撤销访问权限,AppCrane 都会自动清除该租户的目录;清除时使用的正是此辅助工具中的 orgFromEmail。平台会导入该函数而非复制代码,因此删除的路径与应用写入的路径一致。消费者邮箱域名(例如 gmail.com)会共用同一个组织标签,但隔离是按用户进行的,因此数据绝不会混在一起。该辅助工具尚未发布到 npm——请复制 packages/tenant/index.js,或通过路径依赖;请参阅 multitenant-notes 示例。

5. 应用定义的角色——应用自己的词汇

应用可以定义自己的角色——approver、auditor、reviewer——而且 AppCrane 会在每次请求时将每位用户的角色集合传递给应用。AppCrane 是权威方,应用是执行方:平台存储谁持有哪些密钥并负责签发,但不会规定密钥允许做什么。

这两个系统有意分开,连数据表和线路字段也各自独立。应用定义的角色绝不会授予 AppCrane 特权,AppCrane 的任何授权检查也不会读取它——否则,应用所有者就可以创建名为 admin 的角色,分配给自己,再通过设置表单自行提升权限。出于同样的原因,owner、admin、user、viewer、none 和 platform_admin 都不允许作为键,键必须匹配 /^[a-z][a-z0-9_-]{0,31}$/,并且每个应用最多可定义 16 个角色(这样也就能从设计上限制标头长度,而无需事后发现问题)。

  • 在服务端:X-AppCrane-App-Roles,以逗号分隔并排序;用户未持有任何角色时不发送。用户可以持有多个角色——它们是并集,因此应检查集合成员关系,而不是是否相等。该字段会从客户端请求中剥离,并像其他身份标头一样由 /verify 重新签发。
  • 在浏览器中:GET /api/me?app=<slug> 会在 app_role 旁返回 app_roles: [...](没有角色时为 [])。
  • **仅限显式授权。**除非有人授予 platform_admin 应用定义的角色,否则它不会持有任何此类角色;应用的 owner 也一样——与 app_role 不同,这里没有平台管理员回退机制。平台上的普通 user 持有应用角色是常见情况,而非边缘情况。
  • 由应用自身的 owner/admin 层级管理,可通过 /api/apps/<slug>/app-roles(注意:不是平台层级的 /roles),或使用 appcrane_list_app_roles / appcrane_create_app_role / appcrane_set_user_app_roles MCP 工具。删除角色会级联删除其授权。
const appRoles = new Set((req.get('X-AppCrane-App-Roles') || '').split(',').filter(Boolean))
if (!appRoles.has('approver')) return res.status(403).json({ error: 'approver role required' })
6. TCP(第 4 层)入口——无代理、无身份

第 1–5 节都基于同一个前提:应用前面有 Caddy。有些应用不是 HTTP 服务,根本无法通过代理转发——典型场景是正向/CONNECT代理,客户端会打开原始 TCP 连接并建立隧道,而 HTTP 反向代理无法表达这种连接。对于这类应用,平台管理员(而非应用所有者)可以设置 ingress_type: 'tcp'——通过 PUT /api/apps/<slug> 或 appcrane_set_app_ingress——之后,AppCrane 会在容器下次重新创建时,将生产容器的端口发布到主机的 0.0.0.0:<public_port> 上(部署或使用重启路由时均会重新创建——发布端口是 docker run 标志,因此运行中的容器不会发生变化)。现有的回环地址发布仍会保留,沙盒不受影响;这相当于新增一个入口,而不是迁移入口。

两段端口范围,数值并不相同。AppCrane 自动分配的主机端口来自专用区间 31000 到 31999,方便运维人员为一个可预测的端口段配置防火墙。显式指定的主机端口则可以是 1024–65535 之间的任意值——因为客户端可能由人工或通过 MDM 配置端口,而平台无权强行更改已经指向 8080 的设备群。缩小范围从来不是安全保障:限制规则适用于所有端口值(WHATWG 屏蔽的端口、AppCrane 自身的监听端口、槽位分配器可能分配给容器的任何端口,以及确保一个主机端口只分配给一个应用的部分唯一索引)。自动分配区间之外的端口只需单独配置防火墙规则。 无论其 ingress_type 是什么,容器都必须仍能通过 HTTP 响应 /api/health:部署门会轮询 30 s;如果没有返回包含 status 和 version 的 200 响应,就会回滚该版本。因此,tcp ingress 适用于同时支持 HTTP 的容器端口上的应用——促成本功能的 CONNECT 代理就是如此;目前,仅支持非 HTTP 协议的应用无法部署。还要注意,发布会覆盖整个容器端口,因此该端口上的每条 HTTP 路由都会一并暴露,包括健康检查端点和任何管理路由。

这个入口没有上述任何控制措施,也没有 AppCrane 在 v2.35–v2.41 中新增的那些措施:没有 forward_auth,没有 X-AppCrane-* 身份标头(不会注入或剥离这些标头),没有逐请求审计,没有速率限制,没有安全标头,也没有由 AppCrane 终止的 TLS。身份验证完全由应用负责。它不是 auth_mode: 'headless'——headless 应用仍会经过 Caddy,仍会获得 TLS、安全标头以及 X-AppCrane-Auth-Mode: headless 标记。

ingress_type: 'dual'——同时拥有两个平面的应用(v2.45.0)。有些应用确实同时拥有两个平面:一个 HTTP 控制平面(管理界面、REST API),必须保留 Caddy 提供的所有功能;还有一个原始数据平面,其客户端已绑定到特定主机端口。在 tcp 下无法表达这种情况,因为两个发布都指向同一个硬编码的容器端口——因此 tcp 只能重新暴露 Caddy 已在服务的那个端口。dual 应用会在容器内指定第二个端口:

control plane   Caddy → 127.0.0.1:<slot port> → container:3000
data plane      raw   → 0.0.0.0:<public_port> → container:<data_plane_port>

控制平面不受影响——URL、SSO、身份标头、访问日志都保持不变。只有数据平面没有防护,上方的损失表列出了它会失去什么。有三条规则确保这种区分确实成立:

  • data_plane_port 不能是 3000,如果请求设置了该值,就会被拒绝并返回 400。端口 3000 是容器的 HTTP 控制平面,也是 Caddy 代理的端口;直接发布它会在没有 TLS、没有 forward_auth、没有身份标头、也没有审计的情况下,重新暴露应用的普通 HTTP 源站——这恰恰是 Caddy 介入以保护的攻击面,而且操作员不会收到任何提示,无法得知自己做了这件事。出于同样的原因,不带 data_plane_port 的 dual 也会被拒绝:发布必须指向某个容器端口,而这里唯一的其他端口是 3000,AppCrane 不会自行猜测。运行时也会拒绝这样的记录——它不会输出任何公开的 -p,而不是退回到 3000。
  • 健康检查遵循控制平面。tcp 应用无法响应 HTTP 探测,因此会进行 TCP 握手;dual 应用可以响应,所以仍会在容器端口 3000 上进行常规 HTTP 健康检查,并且永远不会探测其数据平面。只要原始监听器的套接字已绑定,握手就会成功,因此探测数据平面会让控制平面(用户实际访问的平面)已经卡死的应用仍报告健康,并让有问题的版本通过检查。
  • 主机端口必须唯一;容器端口则刻意不要求唯一。apps(public_port) 上的部分唯一索引仍确保一个主机端口只分配给一个应用。data_plane_port 没有此类约束,也不应该添加:容器网络命名空间彼此隔离,因此两个应用可以各自在容器端口 8081 上运行数据平面,互不影响。

dual 是第三个枚举值,而不是 http 应用上的标志,因为操作员、审计记录和 MCP 载荷都会读取 ingress_type 字段,以了解应用有哪些入口——如果应用发布了原始端口,记录却写着 http,该字段就会完全错误。它还具有故障安全性:早于 dual 的代码会比较 === 'tcp',得到 false,然后走 HTTP 路径;对于 dual 应用来说,这正是正确的路径。未设置任何值的应用仍为 http,行为与之前完全一致;v2.45.0 之前的 tcp 应用仍会发布容器端口 3000,并且仍会进行握手健康检查。 发布到端口的应用仍可选择通过平台进行身份验证:GET /api/me?app=<slug> 会验证客户端提供的 Bearer 令牌或 X-API-Key: dhk_*,而 POST /api/service/* 则使用注入到每个容器中的 APPCRANE_SERVICE_TOKEN 对应用本身进行身份验证。两者都通过 docker 网桥(CRANE_INTERNAL_URL)传输,而不经过 Caddy。

public_port 按应用分配并存储(绝不根据可能被重新分配的应用槽位推导),在各应用之间保持唯一,且每次更改(包括 data_plane_port)都会以 app-ingress-change 记录到审计日志中。这两个端口号都会固定保留:切回 http 后仍然保留,因此切回发布模式时会恢复客户端集群已配置使用的端口;应用未发布时,读取结果则为 null。切回发布模式时会重新验证 data_plane_port,但不会重新验证 public_port——已保留的端口号会原样恢复,因此如果应用在平台扩容期间一直停留在 http 模式,请显式重新固定该端口。只要 dual 应用仍持有 data_plane_port,就不允许将其切换为 tcp,因为 tcp 会发布容器端口 3000,而切换会悄然将同一个主机端口重新指向控制平面;如要有意移除数据平面,请在同一请求中发送 data_plane_port: null。请将发布视为暴露服务的行为——不要假设主机防火墙会封锁该端口。Docker 发布会设置一条在 FORWARD 中评估、不会经过 INPUT 的 DNAT 规则,因此普通的 ufw deny 不会阻止它;请在 DOCKER-USER 链中或主机上游进行过滤。如果平台运行在 SDP 后方,实际存在的边界就是外围网络:容器重建后,发布的端口会立即对外围网络内的所有对象开放。

切回 http 会停止发布,但不会关闭端口:运行中的容器会一直保留该绑定,直到容器重建,因此在将暴露视为已撤销之前,请先重新部署或重启应用。由于端口仍处于绑定状态,AppCrane 会将其保留给该应用,而不是将其归还端口池——其他应用无法分配正在被运行中容器占用的端口号;当容器恢复为不发布该端口的状态时,AppCrane 会自动释放端口。在此之前,该应用在每个读取界面中都会将此端口号报告为 pending_port_release,确保不会在端口仍开放时声称其已关闭。

对于 CONNECT 代理而言,发布的端口可供所有已能访问主机的对象访问,因此在由 SDP 接入的部署中,可访问者是外围网络内的所有对象,而非互联网。因此,应用代理身份验证中的漏洞就成了未审计的外围网络外流路径;由于流量不会经过 Caddy,AppCrane 不会记录这些流量。应用的 407 Proxy-Authenticate 路径才是安全边界——入口流量不是。

权限模型

操作管理员应用用户
创建/删除应用是否
分配用户是否
服务器健康状态是否
部署/回滚/晋升否是(自己的应用)
查看/编辑环境变量否是(自己的应用)
配置健康检查/Webhook否是(自己的应用)
备份否是(自己的应用)

技术栈

Node.js 22、Express 5、SQLite、Docker、Caddy 2、SAML/OIDC/SCIM、AES-256-GCM、Commander.js、Ubuntu 22.04+

许可证

GNU AGPL v3。免费开源——可使用、修改和自托管。如果将修改后的版本作为网络服务运行,必须按照相同许可证公开源代码。需要将私有修改作为服务运行,或将 AppCrane 嵌入专有产品?现提供商业许可证。

反馈与贡献

提交问题:https://github.com/gitayg/appCrane/issues

欢迎提交拉取请求——请先阅读 CONTRIBUTING.md。其中包含一份简短的 CLA,以确保 AppCrane 能够继续采用双重许可(AGPL + 商业许可)。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。