装那些没人给你打包的小工具。
装的时候一行脚本都不跑。
没有 install、没有 postinstall、没有 build、没有 hook,一个都没有。toy 只会下载、验哈希、解压、建软链。
$ toy install github:Derrity/tinyserve
$ tinyserve
toy.toml,打个 tag,完事name = "tinyserve"
desc = "一个很小的静态文件服务器"
license = "MIT"
[bin]
tinyserve = "tinyserve"
[artifact]
"linux/amd64" = "tinyserve_{version}_linux_amd64.tar.gz"
"darwin/arm64" = "tinyserve_{version}_darwin_arm64.tar.gz"
没有 URL、没有版本号、没有托管方名字 —— 所以下次发版这个文件一个字都不用改。已经在用 goreleaser 的话连写都不用写:toy init 读 ./dist 里的资产名反推出来。
三件事
-
一、装的时候不跑任何代码
这不是「默认关闭」也不是「沙箱里跑」,是这些概念在代码里根本没有对应物。它挡掉的是一整类攻击而不是一个 CVE:吃掉 700+ 个 npm 包的 Shai-Hulud 蠕虫靠的是
preinstall;Homebrew 的 formula 是可执行 Ruby,6.0 不得不为此加了一道 Tap Trust。边界也说清楚:「装的时候不跑」不等于「这个包永远拿不到执行」。一个包声明
[bin] git = "payload",装完~/.toy/bin/git就在 PATH 上,而 toy 自己靠git ls-remote发现版本。所以git是保留名字,无条件拒绝,--force也不放行 —— 全项目唯一一条这样的检查。 -
二、8 行 toml,零审批
不用注册、不用等审核、不用求任何人。打个 tag 发个 release,别人就能
toy install github:你的名字/你的工具。想被搜到再把一行条目提进索引,跑一次toy keygen就能给自己的条目签名 —— 这两步都是可选的。强制签名 = 强制注册 = 又一个门槛。 -
三、下架无害,索引可自建
索引里默认只存一个指向仓库的指针,真相永远是那个 git 地址。从索引里删掉一条只是「不再被搜到」,
toy install github:owner/repo照样装得上 —— 没有人能真的封杀一个包,包括我们。任何人 fork 一份那个 JSON 就是一个完整的平行索引。所以维护者要求下架就下架,不问理由:既然代价这么低,就没有任何理由跟人争。
索引里现在有什么
这份索引就挂在 /index.json,客户端整份下载后本地搜索。toy 装好就带着它(名字叫 default),toy search 直接就能搜。toy 0.1.0 写出来的配置里没有它,补上:
toy index add default https://toy.cirraya.com/index.json
这里每一条都是内联条目。装出来是第三档信任等级(index-toyfile):二进制取自作者自己的 release(下载地址由托管方模板 + tag 拼出来,索引说了不算),但装哪个资产、软链成什么命令名由索引提供 —— 作者本人没写过 toy.toml,多半也不知道我们收录了他。收录只说明「这个包装得上」,不说明我们审计过它的代码,也不说明作者和我们有任何关系。安装时客户端会如实打印这一档。
你在信任谁,toy 会告诉你
安装完成那一行会打印下面四种之一。读的时候要读它证明不了的那部分。
| 安装完成那一行 | 你信的是 | 它证明不了什么 |
|---|---|---|
✓ signed · key a3f9… · registered in index X (this is not a claim about github.com/Y themselves) |
索引里登记的那把 ed25519 公钥 | 不证明是仓库主人签的。签名消息是 id + source + toy.toml 原文,三样都公开可读,里面没有一个字节把这把公钥和仓库的 owner 绑起来。索引谁都能 fork、谁都能提 PR,抄一把公钥零成本 —— 一个人完全可以 fork 一份、用自己的密钥给同一个 id 重新签一条、把 source 指向自己的仓库。它保证的是连续性(公钥首次安装即钉住,换了拒绝安装),不是身份。第一次安装该看的是 source 指向哪里,不是有没有勾。 |
· from github.com/Y · unsigned |
托管方的账号体系:GitHub 说这个仓库属于这个账号,TLS 说你连到的是 GitHub | 没有作者签名,也不需要 —— 这是绝大多数包的默认状态,而且它是一个诚实的状态,不是一个降级。 |
⚠ packaging instructions come from the index · … |
字节是作者的,打包说明不是 | 作者没写过 toy.toml,索引条目里内联了一份。索引能改的是「装哪个资产、软链成什么命令名」,改不了下载地址。内联的 toyfile 永远到不了那个勾 —— 签名者是提交索引记录的人,不是作者。 |
⚠ unsigned · first install, sha256 pinned (trust on first use) |
裸 URL:你什么都没信 | 但内容变了会被拦下来。TOFU 的第一次就是无条件信任 —— 第一次装到的如果就是坏的,toy 会忠实地把它钉住。 |
有 [sha256] 就强制校验,没有则首次安装钉住,之后变了拒绝安装(模型抄的是 SSH 的 known_hosts)。--reinstall 和 --force 是两件事:前者只是「我知道已经装了这一版,再装一次」,不放行任何安全检查;后者才是「我看过了,接受这些变化」,而且会逐条打印自己放行了什么。完整的威胁模型 —— 包括 toy 不防的那一栏 —— 在 SECURITY.md。上表左栏是客户端实际打印的原文(英文);右边两栏是说明,不是输出。
装 toy 自己
先把话说在前面:第一个包管理器总得靠别的方式装。toy 存在的理由是让你不必 curl | bash,而它自己没法用 toy 装 —— 这个循环没有出路,凡是说自己解决了的都是把它挪了个地方。所以下面两种方式都不完美,区别只在于你在信谁。
方式一:go install(有 Go 工具链的话,这是最省事的)
go install github.com/Cirraya-LLC/toy/cmd/toy@latest
你信的是 Go 的模块代理和 checksum database(sum.golang.org)—— 一套公开的、可审计的透明日志。它比我们自己的任何承诺都强,所以有 Go 就用这个。
方式二:下载 release 二进制,自己核对哈希
# 换成你的平台:linux_amd64 / linux_arm64 / darwin_amd64 / darwin_arm64
V=0.1.0; P=linux_amd64
BASE=https://github.com/Cirraya-LLC/toy/releases/download/v$V
curl -fLO $BASE/toy_${V}_${P}.tar.gz
curl -fLO $BASE/checksums.txt
# 核对哈希。这一步不是可选的 —— 跳过它,你就是在无条件信任那条下载链路。
sha256sum --ignore-missing -c checksums.txt # macOS: shasum -a 256 --ignore-missing -c
tar -xzf toy_${V}_${P}.tar.gz
install -m 0755 toy ~/.local/bin/toy
checksums.txt 和二进制来自同一个 release,所以它挡的是「传输过程中被改了」,不是「发布的人本身有问题」。这就是引导悖论的实际形状。 在 Mac 上请照上面用 curl 下载,别用浏览器:浏览器会给文件打上隔离标记,tar 会把它带到解出来的 toy 上,Gatekeeper 就不让这个临时签名的二进制运行。已经这样了的话:xattr -d com.apple.quarantine toy。
这里不会有 curl | bash 安装脚本。那正是这个项目要反的东西 —— 把整台机器交给一个没人看过的脚本。
装完之后
export PATH="$HOME/.toy/bin:$PATH" # 写进 ~/.bashrc / ~/.zshrc
toy doctor
toy 装好就带着本站的索引,所以 toy search 马上能用,toy doctor 也会检查它连不连得上。要是 doctor 说「no index configured」(toy 0.1.0 写的配置就是这样),没有坏掉,它会给出加上的那一行。没有索引也从来不影响装东西:toy install github:owner/repo 那条路本来就不经过索引。
底下的架构
索引是去中心化的,这是架构不是卖点。它就是一个 JSON 文件,客户端整份下载后本地搜索;里面只存指针,source 那个 git 地址永远是真相。删一条只是不再被搜到,fork 一份就是一个平行索引。(把「去中心化」当宣传语的 Entropic —— npm 前 CTO 2019 年在 JSConf EU 发布 —— 10 个月后归档了。所以我们只讲上面那两件事。)
版本发现用的是 git ls-remote --tags,一条命令、所有 git 托管方行为一致,不用任何一家的 REST API —— 也就不会被任何一家的限流卡住。托管方是一张配置表不是三份代码,加三行配置就多支持一家。
toy serve 把你本机的缓存变成一个镜像 —— 因为内容是哈希寻址的,镜像不需要被信任,谁都可以当,五分钟的事。上游哪天没了,包还活着。但「不需要信任」不等于「不需要知道」:真的走了镜像,安装时会明说是从哪拿的。
不做的事:不做源码构建、不做包间依赖、不做 P2P/DHT、不做 Windows(暂时)。包与包之间没有依赖 —— 每个 toy 是一个自包含的二进制或单文件脚本,所以不存在「我审了直接依赖,但它的第 47 个间接依赖被投毒了」这回事。