跳转到内容

远程构建缓存协议

WARNING

Remote build caching is experimental. This document defines protocol version 1 for mise task artifact caching.

该协议是一种用于任务执行及其输出的安全、内容寻址缓存协议。它不会暴露 mise 的本地缓存目录、清单或归档格式。本地存储属于实现细节,可以使用归档或数据包,而无需改变远程协议。

版本 1 存储不可变的构建数据:

  • **内容寻址存储(CAS)**包含由其摘要标识的 Blob 和目录对象。
  • 操作结果将规范构建操作的摘要映射到其输出目录和日志。该模型可以对操作之间的内容进行去重,支持部分传输和并行传输,并允许服务器在发布缓存命中前验证所有引用的内容。

术语

  • 命名空间:不透明的授权与隔离范围,通常代表组织、代码库、分支、拉取请求或用户。
  • 操作:构建操作的类型化规范描述,以及所有会影响其结果的输入。
  • 操作结果:操作成功完成后发布的不可变记录。
  • Blob:CAS 中未经解释的字节。
  • 目录对象:CAS 中描述文件、子目录和符号链接的规范 JSON。
  • 摘要:算法、小写十六进制哈希值以及未压缩字节长度。
  • 提交:在验证所有引用的 CAS 对象后发布操作结果。

传输与版本控制

版本 1 使用 HTTPS 和 HTTP 语义。携带授权凭据的请求必须使用 HTTPS, 但回环开发服务器(localhost127.0.0.0/8::1)除外。客户端在发出可见警告后, 可以连接到未经身份验证的非回环 HTTP 服务。此模式既不提供机密性,也不提供服务器真实性:链路上的攻击者可以替换操作结果及其内部一致的 CAS 图。实现可以使用 HTTP/1.1、HTTP/2 或 HTTP/3。

每个 API 请求都会发送:

HeaderValue
mise-cache-protocol1
mise-cache-namespace操作所使用的命名空间,但发现端点除外

URL 前缀 /v1 是协议的主版本。兼容性新增功能会作为能力进行声明,无需新的 URL 前缀。不兼容的线路或完整性变更需要使用新的主协议;版本 1 不得作为不兼容实现的别名。

服务器必须忽略未知的 JSON 响应字段。除非协商的能力允许,否则客户端不得发送未知的请求字段。

GitHub Actions protected-branch push jobs and GitLab protected-branch push pipelines may use the configured write mode. Pull requests, tags/releases, unprotected branches, unknown CI systems, and local runs are restricted to reads; a configured write-only client disables its remote rather than silently broadening to read access.

摘要

摘要的 JSON 表示形式为:

json
{
  "algorithm": "blake3",
  "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "size": 1234
}

协议版本 1 定义了 blake3sha256。服务器会公布其接受的算法,并且必须支持 BLAKE3。操作描述符和操作结果键始终使用 BLAKE3;对于其他 CAS 对象,如果服务器公布支持 SHA-256,则可以使用 SHA-256。摘要始终涵盖确切的未压缩字节,并包含其长度。服务器必须拒绝格式错误的哈希值、不支持的算法、负数大小,以及与其声明摘要不匹配的内容。

摘要 URL 组件使用 /v1/blobs/{algorithm}/{hash}/{size}。算法和哈希值必须与 JSON 表示形式匹配,且 size 是无符号十进制整数。

能力

GET /v1/capabilities 无需命名空间,并返回协议和服务器限制:

json
{
  "protocol": { "major": 1, "minor": 0 },
  "digest_algorithms": ["blake3", "sha256"],
  "compressors": ["identity", "zstd"],
  "action_kinds": {
    "task": { "action_schema": 1, "metadata_schema": 1 }
  },
  "features": {
    "batch": true,
    "blob_packs": true,
    "resumable_uploads": true,
    "delegated_transfers": true
  },
  "limits": {
    "max_batch_items": 1000,
    "max_inline_blob_bytes": 1048576,
    "max_blob_bytes": 107374182400,
    "max_pack_bytes": 107374182400
  }
}

每个 action_kinds 条目都会公布服务器针对该类型验证的操作描述符和客户端元数据架构版本。除非服务器公布了该类型以及客户端实现的确切架构版本,否则客户端不得读取或发布非 task 操作。服务器必须拒绝未公布的类型和不受支持的架构版本。兼容的协议新增内容可以添加类型或新的架构版本,而无需更改主协议版本。

客户端必须遵守公布的限制,并在可选功能不可用时回退。对于不受支持的主版本,服务器返回 426 Upgrade Required,并在 mise-cache-protocol 中包含其支持的主版本。

GET /v1/status 是一个运行状况检查端点。成功响应表示 API 进程正在运行; 它不能替代能力协商或授权检查。

规范对象

协议 JSON 对象使用 UTF-8,并在其字节被哈希时使用 JSON 规范化方案(RFC 8785)。重复的对象键、无效的 UTF-8、非规范编码,以及无法由声明的模式表示的值都必须被拒绝。

操作描述符

操作描述符包含稳定的操作类型,以及所有声明会影响其结果的内容。 操作架构版本 1 定义了 task

json
{
  "version": 1,
  "kind": "task",
  "task": "build",
  "phase": "normal",
  "run": [{ "task": "cargo build --release" }],
  "args": [],
  "shell": null,
  "outputs": ["target/release/widget"],
  "root": "crates/widget",
  "source_hash": "blake3:...",
  "dependency_keys": [],
  "environment": { "PROFILE": "release" },
  "command_inputs": [],
  "vars": {},
  "tools": ["core:[email protected]"],
  "os": "linux",
  "arch": "x86_64"
}

顺序不具有操作含义的数组必须按照其模式定义的字段进行排序。版本字符串是不透明的,绝不会按语义排序。操作描述符中不得出现机密信息。任务仅在声明环境变量为缓存输入时,才会包含这些环境变量。每种操作类型都定义其自身的规范字段和可缓存性规则;服务器可以拒绝其未声明支持的类型。

source_hash 绑定所声明的源路径及其内容,而不会上传缓存专用操作不需要的任务输入。规范描述符存储在 CAS 中。其摘要即为操作摘要和操作结果 URL 键。描述相同操作的两个客户端必须生成完全相同的规范字节。

目录对象

目录对象的媒体类型为 application/vnd.mise.cache-directory.v1+json

json
{
  "version": 1,
  "directories": [
    {
      "name": "assets",
      "digest": { "algorithm": "blake3", "hash": "...", "size": 321 },
      "mode": 493
    }
  ],
  "files": [
    {
      "name": "widget",
      "digest": { "algorithm": "blake3", "hash": "...", "size": 123456 },
      "executable": true,
      "mode": 493
    }
  ],
  "symlinks": [{ "name": "current", "target": "widget", "mode": 511 }]
}

每个节点列表都按照 name 的 UTF-8 字节排序。名称必须是单一路径组件,不得为空、不得为 ...,不得包含斜杠或 NUL,也不得与其他节点冲突。在恢复期间,必须拒绝绝对符号链接目标,以及会逃逸出所声明输出根目录的目标。

可移植元数据集合包括文件内容、目录结构、符号链接、可执行状态,以及由 mode 表示的可移植权限位。所有者、组、时间戳、设备、套接字、FIFO、平台 ACL 和扩展属性都不会被恢复。硬链接可以作为独立文件恢复。不受支持的源对象会使任务结果不具备远程缓存资格,而不会被悄然更改。

操作结果

操作结果响应和提交正文的媒体类型为 application/vnd.mise.cache-action-result.v1+json

json
{
  "version": 1,
  "action": { "algorithm": "blake3", "hash": "...", "size": 789 },
  "output_root": { "algorithm": "blake3", "hash": "...", "size": 456 },
  "metadata": { "algorithm": "blake3", "hash": "...", "size": 234 }
}

只有成功且具备缓存资格的操作执行结果才可以发布。当操作没有输出文件时,不包含 output_rootmetadata 引用规范的 application/vnd.mise.cache-client-metadata.v1+json,其中包含类型化的客户端元数据。任务元数据包含输出根目录、捕获的输出、任务标识、恢复字节数估计值和执行时长。元数据模式属于远程协议的一部分,并且独立于 mise 的本地缓存清单。

json
{
  "version": 1,
  "kind": "task",
  "task_identity": "build:crates/widget",
  "roots": ["target/release/widget"],
  "output": [{ "stream": "stdout", "line": "built widget" }],
  "restored_bytes": 123456,
  "execution_duration_ns": 900000000
}

每种元数据类型都有带版本的模式。任务根路径使用正斜杠,相对于任务工作目录,并且必须满足与目录节点相同的路径安全规则。任务输出条目保留其声明顺序。

元数据中的 kind 必须等于所引用操作描述符的 kind。即使两个对象分别独立满足各自的模式,服务器也会在发布前拒绝类型不匹配的提交。

操作描述符以及从结果可到达的每个对象都必须存在并通过验证,结果才会变得可读。

保留期限、最后访问时间、配额核算、内部存储位置和服务器注释都不属于不可变操作结果。

CAS 操作

查找缺失的 blob

POST /v1/blobs:missing 接受 application/vnd.mise.cache-digests.v1+json

json
{ "digests": [{ "algorithm": "blake3", "hash": "...", "size": 1234 }] }

对于已验证的 CAS 中不存在的对象,返回其中的子集,状态为 200 OK

json
{ "missing": [{ "algorithm": "blake3", "hash": "...", "size": 1234 }] }

服务器不得披露请求者可读命名空间或 CAS 可见性域之外是否存在对象。

读取 blob

GET /v1/blobs/{algorithm}/{hash}/{size} 在调用方可以读取对象时返回 200 OK,否则返回 404 Not Found。响应包含 DigestContent-Length 元数据。服务器可以支持 Range,也可以返回协商后的 Content-Encoding: zstd;URL 中的摘要始终描述 未压缩的字节。

声明支持委托传输的服务器可以将请求重定向(307 Temporary Redirect)到一个短时有效的 HTTPS URL。重定向只能授予对所请求不可变对象的访问权限。客户端不得将缓存服务的 Authorization 标头转发给委托主机。

客户端在使用下载内容前会验证完整的未压缩摘要。摘要不匹配时视为缓存未命中,发出可见的完整性警告; 启用报告能力后,还必须将其报告到服务器遥测系统。

读取 Blob 数据包

声明 features.blob_packs 的服务器接受针对 /v1/blobs:packPOST 请求,请求正文与 blobs:missing 使用相同的 application/vnd.mise.cache-digests.v1+json 格式。声明的总大小 不得超过 limits.max_pack_bytes,摘要数量不得超过 limits.max_batch_items。超过项目数量限制时,服务器返回 400 Bad Request;声明的总大小超过 字节限制时,返回 413 Content Too Large

成功响应使用 application/vnd.mise.cache-blob-pack.v1,并以八字节 ASCII 魔数 MISEPK01 开始。其余部分是按请求顺序排列的帧流:

FieldEncoding
Algorithmone byte: 1 for BLAKE3 or 2 for SHA-256
Hashraw 32-byte digest
Sizeunsigned big-endian 64-bit byte length
Contentexactly size bytes

服务器会省略缺失和未经授权的 Blob,并且每个重复请求只输出一次。客户端会拒绝未请求或重复的帧,将每个帧流式传输到有界临时存储中,验证其完整摘要,然后才将其纳入本地 CAS。当能力不可用、摘要超过公布的数据包限制,或预期 Blob 被省略时,客户端会回退到普通的单 Blob 读取。数据包仅是传输优化;其帧格式不会改变 CAS 标识或操作语义。

上传 blob

较小的 blob 可以直接通过 PUT /v1/blobs/{algorithm}/{hash}/{size}If-None-Match: * 发送。服务器返回:

  • 验证并发布新内容后返回 201 Created
  • 已存在相同的已验证内容时返回 204 No Content
  • 字节内容与摘要不匹配时返回 400 Bad Request
  • 不可变前置条件失败时返回 412 Precondition Failed
  • 超出公布的限制时返回 413 Content Too Large

较大或可恢复的上传使用上传会话:

  1. POST /v1/uploads 声明一个或多个摘要。
  2. 服务器返回上传 ID、过期时间、偏移量,以及服务器上传 URL 或委托上传 URL。
  3. 客户端上传分块,并在必要时从服务器确认的偏移量处恢复。
  4. POST /v1/uploads/{id}/finalize 验证完整内容,并将其提升到 CAS 中。

委托上传始终以隔离的暂存键为目标,绝不会使用可读的 CAS 键。因此,仅提供预签名的 S3 上传是不够的:最终确认发布前必须验证所声明的摘要。过期或被放弃的暂存对象会被异步删除。

操作结果操作

GET /v1/action-results/{algorithm}/{hash}/{size} 返回已提交的操作结果,或返回 404 未找到。命名空间标识该请求的单一读取范围。配置了多个读取范围的客户端会按照策略顺序查询这些范围,而不是发送含义不明确的多命名空间请求。

PUT /v1/action-results/{algorithm}/{hash}/{size} 提交操作结果。该请求要求使用 If-None-Match: *。服务器必须以原子方式:

  1. 授权对命名空间的写入;
  2. 验证 URL 摘要与结果及存储的操作描述符相匹配;
  3. 验证结果及所引用的元数据架构;
  4. 验证操作描述符与客户端元数据类型相匹配;
  5. 验证完整的可达目录和 Blob 图;
  6. 发布不可变映射。

响应为 201 已创建;对于相同的已提交结果,响应为 204 无内容;当已有不同结果占用该操作键时,响应为 409 冲突;当缺少不可变前置条件或前置条件验证失败时,响应为 412 前置条件失败。并发的有效写入者可以上传相同的 CAS 数据,但只有一个操作结果提交能够成功。

普通缓存写入者不会获得删除权限。管理删除使用单独授权的端点,并且必须在不可达 CAS 数据被垃圾回收前删除操作结果映射。客户端缓存清除操作不得意味着拥有删除共享远程数据的权限。

身份验证与命名空间策略

该协议支持持有者令牌、OIDC 派生令牌、mTLS 以及受信任的反向代理身份。
身份验证机制的发现属于部署配置,而不是 CAS 对象元数据。
凭据必须进行作用域限定,并从诊断信息中脱敏。

服务器分别授权读取和写入。标准的 CI 策略是使用一个共享命名空间:受保护的分支可以写入,而拉取请求作业只能读取。服务器根据已验证的 OIDC 声明(例如仓库、引用、事件和工作流身份)执行这一策略;客户端提供的远程模式属于纵深防御措施,而不是授权边界。

不可变存储无法阻止首个写入者进行缓存投毒。因此,即使后端对象存储拒绝覆盖,也仍然需要基于 OIDC 的命名空间授权。由受信任和不受信任作业共享的单个存储桶凭据不构成符合要求的安全边界。

失败与重试行为

  • 401 Unauthorized 表示缺少身份验证或身份验证无效。
  • 403 Forbidden 表示该身份无权访问请求的命名空间或执行请求的操作。
  • 404 Not Found 表示缓存未命中,且不得泄露无法访问的对象。
  • 409 Conflict 表示不可变操作结果冲突。
  • 412 Precondition Failed 表示缺少条件写入要求,或该要求未通过。
  • 422 Unprocessable Content 表示对象编码有效,但其引用图无效。
  • 426 Upgrade Required 表示主版本不匹配。
  • 429 Too Many Requests5xx 响应可以使用有界指数退避和 抖动进行重试,并遵循 Retry-After

缓存不可用、对象格式错误、引用的对象缺失以及完整性失败通常会降级为缓存未命中,以便本地任务继续执行。身份验证、授权和完整性失败仍必须产生可见警告;客户端不得将其静默标记为普通未命中。部署可以启用严格模式,使选定的失败成为致命错误。

幂等性密钥可以用于上传会话创建及其他可重试的 POST 操作。服务器必须限制其保留时间,并将其作用域限定为经过身份验证的身份和命名空间。

自托管存储要求

符合规范的自托管服务器可以使用文件系统、兼容 S3 的对象存储或其他 Blob 存储。客户端与缓存服务通信,而不是获取通用对象存储凭据。

官方参考服务器单独维护于 jdx/mise-cache。它提供文件系统和兼容 S3 的 Blob 存储、PostgreSQL 元数据、命名空间范围的授权、Docker Compose 以及 Helm chart。服务器的部署和发布生命周期仍与 mise 客户端分离,而本文档是规范协议的权威说明。

使用 S3 的服务器应:

  • 将操作元数据、授权信息、访问时间、引用和配额存储在事务性元数据存储中;
  • 将 CAS 字节存储在基于摘要派生的不可变键下;
  • 为委托上传使用随机暂存键;
  • 使用有条件的对象创建,并拒绝普通覆盖和删除权限;
  • 仅在验证每个可达对象后最终确定操作结果;
  • 对过期的暂存上传和不可达的 CAS 对象执行垃圾回收;
  • 支持短期工作负载凭据和静态数据加密。

对象存储版本控制、保留锁和加密是有用的纵深防御措施,但不能替代应用授权或摘要验证。

一致性

该仓库的兼容性套件是版本 1 所需行为的可执行定义。 它必须涵盖能力协商、规范对象验证、命名空间隔离、独立的读写授权、缺失 Blob 批次、流式传输和可恢复传输、摘要拒绝、 操作结果的原子提交、不可变冲突、委托传输凭据隔离、损坏处理以及重试语义。

服务器可以在 /v1 之外实现额外的管理、指标和健康检查 API。这些 API 不得削弱版本 1 的缓存不变量。