Gloss 将自维护的 BabelDOC 作为独立 runtime 更新,不再要求用户从系统 PATH 安装任意版本。
App 只接受 SunChJ/BabelDOC Release 中由 Gloss 发布密钥签名的 manifest。
默认更新地址:
| 通道 | Manifest |
|---|---|
| stable | https://github.com/SunChJ/BabelDOC/releases/latest/download/gloss-runtime-manifest.json |
本轮发行只开放 stable。beta 与 nightly 枚举值为后续兼容而保留,但不会出现在 App
可选通道中,调用 setChannel 也会在对应 release alias 与签名产物上线前明确拒绝。
每个 manifest 必须有相邻的 detached Ed25519 签名
gloss-runtime-manifest.json.sig。签名覆盖 manifest 文件的原始 bytes;签名文件可以是 64-byte raw
signature 或其 Base64 文本。Gloss 内置并固定 raw 32-byte public key:
0lgbX+CkmBjf4BnH9JO66I7Krd1DYM8lTOjIt+7zWEE=
私钥只存放在 BabelDOC 仓库的 GitHub Actions secret,不能提交到任一仓库。签名或 SHA-256 不匹配时,更新会 fail closed,现用 runtime 保持不变。
Manifest schema v1 示例:
{
"schemaVersion": 1,
"channel": "stable",
"version": "0.6.4+gloss.4",
"releaseTag": "v0.6.4-gloss.4",
"publishedAt": "2026-07-23T10:16:56Z",
"minimumGlossVersion": "0.8.0",
"releaseNotesURL": "https://github.com/SunChJ/BabelDOC/blob/v0.6.4-gloss.4/docs/release-notes/v0.6.4-gloss.4.md",
"assets": [
{
"operatingSystem": "macos",
"architecture": "arm64",
"url": "https://github.com/SunChJ/BabelDOC/releases/download/v0.6.4-gloss.4/gloss-babeldoc-0.6.4-gloss.4-macos-arm64.tar.gz",
"sha256": "8eb8b5b7f629a39715e9e317306861fd1adb6b67b483b7cab89589b986595436",
"size": 228709309,
"archiveFormat": "tar.gz",
"executablePath": "gloss-babeldoc-runtime/gloss-babeldoc"
}
]
}Runtime archive 的入口必须位于 manifest 声明的 executablePath,且文件名为
gloss-babeldoc。安装器在解包前拒绝绝对路径、.. 和 Windows drive 路径,在激活前拒绝
符号链接/硬链接并验证可执行权限。下载与解包发生在相同 filesystem 的 staging 目录,完整
校验后才移动到版本目录;state.json 使用 atomic replace,因此下载中断或进程崩溃不会切换
active runtime。
BabelDOCRuntimeManager 是 actor,并暴露:
snapshot()/snapshots():当前、上一版、可用版本、更新通道、pin、操作状态与错误。currentVersion/currentExecutableURL:由 snapshot 提供给 executor 启动逻辑。checkForUpdates()/update():验证 detached signature 后检查或安装新版本。install(_:):显式安装一个已经验证策略的 manifest。pin(version:):固定 runtime 版本;其他版本的 manifest 会被拒绝。setChannel(_:):切换到已发布通道并清除旧 pin;当前仅允许 stable。rollback():原子交换 current/previous,保留一次快速回滚能力。reclaimableBytes():在确认卸载前计算运行时、历史版本与残留缓存占用。uninstall():先停止服务,再将组件内容隔离并原子提交未安装状态;保留通道与 pin 偏好, 不触碰源 PDF、翻译结果或输出目录,之后仍可重新安装。
默认数据目录是:
~/Library/Application Support/Gloss/BabelDOCRuntime/
├── state.json
└── versions/
├── 0.6.4+gloss.2-<sha-prefix>/
└── 0.6.4+gloss.4-<sha-prefix>/
测试和受控企业分发可以向 manager 注入 manifest URL、transport 和 Ed25519 public key,不需要 访问公网,也不会降低 production 默认校验。
.github/workflows/release.yml 在 v* tag 上:
- 分别在
macos-15arm64 和macos-15-intelx86_64 runner 构建默认轻量版与内置 Codex runtime 版Gloss.app,并显式使用GLOSS_SIGN_IDENTITY=-对 App 与 helper 做 ad-hoc codesign;Homebrew 产物不包含需要 Apple 身份配对的 Safari extension,也不执行 notarization 或 stapling。 - 生成默认的
Gloss-macos-arm64.zip、Gloss-macos-x86_64.zip,以及可选的Gloss-macos-arm64-with-codex.zip、Gloss-macos-x86_64-with-codex.zip。SHA256SUMS覆盖四个归档和两个 Cask;签名gloss-release-manifest.json继续把 App 内自动更新绑定到 默认轻量版的两个 architecture asset。 - 生成并校验使用
on_arm/on_intelURL 与 SHA-256 的根级 Release assetsgloss.rb与gloss-with-codex.rb。两个 Cask 显式互斥;Manifest 签名绑定默认 Cask 的 固定 token、URL、SHA-256 与 size;tap workflow 分别将它们落到同名Casks/文件。Manifest 和 Cask 中的下载地址固定指向公开仓库https://github.com/SunChJ/gloss-releases/releases/download/<tag>/。 - 始终上传私有主仓中的 Actions artifact,便于内部验证。
- 仅在 tag 事件中使用跨仓库 token,把两种 variant 的 app zip、校验和、manifest 与两个
根级 Cask 发布到公开的
SunChJ/gloss-releasesGitHub Release。 - Release 上传成功后,dispatch
SunChJ/homebrew-tap的update-cask.yml,由公开 tap 下载并二次校验 Release,再更新Casks/gloss.rb与Casks/gloss-with-codex.rb。
完整 App 会同时检出并构建私有浏览器扩展仓库。Release workflow 使用三个职责分离的凭据:
| Secret | 用途 |
|---|---|
GLOSS_EXTENSION_SSH_KEY |
只读检出私有 SunChJ/personal-immersive-translator |
GLOSS_APP_UPDATE_MANIFEST_SIGNING_KEY |
使用 Ed25519 对 App 更新 manifest 签名 |
GLOSS_DISTRIBUTION_TOKEN |
向公开 binary repo 上传 Release,并 dispatch 公开 tap workflow |
workflow 的第一个 job 始终检查 GLOSS_EXTENSION_SSH_KEY 和
GLOSS_APP_UPDATE_MANIFEST_SIGNING_KEY;tag 事件以及显式开启 publish_release 的手工恢复任务
还会检查 GLOSS_DISTRIBUTION_TOKEN。缺失即 fail closed,不会开始正式构建。手工
workflow_dispatch 默认不走 public publication 路径,因此不需要 distribution token,但仍需
只读 extension deploy key 与 manifest signing key 才能构建完整 App。
公开分发使用两个独立仓库,私有 SunChJ/gloss 不承载匿名下载:
SunChJ/gloss-releases:public;初始化main分支,仅承载发行说明、tag 和二进制 Release assets。SunChJ/homebrew-tap:public;初始化main分支,包含Casks/gloss.rb以及.github/workflows/update-cask.yml。
在 GitHub 创建 fine-grained personal access token,并按下面的最小边界配置:
- Resource owner 选择
SunChJ,Repository access 只选择SunChJ/gloss-releases和SunChJ/homebrew-tap。 - Repository permissions 设置
Contents: Read and write,用于在gloss-releases创建 tag/Release 和上传 assets。 - Repository permissions 设置
Actions: Read and write,用于 dispatchhomebrew-tap/.github/workflows/update-cask.yml。 - 将 token 保存为私有
SunChJ/gloss仓库的 Actions secretGLOSS_DISTRIBUTION_TOKEN。不要把 token 写入 workflow、日志、公开仓库或本地发行产物; 按 token 到期时间提前轮换。
同一个 fine-grained token 的权限会应用到所选的两个仓库,因此这里使用完成两项跨仓库操作所需
权限的并集。Token 不需要访问私有 SunChJ/gloss;workflow 通过该仓库自己的
GITHUB_TOKEN 只读检出源码。
为私有 SunChJ/personal-immersive-translator 创建独立 Ed25519 SSH key pair,把 public
key 添加为该仓库的 read-only deploy key,把 private key 保存为
GLOSS_EXTENSION_SSH_KEY。不要为 deploy key 启用 write access,也不要复用个人 SSH key。
GLOSS_DISTRIBUTION_TOKEN 不应访问私有扩展源码。
homebrew-tap 的 update-cask.yml 必须声明两个 required workflow_dispatch inputs:
release_tag 和 release_repository。它应只接受
release_repository == "SunChJ/gloss-releases",下载默认版与 -with-codex 版的四个
架构归档、SHA256SUMS、gloss.rb 与 gloss-with-codex.rb,执行 SHA-256 校验并确认
两个 Cask 内的版本、checksum、公开 URL、互斥声明和安全 postflight 后才更新对应的
Casks/ 文件。若 workflow 通过 PR 更新 main,还需在 tap 仓库
Settings → Actions → General
启用 “Allow GitHub Actions to create and approve pull requests”,并给该 workflow
contents: write、pull-requests: write。
本地生成发行元数据:
Scripts/build_app.sh
GLOSS_RELEASE_ARCHITECTURE="$(uname -m)" \
GLOSS_RELEASE_VARIANT=standard Scripts/package_release.sh
GLOSS_CODEX_RUNTIME_MODE=bundled Scripts/build_app.sh
GLOSS_RELEASE_ARCHITECTURE="$(uname -m)" \
GLOSS_RELEASE_VARIANT=with-codex Scripts/package_release.sh
# 收集在两类 Mac 上生成的四个 zip 后:
Scripts/generate_release_metadata.sh \
dist/release/Gloss-macos-arm64.zip \
dist/release/Gloss-macos-x86_64.zip \
dist/release/Gloss-macos-arm64-with-codex.zip \
dist/release/Gloss-macos-x86_64-with-codex.zip \
0.8.0 \
dist/release \
v0.8.0 \
SunChJ/gloss-releasesGloss 主仓不再包含会向自身提交 Cask PR 的 homebrew-cask.yml。正式 Release 成功后,它会
运行等价于下面的跨仓库 dispatch:
gh workflow run update-cask.yml \
--repo SunChJ/homebrew-tap \
--ref main \
-f release_tag=v0.8.0 \
-f release_repository=SunChJ/gloss-releases公开 tap 的校验全部通过后会自动合并生成的 Cask 更新,用户使用标准 tap 名称安装和升级:
brew tap sunchj/tap
# 默认轻量版(不内置 Codex)
brew install --cask sunchj/tap/gloss
# 可选完整版本(内置 Codex runtime;与默认版互斥)
brew install --cask sunchj/tap/gloss-with-codex
brew update
brew upgrade --cask sunchj/tap/gloss # 或 gloss-with-codex- 先发布兼容的
SunChJ/BabelDOCsigned runtime,并确认 stable manifest 可下载。 - 合并 Gloss 的发行提交,确认
Resources/Info.plist版本与准备创建的v*tag 完全一致。 - 确认两个公开仓库、
update-cask.yml、GLOSS_EXTENSION_SSH_KEY、GLOSS_APP_UPDATE_MANIFEST_SIGNING_KEY、GLOSS_DISTRIBUTION_TOKEN和 tap 的 Actions/branch protection 设置均已就绪。 - 在私有 Gloss 仓库的目标 commit 上创建并推送 tag,例如
v0.8.0。 - 等待 Gloss Release workflow 完成 ad-hoc 签名;workflow 会先创建 draft Release,上传全部 资产后再发布,最后 dispatch tap 更新。
- 在
SunChJ/gloss-releases验证默认版和-with-codex版的四个架构 zip、SHA256SUMS、gloss-release-manifest.json、签名文件与两个根级 Cask 均存在且 URL 指向 该公开 Release;tap 会将两个 Cask 落到对应的Casks/文件。 - 等待
SunChJ/homebrew-tap生成的 Cask PR 在 required checks 通过后自动合并;workflow 会在 arm64 与 x86_64 Mac 上分别安装测试默认版和内置 Codex 版,不要求人工批准。
SunChJ/gloss-releases 必须启用 GitHub release immutability。已发布 Release 的 tag 与资产
不可覆盖;相同 tag 的 workflow 重跑会 fail closed。上传中断时 Release 仍保持 draft,
重跑可以修复 draft 资产并重新发布。
如果公开 Release 已成功但 tap dispatch 失败,可以从 SunChJ/homebrew-tap Actions 页面手工
运行 update-cask.yml,输入相同的 tag 和固定 repository
SunChJ/gloss-releases。不要从私有 Gloss Release 或未经 SHA256SUMS 验证的临时 URL
生成公开 Cask。
如果 tag 触发的工作流在公开 Release 创建前因工作流本身失败,先在 main 修复工作流,再从
Gloss 的 Actions 页面手工运行 Release,输入原 release_tag 并显式开启
publish_release。恢复任务仍检出原 tag、校验 tag 与 App 版本完全一致,并拒绝覆盖已经发布的
不可变 Release;不要移动或重建失败的 tag。
这个渠道刻意不使用 Developer ID Application 证书、Apple notarization 或 stapled ticket:
- macOS 无法把
Gloss.app的签名绑定到经过 Apple 验证的发布者身份,也不会获得 Apple notarization 的恶意软件扫描与撤销信号。 - Cask 的下载 SHA-256 和公开 Release 的
SHA256SUMS能证明实际下载内容与 tap 固定的内容 相同,但它们不能替代发布者身份签名;gloss-releases、homebrew-tap或跨仓库 token 同时失守时,攻击者可能替换二进制与 checksum。 - custom tap 的
postflight按最深层优先顺序分别对 Codex helper、CLI、更新 helper 和 最外层Gloss.app执行codesign --force --sign -,不使用可能覆盖嵌套 entitlement 的--deep --sign;Homebrew 资产不会包含无法配对的 Safari.appex。每一步都通过--preserve-metadata=identifier,entitlements,requirements,flags,runtime保留已有 metadata, 并比较签名前后 App 的 entitlement bytes。随后只递归删除com.apple.quarantine、确认该属性已经不存在,最后用codesign --verify --deep --strictfail closed 验证完整签名。这让正常 Homebrew 安装后的首次 启动不需要用户绕过 Gatekeeper,但也主动移除了 Gatekeeper 的隔离检查。 - ad-hoc 签名不能完成 Safari App Extension 与宿主 App 的 Apple 身份配对,因此构建脚本会
完全省略 Safari
.appex并把 Safari 标记为不可用;App、设置页与gloss-cli capabilities --json都只声明 Chrome。需要 Safari 配对时仍应在本地使用 Apple Development 或 distribution identity 构建,并确保签名允许group.com.samsoncj.glossApp Group。构建脚本要求分别提供宿主与扩展 provisioning profile,并将它们嵌入对应 bundle;运行时只会向系统实际授予的 group container 写入令牌。
因此该 Cask 只适用于用户明确信任 SunChJ/homebrew-tap 和
SunChJ/gloss-releases 的自定义分发场景,不应被描述为 Apple 已签名或已公证的软件。