Repository navigation
Conversation
…nds/default
可插拔前端平台的第一阶段:搭出正确的依赖方向,且**不改动任何 UI/UX**。
依赖方向(唯一允许):
Backend → @floatctf/sdk → @floatctf/react → Frontend
↘ (非 React 前端直接用 sdk)
@floatctf/frontend-runtime = 制品/运行时契约(框架无关)
新增包:
- packages/frontend-runtime:frontend.json 严格校验(schemaVersion/安全 ID/semver/
相对路径/路径穿越/兼容性)、registry.json 校验与**显式 currentVersion** 解析、
mount(context) 契约、bootstrapFrontend(同源动态 import + 回退 default + 内置兜底页)。
契约版本独立:FRONTEND_RUNTIME_VERSION / API_CONTRACT_VERSION / registry schema。
- packages/sdk:createFloatCTFClient(DI:getUserToken/getAdminToken/onUnauthorized,
不再 import router 与 AuthStore)、FloatCTFError(HTTP 状态/平台码/平台文案/original,
并保留 axios 兼容的 response 形态以免改动任何页面错误处理)、全部领域 API 客户端、
fetch-based SSE(Bearer 走 Authorization 头,行为逐字保留)、生成实体(@floatctf/sdk/entity)
与从路由页面反向 import 的 DTO 类型。
- packages/react:可选 headless React 绑定(query 工厂、SSE hook、失效工具)。
无 Primer/Tailwind/路由/页面/JSX(非测试目录无 .tsx)。
迁移:
- apps/web/src/** → frontends/default/src/**(286 文件,git mv 保留历史)。
UUID 路由、布局、Primer 样式、AWD/AWDP UX、Zustand 登录态(含 localStorage)
全部原样保留;页面代码仅 import 路径变化。
- frontends/default 自带 Vite(dev HMR:mise run dev:web)+ 生产 lib 构建
(base "./"、确定性入口 assets/frontend.js、单文件 CSS、构建期用真实校验器
校验生成的 frontend.json)。
- apps/web 变成 bootstrap:14KB / gzip 5KB,无 React、无 Primer、无路由;
只做 ?frontend= 破窗 + bootstrapFrontend()。
- 契约常量落到 apps/api/src/core/contract.rs(API/运行时 major + 能力白名单),
与 packages/frontend-runtime 的声明由 scripts/check-architecture.sh 断言一致。
门禁:新增 scripts/check-architecture.sh(SDK 不得依赖 React/路由/状态库/Primer;
react 包必须 headless;默认前端不得逃出自身包;bootstrap 产物不得含默认前端),
接入 mise run lint。测试:frontends/default 173、sdk 51(含 SSE 33 迁移)、react 10、
frontend-runtime 66,全部通过;cargo check 通过。
可插拔前端的后端面**刻意只有两件事**:当前生效的前端标识 + 公开引导元数据。 - 新增 `GET /api/frontend`(未认证,登录前必须可用——登录 UX 属于所选前端): 返回 active_frontend / platform_version / api_contract_version / frontend_runtime_version / capabilities。**不含**任意设置项、密钥、文件系统路径、 注册表内容或资产路径。非法 FRONTEND_ACTIVE 一律回落 `default`(后端不把未校验的 设置值当"前端 ID"传播)。 - 新增 `core/contract.rs`:API 契约 major = 1、前端运行时契约 major = 1、真实稳定的 能力白名单。与 packages/frontend-runtime 的声明由 scripts/check-architecture.sh 断言一致,防止漂移。版本策略:加法式 API 变更留在当前 major,破坏性变更才 +1。 - `FRONTEND_ACTIVE` 进入既有动态设置体系(**不是**进程静态 TOML、**不是**环境变量): seed 默认 `default` 且 `protected = true`(管理端可编辑、不可删除——删掉会静默回落 default,看起来像自定义前端自己消失了)。无 schema 变更。 - seed 结构补 `protected` 字段,其余键保持 false(零行为变化)。 - 写入校验:create/patch 时若 key 为 FRONTEND_ACTIVE,值必须是安全前端 ID,否则 400。 后端**没有**新增路由/页面/布局概念(无 loginPage/sidebar/routePrefix);注册表与安装 仍属文件系统与 CLI 的职责。 实测(隔离库 floatctf_ui_e2e,独立端口 9090,与生产 practice judge 容器期望值对齐以避免 宿主资源争抢):GET /api/frontend → 200 且字段正确;PATCH 安全值 200 并即时反映; PATCH `../../etc/passwd` → 400;DELETE → 400(protected);还原 default 成功。 单测:core::contract 2、frontend::domain 3、infrastructure::settings 10 全绿。
前端安装与生命周期(不在浏览器里做任何安装动作):
- `scripts/frontend.sh`(生产安装为 `$FLOATCTF_HOME/frontend.sh`):
list / info / verify / install(本地目录、Git URL、预构建制品)/ remove / set-current。
- 源码安装一律在**隔离 Docker 构建容器**内完成(cap-drop ALL、no-new-privileges、
只读源码挂载 + 临时产物挂载、无 Docker socket、不共享宿主 PID/网络命名空间),
按 lockfile 识别 pnpm/npm/yarn,只执行约定的 `build` 脚本(不从 JSON 取任意命令)。
- 归档解包前逐条校验成员:拒绝绝对路径、`..`、符号链接/硬链接与特殊文件类型。
- manifest 预检(安全 ID / semver / 相对路径 / 契约 major / entry+styles 真实存在);
权威校验在 @floatctf/frontend-runtime(浏览器每次加载都会重验,失败即回退)。
- 注册表更新原子(tmp+fsync+rename,flock 串行化),**拒绝覆盖**同 ID 同版本;
资产按 `<id>/<version>/` 版本化且全局只读;currentVersion 是**显式指针**,
升级=装新版本+移指针,回滚=装回旧版本+移指针,绝不按文件名排序挑版本。
- `default` 由平台发布并受保护:常规管理不能覆盖/删除(install.sh 用内部 `--platform`)。
同版本同内容=幂等跳过;同版本不同内容需显式 `--platform --reinstall` 原子替换。
- **不修改 FRONTEND_ACTIVE**:激活是应用设置(管理端操作)。
- `web-dist.tar.gz` 布局契约化为 `bootstrap/` + `frontends/default/<version>/`,
由 `scripts/package-web-dist.sh` 组装(确定性归档、不含源码/依赖),
`scripts/verify-release-frontend.sh` 断言布局、manifest/entry/styles、无源码残留、
归档成员安全与脚本语法,并用真实 runtime 校验器再验 manifest。
- `scripts/install.sh`:新增第 5 个 release 产物 frontend.sh(支持同样的 URL/版本覆盖约定);
bootstrap 铺到 `$FLOATCTF_HOME/web`;release 内前端经 frontend.sh 安装(default 会移动
current 指针),**保留第三方前端、其版本与其指针**;FRONTEND_ACTIVE 不受影响。
python3 列入宿主前置依赖(前端管理器的 JSON 依赖)。
- 卸载语义:SAFE 保留 `frontends/`(已安装前端 + registry.json)与 `frontend.sh`
(重新部署可恢复同一套前端集合),并更新用户可见提示;PURGE 明确删除前端与注册表。
- Caddy(生产内嵌模板 + 开发配置):`/__floatctf/frontends/*` 同源提供版本化资产
(长 immutable 缓存),`registry.json` 强制 `no-store`,隐藏内部文件;
`/api/*` 仍先匹配不被遮蔽,任意 SPA 深路由仍回退 bootstrap index.html。
- `scripts/clean.sh`:纳入 packages/*/dist、frontends/default/dist、.frontend-pack;
明确**不触碰** /var/lib/floatctf 与已安装前端。
- CI:web 作业改为构建 workspace 包 → 架构边界门禁 → 五个包 lint/test →
bootstrap+Default Frontend 构建 → 打包并验证发布制品。
- release:发布 5 个产物(新增 frontend.sh),前端产物按新布局打包并验证。
实测(真实脚本文本,非重写):首次安装 / 幂等重部署 / 第三方前端共存且升级不动它 /
注册表通过权威解析器;恶意归档(穿越、绝对路径、符号链接)与非法 manifest
(不安全 ID、跨契约、缺 entry、穿越 entry)全部拒绝;default 保护与第三方 --reinstall 拒绝。
管理端 → 设置 页面顶部新增前端区块(沿用既有 Primer 组件与 MsgBanner 模式):
- **只列出本地注册表里已安装的前端**(GET /__floatctf/frontends/registry.json,同源静态
文件,no-store),并用 `@floatctf/frontend-runtime` 的 `parseRegistry` 解析 —— 与
bootstrap 用的是**同一个权威校验器**,因此"页面能选"与"浏览器能加载"不会分叉。
- 读取既有动态设置 `FRONTEND_ACTIVE`(认证设置接口),选择后用既有 `PATCH /settings/{id}`
保存;保存成功刷新 ["Settings"] 缓存并提示"刷新页面生效"(router 属于前端自己,
运行中热替换没有意义)。
- 展示每个候选的 id / 名称 / 版本 / 安装时间 / 入口 / 契约兼容性与"平台内置"标记;
契约不兼容的候选在下拉里**禁用并说明原因**(避免选出一个必然回退 default 的前端)。
- 失败即真实错误态:注册表读不到/不合法、设置接口失败都打印真实错误文案,不造假数据。
- 文案明确写出破窗恢复路径 `?frontend=default`(只影响当前加载、不改设置、不需登录)。
- **不提供**任何安装能力:没有 Git URL 输入、没有构建、没有上传、没有
"让后端去 clone"的入口 —— 安装是运维 CLI(frontend.sh)的职责。
- 全程无原生 `alert(`/`confirm(`(沿用 useMsgBanner / Primer 组件)。
frontends/default tsc --noEmit 通过;biome lint 无新增问题(5 条为既有 suppression 警告)。
默认前端在生产形态下整页无法加载,bootstrap 兜底页如实报出 `default: process is not defined`。根因是 Vite **lib 模式**刻意不替换第三方 依赖(styled-components / mermaid 等)里的 `process.env.*`(默认假设消费者 会处理),而浏览器里根本没有 `process`;旧的 apps/web 是普通 app 构建, 因此从未暴露。 修复: - `frontends/default/vite.config.ts` 在构建时显式替换 `process.env.NODE_ENV` 与 `process.env`;替换**按 VITEST 分支**,避免测试环境里的 React 被解析成 production 构建(否则 `React.act` 不存在、组件测试全挂)。 - 新增 `frontends/default/src/node-shim.ts` 并在 entry 首位 import,兜底漏网的 `process.*` 引用。副作用是共享 chunk 5.03MB → 4.32MB(gzip 1.23 → 1.08MB)。 - 顺带修掉 FrontendSelector 文案里漏出的 markdown `**`。 迁移后门禁对齐(移动文件必须同步引用这些路径的门禁): - 三个新包各加 biome.json(tab + double quote,仅 recommended,不放松规则), 并修掉迁移代码里 14 处 lint 诊断(unused template literal / useImportType / 与 axios 签名对齐的 `any` 显式豁免)。 - `apps/api/tests/awdp_practice_judge.rs` 源码边界测试改指真实位置 (frontends/default 页面 + packages/sdk/src/api/awdp.ts),覆盖更严格。 - `scripts/gen_web_types.py`:OUTPUT_DIR → packages/sdk/src/entity,并让生成器 输出显式 `.js` 扩展名(SDK 以 ESM 发布,tsc 不改写相对导入);重跑 `db:gen:ts` 现在与提交内容字节一致,同时暴露出并修正了 awd_reset_records 缺失 requested_by_admin 的**既有漂移**(列与 Rust 实体都有,TS 侧漏了)。 - mise 的 web:typecheck / test 先 `pnpm run build:packages`:前端经 exports 解析到各包 dist,否则会拿旧 .d.ts 得出假结果。
实测外部仓库构建时暴露的三个真实问题: - 容器以 root 运行且 `--cap-drop ALL` 时既不能 chown 也读不到调用者的 0600 文件, `cp -a /src/. /work/` 直接失败;改为**以调用者 uid:gid 运行**、用 tar `--no-same-owner` 只复制源码(排除 node_modules/dist/.git 等),构建目录放 /tmp。 - Node 26 镜像已不再内置 corepack,`pnpm` 不可用;改为用镜像自带 npm 把包管理器 装到**可写前缀** `/tmp/npm-global` 并前置进 PATH(版本优先取源码 `packageManager`,否则用平台基线 pnpm 11.20.0;仍只接受 semver 形状)。 - Node 基线对齐平台(node:26-bookworm,可用 --node-image 覆盖)。 仍然保持:cap-drop ALL、no-new-privileges、pids-limit、只读源码挂载、 无 socket、只执行约定的 build 脚本(脚本名,不是 JSON 里的 shell 命令)。
- 新增 docs/frontend/ARCHITECTURE.md:Frontend ≠ Theme、依赖方向图、 包职责、mount 契约与 bootstrap 加载流程、注册表/版本解析、三个契约版本、 **信任模型**(第三方前端 = 可信应用代码,构建隔离不等于沙箱)、 逐项安全审查表、生命周期与非目标。 - 新增 docs/frontend/DEVELOPING.md:仓库内开发(mise run dev 的真实行为、 改哪个包、门禁命令、两条硬约束)、外部仓库开发(骨架、mount 示例、 packed tarball、TOML CORS)、安装/激活/回滚/破窗、常见问题、生产形态验证。 - 新增 docs/frontend/ARTIFACT.md:源码 manifest 与制品 manifest 的区别与全部 字段规则、路径安全规则、注册表 schema、文件系统布局、归档格式、构建隔离与 包管理器识别、版本与指针语义。 - 同步既有文档到新布局:README(仓库表 + 可插拔前端小节)、INSTALL(5 个产物、 web-dist 布局、$FLOATCTF_HOME 新目录与前端生命周期、卸载语义)、AGENTS (仓库一句话、实体路径、门禁命令、环境速记)、docs/agents (ARCHITECTURE 目录树与前端约束、DATABASE/DATA-FETCHING/ADD-FEATURE 的路径、 RULES 的参照页与 alert/confirm 检查范围、TESTING 的前端测试分布)。 - 新增 chore/pluggable-frontend-implementation-report.md:23 节实现报告。
P0-A:SDK 之前用模块级绑定(`service_api` / `admin_api` + `bindHttpClients()`)实现 传输,于是 `createFloatCTFClient()` 创建的多个实例会互相覆盖——后创建的客户端会把 先前客户端的请求劫持到自己的 base URL / token。作为公开 SDK 不可接受。 - 领域模块(35 个文件)全部改成工厂:`createXxxApi(http: FloatCTFHttpClient)`, 只依赖注入进来的 handle,不再 import 任何全局绑定。 - `transport.ts` 去掉模块级 binding,改为 `createFloatCTFTransport(scope, options, baseUrl)` 返回**独立**的 axios 实例 + handle;新增 `resolveBaseUrls` / `normalizeBaseUrl`。 - `client.ts` 用自己的两个 handle 构建 `service` / `admin` / `awd` / `awdp` 门面。 - 公共面移除 `service_api` / `admin_api` / `bindHttpClients` / `resetHttpClientsForTests` / `httpClients`;`dispose()` 一并去掉(不再拥有全局资源, 保留只会是误导性的生命周期 API)。 - `requestConfig.baseURL` 在类型上被排除(`Omit<..., "baseURL">`),运行时也放在 客户端配置**之后**,保证 `client.baseUrl` 与实际请求地址永远一致。 - Default Frontend 的 `@/api` 门面改为从本前端那一个实例取所有域对象, 页面只改 import 来源(`@floatctf/sdk` → `@/api`),调用点零改动。 P0-B:四个 SSE hook 之前硬编码 `/api/...`,导致"REST 走配置的 API、实时通道打前端 自己的源"(外部 React 前端跨源 base URL 时直接连错)。现在统一走 `client.sse.connect()/connectAdmin()`,相对路径由客户端自己的 base URL 解析; 管理端仍然使用 admin token。 测试:新增多客户端隔离测试(创建顺序、并行、user/admin 分离、无全局重置依赖)、 requestConfig.baseURL 权威性测试、以及**不 mock connectSse** 的 React SSE base URL 测试(断言真实 fetch 的绝对 URL 与 Authorization 头)。Web 测试 300 → 316。
P0-C 公开注册表数据边界:`registry.json` 由 Caddy 无鉴权公开提供,却持久化了安装来源
(`path:/home/alice/private-frontend`、`git:https://user:token@host/repo.git`)。
- 只写白名单字段;删掉 `source_label_for` 与所有 source 参数;每次写盘 `sanitize_registry`
(遗留 source 会在下次 install/remove/set-current 被清除)并立即 `check-public` 自检;
打印来源时对 URL userinfo 打码。
- 运行时 `parseRegistry()` fail-closed:root / frontend / version / compatibility 逐层白名单,
`source`/`token`/`localPath`/`cloneUrl` 等未知字段直接校验失败。
P0-D 制品树安全:此前只有归档解包校验成员,源码构建与预构建目录可绕过,且 `[ -f … ]`
会跟随符号链接。新增 `validate_artifact_tree`(python3 + lstat),在**四处**统一执行:
归档解包后、源码构建后、预构建目录输入、安装暂存树 rename 前;拒绝符号链接/硬链接/FIFO/
socket/设备文件,`frontend.json`/`entry`/`styles` 必须是普通文件。
P0-E 构建身份:`sudo frontend.sh install …` 时脚本用 `id -u` → 容器以 UID 0 运行。
新增 `resolve_build_identity`(sudo→SUDO_UID/SUDO_GID;普通用户→自身;root 直调→专用非特权
身份,默认 65534:65534),`docker run` 前断言 uid != 0;源码/输出暂存并 chown 给该身份,
不改用户原仓库权限。
P1-A 源码 manifest 契约:真正实现文档化的 `build.{packageManager,script,outputDir}`
(严格解析:只允许这三个键;script 必须是脚本名而非 shell;outputDir 必须是安全相对目录),
容器内用静态命令 + 环境变量执行(`pnpm run "$FCTF_BUILD_SCRIPT"`),只复制声明目录。
构建没产出 `frontend.json` 时,从源码 manifest **生成**只含运行时字段的制品 manifest
(剥掉 build)并按制品契约复核 —— 绝不 `cp floatctf.frontend.json frontend.json`。
P1-B 校验器同源:`frontend.sh` 的 manifest 校验改为与 `parseFrontendManifest` 逐条等价
(字段白名单、长度上限、ID/semver/路径规则、compatibility 规则、styles 上限)。
P1-C 契约版本分离:新增独立的 `MANIFEST_SCHEMA_VERSION` 只用于 `frontend.json`,
`REGISTRY_SCHEMA_VERSION` 只用于 `registry.json`(两者今天都是 1,但不再共用一个常量)。
P1-D 版本不可变:移除 `--reinstall` / `INSTALL_REINSTALL` / 同版本原子替换分支。
同 ID+同版本+同内容 = 幂等成功;同 ID+同版本+不同内容 = 硬失败(`default` 与 `--platform`
也不例外)。`install.sh` 不再传 `--reinstall`。
P1-E 显式指针:删除当前版本不再用临时排序器"猜"替代版本(违背显式指针语义,混合预发布
版本还会 TypeError);改为拒绝并提示先 `set-current`。顺带修掉 `with_registry_lock`
吞掉内层退出码的问题(曾出现"注册表拒绝写入后资产仍被删掉")。
新增 `scripts/test-frontend-manager.sh`(**72 项**,不依赖 docker,几十秒跑完): - manifest 严格校验(未知字段、schemaVersion、ID、semver、name/description/author 长度、 compatibility 未知键与 major、entry/styles 路径规则与上限) - 源码 manifest 的 build 契约(非法 packageManager、script shell 注入、outputDir 穿越、未知键) - 公开注册表数据边界:断言 **raw 文件**不含 source / 本地与临时路径 / Git clone URL; 模拟"历史遗留 source"验证重写后被清除 - 制品树安全:frontend.json 符号链接、entry→/etc/passwd、相对符号链接逃逸、style 符号链接、 FIFO;归档 ../ 穿越、绝对路径成员、符号链接、硬链接(各自断言拒绝理由, 且被拒后不留任何已安装资产) - 版本不可变:同版本同内容幂等、第三方/--platform/default 变更全部拒绝、--reinstall 已不存在 - 显式指针:删除 currentVersion 被拒 + 拒绝后资产未被删 + set-current 后可删; 1.0.0/1.0.0-alpha/1.0.0-1/1.0.0-beta.1 混排不崩 - 构建身份:普通用户 / sudo / root 直调三种入口都非 UID 0 新增 `cli-manifest-parity.test.ts`(**37 用例**):同一组 fixture 同时跑 `parseFrontendManifest` 与 `scripts/frontend.sh _manifest-validate-json`,断言制品模式 接受/拒绝完全一致;并断言源码模式接受时生成的制品 manifest 一定被权威解析器接受 且不含 `build`(找不到脚本时整组跳过,不影响包发布)。 `scripts/check-architecture.sh` 新增 4 类门禁:SDK 不得有模块级传输绑定或旧全局导出; React 实时 hook 不得硬编码站点路径且必须走 `client.sse.connect*`;frontend.sh 必须分离 manifest/registry schema 常量、不写 source、不存在 --reinstall;Default Frontend 只允许 import 公开子路径。 接线:`mise run test` 与 CI 的 `fast-web` 都会跑前端管理器测试(含发布产物验证之后)。
- ARCHITECTURE:明确 SDK **按实例隔离**(无模块级当前 transport); 新增「实时(SSE)与 base URL」一节(四个 hook 从 client 派生 URL 对照表); 安全审查表补三项(制品树 lstat 校验、公开注册表数据边界、源码构建非 root); 新增「`$FLOATCTF_HOME/frontends` 是公开静态树」与「版本不可变」两节。 - ARTIFACT:注册表改为**未知字段一律拒绝**并说明公开树不得含运维元数据; 版本条目语义改为"同内容幂等 / 异内容硬失败(无例外)";补显式指针的删除语义。 - DEVELOPING:`build` 段三个字段确认为**真实生效**的契约并给出示例; 容器构建补充非 root 身份与制品树 lstat 边界;常见问题补 `--reinstall` 已移除与 `set-current` 指引。 - INSTALL:安装命令去掉 `--reinstall`。 - packages/sdk/README:新增 Multi-client / instance isolation 一节(含双客户端示例、 baseUrl 权威性、工厂导出、`client.sse.connect*` 用法)。 - packages/react/README:说明四个实时 hook 从 client 的 base/admin base 派生 URL。 - 实现报告:修正分支/提交/推送元数据(明确远端 head 与"closure 提交未 push", 并且**不再记录包含报告自身的提交哈希**);新增 `## Phase 12.1 Closure` 一节 (SDK 实例隔离、React SSE base URL、公开注册表数据边界、制品树校验、非 root 构建、 源码 manifest 契约、校验器同源、契约版本分离、版本不可变、显式指针、外部仓库再验证、 安全负向测试、回归验证、门禁结果);Remaining Risks 移除已修复项。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
把 FloatCTF 前端从"仓库里的一个 React 应用"改造成可插拔前端平台:浏览器加载一个极薄
的 bootstrap,由它按本地注册表解析并挂载一个完整的前端应用(官方
default或第三方独立仓库前端)。后端不感知浏览器路由;前端可以换成任意框架。
本 PR 同时包含 Phase 12.1 closure(对 Phase 12 初版的正确性/安全/契约收口)。
架构
主要改动
Phase 12
@floatctf/sdk(传输/错误模型/领域 API/DTO/fetch-SSE/生成实体)、@floatctf/react(可选 headless 绑定)、@floatctf/frontend-runtime(manifest/注册表校验 +mount(context)契约 + bootstrap 加载器与兜底页)。frontends/default(275 renames,页面/路由/布局/Primer/AWD UX 未重设计)。apps/web变为引导页(14 KB / gzip 5 KB,无 React)。GET /api/frontend+FRONTEND_ACTIVE动态设置(protected、写入校验)。无 schema 变更。scripts/frontend.sh前端管理器(安装/升级/回滚/移除;源码构建在隔离容器内)。?frontend=default破窗。Phase 12.1 closure
createXxxApi(http));移除service_api/admin_api/bindHttpClients/resetHttpClientsForTests等全局导出;requestConfig.baseURL不再能覆盖权威 base URL。client.sse.connect/connectAdmin),不再硬编码
/api/...。registry.json由 Caddy 无鉴权公开,因此只写白名单字段;不再持久化安装来源;运行时解析器对未知字段 fail-closed。
validate_artifact_tree(lstat)对归档/源码构建/预构建目录/暂存树统一执行,拒绝符号链接、硬链接、FIFO、socket、设备文件。
SUDO_UID/GID;普通用户→自身;root 直调→专用非特权身份)。build.{packageManager,script,outputDir}契约;frontend.sh校验器与parseFrontendManifest规则等价(37 个 parity 用例)。--reinstall;同 ID+版本+不同内容一律硬失败(含default)。set-current。验证
mise run check= 0、mise run build= 0、git diff --checkOK。登录、赛事列表、深路由整页刷新、AWD 选手页 + 实时 SSE 200(走新 client 路径)、
管理端设置与前端选择器(真实读注册表)。
/workspace路由、packed 包):隔离容器构建(自定义
build:floatctf+out-ui,构建不产出frontend.json由管理器生成)→ 安装 → raw 注册表无来源泄露 → 浏览器成功加载。
manifest 非法字段;注册表未知字段与凭据类元数据 —— 全部 fail-closed。
兼容与风险
FRONTEND_ACTIVE默认default;未安装任何第三方前端时行为与之前一致。frontends/default的版本号(同版本换内容会被拒绝)。chore/pluggable-frontend-implementation-report.md;架构/开发/制品文档见
docs/frontend/{ARCHITECTURE,DEVELOPING,ARTIFACT}.md。