System Design · Review Draft

Tunly 系统设计

Namespace-based AI Agent Sharing Layer:把 Agent 或人的本地服务、静态页面和证据产物,发布为可长期管理、可访问控制、可版本化、可协作 Review 的公共 Link。

版本 1.0 2026-07-16 依据当前实现与 Design Canon 状态:可评审
01 · Executive summary

一句话架构

Tunly 以 Namespace 为唯一归属边界,以 Link 为长期分享对象,以 Route 提供稳定地址,以 Revision 表达每次发布版本;所有公开请求都按 Route → Link → Access → Revision 的顺序确定性解析。

Control plane

管理长期事实

tunlyctl 提供认证、API Key、发布、成员、访问策略、Review API 和 Dashboard。

Edge plane

处理公开流量

tunlyd 接受 HTTP 与 WebTransport/QUIC,解析公共 Route,转发 live 流量或读取 artifact。

Data plane

按生命周期分层

Postgres 存产品事实,Redis 存短期运行态,ArtifactStore 存不可变发布内容。

核心不变量: API 只有在 manifest 和所有 artifact 文件已由同一 ArtifactStore 可读、扫描通过且 Revision 状态为 ready 后,才可以把它提升为 Link 的 Latest。
Review focus Tunly 的产品心智模型应该坚持“一个长期 Link,多个不可变 Revision”,而不是“一个临时 tunnel 对应一个 URL”。请重点评审这个抽象是否足以覆盖设计文档、静态 Demo、Agent evidence 和 live preview。
02 · Scope and boundaries

目标、非目标与所有权边界

系统目标

  • 一条命令把本地服务或 artifact 变成稳定公共 URL。
  • Latest URL 可更新,Version URL 保持不可变。
  • 公开、账号、密码、邮箱、域名和 Guest Token 访问控制行为一致。
  • Agent 获得确定的 JSON/NDJSON 协议,而不是解析人类日志。
  • HTML 文档可按 Revision 做锚点评论、回复、解决与历史回看。

明确非目标

  • 不引入 Organization、Workspace、Account、Business 或 Project 作为平行所有权模型。
  • P0 不为 webhook、OAuth、MCP、snapshot 或 runtime session 建独立产品表。
  • 不让 legacy /pub 或磁盘路径绕过 PublicResolver。
  • 不把高频心跳、live lease、viewer session 或评论事件流写入 Postgres。

唯一根边界:Namespace

Namespace
├── Memberships (owner / member)
├── API Keys
├── Links
│   ├── Routes
│   ├── Revisions
│   └── Review Annotations (P1, pinned to one Revision)
└── Runtime state (Redis keys scoped by Namespace / Link / Revision)

任何请求体里出现的 ID 都不是授权事实。服务端必须从认证身份、API Key 或已验证 viewer grant 得到 Namespace,再用数据库中的复合关系验证 Link 和 Revision 属于同一边界。

03 · System context

系统上下文与信任边界

Agent / Human CLI                              Browser Reviewer
       │ API key / attach grant                       │ HTTPS / session
       ▼                                              ▼
┌──────────────────────────────────────────────────────────────┐
│                    Caddy · TLS edge                          │
│  control paths ───────────────┐  public paths ────────────┐  │
└───────────────────────────────┼────────────────────────────┼──┘
                                ▼                            ▼
                    ┌────────────────────┐       ┌────────────────────┐
                    │ tunlyctl           │       │ tunlyd             │
                    │ control plane      │       │ edge + live proxy  │
                    │ publish / review   │       │ PublicResolver     │
                    └──────┬─────┬───────┘       └──────┬─────┬───────┘
                           │     │                      │     │
                     ┌─────▼─┐ ┌─▼───────────┐   ┌─────▼─┐ ┌─▼───────────┐
                     │Redis  │ │Postgres     │   │Redis  │ │ArtifactStore│
                     │runtime│ │product facts│   │leases │ │manifests   │
                     └───────┘ └─────────────┘   └───────┘ └─────────────┘
                                ▲
                                │ same durable repository contract
                                └─────────────────────────────────────────
边界可信输入不可信输入强制检查
CLI → Control经 hash 验证且未撤销的 API KeyLink ID、route hint、文件名、metadataNamespace membership、配额、幂等键、artifact 扫描
Browser → Public服务端 session 或已签发 viewer grantHost、Path、query、cookie、guest tokenRoute 状态、Link 策略、AccessEvaluator、conceal 规则
Edge → Local Agent一次性 attach grant 与已注册连接来源声明、local target、重连顺序Namespace scope、generation、readiness proof、lease
Review iframe → API精确范围 HMAC read capabilitypostMessage、selector、annotation IDorigin/schema/Link/Revision、账号写权限、乐观锁
04 · Domain model

持久领域模型

P0 长期产品事实保持七张表;P1 Review 只增加一张 revision-pinned annotation 表和一个 Link 开关。

实体职责关键约束删除/失效语义
users账号身份Email 唯一;密码只存强 hashdisabled_at 使认证失效
namespaces根所有权边界所有长期资源最终归属它级联清理 namespace 产品事实
namespace_membershipsowner/member RBACNamespace + User 唯一移除成员后 API Key attach 也必须失败
api_keysAgent/CLI 身份只存 prefix + hash;支持过期与撤销撤销后控制面和 attach 都拒绝
links长期分享对象与 Latest 指针Namespace 复合约束;CAS lock_versiondisabled 后公共解析失败
routesHost + Path → Link活动地址唯一;同 Namespace 目标reserved/active/disabled 状态机
revisions版本边界Link 内版本号唯一;ready artifact 元数据完整失败或隔离版本不可提升
review_annotations根评论与一级回复Namespace + Link + Revision + self FK软删除保留 tombstone;Revision 删除级联

Link 与 Revision 的指针语义

Current

Latest

links.current_revision_id 是明确提升出来的当前版本。新的 publish 只有通过完整准备和 CAS 后才移动它。

Immutable

Version

/versions/{revision_id}/ 精确解析某个 artifact Revision,不跟随 Latest 变化,适合审计、引用与评论历史。

为什么 live 也建 Revision: Revision 是一次发布意图与可审计边界,不是文件表。Live Revision 可以没有 artifact 字段,但只有完成 attach 和 readiness proof 后才可以成为 Latest。
05 · Component architecture

组件职责与依赖方向

apps/tunly · CLI

  • publish:目录/HTML artifact 或 live port。
  • preview:短生命周期 Agent Preview Session。
  • share/http/tcp:建立 live transport。
  • tunly.agent.v1:稳定 JSON/NDJSON 生命周期事件。

apps/tunlyctl · Control

  • 认证、浏览器 session、API Key、成员、配额。
  • artifact publish pipeline 与 manifest 写入。
  • Review API、SSE、sidebar SPA。
  • 签发一次性 attach grant 与 Review read capability。

apps/tunlyd · Edge

  • HTTP public ingress 与 WebTransport/QUIC control。
  • live connection manager、readiness probe、generation/lease。
  • 通过 PublicResolver 统一服务 live 与 artifact。
  • 在生产配置下禁止绕过 durable resolver。

Shared infrastructure

  • Postgres repository:长期事实和事务状态机。
  • Redis stores:viewer grant、session、live lease、SSE invalidation、rate limit。
  • ArtifactStore:LocalStore/PVC 或 S3-compatible provider。
  • Caddy:TLS、控制面路径与 public path 分流。

依赖原则

HTTP/CLI adapter
      │
      ▼
Narrow domain interface  ──►  service / policy  ──►  repository interface
                                                      │
                                                      ▼
                                         Postgres / Redis / ArtifactStore

规则:Public resolver 只有读路由能力;普通 Route admin 不能获得 publish/promote 能力。
06 · Core flows

核心请求流程

A. Artifact 发布与原子提升

1认证与打包CLI 读取 API Key,规范化目录,计算 payload digest 与文件清单。
2解析 Link按 --link、project ref 或 route hint 复用/创建 Link 与 Route。
3创建 Pending事务内锁 Link,分配 version number,创建不可服务的 pending Revision。
4准备 Artifact扫描文件,生成 immutable object keys,将 manifest 与文件写入 ArtifactStore。
5状态推进pending → uploading → processing,记录服务端可信扫描结果。
6Ready gate检查 manifest、hash、entrypoint、size、file count 与 security profile 完整。
7CAS Promote只有 lock_version 未变化时才写 current revision;并发慢发布返回 409。
8返回 URL输出 Link/Revision ID、Latest、Version,Review 文档再输出 Review URLs。
幂等语义:同一个 Link 内,相同 idempotency key + 相同 payload 返回原 Revision;相同 key + 不同 payload 必须冲突,不能偷偷创建新版本。

B. 公共请求解析

HTTP request
  │
  ├─ 1. normalize Host + Path
  ├─ 2. resolve the most specific active Route
  ├─ 3. load active Link inside the same Namespace
  ├─ 4. evaluate Link access policy (fail closed)
  ├─ 5. choose exact Version or Link.current_revision_id
  ├─ 6. require a servable Revision
  │
  ├─ live ─────► validate active Redis lease/generation ─► proxy local origin
  └─ artifact ─► read manifest + file from ArtifactStore ─► apply profile headers

Latest 响应使用可重新验证的 cache 语义;Version artifact 可使用 immutable cache。私有内容、Review rendition 和访问挑战必须使用 private/no-store,避免代理缓存跨用户泄露。

C. Live attach 与 Latest

  1. CLI 先通过控制面用 API Key 请求一次性、短 TTL、精确 Namespace/Link scope 的 attach grant。
  2. CLI 与 edge 建立 WebTransport/QUIC;edge 消费 attach grant,并检查 API Key 所有者仍是活跃成员。
  3. edge 建立带 generation 的 Redis live lease,旧连接不能覆盖新 generation。
  4. 生产环境从 edge 往本地 origin 做真实 readiness round trip。
  5. 只有探针通过才原子提升 live Revision;失败则清理 lease,Latest 保持不动。
Review focus 当前设计把“创建 Revision”和“提升 Latest”严格拆开,以避免上传失败、扫描失败或 live origin 未就绪时污染稳定 URL。请评审这个两阶段模型在 Agent 高频发布场景下是否需要批量、取消或显式 promote API。
07 · HTML review subsystem

HTML Review:Revision 上的协作层

Review 不修改原始 artifact,也不创建新的文档模型。它是 Link 开启的策略,并把每条 annotation 固定在一个 Namespace + Link + Revision 元组上。

Rendition 与隔离

Raw URL

返回发布时的原始 HTML,不注入 bridge,保留正常 Latest/Version cache 语义。

Review URL

带 ?__tunly_review=1,只对 document profile 的 HTML entrypoint 动态注入 bridge 与 iframe。

Sidebar

React UI 由控制面同源提供;artifact bridge 不接触账号 token、评论正文或 API credentials。

锚点定位策略

根评论同时保存 W3C 风格的 TextQuoteSelector 与 TextPositionSelector。渲染时先用位置快速验证,再用 exact + prefix + suffix 回退;匹配不唯一或找不到时显示 orphan,而不是猜测错误位置。

{
  "type": "Text",
  "refinedBy": [
    { "type": "TextQuoteSelector", "exact": "selected text", "prefix": "before ", "suffix": " after" },
    { "type": "TextPositionSelector", "start": 120, "end": 133 }
  ]
}

读写权限与实时更新

能力匿名 capability登录账号一致性
读取线程允许,15 分钟 HMAC,精确 scope通过 Link access 后允许每次返回完整逻辑线程
创建/回复拒绝允许提交数据库后发布 invalidation
编辑/删除拒绝作者或 moderatorexpected_updated_at,冲突返回 409
解决/重开拒绝根作者、成员或 admin只允许操作 root
SSE允许只读允许先发 reconcile;Redis 不是 durable event log
Revision 隔离:发布新版本不会复制旧评论。新 Revision 只显示 prior unresolved count,引导用户回到旧 Version URL 处理上下文仍然准确的评论。
08 · Security and privacy

安全模型

访问模式

模式认证事实运行态关键风险控制
public无无仍执行 Route/Link/Revision 状态检查
account有效账号 session + 活跃 membershipRedis auth session成员移除立即失效
passwordbcrypt hash 比对viewer grant明文不持久化;grant 有 TTL
email/domainOTP 验证身份challenge、identity、grant绑定 Link、过期、尝试限制
guest常量时间比较的 token hashviewer grant支持过期/撤销;URL 清理

Artifact 安全 profile

static_app

允许受约束的静态应用资源,PublicResolver 统一施加 CSP、nosniff、frame 与 cache 头。不会自动获得 Review 注入。

document

偏保守的文档 sandbox。Review rendition 生成 nonce,限制 frame-src 到 sidebar origin,并使用 no-store/noindex。

隐私原则

Review focus 当前 P0/P1 把访问策略放在 Link,而不是 Route 或 Revision。这样 Latest、Version、静态资源和 Review 共用一条策略链;代价是不能给某个单独版本设置不同受众。请评审这一限制是否符合近期产品需求。
09 · Reliability and consistency

一致性、失败模式与恢复

失败用户可见行为系统保证恢复方式
Artifact 上传中断Publish 失败;Latest 不变Pending/failed Revision 不可服务用相同或新 idempotency key 重试
扫描拒绝Revision quarantined/failed不会被 promote修复源文件并重新发布
并发 Publish慢请求收到 409CAS 防止旧内容覆盖新 Latest重新读取 Link 后显式重试
Redis 不可用live/OTP/SSE 降级;artifact 仍可按策略读取评论数据库提交不因事件发布失败而回滚SSE reconnect + polling reconcile
Live agent 断开在线内容不可用或 fallbackgeneration 防止旧连接复活客户端重连并获得新 lease
Postgres 不可用控制与新解析 fail closed不使用 Redis 缓存绕过授权事实恢复数据库;readiness 反映依赖失败
ArtifactStore 不可用artifact not ready/unavailable不返回部分文件或错误版本对象存储恢复后无需改数据库指针
Review anchor 漂移显示 orphan不做错误高亮在原 Revision 处理或新建评论

备份与回滚

10 · Deployment topology

当前部署与扩展路径

当前单节点生产拓扑

Internet :80/:443 TCP ──► Caddy LoadBalancer
                              │
                              ├─ /api, /app, /login, /review/sidebar ─► tunlyctl:8080
                              ├─ /wt (h2c fallback) ──────────────────► tunlyd-control:8081
                              └─ all public Link paths ───────────────► tunlyd:80

Internet :443 UDP ───────────────────────────────────────────────────► tunlyd:443 QUIC

tunlyctl + tunlyd ─► Postgres service
                  ├► dedicated Tunly Redis
                  ├► shared ArtifactStore provider
                  └► OTLP logs collector

Persistent volumes: artifact bytes, Postgres data, Redis data, Caddy state

横向扩展目标

Target

Object Storage

从 LocalStore/PVC 切换到 Hetzner S3-compatible provider;发布和读取接口不变,只迁移 objects 与 secrets。

Target

Multi-edge

Route/Revision 来自共享 Postgres,live binding 用 Redis 指向 edge/generation;artifact 可由任一 edge 读取。

Target

Independent scaling

控制面按 API/Review 负载扩展,edge 按 public traffic 与 live connection 数扩展,两者不共享进程状态。

扩展前置条件:不能把 LocalStore 目录当成节点私有实现细节。所有 publisher 和 resolver 必须始终通过同一个 ArtifactStore contract 与同一套 immutable manifest layout。
11 · External contracts

CLI、HTTP API 与可观测性

Agent-facing CLI contract

tunly publish ./design.html --review --public --route system-design --json

{
  "schema": "tunly.agent.v1",
  "event": "completed",
  "payload": {
    "namespace_id": "...",
    "link_id": "...",
    "revision_id": "...",
    "latest_url": "https://tunly.io/system-design/",
    "version_url": "https://tunly.io/versions/.../",
    "review_url": "https://tunly.io/system-design/?__tunly_review=1",
    "state": "ready"
  }
}

API 设计原则

观测信号

层信号不允许出现
HTTPmethod、normalized path、status、bytes、duration、request IDAuthorization、cookie、OTP、comment body
Publishnamespace/link/revision、状态迁移、文件数、总字节、失败分类artifact 原始内容、API Key
Liveconnection ID、edge、generation、readiness、lease transition代理请求私密 body
Review操作类型、scope IDs、status、SSE reconnect选择文本、评论正文、capability
12 · Evolution plan

演进路线与容量触发器

阶段保持不变允许演进触发条件
P0/P1 当前Namespace/Link/Route/Revision、PublicResolver、ArtifactStore contract单节点 K3s、本地 PVC、单 Redis当前产品验证
存储扩展Manifest layout 与发布状态机S3 provider、生命周期清理、跨区复制PVC 容量/可靠性成为风险
Edge 扩展Route 解析与 generation lease多 edge 调度、区域亲和、连接迁移连接数或网络延迟要求
Review 扩展Revision-pinned annotation 与 selector contractmentions、通知、批量 resolve、导出Review 使用量证明价值
新集成Link + Route + Revision 复用原则协议 adapter 与 metadata已批准 ADR 证明独立表必要
演进护栏:只有当一个概念拥有独立生命周期、授权边界、查询模式和保留策略,并且无法由 Link + Route + Revision 表达时,才考虑新增产品表。
13 · Architecture decisions

关键决策记录

D1 · Namespace root
Namespace 是唯一 durable ownership boundary。拒绝 Organization/Workspace 等平行层,减少 RBAC、迁移和 API scope 分叉。
D2 · Link is the product
用户分享和管理的是 Link;tunnel、session 和 artifact 都是 Link 的运行方式或 Revision 内容。
D3 · Explicit promotion
创建 Revision 不移动 Latest。Readiness、scan 与 CAS 全部通过后才 promote。
D4 · Runtime outside SQL
Redis 保存有 TTL、高频变化且可重建的运行态;Postgres 保存长期事实和审计指针。
D5 · Shared ArtifactStore
Publisher 与 resolver 必须使用同一 provider contract;对象 key 不可变,manifest 是读取入口。
D6 · One public resolver
Latest、Version、asset、live 和 Review 都不能绕过 Route/Link/Access/Revision 的统一决策链。
D7 · Revision-pinned review
评论不随新版本自动漂移。W3C quote + position selector 保留原上下文并显式展示 orphan。
D8 · Capability is read-only
匿名 iframe capability 精确 scope、短 TTL、只读;任何写操作必须有账号身份。

本轮建议重点评审

  1. Link 是否足以成为 live、static、evidence、design doc 的统一产品对象?
  2. 访问策略只挂 Link,是否需要近期支持 revision-specific audience?
  3. 两阶段 publish/promote 对 Agent 高频发布是否需要 cancel 或 manual promote?
  4. 新 Revision 不复制评论,只显示 prior unresolved count,是否符合设计评审习惯?
  5. 从 LocalStore 切换对象存储前,哪些 SLO、容量或恢复指标应作为硬触发器?
文档状态:本设计描述当前实现与已批准的架构边界。评审结论若改变数据模型、授权边界或发布状态机,应新增 ADR/OpenSpec change,而不是直接在实现中引入旁路。