diff --git a/.scratch/saas-production-readiness/assets/product-surface-code-inventory.md b/.scratch/saas-production-readiness/assets/product-surface-code-inventory.md new file mode 100644 index 0000000..1725112 --- /dev/null +++ b/.scratch/saas-production-readiness/assets/product-surface-code-inventory.md @@ -0,0 +1,191 @@ +# Accepted product-surface code inventory + +Audit date: 2026-07-10 + +Scope: source-level inventory for the accepted SaaS product boundary in ADR-0020, +ADR-0021 and ADR-0022. This is not a claim that tests were executed during this +inventory. “Test evidence” below means a repository test exercises the named +path; “no direct test found” means a search of `hub/test/**/*.ts` found no request +to the named route or corresponding service call. + +## Contract baseline + +The destination is not a shared-bot, single-credential Hub. The repository's +current shared language says: + +- an Organization owns its memberships, projects, teams, external connections + and settings (`CONTEXT.md:7-9`); +- a Platform Administrator creates and operates Organizations without becoming + their member, while an Organization Administrator is only an OWNER/ADMIN of + that Organization (`CONTEXT.md:11-17`); +- the Admin Surface is a private web control plane requiring the corresponding + authenticated administrator authority (`CONTEXT.md:19-21`); +- each Organization has its own Feishu application for login, bot messaging and + directory integration (`CONTEXT.md:23-25`); +- provider connections are Organization-scoped BYOK or an Organization-distinct + platform-managed connection (`CONTEXT.md:27-37`); +- capacity uses non-optional platform ceilings, optional lower Organization + limits, durable Organization-fair admission and audited emergency brakes + (`CONTEXT.md:39-77`). + +ADR-0021 makes `/admin/platform` and `/admin/org/:orgSlug` separate guarded web +areas (`docs/adr/0021-org-admin-project-onboarding.md:23-40`), requires a +customer-owned Feishu app with encrypted Organization-scoped secrets +(`docs/adr/0021-org-admin-project-onboarding.md:42-47`), and pins both provider +credential modes as Organization-scoped (`docs/adr/0021-org-admin-project-onboarding.md:49-60`). +ADR-0022 makes its safety controls release requirements, not optional future UI +(`docs/adr/0022-layered-capacity-admission.md:22-77`). + +Status vocabulary in this inventory: + +- **HTTP-backed** / **Feishu-backed**: a production route/event handler is wired. +- **Service/test only**: code can be invoked directly, but there is no production + product entry point. +- **Partial**: part of the journey exists but the accepted end-to-end product + surface cannot complete it. +- **Missing**: no production entry and no suitable persistence/control model. +- **Contract divergence**: reachable behavior actively uses the wrong tenancy or + credential shape. + +## Journey matrix + +| Target journey | Reachable entry | Authorization | Persistence | Test evidence | Verdict and exact gap | +|---|---|---|---|---|---| +| Platform Administrator signs in | None. The admin plugin registers only Feishu auth and Organization routes (`hub/src/admin/plugin.ts:7-8`, `hub/src/admin/plugin.ts:31-46`). The only HTML is the Org Admin login page (`hub/src/admin/routes/authRoutes.ts:171-207`). | `PlatformRoleAssignment` and `PlatformRole.ADMIN` exist in the schema (`hub/prisma/schema.prisma:111-129`), but the HTTP guard explicitly says the platform control plane is not modeled (`hub/src/admin/auth/guards.ts:1-5`). | Role rows can be stored, but there is no platform session/identity binding, invite/bootstrap flow or platform-admin audit surface. | No platform-auth route test exists. | **Missing.** `/admin/platform` from ADR-0021 is not mounted, so no authenticated operator can reach any platform workflow. | +| Platform Administrator creates/lists/updates/operates Organizations | None. No `/api/platform/*` or Organization CRUD route is registered; the route composition is auth + `/api/org/:orgSlug/*` only (`hub/src/admin/plugin.ts:31-46`, `hub/src/admin/routes/orgRoutes.ts:24-103`). | No `requirePlatformAdmin` implementation; only `requireSession` and `requireOrgRole` exist (`hub/src/admin/auth/guards.ts:43-113`). | `Organization` has slug, name and ACTIVE/SUSPENDED/ARCHIVED status (`hub/prisma/schema.prisma:28-52`). | Organizations are created directly only by test setup, e.g. `prisma.organization.upsert` (`hub/test/integration/helpers.ts:60-77`) and a direct cross-org fixture create (`hub/test/integration/admin-explorer.test.ts:198-210`). | **Service/test only at the database layer.** Manual pilot onboarding has no safe production command, HTTP API or UI; status changes are also unreachable. | +| Platform Administrator configures an Organization's customer-owned Feishu app and checks readiness | None. Runtime accepts exactly one `feishuAppId`/`feishuAppSecret` pair in `AdminPluginConfig` (`hub/src/admin/plugin.ts:10-19`), and server startup reads one process-global app id, secret and bot open id (`hub/src/server.ts:47-52`). | No platform guard or Organization connection ownership check. | There is no Feishu application/secret model. `ExternalDirectoryConnection` stores provider/tenant/source metadata but no app id, bot id or secret reference (`hub/prisma/schema.prisma:196-214`). | No provisioning/readiness test exists. | **Missing + contract divergence.** There is no Organization-scoped encrypted Feishu connection or readiness workflow; production can operate only the one env-configured app. | +| Platform Administrator configures an Organization-distinct platform-managed provider key/base URL | None. No platform provider route or UI is registered. | No platform guard. | The accepted Lean type exists only in the semantic contract (`spec/Spec/System/Organization.lean:52-66`); Prisma has no provider-connection or provider-mode model (`hub/prisma/schema.prisma:26-214`). | Runtime tests prove env values are returned, not Organization values (`hub/test/unit/runtime-settings.test.ts:18-55`). | **Missing + contract divergence.** `EnvRuntimeSettings.provider` ignores its project scope and reads one process-global `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` (`hub/src/settings/runtime.ts:20-21`, `hub/src/settings/runtime.ts:46-67`). | +| Platform Administrator applies Organization/platform `DRAIN` or `STOP_NOW` and later resumes | None. | No platform guard, reason capture or scope check. | No workload-brake model exists; only the Lean contract defines `open`/`drain`/`stopNow` semantics (`spec/Spec/System/Capacity.lean:71-87`). | No workload-brake test exists. | **Missing.** The required audited, reason-bearing, reversible emergency control from ADR-0022 (`docs/adr/0022-layered-capacity-admission.md:69-72`) has no product or persistence surface. | +| Organization user logs in with that Organization's Feishu app | `GET /auth/feishu` and callback are HTTP-backed (`hub/src/admin/routes/authRoutes.ts:49-128`). The callback upserts a user and signs a cookie (`hub/src/admin/routes/authRoutes.ts:99-123`). | Login itself is public OAuth; Organization APIs later enforce session + active OWNER/ADMIN membership (`hub/src/admin/auth/guards.ts:43-113`). | `User.feishuOpenId` is one process-global unique identifier (`hub/prisma/schema.prisma:89-109`); the signed cookie carries user id/open id but no Organization or Feishu-connection identity (`hub/src/admin/auth/session.ts:18-23`). | Redirect and mocked callback are covered (`hub/test/integration/admin-auth.test.ts:164-230`); OAuth request construction/exchange is covered (`hub/test/unit/feishu-oauth.test.ts:8-81`). | **HTTP-backed but contract-divergent.** OAuth is built once from the process-global app credentials (`hub/src/admin/routes/authRoutes.ts:38-47`), and the implementation explicitly documents this deferral (`hub/src/admin/auth/feishuOAuth.ts:1-10`). There is no org-selecting login route, connection resolver or issuer/app identity in the user key. | +| Each Organization operates its own Feishu Bot | One WebSocket listener is production-wired (`hub/src/server.ts:76-90`). | Message actions are gated by project grants once a binding resolves (`hub/src/feishu/trigger.ts:715-755`), but there is no initial Organization/connection ownership gate for the inbound app. | Binding stores only `chatId -> projectId`, with no Feishu connection/application id (`hub/prisma/schema.prisma:287-304`). Event receipts store globally unique `eventId`, likewise without connection id (`hub/src/feishu/trigger.ts:687-707`). | Trigger, onboarding and event dedup are integration-tested (`hub/test/integration/trigger.test.ts:211-330`, `hub/test/integration/trigger.test.ts:878-928`). | **Feishu-backed for one bot, contract-divergent for SaaS.** Startup constructs one client and one WS client from one config (`hub/src/feishu/client.ts:854-901`). It cannot operate or route multiple Organization-owned apps. | +| Suspended/archived Organization is prevented from bot work | No platform status-change entry exists. Org HTTP APIs do reject non-ACTIVE Organizations (`hub/src/admin/auth/guards.ts:79-88`). | The Feishu trigger resolves an active chat binding directly and then project permissions; it does not load or check `Project.organization.status` (`hub/src/feishu/trigger.ts:715-755`). | Organization status exists (`hub/prisma/schema.prisma:30-52`). | No suspended-Organization trigger test exists. | **Partial and unsafe.** Even a database-side status change only blocks web admin; an already-bound chat remains triggerable through the bot path. | +| Logged-in Organization OWNER/ADMIN reaches the management panel | OAuth success redirects to `/admin/org/:slug` (`hub/src/admin/routes/authRoutes.ts:237-267`). | All Organization APIs default to OWNER/ADMIN through `requireOrgRole` (`hub/src/admin/auth/guards.ts:66-113`), and tests prove MEMBER is denied while ADMIN succeeds (`hub/test/integration/admin-auth.test.ts:95-131`). | Session is an HMAC cookie; user/membership is re-read for each guard (`hub/src/admin/auth/session.ts:31-57`, `hub/src/admin/auth/guards.ts:43-63`). | Guard behavior is integration-tested (`hub/test/integration/admin-auth.test.ts:56-131`). | **API-backed, UI missing.** The plugin says the static SPA is “later” (`hub/src/admin/plugin.ts:1-3`); no route/static handler serves `/admin/org/:slug`. OAuth therefore redirects to an unmounted page. | +| Organization OWNER/ADMIN manages members and roles | REST endpoints list/add/change/revoke members (`hub/src/admin/routes/membersRoutes.ts:20-98`). | OWNER/ADMIN route guard plus service rules: only OWNER may affect OWNER and the last OWNER cannot be removed (`hub/src/org/members.ts:1-8`, `hub/src/org/members.ts:97-205`). | `OrganizationMembership` persists org, user, role and revocation (`hub/prisma/schema.prisma:54-76`). | Add/list/last-OWNER protection is covered through HTTP (`hub/test/integration/admin-members-teams.test.ts:49-83`). | **HTTP-backed and persisted; management UI missing.** Addition requires a raw `feishuOpenId` and may create the `User` directly (`hub/src/org/members.ts:46-85`); there is no invite/accept or org-connection directory picker. Role-change/revoke variants have less direct HTTP coverage than add/list. | +| Organization OWNER/ADMIN manages teams and team membership | REST endpoints cover list/create/update/archive and membership add/revoke (`hub/src/admin/routes/teamsRoutes.ts:21-139`). | All routes require Organization OWNER/ADMIN; service verifies the team and added user belong to the same Organization (`hub/src/org/teams.ts:177-215`, `hub/src/org/teams.ts:263-275`). | `Team` and `TeamMembership` are Organization/project-principal persistence (`hub/prisma/schema.prisma:143-177`). | Create/add-member/archive is covered through HTTP (`hub/test/integration/admin-members-teams.test.ts:85-160`). | **HTTP-backed and persisted; management UI missing.** No production route exposes `ExternalDirectoryConnection`/external team binding sync, so the Feishu directory portion remains service-only. | +| Organization OWNER/ADMIN creates and manages Folders/Projects | REST endpoints cover explorer, folder create/update/archive, project create/detail/rename/move/archive (`hub/src/admin/routes/explorerRoutes.ts:33-212`). | All routes require Organization OWNER/ADMIN. Service operations scope folder/project ids to the Organization (`hub/src/org/explorer.ts:301-341`); HTTP cross-org lookup is tested. | Folder and Project carry `organizationId`; Folder is transparent and Project owns workspace/session/run relations (`hub/prisma/schema.prisma:238-285`). Project creation also creates a real workspace and default permission rows (`hub/src/projectOnboarding.ts:275-348`). | Create/list and move/archive are HTTP-tested (`hub/test/integration/admin-explorer.test.ts:66-196`); cross-org id access is tested (`hub/test/integration/admin-explorer.test.ts:198-223`). | **HTTP-backed and substantially tested; management UI missing.** This is the strongest accepted product slice. | +| Organization OWNER/ADMIN manages Feishu chat binding | HTTP can archive a binding (`hub/src/admin/routes/explorerRoutes.ts:214-227`). New binding is exposed through the Feishu onboarding card, not web. | Archive requires OWNER/ADMIN at HTTP and then project manager authorization in the shared service (`hub/src/projectOnboarding.ts:231-272`, `hub/src/org/explorer.ts:301-314`). Bind existing requires project MANAGE (`hub/src/projectOnboarding.ts:179-229`). | `ProjectGroupBinding` and the FEISHU_CHAT EDIT grant are transactionally stored (`hub/src/projectOnboarding.ts:190-215`, `hub/prisma/schema.prisma:287-304`). | Bind/one-active/archive/rebind is service-tested (`hub/test/integration/project-onboarding.test.ts:114-160`); card bind is integration-tested (`hub/test/integration/trigger.test.ts:271-330`). No direct HTTP archive test was found. | **Feishu + partial HTTP-backed; UI missing.** Binding identity is not scoped by Feishu application, so the 1:1 uniqueness and lookup are process-global rather than Organization-connection-scoped. | +| Organization OWNER/ADMIN manages project team access | REST list/grant/revoke endpoints exist (`hub/src/admin/routes/accessRoutes.ts:19-79`). | OWNER/ADMIN route guard; service refuses a team from another Organization (`hub/src/permissions/projectTeamAccess.ts:168-195`). | TEAM -> PROJECT `PermissionGrant` persists the role (`hub/prisma/schema.prisma:428-447`). | Grant/list/archive-induced revoke is HTTP-tested (`hub/test/integration/admin-members-teams.test.ts:123-156`). | **HTTP-backed and tested; UI missing.** Only team grants are surfaced. There is no HTTP management of direct USER/chat grants, `PermissionSettings`, or per-role `RoleTriggerGrant`. | +| Organization OWNER/ADMIN manages general project settings, roles/models and agent policies | Only `membersCanCreateProjects` is reachable through GET/PATCH `/settings` (`hub/src/admin/routes/orgRoutes.ts:46-81`). | OWNER/ADMIN route guard. | `OrganizationProjectSettings` stores only that Boolean (`hub/prisma/schema.prisma:78-87`). `PermissionSettings` exists, but no admin route references it (`hub/prisma/schema.prisma:449-467`). Model/role registry is explicitly an in-memory skeleton awaiting DB/admin wiring (`hub/src/agent/models.ts:65-79`). | The Boolean setting is HTTP-tested (`hub/test/integration/admin-auth.test.ts:133-162`); model env behavior is unit-tested (`hub/test/unit/runtime-settings.test.ts:4-64`). | **Partial.** The creation policy works, but the accepted “projects and configuration” control plane lacks editable permission settings, model/role settings and run policies. | +| Organization OWNER/ADMIN chooses BYOK vs platform-managed Provider mode; BYOK admin writes key/base URL | None. | No Organization provider guard because no route/service exists. | No Prisma provider connection, credential mode, encrypted secret handle or rotation metadata. The only persistent `provider` fields are descriptive strings on sessions/runs (`hub/prisma/schema.prisma:308-330`, `hub/prisma/schema.prisma:352-387`). | No provider-mode test. Existing tests assert the global env resolver (`hub/test/unit/runtime-settings.test.ts:18-55`). | **Missing + contract divergence.** Lean/ADR have the mode but production always calls `settings.provider("openrouter", { projectId })` (`hub/src/feishu/trigger.ts:195-199`), whose implementation discards the scope (`hub/src/settings/runtime.ts:46-67`). | +| Organization OWNER/ADMIN views sessions and runs | HTTP list and detail endpoints exist (`hub/src/admin/routes/sessionsRoutes.ts:14-45`). | OWNER/ADMIN route guard; service scopes project/session through Organization id (`hub/src/org/sessions.ts:6-23`, `hub/src/org/sessions.ts:49-87`). | `AgentSession`/`AgentRun` persist provider/model/status/tokens/cost and relations (`hub/prisma/schema.prisma:308-387`). | No direct test for these HTTP routes or `org/sessions.ts` was found. Trigger tests do prove sessions/runs are written, e.g. separate role sessions (`hub/test/integration/trigger.test.ts:850-876`). | **HTTP-backed and persisted, but untested at the product boundary and UI missing.** | +| Organization OWNER/ADMIN views Organization/Project/Folder usage | HTTP Organization and Project usage endpoints exist, with optional folder/date filtering (`hub/src/admin/routes/sessionsRoutes.ts:47-79`). | OWNER/ADMIN route guard. | Rollup derives Organization through Project and uses persisted token/cost facts (`hub/src/org/usage.ts:35-77`, `hub/src/org/usage.ts:106-151`). Missing cost increments `runsWithoutCost`, not known cost (`hub/src/org/usage.ts:106-118`). | No direct usage route/service test was found. `/cost` tests cover the Feishu session command, not these admin endpoints (`hub/test/integration/trigger.test.ts:423-480`). | **HTTP-backed and persisted, but incomplete relative to ADR-0022.** It does not attribute by Provider Connection because none exists, has no alert configuration/delivery, no UI, and no product-boundary tests. | +| Ordinary MEMBER creates a Project from an unbound Feishu group | Mentioning the bot in an unbound chat sends an onboarding card (`hub/src/feishu/trigger.ts:850-887`); card action calls shared project creation (`hub/src/feishu/trigger.ts:554-614`). | User must already exist and have an active Organization membership; ordinary creation is gated by `membersCanCreateProjects` (`hub/src/projectOnboarding.ts:149-177`, `hub/src/projectOnboarding.ts:380-403`). Creator receives USER MANAGE and the chat receives EDIT (`hub/src/projectOnboarding.ts:302-340`). | Project, Folder, workspace, binding, default settings and grants are persisted (`hub/src/projectOnboarding.ts:275-348`). | Card display/create/bind is integration-tested (`hub/test/integration/trigger.test.ts:211-330`); policy denial and grants are service-tested (`hub/test/integration/project-onboarding.test.ts:70-112`). | **Feishu-backed and well-tested for the single-app case.** For a user with multiple active memberships, onboarding refuses and says the org selector does not exist (`hub/src/feishu/trigger.ts:889-929`). With a per-Org app, the inbound connection should supply tenant context; current global bot cannot. | +| Ordinary MEMBER binds an existing manageable Project | Onboarding card lists unbound projects and card action binds one (`hub/src/feishu/trigger.ts:868-885`, `hub/src/feishu/trigger.ts:932-969`). | Non-admin candidate projects require `collaborator.manage`; bind service rechecks project MANAGE (`hub/src/feishu/trigger.ts:951-960`, `hub/src/projectOnboarding.ts:179-215`). | Binding + FEISHU_CHAT EDIT grant persist transactionally. | Card journey is integration-tested (`hub/test/integration/trigger.test.ts:271-330`). | **Feishu-backed for the single-app case; per-Organization connection routing is absent.** | +| Ordinary authorized member triggers an Agent Run from Feishu | Bound message event resolves Project, evaluates `agent.trigger`, resolves role/model/provider, creates session/run and starts the runner (`hub/src/feishu/trigger.ts:684-848`, especially `hub/src/feishu/trigger.ts:715-755` and `hub/src/feishu/trigger.ts:140-256`). | Missing sender id is fail-closed; project EDIT+ and optional role grant are enforced (`hub/src/feishu/trigger.ts:171-194`, `hub/src/feishu/trigger.ts:726-755`). | Session/run, messages, file changes, event receipt and audit fragments persist in Prisma (`hub/prisma/schema.prisma:306-405`, `hub/prisma/schema.prisma:496-525`). | Lifecycle, permission denial, dedup, audit, messages and interrupt have integration coverage (`hub/test/integration/trigger.test.ts:116-154`, `hub/test/integration/trigger.test.ts:597-607`, `hub/test/integration/trigger.test.ts:878-928`, `hub/test/integration/trigger.test.ts:983-1100`). | **Feishu-backed and tested for one process-global provider/Feishu connection.** It is not an Organization-resolved run: provider scope is ignored and bot/identity connection is absent. | +| Accepted work waits under Project/Organization/platform capacity and survives restart | Only a process-local queue per Project exists (`hub/src/feishu/triggerQueue.ts:18-45`). It is drained after the active Project run (`hub/src/feishu/trigger.ts:430-495`). | No Organization admission identity or capacity policy is evaluated. | Queue is a `Map` in memory (`hub/src/feishu/triggerQueue.ts:21-26`), not a Prisma model; accepted requests vanish on process exit. Expired entries are dropped and only logged (`hub/src/feishu/trigger.ts:475-483`). | FIFO/full/expiry behavior is unit-tested (`hub/test/unit/trigger-queue.test.ts:10-78`); locked-trigger enqueue/drain is integration-tested (`hub/test/integration/trigger.test.ts:609-673`). | **Contract divergence.** ADR-0022 requires durable bounded admission, Organization-fair scheduling, explicit EXPIRED/CANCELED states and notifications (`docs/adr/0022-layered-capacity-admission.md:33-46`); none are present. | +| Platform ceiling + optional lower Organization policy protect request/run/file/storage/entity capacity | None beyond Fastify defaults, a five-item Project queue and global max turns. `Fastify({ logger: true })` has no capacity options/plugins (`hub/src/server.ts:55-55`); dependencies include no rate-limit plugin (`hub/package.json:9-17`). | No admin policy route. | No capacity/limit model. `OrganizationProjectSettings` has only `membersCanCreateProjects` (`hub/prisma/schema.prisma:78-87`). Run policy contains only global `maxTurns` (`hub/src/settings/runtime.ts:24-37`, `hub/src/settings/runtime.ts:74-78`). The systemd unit has no CPU/memory/process controls (`hub/deploy/cph-hub.service:6-26`). | Unit tests cover only global max turns and the local queue (`hub/test/unit/runtime-settings.test.ts:58-64`, `hub/test/unit/trigger-queue.test.ts:5-113`). | **Missing.** None of ADR-0022's mandatory platform/org request rate, body, concurrency, file/archive, storage, entity-count, wall-time/tool/output or process limits are product-reachable or persistently enforced (`spec/Spec/System/Capacity.lean:13-60`). | + +## Reachability summary + +### Production-wired HTTP surface + +The actual production plugin mounts these capability groups: + +- global Feishu OAuth/session: `/auth/feishu`, callback, logout, `/api/me`, and + the minimal `/admin/login` HTML (`hub/src/admin/routes/authRoutes.ts:49-207`); +- Organization summary and one onboarding Boolean + (`hub/src/admin/routes/orgRoutes.ts:27-81`); +- Folder/Project explorer and binding archive + (`hub/src/admin/routes/explorerRoutes.ts:33-227`); +- member roles (`hub/src/admin/routes/membersRoutes.ts:20-98`); +- teams/team membership (`hub/src/admin/routes/teamsRoutes.ts:21-139`); +- TEAM -> PROJECT grants (`hub/src/admin/routes/accessRoutes.ts:19-79`); +- sessions and usage (`hub/src/admin/routes/sessionsRoutes.ts:14-79`). + +Every `/api/org/:orgSlug/*` route uses the same OWNER/ADMIN guard. This satisfies +the clarified requirement that a management API cannot be used by an anonymous +or ordinary Organization member (`hub/src/admin/auth/guards.ts:43-113`). It does +not provide the accepted management **panel**: only `/admin/login` is rendered, +and the post-login `/admin/org/:slug` destination is not served. + +### Production-wired Feishu surface + +The trigger handler is a real end-to-end application path for one Feishu app: +event receipt/dedup, chat binding, permission checks, unbound-chat onboarding, +Project creation/binding, sessions/runs, streaming and interrupt. The service +wires the handler to a single SDK client and WebSocket listener +(`hub/src/server.ts:76-90`, `hub/src/feishu/client.ts:854-901`). The code and tests +therefore establish a useful single-app vertical slice, not the required +per-Organization connection architecture. + +### Only database/service/test reachable + +- Organization creation is direct Prisma fixture work + (`hub/test/integration/helpers.ts:60-77`). +- `PlatformRoleAssignment` is a stored type with no platform auth/HTTP surface + (`hub/prisma/schema.prisma:111-129`). +- external-directory sync has service code and can create membership rows + (`hub/src/permissions/externalSync.ts:40-90`), but no registered production + admin route. +- project `PermissionSettings` and `RoleTriggerGrant` are stored/enforced in + parts of the runtime, but have no Organization management API + (`hub/prisma/schema.prisma:449-494`). + +### Entirely absent from runtime persistence/control plane + +- Organization-scoped Feishu application connection and encrypted secret + handle; +- Organization-scoped Provider Connection, BYOK/platform-managed mode, encrypted + secret handle, rotation and runtime resolver; +- platform administrator session/guard, platform Organization CRUD, platform + admin invitation, platform-operation audit; +- versioned platform capacity ceiling loader/validation; +- Organization policy-limit rows and Organization admin endpoints; +- durable run request/admission state, fair Organization scheduler, queue cancel + and expiry notification; +- Organization/platform workload brake state and audited actions; +- usage/cost alert configuration and delivery; +- the Organization and platform management UI. + +## Critical production blockers + +1. **No Organization connection control plane.** The running service has one + Feishu app/bot and one provider token/base URL. This contradicts the defining + SaaS requirement even though Project/Team data is otherwise Organization- + scoped (`hub/src/server.ts:44-52`, `hub/src/settings/runtime.ts:46-67`). + +2. **No platform control plane.** A Platform Administrator cannot authenticate, + create or operate an Organization, provision its Feishu app, configure its + platform-managed provider connection, or apply a workload brake. The only + apparent Organization creation path is direct database/test code. + +3. **No usable management panel.** The backend Organization APIs are meaningful + and guarded, but OAuth redirects to an unmounted `/admin/org/:slug`; platform + UI does not exist at all (`hub/src/admin/plugin.ts:1-8`, + `hub/src/admin/routes/authRoutes.ts:237-267`). + +4. **ADR-0022 is almost wholly unimplemented.** The in-memory Project queue is + specifically the implementation divergence ADR-0022 rejects; there are no + Organization-fair durable admission, mandatory layered limits, hard run/file/ + storage/process budgets or emergency brakes + (`docs/adr/0022-layered-capacity-admission.md:79-88`). + +5. **Tenant context is inferred too late and from global identifiers.** OAuth + creates a globally keyed `User`, bot events bind globally by `chatId`, and an + unbound-chat user with more than one Organization is rejected rather than + routed by the customer-owned Feishu connection (`hub/prisma/schema.prisma:89-109`, + `hub/src/feishu/trigger.ts:715-723`, `hub/src/feishu/trigger.ts:889-929`). This + blocks a correct multi-app implementation and also leaves Organization status + unenforced on the bot path. + +## What is already worth retaining + +The audit does not imply rewriting the working core. The following seams are +concrete and have useful test evidence: + +- Organization ownership on Project/Team/Folder and same-Organization access + checks (`hub/prisma/schema.prisma:143-177`, `hub/prisma/schema.prisma:238-285`); +- OWNER/ADMIN guards and last-OWNER protection + (`hub/src/admin/auth/guards.ts:66-113`, `hub/src/org/members.ts:97-205`); +- reusable service operations for project creation, chat binding and grants + (`hub/src/projectOnboarding.ts:128-229`, `hub/src/projectOnboarding.ts:275-348`); +- Organization-scoped explorer/member/team/team-access HTTP APIs and their + integration tests; +- Feishu onboarding, project permission checks, session/run persistence and + dedup in the single-app vertical slice. + +The shortest path to the accepted destination is therefore to add the missing +platform/connection/capacity control planes and real admin UI around these seams, +then change ingress and runtime resolution to carry an explicit Organization +connection identity end to end. Treating the existing env credentials as an +acceptable pilot fallback would preserve the wrong tenant boundary. diff --git a/.scratch/saas-production-readiness/assets/product-surface-completeness-audit.md b/.scratch/saas-production-readiness/assets/product-surface-completeness-audit.md new file mode 100644 index 0000000..3f5e818 --- /dev/null +++ b/.scratch/saas-production-readiness/assets/product-surface-completeness-audit.md @@ -0,0 +1,259 @@ +# Accepted SaaS product-surface completeness audit + +Audit date: 2026-07-10 + +## Verdict + +The current Hub is **not a usable or production-complete SaaS product surface** +for the accepted destination. It has a worthwhile Organization-scoped backend +foundation and a well-tested single-Feishu-application Agent Run slice, but the +running product still has one process-global Feishu app/bot and one global model +provider credential. It has no platform-administrator control plane, no +Organization connection/secret plane, and no Organization-scoped Provider +Credential Mode. Its only HTML product page is a login form; successful OAuth +redirects an administrator to an unmounted `/admin/org/:orgSlug` route. + +ADR-0022's required production safety surface is also almost wholly absent: +there is no durable Organization-fair admission, layered platform/Organization +limits, hard file/storage/run/process budgets, usage-alert control plane, or +audited workload brake. These are release blockers, not optional polish. + +The strongest code should be retained rather than rewritten: all current +`/api/org/:orgSlug/*` routes reauthorize an active OWNER/ADMIN membership; +Folder/Project, member, Team, Team-to-Project access, session, and usage services +are Organization-scoped; and the shared project-onboarding services correctly +create Project/chat/grant relationships for the single-app case. The shortest +route is to add explicit Organization connection identity and the two private +admin control planes around those seams. + +The claim-by-claim source inventory, including route, schema and test locations, +is in [accepted product-surface code inventory](product-surface-code-inventory.md). +Official Feishu identifier, OAuth, event and long-connection semantics are in +[Feishu product-surface primary sources](product-surface-feishu-primary-sources.md). + +## Accepted boundary + +The audit treats these as required because they are already accepted by the +repository's `CONTEXT.md`, ADR-0020, ADR-0021, ADR-0022, and Lean contract: + +- a Platform Administrator is separate from Organization membership and uses a + private `/admin/platform` area to create and operate Organizations; +- an Organization OWNER/ADMIN uses a private `/admin/org/:orgSlug` area to + manage members, projects, Teams, connections, settings, sessions and usage; +- every Organization authenticates and exchanges bot/directory traffic through + its own customer-owned Feishu Application; +- every Organization chooses BYOK or an Organization-distinct platform-managed + Provider Connection, with different administrators owning the respective + plaintext inputs; +- ordinary members can create/bind a Project from an unbound Feishu group only + under the Organization policy, while all Project actions retain Project-level + authorization; +- production has mandatory platform ceilings, optional lower Organization + limits, durable fair admission, bounded work and audited emergency brakes. + +The audit does not add deferred self-service signup, payment collection, +folder ACL inheritance, commercial billing, or a live editor for platform +capacity ceilings. Guided Feishu app setup and manually initiated Organization +creation by an authenticated Platform Administrator remain valid for the pilot. + +## Journey result + +| Accepted journey | Current product entry | Result | Owning frontier | +| --- | --- | --- | --- | +| Platform Administrator signs in | No platform login/session/guard; only a stored `PlatformRoleAssignment` type | **Missing** | tickets 42, 45 | +| Platform Administrator creates and operates Organizations | Direct Prisma calls in test fixtures only; no `/api/platform/*` or UI | **Test/database only** | ticket 45 | +| Platform Administrator configures each customer Feishu app | One env `FEISHU_APP_ID/SECRET/BOT_OPEN_ID`; no connection or secret model | **Missing and contract-divergent** | tickets 43–45 | +| Platform Administrator supplies a distinct platform-managed provider key/base URL | One process-global `ANTHROPIC_AUTH_TOKEN/BASE_URL`; resolver ignores Project scope | **Missing and contract-divergent** | tickets 43, 45 | +| Platform Administrator applies and reverses `DRAIN`/`STOP_NOW` | No persistence, guard, API or UI | **Missing** | tickets 37, 45 | +| Organization user logs in through that Organization's Feishu app | OAuth exists, but is built once from the global app and has no Organization/application issuer | **Single-app only** | tickets 43, 44 | +| Logged-in Organization OWNER/ADMIN opens the management panel | OAuth redirects to `/admin/org/:slug`; no handler/static application serves it | **Stable 404** | ticket 46 | +| Organization OWNER/ADMIN manages members, Teams, Projects/Folders and Team access | Guarded Organization JSON APIs and useful integration coverage exist | **Backend present; browser product absent** | ticket 46 | +| Organization OWNER/ADMIN manages permission/model/role/run policy | Only `membersCanCreateProjects` is exposed; other stored/runtime settings have no control plane | **Partial** | ticket 46 | +| Organization selects Provider Credential Mode and manages BYOK | No mode/connection model, API or UI | **Missing** | tickets 43, 46 | +| Organization views sessions and usage | Guarded JSON APIs exist, but have no direct product-boundary tests or UI and cannot attribute Provider Connection | **Partial** | tickets 36, 46 | +| Ordinary member creates or binds a Project in Feishu | Card/service path is present and well tested | **Works for the one-global-app case** | ticket 44 for SaaS routing | +| Authorized member runs the agent through its Organization bot | Full single-app trigger path exists; provider/app resolution is global | **Works only as a single-app deployment** | tickets 43, 44 plus lifecycle tickets | +| Accepted work queues fairly and durably across Organizations | Five-item process-memory Project queue | **Contract-divergent** | tickets 23, 32–35 | +| Organization sets lower policy limits and receives soft alerts | No policy/alert persistence or product surface | **Missing** | tickets 32, 34–36, 46 | + +## Executed defect evidence + +The probes used the real Fastify plugin and test PostgreSQL database through a +temporary Vitest integration file. The file was removed after capture and was +run twice to separate deterministic behavior from a transient test failure. + +### 1. The successful-login destination is not served + +The probe seeded an active Organization ADMIN, minted the normal signed session +cookie, and requested the exact destination selected by +`resolvePostLoginRedirect`: + +```text +GET /admin/org/test-default +expected_status=200 +actual_status=404 +``` + +Both runs returned 404. The root cause is structural: `registerAdminPlugin` +mounts cookie, auth and Organization API routes only; its own comment says the +static SPA is “later”. No other repository HTML/TSX/Vue/Svelte/CSS product +application or deployment-level static handler exists. Existing OAuth tests +assert only the redirect `Location`, so they stop immediately before the broken +journey. + +This is not an authorization defect in the JSON APIs. Those APIs correctly +return 401 without a session and 403 to a MEMBER. It is a reachability defect: +there is no management panel for either an authorized or unauthorized request +to reach. Ticket 46 must guard both the page shell and its data, so an anonymous +user may reach only the login entry and never management content. + +### 2. Different Organizations start OAuth with the same application + +The probe seeded two Organizations and requested the current OAuth entry with +two different Organization return paths: + +```text +GET /auth/feishu?returnTo=/admin/org/test-default +client_id=cli_process_global + +GET /auth/feishu?returnTo=/admin/org/second +client_id=cli_process_global +``` + +Both runs produced the same result. The root cause is that +`registerAuthRoutes` constructs one immutable OAuth configuration from plugin +startup arguments. The `returnTo` path is signed only as a destination; it does +not resolve or bind an Organization Feishu Application. `server.ts` obtains the +same app credentials from one environment tuple and also uses them for the only +WebSocket listener. + +Changing only the redirect page cannot fix this. The composition root and every +identifier-bearing record/action need an explicit Organization connection +identity, backed by the encrypted resolver in ticket 43. + +## Feishu identifier and transport consequence + +Feishu's official identifier contract makes the current global identifier +shape unsafe for customer-owned applications: + +- `open_id` identifies a user inside one application and differs across apps; +- `user_id` is stable inside one tenant but differs across tenants; +- `union_id` can link apps from the same application service provider, but the + service cannot assume independently customer-owned apps share that provider; +- OAuth authorization identifies the app with `client_id`; the callback brings + `code` and `state`, so state and token exchange must remain bound to the + selected Organization/application; +- v2 HTTP event/card headers expose application and tenant identity, while an + encrypted outer request must be routed to the correct connection before its + verification/decryption key is known; a long connection is itself created by + one app credential pair, so the local client/listener identity is the issuer; +- Feishu rejects cross-tenant chat/operator combinations and does not provide a + contract that permits `chat_id` to be treated as a global SaaS key. + +The current `User.feishuOpenId @unique`, session `{userId, feishuOpenId}`, +`ProjectGroupBinding.chatId`, `FeishuEventReceipt.eventId`, principal IDs, +sender caches, and batching keys omit this issuer namespace. An `open_id` +collision across customer apps could merge unrelated external identities; +different `open_id` values for one person across apps cannot be resolved as one +global identity automatically. A globally looked-up `chatId` or event ID has +the analogous ambiguity. + +The implementation must therefore persist provider-local external identities +under a Feishu Application Connection, for example conceptually +`(connectionId, externalIdType, externalId)`, and carry that connection through +OAuth state/session, ingress, deduplication, chat binding, authorization, +onboarding/card actions, rate-limit keys, directory sync, outbound delivery and +audit. Whether two external identities represent the same global `User` is a +separate explicit linking decision; it must not be inferred from a provider ID +outside its documented scope. + +## Root causes rather than symptoms + +### 1. Tenant data was added without tenant-scoping external connections + +Organization ownership is correctly present on Projects, Teams, Folders and +directory-connection metadata, but the process composition still comes from +the former single-deployment shape. The app/bot/provider credentials and the +identifiers they issue never crossed an Organization connection boundary. +`RuntimeScope.projectId` exists but is discarded by the env resolver, making +the missing seam especially explicit. + +### 2. The control-plane work stopped at guarded Organization APIs + +The Organization routes provide a credible backend slice, but there is no web +application, platform route family, platform guard, Organization provisioning +service, connection configuration, or browser journey test. The comment +“until admin SPA lands” and redirect-only OAuth assertion document the precise +unfinished handoff. A small catch-all page would only hide the gap; the fix must +deliver the authenticated, authorized workflows and their safe states. + +### 3. Production capacity semantics exist only in the contract + +ADR-0022 intentionally declares the in-memory queue and global-only settings to +be implementation divergences. There is no common admission identity that can +join Organization, Feishu connection, Project, user and request, so fair +scheduling, workload brakes, rate limits, alerts and control-plane operations +cannot currently compose. The previously created capacity tickets own this +implementation; the admin tickets must expose their safe operator/customer +surfaces rather than invent parallel controls. + +## What should remain + +- Keep `requireOrgRole`'s per-request session, active Organization, active + membership and OWNER/ADMIN checks; extend the same principle to page data and + add a separate `requirePlatformAdmin` rather than an Organization override. +- Keep the Organization-scoped explorer/member/Team/access/session/usage APIs + as the initial backend for the org panel, while filling their missing tests, + pagination/limits, audit and typed-error contracts through existing tickets. +- Keep shared Project creation and chat-binding services so web and Feishu + entries do not drift into two rule sets. +- Keep the single-app Feishu handler's authorization, onboarding, run/session + and dedup behavior, but call it from a connection-bound listener context and + move accepted work/delivery into the durable lifecycle tickets. +- Keep missing provider cost unknown rather than fabricating zero; add Provider + Connection attribution before presenting SaaS usage claims. + +## Newly clear execution frontier + +The product audit creates four implementation/decision slices after the +existing secret and browser-boundary design work: + +1. [Decide the platform-administrator identity and audit boundary](../issues/42-decide-platform-admin-identity-audit.md). +2. [Implement the Organization connection and secret plane](../issues/43-implement-org-connection-secret-plane.md). +3. [Route Feishu identity and traffic by Organization connection](../issues/44-route-feishu-by-org-connection.md). +4. [Build the private platform-administrator control plane](../issues/45-build-private-platform-admin-control-plane.md). +5. [Build the private Organization-administrator control plane](../issues/46-build-private-org-admin-control-plane.md). + +The Feishu routing slice precedes durable inbound/action/outbound and signed +traffic limiting so those mechanisms use the right issuer namespace from their +first durable schema. The platform panel precedes the emergency-brake product +entry so ticket 37 can reuse one authenticated/audited operator boundary. +Existing tickets 32–38 continue to own capacity enforcement, attribution, +alerts, brakes and calibration; the new UI tickets do not duplicate them. + +## Required production journey evidence + +The final release gate must run against at least two configured customer-owned +Feishu applications and two Organizations, using deliberately colliding fake +provider-local IDs where the test harness permits. It must prove: + +1. an unauthenticated request and an authenticated wrong-role user cannot load + either management page data or perform mutations; +2. a Platform Administrator can create an Organization, bootstrap its OWNER, + configure/test its Feishu app and distinct platform-managed Provider + Connection, with redacted responses and complete audit history; +3. an Organization OWNER/ADMIN logs in through that Organization's app, reaches + the real org panel, manages the accepted settings/resources, configures BYOK + when selected, and cannot read another Organization or platform secret; +4. each bot receives, binds, authorizes, queues and replies only inside its + connection's Organization even when `open_id`, `chat_id`, event or callback + values collide; SUSPENDED/ARCHIVED Organizations fail closed; +5. provider resolution for every run returns that Organization's selected + connection and never a process-global fallback; +6. current member onboarding, Project authorization and Agent Run behavior + remain intact after connection namespacing; +7. layered limits, fair admission, usage alerts and `DRAIN`/`STOP_NOW` are + visible only to the corresponding authorized control plane and behave as + proven by their dedicated fault/capacity suites; +8. the complete browser + Feishu journey survives the production-shaped deploy, + restart, backup/restore and rollback evidence owned by ticket 08. diff --git a/.scratch/saas-production-readiness/assets/product-surface-feishu-primary-sources.md b/.scratch/saas-production-readiness/assets/product-surface-feishu-primary-sources.md new file mode 100644 index 0000000..0e44a18 --- /dev/null +++ b/.scratch/saas-production-readiness/assets/product-surface-feishu-primary-sources.md @@ -0,0 +1,333 @@ +# Feishu multi-app identity and event-routing primary sources + +This note answers one narrow design question for the production SaaS audit: +what identity and routing boundaries apply when **every Hub Organization uses +its own Feishu App and bot**. External claims below use only first-party Feishu +Open Platform documentation. Statements labelled **Derived requirement** are +Hub design implications, not guarantees made by Feishu. + +Research date: 2026-07-10. + +## Executive answer + +1. A customer-owned enterprise custom app is a single-tenant app: Feishu says + it is developed, reviewed, published, and used inside one tenant and cannot + be used by another organization. That fits “one Organization, one Feishu + App,” but it means the Hub is operating many independent app security and + identity domains, not one shared Feishu identity domain. + ([custom apps versus store apps](https://open.feishu.cn/document/home/app-types-introduction/self-built-apps-and-store-apps)) +2. A bare `open_id` is **not** a SaaS-global user key. It identifies a user + inside one app, and the same user has different Open IDs in different apps. + `user_id` is tenant-scoped; it is stable across apps in one tenant but + differs across tenants. `union_id` crosses apps only for apps from the same + application provider/developer; it differs between providers. + ([user resource overview](https://open.feishu.cn/document/server-docs/contact-v3/user/field-overview?lang=zh-CN), + [official ID parameter definitions](https://open.feishu.cn/document/server-docs/contact-v3/department/search?lang=zh-CN), + [Union ID guide](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-union-id)) +3. OAuth must be routed to the Organization's app **before** authorization. + The authorization request contains that app's App ID as `client_id`; the + callback contains `code` and the echoed `state`, not an independently + trustworthy Organization choice. Exchanging the code requires the matching + app's `client_id` and `client_secret`, and the same `redirect_uri`. + ([obtain authorization code](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code), + [obtain user access token](https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token)) +4. A version 2 event or callback carries `header.app_id` and + `header.tenant_key` after parsing/decryption. An encrypted HTTP delivery, + however, exposes only an `encrypt` envelope before decryption, so a shared + endpoint cannot safely choose among many apps' Encrypt Keys from the body. + ([callback structure](https://open.feishu.cn/document/event-subscription-guide/callback-subscription/callback-overview), + [encrypted Webhook delivery](https://open.feishu.cn/document/event-subscription-guide/event-subscriptions/event-subscription-configure-/choose-a-subscription-mode/send-notifications-to-developers-server?lang=zh-CN)) +5. A WebSocket/long-connection client is authenticated with one app's App ID + and App Secret. Therefore its local client instance is the pre-authenticated + source-app route. It is not a process-global “all customer apps” listener. + ([long-connection event subscription](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case?lang=zh-CN)) +6. Feishu documents `chat_id` as the conversation identifier, but the cited + documentation does not promise that a raw `chat_id` is globally unique + across every tenant and app. Chat APIs explicitly reject an operator and + chat from different tenants and often require the app bot to be in the + chat. A bot is represented by its application's `app_id`. + ([common parameters](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology), + [get chat members](https://open.feishu.cn/document/server-docs/group/chat-member/get?lang=zh-CN), + [message resource](https://open.feishu.cn/document/server-docs/im-v1/message/intro)) + +## 1. Application and tenant boundary + +### Official facts + +- `app_id` is the immutable identifier generated by Feishu for an application. + `tenant_key` is the enterprise identifier; one `tenant_key` corresponds to + one enterprise in actual use. + ([common parameters](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology)) +- An enterprise custom app is restricted to one tenant. Its developers, + administrators, and users belong to that tenant, and another organization + cannot use it. A store app is the contrasting model that can be installed by + multiple tenants. + ([custom apps versus store apps](https://open.feishu.cn/document/home/app-types-introduction/self-built-apps-and-store-apps)) +- Access tokens tell Feishu which app/user is calling and which tenant's data + is being accessed. A `tenant_access_token` calls as the application and its + data range is determined by that application's permissions. + ([access-token overview](https://open.feishu.cn/document/server-docs/api-call-guide/calling-process/get-access-token)) +- A custom app obtains its `tenant_access_token` using that app's `app_id` and + `app_secret`. + ([custom-app tenant access token](https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal?lang=zh-CN)) + +### Derived requirements + +- Model a durable `FeishuAppConnection` (or equivalent) owned by exactly one + Hub Organization. At minimum it binds the internal connection ID, + `organization_id`, `app_id`, expected `tenant_key`, encrypted `app_secret`, + event/callback credentials, lifecycle state, and credential version. +- Do not select Feishu credentials from a process-global default. Every OAuth + exchange, token cache entry, OpenAPI call, Webhook verification, and + long-connection client must start from the Organization's connection. +- Pin the first verified `tenant_key` to the connection. Later login, event, + and callback data must match both the expected `app_id` and pinned + `tenant_key`; unknown or mismatched pairs must be rejected rather than + auto-attached to another Organization. + +## 2. User identifier scopes + +### Official scope matrix + +| Identifier | Feishu's documented scope | Cross-app/tenant behavior | Availability caveat | +| --- | --- | --- | --- | +| `open_id` | User identity inside one application | The same user has a different `open_id` in different apps | The normal app-relative user identifier | +| `user_id` | User identity inside one tenant | Stable across all apps in the same tenant; different in tenant A and tenant B | Reading it is a sensitive field permission and is limited to custom apps in the cited user API | +| `union_id` | User identity across apps from the same application provider/developer | Same across that provider's apps; different across different providers | It does not make unrelated customer-owned apps one identity domain | +| `tenant_key` | Enterprise identity | One key corresponds to one enterprise | Returned by login and carried in events/callbacks | + +Sources: + +- Feishu's current user resource overview states that `user_id` is tenant + identity, `open_id` is app identity, and `union_id` is identity across apps + developed by the same application provider. It expressly says the same + user's Open ID differs between apps. + ([user resource overview](https://open.feishu.cn/document/server-docs/contact-v3/user/field-overview?lang=zh-CN)) +- Feishu's API parameter text is more explicit: the same user's User ID differs + between tenants but remains the same across apps within one tenant; the same + user's Union ID is the same under one developer and different under another. + ([official ID parameter definitions](https://open.feishu.cn/document/server-docs/contact-v3/department/search?lang=zh-CN)) +- Feishu describes Union ID as the identity linking one user across multiple + applications supplied by the same application service provider. + ([Union ID guide](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-union-id)) +- Feishu's common-error guidance says not to use an Open ID obtained from app A + in app B, and even a test app and its production app produce different Open + IDs for the same user. It also reports cross-tenant errors for both User ID + and Union ID use. + ([common errors](https://open.feishu.cn/document/ukTMukTMukTM/ugjM14COyUjL4ITN?op_tracking=hc)) +- The current login user-info API returns `open_id`, `union_id`, optional + sensitive `user_id`, and `tenant_key` from a `user_access_token`. + ([get login user information](https://open.feishu.cn/document/server-docs/authentication-management/login-state-management/get?lang=zh-CN)) + +### Answer: can different customer Apps share one global `open_id` key? + +**No.** This directly contradicts Feishu's documented Open ID semantics. Two +customer-owned custom apps are different applications, so the same person has +different Open IDs in them. Because those apps are independently owned custom +apps, the Hub also has no documented basis to assume they are applications of +one application provider and therefore no basis to auto-merge on `union_id`. +`user_id` cannot solve cross-customer identity either because it changes across +tenants. + +Derived identity model: + +- The safe external login key is `(feishu_app_connection_id, open_id)`; retain + `tenant_key`, `user_id`, and `union_id` as scoped attributes, not as an + unqualified SaaS-global primary key. +- If the product needs one Hub human account to participate in multiple + Organizations, represent each Feishu login as a separate external identity + attached through an explicit, separately verified account-linking flow. Do + not silently merge on Open ID, Union ID, email, phone, or display name. +- Do not make `user_id` mandatory for login unless every customer app is + required to obtain its sensitive field permission. `open_id` and + `tenant_key` are returned by the user-info flow without using `user_id` as + the login key. +- Replacing an Organization's Feishu App changes the application identity + domain and therefore its users' Open IDs. Treat app replacement as an + explicit connection migration/re-link operation, not an in-place secret + edit. + +## 3. OAuth login must be app-routed + +### Official facts + +- The authorization URL requires `client_id`, which is the selected + application's App ID. `redirect_uri` must already appear in that + application's security settings. On success Feishu redirects with `code` + and, when supplied, the original `state`. + ([obtain authorization code](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)) +- Feishu says `state` maintains request/callback context and must be compared + to prevent CSRF. Its official sample rejects a callback whose state differs + from the stored state. + ([obtain authorization code](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code), + [user-access-token sample](https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token)) +- The code lasts five minutes and can be used once. Token exchange requires + `client_id`, `client_secret`, `code`, and, when used during authorization, + the same `redirect_uri`. Feishu defines error 20024 for a code/refresh token + that does not match the supplied client ID and error 20071 for a mismatched + redirect URI. + ([obtain user access token](https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token)) +- Only redirect URLs configured for the selected application pass Feishu's + security check; an app may configure multiple URLs. + ([access-token acquisition flow](https://open.feishu.cn/document/server-docs/api-call-guide/calling-process/get-access-token)) + +### Derived request flow + +1. Begin at an Organization-qualified route or invitation, resolve exactly one + active `FeishuAppConnection`, and create a short-lived, one-time server-side + login transaction bound to that Organization, connection, redirect URI, + desired return path, and PKCE verifier if used. +2. Put only a high-entropy opaque transaction nonce in OAuth `state`; generate + the authorization URL with that connection's `app_id` as `client_id`. +3. On callback, atomically consume and validate `state` before selecting + credentials. The callback's untrusted query parameters must never be + allowed to choose another Organization or App. +4. Exchange `code` with the connection's `app_id`, decrypted `app_secret`, and + the exact stored redirect URI. Then call user-info and require its + `tenant_key` to equal the connection's pinned tenant. +5. Resolve or create the membership through + `(feishu_app_connection_id, open_id)`. Never search all Organizations for a + bare Open ID and accept the first match. + +A single physical callback URL may be configured independently in multiple +apps, but a shared URL does not remove the need for state-bound app routing. + +## 4. HTTP event and callback routing + +### Official facts + +- A version 2 callback header contains the application's Verification Token, + `event_type`, `tenant_key` (the callback application's tenant), and `app_id` + (the callback application's App ID). + ([callback overview and structure](https://open.feishu.cn/document/event-subscription-guide/callback-subscription/callback-overview)) +- Version 2 event examples likewise place `app_id` and `tenant_key` in the + event header. + ([user-leaves-chat event](https://open.feishu.cn/document/server-docs/group/chat-member/event/deleted-2)) +- Verification Token is an application validation identifier. Feishu says the + server can compare the event's token with that application's token to check + that a request is for the specified app. With an Encrypt Key, the request + signature is calculated from request timestamp, nonce, that app's Encrypt + Key, and the original request body. + ([receive and verify events](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/encrypt-key-encryption-configuration-case)) +- When an Encrypt Key is configured, the HTTP request body visible before + decryption is an envelope containing only `{"encrypt":"..."}`; `app_id` + and `tenant_key` are inside the ciphertext. + ([send events to developer server](https://open.feishu.cn/document/event-subscription-guide/event-subscriptions/event-subscription-configure-/choose-a-subscription-mode/send-notifications-to-developers-server?lang=zh-CN)) + +### Derived routing and verification requirements + +- Give every Feishu connection a stable, unguessable callback route such as a + connection-specific path token. Resolve the connection from that route, + load exactly its Encrypt Key/Verification Token, verify and decrypt, then + require the authenticated payload's `app_id` and `tenant_key` to match the + route's connection. +- For an unencrypted common endpoint, `header.app_id` may identify a candidate + connection, but it is untrusted until that connection's signature/token has + been verified. Never route the side effect solely from an unverified body. +- A single encrypted URL shared by every customer app has no documented + pre-decryption app selector. Trying every tenant's secret is not an identity + protocol. Use connection-specific routing instead. +- Namespace delivery deduplication by the authenticated connection (for + example `(connection_id, event_id)`), and dispatch business work only after + app/tenant verification. Unknown, disabled, or mismatched connections fail + closed and produce an auditable security log. + +## 5. Long-connection routing + +### Official facts + +- Feishu's SDK creates a WebSocket client with one app's required `APP_ID` and + `APP_SECRET`. Events subscribed by that application are then delivered over + that channel. Authentication is performed when the connection is + established. + ([long-connection event subscription](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case?lang=zh-CN)) +- Long-connection event subscription supports enterprise custom apps, not + store apps. Each app may establish at most 50 connections. If one app has + multiple clients, delivery is cluster-mode rather than broadcast: one random + client receives a message. + ([long-connection event subscription](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case?lang=zh-CN)) +- Long-connection callbacks use the same per-app App ID/App Secret client + construction and have the same cluster-mode single-recipient behavior. + ([long-connection callback subscription](https://open.feishu.cn/document/event-subscription-guide/callback-subscription/step-1-choose-a-subscription-mode/configure-callback-request-address?lang=zh-CN)) + +### Derived routing requirements + +- Create one logical client binding per enabled `FeishuAppConnection`, even if + an in-process supervisor manages many clients. Attach immutable + `organization_id` and `connection_id` context to each handler; the client + that received the frame is the pre-authenticated application route. +- Never infer the receiving Organization by looking up a bare actor Open ID or + chat ID. First use the authenticated client binding; then cross-check any + delivered `app_id`/`tenant_key` fields available in the event structure. +- Scaling several replicas for the same app does not create duplicate + broadcast processing according to Feishu's contract, but delivery still + requires application-level idempotency and health ownership. Capacity must + count clients per app against the documented 50-connection ceiling. + +## 6. Bot, chat, and message identifiers + +### Official facts + +- In the message resource, a sender can be a user or an application. For an + application sender, `id_type` is `app_id`; the sender also carries a + `tenant_key`. Thus the Feishu bot identity used by messaging is its + application identity, not a Hub-global bot ID. + ([message management overview](https://open.feishu.cn/document/server-docs/im-v1/message/intro)) +- `chat_id` identifies a conversation, including direct and group chats. + Feishu's group resource also exposes the chat's `tenant_key`. + ([common parameters](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology), + [group management overview](https://open.feishu.cn/document/server-docs/group/chat/intro?lang=zh-CN)) +- Chat APIs require the operator to be in the same tenant as an internal chat; + common errors include `Operator and chat can NOT be in different tenants`. + Many operations additionally require the calling app's bot to be in the + group and reject an app that is unavailable in that tenant. + ([get chat members](https://open.feishu.cn/document/server-docs/group/chat-member/get?lang=zh-CN)) +- The first bot direct-chat event calls `chat_id` the conversation ID between + **that robot and user** and includes `app_id`, `tenant_key`, and app-scoped + user Open IDs in the same event. + ([user and bot first conversation](https://open.feishu.cn/document/server-docs/group/chat-member/event/bot-events?lang=zh-CN)) + +### What the official sources do not establish + +The cited documentation calls `chat_id` a conversation/group identifier but +does not state that its value is globally unique across every Feishu tenant and +every independently created app. It does establish tenant and bot-membership +authorization boundaries. Under the repository rule that unspecified scope +must not be assumed, raw global uniqueness would be an unsupported inference. + +Derived storage/call rules: + +- Store chats as `(feishu_app_connection_id, chat_id)` and retain observed + `tenant_key` and chat type. Do the same for other resource identifiers unless + Feishu explicitly contracts a wider scope. +- Always call chat/message APIs with the token obtained from the chat's bound + connection. Never retry an authorization failure with another Organization's + token. +- Treat bot direct chats as connection-specific. A `chat_id` for user X's + conversation with Organization A's bot is not evidence of a conversation + with Organization B's bot. +- For group chats, two customer apps may both be members of a real Feishu + group, but that does not authorize the Hub to move configuration, Projects, + or memberships between Organizations. The authenticated connection remains + the tenant route and Hub Organization remains the authorization root. + +## Minimum invariants for implementation review + +1. `FeishuIdentity` uniqueness is `(connection_id, open_id)`, never bare + `open_id`. +2. OAuth `state` resolves one server-side transaction bound to one Organization + and connection; code exchange uses only that connection's credentials and + stored redirect URI. +3. Login accepts the returned identity only when `tenant_key` matches the + connection's pinned tenant. +4. HTTP Webhooks choose a connection-specific verification/decryption context + before trusting payload routing fields; authenticated `app_id` and + `tenant_key` must match it. +5. Every long-connection handler carries the local connection/Organization + binding of the App ID/App Secret client that received the frame. +6. Bot, chat, event-deduplication, token-cache, and user identifiers are all + namespaced by the Feishu connection unless Feishu explicitly guarantees a + wider scope. +7. A credential, app, or tenant mismatch is a hard failure with traceable + logging; there is no fallback to a default app or search across tenant + secrets. diff --git a/.scratch/saas-production-readiness/issues/07-audit-product-surface-completeness.md b/.scratch/saas-production-readiness/issues/07-audit-product-surface-completeness.md index ebcf490..b1556dc 100644 --- a/.scratch/saas-production-readiness/issues/07-audit-product-surface-completeness.md +++ b/.scratch/saas-production-readiness/issues/07-audit-product-surface-completeness.md @@ -1,7 +1,7 @@ # Audit the accepted product surface for production completeness Type: research -Status: open +Status: resolved ## Question @@ -11,3 +11,48 @@ policy-limit, emergency workload-control, and Feishu flows are absent, unreachable, or only test-facing rather than usable in the initial production service? Treat ADR-0022's capacity control surfaces as part of the accepted product boundary. + +## Answer + +The current Hub is not a usable or production-complete SaaS product surface. +Its Organization-scoped JSON APIs and single-Feishu-app Agent Run path are +worth retaining, but the running service has one process-global Feishu +app/bot, one global provider key/base URL, no Organization connection/secret +plane, no platform-administrator control plane, and no real Organization or +platform management UI. OAuth redirects a successful login to an unmounted +`/admin/org/:orgSlug` route. + +Two real Fastify integration probes, each run twice, proved that an authenticated +Organization ADMIN receives 404 at the post-login destination and two distinct +Organization return paths produce the same OAuth `client_id`. Official Feishu +documentation also proves that `open_id` is application-scoped, `user_id` is +tenant-scoped, and `union_id` only links applications from the same application +provider. The current globally unique `User.feishuOpenId`, raw `chatId`, event +identity, OAuth state and one-listener composition therefore cannot safely +route independently customer-owned applications. + +ADR-0022 remains an additional release blocker: beyond a process-memory +Project queue and global max-turn setting, layered limits, durable +Organization-fair admission, hard storage/run/process budgets, alerts and +audited emergency brakes have no complete persistence or product surface. + +The full verdict, deterministic probe results, root causes, retained seams and +required end-to-end release evidence are in the +[accepted SaaS product-surface completeness audit](../assets/product-surface-completeness-audit.md). +The detailed route/schema/test matrix is in the +[accepted product-surface code inventory](../assets/product-surface-code-inventory.md), +and the Feishu identifier/OAuth/event/long-connection facts are in the +[Feishu product-surface primary sources](../assets/product-surface-feishu-primary-sources.md). + +The newly-clear execution frontier is: + +- [Decide the platform-administrator identity and audit boundary](42-decide-platform-admin-identity-audit.md) +- [Implement the Organization connection and secret plane](43-implement-org-connection-secret-plane.md) +- [Route Feishu identity and traffic by Organization connection](44-route-feishu-by-org-connection.md) +- [Build the private platform-administrator control plane](45-build-private-platform-admin-control-plane.md) +- [Build the private Organization-administrator control plane](46-build-private-org-admin-control-plane.md) + +Existing tickets 23 and 32–38 remain the owners of durable ingress/admission, +capacity enforcement, usage alerts, workload brakes and calibration. Ticket 08 +now requires all of these product slices before claiming the production release +gate. diff --git a/.scratch/saas-production-readiness/issues/08-prove-production-release-gate.md b/.scratch/saas-production-readiness/issues/08-prove-production-release-gate.md index 75592dc..7603fe4 100644 --- a/.scratch/saas-production-readiness/issues/08-prove-production-release-gate.md +++ b/.scratch/saas-production-readiness/issues/08-prove-production-release-gate.md @@ -2,7 +2,7 @@ Type: task Status: open -Blocked by: 01, 02, 03, 04, 05, 06, 07, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41 +Blocked by: 01, 02, 03, 04, 05, 06, 07, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46 ## Question diff --git a/.scratch/saas-production-readiness/issues/18-bind-and-reauthorize-deferred-actions.md b/.scratch/saas-production-readiness/issues/18-bind-and-reauthorize-deferred-actions.md index 9a2c4b7..762ad9d 100644 --- a/.scratch/saas-production-readiness/issues/18-bind-and-reauthorize-deferred-actions.md +++ b/.scratch/saas-production-readiness/issues/18-bind-and-reauthorize-deferred-actions.md @@ -2,6 +2,7 @@ Type: task Status: open +Blocked by: 44 ## Question diff --git a/.scratch/saas-production-readiness/issues/23-durable-feishu-inbox-and-queue.md b/.scratch/saas-production-readiness/issues/23-durable-feishu-inbox-and-queue.md index df4e3de..41de429 100644 --- a/.scratch/saas-production-readiness/issues/23-durable-feishu-inbox-and-queue.md +++ b/.scratch/saas-production-readiness/issues/23-durable-feishu-inbox-and-queue.md @@ -2,7 +2,7 @@ Type: task Status: open -Blocked by: 18 +Blocked by: 18, 44 ## Question diff --git a/.scratch/saas-production-readiness/issues/25-durable-outbound-delivery.md b/.scratch/saas-production-readiness/issues/25-durable-outbound-delivery.md index 255572e..ec6ffde 100644 --- a/.scratch/saas-production-readiness/issues/25-durable-outbound-delivery.md +++ b/.scratch/saas-production-readiness/issues/25-durable-outbound-delivery.md @@ -2,6 +2,7 @@ Type: task Status: open +Blocked by: 44 ## Question diff --git a/.scratch/saas-production-readiness/issues/32-layered-limits-rate-limiting.md b/.scratch/saas-production-readiness/issues/32-layered-limits-rate-limiting.md index 974ee37..fc9de20 100644 --- a/.scratch/saas-production-readiness/issues/32-layered-limits-rate-limiting.md +++ b/.scratch/saas-production-readiness/issues/32-layered-limits-rate-limiting.md @@ -2,7 +2,7 @@ Type: task Status: open -Blocked by: 20, 22 +Blocked by: 20, 22, 44 ## Question diff --git a/.scratch/saas-production-readiness/issues/36-usage-soft-alerts.md b/.scratch/saas-production-readiness/issues/36-usage-soft-alerts.md index ffda2d1..3dac6b9 100644 --- a/.scratch/saas-production-readiness/issues/36-usage-soft-alerts.md +++ b/.scratch/saas-production-readiness/issues/36-usage-soft-alerts.md @@ -2,7 +2,7 @@ Type: task Status: open -Blocked by: 19, 24, 29, 32 +Blocked by: 19, 24, 29, 32, 43 ## Question diff --git a/.scratch/saas-production-readiness/issues/37-emergency-workload-brakes.md b/.scratch/saas-production-readiness/issues/37-emergency-workload-brakes.md index 776f70c..490f557 100644 --- a/.scratch/saas-production-readiness/issues/37-emergency-workload-brakes.md +++ b/.scratch/saas-production-readiness/issues/37-emergency-workload-brakes.md @@ -2,7 +2,7 @@ Type: task Status: open -Blocked by: 22, 23, 24, 27, 30, 32, 33 +Blocked by: 22, 23, 24, 27, 30, 32, 33, 45 ## Question diff --git a/.scratch/saas-production-readiness/issues/40-verified-backup-restore.md b/.scratch/saas-production-readiness/issues/40-verified-backup-restore.md index 68cb217..571d77b 100644 --- a/.scratch/saas-production-readiness/issues/40-verified-backup-restore.md +++ b/.scratch/saas-production-readiness/issues/40-verified-backup-restore.md @@ -2,7 +2,7 @@ Type: task Status: open -Blocked by: 10, 12, 14, 19, 23, 24, 25, 26, 29, 39, 41 +Blocked by: 10, 12, 14, 19, 23, 24, 25, 26, 29, 39, 41, 43 ## Question diff --git a/.scratch/saas-production-readiness/issues/42-decide-platform-admin-identity-audit.md b/.scratch/saas-production-readiness/issues/42-decide-platform-admin-identity-audit.md new file mode 100644 index 0000000..f6eb4f3 --- /dev/null +++ b/.scratch/saas-production-readiness/issues/42-decide-platform-admin-identity-audit.md @@ -0,0 +1,13 @@ +# Decide the platform-administrator identity and audit boundary + +Type: grilling +Status: open + +## Question + +Which identity issuer, bootstrap and recovery procedure, invite/allowlist model, +session claims, role lifecycle, separation from Organization membership, and +append-only audit contract will authenticate Platform Administrators for the +initial service? The decision must keep `/admin/platform` private, make every +Organization/connection/workload-control mutation attributable, and define a +reviewable break-glass path without treating platform staff as customer members. diff --git a/.scratch/saas-production-readiness/issues/43-implement-org-connection-secret-plane.md b/.scratch/saas-production-readiness/issues/43-implement-org-connection-secret-plane.md new file mode 100644 index 0000000..cdf9fde --- /dev/null +++ b/.scratch/saas-production-readiness/issues/43-implement-org-connection-secret-plane.md @@ -0,0 +1,16 @@ +# Implement the Organization connection and secret plane + +Type: task +Status: open +Blocked by: 19 + +## Question + +Implement encrypted, versioned, Organization-scoped Feishu Application and +model Provider Connection records plus one resolver boundary. Support one +customer-owned Feishu app per Organization, Organization-admin-managed BYOK, +and one distinct platform-managed key/base URL per Organization; enforce which +administrator may write each mode, readiness and rotation lifecycle, runtime +resolution from Organization/Project, redacted observability, backup/key +recovery hooks, and no process-global credential fallback or plaintext leakage +into business rows, logs, HTTP responses, or agent environments. diff --git a/.scratch/saas-production-readiness/issues/44-route-feishu-by-org-connection.md b/.scratch/saas-production-readiness/issues/44-route-feishu-by-org-connection.md new file mode 100644 index 0000000..c7e86b9 --- /dev/null +++ b/.scratch/saas-production-readiness/issues/44-route-feishu-by-org-connection.md @@ -0,0 +1,17 @@ +# Route Feishu identity and traffic by Organization connection + +Type: task +Status: open +Blocked by: 20, 22, 43 + +## Question + +Make every Feishu OAuth, WebSocket/event, card action, directory sync, chat +binding, API call, and outbound message start from an explicit active +Organization Feishu Application Connection. Model external user, chat, event, +and callback identifiers in that connection's namespace; bind OAuth state to +the intended Organization/application; run and observe one listener/client +lifecycle per configured app; and prove two customer-owned apps with colliding +provider-local identifiers cannot authenticate, bind, authorize, deduplicate, +rate-limit, or deliver across Organizations. Do not infer tenant from a global +`open_id`, `chat_id`, or from the user's membership count. diff --git a/.scratch/saas-production-readiness/issues/45-build-private-platform-admin-control-plane.md b/.scratch/saas-production-readiness/issues/45-build-private-platform-admin-control-plane.md new file mode 100644 index 0000000..a18a693 --- /dev/null +++ b/.scratch/saas-production-readiness/issues/45-build-private-platform-admin-control-plane.md @@ -0,0 +1,17 @@ +# Build the private platform-administrator control plane + +Type: task +Status: open +Blocked by: 20, 27, 28, 42, 43, 44 + +## Question + +Implement authenticated `requirePlatformAdmin` APIs and a private +`/admin/platform` web area for Platform Administrators to create/list/update +Organizations, bootstrap their OWNER, view/change lifecycle state, configure +and test each customer-owned Feishu Application, configure the Organization's +distinct platform-managed Provider Connection, and manage platform-admin +allowlisting. Prove anonymous users, ordinary members, and Organization admins +cannot load either page data or mutations; every sensitive action is validated, +redacted, audited, observable, and covered by browser-level critical-journey +tests. Emergency workload controls remain integrated through ticket 37. diff --git a/.scratch/saas-production-readiness/issues/46-build-private-org-admin-control-plane.md b/.scratch/saas-production-readiness/issues/46-build-private-org-admin-control-plane.md new file mode 100644 index 0000000..37eb8ac --- /dev/null +++ b/.scratch/saas-production-readiness/issues/46-build-private-org-admin-control-plane.md @@ -0,0 +1,17 @@ +# Build the private Organization-administrator control plane + +Type: task +Status: open +Blocked by: 20, 27, 28, 32, 36, 43, 44 + +## Question + +Turn the guarded Organization JSON APIs into a usable private +`/admin/org/:orgSlug` web control plane for OWNER/ADMIN users. Cover members and +roles, teams and Project access, Folders/Projects/chat unbinding, sessions and +usage, model/role/run policy, Organization policy limits and alerts, Feishu +connection readiness, Provider Credential Mode selection, and BYOK key/base URL +management without ever displaying stored plaintext. Preserve server-side +authorization on every request, define safe loading/error/empty states, and +prove login redirect plus browser journeys, cross-Organization denial, MEMBER +denial, secret redaction, and logout/session expiry end to end. diff --git a/.scratch/saas-production-readiness/map.md b/.scratch/saas-production-readiness/map.md index b2bb132..ada7cf9 100644 --- a/.scratch/saas-production-readiness/map.md +++ b/.scratch/saas-production-readiness/map.md @@ -41,6 +41,7 @@ rollback and recovery, and no known critical security or data-integrity gaps. - [Define production observability and incident recovery](issues/04-define-observability-recovery.md) — keep the existing Fastify/Prisma/systemd signal fragments, but production requires dependency-aware readiness, typed and correlated errors, durable-work telemetry, an explicit SLO/retention/alerting control plane, and recovery runbooks proven by fault drills. - [Define initial abuse and capacity controls](issues/05-define-abuse-capacity-controls.md) — require layered fail-closed limits, durable Organization-fair admission, explicit backpressure, bounded files/storage/runs, soft usage alerts, and audited emergency workload brakes; calibrate numerical ceilings under production-like load. - [Audit backup, migration, and disaster recovery safety](issues/06-audit-backup-migration-recovery.md) — separate persistent state from releases, define a database/workspace checkpoint and recovery objectives, make migrations compatibility-gated, and prove complete off-host backup sets through isolated restore and cutover drills. +- [Audit the accepted product surface for production completeness](issues/07-audit-product-surface-completeness.md) — retain the guarded Organization APIs and single-app Feishu vertical slice, but require explicit Organization-scoped Feishu/provider connections and external-identity namespaces, private platform and Organization admin control planes, and the full ADR-0022 capacity surface before release. ## Fog