Revalidate Docs / Revalidate Docs (push) Failing after 2s
E2E CI / Check Duplicate Run (push) Failing after 5s
Test CI / Check Duplicate Run (push) Failing after 6s
E2E CI / Test Web App (push) Has been skipped
Test CI / Test Packages (push) Has been skipped
Test CI / Test App (shard 1/3) (push) Has been skipped
Test CI / Test App (shard 2/3) (push) Has been skipped
Test CI / Test App (shard 3/3) (push) Has been skipped
Test CI / Test Desktop App (push) Has been skipped
🔄 Branch Synchronization / sync-branches (push) Failing after 11s
Test CI / Test Database (push) Has been skipped
Test CI / Merge and Upload App Coverage (push) Has been skipped
Database Schema Visualization CI / build (push) Failing after 4m14s
11 KiB
11 KiB
Enterprise AI Workspace / Enterprise Gateway — Implementation Plan
Goal
Local-Docker prototype that sits between a future LobeChat-native chat UI and legacy enterprise systems (gongdan / xiaoshou / cloudcost / kb / ai_search / sandbox / doc). It enforces:
- chat never manages permissions
- no customer admin
- permissions only via backend admin
- frontend capabilities != security boundary
- every enterprise tool call routed through Enterprise Gateway
- legacy full responses filtered by
identity_map+data_scope, then field-masked byfield_policies, then audited
Stack
- Backend: Node 20 + TypeScript + Fastify + Prisma + Postgres 16
- Frontend: Next.js 14 (App Router) + React 18, server components for admin pages
- Deploy: docker-compose (db, api, web)
- Casdoor: external (http://casdoor.ashyglacier-8207efd2.eastasia.azurecontainerapps.io). Dev mode accepts a
X-Dev-Userheader that maps to a seeded enterprise_user; production-ready Casdoor OIDC stub included but not wired (acceptance is local dev mode).
Layout
lobechat/
docker-compose.yml
.env.example
.gitignore
README.md
apps/
api/
package.json
tsconfig.json
Dockerfile
prisma/
schema.prisma
migrations/ # created by `prisma migrate`
seed.ts
src/
server.ts
env.ts
auth/
casdoor.ts
devAuth.ts
middleware.ts
core/
gateway.ts # central tool-call executor
capabilities.ts # role→tool resolution
filter.ts # identity_map + data_scope filter
masking.ts # field_policies
audit.ts
tools/
registry.ts
gongdan.ts
xiaoshou.ts
cloudcost.ts
kb.ts
aiSearch.ts
sandbox.ts
docGenerate.ts
routes/
health.ts
me.ts
capabilities.ts
toolsCall.ts
admin/
users.ts
roles.ts
tools.ts
dataScopes.ts
identityMap.ts
audit.ts
web/
package.json
tsconfig.json
next.config.mjs
Dockerfile
src/app/
layout.tsx
page.tsx # login/select test user
capabilities/page.tsx
tools/page.tsx # tool-call tester
admin/
layout.tsx # permission_admin+super_admin only
users/page.tsx
tools/page.tsx
scopes/page.tsx
audit/page.tsx
api/session/route.ts # cookie: selected dev user id
src/lib/apiClient.ts
Data Model (Prisma, snake_case tables via @@map)
- enterprise_users: id (uuid), casdoor_sub (unique), username, display_name, email, department_id, region, metadata (jsonb), is_active, timestamps
- enterprise_roles: id, key (unique: super_admin|permission_admin|internal_sales|internal_ops|internal_tech|customer), name, description
- enterprise_user_roles: user_id, role_id, granted_by, granted_at (pk composite)
- enterprise_tool_registry: id, key (unique: e.g.
gongdan.get_ticket), category, display_name, description, input_schema jsonb, is_enabled - enterprise_tool_permissions: id, subject_type (role|user), subject_id, tool_id, allow (bool), constraints jsonb (optional)
- enterprise_data_scopes: id, subject_type, subject_id, source_system, entity_type, scope jsonb (e.g.
{ "owner_user_id": "$self" }or{ "region": ["CN-EAST"] },{ "all": true }) - enterprise_field_policies: id, source_system, entity_type, field_path, policy (drop|mask|hash), role_keys text[] (list of role keys this policy excludes, i.e. roles that still see it)
- enterprise_identity_map: id, source_system, entity_type, source_entity_id, tenant_id, customer_id, owner_user_id, department_id, sales_user_id, operation_user_id, region, visibility_level, metadata jsonb; unique(source_system, entity_type, source_entity_id)
- enterprise_audit_logs: id, at, user_id, username, tool_key, action (call|filter|mask|deny|missing_identity_map), request jsonb, response_summary jsonb, outcome (ok|denied|error), meta jsonb
APIs
GET /health→{status:"ok", db:"ok"}GET /api/me→{user, roles[], casdoor_sub, department_id, region}GET /api/capabilities→{tools:[{key, category, display_name, description, allowed:true}]}(only ones the user can call)POST /api/tools/callbody{tool, params}→ executes via gateway; returns{data, meta:{filtered_count, dropped_count, masked_fields[]}}- Admin (requires permission_admin or super_admin):
GET/POST /api/admin/users+POST /api/admin/users/:id/rolesGET /api/admin/roles,GET/POST/DELETE /api/admin/tool-permissionsGET/POST/DELETE /api/admin/data-scopesGET/POST /api/admin/identity-mapGET /api/admin/audit?user&tool&outcome&from&to
- Guardrail: admin APIs check role; additionally,
POST /api/tools/callexplicitly rejects tool keys in the admin namespace (admin.*) so permissions cannot be changed from chat.
Gateway flow for POST /api/tools/call
- Resolve user → roles → allowed tool keys (merge role grants + user grants, deny wins).
- If requested tool not in allowed set → audit
deny, return 403. - Validate params against tool input schema (zod).
- Tool adapter calls external service or returns seeded mock fallback (env
MOCK_MODE=trueby default). - Post-processing pipeline (always, even on mock):
a.
filterByIdentityMap(items, source, entity, user):- For each item, look up identity_map by (source_system, entity_type, source_entity_id).
- No mapping → drop + emit
missing_identity_mapaudit entry. - Mapping exists → check against user's
data_scopesfor (source, entity). Any matching scope passes (all:true, orowner_user_id:$self, or attribute inclusion). b.applyFieldPolicies(items, source, entity, userRoles): - For each field_policy, if user has no role in
role_keys, apply policy tofield_path. dropdeletes,maskreplaces with***,hashreplaces withsha256(...).slice(0,12). c.audit.write({tool, outcome:'ok', filtered_count, dropped_count, masked_fields}).
- Return trimmed data to caller.
Tool adapters
Each exports { key, inputSchema, sourceSystem, entityType, run(ctx, params) → items }.
gongdan.create_ticket,gongdan.get_own_tickets,gongdan.search_tickets,gongdan.get_ticket→ HTTP client w/X-Api-Key: GONGDAN_API_KEY→ mock fallback returns 5 seeded tickets with mixed owners/customers.xiaoshou.search_customers,xiaoshou.get_customer,xiaoshou.get_customer_insight,xiaoshou.get_allocations→X-Api-Key: SUPER_OPS_API_KEYagainst xiaoshou external API → mock fallback.cloudcost.get_overview→/api/dashboard/overview;cloudcost.get_daily_report→/api/service-accounts/daily-report. Requires Casdoor Bearer — in mock we return seeded floats.kb.search→ POST toKB_AGENT_URL/api/v1/searchwithapi-keyheader.ai_search.web→ Serper.dev or Jina; mock returns 3 synthesized hits.sandbox.run→ POST Daytona; mock echoes code withstdout: "(mock)".doc.generate→ POST doc-creator-agent; mock returns markdown stub.
Each adapter sets sourceSystem/entityType so the gateway filter/mask can route policies.
Seed
- 6 roles (all listed above)
- 6 users:
sa→ super_adminpa→ permission_adminsales1→ internal_sales (department=sales, region=CN-EAST)ops1→ internal_ops (department=ops)tech1→ internal_techcust1→ customer (customer_id=CUST-0001)
- tool_registry: 16 tools (7 gongdan + 4 xiaoshou + 2 cloudcost + 1 kb + 1 ai_search + 1 sandbox + 1 doc)
- tool_permissions: per-role defaults per spec
- data_scopes:
- customer →
gongdan/ticket:{"customer_id":"$user.customer_id"},{"owner_user_id":"$self"} - internal_sales →
xiaoshou/customer:{"sales_user_id":"$self"};cloudcost/*: none (denied) - internal_ops →
gongdan/ticket:{"all":true};cloudcost/*:{"all":true} - internal_tech →
gongdan/ticket:{"owner_user_id":"$self","operation_user_id":"$self"} - super_admin/permission_admin → all systems
{"all":true}(permission_admin read-only via chat because tool perms deny most tools)
- customer →
- field_policies (examples):
gongdan/ticket.contactInfo→ mask, keep for internal_ops/super_adminxiaoshou/customer.current_month_consumption→ drop, keep for internal_ops/super_admin/cloud_finance-likecloudcost/*.cost→ mask for customer/internal_tech*.apiKey,*.api_key,*.secret_*→ drop for everyone except super_admin
- identity_map: pre-seed 10 gongdan tickets, 5 xiaoshou customers, 3 cloud service-accounts with realistic owner/department/region. Seed also creates a ticket WITHOUT identity_map to prove missing_identity_map path.
Dev auth
apps/api/src/auth/devAuth.ts— reads headerX-Dev-User: <username>or cookiedev_user. Lookups enterprise_users.username; populatesreq.user,req.roles.- Casdoor OIDC stub in
casdoor.ts(not enabled unlessAUTH_MODE=casdoor); validates Bearer via JWKS. Not required for acceptance.
Frontend
/login page lists seed users as buttons. Clicking sets cookie via/api/session./capabilitiesfetches/api/capabilities./toolspicks a tool from capabilities, renders JSON param form (based on input_schema), POSTs/api/tools/call, shows result + meta./admin/*links hidden unless role is super_admin|permission_admin. Pages call admin API.- Customer role: admin links hidden, capability list shows only customer tools. (Security boundary is still the backend.)
Acceptance tests (Phase 3)
Scripts under apps/api/scripts/acceptance.sh — hits the running compose stack via localhost:3001 and verifies:
customercallingxiaoshou.search_customers→ 403 deny, audit row created.permission_admincallinggongdan.search_tickets→ 403 (no tool grant) even though they can use admin APIs.internal_salescallingxiaoshou.search_customers→ only customers with sales_user_id == self return; others filtered (audit records filtered_count).internal_opscallinggongdan.search_tickets→ seeded ticket without identity_map is absent;missing_identity_mapaudit row present.customercallinggongdan.get_own_tickets→ only own tickets;contactInfomasked.super_admincalling/api/admin/audit→ sees all events.- Sensitive fields like
apiKey/secretnever in any non-super_admin response (grep on full JSON output).
Phase 4 validators
- architect (functional completeness vs spec)
- security-reviewer (secrets, SQLi, authz bypass)
- code-reviewer (quality)
Phase 5 cleanup
Remove .omc/state/autopilot-state.json and siblings; leave .omc/plans/autopilot-impl.md + .omc/autopilot/spec.md as artifacts.