Files
smart-org/docs/plan/TODO-agent-1-org-platform.md
T

130 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TODO — Agent 1: Org Platform (invites, wizard, app profile, sharing, model grants, memory, MCP)
- **Worktree:** `/projects/smart-org-wt/agent1` · **Branch:** `agent1/org-platform` (already created, already installed)
- **Read first:** `docs/plan/PLAN-v2.md` (all of it). Then skim: `apps/api/app/interfaces/http/routers/organizations.py`,
`invites.py`, `admin_users.py`, `app/application/services/platform_users.py`, `app/domain/identity/models.py`,
`apps/web/src/app/onboarding/page.tsx`, `apps/web/src/app/(app)/settings/`.
- **Your files:** only the "Agent 1" column of PLAN-v2 §6, your block in `config.py` / `.env.example`, and the
migration `alembic/versions/a1b2c3d4e5f1_org_platform.py` (`down_revision = "a0f0e1d2c3b4"`).
- **Never touch:** the locked files and the other agents' files (PLAN-v2 §6). Need something there? → `CHANGE REQUEST` in your report.
- **Do NOT commit, push, merge or rebase.** Never `cd` outside the worktree, never write to `/tmp`.
- **After each phase:** run all checks (PLAN-v2 §7.4), write `docs/plan/reports/agent-1-phase-<K>.md`, then STOP and wait.
You consume these contracts (do not change them): `DeployTargetTester`, `RepoWorkspacePort.test_remote`,
`DeliveryHooks.on_app_created`, `WorkforceBootstrapPort.bootstrap_team`. Get them only from the builders in
`app/application/container.py`. In tests use the Fake/Null versions from the ports package.
You implement these contracts: `MemoryPort`, `ModelGrantPort`. Your real classes are wired in
`container_org.py`.
---
## Phase 1 — Platform invites, roles and org sharing
Backend
- [ ] 1.1 New table `platform_invites` (in `db/models_org.py`): id, token_hash, email (optional), created_by (owner), role (`manager` only for now), expires_at (`PLATFORM_INVITE_TTL_HOURS`), used_at, used_by_user_id, revoked_at. Migration in your file.
- [ ] 1.2 Owner-only endpoints in `admin_users.py`: `POST /admin/platform-invites` (returns `invite_url` once, token shown once), `GET /admin/platform-invites`, `POST /admin/platform-invites/{id}/revoke`. Audit every action.
- [ ] 1.3 In `invites.py`, extend `GET /invites/{token}` and `POST /invites/accept` to recognise a platform invite: if no account exists, create a `manager` account with the chosen password; if the email exists, require login (same rule as org invites: an invite never resets a password). Single use, constant-time compare, expired or revoked → the same generic 404.
- [ ] 1.4 `GET /auth/me` (you own `auth.py`) gains `needs_onboarding: bool` = platform role is `manager`, the user has zero org memberships, and they are not the owner.
- [ ] 1.5 Org sharing in `organizations.py`: `POST /organizations/{id}/transfer-ownership` (owner → an existing admin member, in one transaction, audited), `POST /organizations/{id}/leave` (the last owner cannot leave), and `GET /organizations/{id}/invites` + `DELETE .../invites/{invite_id}` (list and revoke pending org invites). Keep the role ladder `owner > admin > manager > member > viewer`.
- [ ] 1.6 Tests: `tests/test_org_platform_invites.py`, `tests/test_org_sharing.py`. Cover: non-owner cannot create platform invites; token single use; expired/revoked; existing-email path; transfer + leave edge cases; tenant isolation (a member of org A cannot list org B invites).
Frontend
- [ ] 1.7 `/admin/users`: a "دعوت مدیر جدید" button → modal → shows the link once with a copy button; a list of pending invites with revoke.
- [ ] 1.8 `/accept-invite`: handle the platform-invite variant (title "ساخت حساب مدیر").
- [ ] 1.9 After login: if `needs_onboarding`, redirect to `/onboarding`. Do this in the `onboarding` page and in a small hook in `lib/api-org.ts`. `AppShell` is locked, so if a redirect there is unavoidable, write a CHANGE REQUEST.
- [ ] 1.10 `(app)/settings`: a "اشتراک‌گذاری سازمان" section: members with role selector, invite form (email + role), pending invites with revoke, transfer ownership (confirmation modal that requires typing the org slug), leave org.
- [ ] 1.11 Web tests `src/test/org-sharing.test.tsx`.
**Phase 1 done when:** the owner creates a link → a new person opens it, sets a password → logs in → lands on `/onboarding`. All checks are green.
---
## Phase 2 — Org creation wizard + app profile (1 org = 1 app)
Backend
- [ ] 2.1 `app/domain/projects/catalog.py`: pure-Python catalog. `APP_TYPES` (`web_app`, `api`, `static_site`, `bot`, `worker`, `other`) and `LANGUAGES` → frameworks → defaults (runtime_version, package_manager, setup/build/test/start commands, app_port, healthcheck_path, docker base image name). Cover at least:
- node: next, express, nestjs, vite-react
- python: fastapi, django, flask
- php: laravel, plain
- go: plain
- static: html
Add a function `defaults_for(app_type, language, framework) -> ProfileDefaults`. Unit tests for every entry.
- [ ] 2.2 New table `org_setup_drafts` (user_id, step, data_json **without secrets**, updated_at). Secrets typed during the wizard are held only in the final submit request, never in the draft.
- [ ] 2.3 `org_setup_service.py` + router `app_setup.py`:
- `GET /setup/catalog`.
- `GET/PUT /setup/draft`.
- `POST /setup/check-git`: external URL + credentials → `RepoWorkspacePort.test_remote`. Validate the URL **format** first in your own service: `https://` or `ssh://`/`git@` only, no credentials embedded in the URL, no IP-literal localhost/private hosts. The DNS-resolved SSRF check happens inside `test_remote` (Agent 3); do not import Agent 3's code.
- `POST /setup/check-deploy`: → `DeployTargetTester.test_connection`.
- `POST /setup/complete`: in ONE transaction, create the org (caller = org owner), the project with its app profile (`wizard_completed_at=now`), the encrypted external git credentials or the platform Gitea repo through the existing `GitProviderPort.ensure_project_repo`, the optional deploy target (encrypted), and the default model grants (Phase 3; until then, none). After commit, call `WorkforceBootstrapPort.bootstrap_team` and `DeliveryHooks.on_app_created`. Each hook is called once; a hook failure is recorded with `error_log` and does not undo the org.
- Slug is unique (reuse the existing slug rules). Audit `org.created_via_wizard`.
- [ ] 2.4 App profile editing for org owner/admin: `GET/PATCH /organizations/{org}/app/profile` (commands, port, healthcheck, framework), `PUT /organizations/{org}/app/deploy-target` (+ test), `PUT /organizations/{org}/app/git` (switch platform ↔ external is NOT allowed after creation; only credential rotation). Secrets are write-only: responses return `has_secret` and masked usernames only.
- [ ] 2.5 Enforce 1 org = 1 app: any other path that creates a second project returns a clear 409 (the DB constraint exists already; make the service error friendly). Note: `projects.py` is Agent 3's file. Do NOT edit it; only your service/router may create the app.
- [ ] 2.6 Tests `tests/test_org_wizard.py`: happy path platform git; happy path external git with a fake `test_remote`; a deploy target with a failing test does not block completion (it is saved as `last_test_ok=false`) but the user sees the warning; secrets never in any response, draft, audit metadata or log (assert by searching the captured output and DB rows for the plaintext secret); a second app → 409; a user with role `user` cannot complete the wizard; SSRF cases for the git URL.
Frontend
- [ ] 2.7 Rebuild `/onboarding` as a 5-step wizard (stepper, back/next, draft auto-save, refresh resumes):
1. Organization: name, slug (auto-suggest).
2. Application: name, type, language, framework (cards from the catalog), "advanced" accordion with prefilled commands, port and healthcheck.
3. Git: "Git پلتفرم (پیشنهادی)" or "Git خودم" (URL + token or SSH key + "بررسی اتصال" button).
4. Production (optional, skippable): SSH / SFTP / FTP / FTPS fields + "تست اتصال". Show a clear note that production deploys only happen when they press the button.
5. Review → create → success screen with the demo URL placeholder and a "برو به مدیر AI" button.
- [ ] 2.8 `(app)/settings` → "اپلیکیشن" tab: edit the profile, rotate git credentials, edit or test the deploy target.
- [ ] 2.9 Web tests `src/test/org-wizard.test.tsx` (step navigation, validation, secrets never placed in the draft payload).
**Phase 2 done when:** a fresh manager completes the wizard in the browser and lands on the dashboard of the new org. The DB has exactly one project with a full profile. The Null hooks recorded one `bootstrap_team` and one `on_app_created` call.
---
## Phase 3 — Owner model grants (employees only use what the owner provides)
- [ ] 3.1 `model_grant_service.py` implementing `ModelGrantPort` over `org_model_grants` + `ai_models` (only active models, only active grants). `GrantedModel.tier` is a neutral label derived from capabilities (`سریع`, `متعادل`, `قوی`). Never the model name.
- [ ] 3.2 Owner endpoints `admin_grants.py`: list grants per org, grant/revoke model, set daily credit cap, and "default grant set for new orgs" (new table `default_model_grants`, or a column in your own table; do NOT edit `ai_models`). Audit all.
- [ ] 3.3 Wizard `complete` copies the default grant set into the new org.
- [ ] 3.4 Owner org policy (`org_policies` table, yours): max_employees (≤ `ENGINE_MAX_EMPLOYEES`), monthly credit budget, automation_enabled (bool). Endpoints under `admin_grants.py`. Expose `get_org_policy(org_id)` in `model_grant_service.py` for Agent 2 to read later. Write a CHANGE REQUEST so Claude adds it to the `ModelGrantPort` contract at integration.
- [ ] 3.5 Wire your real `ModelGrantPort` in `container_org.py`.
- [ ] 3.6 UI `/admin/model-grants`: org list → grant matrix (models × checkbox + cap), default set editor, policy editor. Org users see nothing new.
- [ ] 3.7 Tests `tests/test_model_grants.py`: non-owner forbidden everywhere; revoked or inactive model not returned; a grant on an org you query for another org is never returned (isolation); defaults copied on wizard completion.
---
## Phase 4 — Org memory (self-improving org)
- [ ] 4.1 Domain `app/domain/memory/`: `MemoryKind` (`rule`, `preference`, `lesson`, `fact`, `code_map`), `MemorySource` (`user`, `manager`, `system`, `reflection`), validation (key ≤ 80 chars, content ≤ 1,500 chars), normalisation + content hash for dedupe.
- [ ] 4.2 Table `org_memories` (in `models_org.py`): id, organization_id, kind, key (nullable), content, source, evidence_task_id, pinned, confidence (0..1), expires_at (lessons: +30 days unless pinned or confirmed), hits, last_used_at, created_by_user_id, created_at, updated_at, deleted_at (soft). Index `(organization_id, kind)`.
- [ ] 4.3 `memory_service.py` implementing `MemoryPort`:
- `record`: dedupe by hash or same key (update instead of insert), org cap `MEMORY_ORG_CHAR_CAP` (evict oldest unpinned lessons first), max 3 writes per task.
- `build_context(role, query, budget)`: pinned rules and preferences first, then the top memories by keyword overlap with `query` and recency. Role filter: `code_map` and `fact` only for planner/programmer/reviewer/tester; preferences for the manager. Returns a compact Persian/English block within `budget_chars`; updates `hits/last_used_at`.
- `on_task_finished(TaskOutcome)`: one cheap model call (through the existing ai_control routing in "cheap" mode and `ModelProviderPort`, no new adapter) asking for ≤ 2 lessons + a 1-paragraph code-map delta. It stores them with `source=reflection`, `confidence=0.5`. If the call fails, it records the error and returns (it never raises into the engine). Record usage for that call in the usage ledger like other model calls.
- Expire job: function `expire_memories()` (called from the existing billing worker? NO, that is not your file → expose the function and write a CHANGE REQUEST for a worker hook).
- [ ] 4.4 Router `memory.py`: list (filter by kind, search), create (users can "teach" a rule or preference), edit, pin/unpin, delete (soft, audited), and a `confirm` action (lesson → confidence 1, no expiry). Roles: viewer can read; member+ can add preferences; admin+ can edit or delete anything.
- [ ] 4.5 Fold the existing knowledge sources into the same page: keep `knowledge.py` endpoints working and show sources as a second tab.
- [ ] 4.6 UI `/memory` (replace the placeholder; also redirect `/knowledge` → `/memory`): tabs "قوانین و ترجیحات", "درس‌های آموخته", "نقشهٔ کد", "منابع دانش"; inline edit; pin; confirm a lesson; "به سازمان یاد بده" input. Publish `memory.changed` on `org:<id>` and refresh live.
- [ ] 4.7 Wire the real `MemoryPort` in `container_org.py`.
- [ ] 4.8 Tests `tests/test_memory.py`: dedupe, caps and eviction, TTL, role filtering, budget respected, isolation, reflection failure does not raise, no secrets: run content through `app/core/redact.py` before storing and test it.
---
## Phase 5 — MCP gateway (AI agents control our app)
- [ ] 5.1 Table `mcp_tokens` (id, organization_id, name, token_hash, prefix (first 6 chars for display), scopes CSV, created_by, expires_at, last_used_at, revoked_at). Tokens look like `smo_<random>` and are shown once.
- [ ] 5.2 `mcp_service.py`: issue, list, revoke, verify (constant-time; expired or revoked → reject), scope check.
- [ ] 5.3 `app/interfaces/mcp/server.py` with the official `mcp` SDK (streamable HTTP, bound to `MCP_GATEWAY_HOST:MCP_GATEWAY_PORT`) and `main()` for `pnpm mcp:dev`. Tools (each resolves the org from the token, never from arguments):
- `app_get_profile` (`app:read`)
- `tasks_list` / `tasks_get` (`tasks:read`)
- `tasks_create` (`tasks:write`): goes to the manager as a user order through the **current** public `WorkforceService.create_task` signature; read it, do not edit it (Agent 2 owns it; Claude re-wires at integration if it changes)
- `memory_search` (`memory:read`) / `memory_write` (`memory:write`, kinds `fact|preference` only, `source=user`)
- `knowledge_search` (`memory:read`)
- `pipeline_status` (`delivery:read`): register the tool but return `{"available": false}`. Agent 3's tables do not exist in your branch; Claude wires it at integration
Every call is audited (`mcp.tool_call`, token prefix, tool, ok/error), rate limited (60/min per token, in-memory), and its output passes through `redact.py`.
- [ ] 5.4 `services/mcp-gateway/README.md`: how to run it, the Caddy host suggestion `mcp.<root>` (Agent 3/owner configures it), and a client config snippet.
- [ ] 5.5 Router `mcp_tokens.py`: org admin+ can create, list and revoke org tokens. Owner endpoint to disable MCP globally or per org (setting stored in your `org_policies`).
- [ ] 5.6 UI: `(app)/settings` → "یکپارچه‌سازی (MCP)" tab (create with scope checkboxes, show-once modal with a copy button, list, revoke); `/admin/mcp` (global switch, per-org switch, token counts).
- [ ] 5.7 Tests `tests/test_mcp.py`: use the SDK's in-process client or call the tool functions directly. Cover: a token from org A can never read org B; scope enforcement; revoked/expired; rate limit; audit row written; no secret in outputs.
---
## Final report (after Phase 5)
`docs/plan/reports/agent-1-final.md`: summary, all endpoints added (method + path + role), tables added, the
CHANGE REQUESTs still open, manual QA steps for the wizard and MCP, known limitations.