Files
smart-org/docs/plan/TODO-agent-1-org-platform.md
T
mjalmoudy94andClaude Sonnet 5.5 fef5a9deab feat(integration): wire v2 workstreams together (engine policy/budget, sandbox, tenant checks, order route, docs)
Engine honours org policy and monthly budget, legacy runner skips engine tasks,
sandbox session provisioned on demand, project fallback for threads, reviewer
fallback, tenant checks on project ids, memory reflection respects grants,
expiry worker, MCP pipeline_status, login redirect, sharing settings, e2e smoke.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:57:56 +03:30

16 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. (token matched by its hash, as org invites are)
  • 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). (sharing cases are in tests/test_org_platform_invites.py; tests/test_org_sharing.py was not created)

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. (login redirect in login/page.tsx via fetchNeedsOnboarding in lib/api-org.ts, T12)
  • 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. (T13: settings/_components/sharing-section.tsx)
  • 1.11 Web tests src/test/org-sharing.test.tsx. (accept-invite tests here; settings sharing tests in org-settings-sharing.test.tsx, T13)

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. — open: defaults_for exists in catalog.py, but no unit test covers the catalog entries; test_org_wizard.py only checks the endpoint and the next.js defaults.
  • 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. (hook failures use record_error, the T11 change; the wizard audit row is organization.wizard_complete)
  • 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. (409 test: test_second_app_for_same_org_returns_409)
  • 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. — open: the secrets test checks responses, audit and draft; it does not capture log output or scan DB rows for the plaintext secret.

Frontend

  • 2.7 Rebuild /onboarding as a 5-step wizard (stepper, back/next, draft auto-save, refresh resumes): — open: step 5's success screen (onboarding/page.tsx) has no demo URL placeholder; the other steps are in place.
    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). — open: there is no validation case; step navigation, slug suggestion and the secret-free draft are covered.

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. (the ModelGrantPort contract change is T1: get_org_policy)
  • 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). (expire hook: T9, called from the billing pass)
  • 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 (pipeline_status: T10) 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.