Portico 官方文档
一句话看懂 Portico:
你的团队写了一堆 Agent 和 MCP 工具,散落在各处不知道谁在管、谁能用、安不安全?
Portico 就是团队所有 Agent 的“门卫与接待大厅”——登记造册、发工牌、办审批、对外展示。
Agent 在你自己那儿跑,Portico 只管把好这道门。
核心搞懂三件事(说人话)
1. 它不替你跑 Agent
Portico 不是执行循环,不跑大模型,也不代调工具。
你的 Agent 该跑在本地、Docker 容器还是云服务器,完全由你决定。Portico 负责记录它的名字、功能介绍、怎么联系它、谁负责维护。
2. 内部随便用,对外必须人点头
- 内部协作:团队成员拿着工牌(会话),随时可以在内部网页或命令行里查到最新的测试版 Agent。
- 公开上线:想把某个 Agent 对外开放给所有人?必须人类安全员打勾批准。自动化 Agent 无论权限多高,也绝对不能自导自演批准自己公开。
3. 三个入口,看同一本账
- Web 网页:打开浏览器就能浏览、搜索、一键跳转直连。
- CLI 命令行:运维和 CI/CD 脚本一条命令查状态、办发布。
- MCP 协议:直接接入 Cursor、Claude Desktop 等外部智能体,同伴之间自动按规则发现。
30 秒极速体验
只要本地装了 Deno,一条命令就能把整套系统拉起来(自带网页、网关和 MCP 接口):
PORTICO_DATA_DIR=./data deno task up
起动成功后,终端会打印出三个本地地址:
{"ok":true,"data":{"dataDir":"./data","portal":{"url":"http://127.0.0.1:8788"},"gateway":{"url":"http://127.0.0.1:8789"},"mcp":{"url":"http://127.0.0.1:8790"}}}
打开浏览器访问 http://127.0.0.1:8788/public,立刻就能看到公开发布页!
接下来看什么
- 想马上动手实践? 👉 查看 快速上手指南
- 想搞懂发工牌与登录会话? 👉 查看 身份与访问控制
- 想知道服务怎么登记、修改与审批? 👉 查看 目录与生命周期治理
- 想查所有 CLI 命令行用法? 👉 查看 CLI 命令行完整参考
概览与核心架构
本章介绍 Portico 的核心定位、整体系统架构、产品铁律以及 5 分钟极速上手指引。
本章导航
- 什么是 Portico:深入理解 Portico 解决的问题与“门廊而非机房”的设计定位。
- 核心架构与信任边界:系统拓扑划分、三大独立进程沙箱与双重信任边界。
- 产品铁律与非目标:开发与运维不可触碰的安全红线。
- 快速上手指南:本地一键起动、Bootstrap 初始化与首个服务登记实战。
什么是 Portico
Portico 是组织的 Agent 治理门户与准入网关。
在企业与团队构建 AI Agent 的过程中,Agent 迅速在各处涌现:有些以本地 CLI 工具形式存在,有些暴露为 MCP (Model Context Protocol) 服务的 HTTP 端点,有些则提供了 Web 管理界面。然而,随着 Agent 数量的爆发,团队面临着严峻的治理危机:
- 孤岛与失控:没有人能准确回答组织内部究竟有哪些 Agent、分别由谁维护、版本为何、能力如何。
- 边界模糊:内部使用的实验性 Agent 与面向外部开放的生产服务混为一谈,存在严重的未授权公开访问与数据外泄风险。
- 特权越界:负责维护 Agent 描述的自动化 Bot 容易获得过高特权,甚至能够自主将未受审的 Agent 发布给公开互联网。
- 审计黑洞:缺乏集中、不可篡改的变更记录与访问审计,导致安全合规团队无法追溯“谁在何时批准了公开访问“。
Portico 专为解决上述痛点而设计。
核心定位:门廊,而非机房
Portico 像门廊,不像机房。
┌──────────────────────────────────────────────┐
│ Portico │
│ │
Humans (审计) │ Portal ────── 发现页 / 内部工作台 / 仪表盘 │
Agents (维护) │ Registry ──── 目录、版本、可见性、引用 │
│ │ Approval ──── 跨越公开信任边界的审批 │
▼ │ Access ────── 分级权限 + 会话防伪认证 │
CLI / MCP / API│ Gateway ───── 鉴权、路由、不可篡改访问审计 │
└──────────────────────┬───────────────────────┘
│ 不运行 Agent
▼
外部 Agent / MCP / CLI 运行时
Portico 明确拒绝成为 Agent 的执行循环(Execution Loop)或工作流编排器:
- Agent 在别处运行:Agent 可以运行在 Kubernetes、云函数、Docker 容器或开发者的本地机器上。
- Portico 负责元治理:Portico 登记其名称、描述、维护者、访问协议入口及可用性状态。
- 严格的门禁控制:任何试图越过内部网络暴露给公众的 Agent,必须在 Portico 留下经过人类安全审计者签名的审计记录。
三大统一治理入口
Portico 提供了三类客户端交互界面,它们严格呈现同一治理状态:
1. Web Portal (浏览器界面)
- 双平面呈现:提供面向组织内成员的
/internal内部笔记台,以及面向全网公开的/public目录。 - 零客户端脚本:界面采用现代语义 CSS 实现交互与主题切换,严格执行
Content-Security-Policy: default-src 'none',根绝 XSS 与脚本注入。 - 受约束组件盒:仅提供
catalog_card、catalog_detail等固定组件,杜绝将门户退化为任意代码可执行的 CMS。
2. Portico CLI (自动化命令行)
- 适合运维人员与自动化维护 Agent 执行编目、发版与状态检查。
- 所有非匿名命令均必须携带严格的会话令牌(
--session),杜绝参数伪造。 - 标准机读输出:统一以
{ok: true, data: ...}或{ok: false, error: ...}输出规范 JSON。
3. MCP Protocol Server (Agent 机器交互入口)
- 暴露标准的 JSON-RPC 2.0 协议(HTTP 传输)。
- 提供只读治理工具(如
portico_list、portico_describe、portico_dashboard、portico_approvals、portico_identities、portico_grants、portico_revokes、portico_whoami、portico_sessions、portico_credentials、portico_credential_revokes、portico_page等),使其他 Agent 能以符合 MCP 规范的标准协议发现同伴服务。
质量与可靠性承诺
Portico 项目的所有核心逻辑均经受严苛的自动化测试矩阵验证:
- 覆盖高风险路径的防御性拦截(自批拦截、伪造头拒绝、越权防护)。
- 验证写操作失败后的严格回滚(不产生脏数据、不篡改文件字节)。
- 单一 Deno L0 运行时保障,权限按最小必要原则配置。
核心架构与信任边界
Portico 的系统架构围绕信任边界划分与严格权限隔离构建。
系统拓扑与进程划分
Portico 运行时包含三个相互独立的常驻服务进程,由 Supervisor(src/up/main.ts)统一监控。进程间不存在共享内存或可执行代码交叉引用,各自遵循最严苛的操作系统级权限沙箱:
┌─────────────────────────────┐
│ Supervisor (deno task up) │
└──────────────┬──────────────┘
┌─────────────────────────┼─────────────────────────┐
│ (fork/pipe) │ (fork/pipe) │ (fork/pipe)
▼ ▼ ▼
┌──────────────────────────┐┌──────────────────────────┐┌──────────────────────────┐
│ Portal Process ││ Gateway Process ││ MCP Process │
│ (HTTP :8788, Read-Only)││ (HTTP :8789, Append) ││ (HTTP :8790, Read-Only)│
│ 权限: --allow-read ││ 权限: --allow-read ││ 权限: --allow-read │
│ --allow-env ││ --allow-write ││ --allow-env │
│ --allow-net=127... ││ --allow-env ││ --allow-net=127... │
│ 严格无 --allow-write ││ --allow-net=127... ││ 严格无 --allow-write │
└─────────────┬────────────┘└─────────────┬────────────┘└─────────────┬────────────┘
│ │ │
└───────────────────────────┼───────────────────────────┘
▼
┌─────────────────────────────┐
│ Local Trust Root (Files) │
│ - catalog.json │
│ - identities.json │
│ - sessions.json │
│ - approvals.json │
│ - gateway-audit.json │
└─────────────────────────────┘
1. Portal 进程 (src/portal/main.ts)
- 端口:默认
8788。 - 定位:Web 门户页面与只读 REST API。
- 安全沙箱:绝不授予
--allow-write写权限。即使页面层存在未知漏洞,攻击者也物理上无法通过 Portal 篡改底层目录或身份文件。
2. Gateway 进程 (src/gateway/main.ts)
- 端口:默认
8789。 - 定位:MCP 服务的安全准入网关与访问路由。
- 安全沙箱:授予
--allow-write仅用于向gateway-audit.json追加访问流水;不代理流量,不执行工具,不代发模型推理。
3. MCP 进程 (src/mcp/main.ts)
- 端口:默认
8790。 - 定位:面向 AI 智能体的只读发现协议端点(JSON-RPC 2.0)。
- 安全沙箱:同 Portal 一样,绝无
--allow-write权限,只作为治理状态的目录投影。
两大信任边界
Portico 将整个系统严格划分为两个不可妥协的信任区域:
[ 组织内部信任区 (Internal) ] │ [ 外部公网不可信区 (Public) ]
│
┌──────────────┐ ┌──────────────┐ │
│ Agent 维护者 │ ───► │ Catalog 内核 │ │
│ (Maintainer) │ register│ (记录状态为 │ │
└──────────────┘ │ internal) │ │
└──────┬───────┘ │
│ │
│ publish public │
▼ │
┌──────────────┐ │
│ pending_public│ │
│ (公开候选) │ │
└──────┬───────┘ │
│ │
approve ▼ │
┌────────────────────────┐ │
│ 人类审计者 (Auditor) │ ──────────────┼──────────────► [ 批准公开 ]
│ 签署审批记录至 approvals │ 通过审批 │ │
└────────────────────────┘ │ ▼
│ ┌──────────────┐
│ │approved_public│
│ │ 全网匿名可达 │
│ └──────────────┘
信任边界 A:内部与公开边界
- 内部登记(Internal):仅对组织内已鉴权身份(具有登录会话的 Reader / Maintainer / Auditor)可见。未通过鉴权的匿名访客完全无法感知内部服务的存在。
- 公开候选(Pending Public):维护者提交公开申请后,服务进入候选状态,此时对外依然保持绝对不可见。
- 公开批准(Approved Public):必须经由持有
auditor角色的独立自然人审计者显式批准。批准通过后,外部匿名请求方可在 Portal、CLI、MCP 发现该入口。
信任边界 B:维护权与审计权边界
- 维护者(Maintainer,人类或 Agent):拥有登记服务、更新描述、提交公开的权力;严禁批准自己提交的公开请求,严禁自封 Auditor 角色,严禁删除审计时间线。
- 审计者(Auditor,仅限人类):拥有批准或拒绝公开、撤回已公开服务、作废危险凭证与注销身份的特权;不直接参与日常业务代码维护。
联动退出与原子恢复保证
在系统监督器架构中:
- 协同崩溃退出:Portal、Gateway、MCP 三大子进程中的任意一个发生未捕获异常退出,Supervisor 会通过 SIGTERM 级联关闭其余所有存活进程,杜绝留下只有部分入口可用的“半死系统”。
- 写操作故障零脏写:所有修改底层 JSON 文件的操作均遵循原子写入协议(先写临时文件
.tmp,再执行原子性系统重命名renameSync)。任何校验失败、权限拦截或断电异常均保证原始状态文件字节不变。
产品铁律与非目标
Portico 在设计之初即确立了不可触碰的产品红线。任何新功能的引入、Bug 修复或架构演进,均不得以“便利”为由突破以下铁律。
产品铁律 (Iron Rules)
1. 不运行、编排、托管 Agent
- Portico 绝不包含 Agent 推理循环、Prompt 模板执行器或工作流编排引擎。
- Portico 绝不代发模型推理请求(如 OpenAI / Anthropic / Gemini API)。
- Portico 网关只负责鉴权与路由发现,绝不替调用方执行任何 MCP 工具。
2. 内部与公开是绝对的信任边界
- 未经审批的草稿、内部服务与公开候选,在任何匿名渠道上均绝对不可达、不可见。
- 任何入口(Portal、CLI、MCP)均不得提供绕过审批直接公开后门的选项。
- 登记操作不能“顺便”完成公开发布。
3. Agent 维护权绝不等于安全审计权
- 自动化维护者 Agent 严禁被授予
auditor角色。 - 提交同一条公开申请的身份绝对不能自批(
SELF_APPROVAL拦截)。 - 维护者身份不能关闭、删除或改写系统审计日志。
4. 三大入口呈现严格一致的治理状态
- Portal、CLI、MCP 必须读取同一套目录内核与权限状态。
- 严禁出现“CLI 能看到公开,但 Portal 页面显示 404”或“MCP 显示内部,Portal 却已向外暴露”的状态分裂。
5. 密钥只引用,绝不落明文
- 登记的元数据、URL、包坐标、日志与审计记录中,严禁写入任何明文 API Key、Token、密码或非公开私钥。
- 端点 URL 严禁包含凭证信息(如
http://user:pass@host)或伪协议(如javascript:)。
6. 单一 Deno L0 运行时
- 纯 Deno + TypeScript 体系。
- 严禁把 Node.js 或 Bun 加入产品依赖、CI 流程或可分发产物。
- 权限默认全拒,禁止使用
--allow-all或等价的无节制通配授权。 - CLI 分发产物必须为纯自包含二进制,运行绝不依赖机器上的
node或bun命令。
明确的非目标 (Non-Goals)
为了保持系统的专注与精悍,以下功能被永久排除在 Portico 之外:
| 非目标领域 | 排除理由 | 替代解法 |
|---|---|---|
| 通用 CMS / 动态建站器 | 复杂的页面自定义与插件生态极易引入 XSS 与代码执行漏洞,破坏审计可读性。 | 提供固定组件盒(catalog_card 等)与严格的语义 Token。 |
| 应用计费与商业市场 | 计费、订单与订阅属于商业中台范畴,与准入治理职责正交。 | 由外部商业系统通过鉴权网关的访问日志进行下游对账。 |
| 分布式工作流调度器 | 编排工具(如 Temporal、LangGraph、Airflow)已有成熟实现,Portico 只做入口发现。 | Agent 自行在外部宿主中执行状态转移。 |
| 集中式流量反向代理 | 代理海量高带宽 Payload 会让网关成为性能与安全单点,且容易偷窥明文数据。 | Gateway 仅提供一次性鉴权路由信息(connect.mode = direct),客户端直连后端。 |
快速上手指南
本指南将带您在 5 分钟内完成 Portico 的起动、首位审计者初始化、维护者授权、服务登记与公开审批流程。
1. 环境准备
确保您的本地环境中已安装 Deno 2.9.x:
deno --version
# 输出应包含 deno 2.9.x
Note
请勿尝试使用 Node.js 或 Bun 运行本仓库命令,Portico 运行时严格锁定为 Deno。
2. 一键起动整套系统
Portico 内置 supervisor 管理脚本。只需指定数据存储目录,即可同时起动 Portal、Gateway 与 MCP 服务:
PORTICO_DATA_DIR=./data deno task up
起动成功后,标准输出将打印包含三个服务 URL 的 JSON:
{"ok":true,"data":{"dataDir":"./data","portal":{"url":"http://127.0.0.1:8788"},"gateway":{"url":"http://127.0.0.1:8789"},"mcp":{"url":"http://127.0.0.1:8790"}}}
保持该终端窗口运行,另开一个新终端进行后续操作。
3. 引导第一位人类安全审计者 (Bootstrap)
在初始状态下,data/identities.json 为空。系统允许无凭证引导首位拥有 auditor 角色的人类身份:
# 1. 注册首位人类审计者
deno task cli -- identity grant \
--identities ./data/identities.json \
--id human:security-auditor --kind human --role auditor
# 2. 签发一次性登录凭证令牌 (token 只打印一次,系统仅存哈希)
deno task cli -- identity credential issue \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--id human:security-auditor
# 控制台输出:{"ok":true,"data":{"token":"pct1_..."}}
# 3. 使用凭证换取有效会话 (Session)
deno task cli -- identity login \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--id human:security-auditor \
--token pct1_<您获取的token>
# 控制台输出:{"ok":true,"data":{"session":"pst1_..."}}
Important
首张凭证发放成功后,系统的 Bootstrap 模式将永久关闭!后续的所有授权与变更操作必须由已登录审计者签署。
我们将拿到的会话令牌保存到环境变量中便于调用:
export AUDITOR_SESSION="pst1_..."
4. 授权维护者 Agent
由审计者授权一个负责编目维护的 Agent:
# 1. 授予 Agent maintainer 权限
deno task cli -- identity grant \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $AUDITOR_SESSION \
--id agent:docs-bot --kind agent --role maintainer
# 2. 为该 Agent 签发凭证
deno task cli -- identity credential issue \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $AUDITOR_SESSION \
--id agent:docs-bot
# 3. Agent 登录获取会话
deno task cli -- identity login \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--id agent:docs-bot \
--token pct1_<agent的token>
保存维护者会话:
export AGENT_SESSION="pst1_..."
5. 登记并发布服务
维护者 Agent 准备一份服务描述文件 mcp-agent.json:
{
"id": "code-helper",
"name": "代码助手 MCP 服务",
"description": "提供代码审查与重构建议的外部 MCP 工具集合",
"channels": ["mcp"],
"entry": {
"mcp_endpoint": "http://127.0.0.1:9000/sse"
},
"version": "1.0.0"
}
执行内部登记:
deno task cli -- catalog register \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $AGENT_SESSION \
--input ./mcp-agent.json
此时打开浏览器访问 http://127.0.0.1:8788/internal(携带会话 Header),可查看到该记录;但公开页 http://127.0.0.1:8788/public 上该服务绝对不可见。
6. 申请公开与独立审批
维护者 Agent 提交公开申请:
deno task cli -- catalog publish \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $AGENT_SESSION \
--id code-helper \
--visibility public
此时服务进入 pending_public(待审)状态。维护者尝试自批将被系统拦截(SELF_APPROVAL)。
人类审计者进行独立安全审计并批准:
deno task cli -- catalog approve \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $AUDITOR_SESSION \
--id code-helper
审批完成!现在在任何匿名浏览器窗口直接打开:
- 门户公开页:
http://127.0.0.1:8788/public - 即可查看到
code-helper服务已经合规向外界发布!
身份与访问控制概览
Portico 的访问控制(Access Control)系统旨在为人与 Agent 共存的组织环境提供严密、透明且防篡改的鉴权体系。
核心设计理念
- 真实证明,拒绝自称:
系统严禁信任客户端自定义的请求头(如
X-Portico-Actor-*或 CLI 参数--actor-*)。CLI、Gateway 与 MCP 必须出示经由服务端校验的登录会话(Session)。Portal 在显式启用时可把已校验的 Cloudflare Access JWT 映射到名册人类身份,这不是会话,也不会写入sessions.json。 - 零明文密码与密钥: 所有凭证(Credentials)在下发后服务端仅保存加盐后的 SHA-256 哈希值。即便数据文件遭读取,攻击者也无法复原凭证或伪造未授权会话。
- 不可逾越的角色边界: 区分人类(Human)与智能体(Agent)。关键安全治理动作(包括审批公开、作废凭证、吊销主体)被硬性限制为仅限人类审计者执行,智能体维护者绝对无法越权。人类身份可绑定唯一 email,只作名册映射,不能代替会话,也不能赋给 Agent。
章节导览
- 信任根与数据存储:深入理解底层存储文件(
identities.json与sessions.json)的原子写入与文件级安全。 - 角色模型与权限矩阵:详解
auditor、maintainer与reader的权限差异,以及主体类型(humanvsagent)的刚性约束。 - 首位人类审计者引导:解析安全 Bootstrap 机制如何实现无需硬编码管理员密码的初始入驻。
- 凭证签发与登录会话:展示基于
pct1_...凭证换取pst1_...会话的完整工作流与跨协议鉴权(CLI / Portal / MCP)。 - Cloudflare Access JWT 映射:Portal 可选、默认关闭的 JWT 映射;密钥与登录界面留在 Cloudflare 边缘,本仓库只认已校验 email。
- 身份注销与凭证作废:剖析软作废(Credential Revoke)与硬吊销(Identity Revoke)的机制差异及其安全回滚保证。
信任根与数据存储
Portico 采用诚实、透明的本地数据文件作为本地信任根(Local Trust Root)。
核心数据结构与职责划分
数据目录(默认为 data/)下由五个核心 JSON 文件维系系统的治理状态:
data/
├── identities.json # 身份名册与角色授权记录
├── sessions.json # 签发的凭证哈希与活跃会话状态
├── catalog.json # 服务登记表与历史变更日志
├── approvals.json # 公开发布的独立审批/拒绝轨迹
└── gateway-audit.json # MCP Gateway 鉴权与直连路由不可篡改流水
1. identities.json (身份名册)
记录系统认可的实体主体及其角色。结构如下:
{
"identities": [
{
"id": "human:security-auditor",
"kind": "human",
"roles": ["auditor"],
"createdAt": "2026-09-14T10:00:00.000Z"
},
{
"id": "agent:catalog-bot",
"kind": "agent",
"roles": ["maintainer"],
"createdAt": "2026-09-14T10:05:00.000Z"
}
]
}
2. sessions.json (凭证与会话库)
记录已发放的凭证哈希、生命周期及通过凭证登录换取的有效会话:
{
"credentials": [
{
"id": "cred-1",
"identityId": "human:security-auditor",
"tokenHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"createdAt": "2026-09-14T10:00:00.000Z",
"revokedAt": null
}
],
"sessions": [
{
"token": "pst1_abc...",
"identityId": "human:security-auditor",
"credentialId": "cred-1",
"createdAt": "2026-09-14T10:01:00.000Z",
"expiresAt": "2026-09-15T10:01:00.000Z"
}
]
}
文件级权限与运维边界
Important
本地信任根的安全边界与操作系统文件权限对齐。
对data/目录拥有操作系统级别写入权限的进程或用户,理论上具备修改甚至重置名册的能力。因此,在生产环境中,数据目录必须严格配置所属用户组与文件权限(如chmod 700 ./data)。
原子写入保证 (Atomic Persistence)
所有对 JSON 文件的持久化写入均由 src/fs.ts 的 writeJsonAtomic 实施原子操作:
- 先写入附带
.tmp.<pid>.<timestamp>后缀的临时文件。 - 调用系统底层
Deno.rename替换原文件。 - 保证即使在遭遇强行杀进程、断电或磁盘写满时,原有文件要么完整更新,要么保持上一次一致状态,绝不产生半截脏文件。
角色模型与权限矩阵
Portico 采用精简而刚性的基于角色的访问控制(RBAC)模型,同时将操作主体区分为人类(Human)与智能体(Agent)。
主体类型 (Kind)
human(人类):组织内的自然人开发、运维或安全人员。拥有充当安全审计者(auditor)的排他性资格。可选绑定一个唯一email(大小写归一),只作名册映射字段,不能代替登录会话,也不能作为外部 IdP 证明。agent(智能体):组织内运行的自动化脚本、CI Runner 或自主智能体。可以担任目录维护者或只读者,但绝对禁止被赋予auditor角色,也不能绑定 email。
三大治理角色 (Roles)
| 角色 | 允许的主体 | 职责定位 | 核心权限 |
|---|---|---|---|
auditor | 仅限 human | 安全合规底线把关 | 审批/拒绝公开申请、撤回公开服务、签发凭证、作废凭证、注销主体、查阅完整审计时间线。 |
maintainer | human 或 agent | 服务编目日常维护 | 登记内部服务、更新已登记表面(draft/internal/rejected)、提交公开申请、排布门户组件盒。 |
reader | human 或 agent | 组织内部服务消费 | 查阅所有内部目录服务、获取已授权直连入口、获取 CLI 包坐标、查询 MCP 描述信息。 |
anonymous | 未登录外部访客 | 全网公开访问 | 仅能在 Portal/CLI/MCP 查阅状态为 approved_public 的已审批公开服务;无法感知任何内部或待审服务。 |
权限决策矩阵
| 操作动作 | 匿名 (Anonymous) | 只读 (Reader) | 维护者 (Maintainer) | 审计者 (Auditor) |
|---|---|---|---|---|
浏览公开记录 (approved_public) | ✅ | ✅ | ✅ | ✅ |
浏览内部记录 (internal) | ❌ | ✅ | ✅ | ✅ |
登记新表面 (catalog register) | ❌ | ❌ | ✅ | ✅ |
更新表面描述 (catalog update) | ❌ | ❌ | ✅ | ✅ |
提交公开申请 (publish public) | ❌ | ❌ | ✅ | ✅ |
审批公开申请 (catalog approve) | ❌ | ❌ | ❌ (严禁自审) | ✅ (独立审计) |
撤回公开服务 (catalog withdraw) | ❌ | ❌ | ❌ | ✅ |
授予主体权限 (identity grant) | ❌ | ❌ | ❌ | ✅ |
注销主体 (identity revoke) | ❌ | ❌ | ❌ | ✅ |
签发新凭证 (credential issue) | ❌ | ❌ | ❌ | ✅ |
作废凭证 (credential revoke) | ❌ | ❌ | ❌ | ✅ |
列出凭证作废轨迹 (credential revokes) | ❌ | ❌ | ❌ | ✅ |
| 查看完整系统审计时间线 | ❌ | ❌ | ❌ | ✅ |
Caution
防越权铁律:如果一个维护者 Agent 尝试调用
identity grant --role auditor试图自我提权,或者尝试调用catalog approve,系统鉴权中间件将立即抛出FORBIDDEN错误并中止执行,不修改任何文件。
首位人类审计者引导 (Bootstrap)
在首次部署 Portico 时,系统面临典型的“先有鸡还是先有蛋”的信任根初始化难题:当名册文件完全为空时,谁来执行第一次授权?
Portico 设计了严谨的 空名册一次性 Bootstrap 机制。
Bootstrap 的三步流程
第一步:登记首位人类审计者
当 identities.json 为空(或文件尚不存在)时,系统允许无需任何会话参数执行首次 grant:
deno task cli -- identity grant \
--identities ./data/identities.json \
--id human:security-auditor \
--kind human \
--role auditor
内部校验规则:
- 仅当名册内没有任何有效主体时,才允许无
--session执行。 - 引导的主体必须显式指定
--kind human且包含--role auditor。严禁在引导阶段创建 Agent。
第二步:一次性发放首张初始凭证
首次签发凭证同样豁免会话校验:
deno task cli -- identity credential issue \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--id human:security-auditor
控制台将输出包含一次性明文 Token 的 JSON:
{
"ok": true,
"data": {
"identityId": "human:security-auditor",
"token": "pct1_7f8a3c4d5e6f..."
}
}
Warning
Token 仅在此时打印一次!
服务端在sessions.json中仅保存该 Token 的 SHA-256 哈希。一旦首张凭证签发完成,系统的 Bootstrap 模式即刻永久关闭。后续所有credential issue必须携带已登录审计者的有效会话。
第三步:凭证登录并获取初始会话
deno task cli -- identity login \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--id human:security-auditor \
--token pct1_7f8a3c4d5e6f...
输出换取的 Session 令牌:
{
"ok": true,
"data": {
"identityId": "human:security-auditor",
"session": "pst1_1a2b3c4d5e..."
}
}
至此,首位人类审计者拥有了合法的会话令牌 pst1_...。从此往后,系统中所有的名册变更、Agent 授权和凭证轮转,都必须出示该会话(或派生出的其他审计者会话)进行签名。
凭证签发与登录会话
在 Portico 中,会话(Session)是 CLI、Gateway 与 MCP 的唯一有效证明。Portal 默认同样只认会话;仅当显式启用 Cloudflare Access 时,校验通过的 JWT 可以映射到名册上的人类身份,且不会签发会话。
令牌前缀与分类
为了便于机读与肉眼审计,Portico 的安全令牌遵循严格的前缀规范:
| 令牌类型 | 前缀格式 | 说明 | 存储形式 |
|---|---|---|---|
| 凭证令牌 (Credential Token) | pct1_<hex> | 用于换取会话的一次性/长期密钥。仅在 issue 时下发一次。 | 服务端仅存加盐 SHA-256 哈希 |
| 会话令牌 (Session Token) | pst1_<hex> | 登录成功后返回的上下文凭据,用于 API/CLI/MCP 请求。 | 服务端存明文用于比对,具备过期时间 |
跨入口鉴权协议
无论使用哪个客户端入口,客户端均必须携带有效的 Session 令牌:
1. CLI 命令行
通过全局参数 --session 显式传入:
deno task cli -- catalog list \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session pst1_1a2b3c4d5e...
2. Web Portal 与 REST API
支持标准 HTTP Header。可选的 Cloudflare Access JWT(Cf-Access-Jwt-Assertion)只在 Portal 进程、且环境变量显式启用时生效;明文 Cf-Access-Authenticated-User-Email 不是证明。Gateway 与 MCP 忽略该头。Portico 侧步骤见 Cloudflare Access JWT 映射。
# 推荐方式:标准 Authorization Bearer
curl -s -H "Authorization: Bearer pst1_1a2b3c4d5e..." http://127.0.0.1:8788/api/catalog
# 备选方式:自定义 Header
curl -s -H "X-Portico-Session: pst1_1a2b3c4d5e..." http://127.0.0.1:8788/api/catalog
3. MCP Protocol Server (JSON-RPC)
在发送 HTTP POST 请求时附带 Bearer Token:
curl -s -X POST http://127.0.0.1:8790 \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer pst1_1a2b3c4d5e...' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"portico_list","arguments":{}}}'
审计者查看会话轨迹
登录与作废仍走 CLI。人类审计者可以用同一只读投影核对现有会话,而不接触令牌或哈希:
- CLI:
identity sessions - Portal:
GET /api/sessions - MCP:
portico_sessions
三者返回同一批 id / subjectId / createdAt / expiresAt(已作废则含 revokedAt)。维护者、只读者与匿名得到 FORBIDDEN。读操作不改 sessions.json。
审计者查看凭证轨迹
签发与作废仍走 CLI。人类审计者可以用同一只读投影核对已签发凭证,而不接触令牌或哈希:
- CLI:
identity credentials - Portal:
GET /api/credentials - MCP:
portico_credentials
三者返回同一批 id / subjectId / credentialRef / issuedBy / issuedAt(已作废则含 revokedAt)。维护者、只读者与匿名得到 FORBIDDEN。读操作不改 sessions.json。这不是签发或作废入口。
凭证作废轨迹(谁在何时切断了哪个主体的登录面)走另一条只读合同:
- CLI:
identity credential revokes - Portal:
GET /api/credential-revokes - MCP:
portico_credential_revokes
三者返回同一批 id / subjectId / kind / role / revokedBy / revokedAt / credentials / sessions。维护者、只读者与匿名得到 FORBIDDEN。读操作不改 identities.json 或 sessions.json。这不是作废入口。
伪造头全面拦截机制
在过去很多轻量级管理界面中,系统容易草率地读取请求头中的自称信息(例如 X-Actor-Id)。这在多 Agent 协作环境中极度危险:由于名册中的 Agent ID 是公开的元数据,恶意脚本只要在 Header 中填入审计者的 ID,就能冒充审计者批准自身公开!
Portico 在所有协议层做出如下防御性断言:
- 服务端完全废弃
X-Portico-Actor-*:Portal、Gateway 与 MCP 服务端完全不再读取此类请求头。带有此类头的请求只会被当作纯粹的匿名请求。 - CLI 拒绝单独传入
--actor-*:如果命令中未提供--session,却试图通过--actor-id伪装身份,CLI 将直接报错拒绝(错误代码USAGE),并中止执行。
Cloudflare Access JWT 映射(Portal 可选)
Portico 不是身份提供者,也不托管登录界面。Portal 在显式启用时,可以把 Cloudflare Access 签发的 JWT 映射到本地名册上的人类身份。这不是第二套可写身份,也不会签发 pst1_ 会话。
CLI、Gateway 与 MCP 继续只认 Portico 会话。GitHub 登录与邮箱 One-time PIN 如果要存在,只存在于 Cloudflare 边缘,不进入本仓库。
何时启用
默认关闭。未设置 PORTICO_CF_ACCESS_ENABLED,或缺少合法 team / audience 时,现有 CLI 会话路径不变,JWT 被忽略。
同时满足下列条件才启用:
| 变量 | 作用 | 示例(占位符) |
|---|---|---|
PORTICO_CF_ACCESS_ENABLED | 打开映射。仅 true / yes / on / 1 视为开启 | true |
PORTICO_CF_ACCESS_TEAM | Access team 名,只允许 [a-z0-9-],用于拼 ISS 与默认 JWKS | example |
PORTICO_CF_ACCESS_AUD | Application audience,防止跨应用重放 | example-audience |
PORTICO_CF_ACCESS_JWKS_URL | 可选覆盖。缺省为该 team 的 Cloudflare certs URL | http://127.0.0.1:9/certs(仅测试) |
example team 对应的 ISS 是 https://example.cloudflareaccess.com。JWKS 覆盖只允许:
- 测试用
http://127.0.0.1/... - 该 team 的
https://<team>.cloudflareaccess.com/cdn-cgi/access/certs
其它 URL、缺 team、缺 audience:功能保持关闭,而不是 500。
真实 team 名与 AUD 只在安装现场注入,不要写进本仓库、CI 或示例实值。
名册绑定
外部登录成功不等于拥有特权。JWT 里的 email 必须命中名册中的人类身份;Agent 不能带邮箱。
deno task cli -- identity grant \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session "$AUDITOR_SESSION" \
--id human:reader \
--kind human \
--role reader \
--email reader@example.invalid
邮箱会大小写归一。reader@example.invalid 只是文档占位符。邮箱不是第二身份证明:没有合法 JWT 或会话时,它不会让请求变成已登录。
Portal 如何判定身份
- 已有 Portico 会话(
Authorization: Bearer或X-Portico-Session)优先。 - 无会话且功能已启用时,读取
Cf-Access-Jwt-Assertion,校验 RS256 签名、aud、iss、exp,再用已校验 email 查名册。 - 明文
Cf-Access-Authenticated-User-Email不是证明,单独携带该头仍是匿名。 - 都没有或任何一步失败:匿名。
/internal返回 HTML 404,不泄漏目录。
成功或失败都不写 identities.json / sessions.json,不签发会话。Portal 进程仍然没有 --allow-write。
这是 fail-closed:伪造签名、过期、AUD 不匹配、JWKS 不可达、名册未登记,全部降为匿名,而不是变成 500 打开内部面。
边缘 IdP(不在本仓库)
GitHub 组织登录与邮箱 One-time PIN 在 Cloudflare Access identity providers 配置。Client Secret 与 OTP 投递留在 Cloudflare,不要复制进 Portico、环境示例或审计日志。
JWT 校验口径见 Validating JSON Web Tokens。
明确不做
- 不在 Portico 实现 GitHub OAuth 客户端、邮箱 OTP 或本地 SMTP。
- 不把 Cloudflare Tunnel /
cloudflared配置、隧道 token 或反代模板收进本仓库。那是边缘基础设施。 - 不把 JWT 扩到 CLI、Gateway 或 MCP。
- 不把未审批对象变成公开可达。匿名与失败映射看到的仍是公开面。
身份注销与凭证作废
当 Agent 行为异常、凭证泄露或人员离职时,安全审计者需要迅速阻断其访问能力。Portico 提供了两级处置手段:凭证作废(Credential Revocation) 与 身份注销(Identity Revocation)。
凭证作废 vs 身份注销
| 维度 | 凭证作废 (identity credential revoke) | 身份注销 (identity revoke) |
|---|---|---|
| 操作目的 | 阻断失窃或泄露的密钥/会话,允许重新发放凭证 | 永久注销主体在名册中的资格 |
| 名册影响 | 主体保留在 identities.json 中 | 从 identities.json 中移除主体资格 |
| 会话影响 | 立即作废该主体名下所有活跃凭证与会话 | 立即导致持有原会话的任何请求判定为 FORBIDDEN |
| 历史产物 | 该主体登记的 Catalog 记录保留 | 该主体留下的历史 Catalog 与审计记录依然完整保留 |
| 后续恢复 | 审计者再次 credential issue 即可恢复使用 | 必须由审计者重新 grant 授予身份 |
凭证作废工作流
deno task cli -- identity credential revoke \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $AUDITOR_SESSION \
--id agent:compromised-bot
执行保证:
- 两阶段原子提交:系统首先将事件计入审计流水,随后将
sessions.json中该主体的所有凭证打上revokedAt时间戳,并删除关联会话。若更新失败,状态自动回滚。 - 免受无状态污染:若该主体当前没有任何存活凭证或有效会话,命令将返回
INVALID_STATE,避免产生空写。
作废写入之后,轨迹本身是只读的。同一人类审计者经 CLI identity credential revokes、Portal GET /api/credential-revokes 与 MCP portico_credential_revokes 看到同一批追加式记录;维护者、只读者与匿名得到 FORBIDDEN。读操作不改名册或会话文件,也不是作废入口。
身份注销工作流
deno task cli -- identity revoke \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $AUDITOR_SESSION \
--id agent:retired-agent
安全铁律保证:
- 禁止自撤:审计者无法注销自己当前的会话主体(防止组织陷入无管理人的闭锁死局)。
- 保护最后一位人类审计者:系统禁止注销名册中仅剩的最后一名人类审计者。尝试此类操作将直接抛出
INVALID_STATE。 - 独立性保障:注销某一个主体绝不影响名册中其他无辜主体的凭证与会话。
撤回写入之后,轨迹本身是只读的。同一人类审计者经 CLI identity revokes、Portal GET /api/revokes 与 MCP portico_revokes 看到同一批追加式记录;维护者、只读者与匿名得到 FORBIDDEN。读操作不改名册,也不是撤回入口。
目录内核与治理模型概览
Portico 的 Catalog 目录模块是整个治理门户的数据中心与唯一事实来源(Single Source of Truth)。
核心职责
- 服务画像登记:记录所有 Agent 的身份标识、中英文元数据、通信渠道与协议入口坐标。
- 生命周期治理:驱动记录在
draft、internal、pending_public、approved_public与rejected五态之间有序转移。 - 公开审批缓冲:拦截未经授权的公开暴露行为,强行注入人类审计门禁。
- 统一投影分发:确保 Web Portal、CLI 以及 MCP 协议端点对外呈现完全一致的目录视图。
目录文件结构
目录数据保存在 data/catalog.json 中,核心包含两部分:
records:当前存活的服务表面列表。changes:追加写变更历史,记录谁在何时创建或修改了何种字段。
所有底层写操作均受原子文件重命名保护,杜绝因进程被杀导致的 JSON 结构损坏。
记录元数据规范 (Schema)
所有登记到 Portico 的服务表面必须符合严格的 JSON Schema 契约。非法字段、未知属性或疑似包含敏感信息的值将被系统直接拒绝。
完整字段定义
interface CatalogRecord {
/** 唯一标识符:只允许小写字母、数字与连字符 [a-z0-9-],长度 1-64 */
id: string;
/** 服务人类可读展示名称,长度 1-128 */
name: string;
/** 详细功能概述,支持 Markdown 纯文本描述,长度 1-2048 */
description: string;
/** 语义化版本号,例如 "1.0.0" */
version: string;
/** 支持的接入渠道列表,可选值:["mcp", "web", "cli"] */
channels: Array<"mcp" | "web" | "cli">;
/** 渠道对应的实际入口坐标(严禁包含明文 Token/私钥) */
entry: {
/** 当 channels 包含 mcp 时必填,必须为合法的 http(s) URL */
mcp_endpoint?: string;
/** 当 channels 包含 web 时必填,必须为合法的 http(s) URL */
url?: string;
/** 当 channels 包含 cli 时必填,必须为以 jsr: 或 npm: 开头的包坐标 */
package?: string;
};
/** 登记该服务的维护者主体 ID,例如 "agent:code-bot" */
maintainer: string;
/** 治理生命周期状态 */
governanceState: "draft" | "internal" | "pending_public" | "approved_public" | "rejected";
/** 可见性范畴 */
visibility: "internal" | "public";
/** ISO 8601 创建时间戳 */
createdAt: string;
/** ISO 8601 最后修改时间戳 */
updatedAt: string;
}
字段约束与防注入规则
id严格合规:- 必须匹配正则表达式
^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$。 - 禁止使用包含路径遍历特征(如
../)、特殊符号或中文。
- 必须匹配正则表达式
channels与entry强制对称:- 若
channels声明了"mcp",则entry.mcp_endpoint必须存在且为合法 URL。 - 若未声明某一渠道,则
entry中对应字段不得传入冗余数据。
- 若
- URL 安全硬边界:
- 协议必须为
http:或https:。 - 严禁包含凭证信息(如
https://user:password@host)。 - 严禁使用
javascript:、data:或file:伪协议。 - Web 入口不得指向 Portico 自己的阅读页:路径为
/s/<id>或/public/s/<id>(忽略主机、尾斜杠与查询串)时写入失败。已公开记录不会被这条规则改写,须先撤回再更新。
- 协议必须为
- CLI 包坐标规则:
- 必须显式以
jsr:或npm:作为前缀(例如jsr:@scope/tool或npm:some-cli-bin)。 - 严禁包含系统命令拼接字符(如
;、|、&、$、反引号)。
- 必须显式以
- 未知字段全面拦截:
- 传入任何未在 Schema 中定义的字段(例如试图附加私有配置或模型参数),写入将失败并报错
INVALID_INPUT。
- 传入任何未在 Schema 中定义的字段(例如试图附加私有配置或模型参数),写入将失败并报错
生命周期状态机
Portico 将服务的生命周期划分为 5 种明确的状态,状态流转受到角色权限与治理动作的刚性约束。
状态定义一览
| 状态标识 | 说明 | 组织内可见度 | 外部匿名可见度 | 允许的操作 |
|---|---|---|---|---|
draft | 本地草稿,尚未在组织内发布 | 仅创建者可见 | ❌ 不可见 | publish internal, update |
internal | 组织内部正式服务 | 组织内所有人可见 | ❌ 不可见 | publish public, update |
pending_public | 公开候选,等待独立人类审计 | 组织内可见 | ❌ 不可见 | approve, reject (仅限审计者) |
approved_public | 已通过审批,全网公开可用 | 全局可见 | ✅ 匿名可见 | withdraw (仅限审计者) |
rejected | 公开审批被驳回,退回内部修改 | 组织内可见 | ❌ 不可见 | update, 重新 publish |
状态流转图解
┌─────────────┐
│ draft │ (草稿:维护者可见)
└──────┬──────┘
│ publish internal
▼
┌─────────────┐
┌────► │ internal │ (组织内部正式服务)
│ └──────┬──────┘
│ │ publish public
│ ▼
│ ┌──────────────┐
│ │pending_public│ (公开申请中:等待人类安全审计)
│ └───┬──────┬───┘
withdraw│ approve │ │ reject
(审计者)│ (审计者)│ │ (审计者)
│ ▼ ▼
│ ┌──────────────┐ ┌───────────┐
└──┤approved_public │ rejected │ (被驳回:可继续修改并重提)
└──────────────┘ └─────┬─────┘
│ update & publish public
└────────────► 回到 pending_public
核心流转规则说明
- 草稿(draft)的隔离性:
维护者通过
catalog draft创建的记录不会出现在常规 Reader 的列表中,便于维护者打磨描述。 - 公开绝不能“顺便成功”:
无论调用者是谁,即使是拥有最高权限的系统管理员,调用
catalog register或catalog publish --visibility public时,记录状态也只能进入pending_public,绝对无法直接落盘为approved_public。 - 退回与可编辑性设计:
被驳回(
rejected)的记录由于从未越过公开信任边界,因此允许维护者使用catalog update修改不合规的说明或端点,随后重新提交审批。 - 已公开记录禁止原地偷改:
一旦记录成为
approved_public,维护者严禁直接调用update修改端点或版本(防止恶意替换已被批准的合规服务)。若需变更,审计者必须先执行withdraw撤回公开,再走重新提交流程。
内部草稿与发布
本章介绍维护者如何使用 Portico CLI 进行服务的草稿保存、内部发布以及向组织外部提交公开候选。
1. 保存为草稿 (catalog draft)
当服务描述尚未定稿,或端点尚在联调中时,维护者可以先将其登记为草稿:
# 准备 draft-agent.json
cat << 'EOF' > draft-agent.json
{
"id": "sql-optimizer",
"name": "SQL 优化专家",
"description": "分析复杂 SQL 执行计划并输出重写建议",
"channels": ["cli"],
"entry": {
"package": "jsr:@tools/sql-optimizer@0.1.0"
},
"version": "0.1.0"
}
EOF
# 登记草稿
deno task cli -- catalog draft \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $MAINTAINER_SESSION \
--input ./draft-agent.json
特点:
- 此时
governanceState为draft,visibility为internal。 - 其他普通
reader角色调用catalog list时,该记录被自动过滤,不会造成视图干扰。
2. 内部正式发布 (catalog publish internal)
当草稿联调完毕,可将其发布为组织内部可消费的正式服务:
deno task cli -- catalog publish \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $MAINTAINER_SESSION \
--id sql-optimizer \
--visibility internal
特点:
governanceState转换为internal。- 组织内所有拥有
reader、maintainer或auditor角色的成员均可在 Portal 内部笔记台或 CLI 查阅并使用该服务。 - 外部公网访问依然完全无法察觉该服务的存在。
3. 直接内部登记 (catalog register)
如果一开始服务就已完备,可直接一步登记为内部服务:
deno task cli -- catalog register \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $MAINTAINER_SESSION \
--input ./sql-agent.json
Tip
catalog register创建的记录默认进入internal状态。若传入文件试图硬编码"visibility": "public",命令将直接抛出PUBLIC_REQUIRES_APPROVAL错误并拒绝写入。
4. 提交公开申请 (catalog publish public)
当希望向组织外部公开该服务时,维护者发起公开申请:
deno task cli -- catalog publish \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $MAINTAINER_SESSION \
--id sql-optimizer \
--visibility public
此时:
- 记录的
governanceState变为pending_public。 - 匿名公网访客依然不可见。
- 审批队列中将新增待办,等待独立人类审计者查验合规性。
受治理的表面更新
在服务迭代过程中,维护者经常需要修订 Agent 的功能描述、更新入口版本号或增减访问渠道。Portico 提供了受治理的表面更新机制(catalog update)。
允许更新的范围与限制
deno task cli -- catalog update \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $MAINTAINER_SESSION \
--id sql-optimizer \
--input ./updated-patch.json
允许修改的字段
name:修改人类可读名称。description:修改业务说明、参数指引等文档文本。version:更新版本号(如0.1.0->0.2.0)。channels&entry:调整通信渠道或更新入口地址(如更换 MCP 链接、升级 CLI 包版本)。
严格禁止直接修改的字段
id:服务的唯一业务主键不可篡改。maintainer:维护归属关系受审计追踪约束。governanceState与visibility:严禁通过 update 绕过状态机直接提权或发布公开!
状态与可编辑性矩阵
Portico 针对不同生命周期阶段实行差异化编辑准入:
| 当前状态 | 是否允许直接 update | 治理设计原理 |
|---|---|---|
draft | 允许 | 处于草稿阶段,维护者可自由修改调试。 |
internal | 允许 | 组织内部服务,变更不会造成公开信任边界外泄。系统将记录原子变更日志。 |
rejected | 允许 | 核心设计! 被拒绝的表面从未越过边界。如果将其做成不可变终态,会导致“拒绝→修改→重新提交”闭环断裂,并永久污染该 id。 |
pending_public | 禁止 | 正在接受审计,锁定快照,防止“偷梁换柱”(在审计者阅读后偷改恶意端点)。必须先由审计者 reject 退回。 |
approved_public | 禁止 | 已对全网公开,必须保持与审批快照的一致性。若需更新,审计者必须先 withdraw 撤回至内部。 |
渠道与入口的一致性再校验
当 catalog update 同时或分别修改 channels 与 entry 时,系统会像首次登记一样执行强一致性校验:
- 若将 channels 扩展为
["mcp", "web"],但entry中未提供url,更新将被立即拒绝并回滚。 - 若传入了非法协议(如
javascript:)或疑似包含凭证的 URL,操作将报错拦截。
公开发布与独立审批
公开发布意味着 Agent 将越过组织内网边界,暴露在公网视线之下。Portico 将这一动作确立为最高风险级别,必须经由独立自然人审计者双人把关。
审批流程全景
[ 维护者 Agent / 开发者 ] [ 独立人类审计者 (Auditor) ]
│ │
│ 1. publish --visibility public │
├───────────────────────────────────────►│ (进入 pending_public)
│ │
│ 2. 尝试自我批准 (自批) │
├──────┐ │
│ ▼ │
│ ❌ [ 403 SELF_APPROVAL ] │
│ │
│ │ 3. 查验端点、代码与权限
│ │ 4. approve 或 reject
│ ├──────────────┐
│ │ ▼
│ │ 写入 approvals.json
│ │ 更新 catalog.json
│ │ │
│ ▼ ▼
[ 外部匿名公众 ] ◄───────────────────────── [ 公开可用:approved_public ]
审批命令实操
0. 列出审批记录 (catalog approvals)
公开边界上的通过、拒绝与撤回记录是同一份只读列表。CLI catalog approvals、Portal GET /api/approvals 与 MCP portico_approvals 对同一身份返回同一批记录、同一顺序。已登录身份(只读 / 维护者 / 人类审计者)可以看到记录;匿名得到空列表,不泄漏待审或已拒绝入口。这不是写路径:Portal POST 返回 405,读操作不改目录。
deno task cli -- catalog approvals \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $SESSION
1. 批准公开 (catalog approve)
deno task cli -- catalog approve \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $HUMAN_AUDITOR_SESSION \
--id sql-optimizer
生效结果:
catalog.json中的governanceState更新为approved_public,visibility更新为public。approvals.json中永久追加一条由该审计者签名的审计记录,包含当时的记录哈希与审批时间戳。- 外部匿名访客通过浏览器打开
/public即可立即查看到该服务卡片。
2. 驳回公开 (catalog reject)
如果审计者发现该 Agent 描述不清晰、存在敏感信息或入口端点有安全隐患,可以果断驳回:
deno task cli -- catalog reject \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $HUMAN_AUDITOR_SESSION \
--id sql-optimizer
生效结果:
- 状态退回到
rejected。 approvals.json中记录驳回事件。- 公开面保持绝对不可见。维护者可以在修改不合规项后重新提交申请。
防自批铁律 (Anti-Self-Approval)
系统在审批逻辑层注入了不可绕过的自审拦截器:
- 同主体拦截:若执行
catalog approve的会话主体 ID 与该服务当前快照的maintainer完全一致,系统将无条件抛出SELF_APPROVAL错误。 - 角色互斥:即使维护者是人类,只要他是该服务的登记人,他就无法自行审批该记录,必须由另一位具备 auditor 角色的人类同事进行跨主体审批。
- Agent 免谈:Agent 根本无法被授予
auditor角色,从源头上杜绝了自动化脚本自导自演批准公开的可能性。
公开发布撤回
当公开服务出现突发安全漏洞、上游依赖失效或业务下线时,必须能够迅速切断外部公开暴露。Portico 提供了安全、不可篡改的公开发布撤回机制(catalog withdraw)。
撤回的治理意义
撤回是公开信任边界的收缩方向:
- 撤回后,记录立即回到
internal状态,对匿名公众与外部 API 调用完全隐藏,响应不泄漏任何端点。 - 组织内成员(Reader 及以上)依然可以从内部通道访问该记录,以便定位排查问题。
- 撤回操作与批准操作处于同一最高信任平面:仅限拥有
auditor角色的人类审计者可以执行。维护者、只读人员或匿名访客执行撤回将收到FORBIDDEN错误。
撤回命令实操
deno task cli -- catalog withdraw \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $HUMAN_AUDITOR_SESSION \
--id sql-optimizer
响应示例:
{
"ok": true,
"data": {
"id": "sql-optimizer",
"governanceState": "internal",
"visibility": "internal",
"withdrawnAt": "2026-09-14T12:00:00.000Z"
}
}
核心安全保障
- 多端即时切断:
- Portal 的
/public页面立刻移除该服务卡片;直接访问/public/s/sql-optimizer立即返回标准 HTML 404,且不返回任何内部入口地址。 - Gateway 收到该服务的匿名鉴权请求立即返回 404。
- MCP Protocol 端点对匿名只返回存活公开服务,被撤回服务瞬间消失。
- Portal 的
- 状态前置机读校验:
- 只有当前处于
approved_public的记录才允许撤回。对internal、pending_public、rejected或已撤回的记录执行撤回,将返回INVALID_STATE。
- 只有当前处于
- 不可篡改审计证据:
- 撤回事件会以
action: "withdrawn"原子追加至approvals.json(包含执行者、时间戳与版本号)。
- 撤回事件会以
- 重新上线门槛:
- 撤回后的记录若需再次公开,维护者必须重新执行
catalog publish --visibility public,使其回到pending_public候选状态,必须重新经受独立人类审计者的全流程审批,绝无“自动恢复”后门。
- 撤回后的记录若需再次公开,维护者必须重新执行
多渠道治理入口概览
现代 AI 智能体体系不是单一形态的软件,它们以多种协议和载体存在。
Portico 原生治理三大访问渠道:
- MCP 渠道:面向智能体间通信的 Model Context Protocol 端点。
- Web 渠道:面向人类管理员或人机协同操作的浏览器界面。
- CLI 渠道:面向本地终端开发与脚本集成的命令行分发包坐标。
渠道抽象原则
在 Portico 中,渠道的登记与治理遵循以下铁律:
- 只登记元数据,不充当代理执行器:
- MCP 渠道登记端点 URL,Portico 网关只发放已授权的连接路由,不转发 JSON-RPC 工具调用。
- Web 渠道登记 Web URL,Portal 仅提供直连超链接,不爬取、不反向代理页面内容。
- CLI 渠道登记包发布坐标(如 JSR 或 NPM),CLI 命令仅输出坐标信息,不自动安装、不代跑脚本。
- 多通道并存与入口分离:
- 一个 Agent 服务可以同时拥有多种渠道(例如同时拥有 Web 调试台与 MCP 接口)。
- 在 Portal 页面上,不同渠道将以鲜明的语义 Badge 与专有直连动作展示。
MCP 渠道与连接信息
Model Context Protocol (MCP) 是当前连接 AI 智能体与外部工具、知识库的主流开放协议。Portico 针对外部 MCP Server 提供了严格的元数据治理规范。
登记与入口要求
在服务描述中声明 MCP 渠道:
{
"id": "github-tools",
"name": "GitHub 运维助手 MCP",
"description": "提供仓库分支、Issue 与 PR 自动分析的 MCP 服务",
"channels": ["mcp"],
"entry": {
"mcp_endpoint": "https://mcp.example.internal/sse"
},
"version": "1.2.0"
}
安全规范
- 端点协议:必须为绝对的
http://或https://地址。 - 严禁包含凭证:URL 中严禁携带基本认证信息(例如
https://token:secret@...)。 - 禁止命令注入:严禁在此处传入可执行命令(如
npx -y @modelcontextprotocol/server-github)。Portico 登记的是已经启动的远程服务端点,而非在本地主机拉起进程的命令。
查询与连接信息发现
用户或调用 Agent 可以通过 CLI 或 REST API 查询已授权的 MCP 连接信息:
CLI 查询
# 列出可见的 MCP 渠道服务
deno task cli -- mcp list \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $READER_SESSION
# 查看特定服务的连接坐标
deno task cli -- mcp describe \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $READER_SESSION \
--id github-tools
输出标准结构:
{
"ok": true,
"data": {
"id": "github-tools",
"name": "GitHub 运维助手 MCP",
"version": "1.2.0",
"connect": {
"mode": "direct",
"endpoint": "https://mcp.example.internal/sse"
}
}
}
Portal API
curl -s -H "Authorization: Bearer $READER_SESSION" http://127.0.0.1:8788/api/mcp
返回经过当前会话鉴权过滤后的全部可访问 MCP 服务列表。
Web 渠道与直连发现
许多 Agent 系统附带了人类可交互的 Web 控制台或调试看板。Portico 为 Web 渠道提供了安全的登记与受控链接分发。
登记与入口要求
在服务描述中声明 Web 渠道:
{
"id": "agent-dashboard",
"name": "多智能体协作观测台",
"description": "实时查看多 Agent 交互拓扑与任务状态的 Web 前端",
"channels": ["web"],
"entry": {
"url": "https://dashboard.example.internal/workspace"
},
"version": "2.0.0"
}
安全限制
- 必须为绝对的
http://或https://地址。 - 严禁包含凭证信息或用户名。
- 严禁包含
javascript:、vbscript:或data:伪协议(防止存储型 XSS 漏洞)。
查询与直连发现
1. CLI 命令行
# 列出可见的 Web 渠道服务
deno task cli -- web list \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $READER_SESSION
# 查询 Web 直连入口
deno task cli -- web describe \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $READER_SESSION \
--id agent-dashboard
输出机读 JSON:
{
"ok": true,
"data": {
"id": "agent-dashboard",
"name": "多智能体协作观测台",
"version": "2.0.0",
"connect": {
"mode": "direct",
"href": "https://dashboard.example.internal/workspace"
}
}
}
2. Portal 页面直连按钮
在 Web Portal 的卡片与详情页中,经过授权的身份将看到一个直接跳转的外链按钮(带有 rel="noopener noreferrer"),用户可一键点击进入目标系统的控制台,而 Portico 不对目标网页实施反向代理或内容修改。
CLI 渠道与包坐标发现
很多团队将 Agent 封装为可以在开发者本地终端或 CI/CD 流程中调用的 CLI 二进制或包分发物。
登记与入口规范
{
"id": "code-review-cli",
"name": "代码评审命令行工具",
"description": "本地执行静态检查与 Agent 自动评审的 CLI 客户端",
"channels": ["cli"],
"entry": {
"package": "jsr:@tools/reviewer@1.1.0"
},
"version": "1.1.0"
}
严格的坐标限制
为了防止恶意维护者通过登记参数在客户端主机上执行任意 Bash 脚本,Portico 对 entry.package 施加了极端的约束:
- 仅允许支持的前缀:必须以
jsr:或npm:开头。 - 纯粹的包标识符:例如
jsr:@scope/tool@1.0.0或npm:some-pkg@^2.0。 - 禁止命令语法:严禁传入形如
npx ...、curl | bash、带管道符、重定向符或本地文件系统绝对路径的字符串。
发现与使用
CLI 查询
deno task cli -- cli list \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $READER_SESSION
deno task cli -- cli describe \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $READER_SESSION \
--id code-review-cli
输出标准结构:
{
"ok": true,
"data": {
"id": "code-review-cli",
"name": "代码评审命令行工具",
"version": "1.1.0",
"connect": {
"mode": "coordinate",
"package": "jsr:@tools/reviewer@1.1.0"
}
}
}
开发人员或 CI 脚本获取到该坐标后,可通过原生包管理器(如 deno run jsr:@tools/reviewer)在自己的沙箱环境中执行,Portico 本身绝不参与该工具的下载与执行。
MCP 协议服务器 (JSON-RPC)
除了登记外部 MCP 服务外,Portico 自身在 8790 端口上实现了一个纯纯只读的 MCP Protocol Server。这使得任何支持 MCP 协议的智能体(如 Claude Desktop、Cursor、Cline、Gemini 等)可以直接将 Portico 添加为 Tool Server,从而以结构化的方式感知组织内部的治理资产。
协议与传输规范
- 协议规范:JSON-RPC 2.0。
- 传输层:HTTP POST 请求(默认监听
http://127.0.0.1:8790)。 - 只读保证:服务进程运行时严格无
--allow-write文件写权限。
内置治理工具一览
Portico MCP 服务端暴露了 15 个经过安全收敛的只读治理工具:
| 工具名称 (Tool Name) | 参数说明 | 权限要求 | 功能描述 |
|---|---|---|---|
portico_list | { channel?: string, state?: string } | 匿名或持会话 | 查询当前可见的服务列表。匿名请求仅返回 approved_public 记录。 |
portico_describe | { id: string } | 匿名或持会话 | 查询指定服务的元数据与连接信息。若无权访问返回 NOT_FOUND。 |
portico_entry | { id: string, channel?: string } | 匿名或持会话 | 获取特定渠道的直连端点或包坐标。 |
portico_dashboard | {} | 匿名或持会话 | 获取系统资产大盘统计(服务总数、渠道分布、公开数)。 |
portico_audit | { q?: string, kind?: string, action?: string, subject?: string } | 仅限人类审计者 | 查询审计时间线流水。非审计者调用返回 FORBIDDEN。 |
portico_approvals | {} | 已登录会话;匿名为空列表 | 列出公开边界审批记录(通过 / 拒绝 / 撤回)。与 CLI catalog approvals、Portal GET /api/approvals 同一载荷。 |
portico_identities | {} | 维护者与人类审计者 | 列出名册身份(id / kind / role,可选 email)。只读与匿名返回 FORBIDDEN。不返回凭证或会话。 |
portico_grants | {} | 仅限人类审计者 | 列出追加式授权轨迹。与 CLI identity grants、Portal GET /api/grants 同一载荷。维护者、只读与匿名返回 FORBIDDEN。不返回凭证或会话。 |
portico_revokes | {} | 仅限人类审计者 | 列出追加式身份撤回轨迹。与 CLI identity revokes、Portal GET /api/revokes 同一载荷。维护者、只读与匿名返回 FORBIDDEN。不返回凭证或会话。读操作不写名册。 |
portico_whoami | {} | 已登录会话;匿名 FORBIDDEN | 返回当前已证明身份的 id / kind / role。与 CLI identity whoami、Portal GET /api/whoami 同一载荷。不返回邮箱、凭证或会话。 |
portico_sessions | {} | 仅限人类审计者 | 列出登录会话轨迹(id / 主体 / 时间,作废则含 revokedAt)。与 CLI identity sessions、Portal GET /api/sessions 同一载荷。维护者、只读与匿名返回 FORBIDDEN。不返回令牌或哈希。 |
portico_credentials | {} | 仅限人类审计者 | 列出登录凭证轨迹(id / 主体 / credentialRef / 签发者 / 时间,作废则含 revokedAt)。与 CLI identity credentials、Portal GET /api/credentials 同一载荷。维护者、只读与匿名返回 FORBIDDEN。不返回令牌或哈希。 |
portico_credential_revokes | {} | 仅限人类审计者 | 列出追加式登录凭证作废轨迹。与 CLI identity credential revokes、Portal GET /api/credential-revokes 同一载荷。维护者、只读与匿名返回 FORBIDDEN。不返回令牌或哈希。读操作不写名册。 |
portico_gateway_audit | {} | 仅限人类审计者 | 列出 Gateway 访问审计。与 CLI gateway audit、Portal GET /api/gateway-audit 同一载荷。维护者、只读与匿名返回 FORBIDDEN。读操作不写目录或审计文件。这不是授权入口,也不执行工具。 |
portico_page | {} | 匿名或持会话 | 读取维护者排布的门户组件盒。与 CLI page get、Portal GET /api/page 同一载荷。匿名看不到内部卡片。读操作不写 page 或目录。 |
调用示例
使用 curl 模拟智能体调用 portico_list:
curl -s -X POST http://127.0.0.1:8790 \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $READER_SESSION" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "portico_list",
"arguments": {
"channel": "mcp"
}
}
}'
返回标准的 MCP 工具响应信封:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"ok\":true,\"data\":[{\"id\":\"github-tools\",\"name\":\"GitHub 运维助手 MCP\",...}]}"
}
]
}
}
Note
MCP 协议服务器仅做治理信息的投影,绝对不提供任何
execute_tool或代发外部调用的代理方法。
MCP Gateway 鉴权网关
Portico 的 MCP Gateway 运行在 8789 端口,它充当 MCP 服务访问的安全准入关卡与不可篡改审计记录器。
门卫角色:不执行、不代理
很多初次接触 Portico 的开发者会误以为 Gateway 是一个反向代理(Proxy)或执行网关。必须再次重申:
Gateway 是门卫,不是执行器。
Gateway 的职责是:核验来访者的会话合法性 -> 校验目标 MCP 服务是否已授权 -> 返回直连端点 -> 向审计日志追加一条访问记录。它绝不替调用方转发工具调用流量。
鉴权与路由工作流
[ 客户端 Agent ] [ MCP Gateway :8789 ] [ 外部 MCP Server ]
│ │ │
│ 1. POST /gateway/mcp/:id/authorize │ │
│ Header: Authorization: Bearer <session> │ │
├──────────────────────────────────────────────────────►│ │
│ │ 2. 验证 session 合法性 │
│ │ 3. 校验该主体是否有权访问该服务 │
│ │ 4. 向 gateway-audit.json 追加流水 │
│ 5. 返回直连路由: │ │
│ { ok: true, data: { endpoint: "https://..." } } │ │
│◄──────────────────────────────────────────────────────┤ │
│ │
│ 6. 客户端使用获取到的 endpoint 直接发起 MCP 调用 ──────────────────────────────────────────►│
API 接口与命令行
1. HTTP 接口调用
curl -s -X POST http://127.0.0.1:8789/gateway/mcp/github-tools/authorize \
-H "Authorization: Bearer $READER_SESSION"
响应:
{
"ok": true,
"data": {
"id": "github-tools",
"authorized": true,
"connect": {
"mode": "direct",
"endpoint": "https://mcp.example.internal/sse"
}
}
}
2. CLI 快捷调用
deno task cli -- gateway authorize \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--audit ./data/gateway-audit.json \
--session $READER_SESSION \
--id github-tools
网关访问审计流水 (gateway-audit.json)
每次成功的准入鉴权都会原子追加至 gateway-audit.json:
{
"id": "gw-evt-101",
"timestamp": "2026-09-14T10:30:00.000Z",
"serviceId": "github-tools",
"actorId": "human:developer-alice",
"action": "authorize",
"granted": true
}
人类审计者可通过 CLI gateway audit、Portal GET /api/gateway-audit 与 MCP portico_gateway_audit 看到同一批访问放行记录。维护者不能读、也不能改写。
Web 门户架构概览
Portico 的 Web 门户(Web Portal)运行于 8788 端口,它是一个专门为人机协同治理定制的现代化、高性能只读 Web 应用。
核心设计哲学
- 绝对只读安全:
- Portal 运行进程完全没有操作系统文件写权限(无
--allow-write)。 - 彻底杜绝了因 Web 层面漏洞(如反序列化、文件上传、任意写)危及底层数据文件的可能性。
- Portal 运行进程完全没有操作系统文件写权限(无
- 零客户端 JavaScript 脚本:
- 整个页面不引入任何客户端 JS 框架(无 React、Vue、Svelte、jQuery 等),不包含
<script>标签与内联事件监听器。 - 所有交互(包括主题切换、抽屉展开、链接跳转)均纯粹依靠 HTML5 与语义 CSS 完成。
- 整个页面不引入任何客户端 JS 框架(无 React、Vue、Svelte、jQuery 等),不包含
- 严苛的 Content Security Policy (CSP):
- 默认响应头为
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'。 - 绝不加载任何外部 CDN 资源、第三方 Web 字体或外部图片,杜绝跟踪探针与供应链投毒。
- 默认响应头为
- 双平面(Dual Plane)架构:
- 物理隔离组织内部协作视界与全网公开发布视界。
路由总览
| 路由地址 | 面向受众 | 权限要求 | 核心功能 |
|---|---|---|---|
/ | 全员/访客 | 匿名或登录 | 杂志社论风格的多渠道发现大厅与 /s/:id 侧栏详情。 |
/internal | 组织内部人员 | 持有会话 (Reader+) | 内部笔记台:展现全部草稿、内部服务、公开候选与详细治理路径。 |
/public | 外部公众 | 完全匿名 | 公开发布目录:仅展示经过独立人类审计批准的 approved_public 服务。 |
/internal/audit | 人类审计者 | 仅限 Auditor | 安全审计台:按时间线聚合目录变更、授权撤回与网关流水。 |
/api/* | 开发者/脚本 | 按接口鉴权 | 供自动化脚本消费的只读 REST API。 |
双平面设计哲学
Portico 的 Web 呈现分为面向人与面向机器两个截然不同的信任平面:内部笔记台(Internal Workbench) 与 公开发布目录(Public Releases)。
为什么需要物理隔离的双平面?
在传统软件设计中,很多门户采用“同一张列表,按权限隐藏部分按钮”的逻辑。实践证明,这种设计极易因为前端渲染漏判或 API 序列化漏洞而泄露敏感元数据。
Portico 在视图路由层实现了绝对的物理双平面隔离:
HTTP Request
│
┌────────────────┴────────────────┐
▼ ▼
/internal/* /public/*
[ 内部笔记台平面 (Internal) ] [ 公开发布平面 (Public) ]
│ │
校验有效会话 (Session)? 无需任何会话 (完全匿名)
├── 否 ──► 直接返回 HTML 404 │
│ (绝不返回登录跳转) │
└── 是 ──► 渲染内部视图 │
- draft 状态草稿 │
- internal 内部服务 │
- pending_public 候选 │
- 真实维护者主体 ID ▼
- 内部端点地址 仅读取 approved_public 服务
- 剔除内部端点
- 剔除维护者敏感信息
- 渲染公开展现卡片
平面特性对比
| 维度 | 内部笔记台 (/internal) | 公开发布目录 (/public) |
|---|---|---|
| 受众主体 | 组织内部员工、内部维护 Agent | 外部合作伙伴、全网匿名公众 |
| 未鉴权表现 | 直接返回 404 Not Found(防止外界探针嗅探内部服务的存在) | 正常渲染公开页面 |
| 呈现状态 | draft、internal、pending_public、rejected、approved_public | 严格仅限 approved_public |
| 信息粒度 | 详细展示版本历程、维护者标识、准入审计时间线、直连调试入口 | 仅展示已脱敏的服务介绍、公开包坐标或公开 URL |
| 默认主题 | 偏向工程研讨风格的纸面/控制台质感(internal-light / internal-dark) | 偏向高对比度、清晰雅致的社论杂志质感(editorial-light / editorial-dark) |
Important
公开页的双重过滤保证:即使请求者持有人类审计者的顶级特权会话,当其访问
/public页面时,服务端视图层依然对其强行执行公开过滤——未经审批的记录绝对不会混杂进公开目录,杜绝了“以管理员视角误把内部服务截屏外流”的安全隐患。
内部工作台 (/internal)
内部笔记台(Internal Workbench)是组织内部工程师与维护 Agent 了解团队资产全貌、跟踪生命周期演进的协作中心。
访问与鉴权
- 基础路由:
http://127.0.0.1:8788/internal - 鉴权方式:通过浏览器 Cookie、HTTP Header
Authorization: Bearer <session>或X-Portico-Session: <session>。 - 匿名保护:无会话请求将直接收到标准 HTTP 404 响应,绝不向未授权探测者泄露系统存在
/internal路径的信息。
页面组成模块
1. 资产大盘看板 (Overview Stats)
顶部呈现四个关键治理维度的汇总卡片:
- 存活服务总数:当前已被纳入治理的所有内部与公开服务。
- 渠道分布数:分别统计 MCP、Web 与 CLI 渠道的占比情况。
- 待审公开队列 (Pending Review):提示当前有多少项申请等待独立人类审计。
- 受控版本数:已治理服务的最新语义版本分布。
2. 多状态泳道与过滤 (/internal/c)
可按照状态标签筛选查看:
- Drafts (草稿):处于本地草稿阶段、仅维护者可见的服务。
- Internal (内部):团队日常依赖的生产/测试服务。
- Pending Public (待审):已发起公开申请但尚未签署审批凭证的服务。
- Rejected (驳回):审核不通过、等待维护者修改重新提交的服务。
3. 详情与治理时间线 (/internal/s/:id)
点击任意内部服务进入详情页,右侧/底部将展开该服务的完整不可篡改时间线:
- 何时由谁首次登记。
- 何时发起过字段更新(展示具体的增改差分)。
- 何时发起过公开申请,审批人是谁,给出的审计意见为何。
4. 专属安全审计台 (/internal/audit)
仅向拥有 auditor 角色的人类展示:
- 聚合显示全量操作流水(登记、更新、注销、凭证作废、网关放行)。
- 支持按服务 ID 或执行者快速回溯历史安全事件。
公开发布目录 (/public)
公开发布目录是面向全网外部访客、合作伙伴与客户的正式橱窗。它仅陈列经过独立人类审计者严格把关、已批准公开发布(approved_public)的高质量 Agent 服务。
访问与表现
- 基础路由:
http://127.0.0.1:8788/public - 准入门槛:完全公开匿名访问,无需任何登录、账号或会话。
- 安全过滤:不论底层数据库中包含多少草稿、内部服务或待审候选,本页面渲染引擎严格只抽取状态为
approved_public的记录。
页面核心特性
1. 渠道快速分栏 (/public/t/:channel)
用户可以一键按渠道过滤公开服务:
/public/t/mcp:只展示提供标准 MCP 协议端点的工具与 Agent。/public/t/web:只展示提供直接访问 Web 控制台与可视化看板的 Agent。/public/t/cli:只展示提供 JSR 或 NPM 终端安装包坐标的开发工具。
2. 公开详情页面 (/public/s/:id)
对于已批准公开的服务,其详情页展示脱敏后的合规信息:
- 服务的官方中英文名称与详细说明。
- 经过校验的对外公开连接方式(公开端点或安装坐标)。
- 语义版本号与发布时间。
Caution
未审批或撤回服务的 404 保障:
若访客尝试在浏览器中直接拼凑内部或待审服务的公开详情 URL(如/public/s/internal-secret-bot),服务端将立刻返回纯 HTML 404 页面,绝不泄露任何该服务存在与否的蛛丝马迹。
发现主页与详情栏 (/ & /s/:id)
Portico 的根路由 / 承担着全角色通用发现门户的职责。它兼具社论杂志(Editorial Magazine)的优雅版式与高信息密度的治理检索能力。
页面版式设计
┌──────────────────────────────────────────────────────────────────────────┐
│ [PORTICO] 治理门户 (主题切换: ?theme=...) [身份标识: Reader] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ [Hero 展区] 聚焦当前重点推荐或最新审核通过的 Agent │
│ │
├────────────────────────────────┬─────────────────────────────────────────┤
│ [服务卡片矩阵] │ [侧栏阅读视窗 /s/:id] │
│ ┌──────────────────────────┐ │ │
│ │ Code Review Helper (v1.1)│ │ 选择左侧卡片后展开: │
│ │ 渠道: [MCP] [CLI] │ │ - 完整功能说明 Markdown │
│ │ 状态: approved_public │ │ - 快速接入代码片段 │
│ └──────────────────────────┘ │ - 维护者与安全审批印章 │
│ ┌──────────────────────────┐ │ │
│ │ SQL Optimizer (v0.2) │ │ │
│ │ 渠道: [CLI] │ │ │
│ │ 状态: internal │ │ │
│ └──────────────────────────┘ │ │
└────────────────────────────────┴─────────────────────────────────────────┘
动态身份适应性 (Role Adaptability)
根路由 / 会根据请求携带的身份动态呈现不同密度的内容:
- 匿名访客访问
/:- 展现经过批准公开的 Agent 卡片。
- 详情栏提供直连入口或安装坐标。
- 已登录读者 (Reader) 访问
/:- 同时展示组织内部可见的服务与公开服务。
- 显示内部服务标识 Badge,提供进入
/internal完整笔记台的快速跳转。
- 人类审计者 (Auditor) 访问
/:- 标红展示当前处于
pending_public的公开申请。 - 提供快速跳转至审批界面的操作链接。
- 标红展示当前处于
颜色主题系统与无脚本规范
Portico 的 Web 界面全面践行现代 CSS 现代化设计,不依赖任何构建期打包工具或第三方 CSS 框架(如 Tailwind、Bootstrap),完全依靠原生 CSS 自定义属性(Variables)与纯语义 Token 实现主题渲染。
四大固定颜色主题预设
根据双平面(内部工作台 vs 公开发布页)与明暗模式的组合,Portico 固化了四个精心调校的主题预设:
| 主题标识符 | 适用平面 | 调性基调 | 典型色彩 |
|---|---|---|---|
internal-light | 内部笔记台 | 沉稳纸质白底、高信息对比度 | 背景 #f6f4ee,文字 #1b2228,边框 #d5d0c3 |
internal-dark | 内部笔记台 | 工程控制台深灰、护眼暗调 | 背景 #11161b,文字 #e1e7ec,强调色 #3bb273 |
editorial-light | 公开发布页 | 社论杂志纸张米白、典雅排版 | 背景 #faf8f5,标题 #121820,品牌蓝 #1f4e5b |
editorial-dark | 公开发布页 | 雅致夜间杂志黑、精致对比 | 背景 #0e1318,柔和白 #f0f4f8,点缀金 #e6af2e |
主题切换机制与优雅降级
用户可通过 URL 查询参数 ?theme=<theme_name> 显式指定期望的视觉主题:
http://127.0.0.1:8788/internal?theme=internal-dark
http://127.0.0.1:8788/public?theme=editorial-dark
解析与降级决策顺序
- URL 参数优先:若
?theme=参数为合法预设,则优先使用。 - 操作系统偏好探测:若未显式传参,浏览器将通过媒体查询
@media (prefers-color-scheme: dark)自动选择该平面的暗色或浅色版本。 - 非法与跨平面降级:
- 若在
/internal传入了不存在的主题(如?theme=unknown),系统静默回退为internal-light。 - 若在
/public强行传入内部主题(如?theme=internal-dark),系统将其优雅降级为公开发布页对应的editorial-dark,杜绝因非法参数造成 500 异常。
- 若在
零 JavaScript 规范 (Zero-JS Policy)
Portico 的前端页面具备严格的防御性安全边界:
- 严格禁止引入
<script>标签与内联 JS 事件代码(如onclick="...")。 - 抽屉展示、模态浮层、分类标签高亮均采用纯 CSS(如 CSS
:checked、:target或详情标签<details>/<summary>)实现。 - 绝不调用任何外部字体(如 Google Fonts)或外部 CDN 样式表,所有排版字体优先采用操作系统无衬线系统字体栈(System Font Stack),确保在无外网连接的物理隔离内网中秒级秒开。
受约束的组件盒 (UI Components)
为了满足自定义门户页面的诉求,同时杜绝引入任意 HTML 或富文本编辑器带来的 XSS 与视觉混乱风险,Portico 提供了受严格约束的门户组件盒(Constrained Component Box)。
门户不是 CMS
核心原则:组件盒保持小而硬。
Portico 不是 WordPress,不是通用低代码建站器,也不提供任意 HTML 注入能力。维护者只能通过 JSON 格式组合系统预定义的受约束组件。
预定义组件类型一览
系统仅允许在页面配置(page.json)中使用以下 5 种组件:
| 组件标识符 | 功能说明 | 渲染安全限制 |
|---|---|---|
catalog_card | 服务简要卡片,展示标题、描述片段、渠道 Badge 与状态标识。 | 仅展示已授权字段,自动转义所有文本。 |
catalog_detail | 服务全量详情面板,展示完整的 Markdown 描述与连接方式。 | 严格过滤 HTML 标签与伪协议链接。 |
permission_hint | 权限状态提示横条,告知访客当前是以匿名还是以某已登录角色浏览。 | 纯静态文本占位。 |
approval_status | 公开审批状态卡片,呈现独立人类审计者的签字印章与审计时间戳。 | 仅对已签署的记录渲染。 |
audit_snippet | 简要审计流水切片,展示该服务最近 3 次治理变更记录。 | 仅对拥有审计权限的人员可见。 |
页面配置与维护 (page set / get)
维护者可通过 CLI 设置门户的页面组件排布:
# 准备 page.json
cat << 'EOF' > page.json
{
"title": "研发基础设施 Agent 导航台",
"blocks": [
{
"type": "permission_hint"
},
{
"type": "catalog_card",
"targetId": "github-tools"
},
{
"type": "catalog_card",
"targetId": "code-review-cli"
}
]
}
EOF
# 保存页面排布
deno task cli -- page set \
--page ./data/page.json \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $MAINTAINER_SESSION \
--input ./page.json
安全保证:
- 系统在保存前会逐一校验引用的
targetId是否真实存在。 - 如果页面配置中引用了一个尚未审批通过公开的内部服务,匿名访客浏览页面时,系统会自动将该组件剔除,绝不发生通过页面排布泄露未审批服务信息的安全事故。
Portal REST API 接口规范
Portal 进程不仅提供 HTML 视图,同时对外暴露了一套规范的纯只读 REST API,供自动化脚本、监控工具或其他微服务集成。
接口设计原则
- 统一 HTTP 方法:除特殊握手外,Portal 的业务接口严格只接受
GET请求。任何试图向 Portal 发送POST、PUT、PATCH或DELETE的请求均直接返回405 Method Not Allowed。 - 信封结构:成功返回
{ "ok": true, "data": ... },失败返回{ "ok": false, "error": { "code": "...", "message": "..." } }。 - 只读免责:Portal 运行进程不具备操作系统写权限,任何接口调用均不修改磁盘数据。
核心接口列表
1. 目录列表接口 (GET /api/catalog)
获取当前鉴权上下文可见的所有服务表面列表。
curl -s -H "Authorization: Bearer $READER_SESSION" http://127.0.0.1:8788/api/catalog
响应示例:
{
"ok": true,
"data": [
{
"id": "github-tools",
"name": "GitHub 运维助手 MCP",
"version": "1.2.0",
"channels": ["mcp"],
"governanceState": "internal",
"visibility": "internal"
}
]
}
2. 渠道专用接口 (GET /api/mcp、GET /api/web、GET /api/cli)
根据请求的渠道类型快速筛选并返回直连连接或包坐标:
# 获取所有可见的 Web 渠道服务及直连 URL
curl -s -H "Authorization: Bearer $READER_SESSION" http://127.0.0.1:8788/api/web
# 获取所有可见的 CLI 渠道服务及 JSR/NPM 包坐标
curl -s -H "Authorization: Bearer $READER_SESSION" http://127.0.0.1:8788/api/cli
3. 自定义门户布局接口 (GET /api/page)
获取维护者排布的自定义门户卡片与组件列表。与 CLI page get、MCP portico_page 同一载荷:匿名看不到内部卡片,读操作不写 page 文件。
curl -s http://127.0.0.1:8788/api/page
4. 安全审计视图接口 (GET /api/audit)
供人类安全审计者获取合并后的系统审计时间线流水。
curl -s -H "Authorization: Bearer $HUMAN_AUDITOR_SESSION" http://127.0.0.1:8788/api/audit
Note
仅限具备
auditor角色的人类会话可成功调用。维护者、普通只读者或匿名调用将收到403 Forbidden。
4a. Gateway 访问审计 (GET /api/gateway-audit)
列出 Gateway 允许与拒绝的直连授权记录。与 CLI gateway audit、MCP portico_gateway_audit 同一载荷。
curl -s -H "Authorization: ******" http://127.0.0.1:8788/api/gateway-audit
Note
仅人类审计者可读。维护者、只读者与匿名得到
403 Forbidden。这不是授权写入入口;POST返回405。读操作不改目录或审计文件。
5. 授权轨迹接口 (GET /api/grants)
列出追加式身份授权记录。与 CLI identity grants、MCP portico_grants 同一载荷。
curl -s -H "Authorization: ******" http://127.0.0.1:8788/api/grants
Note
仅人类审计者可读。维护者、只读者与匿名得到
403 Forbidden。这不是授权写入入口;POST返回405。
5a. 身份撤回轨迹接口 (GET /api/revokes)
列出追加式身份撤回记录。与 CLI identity revokes、MCP portico_revokes 同一载荷。
curl -s -H "Authorization: Bearer <session>" http://127.0.0.1:8788/api/revokes
Note
仅人类审计者可读。维护者、只读者与匿名得到
403 Forbidden。这不是撤回写入入口;POST返回405。载荷不含凭证或会话令牌。
6. 当前身份接口 (GET /api/whoami)
返回当前已证明身份的 id / kind / role。与 CLI identity whoami、MCP portico_whoami 同一载荷。
curl -s -H "Authorization: ******" http://127.0.0.1:8788/api/whoami
Note
已登录会话(含 Portal 上已校验的 Cloudflare Access 映射)看到自己。匿名得到
403 Forbidden。这不是登录入口;POST返回405。载荷不含邮箱、凭证或会话令牌。
7. 登录会话轨迹 (GET /api/sessions)
列出登录会话轨迹(id / 主体 / 创建与过期时间,作废则含 revokedAt)。与 CLI identity sessions、MCP portico_sessions 同一载荷。
curl -s -H "Authorization: Bearer <session>" http://127.0.0.1:8788/api/sessions
Note
仅人类审计者可读。维护者、只读者与匿名得到
403 Forbidden。这不是登录或作废入口;POST返回405。载荷不含令牌或哈希。
8. 登录凭证轨迹 (GET /api/credentials)
列出登录凭证轨迹(id / 主体 / credentialRef / 签发者 / 签发时间,作废则含 revokedAt)。与 CLI identity credentials、MCP portico_credentials 同一载荷。
curl -s -H "Authorization: Bearer pst1_..." http://127.0.0.1:8788/api/credentials
Note
仅人类审计者可读。维护者、只读者与匿名得到
403 Forbidden。这不是签发或作废入口;POST返回405。载荷不含令牌或哈希。
8a. 登录凭证作废轨迹 (GET /api/credential-revokes)
列出追加式登录凭证作废记录。与 CLI identity credential revokes、MCP portico_credential_revokes 同一载荷。
curl -s -H "Authorization: Bearer <session>" http://127.0.0.1:8788/api/credential-revokes
Note
仅人类审计者可读。维护者、只读者与匿名得到
403 Forbidden。这不是作废写入入口;POST返回405。载荷不含凭证或会话令牌。
安全治理与审计概览
Portico 的核心价值在于建立对 AI 智能体体系的可信治理与审计能力。
核心安全支柱
- 不可抵赖性 (Non-Repudiation): 每一次状态迁移、配置更新、权限授予、凭证签发与注销,均要求出示经过签发核验的 Session,并在底层日志中记录明确的时间戳与发起主体。
- 职责分离 (Separation of Duties): 日常业务编目操作权交由 Agent 和普通开发维护者;跨越公开信任边界的审批、撤回与安全作废特权严格收敛于独立人类审计者。
- 安全默认 (Secure by Default): 未鉴权请求默认为最低的匿名权限;未显式批准的服务默认仅限内网;未白名单显式允许的系统资源默认被 Deno 运行时拦截。
- 透明可追溯 (Traceability): 所有审计数据采用追加写流水设计,提供 CLI、Portal 与 MCP 三端完全一致的审计事件聚合视图。
追加写审计日志体系
Portico 拒绝可被任意篡改或静默清空的传统日志。系统的核心治理行为均转化为结构化追加写审计事件(Append-Only Audit Trail)。
审计事件源的四大支柱
系统的全局审计时间线(Timeline)由四个分散存储的底层数据流归并排序生成:
┌───────────────────────────┐ ┌───────────────────────────┐
│ catalog.json (changes) │ │ approvals.json │
│ - 服务创建 (register) │ │ - 公开发布批准 (approved) │
│ - 服务更新 (update) │ │ - 公开发布驳回 (rejected) │
│ - 状态流转 (publish) │ │ - 公开服务撤回 (withdrawn)│
└─────────────┬─────────────┘ └─────────────┬─────────────┘
│ │
└────────────────┬─────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 聚合审计时间线 (Audit Timeline) │
│ - 按时间戳降序全局排序 │
│ - 抹平不同存储源字段差异 │
└──────────────────┬──────────────────────┘
▲
┌────────────────┴─────────────────┐
│ │
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐
│ identities.json │ │ gateway-audit.json │
│ - 身份授予 (grant) │ │ - MCP Gateway 准入授权 │
│ - 身份注销 (revoke) │ │ (authorize) 流水 │
│ - 凭证作废 (credential) │ │ │
└───────────────────────────┘ └───────────────────────────┘
审计事件结构规范
所有审计事件最终投影为统一的信封结构:
interface AuditEvent {
/** 全局唯一事件 ID */
id: string;
/** 事件类型:catalog_register | catalog_update | approval | revoke | gateway_authorize 等 */
type: string;
/** 事件发生的时间戳 (ISO 8601) */
timestamp: string;
/** 触发此事件的主体 ID,例如 "human:auditor-bob" */
actorId: string;
/** 关联的服务 ID 或主体 ID */
targetId: string;
/** 详细审计载荷(含字段变更差分、版本号、裁决结果等) */
details: Record<string, unknown>;
}
查询与检验方式
人类审计者可通过 CLI 查询聚合后的审计流水:
deno task cli -- audit list \
--catalog ./data/catalog.json \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session $HUMAN_AUDITOR_SESSION \
--limit 20
不可篡改保证:
- 维护者(Maintainer)即使拥有写 Catalog 的权限,也无法调用任何“清空审计记录”或“修改审计时间戳”的命令。
- 试图通过 CLI、API 或 MCP 篡改审计日志的请求将被直接判定为非法语义而拒绝。
人类安全审计检查清单
在 Portico 的治理闭环中,人类审计者是唯一的最高安全把关人。自动化 Agent 负责高效填报和更新,人类负责在公开信任边界前审视潜在威胁。
审批公开时的五步检查清单
当一个 Agent 处于 pending_public 状态、等待批准公开发布时,人类审计者必须逐一核验以下 5 项指标:
1. 入口端点是否包含内网资产或明文凭证?
- 检查内容:审查
entry.mcp_endpoint或entry.url。 - 红线:若端点包含内网私有 IP(如
10.x.x.x、192.168.x.x、127.0.0.1)但被申请公开发布,或者 URL 中携带?token=...、api_key=...,必须果断驳回(reject)!
2. 功能描述是否存在误导或越界承诺?
- 检查内容:查阅
description中的能力描述。 - 红线:是否声明了未受约束的自主转账、删库、执行任意命令等高危动作?是否含有虚假宣称?
3. CLI 包坐标是否属于官方认证命名空间?
- 检查内容:审查
entry.package。 - 红线:若是 JSR/NPM 包,检查 Scope(如
@company/)是否经过组织官方认证,防范依赖混淆与仿冒包(Typosquatting)。
4. 提交人与维护者身份是否合规?
- 检查内容:核实
maintainer字段对应的身份。 - 红线:若该 Agent 由一个不可信的外部脚本或已被撤销的主体提交,直接驳回并介入排查。
5. 变更历史是否有频繁改动或异常抖动?
- 检查内容:通过审计时间线查阅该服务在提交公开前的
catalog_update历史。 - 红线:防范“先提交合规版本,在被审计前夕快速偷换成恶意外链”的行为。
异常处置响应指引
- 立即撤回公开:一旦发现已上线的公开服务出现安全漏洞,立刻执行
catalog withdraw --id <id>,瞬间切断外网流量。 - 紧急凭证作废:若维护者 Token 遭泄露,立刻执行
identity credential revoke --id <id>作废所有活跃会话。 - 主体永久注销:确认主体恶意越界时,执行
identity revoke --id <id>将其彻底移出组织名册。
防自批与会话防伪机制
在组织治理中,“既当运动员又当裁判员”是导致严重安全责任事故的温床。Portico 在代码内核深度固化了防自批(Anti-Self-Approval)与会话防伪机制。
防自批机制的深度实现
Portico 杜绝自审自批的防御逻辑包含多层交叉校验:
// 伪代码展示 src/catalog/approval.ts 中的强校验逻辑
export function assertCanApprove(
record: CatalogRecord,
actor: SessionIdentity,
): void {
// 1. 角色必须包含 auditor
if (!actor.roles.includes("auditor")) {
throw new PorticoError("FORBIDDEN", "只有审计者角色有权批准公开");
}
// 2. 主体必须为人类
if (actor.kind !== "human") {
throw new PorticoError("FORBIDDEN", "智能体主体严禁执行公开审批");
}
// 3. 核心防自批:当前审批者不得为服务维护者
if (record.maintainer === actor.id) {
throw new PorticoError(
"SELF_APPROVAL",
`禁止自我批准:主体 ${actor.id} 是该服务的登记维护者,必须由其他独立审计者审批`
);
}
}
为什么即使是人类审计者也不能自批?
假设某人类工程师张三同时拥有某代码助手服务的维护权与公司的 Auditor 角色:
- 当张三自己提交了一个服务公开申请时,他依然不能批准自己提交的这一条申请。
- 必须由团队内的另一位人类审计者李四进行交叉复审。
- 该设计彻底消除了因个人疏忽或内部账号被盗导致的单点安全沦陷。
会话防伪与不可逆哈希
为了防止客户端通过篡改网络包伪造身份:
- 彻底废除自称请求头:服务端不接受任何形式的
X-Portico-Actor-Id或X-Portico-Actor-Role。所有关于角色与身份的判定完全以服务端解出的有效 Session 为唯一事实来源。 - 凭证单向哈希比对:
- 客户端持有的凭证为明文令牌(如
pct1_...)。 - 服务端读取令牌后执行
crypto.subtle.digest("SHA-256", tokenBytes),仅与库中存储的十六进制哈希比对。 - 即使日志文件或中间临时文件外泄,攻击者也绝无法逆向推导出原始凭证以获取未授权会话。
- 客户端持有的凭证为明文令牌(如
零明文密钥引用原则
在 Agent 生态中,硬编码密钥(如 OpenAI API Key、GitHub Personal Access Token、AWS AccessKey)是极易发生外泄的高危隐患。
Portico 实行彻底的零明文密钥(Zero-Plaintext-Secrets)原则。
密钥治理铁律
- 绝对不存明文:
- 登记的 Catalog 元数据、自定义页面组件、审计流水中,严禁写入任何明文密码或 API Token。
- 提交的数据如果包含疑似密钥字段(如检测到以
sk-开头或带有明文密码属性),写入校验逻辑将直接抛出错误并拒绝落盘。
- 只引用,不托管:
- Agent 与服务若需要认证凭证,应在运行时通过外部专业的密钥管理系统(如 HashiCorp Vault、AWS Secrets Manager 或本地环境变量)自行注入。
- Portico 只负责登记服务的访问入口,绝不代存、代传任何外部服务的调用密钥。
- 日志与错误脱敏:
- CLI 与 Portal 在报错输出中严禁将可能包含敏感信息的环境变量内容或完整堆栈直接暴露给终端。
运维管理概览
Portico 的运维设计秉承极简可靠、权限显式、无隐式依赖的工程准则。
运维特性矩阵
- 零外部中间件依赖:不依赖 Redis、MySQL、PostgreSQL 或消息队列;纯基于原子文件持久化,拷走数据目录即可完成迁移或备份。
- 开箱即用 Supervisor:一条命令同时拉起 Portal、Gateway、MCP 三大子进程,自带级联退出与健康监听。
- 免环境编译分发:支持通过
deno task build编译为原生独立免依赖可执行文件,部署节点甚至无需预装 Deno。 - 全方位环境变量覆盖:通过标准的环境变量控制数据路径、监听端口与内网绑定网段。
- 系统级服务模版:提供开箱即用的 Linux Systemd 服务配置范例。
Supervisor 进程管理 (up)
为了让开发者与运维人员在单机部署或本地开发时免除“开三个窗口分别起动三个进程”的繁琐步骤,Portico 提供了内置的监管脚本 src/up/main.ts(通过 deno task up 触发)。
协同退出与进程编排机制
up 是一个轻量的进程监督器,而不是将三个服务代码合并在同一个进程内执行:
- 真实多进程隔离:
up会通过操作系统Deno.Command分别派生出三个完全独立的子进程:portal子进程(严格无--allow-write)gateway子进程(仅对审计文件开放--allow-write)mcp子进程(严格无--allow-write)
- 信号级联联动(Cascading Shutdown):
- 监听宿主的
SIGINT(Ctrl+C)与SIGTERM信号,收到后向三个子进程分发终止信号,优雅回收端口。 - 监听任意子进程的崩溃事件:一旦其中某一个服务由于未捕获异常退出,
up会立即强行收走其余存活的子进程,绝不留下一个 Gateway 还活着但 Portal 已经挂掉的半死系统!
- 监听宿主的
使用与机读输出
PORTICO_DATA_DIR=./data deno task up
起动成功后,标准输出将输出唯一一行标准机读 JSON:
{"ok":true,"data":{"dataDir":"./data","portal":{"url":"http://127.0.0.1:8788"},"gateway":{"url":"http://127.0.0.1:8789"},"mcp":{"url":"http://127.0.0.1:8790"}}}
自动化部署脚本或外部进程管理器可直接解析该行 JSON 获取各入口的实际服务地址。
独立二进制编译 (build)
借助 Deno 内置的静态编译引擎,Portico 可以打包为完全独立的原生免依赖二进制文件。在目标生产服务器上,您甚至无需安装 Deno、Node.js 或任何解释器即可直接执行。
构建命令
执行全局构建:
deno task build
构建脚本(src/build/main.ts)将在根目录下的 dist/ 文件夹内编译生成 4 个独立产物:
dist/portico:Portico CLI 客户端工具。dist/portico-portal:Web Portal 服务端。dist/portico-gateway:MCP Gateway 网关服务。dist/portico-mcp:MCP Protocol Server 服务。
也可以指定单个产物进行按需构建:
deno task build cli
deno task build portal
权限内嵌特性 (Embedded Permissions)
这是 Deno 编译产物非常强大的安全特性:
- 在调用
deno compile时,构建脚本已经将每个二进制所需的最小权限白名单硬编码嵌入了二进制头部。 - 例如:
portico-portal二进制在编译时未赋予--allow-write,那么无论运维人员在宿主机上以何种用户权限运行该二进制,该进程物理上都绝对无法执行写文件操作,从而实现了操作系统级别的强沙箱固化。
环境变量配置参考
Portico 支持通过环境变量对各进程的监听地址、端口与数据文件路径进行精细化配置。
核心环境变量清单
| 环境变量名 | 适用服务 | 默认值 | 详细说明 |
|---|---|---|---|
PORTICO_DATA_DIR | 全局/up | ./data | 数据总根目录。各服务的文件路径未单独指定时,从此目录派生。 |
PORTICO_BIND | 全局 | 127.0.0.1 | 网络监听绑定地址。接受 127.0.0.1、localhost 或 RFC1918 私网 IP(如 10.x.x.x、192.168.x.x)。拒绝绑定 0.0.0.0 或公网 IP。 |
PORTICO_PORT | Portal | 8788 | Portal 服务的 HTTP 监听端口。 |
PORTICO_GATEWAY_PORT | Gateway | 8789 | MCP Gateway 网关服务的 HTTP 监听端口。 |
PORTICO_MCP_PORT | MCP | 8790 | MCP Protocol Server 的 HTTP 监听端口。 |
PORTICO_CATALOG_PATH | Portal/GW/MCP | ${DATA_DIR}/catalog.json | 目录服务数据文件的绝对或相对路径。 |
PORTICO_IDENTITIES_PATH | 全局 | ${DATA_DIR}/identities.json | 身份名册数据文件路径。 |
PORTICO_SESSIONS_PATH | 全局 | ${DATA_DIR}/sessions.json | 凭证与会话数据文件路径。 |
PORTICO_GATEWAY_AUDIT_PATH | GW/Portal/MCP | ${DATA_DIR}/gateway-audit.json | 网关访问流水文件路径。若给 Portal/MCP 配置,则其审计视图会并入网关流水。 |
PORTICO_PAGE_PATH | Portal/CLI/MCP | ${DATA_DIR}/page.json | 自定义门户组件盒布局配置文件路径(可选)。Portal GET /api/page 与 MCP portico_page 读同一文件;缺省则组合结果为空。 |
PORTICO_CF_ACCESS_ENABLED | Portal | 关闭 | 是否启用 Cloudflare Access JWT 映射。未设为 true/yes/on/1 时忽略 JWT,现有会话路径不变。 |
PORTICO_CF_ACCESS_TEAM | Portal | (无) | Cloudflare Access team 名,只允许 [a-z0-9-]。用于拼 ISS 与默认 JWKS URL。缺省则功能保持关闭。 |
PORTICO_CF_ACCESS_AUD | Portal | (无) | Access Application audience。缺省则功能保持关闭。 |
PORTICO_CF_ACCESS_JWKS_URL | Portal | team 默认证书 URL | 可选覆盖。只允许 http://127.0.0.1/...(测试)或该 team 的 https://<team>.cloudflareaccess.com/cdn-cgi/access/certs。其它 URL 会使功能保持关闭。操作说明见 Cloudflare Access JWT 映射。 |
内网监听绑定安全提示
Caution
出于严谨的最小暴露原则,Portico 默认仅绑定本地回环接口
127.0.0.1。
若需要在内网集群中使用,请显式指定具体的私有网卡 IPv4 地址(例如PORTICO_BIND=192.168.1.100)。系统会主动拒绝0.0.0.0,以防止运维人员无意间将未加密的 HTTP 端口直接暴露在公网路由器上。
Systemd 生产服务编排
在 Linux 生产环境中,推荐使用 Systemd 托管独立编译的二进制服务。
仓库的 deploy/ 目录下提供了完整的开箱即用脚本与服务配置模板。
服务架构切分方案
生产环境推荐将 Portal、Gateway、MCP 作为三个独立的 Systemd 服务运行:
/etc/systemd/system/
├── portico-portal.service
├── portico-gateway.service
└── portico-mcp.service
Systemd 单元配置示例 (portico-mcp.service)
参考 deploy/portico-mcp.service:
[Unit]
Description=Portico MCP Protocol Server
After=network.target
[Service]
Type=simple
User=portico
Group=portico
WorkingDirectory=/opt/portico
Environment=PORTICO_DATA_DIR=/var/lib/portico
Environment=PORTICO_BIND=127.0.0.1
Environment=PORTICO_MCP_PORT=8790
ExecStart=/opt/portico/bin/portico-mcp
Restart=always
RestartSec=5s
# 强化 Linux 命名空间与沙箱隔离
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/var/lib/portico/catalog.json /var/lib/portico/identities.json /var/lib/portico/sessions.json
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
生产配置关键点
- 专用非特权用户:使用无特权的
portico系统账号运行。 - 只读挂载:在 Systemd 层面使用
ReadOnlyPaths为 Portal 和 MCP 进程锁死底层数据文件的读取权限,即便在 Linux 内核级别也禁止其改写数据。
Deno L0 运行时与权限白名单
Portico 将系统运行时刚性锁定在 Deno + TypeScript(L0),这是整个项目的安全架构基石。
为什么选择 Deno L0?
传统 Node.js 运行时的最大软肋在于:一旦引入第三方 npm 依赖包,任何包均默认拥有全量的磁盘读写、子进程拉起和网络发起权限。
在治理多 Agent 的系统中,恶意依赖或投毒包极易直接读取系统私钥或修改底层数据。Deno 提供了与操作系统契合的原生默认拒绝权限沙箱:
- 默认无权:任何未经命令行显式授权的
Deno.readTextFile、fetch或Deno.Command都会直接崩溃抛出安全异常。 - 细粒度白名单:网络连接精确限定到特定的 IP(
--allow-net=127.0.0.1),文件读写精确限定到特定路径。 - 消除第二运行时:禁止把 Node.js 或 Bun 纳入生产依赖或发布产物,消除维护两套包管理器和锁文件的脆弱性。
各组件权限集定义清单
系统各进程的权限声明集中维护在 src/perms.ts,确保 up supervisor、build 编译器与自动化测试执行同一份事实基准:
| 运行产物 | 读权限 (--allow-read) | 写权限 (--allow-write) | 环境变量 (--allow-env) | 网络权限 (--allow-net) |
|---|---|---|---|---|
| Portico CLI | ✅ 允许读取配置文件 | ✅ 允许原子写入数据文件 | ✅ 允许读取 PORTICO_* 变量 | ❌ 严禁发起网络请求 |
| Portal 服务 | ✅ 允许读取静态与数据文件 | ❌ 严格禁止写操作 | ✅ 允许读取 PORTICO_* 变量 | ✅ 允许绑定本地回环端口 |
| MCP 网关 | ✅ 允许读取数据文件 | ✅ 仅限写入 gateway-audit.json | ✅ 允许读取 PORTICO_* 变量 | ✅ 允许绑定本地回环端口 |
| MCP 服务端 | ✅ 允许读取数据文件 | ❌ 严格禁止写操作 | ✅ 允许读取 PORTICO_* 变量 | ✅ 允许绑定本地回环端口 |
Caution
绝对禁止使用
--allow-all:在 Portico 的 CI、测试用例或运行脚本中,任何试图使用-A或--allow-all的修改均会被 CI 静态检查或代码审计直接驳回。
参考手册与规范索引
本部分收录了 Portico 的详细技术规范、完整的命令行参数手册以及质量验收矩阵。
章节索引
- CLI 命令行工具完整参考:提供所有子命令(
identity、catalog、mcp、web、cli、gateway、audit、page)的完整参数与错误码字典。 - Agent 协作工作规范:解读
AGENTS.md,明确维护者 Agent 的权限红线与行为准则。 - 验收矩阵与质量底线:梳理 L0 业务能力覆盖矩阵,阐述 Happy Path E2E、失败路径、双角色测试与写恢复底线。
- 附录 A:项目画像与路线图:保留原有的核心架构画像与演进规划(完整 45KB 文档)。
- 附录 B:UI 设计规范全貌:保留原有的双平面 UI 样式系统规范与 CSS Token 约定。
CLI 命令行工具完整参考
Portico CLI (src/cli/main.ts) 是系统运维与 Agent 自动化的核心工具。
全局通用规范
- 执行方式:通过
deno task cli -- <subcommand> [args]或独立二进制dist/portico <subcommand> [args]。 - 输出格式:标准输出(stdout)严格输出单行机读 JSON:
- 成功:
{"ok": true, "data": ...} - 失败:
{"ok": false, "error": {"code": "...", "message": "..."}}
- 成功:
- 退出状态码:成功退出码为
0;发生业务拦截或参数错误退出码为1。
错误代码字典 (Error Codes)
| 错误代码 (Error Code) | 触发场景说明 |
|---|---|
USAGE | 命令行参数缺失、参数格式错误或单独传入了伪造的 --actor-* 参数。 |
FORBIDDEN | 会话主体无权执行该操作(例如 Reader 尝试登记服务、维护者尝试审批公开)。 |
UNAUTHENTICATED | 会话令牌无效、已过期或在 sessions.json 中不存在。 |
NOT_FOUND | 指定的服务 ID 或主体 ID 在数据存储中不存在。 |
INVALID_STATE | 当前服务的生命周期状态不满足该操作的前提条件(例如对非公开服务执行撤回)。 |
INVALID_INPUT | 传入的 JSON 数据违反 Schema 约束(如缺少必填字段、出现未知字段)。 |
SELF_APPROVAL | 审计者尝试审批由自己登记或维护的服务公开申请。 |
PUBLIC_REQUIRES_APPROVAL | 试图在 catalog register 中绕过审批直接声明公开。 |
子命令手册
1. identity 身份与会话管理
# 授予主体角色
portico identity grant --identities <path> [--sessions <path> --session <token>] --id <id> --kind <human|agent> --role <auditor|maintainer|reader> [--email <address>]
# 注销主体
portico identity revoke --identities <path> --sessions <path> --session <token> --id <id>
# 签发一次性登录凭证
portico identity credential issue --identities <path> --sessions <path> [--session <token>] --id <id>
# 作废凭证与活跃会话
portico identity credential revoke --identities <path> --sessions <path> --session <token> --id <id>
# 凭证登录换取 Session
portico identity login --identities <path> --sessions <path> --id <id> --token <pct1_...>
# 退出登录作废当前 Session
portico identity logout --sessions <path> --session <pst1_...>
# 查看当前会话所属身份与角色
portico identity whoami --identities <path> --sessions <path> --session <pst1_...>
# 列出追加式授权轨迹(仅人类审计者)
portico identity grants --identities <path> --sessions <path> --session <token>
# 列出追加式身份撤回轨迹(仅人类审计者)
portico identity revokes --identities <path> --sessions <path> --session <token>
# 列出登录会话轨迹(仅人类审计者;不含令牌或哈希)
portico identity sessions --identities <path> --sessions <path> --session <token>
# 列出登录凭证轨迹(仅人类审计者;不含令牌或哈希)
portico identity credentials --identities <path> --sessions <path> --session <token>
# 列出追加式登录凭证作废轨迹(仅人类审计者;不含令牌或哈希)
portico identity credential revokes --identities <path> --sessions <path> --session <token>
2. catalog 目录与发布管理
# 内部登记新服务表面
portico catalog register --catalog <path> --identities <path> --sessions <path> --session <token> --input <record.json>
# 登记本地草稿
portico catalog draft --catalog <path> --identities <path> --sessions <path> --session <token> --input <record.json>
# 正式发布 (内部或公开候选)
portico catalog publish --catalog <path> --identities <path> --sessions <path> --session <token> --id <id> --visibility <internal|public>
# 更新受治理表面字段 (仅限 draft/internal/rejected)
portico catalog update --catalog <path> --identities <path> --sessions <path> --session <token> --id <id> --input <record.json>
# 审批公开申请 (仅限独立人类审计者)
portico catalog approve --catalog <path> --identities <path> --sessions <path> --session <token> --id <id>
# 驳回公开申请 (仅限独立人类审计者)
portico catalog reject --catalog <path> --identities <path> --sessions <path> --session <token> --id <id>
# 撤回已公开服务至内部 (仅限人类审计者)
portico catalog withdraw --catalog <path> --identities <path> --sessions <path> --session <token> --id <id>
# 列出公开边界审批记录(通过 / 拒绝 / 撤回);匿名为空列表
portico catalog approvals --catalog <path> --identities <path> --sessions <path> --session <token>
# 列表查询可见服务
portico catalog list --catalog <path> [--identities <path> --sessions <path> --session <token>]
# 查询服务详情
portico catalog get --catalog <path> [--identities <path> --sessions <path> --session <token>] --id <id>
3. 渠道与网关命令
# 查询 MCP 渠道服务及连接信息
portico mcp list --catalog <path> --identities <path> --sessions <path> --session <token>
portico mcp describe --catalog <path> --identities <path> --sessions <path> --session <token> --id <id>
# 查询 Web 渠道服务及直连链接
portico web list --catalog <path> --identities <path> --sessions <path> --session <token>
portico web describe --catalog <path> --identities <path> --sessions <path> --session <token> --id <id>
# 查询 CLI 渠道服务及包坐标
portico cli list --catalog <path> --identities <path> --sessions <path> --session <token>
portico cli describe --catalog <path> --identities <path> --sessions <path> --session <token> --id <id>
# 网关准入鉴权与路由获取
portico gateway authorize --catalog <path> --identities <path> --sessions <path> --audit <audit.json> --session <token> --id <id>
# 查询网关访问审计流水
portico gateway audit --identities <path> --sessions <path> --audit <audit.json> --session <token>
4. 审计与门户组件盒命令
# 查询系统全局聚合审计时间线
portico audit list --catalog <path> --identities <path> --sessions <path> --session <token> [--limit <n>]
# 设置自定义门户卡片配置
portico page set --page <path> --catalog <path> --identities <path> --sessions <path> --session <token> --input <page.json>
# 获取当前门户排布配置
portico page get --page <path> --catalog <path> --identities <path> --sessions <path> --session <token>
Agent 协作工作规范
本文档为委派维护 Portico 仓库或与其交互的 AI 智能体设定了明确的行为守则。
完整约束条文以仓库根目录下的
AGENTS.md为唯一准则。
智能体的角色定位
你是被委派维护本系统的自动化 Agent。你的核心职责是:
- 日常维护:实现与修复业务逻辑、维护目录模型、补全单元与集成测试、完善技术文档。
- 辅助编目:编写与提交内部 Agent 服务描述,协助开发者进行草稿打磨。
智能体绝对禁止的行为 (Hard Prohibitions)
- 绝对不可把 Portico 变成 Runtime:
- 严禁为“未来可能需要直接运行 Agent”预留执行抽象。
- 严禁引入 LangChain、LlamaIndex 或任何试图在 Portico 进程内执行 Prompt、模型推理的外部库。
- 绝对不可让未审批对象公开可达:
- 严禁在代码中编写绕过审批直接将服务修改为
approved_public的特殊逻辑。 - 严禁在公开路由(
/public)中泄露未公开服务的入口与标识。
- 严禁在代码中编写绕过审批直接将服务修改为
- 绝对不可利用维护者特权自审自批:
- 严禁向名册中的 Agent 授予
auditor角色。 - 严禁绕过
SELF_APPROVAL校验逻辑。
- 严禁向名册中的 Agent 授予
- 绝对不可删除或改写审计结论:
- 严禁提供任何清理或编辑历史审计日志的 API 或 CLI 指令。
- 绝对不可在仓库或日志中写入明文密钥:
- 测试用例中的 Mock 数据严禁使用生产真实 Key。
- 绝对不可引入 Node.js 或 Bun 依赖:
- 不要因为“某个 npm 工具更熟悉”而引入 Node 脚本或在 CI 中添加第二运行时。必须在纯 Deno 环境中解决问题。
验收矩阵与质量底线
Portico 对所有已发布的一级业务功能执行铁律级的质量保障,详细业务能力矩阵维护于 docs/roadmap.md 中。
质量覆盖四大铁律 (MUST)
- Happy Path E2E:每个一级功能必须至少有一条端到端(E2E)成功路径测试。
- 高风险失败路径覆盖:每个高风险功能必须至少覆盖一条失败路径(例如非法入参、偷写公开、伪造身份被拦截)。
- 权限双角色覆盖:每个涉及权限判定的功能,必须在用例中至少验证两种截然不同的角色(例如:有权 vs 无权,或维护者 vs 人类审计者)。
- 失败状态零脏写:每个修改底层状态的操作,必须验证在发生失败或异常中断后,底层文件状态无脏写且能够恢复或回滚。
核心业务能力证据清单
| 一级功能模块 | 风险级别 | 覆盖范围重点 | 核心测试证据文件 |
|---|---|---|---|
| Registry 登记与目录 | 高 | 维护者登记内部表面,只读者读取;拦截非法偷写公开、明文密钥,以及指向自身阅读页的 Web 入口。 | tests/catalog_service_test.ts、tests/web_channel_test.ts、tests/e2e/cli_catalog_e2e_test.ts |
| 受治理表面更新 | 高 | 维护者更新 draft/internal/rejected;禁止直接改 pending_public/approved_public;禁止把 web entry 改成自身阅读页。 | tests/catalog_update_test.ts、tests/e2e/cli_update_e2e_test.ts |
| Publisher 内部发布 | 高 | 草稿发布为内部;提交公开候选;匿名渠道保持完全不可见。 | tests/catalog_publisher_test.ts、tests/e2e/cli_publish_e2e_test.ts |
| Approval 公开审批 | 高 | 人类审计者通过/驳回;严格触发 SELF_APPROVAL 拦截;禁止 Agent 审批。 | tests/catalog_approval_test.ts、tests/e2e/cli_approval_e2e_test.ts |
| 公开发布撤回 | 高 | 仅限审计者执行;撤回后多端瞬间切断;重提需重新走全流程审批。 | tests/catalog_withdraw_test.ts、tests/e2e/cli_withdrawal_e2e_test.ts |
| Access Control 名册 | 高 | 空名册 Bootstrap 引导;禁止 Agent 担任 auditor;禁止注销唯一审计者。 | tests/access_service_test.ts、tests/e2e/cli_access_e2e_test.ts |
| 名册可选邮箱 | 高 | 人类身份可绑定唯一 email;Agent 不可带邮箱;列表不泄漏未知字段;邮箱不是第二证明。 | tests/access_service_test.ts、tests/e2e/identity_roster_e2e_test.ts |
| Portal CF Access JWT 映射 | 高 | 默认关闭;合法 JWT 映射名册 email;伪造/过期/明文邮箱头匿名;不写会话;Gateway 不接受。 | tests/access_cf_access_test.ts、tests/e2e/portal_cf_access_e2e_test.ts |
| 登录凭证与会话 | 高 | 一次性凭证签发;SHA-256 哈希比对;伪造请求头全面拒绝。 | tests/access_session_test.ts、tests/e2e/cli_session_e2e_test.ts |
| 登录会话只读查询 | 高 | 人类审计者在 CLI / Portal / MCP 看到同一会话轨迹;维护者/只读/匿名拒绝;不泄漏令牌或哈希。 | tests/access_session_test.ts、tests/e2e/identity_sessions_e2e_test.ts |
| 登录凭证只读查询 | 高 | 人类审计者在 CLI / Portal / MCP 看到同一凭证轨迹;维护者/只读/匿名拒绝;不泄漏令牌或哈希。 | tests/access_session_test.ts、tests/e2e/identity_credentials_e2e_test.ts |
| 登录凭证作废轨迹只读查询 | 高 | 人类审计者在 CLI / Portal / MCP 看到同一凭证作废轨迹;维护者/只读/匿名拒绝;读操作不改名册。 | tests/access_credential_revoke_test.ts、tests/e2e/identity_credential_revokes_e2e_test.ts |
| 身份撤回轨迹只读查询 | 高 | 人类审计者在 CLI / Portal / MCP 看到同一撤回轨迹;维护者/只读/匿名拒绝;读操作不改名册。 | tests/access_revoke_test.ts、tests/e2e/identity_revokes_e2e_test.ts |
| 凭证作废与回滚 | 高 | 人类审计者作废泄露凭证;已作废凭证与会话瞬间失效;失败原子回滚。 | tests/access_credential_revoke_test.ts、tests/e2e/cli_credential_revoke_e2e_test.ts |
| Portal 发现与双平面 | 中 | /internal 内部笔记台;/public 公开发布目录;主题平滑降级;零 JS。 | tests/portal_ui_test.ts、tests/portal_theme_test.ts、tests/e2e/portal_ui_e2e_test.ts |
| MCP 渠道与网关鉴权 | 高 | 外部 MCP 连接信息发现;网关准入鉴权流水追加;严禁代理工具调用。 | tests/mcp_channel_test.ts、tests/gateway_service_test.ts、tests/e2e/cli_gateway_e2e_test.ts |
| Gateway 访问审计只读查询 | 高 | 人类审计者在 CLI / Portal / MCP 看到同一访问审计;维护者/只读/匿名拒绝;读操作不改目录或审计文件。 | tests/gateway_service_test.ts、tests/e2e/gateway_audit_e2e_test.ts |
| MCP 只读协议服务 | 高 | JSON-RPC 2.0 协议标准响应;portico_* 工具安全边界;只读无写权限。 | tests/mcp_protocol_test.ts、tests/e2e/mcp_http_e2e_test.ts |
Portico 项目画像与方向
项目概述
Portico 是组织的 Agent 门户与治理层:Agent 在别处运行,通过这里被登记、发布、发现、授权和访问。它提供 Web Portal、CLI、MCP 三类入口,把内部可见与公开可见分成两条信任边界;公开必须经过审批。系统按 CMS 式分级权限运转,但日常维护委派给 Agent,人类只做安全审计。
-
需修订(已修订):原文写“当前仓库几乎是空的……没有运行时代码、测试或 CI”。2026-09-13 起仓库已有 Deno 运行时骨架、内部 Registry 目录、Publisher 草稿/内部发布/公开候选、Approval 公开发布审批、Access Control 身份名册、只读 Portal 发现、MCP 渠道登记与授权连接信息、MCP Gateway 鉴权/路由/访问审计(只做门卫,不执行工具、不代理流量)、人类安全审计视图、受约束的 UI Components 门户组件盒(固定种类,不能当 CMS),以及一次性下发的登录会话(哈希存储,非外部 IdP)、身份授权撤回(人类审计者撤回名册主体,不能自撤、不能撤最后一位审计者)、Registry 受治理表面更新(
catalog update,只能改draft/internal记录,待审与已公开记录须先撤回/拒绝才能改),2026-09-14 的登录凭证作废(identity credential revoke,人类审计者作废凭证与会话但不撤名册),和 Web 渠道登记与已授权入口发现(web list/web describe、PortalGET /api/web,直连 http(s) 链接,不代理页面),以及 CLI 包坐标发现(cli list/cli describe、PortalGET /api/cli,只返回jsr:/npm:坐标,不安装不执行),以及 Portal 双平面页面层(内部笔记台与公开发布页、四个颜色主题预设、纯 CSS 主题切换),以及GET /社论杂志风发现壳(渠道过滤、明暗data-theme、/s/:id阅读栏,不是 CMS,不替代/internal与/public,只读跨面入口:杂志壳↔/public,已登录才见/internal),以及目录过滤查询(CLIcatalog list --q/--channel/--state、PortalGET /api/catalog与 MCPportico_list共用同一过滤器,只匹配当前身份可见的 id/名称/说明,不搜索入口 URL 或包坐标),以及治理仪表盘(CLIcatalog dashboard、PortalGET /api/dashboard与 MCPportico_dashboard共用dashboardFrom,按当前身份可见性计数,不是运行指标大盘),以及审计时间线过滤查询(CLIaudit list --q/--kind/--action/--subject、PortalGET /api/audit与 MCPportico_audit共用同一过滤器,只匹配当前审计者可见时间线的 id/主体/摘要/动作,不搜索入口 URL 或包坐标),以及身份名册只读查询(CLIidentity list、PortalGET /api/identities与 MCPportico_identities共用AccessService.list,返回 id/kind/role 与可选的人类 email,不泄漏凭证或会话,只读与匿名 FORBIDDEN),以及名册可选邮箱(人类审计者identity grant --email给人类身份绑定唯一地址,大小写归一;Agent 不可带邮箱;邮箱不是第二身份证明),以及 Portal 可选的 Cloudflare Access JWT 映射(默认关闭;只校验Cf-Access-Jwt-Assertion的签名与 aud/iss/exp,再用已校验 email 命中名册人类身份;明文邮箱头不是证明;失败为匿名且不写 identities/sessions;CLI / Gateway / MCP 仍只认会话),以及公开审批记录只读查询(CLIcatalog approvals、PortalGET /api/approvals与 MCPportico_approvals共用listApprovals,已登录身份看到同一批通过/拒绝/撤回记录,匿名得到空列表),以及授权轨迹只读查询(CLIidentity grants、PortalGET /api/grants与 MCPportico_grants共用AccessService.listGrants,仅人类审计者看到同一批追加式授权记录,维护者/只读/匿名 FORBIDDEN),以及身份撤回轨迹只读查询(CLIidentity revokes、PortalGET /api/revokes与 MCPportico_revokes共用AccessService.listRevokes,仅人类审计者看到同一批追加式撤回记录,维护者/只读/匿名 FORBIDDEN),以及当前会话身份只读查询(CLIidentity whoami、PortalGET /api/whoami与 MCPportico_whoami共用AccessService.whoami,已登录身份看到自己的 id/kind/role,匿名 FORBIDDEN,载荷不含邮箱、凭证或会话),以及登录会话只读查询(CLIidentity sessions、PortalGET /api/sessions与 MCPportico_sessions共用AccessService.listSessions,仅人类审计者看到会话 id/主体/时间,不含令牌或哈希,维护者/只读/匿名 FORBIDDEN),以及登录凭证只读查询(CLIidentity credentials、PortalGET /api/credentials与 MCPportico_credentials共用AccessService.listCredentials,仅人类审计者看到凭证 id/主体/credentialRef/签发者/时间,不含令牌或哈希,维护者/只读/匿名 FORBIDDEN),以及登录凭证作废轨迹只读查询(CLIidentity credential revokes、PortalGET /api/credential-revokes与 MCPportico_credential_revokes共用AccessService.listCredentialRevokes,仅人类审计者看到同一批追加式凭证作废记录,维护者/只读/匿名 FORBIDDEN),以及 Gateway 访问审计只读查询(CLIgateway audit、PortalGET /api/gateway-audit与 MCPportico_gateway_audit共用listGatewayAudit,仅人类审计者看到同一批允许/拒绝记录,维护者/只读/匿名 FORBIDDEN,读操作不写目录或审计文件),以及门户组件盒只读查询(CLIpage get、PortalGET /api/page与 MCPportico_page共用PageService.get,同一身份看到同一组解析后的组件,匿名看不到内部卡片,读操作不写 page);未实现的模块仍是产品意图,不是现存实现。 -
架构图
┌─────────────────────────────────────────┐
Humans (审计) │ Portico │
Agents (维护) │ │
│ Portal ── Dashboard / 发现 / 门户页 │
publish │ Registry ── 目录、版本、可见性、引用 │
CLI / MCP / API ─► Publisher ── 内部发布 / 公开提交 │
│ Approval ── 跨越公开边界的审批 │
│ Access Control ── 分级权限 + Agent 身份 │
│ UI Components ── 受约束的门户组件 │
│ MCP Gateway ── 鉴权、路由、访问审计 │
│ CLI ── 发布、查询、状态机读输出 │
└─────────────┬───────────────────────────┘
│ 不运行 Agent
▼
外部 Agent 运行时 / MCP Server / CLI 制品
数据流:维护者(人或 Agent)提交登记与发布 → Registry 成为唯一事实来源 → 内部立即按权限可见 → 公开进入 Approval → 通过后 Portal / CLI / MCP Gateway 才对外暴露入口。Portico 保存元数据、权限、审批与审计记录,不托管推理循环,不执行 Agent 工具。进程运行时见「运行时边界(L0)」。
项目画像(目标状态)
做好之后,组织能在一个地方回答:有哪些 Agent、以什么渠道存在、谁能看见、谁能调用、何时变成公开、变更由谁做出、安全审计看什么。
目标体验:
- 发布一条内部 CLI 或 MCP,一次调用完成,失败可机读。
- 公开发布不能“顺便成功”;未审批的公开入口在任何渠道都不可见、不可达。
- 仪表盘先给目录与治理状态,而不是运维大盘或模型监控。
- Agent 能维护目录、元数据、门户页;不能批准自己的公开、不能关闭审计、不能改他人的安全结论。
- 人类打开审计视图能看到谁改了什么、是否越界、公开入口指向何处。
关键品质与冲突时的优先级:
- 信任边界正确(内部 / 公开 / 审批)高于功能完整。
- 可审计高于编辑体验。改目录可以不漂亮,但不能没有记录。
- 不运行 Agent 高于“网关更方便”。网关只能鉴权、路由、记访问,不能执行工具或托管运行时。
- 机读失败高于交互花活。CLI / MCP / API 的错误必须可判定。
- 一致性高于自定义。门户组件是受约束的组件盒,不是建站器。
设计取向:Portico 像门廊,不像机房。Registry 是内核;Portal、CLI、MCP 只是同一治理状态的不同入口。运行时用 Deno 的默拒权限模型对齐产品的信任边界,而不是用更快的全开运行时换迭代速度。
运行时边界(L0,铁律)
用户已选定 L0,这是硬边界,不是建议。
- 系统运行时是 Deno + TypeScript。Portal、API、Portico CLI、MCP Gateway、检查与测试都在这一个运行时上。
- 不使用 Node 作为运行时、包管理根基或测试运行器。
- 不以 Bun 作为产品运行时。 禁止 Deno / Bun / Node 双运行时或第二套锁文件。
- 权限默认拒绝。网络、文件系统、环境变量必须显式白名单;禁止用全开权限把默拒掏空。
npm:导入只允许作为适配层。某个 SDK 不适配,先隔离适配,不改换平台。- CLI 以 Deno 可分发产物提供,启动不依赖
node可执行文件。
当前能力清单
- 定位陈述
README.md 写明:Portico 是受治理的 Agent 门户;Agent 不在这里运行,而在这里被发布、发现、授权和访问。
- 运行时骨架(Deno L0)
deno.json + .github/workflows/ci.yml。检查与测试走 deno lint / deno check / deno test。产品命令不使用 --allow-all,CLI 仅 --allow-read --allow-write --allow-env,Portal 仅 --allow-read --allow-env --allow-net=127.0.0.1(无 --allow-write),Gateway 仅 --allow-read --allow-write --allow-env --allow-net=127.0.0.1(写权限只为访问审计文件)。无 package.json、无 Node/Bun 锁文件。
- 一键起动与可分发产物
src/up/main.ts(deno task up)用一条 PORTICO_DATA_DIR 起动整套系统:Portal、Gateway 与 MCP 是三个子进程,各自带自己的权限集(Portal 与 MCP 仍无 --allow-write),任一退出则其余一起收走,不留半死系统;stdout 为一行机读 JSON,给出 portal / gateway / mcp 三个入口 URL。src/build/main.ts(deno task build)用 deno compile 产出 dist/ 下四个产物(portico / portico-portal / portico-gateway / portico-mcp),各自内嵌权限集,启动不依赖 node。权限集集中声明在 src/perms.ts,up、build 与进程测试读同一份,不会各写一套。内网测试部署的三份 deploy/run-*.sh 与三份 deploy/portico-*.service 版本化:systemd 必须 ExecStart 仓库脚本,仓库默认绑定 127.0.0.1:8788/8789/8790,真实 RFC1918 地址只在安装现场注入,不得指向未审查的额外副本;这是测试环境,不是生产上线。
- Registry 内部目录
src/catalog/ 是目录内核。维护者可登记内部 Agent 表面(身份、名称、说明、渠道、版本、入口引用、维护者、治理状态=internal)。公开可见性不能通过登记“顺便成功”;未知字段或明文密钥字段被拒绝;失败不写目录。只读者可见内部记录,匿名不可见。
- Registry 受治理的表面更新
已登记的 draft / internal / rejected 记录可由维护者 catalog update --id <id> --input <file> 更新名称、说明、版本、渠道、入口(不能直接改 visibility / governanceState);渠道与入口一起改会重新校验 MCP / Web 一致性。rejected 可编辑是有意的:被拒绝的表面从未越过边界,它的唯一结果是“这次提交失败“;把它做成终态会让“拒绝→修改→重新提交“这条已写明的路径不可能成立,并永久污染该 id。pending_public(待审)与 approved_public(已公开)的记录仍不能直接更新——必须先 reject / withdraw;重新 publish 只回到待审,仍须独立审批,因此边界没有被削弱。未知字段、明文密钥字段与空更新被拒;失败不写目录。
- CLI 目录登记、草稿、发布、更新与查询
src/cli/main.ts:identity grant|revoke|list|grants|revokes|sessions|credentials|credential issue|credential revoke|credential revokes|login|logout|whoami、catalog register|draft|publish|update|approve|reject|withdraw|approvals|dashboard|list|get、mcp list|describe、web list|describe、cli list|describe、gateway authorize|audit、audit list 与 page set|get,一次调用结束,stdout 为 {ok,data} / {ok,error:{code,message}}。catalog dashboard 与 Portal GET /api/dashboard、MCP portico_dashboard 共用 src/catalog/dashboard.ts:只对当前身份可见的记录计数,读操作不写目录。catalog list 接受 --q / --channel / --state,与 Portal GET /api/catalog、MCP portico_list 共用 src/catalog/query.ts:过滤发生在可见性判定之后,只匹配 id / 名称 / 说明,不搜索入口 URL 或包坐标;非法过滤器 INVALID_INPUT,失败不写目录。非匿名命令必须用登录后的 --session 证明身份;--actor-* 单独出现会被拒绝(USAGE),不带任何身份参数即匿名。Portal 在显式启用时可把已校验的 Cloudflare Access JWT 映射到名册 email;CLI / Gateway / MCP 没有这条路径。
- Publisher 内部发布与公开候选
维护者可以把草稿发布为内部(只读者立即可见),或把内部/草稿提交为公开候选(governanceState=pending_public)。公开候选对匿名仍不可见、不可达;不能经 publish 写成 approved_public;无权或非法字段失败后不脏写。
- Approval 公开发布审批
独立人类审计者可 approve / reject 公开候选。提交同一请求的身份不能自批(SELF_APPROVAL);维护者与 Agent 不能审。通过后匿名可见 approved_public;拒绝后公开面仍不可达。审批记录与目录记录分开追加,维护者不能改写。审批者必须持有名册中被授予 human auditor 的会话;角色以名册为准,自称不构成证明。catalog approvals、Portal GET /api/approvals 与 MCP portico_approvals 共用 CatalogService.listApprovals:已登录身份看到同一批通过/拒绝/撤回记录与同一顺序;匿名得到空列表,不泄漏待审或已拒绝入口。读操作不写目录。
- 撤回公开发布
已 approved_public 的记录可由人类审计者 catalog withdraw --id <id> 撤回:记录回到 governanceState=internal、visibility=internal,公开入口(CLI、Portal、Gateway)对匿名与组织外主体立即消失,响应不泄漏端点;只读及以上身份仍可见同一条内部记录。撤回是公开信任边界的收缩方向,因此与批准同一权限面:只有人类审计者能撤回,维护者、Agent、只读者与匿名得到 FORBIDDEN。只有仍然公开的记录可撤回,internal / pending_public / rejected 得到 INVALID_STATE,重复撤回同样 INVALID_STATE。撤回在审批轨迹中留下 withdrawn 记录(谁撤回、何时、撤回的是哪个版本),维护者不能改写。撤回后再次 publish --visibility public 只回到 pending_public,必须重新经独立审批才能重新公开。失败撤回不写目录、不追加审批记录。
- Access Control 身份名册
src/access/ 保存身份与追加式授权记录。空名册只能引导第一位人类审计者;之后仅人类审计者可 grant。Agent 不能被授予 auditor;主体不能给自己提权。失败不写名册。catalog CLI 用名册解析角色。identity list、Portal GET /api/identities 与 MCP portico_identities 共用 AccessService.list:维护者与人类审计者看到同一批 {id,kind,role},只读者与匿名得到 FORBIDDEN,载荷不含凭证、会话或哈希。identity grants、Portal GET /api/grants 与 MCP portico_grants 共用 AccessService.listGrants:仅人类审计者看到同一批追加式授权记录与同一顺序;维护者、只读者与匿名得到 FORBIDDEN,载荷不含凭证、会话或哈希。identity revokes、Portal GET /api/revokes 与 MCP portico_revokes 共用 AccessService.listRevokes:仅人类审计者看到同一批追加式身份撤回记录与同一顺序;维护者、只读者与匿名得到 FORBIDDEN,载荷不含凭证、会话或哈希。identity whoami、Portal GET /api/whoami 与 MCP portico_whoami 共用 AccessService.whoami:已登录身份看到自己的 {id,kind,role},匿名得到 FORBIDDEN,载荷不含邮箱、凭证或会话。identity sessions、Portal GET /api/sessions 与 MCP portico_sessions 共用 AccessService.listSessions:仅人类审计者看到同一批会话轨迹(id / 主体 / 创建与过期时间,作废则含 revokedAt)与同一顺序;维护者、只读者与匿名得到 FORBIDDEN,载荷不含令牌或哈希。identity credentials、Portal GET /api/credentials 与 MCP portico_credentials 共用 AccessService.listCredentials:仅人类审计者看到同一批凭证轨迹(id / 主体 / credentialRef / 签发者 / 签发时间,作废则含 revokedAt)与同一顺序;维护者、只读者与匿名得到 FORBIDDEN,载荷不含令牌或哈希。identity credential revokes、Portal GET /api/credential-revokes 与 MCP portico_credential_revokes 共用 AccessService.listCredentialRevokes:仅人类审计者看到同一批追加式凭证作废记录与同一顺序;维护者、只读者与匿名得到 FORBIDDEN,载荷不含令牌或哈希。名册是被委派主体的登记簿,不是凭证:没有会话时任何 --actor-* 都不构成证明。名册与 sessions.json 仍是本地信任根——对数据目录有写权限的主体可以重置它,因此文件权限属于部署的一部分,不属于代码。外部断言不能创建名册记录。
- 本地持久化与写序
所有 store 都是整文件 JSON:临时文件写入后 rename 落地(同目录,原子)。同一进程内对同一文件的读-改-写串行化——await 会交错,两次并发调用会各自载入、各自修改、后写覆盖先写;Gateway 每个请求都要追加访问审计,丢一条就是审计缺口而非数据抖动。临时文件固定为 <path>.tmp(部署的 --allow-write 白名单正是这两个路径,唯一后缀会被拒)。每个文件只允许一个写进程:跨进程并发仍可能互相覆盖,up 与部署脚本都按此假设组织(Gateway 是 gateway-audit.json 的唯一写者)。这是当前规模的取舍,不是终态设计。
- 身份证明与信任根
非匿名写操作与 CLI / Gateway / MCP 只能由登录会话证明。CLI --actor-* 单独出现即拒绝(USAGE,不写任何文件);Portal / Gateway / MCP 完全不接受 X-Portico-Actor-*,伪造头只得到匿名视图。Portal 在显式启用时可以把已校验的 Cloudflare Access JWT 映射到名册,仍然不是自称头,也不会签发会话。空名册的引导路径有且只有两条:第一位人类审计者的 identity grant(无 actor),以及一次性的首张凭证 identity credential issue(无 actor,仅当系统从未签发过任何凭证、且主体是人类审计者)。凭证 token 只显示一次、只存 SHA-256,因此它是数据目录中唯一无法被读者还原的秘密;把它交给人类审计者属于运维动作。首个凭证签发后,bootstrap 永久关闭,之后每一次签发都必须由现有审计者会话发起。信任根的边界是诚实的:对数据目录有写权限的主体可以重置名册,因此文件权限属于部署的一部分。
- 身份授权撤回
已在名册中的身份可由独立人类审计者 identity revoke --id <id> 撤回:该身份立即从现行名册消失,resolve 与既有会话都不再把它当成有效主体;既有 grant 记录保持追加式不可改写,并另追加一条 revoke 记录(谁撤、何时、撤的是哪个角色)。维护者、Agent、只读者与匿名得到 FORBIDDEN。主体不能撤自己;最后一位人类审计者得到 INVALID_STATE,避免名册无人可审。未知或已撤回的 id 得到 NOT_FOUND。明文密钥字段被拒。失败撤回不改 identities/grants/revokes,也不作废会话或凭证。成功撤回会作废该主体尚未过期的会话和已签发凭证(只标 revokedAt,仍只存哈希);重新 grant 需要新的凭证。被撤维护者留下的目录记录仍在,只是他们不能再写。撤回轨迹进入人类安全审计视图,维护者不能读、不能改写。同一人类审计者经 CLI identity revokes、Portal GET /api/revokes 与 MCP portico_revokes 看到同一批追加式撤回记录;读操作不改名册。
- Portal 发现与治理仪表盘
src/portal/ 是只读 HTTP 入口,消费同一 CatalogService。GET /api/catalog、GET /api/catalog/:id、GET /api/dashboard(与 CLI catalog dashboard、MCP portico_dashboard 同一载荷)、GET /api/mcp、GET /api/mcp/:id、GET /api/web、GET /api/web/:id、GET /api/cli、GET /api/cli/:id、GET /api/audit、GET /api/identities(与 CLI identity list、MCP portico_identities 同一载荷,仅维护者与人类审计者)、GET /api/grants(与 CLI identity grants、MCP portico_grants 同一载荷,仅人类审计者)、GET /api/revokes(与 CLI identity revokes、MCP portico_revokes 同一载荷,仅人类审计者,维护者/只读/匿名 FORBIDDEN)、GET /api/whoami(与 CLI identity whoami、MCP portico_whoami 同一载荷,已登录看到自己,匿名 FORBIDDEN)、GET /api/sessions(与 CLI identity sessions、MCP portico_sessions 同一载荷,仅人类审计者,维护者/只读/匿名 FORBIDDEN)、GET /api/credentials(与 CLI identity credentials、MCP portico_credentials 同一载荷,仅人类审计者,维护者/只读/匿名 FORBIDDEN)、GET /api/credential-revokes(与 CLI identity credential revokes、MCP portico_credential_revokes 同一载荷,仅人类审计者,维护者/只读/匿名 FORBIDDEN)、GET /api/gateway-audit(与 CLI gateway audit、MCP portico_gateway_audit 同一载荷,仅人类审计者,维护者/只读/匿名 FORBIDDEN)、GET /api/page(与 CLI page get、MCP portico_page 同一载荷;匿名看不到内部卡片)、GET /api/approvals(与 CLI catalog approvals、MCP portico_approvals 同一载荷;已登录可见,匿名为空列表)与 HTML GET /、GET /s/:id 对同一身份呈现与 CLI 相同的可见性(审计面仅人类审计者)。GET / 是社论杂志风发现壳(PORTICO 顶栏、内容/专题/收藏展示、渠道过滤、明暗 data-theme、精选 hero),映射的仍是 Agent 表面目录,不是文章 CMS;GET /s/:id 是阅读栏。匿名只能看见 approved_public;内部详情对匿名为 HTML 404 且不泄漏入口。收藏为展示项,无写入。发现页对可见记录展示已授权入口:Web 为直连 http(s) 链接,CLI 为转义后的包坐标,其它渠道为转义后的引用,不代理页面。/internal 与 /public 双平面仍是正式页面层,杂志壳不替代它们。三层之间只有只读入口:杂志壳链到 /public,已登录身份才链到 /internal,公开发布面链回 /,公开详情可链到同一条 /s/:id;匿名 /internal 仍 404。无会话且无已校验 JWT 为匿名;X-Portico-Actor-* 不构成证明,Portal 不再读取,伪造头只得到匿名视图。Authorization: Bearer / X-Portico-Session 解析已登录会话且角色以名册为准;会话优先于 JWT。人看的页面(/internal*、/public*、/、/s/:id)找不到或无权看见时返回 HTML 404,不返回 JSON 信封,也不泄漏记录名;/api/* 仍是 {ok,error} JSON。默认绑定 127.0.0.1,也接受 RFC1918 单播 IPv4。POST/PUT/PATCH/DELETE 返回 405,不写目录。登录在 CLI 完成;Portal 只读消费会话文件,JWT 映射也不写会话。
- MCP 渠道登记与访问
维护者可登记 channels=["mcp"] 且 entry.kind=mcp_endpoint 的外部 MCP Server。端点必须是绝对 http(s) URL,禁止 userinfo、查询串密钥和命令式入口。mcp list / mcp describe 与 Portal GET /api/mcp 对同一身份返回连接信息 {endpoint, connect:{mode:"direct"}},由客户端直连;Portico 不执行工具、不代理流量。可见性与目录相同:内部对匿名不可见,公开须审批。CLI 表面不会出现在 MCP 列表。失败登记不写目录。
- MCP 协议入口(只读治理发现)
src/mcp/ 是 MCP(JSON-RPC 2.0 over HTTP)服务端,暴露十五个只读工具:portico_list、portico_describe、portico_entry、portico_dashboard、portico_audit、portico_approvals、portico_identities、portico_grants、portico_revokes、portico_whoami、portico_sessions、portico_credentials、portico_credential_revokes、portico_gateway_audit、portico_page。每个工具都是 CatalogService / AuditService / AccessService / PageService / GatewayService 已有调用的薄投影,因此可见性、审批与角色规则不会分叉——同一身份在 MCP、CLI、Portal 上看到的是同一批记录、同一顺序。工具结果是 CLI 同一个 {ok,data} / {ok,error:{code,message}} 信封,所以“三入口一致“可以靠比对载荷验证,而不是靠读三份实现。鉴权只认会话(Authorization: Bearer / X-Portico-Session):网络调用者不得靠自称头证明身份。协议层错误(未知方法 -32601、未知工具 -32602、批量请求 -32600、解析失败 -32700)与工具层失败(in-band isError)分开;非 POST 返回 405。portico_audit 仍只对人类审计者开放;portico_approvals 对已登录身份开放、匿名得到空列表;portico_identities 对维护者与人类审计者开放,只读与匿名 FORBIDDEN;portico_grants 仅人类审计者,维护者/只读/匿名 FORBIDDEN;portico_revokes 仅人类审计者,维护者/只读/匿名 FORBIDDEN;portico_whoami 对已登录身份开放、匿名 FORBIDDEN;portico_sessions 仅人类审计者,维护者/只读/匿名 FORBIDDEN;portico_credentials 仅人类审计者,维护者/只读/匿名 FORBIDDEN;portico_credential_revokes 仅人类审计者,维护者/只读/匿名 FORBIDDEN;portico_gateway_audit 仅人类审计者,维护者/只读/匿名 FORBIDDEN;portico_page 对匿名与已登录身份开放,内部卡片对匿名不可见。MCP 进程只读(无 --allow-write),不执行、不代理、不编排任何外部工具。
- Web 渠道登记与已授权入口
维护者可登记 channels=["web"] 且 entry.kind=url 的外部 Web 入口。URL 必须是绝对 http(s),禁止 userinfo、查询串密钥、javascript: 及其它非 http(s) 方案。路径不得是 Portico 自己的阅读页(/s/<id> 或 /public/s/<id>,含尾斜杠与查询串),避免把目录卡链回自身详情。web list / web describe 与 Portal GET /api/web 对同一身份返回 {href, connect:{mode:"direct"}},由客户端直连;Portico 不抓取、不代理、不渲染远程页面。可见性与目录相同:内部对匿名不可见,公开须审批。CLI / MCP 表面不会出现在 Web 列表。失败登记不写目录。Portal HTML 只给当前身份可见的记录展示入口,并对 href 做 HTML 转义。
- CLI 渠道登记与已授权包坐标
维护者可登记 channels=["cli"] 且 entry.kind=package 的包坐标。坐标必须是 jsr:@scope/name 或 npm:name / npm:@scope/name(可带精确版本后缀),禁止命令、空白、shell 元字符、http(s)/git URL 及其它 registry。cli list / cli describe 与 Portal GET /api/cli 对同一身份返回 {package, connect:{mode:"coordinate"}};Portico 不安装、不执行、不下载该包。可见性与目录相同:内部对匿名不可见,公开须审批。MCP / Web 表面不会出现在 CLI 列表。失败登记不写目录。Portal HTML 把包坐标当转义后的代码展示,不当成可点击下载链接。
- MCP Gateway 鉴权与路由(门卫)
src/gateway/ 对已登记 MCP 做身份、可见性与访问审计,再发出直连路由。gateway authorize 与 POST /gateway/mcp/:id/authorize 对同一身份返回 {endpoint, connect:{mode:"direct"}};不转发 JSON-RPC、不执行工具、不代理流量。未授权或未审批公开得到 NOT_FOUND,响应不泄漏端点。允许与拒绝都追加到独立审计文件;人类审计者可 gateway audit、Portal GET /api/gateway-audit、MCP portico_gateway_audit 与 GET /gateway/audit 看到同一批记录,维护者不能读、也不能改写。工具调用类 POST 返回 405。默认绑定 127.0.0.1。失败授权不改目录。
- 人类安全审计视图
src/audit/ 给人类审计者一条只读时间线。audit list、Portal GET /api/audit 与 MCP portico_audit 对同一身份合并同一份时间线:追加式目录变更(register / draft / publish / update)、身份授权、身份撤回、登录凭证作废、公开审批,以及 Gateway 访问审计。三处都读同一个可选 PORTICO_GATEWAY_AUDIT_PATH;up 会把它同时交给 Portal、MCP 与 Gateway,所以默认情况下三者呈现相同内容,而不是“CLI 有、其他入口没有“。audit list 接受 --q / --kind / --action / --subject,与 Portal GET /api/audit、MCP portico_audit 共用 src/audit/query.ts:过滤发生在审计者鉴权之后,只匹配 id / 主体 / 摘要 / 动作,不搜索入口 URL 或包坐标;非法过滤器 INVALID_INPUT,失败不写目录。维护者、只读者与匿名即使带过滤器也得到 FORBIDDEN;没有改写或删除入口。Portal /internal/audit 是无脚本 GET 表单,只给人类审计者。Portal 写方法仍 405,失败登记不写目录变更。审计结论与维护轨迹分开存储,维护者身份不能覆盖。
- UI Components 受约束门户组件
src/ui/ 是固定种类的组件盒:catalog_card、catalog_detail、permission_hint、approval_status、audit_snippet。维护者可用 page set 把组件绑定到已有目录记录;page get、Portal GET /api/page / GET / 与 MCP portico_page 对同一身份解析。解析走目录可见性:内部与待审公开对匿名不可见,不能靠放进页面绕过审批。未知种类、HTML/主题字段、明文密钥被拒。audit_snippet 只对人类审计者填充。Portal 与 MCP 仍只读,POST /api/page 为 405。这不是 CMS、建站器或任意页面。
- 登录会话
人类审计者可 identity credential issue 一次性下发登录凭证;主体 identity login 换会话。凭证与会话只存 SHA-256,不落明文密钥或口令。CLI --session 与 Portal / Gateway Authorization: Bearer(或 X-Portico-Session)解析同一会话,角色始终从名册读取,不能靠会话头自封 auditor。失败登录不写会话;logout 后原令牌不可用。--actor-* 只能与 --session 同时出现且必须与之一致,单独出现被拒。Portal 仍无 --allow-write。这不是口令库,也不在 Portico 里跑 OAuth。
- 登录凭证作废
已签发的登录凭证与活动会话可由独立人类审计者 identity credential revoke --id <subject> 作废:该主体的未过期凭证和会话立即标 revokedAt(仍只存哈希),CLI --session 与 Portal / Gateway 会话头变为 FORBIDDEN;名册身份仍在,重新 credential issue + login 即可再次进入;目录记录不变。这与 identity revoke 不同:后者撤走主体;前者只切断登录面,主体留在名册中,可重新签发。切断登录面即切断 CLI / Gateway / MCP 与默认 Portal 会话路径。若 Portal 启用了 Cloudflare Access JWT 映射,名册上仍有 email 的人类身份仍可能经已校验 JWT 只读进入 Portal,直到 identity revoke 或关闭该功能。维护者、Agent、只读者与匿名得到 FORBIDDEN。未知主体 NOT_FOUND。没有活动凭证或会话、以及重复作废得到 INVALID_STATE。明文密钥字段被拒。作废审计记录先写入,随后才失效会话与凭证(与 identity revoke 同序):commitCredentialRevoke 失败则什么都没发生,不会出现“会话已死但无记录“;若随后的失效步骤部分失败,调用方得到错误,重试可以补齐(主体仍有可作废的活动凭证可供重试)。反向顺序可能在“已无活动凭证“时失败,导致真实作废永久无记录且重试只会得到 INVALID_STATE。成功后审计者可重新 credential issue。作废轨迹进入人类安全审计视图(kind=credential),维护者不能读、不能改写。同一人类审计者经 CLI identity credential revokes、Portal GET /api/credential-revokes 与 MCP portico_credential_revokes 看到同一批追加式作废记录;读操作不改名册或会话文件。
- Portal 双平面 UI(内部笔记台 / 公开发布)
src/portal/design/ 是页面层。同一套语义 token 支撑两个平面:内部笔记台(/internal、/internal/c、/internal/audit、/internal/s/:id)面向只读及以上身份,呈治理状态、入口、维护者与治理路径;公开发布页(/public、/public/t/:channel、/public/s/:id)面向任何人,把已过审批的登记当作稿件呈现。公开发布页在视图层再筛一次,只渲染 visibility=public 且 governanceState=approved_public 的记录——即使请求者是维护者,草稿与待审公开也不进入公开页。颜色主题为四个固定预设(内部/公开 × 浅色/深色),解析顺序是 ?theme= → 操作系统提示 → 平面默认;非法或跨平面取值静默降级。主题与模式是纯 CSS 属性,页面不含脚本、不引用远程字体或图片,CSP 仍为 default-src 'none'。审计路由只对人类审计者存在,其他身份得到 404。/ 是社论杂志风发现索引(渠道过滤、/s/:id 阅读栏),不是两个新平面的替代品。杂志壳只读链到 /public;已登录身份才链到 /internal;公开发布面链回 /,公开详情可链到同一条已审批记录的 /s/:id。规范见 ui-spec.md。
- Portal Cloudflare Access JWT 映射
src/access/cf-access.ts 用 Web Crypto 校验 RS256 JWT(aud / iss / exp / 签名)。默认关闭:PORTICO_CF_ACCESS_ENABLED 未显式打开,或缺少合法 team/aud 时,现有会话路径不变。启用后 Portal 在没有会话时读取 Cf-Access-Jwt-Assertion,用已校验 email 查名册人类身份;明文 Cf-Access-Authenticated-User-Email 不是证明。命中则与该身份的会话看到同一治理状态;失败为匿名(/internal HTML 404)。不写 identities.json / sessions.json,不签发 pst1_ 会话。CLI / Gateway / MCP 不读该头。JWKS URL 只允许 loopback 测试地址或该 team 的 Cloudflare certs 路径。这不是 GitHub OAuth 客户端,也不托管 Tunnel。
- 尚未实现
GitHub OAuth / 邮箱 OTP 登录界面,以及 Cloudflare Tunnel / cloudflared 配置。它们属于边缘 IdP 与基础设施,不进本仓库。标为待核验以外的“已有能力”一律不应被写出。
目标功能清单
下列是目标画像中的一级业务能力,不是实现顺序,也不是当前事实。细节方案由执行者决定;边界以「非目标」为准。
1. Registry(目录)
登记 Agent 表面,而不是登记进程。一条记录至少能表达:身份、名称、说明、渠道(CLI / MCP / Web)、版本、可见性(内部 / 公开)、入口引用(URL、包坐标、MCP endpoint 等)、维护者(人与 Agent)、当前治理状态。
目录是唯一事实来源。Portal、CLI、MCP Gateway 只消费目录,不各写一份“谁已发布”。
2. Publisher(发布)
把登记从草稿变成内部或公开候选。内部发布按权限立即进入目录。公开发布只产生待审请求,不改变公开可达性。同一 Agent 可有多渠道,但渠道不能偷偷改变可见性。
3. Approval(公开审批)
公开是跨越组织信任边界的动作。审批必须独立于提交者:提交发布的 Agent 或用户不能自批。未通过、撤回、拒绝后,公开入口必须消失或保持不可达。审批记录保留:谁提交、谁审、审了什么、指向何处。
4. Portal(发现与仪表盘)
Web 入口用于浏览目录、查看治理状态、进入被授权的 Agent 表面。仪表盘默认回答治理问题(有什么、是否公开、待审多少、最近谁改了),不为 Agent 运行指标负责。
5. Access Control(分级权限)
类似传统 CMS 的分级,但身份分两类:人类与 Agent。至少能区分:只读、内部维护、提交公开、批准公开、安全审计。权限作用于可见性、发布、审批、组件/页面维护、审计日志。Agent 身份可维护,不可接管审计结论,不可批准自己的公开。
6. CLI
Portico 自己的命令行入口:登录/凭证、登记、发布、查询目录、看审批状态。任何命令一次调用内结束,成功与失败都可机读。CLI 发布的是对 Portico 的操作,不是在 Portico 里安装并运行第三方 Agent。
被登记的第三方 CLI 只是目录中的一种渠道引用,由外部提供。
7. MCP 渠道与 MCP Gateway
MCP 渠道:把外部 MCP Server 登记为可发现、可授权的入口。
MCP Gateway:站在门口做身份、权限、路由和访问审计。它不执行工具、不解释模型、不代跑 Agent。若某次实现无法安全代理流量,允许降级为“只发已授权的连接信息,由客户端直连”,但目录与权限仍以 Portico 为准。
8. UI Components(门户组件)
内置少量可组合的门户组件(目录卡、详情、权限提示、审批状态、审计片段等),让 Agent 能维护一致的门户页。组件盒的目的是可审计的一致性,不是通用 CMS 或可视化建站。
9. Agent 委派维护与人类安全审计
日常增改目录、元数据、门户页由 Agent 执行。人类只审安全:公开边界、入口指向、权限变化、密钥与凭据是否泄漏、网关是否越权。维护轨迹与审计结论分离存储;审计结论不能被维护者身份覆盖。
非目标(铁律)
- 不运行、编排、托管 Agent。 Portico 不是 runtime、不是队列、不是模型网关。原因:一旦代跑,门户变成机房,信任模型坍塌。
- MCP Gateway 不执行工具、不代发推理。 它只做门卫。原因:执行即运行时。
- 不做成通用 CMS / 建站系统 / 应用商店。 没有内容类型工厂、主题市场、计费与交易。原因:那会把治理产品做成另一套 WordPress。
- 不绕过公开发布审批。 任何入口(API、CLI、MCP、直接写库)都不能把未审批对象变成公开可达。
- Agent 维护权 ≠ 安全审计权。 维护者不能自批公开、不能删除或改写审计结论、不能关闭审计。
- 不在门户或日志中保存明文密钥。 凭据只引用外部密钥设施或一次性下发。
- 不把 Portico 当成唯一制品仓库。 包、镜像、源码仍在外部;这里管登记、可见性、授权与入口。
- 不为单个 Agent 的业务逻辑提供工作流引擎。 那是 Agent 自己的事。
- 不使用 Node 作为运行时或工具链根基。 原因:已选定 Deno;再引入 Node 会把权限模型与依赖图拆成两套。
- 不以 Bun 作为产品运行时,不引入第二运行时。 原因:Bun 默认全开,和「人类只做安全审计」冲突;双运行时是熵增。某个原生模块在 Deno 上不可用,只允许隔离适配层,不允许升格为第二平台。
方向与意图
- 成为组织里 Agent 的唯一治理前门
服务于“能回答有什么、谁能用、是否公开”。没有第二份影子目录。
- 把内部与公开做成硬边界,而不是标签
服务于信任边界优先。公开失败必须比发布失败更显眼。
- 让 Agent 成为合法维护者,让人类成为审计者
服务于与传统 CMS 的本质差异。产品要默认 Agent 会写,人会审,而不是人填表。
- 三入口语义一致
Portal、CLI、MCP 看到同一可见性与同一审批状态。一个入口公开、另一个入口仍隐藏,视为缺陷。
- 网关保持门卫身份
服务于“不运行 Agent”。流量代理可以有,执行循环不能有。
- 组件盒保持小而硬
服务于可审计的门户一致性。组件变少可以,变成建站器不行。
- 单一 Deno 运行时,权限默拒
服务于信任边界与可审计性。L0 已锁定:不回到 Node,不把 Bun 升格为平台。
完成的样子
当下列可观察结果同时成立,这个项目才算做成门户,而不是又一个后台:
- 内部发布后,有权限者能在 Portal / CLI / MCP 发现同一条记录;无权限者不能。
- 公开发布在审批完成前,对匿名与组织外主体不可见、不可达;审批拒绝或撤回后保持如此。
- 提交公开的身份不能批准同一请求。
- 目录变更、权限变更、公开审批都留下不可被维护者改写的审计记录。
- 测试或探测无法通过 Portico 执行外部 Agent 的工具或推理。
- 进程不依赖
node;未在白名单中的 net/read/env 调用不能跑通。 - 核心治理数据流有自动化验证;回归能在 CI 被挡住。运行时侧用 Deno 的检查与测试(例如
deno check/deno test/deno lint);一级业务功能仍须达到下方矩阵的覆盖底线。
验收矩阵(业务能力覆盖矩阵)
覆盖底线(硬性规定):
- 每个一级功能至少有一条 Happy Path E2E。
- 每个高风险功能至少覆盖一条失败路径。
- 每个涉及权限的功能至少验证两种角色。
- 每个会修改系统状态的操作至少验证一次失败后的恢复或回滚。
- 每次新增一级业务功能,必须同步新增对应的 E2E 并更新本矩阵。
Registry 内部登记、Publisher 草稿/内部发布/公开候选、Approval 通过/拒绝、公开发布撤回、Access Control 身份名册、身份名册只读查询、当前会话身份只读查询、登录会话只读查询、登录凭证只读查询、登录凭证作废轨迹只读查询、身份撤回轨迹只读查询、名册可选邮箱、身份授权撤回、登录会话、登录凭证作废、Portal 可选 Cloudflare Access JWT 映射、Portal 发现/仪表盘、Portal 双平面 UI 与颜色主题、目录过滤查询、审计时间线过滤查询、MCP 渠道登记/连接信息、Web 渠道登记/已授权入口、CLI 渠道登记/已授权包坐标、MCP Gateway 鉴权路由、人类安全审计视图、UI Components 门户页维护,以及 CLI 对应命令已有测试证据。邮箱仍不是第二身份证明。三个非匿名入口里,CLI / Gateway / MCP 仍只认会话:CLI --session,Gateway / MCP 的 Authorization: Bearer 或 X-Portico-Session。Portal 在默认关闭时同样只认会话;显式启用 Cloudflare Access 后,校验通过的 JWT 只映射名册,不签发会话。--actor-* 与 X-Portico-Actor-* 不构成证明,Portal / Gateway / MCP 已不接受,CLI 单独给出即拒绝。GitHub OAuth / 邮箱 OTP 界面与 Tunnel 仍是边缘基础设施,不在本仓库。
| 一级功能 | 风险级别 | Happy Path E2E | 失败路径 | 权限角色覆盖 | 失败恢复/回滚 | 证据(测试路径/用例) |
|---|---|---|---|---|---|---|
| Registry 登记与目录 | 高 | ✅ 维护者登记内部表面,只读者 list/get 同一条 | ✅ 公开可见性被拒;偷写 approved_public 被拒;非法 id / 明文密钥字段被拒;Web 入口指向自身阅读页 /s/:id 或 /public/s/:id 被拒 | ✅ 只读 vs 维护者;匿名看不到内部记录 | ✅ 失败不写 Memory/File 目录 | tests/catalog_service_test.ts;tests/web_channel_test.ts;tests/e2e/cli_catalog_e2e_test.ts |
| Registry 受治理表面更新 | 高 | ✅ 维护者 catalog update 改 internal/draft 记录的 name/description/version/channels/entry;reader 看到同一条更新 | ✅ 待审 pending_public 与已公开 approved_public 记录更新被拒(INVALID_STATE);rejected 记录可更新(拒绝→修改→重新提交这条路径必须成立);reader/匿名更新 FORBIDDEN;未知字段/明文密钥/空更新被拒;渠道与入口不一致被拒;把 web entry 改成自身阅读页被拒 | ✅ 维护者 vs 只读/匿名 | ✅ 失败更新不改目录文件字节;已公开记录须先 withdraw,被拒记录改后须重新 publish+approve 才能公开可见(重新提交只回到待审) | tests/catalog_update_test.ts;tests/web_channel_test.ts;tests/e2e/cli_update_e2e_test.ts |
| Publisher 内部发布 | 高 | ✅ 草稿对只读隐藏;publish --visibility internal 后只读者可见同一条 | ✅ 无权 draft/publish 被拒;偷写 approved_public/明文密钥被拒;不能把 pending_public 降回 internal | ✅ 维护者 vs 只读;匿名看不到公开候选 | ✅ 失败不写/不改目录;公开候选对匿名仍不可达 | tests/catalog_publisher_test.ts;tests/e2e/cli_publish_e2e_test.ts |
| Approval 公开发布审批 | 高 | ✅ 独立人类审计者 approve 后匿名 list/get 同一条 approved_public | ✅ 自批 SELF_APPROVAL;维护者/Agent/只读 FORBIDDEN;拒绝后匿名仍不可见;密钥字段被拒 | ✅ 提交者 vs 人类审计者;维护者不能审 | ✅ 失败不写审批记录、不改公开面;已拒绝不能再 reject 改写 | tests/catalog_approval_test.ts;tests/e2e/cli_approval_e2e_test.ts |
| 公开审批记录只读查询 | 高 | ✅ 同一已登录身份经 CLI catalog approvals、Portal GET /api/approvals 与 MCP portico_approvals 得到同一批通过/拒绝/撤回记录与同一顺序 | ✅ 匿名得到空列表且不泄漏待审或已拒绝入口;Portal POST /api/approvals 405;载荷不含凭证或会话令牌 | ✅ 已登录(审计者/维护者/只读)vs 匿名 | ✅ 读审批记录不改 catalog 文件字节;失败 POST 不追加审批记录;Portal / MCP 只读入口无 --allow-write | tests/catalog_approval_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/catalog_approvals_e2e_test.ts |
| 公开发布撤回 | 高 | ✅ 人类审计者 catalog withdraw --id 后,匿名在 CLI list/get、Portal /api/catalog 与 /api/mcp、Gateway authorize 上同时不可达;只读者仍见同一条 internal | ✅ 维护者/Agent/只读者/匿名撤回 FORBIDDEN;未知 id NOT_FOUND;对 internal/pending_public/rejected 与重复撤回 INVALID_STATE;密钥字段被拒 | ✅ 人类审计者 vs 维护者/Agent/只读者/匿名 | ✅ 失败撤回不改目录文件字节、不追加审批记录,公开面仍 approved_public;撤回后重发只回 pending_public,须重新独立审批才能再公开 | tests/catalog_withdraw_test.ts;tests/e2e/cli_withdrawal_e2e_test.ts |
| Portal 发现与仪表盘 | 中 | ✅ CLI register 后 reader 在 Portal list/HTML 与 /s/:id 看到同一条;approve 后匿名在 hero/list/详情看到同一条 approved_public;同一身份经 CLI catalog dashboard、Portal GET /api/dashboard 与 MCP portico_dashboard 得到同一计数与同一顺序;无 channel 的详情页顶栏不默认 Web,「内容」高亮;阅读页只保留一组渠道入口且回到带筛选的列表;渠道展示名来自 CHANNEL_LABEL;面包屑渠道层链到带筛选的列表;q 透传到左栏渠道入口、面包屑首页与面包屑渠道层;可见记录上不匹配的 channel/q 会 302 到自身渠道(q 仍命中则保留),规范化后再渲染时列表包含选中记录;杂志壳链到 /public,已登录身份另链到 /internal;公开发布面链回杂志 /,公开详情链到同一条 /s/:id;阅读页「返回列表」保留 channel/q | ✅ 内部与 pending_public 对匿名不可见;渠道过滤不泄漏匿名不可见记录;内部详情对匿名 HTML 404 且不泄漏入口;匿名带错误 channel 访问内部详情仍 404、Location 不泄漏;匿名杂志壳与公开面都不出现 /internal;匿名 /internal 仍 404;POST 405;冒充 auditor FORBIDDEN;HTML 转义名称;缺 --catalog 的 dashboard 为 USAGE | ✅ reader vs 匿名 | ✅ 失败写不改 catalog 文件;读 dashboard 不改目录字节;阅读页筛选规范化不改目录字节;跨面导航是只读链接,不改目录字节;只读入口无 --allow-write | tests/portal_handler_test.ts;tests/portal_magazine_test.ts;tests/portal_ui_test.ts;tests/e2e/portal_discovery_e2e_test.ts;tests/e2e/portal_magazine_e2e_test.ts;tests/e2e/catalog_dashboard_e2e_test.ts |
| 目录过滤查询 | 中 | ✅ 同一身份经 CLI catalog list --q、Portal GET /api/catalog?q= 与 MCP portico_list 得到同一批过滤结果与同一顺序;杂志 GET /?q= 是无脚本 GET 表单,只展示已授权表面 | ✅ 匿名 q 匹配内部记录为空且不泄漏入口/包坐标;q 不搜索 endpoint 或 jsr:/npm: 坐标;非法 channel / 过长 q / 控制字符 INVALID_INPUT | ✅ reader vs 匿名 | ✅ 非法查询不改 catalog 文件字节;Portal / MCP 只读入口无 --allow-write | tests/catalog_query_test.ts;tests/portal_handler_test.ts;tests/portal_magazine_test.ts;tests/mcp_protocol_test.ts;tests/e2e/cli_catalog_e2e_test.ts;tests/e2e/catalog_query_e2e_test.ts;tests/e2e/portal_magazine_e2e_test.ts |
| Portal 双平面 UI 与颜色主题 | 中 | ✅ 维护者在 /internal 看到草稿、待审与内部记录;同一批数据在 /public 只呈现 approved_public;四个主题预设各自渲染,?theme= 切换生效 | ✅ 匿名访问 /internal* 全部 HTML 404(非 JSON)且不泄漏记录;非审计者 /internal/audit HTML 404;未审批记录的 /public/s/:id HTML 404;撤回后公开页与文章同时消失;未知或跨平面 ?theme= 静默降级;页面不含 script / inline handler / javascript: | ✅ 匿名 / reader / maintainer / human auditor 四种身份在公开页与审计面上结果不同 | ✅ 撤回与拒绝后公开面立即不可达且不脏写;主题解析失败不改目录、不返回 500 | tests/portal_ui_test.ts;tests/portal_theme_test.ts;tests/e2e/portal_ui_e2e_test.ts |
| Access Control 分级权限 | 高 | ✅ 审计者授予 Agent 维护者后,维护者 register、只读者 list 同一条 | ✅ 维护者自封 auditor FORBIDDEN;Agent 不能被授予 auditor;未知身份不能 register | ✅ 人类审计者 vs Agent 维护者;只读者不能 list 名册 | ✅ 失败 grant 不改 identities/grants;失败 approve 不改公开面 | tests/access_service_test.ts;tests/e2e/cli_access_e2e_test.ts |
| 身份名册只读查询 | 高 | ✅ 同一维护者经 CLI identity list、Portal GET /api/identities 与 MCP portico_identities 得到同一批 {id,kind,role}(有邮箱时含 email)与同一顺序;人类审计者看到同一载荷 | ✅ 只读者与匿名 FORBIDDEN;载荷不含 secretHash / token / 会话令牌;磁盘上的未知字段不会出现在列表里 | ✅ 维护者/人类审计者 vs 只读/匿名 | ✅ 读名册不改 identities.json 与 catalog 文件字节;Portal / MCP 只读入口无 --allow-write | tests/access_service_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/identity_roster_e2e_test.ts |
| 授权轨迹只读查询 | 高 | ✅ 同一人类审计者经 CLI identity grants、Portal GET /api/grants 与 MCP portico_grants 得到同一批追加式授权记录与同一顺序 | ✅ 维护者/只读/匿名 FORBIDDEN;Portal POST /api/grants 405;载荷不含凭证或会话令牌 | ✅ 人类审计者 vs 维护者/只读/匿名 | ✅ 读授权轨迹不改 identities.json 与 catalog 文件字节;失败 POST 不追加 grant;Portal / MCP 只读入口无 --allow-write | tests/access_service_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/identity_grants_e2e_test.ts |
| 身份撤回轨迹只读查询 | 高 | ✅ 同一人类审计者经 CLI identity revokes、Portal GET /api/revokes 与 MCP portico_revokes 得到同一批追加式撤回记录与同一顺序 | ✅ 维护者/只读/匿名 FORBIDDEN;Portal POST /api/revokes 405;载荷不含凭证或会话令牌;磁盘未知字段不出现在列表 | ✅ 人类审计者 vs 维护者/只读/匿名 | ✅ 读撤回轨迹不改 identities.json 与 catalog 文件字节;失败 POST 不追加 revoke;Portal / MCP 只读入口无 --allow-write | tests/access_revoke_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/identity_revokes_e2e_test.ts |
| 当前会话身份只读查询 | 高 | ✅ 同一已登录身份经 CLI identity whoami、Portal GET /api/whoami 与 MCP portico_whoami 得到同一 {id,kind,role} | ✅ 匿名 Portal / MCP FORBIDDEN;CLI 缺会话 USAGE;Portal POST /api/whoami 405;载荷不含邮箱、凭证或会话令牌 | ✅ 已登录(审计者/维护者/只读)vs 匿名 | ✅ 读 whoami 不改 identities.json 与 catalog 文件字节;失败 POST 不写名册;Portal / MCP 只读入口无 --allow-write | tests/access_service_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/identity_whoami_e2e_test.ts |
| 登录会话只读查询 | 高 | ✅ 同一人类审计者经 CLI identity sessions、Portal GET /api/sessions 与 MCP portico_sessions 得到同一批会话轨迹(id / subjectId / createdAt / expiresAt,作废则含 revokedAt)与同一顺序 | ✅ 维护者/只读/匿名 FORBIDDEN;CLI 缺 --sessions USAGE;Portal POST /api/sessions 405;载荷不含令牌或哈希 | ✅ 人类审计者 vs 维护者/只读/匿名 | ✅ 读会话轨迹不改 sessions.json、identities.json 与 catalog 文件字节;失败 POST 不签发/不作废会话;Portal / MCP 只读入口无 --allow-write | tests/access_session_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/identity_sessions_e2e_test.ts |
| 登录凭证只读查询 | 高 | ✅ 同一人类审计者经 CLI identity credentials、Portal GET /api/credentials 与 MCP portico_credentials 得到同一批凭证轨迹(id / subjectId / credentialRef / issuedBy / issuedAt,作废则含 revokedAt)与同一顺序 | ✅ 维护者/只读/匿名 FORBIDDEN;CLI 缺 --sessions USAGE;Portal POST /api/credentials 405;载荷不含令牌或哈希 | ✅ 人类审计者 vs 维护者/只读/匿名 | ✅ 读凭证轨迹不改 sessions.json、identities.json 与 catalog 文件字节;失败 POST 不签发/不作废凭证;Portal / MCP 只读入口无 --allow-write | tests/access_session_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/identity_credentials_e2e_test.ts |
| 登录凭证作废轨迹只读查询 | 高 | ✅ 同一人类审计者经 CLI identity credential revokes、Portal GET /api/credential-revokes 与 MCP portico_credential_revokes 得到同一批追加式作废记录与同一顺序 | ✅ 维护者/只读/匿名 FORBIDDEN;Portal POST /api/credential-revokes 405;载荷不含凭证或会话令牌;磁盘未知字段不出现在列表 | ✅ 人类审计者 vs 维护者/只读/匿名 | ✅ 读作废轨迹不改 identities.json、sessions.json 与 catalog 文件字节;失败 POST 不追加 credential revoke;Portal / MCP 只读入口无 --allow-write | tests/access_credential_revoke_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/identity_credential_revokes_e2e_test.ts |
| 名册可选邮箱 | 高 | ✅ 人类审计者 identity grant --email 给人类身份绑定唯一地址;同一维护者经 CLI identity list、Portal GET /api/identities 与 MCP portico_identities 看到同一规范化 email | ✅ Agent 带 email INVALID_INPUT;非法或重复邮箱 INVALID_INPUT;只读/匿名仍 FORBIDDEN;密钥字段被拒 | ✅ 人类审计者可写 vs 维护者只读 vs 只读/匿名不可见名册 | ✅ 失败 grant 不改 identities 文件字节;省略 email 的再次 grant 保留已绑定地址 | tests/access_service_test.ts;tests/e2e/identity_roster_e2e_test.ts |
| 身份授权撤回 | 高 | ✅ 人类审计者 identity revoke --id 后,被撤维护者不能再 register;只读者仍见其留下的目录记录;audit list 出现 revoke | ✅ 维护者/Agent/只读 FORBIDDEN;自撤 FORBIDDEN;最后一位人类审计者 INVALID_STATE;未知或重复撤回 NOT_FOUND;密钥字段被拒 | ✅ 人类审计者 vs 维护者;被撤主体 vs 仍在名册的只读者 | ✅ 失败撤回不改 identities 文件字节、不追加 revoke、不作废会话/凭证;成功撤回后原 session 立即 FORBIDDEN | tests/access_revoke_test.ts;tests/audit_service_test.ts;tests/e2e/cli_identity_revoke_e2e_test.ts |
| 登录会话 | 高 | ✅ 审计者一次性下发凭证;主体 login 后 CLI --session list 与 Portal 会话头看到同一条内部记录 | ✅ 错误 token 登录 FORBIDDEN;会话上伪造 auditor 头 FORBIDDEN;无效 session 不能 approve | ✅ 只读 session 不能 register;维护者 session 不能审公开 | ✅ 失败登录不写 session 记录;logout 后原令牌不可用且不改 catalog | tests/access_session_test.ts;tests/e2e/cli_session_e2e_test.ts;tests/e2e/portal_session_e2e_test.ts |
| Portal Cloudflare Access JWT 映射 | 高 | ✅ 显式启用后,合法 RS256 JWT(aud/iss/exp 通过)且名册 email 命中时,Portal /api/catalog 与 /internal 与该人类会话看到同一内部记录 | ✅ 伪造签名、过期、AUD 不匹配、名册未登记、明文 Cf-Access-Authenticated-User-Email 头全部匿名(/internal HTML 404);未启用时 JWT 被忽略;Gateway 不接受该 JWT | ✅ 映射后的只读者不能读 /api/audit;匿名/失败 JWT 看不到内部记录 | ✅ 成功或失败都不写 identities/sessions 文件字节;不签发 Portico 会话 | tests/access_cf_access_test.ts;tests/portal_handler_test.ts;tests/gateway_handler_test.ts;tests/e2e/portal_cf_access_e2e_test.ts |
| 身份证明与信任根 | 高 | ✅ 会话可完成各自角色的操作:审计者 approve、维护者 register、只读者 list 同一条;空名册 bootstrap 签发首张凭证后 login 成功 | ✅ --actor-* 单独出现被拒且不写文件;Portal / MCP 伪造 X-Portico-Actor-* 只得到匿名视图(审计面 403);非审计者会话 approve FORBIDDEN;首个凭证签发后再次无会话 credential issue FORBIDDEN;非人类审计者 bootstrap FORBIDDEN | ✅ 匿名 / 只读 / 维护者 / 人类审计者四种会话;伪造头 vs 真会话 | ✅ 被拒的冒名不写 identities/catalog/approvals;bootstrap 被拒不改 sessions 文件 | tests/e2e/cli_access_e2e_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/portal_session_e2e_test.ts;tests/e2e/cli_session_e2e_test.ts |
| 登录凭证作废 | 高 | ✅ 人类审计者 identity credential revoke --id 后,原 login token 与 --session / Portal 会话头立即 FORBIDDEN;名册仍有该身份,重新 credential issue + login 后仍能 list 同一条内部记录;audit list 出现 revoke_credential | ✅ 维护者/只读/匿名 FORBIDDEN;未知主体 NOT_FOUND;无活动凭证/会话与重复作废 INVALID_STATE;密钥字段被拒 | ✅ 人类审计者 vs 维护者;被作废主体 vs 重新签发后的同一主体 | ✅ 审计记录先写、失效后做:commitCredentialRevoke 失败则 sessions/credentials 文件字节不变;部分失败可重试补齐;成功后可重新 issue 新凭证 | tests/access_credential_revoke_test.ts;tests/audit_service_test.ts;tests/e2e/cli_credential_revoke_e2e_test.ts |
| CLI 发布与查询 | 高 | ✅ identity grant 后 catalog register,reader list/get;draft→publish internal 后 reader 可见;approve 后匿名可见 | ✅ reader 登记/draft/publish FORBIDDEN;公开登记 PUBLIC_REQUIRES_APPROVAL;公开 publish 后匿名 list 为空;自批/维护者 approve 失败;未授权身份 FORBIDDEN | ✅ 维护者 vs 只读 vs 人类审计者;匿名看不到内部、待审与审批记录 | ✅ 失败不创建/不改 catalog 文件、approvals 与 identities | tests/e2e/cli_catalog_e2e_test.ts;tests/e2e/cli_publish_e2e_test.ts;tests/e2e/cli_approval_e2e_test.ts;tests/e2e/cli_access_e2e_test.ts |
| MCP 渠道登记与访问 | 高 | ✅ 维护者登记 mcp_endpoint;只读者 mcp list/describe 与 Portal /api/mcp 同一连接信息 | ✅ 密钥查询/userinfo/命令式入口被拒;CLI 表面不出现在 MCP 列表;匿名看不到内部 MCP;未审批公开 MCP 对匿名不可达 | ✅ 只读 vs 匿名;维护者可登记、只读者可描述 | ✅ 失败登记不写 catalog 文件;Portal POST /api/mcp 不改目录 | tests/mcp_channel_test.ts;tests/e2e/cli_mcp_e2e_test.ts;tests/e2e/portal_mcp_e2e_test.ts;tests/portal_handler_test.ts |
| MCP 协议入口(只读治理发现) | 高 | ✅ 同一身份经 CLI catalog list、Portal GET /api/catalog 与 MCP portico_list 得到同一批记录与同一顺序;匿名经 Portal 与 MCP 都只看到 approved_public 且严格少于只读者 | ✅ 伪造 X-Portico-Actor-* 头不产生审计者身份(portico_audit 仍 FORBIDDEN);非审计者 portico_audit FORBIDDEN;未知方法 -32601;未知工具 -32602;批量请求 -32600;非法 channel/state 过滤 INVALID_INPUT;非 POST 405 | ✅ 匿名 vs 只读会话;人类审计者 vs 其他角色 | ✅ 工具失败只回 in-band {ok:false,error},不改目录、不追加审计;MCP 进程无 --allow-write;up 停止后端口关闭 | tests/mcp_protocol_test.ts;tests/e2e/mcp_http_e2e_test.ts;tests/e2e/entrypoint_boot_e2e_test.ts;tests/e2e/system_up_e2e_test.ts |
| Web 渠道登记与已授权入口 | 高 | ✅ 维护者登记 url;只读者 web list/describe 与 Portal /api/web、HTML 同一 href(connect.mode=direct) | ✅ 密钥查询/userinfo/javascript: 入口被拒;指向 Portico 自身 /s/:id 或 /public/s/:id 阅读页的 url 被拒;CLI 表面不出现在 Web 列表;匿名看不到内部 Web;未审批公开 Web 对匿名不可达 | ✅ 只读 vs 匿名;维护者可登记、只读者可描述 | ✅ 失败登记不写 catalog 文件;失败更新不改目录文件字节;Portal POST /api/web 不改目录 | tests/web_channel_test.ts;tests/catalog_update_test.ts;tests/e2e/cli_web_e2e_test.ts;tests/e2e/portal_web_e2e_test.ts;tests/portal_handler_test.ts |
| CLI 渠道登记与已授权包坐标 | 高 | ✅ 维护者登记 package;只读者 cli list/describe 与 Portal /api/cli、HTML 同一 jsr:/npm: 坐标(connect.mode=coordinate) | ✅ 命令式/npx/URL/未知 registry 被拒;MCP/Web 表面不出现在 CLI 列表;匿名看不到内部包坐标;未审批公开 CLI 对匿名不可达 | ✅ 只读 vs 匿名;维护者可登记、只读者可描述 | ✅ 失败登记不写 catalog 文件;Portal POST /api/cli 不改目录 | tests/cli_channel_test.ts;tests/e2e/cli_package_e2e_test.ts;tests/e2e/portal_cli_e2e_test.ts;tests/portal_handler_test.ts |
| MCP Gateway 鉴权与路由 | 高 | ✅ 维护者登记 MCP 后,只读者 gateway authorize 与 HTTP POST /gateway/mcp/:id/authorize 得到同一 connect.mode=direct 路由;审计者可读到 allowed 记录 | ✅ 匿名内部/待审公开 NOT_FOUND 且不泄漏端点;CLI 表面不可授权;tools/call 返回 405 且不执行 | ✅ 已授权 reader vs 匿名;维护者不能读审计 | ✅ 失败授权不改 catalog 文件;拒绝工具调用不脏写目录 | tests/gateway_service_test.ts;tests/gateway_handler_test.ts;tests/e2e/cli_gateway_e2e_test.ts;tests/e2e/gateway_http_e2e_test.ts |
| Gateway 访问审计只读查询 | 高 | ✅ 同一人类审计者经 CLI gateway audit、Portal GET /api/gateway-audit 与 MCP portico_gateway_audit 得到同一批允许/拒绝记录与同一顺序 | ✅ 维护者/只读/匿名 FORBIDDEN;Portal POST /api/gateway-audit 405;载荷不含凭证或会话令牌;未配置 Gateway 时审计者得到空列表且其他人仍 FORBIDDEN | ✅ 人类审计者 vs 维护者/只读/匿名 | ✅ 读访问审计不改 catalog 文件与 gateway-audit 文件字节;失败 POST 不追加授权记录;Portal / MCP 只读入口无 --allow-write | tests/gateway_service_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/gateway_audit_e2e_test.ts |
| UI Components 门户页维护 | 中 | ✅ 维护者 page set 组合 catalog_card;同一只读者经 CLI page get、Portal GET /api/page 与 MCP portico_page 看到同一张卡与同一顺序 | ✅ 未知种类/HTML/密钥字段被拒;内部卡对匿名不可见;待审公开仍不可达;Portal POST /api/page 405 | ✅ 维护者 vs 只读;匿名看不到未审批引用 | ✅ 失败 set 不写 page 文件;读 page 不改 page/catalog 字节;Portal POST /api/page 405 且不改 page/catalog;Portal / MCP 只读入口无 --allow-write | tests/ui_page_test.ts;tests/portal_page_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/cli_page_e2e_test.ts;tests/e2e/portal_page_e2e_test.ts;tests/e2e/page_get_e2e_test.ts |
| Agent 维护与人类安全审计 | 高 | ✅ 维护者登记并提交公开后,人类审计者 audit list、Portal GET /api/audit 与 MCP portico_audit 看到同一条目录变更、授权、撤回与审批时间线;配置 PORTICO_GATEWAY_AUDIT_PATH 后三处都并入 Gateway 访问事件 | ✅ 维护者/只读/匿名 FORBIDDEN;Portal PATCH/POST /api/audit 405;失败公开登记不出现 catalog 事件 | ✅ 维护 Agent vs 人类审计者;维护者不能读、不能改写 | ✅ 失败登记不写 catalog 文件与变更日志;读审计不改授权/审批记录 | tests/catalog_change_test.ts;tests/audit_service_test.ts;tests/portal_handler_test.ts;tests/e2e/cli_audit_e2e_test.ts;tests/e2e/portal_audit_e2e_test.ts |
| 审计时间线过滤查询 | 高 | ✅ 同一人类审计者经 CLI audit list --kind/--q、Portal GET /api/audit?kind=&q= 与 MCP portico_audit 得到同一批过滤结果与同一顺序;/internal/audit 是无脚本 GET 表单,只展示匹配事件 | ✅ 维护者/只读/匿名带过滤器仍 FORBIDDEN;q 不搜索入口 URL 或包坐标;非法 kind / 过长 q / 控制字符 INVALID_INPUT | ✅ 人类审计者 vs 维护者/只读/匿名 | ✅ 非法查询不改 catalog 文件字节;Portal / MCP 只读入口无 --allow-write | tests/audit_query_test.ts;tests/portal_handler_test.ts;tests/mcp_protocol_test.ts;tests/e2e/audit_query_e2e_test.ts |
缺口的最低期望:每行至少先有一条跨入口的 Happy Path(发布或发现能在 CLI 与 Portal 对上);所有高风险行必须再有失败路径(未审批公开、越权、自批);权限行必须打两种身份;写操作必须证明失败后公开面与目录不被脏写。
三个入口各有进程级启动烟测(tests/e2e/entrypoint_boot_e2e_test.ts):spawn 真实的 src/portal/main.ts / src/gateway/main.ts / src/cli/main.ts,读它播报的那行 JSON,再打一次请求。up 整机起动另有系统级 E2E(tests/e2e/system_up_e2e_test.ts):空数据目录 → up → 治理动作 → 重启 → 断言状态仍在,并断言停下后端口真的关闭。这两条覆盖的是产物进程本身,不是直接 import 的 listen* 函数——src/gateway/main.ts 曾经在 275 个测试全绿的情况下完全无法启动,原因就是当时没有任何用例执行过它。
Portico UI 规范
本文件是 Portal 页面层的设计契约。代码是事实来源;本规范与代码冲突时,以代码为准并同步修正本文。
实现位置:src/portal/design/。
适用范围: 只约束「人看的页面」——内部笔记台与公开发布页。 不约束
GET /api/*的 JSON 契约,也不约束 CLI / MCP 输出。
0. 两条不该被 UI 推翻的铁律
- 公开页只渲染已过审批的记录。
CatalogService.list对 reader / maintainer 会返回internal与pending_public记录;公开页必须在视图层再筛一次(publicOnly())。内部身份不得把公开页 当作绕过审批的展示窗。 - 维护者不能改界面。 主题是固定预设,不是可配置字段;页面没有 body / HTML / 主题输入。
组件盒仍只有
catalog_card、catalog_detail、permission_hint、approval_status、audit_snippet。
1. 两个平面(tone)
同一套 token 词汇,两种气质。差别只在密度、排版与强调色,不在语义。
internal 内部笔记台 | public 公开发布 | |
|---|---|---|
| 路由 | /internal、/internal/c、/internal/audit、/internal/s/:id | /public、/public/t/:channel、/public/s/:id |
| 读者 | 只读及以上(匿名 404) | 任何人(含匿名) |
| 可见记录 | 当前身份可见的全部(含草稿、待审、已拒绝) | 仅 approved_public |
| 布局 | 三栏器物面板:导航栏 + 记录列表 + 阅读区 | 报头 + 头条 + 分栏 + 右栏 |
| 字体 | 全无衬线 + 等宽数字 | 衬线标题 + 无衬线正文 + 等宽元数据 |
| 强调色 | 青绿(治理、工具) | 深绛红(出版物、署名) |
| 语气 | 陈述治理事实、给状态、给路径 | 陈述它是什么、怎么访问、为何公开 |
/(根路径)是社论杂志风发现索引(渠道过滤、明暗 data-theme、/s/:id 阅读栏),不是两个新平面的替代品。它映射同一目录,不是 CMS。跨面只做只读发现:杂志壳链到 /public;已登录身份才看到 /internal;公开发布面链回 /,公开详情可链到同一条 /s/:id。匿名访问 /internal 仍是 404。
2. 颜色主题
2.1 模式解析
模式是纯 CSS 属性,不需要脚本——Portal 的 CSP 是 default-src 'none',需要脚本才能生效的主题
等于不能交付的主题。
优先级(resolveTheme()):
?theme=<preset>显式指定,且 preset 的 tone 必须与页面 tone 一致;- 非法 / 跨 tone 的值静默降级,不报错——读者拼错 URL 不该得到 500;
Sec-CH-Prefers-Color-Scheme/X-Portico-Prefers-Color-Scheme请求头提示;- 落到该 tone 的默认预设(浅色)。
页头「浅色 / 深色」是链接(?theme=...),不是按钮:没有脚本时,唯一诚实的选择控件是导航,
而且链接可收藏、可被缓存正确对待。
2.2 预设
| preset | tone | mode |
|---|---|---|
portico-internal-light | internal | light |
portico-internal-dark | internal | dark |
portico-editorial-light | public | light |
portico-editorial-dark | public | dark |
URL 短名同样被接受:internal、internal-dark、editorial、editorial-dark、public、public-dark、
light、dark。短名是 URL 契约的一部分,改动需视为破坏性变更。
2.3 基础 token
命名规则:--tk-<语义>。布局尺寸与颜色共用 --tk- 命名空间,因此 palette 字段名不得等于任何
布局 token 名(见 §6 回归测试)。
| 类别 | token |
|---|---|
| 画布与表面 | canvas surface raised sunken |
| 线 | border borderStrong rule |
| 文字 | ink muted faint |
| 强调 | accent accentInk accentSoft accentLine accentOnFill |
| 侧栏 | panel panelBorder panelInk panelMuted panelActive |
| 报头 | mastheadInk mastheadMuted |
| 阴影 | --tk-shadow --tk-shadow-lift |
| 字体 | --tk-font-app --tk-font-body --tk-font-display --tk-font-mono --tk-font-num |
| 圆角 | --tk-r-xs … --tk-r-xl、--tk-r-pill |
| 间距 | --tk-s1(4) --tk-s2(8) --tk-s3(12) --tk-s4(16) --tk-s5(20) --tk-s6(24) --tk-s7(32) --tk-s8(40) --tk-s9(56) --tk-s10(80) |
| 尺寸 | --tk-shell(1440) --tk-read(42rem) --tk-list(336px) --tk-header-h(56px) |
| 布局 | --tk-rail(232px) — 仅布局宽度,不是颜色 |
2.4 治理状态语义色
状态色是语义而非装饰:待审公开 在两个平面里都是同一个琥珀色。draft 与 rejected 刻意保持
中性——一个灰掉的 chip 不能看起来和「已公开」一样确定。
用法:容器上加 data-state="state-<name>",自动获得 --tk-state / --tk-state-soft /
--tk-state-line。token 名与状态名一一对应,stateAttr() 是唯一生成入口。
| 状态 | token | 含义 |
|---|---|---|
draft | state-draft | 仅维护者可见 |
internal | state-internal | 内部可达 |
pending_public | state-pending | 待独立审批(chip 用虚线边) |
approved_public | state-public | 已公开可达 |
rejected | state-rejected | 惰性,公开面不可达 |
3. 间距、字体与密度
- 基础字号 15px,行高 1.6;正文行长上限
--tk-read(42rem)。 - 页面栅格上限
--tk-shell(1440px)。 - 字体栈只用系统字体:CSP 不允许引用远程字体,也不嵌入 base64 字体。
- 正文 / 界面:
ui-sans-serif+ PingFang SC / Noto Sans CJK SC 回退 - 公开展示字体:
Iowan Old Style/ Palatino / Georgia + Songti SC / Noto Serif CJK SC 回退 - 等宽:
ui-monospace/ SF Mono / JetBrains Mono
- 正文 / 界面:
- 所有数字(计数、日期、版本)用
--tk-font-num+tabular-nums,避免列表跳动。 - 断点:1080px(收起右栏 / 列表栏)、1180px(三栏 → 两栏)、900px(收起导航栏)、820px(报头导航隐藏)、720px(定义列表单列)。
prefers-reduced-motion: reduce时全部过渡降到 0.001ms。
4. 组件契约
公共组件在 src/portal/design/components.ts,两平面共用;平面专属在 views/internal.ts 与 views/public.ts。
| 组件 | 类名 | 规则 |
|---|---|---|
| 状态 chip | .tk-chip--state | 必须来自 stateChip(),不手写颜色 |
| 渠道 chip | .tk-chip--channel | 只用 cli / mcp / web,显示为大写缩写 |
| 面板 | .tk-panel | 唯一的内容容器;不引入第二种卡片风格 |
| 缩略图 | .int-thumb / .pub-art | 由 id 哈希决定图案,禁用远程资源 |
| 空态 | .tk-empty | 必须同时给标题与下一步提示 |
| 边界提示 | .tk-note--boundary | 任何可能被误认为 runtime 的页面必须出现 |
| 键值表 | .tk-dl | 标签在左、值在右,窄屏单列 |
| 时间线 | .tk-timeline | 仅用于审计与近期轨迹 |
4.1 生成式图形
缩略图与插图由 hash(id) % N 选择 CSS 图案 + 渠道字形,不使用图片、不引用网络、不接受
维护者输入。CSP 里只放开了 img-src data:,这条约束保证它长期成立。
4.2 搜索框
.tk-search 是 aria-hidden 的装饰性占位,不是输入控件。Portal 是只读入口且不发送脚本,
渲染一个假的可用搜索框会是对用户的误导。真正可用的检索入口是 /api/catalog 与 CLI。
5. 无障碍
- 正文/画布对比度 ≥ 7:1;次要文字 ≥ 4.5:1;强调色 ≥ 4.5:1。由
auditContrast()断言。 - 焦点环统一为
:focus-visible+--tk-accent,偏移 2px。 - 语义标签:
<header><nav aria-label><main><aside><footer>;当前项用aria-current。 - 装饰性图形
aria-hidden="true";图标不承载语义。 - 面包屑、筛选组、主题切换均有可读标签。
- 打印样式:公开页隐藏报头、右栏与主题切换,图形不打印。
6. 安全与不变量(回归测试覆盖)
| 不变量 | 测试 |
|---|---|
公开页不出现 internal / draft / pending_public 记录,即使请求者是维护者 | tests/portal_ui_test.ts |
| 未审批记录的文章页返回 404 | tests/portal_ui_test.ts |
匿名访问 /internal* 返回 HTML 404,不泄漏 403,不返回 JSON 信封 | tests/portal_ui_test.ts;tests/e2e/portal_ui_e2e_test.ts |
| 非审计者的审计页不出现在导航,也不出现在 HTML | tests/portal_ui_test.ts |
| 所有 token 对比度达标 | tests/portal_theme_test.ts |
布局 token 与 palette 名不冲突(--tk-rail 必须仍是长度) | tests/portal_theme_test.ts |
页面不含 <script>;CSP 保持 default-src 'none' | tests/portal_ui_test.ts |
| 名称、描述、入口转义后输出 | tests/portal_ui_test.ts |
| 包坐标不渲染成可下载链接 | tests/portal_ui_test.ts |
7. 修改 UI 的检查清单
- 新颜色只能进 palette,作为语义 token;不得在模板里写十六进制色值。
- 新 token 名不得与任何布局尺寸 token 重名(跑
tests/portal_theme_test.ts)。 - 新页面若在两个平面都出现,差异必须落在 tone,不得复制两份语义。
- 公开面新增任何内容前,先问「它是否已过审批边界」。
- 不引入脚本、远程字体、远程图片、iframe。
- 同步更新本文与
docs/roadmap.md验收矩阵。