From aa18701130de92b5a3013db4645496d815ddac97 Mon Sep 17 00:00:00 2001 From: xcs Date: Tue, 25 Aug 2026 18:32:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3:ADR-012=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E4=B8=AD=E5=BF=83;=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E9=89=B4=E6=9D=83=E5=AE=9A=E8=B0=83=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E8=B5=84=E6=BA=90=E6=9C=8D=E5=8A=A1=E5=99=A8=20+=20?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=8F=AA=E5=88=B0=E7=AB=AF;org=20CONTRACT.md?= =?UTF-8?q?=20=E9=87=8D=E5=86=99;debt=20=E9=89=B4=E6=9D=83=E6=AE=B5;CLAUDE?= =?UTF-8?q?.md=20=E9=93=81=E8=A7=84=E7=9F=A9=205/6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 17 ++++-- apps/api/src/modules/org/CONTRACT.md | 77 +++++++++++++-------------- docs/adr/ADR-012-keycloak-identity.md | 47 ++++++++++++++++ docs/debt.md | 22 +++++--- docs/编码规范.md | 4 +- 5 files changed, 114 insertions(+), 53 deletions(-) create mode 100644 docs/adr/ADR-012-keycloak-identity.md diff --git a/CLAUDE.md b/CLAUDE.md index 2cf2dcf..2e77a72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1309,6 +1309,12 @@ 用户那一单误存的三份已删,工作成果现摆 1 份、点开即那份 .docx。5 测。 顺手清两笔:`wake.test.ts` 的 `as unknown as`(§12.5 禁止清单,lint 红)、 `wake` 契约的 api-report 快照(上一轮没入库,api:check 一直红)。 +- **统一鉴权中心(Keycloak)接入,只做后端**(2026-08-25,ADR-012):资源服务器 —— 后端只验 Bearer JWT + (`jose`,RS256 / iss / aud 本地验,不回源);`Staff.id` = `sub`,首登即建、Admin API 同步名册; + **权限只到端**:`sys.role_access` 一行一个 realm 角色,运营中心可改(锁死不变量:改完得有人还能进运营中心); + 路由归属写在契约 `access` 上,app 层一处中间件统一判 401/403,治理路由里约 100 处守卫整体删除, + 覆盖测试 fail-closed;SSE 与下载走 60 秒票据;口令 / 会话表 / 岗位授权退役(八条路由回 410,契约留一轮)。 + web 待按前端手册接入,接入前登不进。 ## 文档权威地图 @@ -1318,6 +1324,8 @@ | | `docs/产品设计.md` | 三面一体的产品形态与决策记录 | | | `docs/活动开发规范.md` | **开发活动照这份**(2026-08-02 第三版,按四轴重组:解耦机制/Harness 契约/Loop/活动怎么写):基座读数与机检纪律、门与闸、五类卡、剧本 steps、建模十纪律、三常备案例与验收口径(released+被拒0)、**排障速查表**;§8 = 待实现 | | | `docs/HTML报告生成规范.md` | **报告模版照这份**(2026-08-02 从开发规范拆出):一份 HTML 三个身份(件/表单/归档件)、**报告与卡的关系与同源纪律**、绑定语言全表、编辑态(闸/孪生件/增删行/红带)、PRD 平移法、陷阱表 | +| | `docs/前端对接说明-统一鉴权中心.md` | **前端接统一鉴权中心照这份**(ADR-012):要改哪些文件、`/api/me` 与端、401/403/410、SSE 与下载的票据 | +| | `docs/API文档-鉴权与人员.md` | 鉴权 / 人员 / 角色→端接口的人话版 + 全部路由归属总表;契约不一致时以 `packages/contracts` 为准 | | | `docs/debt.md` | 欠账台账(只增,销账划线) | | | `docs/adr/` · 各模块 `CONTRACT.md` | 架构决策与模块级不变量 | | 业务样例(活) | `docs/MVP示例工作流.html` | 七节点 SOP + 判定表 + 台账域 —— 开发活动的业务素材 | @@ -1333,10 +1341,11 @@ 3. **§12.5 禁止清单**:no `any` / `as unknown as` / `@ts-ignore`;domain 零 IO; 不删测试;不动门禁配置;不绕写三分/两缝。 4. **mock 只许出现在外界**(邮件通道、外部系统的录制回放替身);系统内部机制不得 mock。 -5. **密钥只从环境读**:`JCAI_DEEPSEEK_API_KEY`(模型)、`JCAI_SEED_PASSWORD`(种子口令)、 - `JCAI_SECRET_KEY`(凭证密封);不入源码、不入包、不入留痕;E2E 永不回退开发库。 -6. **试跑一律用 AI 账号**(密码 `jcai-dev-2026`),绝不派到用户工作台;测完 - `pnpm --filter @app/api reset-runs` 清运行数据(清跑出来的,留配置好的)。 +5. **密钥只从环境读**:`JCAI_DEEPSEEK_API_KEY`(模型)、`JCAI_SECRET_KEY`(凭证密封 + 票据派生)、 + `KEYCLOAK_URL` / `KEYCLOAK_REALM` / `KEYCLOAK_CLIENT_ID`(启动必填)、`KEYCLOAK_PROVISIONING_CLIENT_*`(名册同步,可选); + 不入源码、不入包、不入留痕;E2E 永不回退开发库。 +6. **试跑一律用 AI 服务账号**(Keycloak 里 `bpos-e2e` 的服务账号,client_credentials 拿 token;`JCAI_TEST_ACCOUNTS` 列上它的用户名), + 绝不派到用户工作台;测完 `pnpm --filter @app/api reset-runs` 清运行数据(清跑出来的,留配置好的)。 7. **安装恒新增**:活动/工具改了内容必须升版本号,同版重装是 no-op(静默丢失,踩过)。 8. **界面不撒谎**:状态如实(未绑就写未绑)、错误带厂商原话、不可用不摆假数字。 diff --git a/apps/api/src/modules/org/CONTRACT.md b/apps/api/src/modules/org/CONTRACT.md index 05e7412..39d5eb9 100644 --- a/apps/api/src/modules/org/CONTRACT.md +++ b/apps/api/src/modules/org/CONTRACT.md @@ -1,51 +1,46 @@ # module: org -职责: 员工与岗位:账号、口令与登录会话(ADR-010)、岗位授权(客体三类 × 动作三种)、 - 停用前置检查、岗位在岗人员查询(负责人四策略解析的候选集来源)、 - 工作组(ADR-011:起单时的岗位绑定)、种子数据播种 -不负责: 负责人解析算法本身(见 process)、任务与卡的在途判定(经 InFlightWorkProbe 端口由 app 注入)、 - 鉴权中间件与 cookie 的读写(app 层:§4「中间件在 app 层,身份判定读 org 出口」) +职责: 员工与岗位(ADR-012):验签后的人是谁(名册即时更新)、他能进哪几个端(角色→端)、 + 岗位指派与在岗人员查询(负责人四策略解析的候选集来源)、名册同步(Admin API)、 + 在途工作(只读)、工作组(ADR-011)、种子(岗位 + 缺省角色→端) +不负责: 验签之外的 HTTP 语义(401/403 由 app 层中间件按路由归属判)、 + 负责人解析算法本身(见 process)、任务与卡的在途判定(经 InFlightWorkProbe 由 app 注入) 公开出口: index.ts - - 类型:`OrgService` `StaffWithPosition` `CreateStaffInput` `IssuedSession` 与 @contracts 的 - `Staff` / `Position` / `Grant` / `GrantObject` / `GrantAction` / `DisableCheck` / `Session` 转出 - - 错误:`StaffNotFoundError` `PositionNotFoundError` `AccountTakenError` `DisableBlockedError` - - 端口类型:`PositionRepository` `StaffRepository` `GrantRepository` `SessionRepository` - `WorkGroupRepository` `PasswordHasher` `InFlightWorkProbe` `Clock` `IdGenerator` + - 类型:`OrgService` `StaffWithPosition` `Identity` 与 @contracts 的 `Staff` / `Position` / `Principal` / + `RoleAccess` / `RosterSyncOutcome` / `InFlightWork` / `Surface` / `WorkGroup` / `WorkGroupBinding` 转出 + - 错误:`TokenRejectedError`(→401)`AccountDisabledError`(→403)`AccessLockoutError`(→409) + `StaffNotFoundError` `PositionNotFoundError` + - 端口类型:`TokenVerifier` `VerifiedClaims` `RosterSource` `RosterUser` `IdentityPatch` + `PositionRepository` `StaffRepository` `StaffRecord` `RoleAccessRepository` + `WorkGroupRepository` `InFlightWorkProbe` `Clock` `IdGenerator` - 用例工厂:`createOrgService(deps: OrgServiceDeps): OrgService` - - 纯函数:`isAuthorized` `actionCovers` `objectCovers` - - 常量:`POSITION_SEED` `STAFF_SEED` `DEFAULT_LOGIN_ACCOUNT` - - adapter 工厂(供 app 装配):`createPositionRepository` `createStaffRepository` - `createGrantRepository` `createSessionRepository` `createWorkGroupRepository` `objectKeyOf` - - 口令散列(供 app 装 `PasswordHasher`):`hashPassword` `verifyPassword` `newSessionToken` - - Drizzle 表(供 app 迁移与 join):`positionTable` `staffTable` `grantTable` `sessionTable` - `workGroupTable` + - 纯函数:`identityOf` `isNoiseRole` `normalizeRoles` `endsOf` `assertNotLockedOut` `normalizeEnds` `buildInFlightWork` + - 常量:`POSITION_SEED` `NONE_POSITION_CODE` `TEST_POSITION_CODE` `DEFAULT_ROLE_ACCESS` + - adapter 工厂(供 app 装配):`createKeycloakTokenVerifier`(jose,配置型 `KeycloakVerifierOptions`) + `createKeycloakRoster`(Admin API,配置型 `KeycloakRosterOptions`) + `createPositionRepository` `createStaffRepository` `createRoleAccessRepository` `createWorkGroupRepository` + - 错误译文(供 app 拼 401 message):`explainJoseError`(jose 错误 → 人话,手册 4.4) + - Drizzle 表:`positionTable` `staffTable` `roleAccessTable` `workGroupTable` 层级: L1(编码规范 §4) -依赖: @contracts;@shared/db(Drizzle 句柄与 sys schema) - port:`InFlightWorkProbe`(app 注入 task / human-tool 的只读用例)、`IdGenerator` -被依赖: process(负责人解析的候选集)、message、interaction、human-tool、report、 - app(鉴权中间件与 /api/gov/staff 路由组装) +依赖: @contracts;@shared/db(Drizzle 句柄与 sys schema);jose(仅 adapter) + port:`InFlightWorkProbe`(app 注入 task / human-tool 的只读用例)、`IdGenerator`、 + `TokenVerifier`(app 给 jose 远程 JWKS;测试给本地密钥对)、`RosterSource`(可缺省) +被依赖: process(负责人解析的候选集)、message(治理台收件人 = staffWithEnd('ops'))、 + app(鉴权中间件与 /api/gov/staff、/api/gov/access 路由组装) 不变量: - - 动作蕴含:治理 ⊃ 写 ⊃ 读;数据客体三级覆盖(域 ⊃ 对象 ⊃ 字段) - - 停用是全有全无:前置检查未过即抛 `DisableBlockedError`,不做部分执行 - - `authorize` 对停用员工恒 false - - `ensureSeeded` 幂等:按 code / account 判存在,已存在则跳过,不覆盖人工改动; - 唯一例外是**补种口令** —— 已存在但 `password_hash` 为空的员工会被补上初始口令, - 否则 ADR-010 之前建的账号永远登不进(这不算覆盖人工改动:空不是改动) - - **登录失败不区分原因**:账号不存在 / 已停用 / 未设口令 / 口令错,一律 `undefined`; - 且无论账号是否存在都走一遍散列比对,免得响应时间泄漏账号是否存在 - - **明文永不落库**:口令只在 `login` / `setPassword` 的入参上停留一瞬,落库前即散列 - - **会话可即刻作废**:服务端表而非签名 cookie —— `logout` 立刻生效; - `disableStaff` 连带 `revokeAllOf`,被停用的人拿旧 cookie 也办不了事 - - **工作组是绑定不是策略**(ADR-011):它给出岗位的人选,四策略于是无从发问; - `bindingOf` 只出成员表快照,在岗与否由调用方(process)核 —— 岗位候选集核不了跨岗位的绑定 + - `Staff.id` 就是 Keycloak `sub`;`account` 只作显示,永不当键 + - `state` 只由名册同步置 `disabled`;`authenticate` / `principalOf` 对 disabled 一律抛 AccountDisabledError, + 有效 token 不能把它翻回 active(撤权在同步之后即时生效) + - 名册即时更新只写变了的列;`last_seen_at` 60 秒内不重写 + - 角色→端:`setRoleAccess` / `deleteRoleAccess` 先算新表再验锁死不变量(名册为空不算锁死) + - `staffOfPosition` 只回 active;哨兵岗 NONE 恒回 [] + - `ensureSeeded` 幂等:岗位按 code 判存在;缺省角色→端只在表空时播;测试账号策略只补不覆盖 + - 工作组是绑定不是策略(ADR-011):`bindingOf` 只出成员表快照,在岗与否由调用方核 -影响半径: contract 增 `login` / `logout` / `resolveSession` / `setPassword` 与 `IssuedSession`; - port 增 `SessionRepository` / `PasswordHasher`,`StaffRepository` 增两个口令读写方法; - 下游只有 app(装配 + 鉴权中间件),depcruise 已反查确认 - -近变(2026-07-27):模块面**未变**。app 层新增工作组读写路由 - (`GET|PUT /api/gov/work-groups`),组装既有的 `workGroupOf` / `saveWorkGroup` - —— ADR-011 落地时欠的那个治理界面就此清账,一人一组的口径由 app 层按 ownerId 保证。 +影响半径(2026-08-25,ADR-012):contract / port / index 全面重写 —— 退役 `login` `logout` `resolveSession` + `setPassword` `authorize` `loadSession` `listGrants` `setGrants` `createStaff` `checkDisable` + `disableStaff` `findByAccount` 与 `SessionRepository` `PasswordHasher` `GrantRepository`; + 下游 app(中间件 / 装配 / 路由)与 message 装配同批改,depcruise 已反查确认。 diff --git a/docs/adr/ADR-012-keycloak-identity.md b/docs/adr/ADR-012-keycloak-identity.md new file mode 100644 index 0000000..8c38a4e --- /dev/null +++ b/docs/adr/ADR-012-keycloak-identity.md @@ -0,0 +1,47 @@ +# ADR-012 统一鉴权中心(Keycloak):身份、名册与「角色 → 端」 + +状态:已接受(2026-08-25) +背景出处:`docs/keycloak-backend-guide.md`(Keycloak 26.7.1 / jose,核实日期 2026-08-12); +设计稿 `docs/superpowers/specs/2026-08-25-keycloak-auth-design.md`。 +取代:ADR-010(口令 + 服务端会话)与编码规范 §4 原「不引 OAuth/OIDC」的定调。 + +## 问题 + +公司有了统一鉴权中心,本系统还在自建账号口令、服务端会话表与岗位授权(客体三类 × 动作三种)。 +人要在两处维护,权限模型也比产品实际需要的细太多 —— 用户只要「不同角色可以动态配置能进哪几个端」。 + +## 决策 + +1. **资源服务器,不做 BFF**:前端(`web-portal` / `web-console`,PKCE)自取 token;后端只本地验签 + Bearer JWT(`jose`:RS256 写死、校 iss 与 aud、30s 容差、远程 JWKS 缓存 10 分钟 / 冷却 30 秒), + 不回源、不 introspect、**不配 client secret**。 +2. **身份在 token 里**:`Staff.id` = `sub`;`account` = `preferred_username` 只作显示; + 首次见到即建档(JIT),账号 / 姓名 / 邮箱 / 角色变了才写,`last_seen_at` 60 秒内不重写。 +3. **权限只到端**:三个端 design / ops / workbench;`sys.role_access` 一行一个 realm 角色, + 运营中心可改;一个人的端 = 其角色命中行的并集。缺省 `admin → 三端`,`user → workbench`。 + **锁死不变量**:改完至少一名在册活跃员工仍能进运营中心,否则 409。 +4. **路由归属进契约**:`RouteContract.access` 必填(public / any / 端 / 端数组);app 层一处中间件 + 按注册表判 401 / 403;治理路由里约 100 处逐条守卫整体删除;覆盖测试 fail-closed。 +5. **名册**:岗位仍是本系统的表(哨兵岗 `NONE / 未分配`,运营中心指派);名册经 Admin API 同步 + (master realm 的 provisioning 服务账号,只要 `view-users`)+ 首登即建。`state` 只由同步置 + `disabled`(Keycloak 停用或删除),有效 token 不改它 —— 撤权在同步之后即时 403。 +6. **票据**:`EventSource` 与 `` 带不了 Authorization 头 —— `GET /api/auth/ticket` + 换一枚 60 秒 HMAC 票据(`HKDF(JCAI_SECRET_KEY, 'jcai-ticket')`),只在 GET 的 `?ticket=` 上认; + 票据里没有权限,中间件拿 sub 回库现算。无状态、不单次:60 秒重放窗换零状态表。 +7. **退役**:`login` / `logout` / `session` / 建人 / 停用 / 岗位授权八条路由回 410(新错误码 `gone`), + 契约标 `@deprecated` 留一轮(§12.4 三步);`sys.session` `sys.position_grant` `staff.password_hash` drop。 +8. **测试账号**:`JCAI_TEST_ACCOUNTS`(Keycloak 用户名)命中者落 TEST 岗 + 独立工作组(ADR-011 原语义); + e2e 与试跑用服务账号(client_credentials),不用人的账号。 + +## 后果 + +- 撤角色的时效 ≤ 5 分钟(token 寿命);停用经同步后即时。 +- web 需按《前端接入手册》改造(登录 / Bearer / 401 跳转 / SSE 与下载改走票据 / 入口亮灯改读 `/api/me` 的 `ends`), + 改造前登不进 —— 这是「只做后端」的既定后果。 +- **部署顺序是先跑迁移、再 `reset-runs`**:迁移 0032 清空 `sys.staff` 并连带清掉两张持有身份的 + **配置**表(`sys.node_owner`、`sys.trigger` 的确认位);而 `sys.task.owner_id` 这类**无外键**的 + 运行数据列不受迁移约束,若顺序颠倒,这些运行数据会指着一个已经不存在的身份变成孤儿 —— + `reset-runs` 必须排在迁移之后跑,才能把这批指着旧身份的运行数据一并清掉。 +- Keycloak 侧前置(管理员):本服务 client(建议复用 `api-gateway`)、`web-console` 补 audience mapper、 + provisioning 服务账号、e2e 服务账号 client(`bpos-e2e`,realm 角色 admin,firstName「AI」)。 +- 欠账见 `docs/debt.md`「鉴权」段。 diff --git a/docs/debt.md b/docs/debt.md index 20d763a..a02264d 100644 --- a/docs/debt.md +++ b/docs/debt.md @@ -56,14 +56,22 @@ ADR-011 的「绑定先于策略」随岗位一起退场 —— 工作组快照 `.parallel()` 任一失败即整体失败(失败走向不进 Mastra)、三态聚合投影 (读实例表不读 run 状态)、`node_instance` 投影表照写(发号权在主工程)。 -## 鉴权(ADR-010 落地后的已知欠账) +## 鉴权(ADR-012 落地后的已知欠账) -- **登录失败无次数限制**:可无限次试口令。单组织 ≤50 人、内网部署下暂缓; - 接入外网前必须补(按账号 + 来源 IP 双维度退避)。 -- **无口令强度校验、无首登强制改密**:全员初始口令相同(`JCAI_SEED_PASSWORD`), - 谁都没改过就等于没有口令。治理台「员工」面需补:改密 / 重置 / 强制改密标记。 -- **`setPassword` 已在 org 出口就位但无路由**:改密只能经播种或直接调用例,界面上没有入口。 -- **会话无「登出全部设备」**:`revokeAllOf` 已实现(停用员工时调),但本人无从触发。 +- **撤角色时效 ≤ 5 分钟**:改了 Keycloak 里的角色,现有 access token 到期前仍按旧角色算(手册 13.4); + 要立即生效在 Keycloak 上 `POST /users/{id}/logout`。停用经名册同步后即时 403。 +- **票据 60 秒可重放窗**(无状态换来的):绑定 sub、只认 GET;要单次得加一张已用票据表。 +- **名册同步是手动按钮**(运营中心 · 员工 · 同步),没有定时;没配 provisioning 就只有首登即建。 +- **web 待按《Keycloak 前端接入手册》接入**:登录页 / Bearer / 401 跳转 / SSE 与下载改走 `GET /api/auth/ticket` / + 入口亮灯改读 `GET /api/me` 的 `ends`;接入前登不进。那一刻一并清掉 `SURFACE_FNS` 与退役契约。 +- **退役契约下个迭代删**(§12.4 三步之第三步):`loginRoute` `logoutRoute` `sessionRoute` `createStaffRoute` + `disableStaff*Route` `listGrantsRoute` `setGrantsRoute` 与 `sessionSchema` / `grant*` / `governanceFunction*` / `disableCheckSchema`。 +- **`groups` 未用**:日后要按部门过滤数据,需 Keycloak 配 group membership mapper。 +- **aud mapper 未配即全线 401**:401 的 message 会点名「给发 token 的那个 client 加 audience mapper」,但没有启动期探测。 +- **无库环境里,路由覆盖的 fail-closed 保险会静默消失**:`apps/api/src/app/__tests__/route-coverage.it.test.ts` + 用 `describe.skipIf(database === undefined)` 跳过(同目录另外三份 it-test 同款,是仓库级惯例)—— + 没有库的环境里这条测试根本不跑,进程仍 exit 0;正在搭 CI 的人必须先给它一个库, + 否则「写了路由处理器却没在契约里声明 access」这类漏洞会在 CI 里悄悄放过。 ## 工作组(ADR-011 落地后的已知欠账) diff --git a/docs/编码规范.md b/docs/编码规范.md index 8033e29..07ad295 100644 --- a/docs/编码规范.md +++ b/docs/编码规范.md @@ -44,6 +44,7 @@ | 台账自生长 | node-postgres(`pg`)直写(ledger adapter 内) | 运行时 DDL/DML 由声明生成,ORM 静态 schema 不适用 | | 队列/定时 | pg-boss | PG 原生,先持久后投递、至少一次天然契合 | | 代码契约校验 | zod | 仓库内 API/DTO | +| 鉴权 | Keycloak(统一鉴权中心)+ `jose` 本地验签 | 资源服务器:RS256 / iss / aud 本地验,不回源;权限只到端(ADR-012) | | 声明数据校验 | ajv(JSON Schema 2020-12 子集) | 活动声明是运行时数据,不是编译期代码(ADR-003) | | 测试 | Vitest;集成测试用 Testcontainers(PG) | domain 层测试零 mock | | 门禁 | tsc / ESLint(flat) / dependency-cruiser / 契约快照 | §11 | @@ -154,7 +155,7 @@ repo/ - 技能编排属于活动(AM:活动内容物含技能清单)——`skill-engine` 与 loop/flow 同列范式运行器;置于 L4 因两范式依赖它(临场装配/步引用),import 恒向下、免 port。 - 找不到归属的代码,先怀疑模块拆分错了,再考虑 `shared/`;`shared/` 只收无业务语义的纯工具 - 度量八指标(OS 8.3)是 `memory`(任务日志)与 `task`(终态/纠正信号)之上的**只读投影**,落 `report` 的用例,不设独立模块 -- 鉴权定调:单组织 cookie session——登录、会话与岗位权限校验中间件在 apps/api 的 app 层实现,权限判定读 `org` 出口;不引 OAuth/OIDC 与多租户机制 +- 鉴权定调(ADR-012):统一鉴权中心(Keycloak)资源服务器——后端只验 Bearer JWT;鉴权中间件在 apps/api 的 app 层实现,身份判定读 `org` 出口;**权限只到端**(设计中心 / 运营中心 / 工作台),角色→端由运营中心的表定,路由归属写在契约的 `access` 上;不做多租户 --- @@ -640,6 +641,7 @@ pnpm test - **ADR-004** 活动 .JS 装载 ABI:单文件 ESM,默认导出 `defineActivity()`;能力全部注入(基座六件句柄),零环境能力;静态审扫描产物 import 与全局引用 - **ADR-005** 工具 `validate` 能力位:调用契约 `capabilities.validate?: true`,预检存在即调(OS 7.3 落地预检第四项) - **ADR-006** S-FEEL 自研于 `@sfeel`,子集文法冻结于 OS 03;不引第三方 FEEL 引擎 +- **ADR-012** 统一鉴权中心(Keycloak):资源服务器本地验签(`jose`),`Staff.id` = `sub`;权限只到端,`sys.role_access` 可改;名册 Admin API 同步 + 首登即建;60 秒票据给 SSE 与下载;口令 / 会话 / 岗位授权退役(取代 ADR-010) 留白(实现期再定,各建 ADR 后方可动工):鉴权会话与凭证加密方案;JSON Schema→zod 转换库选型;活动装载的 worker 隔离硬化;活动记忆向量召回启停;e2e 框架。