Files
smart-org/docs/plan/TODO-agent-1-org-platform.md
T
mjalmoudy94andClaude Sonnet 5.5 0cd9194b64 v2 foundation: shared contracts, app profile schema, wiring stubs
- projects: app profile columns + one app per org; project_external_git,
  deploy_targets, org_model_grants tables (migration a0f0e1d2c3b4)
- ports + fakes: RepoWorkspace, DeliveryHooks, DeployTarget, Memory,
  ModelGrants, WorkforceBootstrap; AppProfile + AppProfileRepository
- per-workstream wiring (container_org/engine/delivery), config blocks,
  empty routers, delivery worker, MCP/delivery service shells, web stubs
- admin nav derives from ADMIN_ITEMS; plan docs updated for ext4 worktrees

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 23:21:17 +03:30

15 KiB
Raw Blame History

TODO — Agent 1: Org Platform (invites, wizard, app profile, sharing, model grants, memory, MCP)

  • Worktree: /home/mamad/work/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. The worktree is on ext4 (the main repo under /projects is exFAT: no symlinks), so .venv and node_modules work here; do not copy files to /projects.
  • 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.