chipallen2/snazi
GitHub: chipallen2/snazi
一个通过发送者批准列表来控制 AI agent 是否能读取消息内容的通信门控层,旨在防止陌生人通过未授权消息进行 prompt injection。
Stars: 0 | Forks: 0
# Soup Nazi AI - snazi
**已在 [snazi.dev](https://snazi.dev) 上线** — 免费注册,或自托管(参见 [设置](#setup))。
一个用于 AI agent 的通信**门控层**。它通过控制 agent 是否能查看消息*内容*——仅基于已批准/已拒绝的发送者列表,来防止陌生人进行 prompt injection。
Agent 始终可以知道**谁**发送了消息。只有当发送者被批准时,它才能读取他们说了**什么**。
它是**多租户**的:任何人都可以注册,并且每个账户都有其自己的私密批准/拒绝列表及其自己的读取 token。运行你自己的部署,或使用共享部署。
## 两个部分(没有其他部分)
### A 部分 — 服务器 (`packages/web`):列表管理器
一个在 Vercel 上运行并由 Supabase 提供支持的 Next.js 14 + Tailwind 应用。它只做一件事:管理每个账户在每个 channel 上的批准/拒绝发送者列表。
- **它不存储任何消息。没有收件箱表。任何内容都不会触及服务器。** 这就是核心所在。
- **账户:** 在 `/signup` 注册(邮箱 + 密码)。每个查询都限定在已登录的所有者范围内,因此一个账户永远不会看到另一个账户的列表。每个账户都会获得一个用于 CLI 的按用户的 **READ token**(显示在 `/account` 页面上)。
- API(通过 `x-api-key` / `Bearer` 的按用户读取 token 进行身份验证,并限定于该账户):
- `GET /api/senders/check?channel=imessage&address=` →
`{ status: "approved" | "denied" | "unknown" }`
- `GET /api/senders?channel=imessage` → 该账户的完整列表
- `PATCH /api/senders/label` → 设置发送者的显示标签(仅限 UPDATE;永远不能创建行或更改状态)
- `GET /api/decide-link?channel&sender&label` → 一个绑定到你账户的**已签名且会过期的** `/decide` 链接
- **没有 mutate API**:读取 token 永远无法批准/拒绝。批准操作在控制面板(session)中进行,或通过签名的 `/decide` 链接进行。
- 控制面板:`/`(管理你的发送者),`/account`(你的读取 token)。通过 `/login` + middleware 的 HMAC 签名 session cookie 进行门控。
- Session cookie 和 `/decide` 链接使用 `SOUP_NAZI_AUTH_SECRET` 进行 HMAC 签名;decide 链接的签名涵盖了其所有者,因此它永远只能修改创建它的账户。
### B 部分 — 门控 CLI (`packages/snazi`):本地门控
一个普通的按需 CLI(TypeScript → `dist/`),可在任何 OS 上运行(今天从源码安装;参见 [设置](#setup))。Agent 按需运行它。它通过可插拔的 **channel adapter** 读取消息;iMessage adapter 在 macOS 上以只读方式读取 `~/Library/Messages/chat.db`。
- `snazi init` / `snazi doctor` → 设置 config 并进行诊断(无需编辑 JSON)。
- `snazi list-new` → 显示**谁**发了消息 + 批准状态。从不显示文本。
- `snazi read ` → 首先检查服务器;**仅在批准时**打印文本,否则提示 "No messages for you."
- `snazi send --text ` → 发送一条 iMessage。**从不被门控** — 你随时可以发送给任何人。
- `snazi check --channel ` → 查询单个发送者的状态。
- `snazi channels list|add` → 管理 channel 并在此查看 adapter 可用性。
- `snazi status` → 查看 config + 平台 + 连接状态。
该 CLI 是**只读的**:没有 `approve`/`deny`。批准操作在控制面板中进行,或通过签名的 `/decide` 链接进行。
参见 [`packages/snazi/README.md`](packages/snazi/README.md)。
#### 服务模式 — 基于 tailnet 的最小权限 HTTP 门控(可选启用)
当 agent 运行在装有 iMessage 的 Mac *之外的*机器上时,SSH 会为其提供一个完整的 shell。相反,`snazi serve` 仅通过 HTTP 暴露**只读的**门控操作,且只能在私有的 Tailscale tailnet 上访问:
- `GET /health`(无需身份验证)· `GET /list-new` · `GET /check` · `GET /read` · `GET /resolve` · `POST /label` · `POST /send` — 除 `/health` 外,均受 bearer-token 保护。`/read` 是只读门控的;`/send` 从不被门控(你随时可以发送给任何人)。`/label` 仅限 UPDATE(显示名称,无法更改批准状态)。
- **HTTP 上不提供 `approve`/`deny`**(批准操作仅保留在控制面板/`/decide` 中)。
- 绑定 **tailnet 100.x IP**(或使用 `tailscale serve` 时的 `127.0.0.1`),**绝不是 `0.0.0.0`**。Bearer token(`serveToken`)通过常量时间进行比较,且从不被记录到日志。`/read` 执行与 CLI **相同的批准列表门控**。
- 通过 `snazi serve --install-daemon` 作为 launchd LaunchAgent 运行。node 二进制文件需要 **Full Disk Access** 才能读取 `chat.db`。
- 远程 agent 使用 `snazi remote-list-new` / `remote-read` / `remote-check` / `remote-resolve` / `remote-label` / `remote-send`(配置:`remoteUrl`,`remoteToken`)或普通的 `curl`。
完整的 security 模型、config 密钥以及 FDA 设置,请参见 [`packages/snazi/README.md`](packages/snazi/README.md#serve-mode--least-privilege-http-gate-over-a-tailnet)。
基础 CLI 仍然**按需运行**;服务模式默认关闭,且完全可选启用。双方都不存储消息内容。
## 批准流程
1. Agent:`snazi list-new` → 看到 `+1555… (unknown)`。
2. Agent 询问你(或通过 `GET /api/decide-link` 发送一个一键式 `/decide` 链接):“来自 +1555… 的新消息,是否批准?”
3. 你在控制面板中批准(或通过在 `/decide` 链接上点击 **Allow**)。
4. Agent:`snazi read +1555…` → 现在门打开并返回文本。
未知/被拒绝的发送者保持不可见状态。恶意的陌生人无法将内容注入 agent,因为 agent 永远看不到他们的言语。**发送永远不被门控** — agent 随时可以通过 `snazi send` 回复或通知你。
## 布局
```
packages/
web/ Next.js list-manager (API + dashboard) → Vercel
snazi/ On-demand gate CLI (pluggable channels) → your computer (any OS)
```
## 数据模型(Supabase,以 `sna_` 为前缀)
- `sna_users` — 账户:邮箱、密码 hash、按用户的读取 token。**无消息数据。**
- `sna_channels` — 全局 channel 注册表(已填充 `imessage`;可扩展至 gmail…)。共享参考数据,而非按用户划分。
- `sna_senders` — 批准/拒绝列表,**范围限定于 `owner_id`**。`status` ∈ {`approved`, `denied`};缺失 = `unknown`。**不存在消息表。**
租户隔离在应用层强制执行:每个发送者查询都经过 `packages/web/src/lib/data.ts`,这需要提供 `owner_id`。
## 设置
**服务器 (`packages/web`)**
1. 创建一个 Supabase 项目。
2. 按顺序运行 `packages/web/supabase/migrations/` 中的 SQL:`001` 和 `003` 是必需的。`002` 是一个可选的性能 index(参见文件头);除非你想在大规模下获得更快的名称解析速度,否则跳过它。
3. 使用以下环境变量将 `packages/web` 部署到 Vercel(参见 `.env.example`):`SUPABASE_URL`, `SUPABASE_SERVICE_KEY`, `SOUP_NAZI_AUTH_SECRET`
(`openssl rand -hex 32`)。
4. 打开部署的站点,在 `/signup` 注册,并从 `/account` 复制你的读取 token。
**CLI (`packages/snazi`)** — macOS、Windows 或 Linux(Node 18+):
```
npm install -g @chipallen2/snazi # scoped package; the command is just `snazi`
snazi init # writes ~/.snazi/config.json (deployment URL + your READ token)
snazi doctor # verifies Node, config, connectivity, and channel access
```
更倾向于从源码安装?`git clone https://github.com/chipallen2/snazi.git && cd snazi/packages/snazi && ./install.sh`。
在 macOS 上,授予 Full Disk Access 以便 CLI 能够读取 `~/Library/Messages/chat.db`(系统设置 → 隐私与安全性 → Full Disk Access)。如果缺少权限,`snazi doctor` 会进行标记提示。CLI 可在任何 OS 上运行,但特定的 channel 只能在其 adapter 支持的环境下工作(iMessage 仅限 macOS);非 Mac 主机仍然可以通过 `remote-*` 命令在 tailnet 上驱动 Mac 的 `snazi serve`。
## 扩展到其他 channel
一个 channel 包含两个部分:
1. **服务器:** 向 `sna_channels`(全局注册表)添加一行。列表 API、门控和 `/decide` 链接已经是与 channel 无关的了。
2. **本地 adapter:** 在 `packages/snazi/src/channels/` 下添加一个 `ChannelAdapter`(实现 `availability`、`listInboundSenders`、`readMessagesFrom`)并在 `src/channels/index.ts` 中注册它。然后,每一个 CLI/服务命令都会自动在 adapter 支持的任何 OS 上为它工作。
无论何种 channel,门控(`GET /api/senders/check`)都会在显示任何内容之前被强制执行。
## CI 和发布
GitHub Actions 负责构建、测试和发布(参见 `.github/workflows/`):
- **`ci.yml`** 在每次向 `main` 发起的 push/PR 时运行 — 构建并测试 `snazi`(Node 18 + 20)和 `web`。向 `main` 的常规提交**绝不**会发布。
- **`release.yml`** 仅在推送 `v*` tag 时运行 — 它会重新测试,验证 tag 是否与 `package.json` 匹配,运行 `npm publish --provenance`,并创建一个 GitHub Release。身份验证使用的是 **npm Trusted Publishing (OIDC)** — 没有存储的 token。
发布版本(在 `packages/snazi` 中,基于干净的 `main` 分支):
```
cd packages/snazi
npm run release:patch # 0.1.0 -> 0.1.1 (or release:minor / release:major)
```
这会增加版本号、提交、创建 `vX.Y.Z` tag 并将两者一并推送 — 推送 tag 会触发 `release.yml`,从而将其发布到 npm。无需手动执行 `npm publish`。
**一次性设置(Trusted Publishing)。** npm 无法通过 OIDC 发布全新的 package,因此:
1. 手动发布第一个版本(在干净的 `main` 分支上,并已登录 npm):
cd packages/snazi && npm publish --access public
2. 在 npmjs.com 上 → 该 package → **Settings → Trusted Publisher**,添加一个 GitHub Actions 发布者:org/user `chipallen2`,repository `snazi`,workflow filename 为 `release.yml`。
此后,每次 `npm run release:*` 都会通过 OIDC 自动发布 — 无需创建、存储或轮换 `NPM_TOKEN` 密钥。
标签:AI智能体, MITM代理, Streamlit, 提示词注入防护, 自动化攻击, 访问控制, 通信网关