- 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>
15 KiB
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 migrationalembic/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 REQUESTin your report. - Do NOT commit, push, merge or rebase. Never
cdoutside the worktree, never write to/tmp. The worktree is on ext4 (the main repo under /projects is exFAT: no symlinks), so.venvandnode_moduleswork 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(indb/models_org.py): id, token_hash, email (optional), created_by (owner), role (manageronly 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(returnsinvite_urlonce, token shown once),GET /admin/platform-invites,POST /admin/platform-invites/{id}/revoke. Audit every action. - 1.3 In
invites.py, extendGET /invites/{token}andPOST /invites/acceptto recognise a platform invite: if no account exists, create amanageraccount 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 ownauth.py) gainsneeds_onboarding: bool= platform role ismanager, 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), andGET /organizations/{id}/invites+DELETE .../invites/{invite_id}(list and revoke pending org invites). Keep the role ladderowner > 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 theonboardingpage and in a small hook inlib/api-org.ts.AppShellis 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) andLANGUAGES→ 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+ routerapp_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://orssh:///git@only, no credentials embedded in the URL, no IP-literal localhost/private hosts. The DNS-resolved SSRF check happens insidetest_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 existingGitProviderPort.ensure_project_repo, the optional deploy target (encrypted), and the default model grants (Phase 3; until then, none). After commit, callWorkforceBootstrapPort.bootstrap_teamandDeliveryHooks.on_app_created. Each hook is called once; a hook failure is recorded witherror_logand 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 returnhas_secretand 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.pyis 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 faketest_remote; a deploy target with a failing test does not block completion (it is saved aslast_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 roleusercannot complete the wizard; SSRF cases for the git URL.
Frontend
- 2.7 Rebuild
/onboardingas a 5-step wizard (stepper, back/next, draft auto-save, refresh resumes):- Organization: name, slug (auto-suggest).
- Application: name, type, language, framework (cards from the catalog), "advanced" accordion with prefilled commands, port and healthcheck.
- Git: "Git پلتفرم (پیشنهادی)" or "Git خودم" (URL + token or SSH key + "بررسی اتصال" button).
- Production (optional, skippable): SSH / SFTP / FTP / FTPS fields + "تست اتصال". Show a clear note that production deploys only happen when they press the button.
- 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.pyimplementingModelGrantPortoverorg_model_grants+ai_models(only active models, only active grants).GrantedModel.tieris 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 tabledefault_model_grants, or a column in your own table; do NOT editai_models). Audit all. - 3.3 Wizard
completecopies the default grant set into the new org. - 3.4 Owner org policy (
org_policiestable, yours): max_employees (≤ENGINE_MAX_EMPLOYEES), monthly credit budget, automation_enabled (bool). Endpoints underadmin_grants.py. Exposeget_org_policy(org_id)inmodel_grant_service.pyfor Agent 2 to read later. Write a CHANGE REQUEST so Claude adds it to theModelGrantPortcontract at integration. - 3.5 Wire your real
ModelGrantPortincontainer_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(inmodels_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.pyimplementingMemoryPort:record: dedupe by hash or same key (update instead of insert), org capMEMORY_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 withqueryand recency. Role filter:code_mapandfactonly for planner/programmer/reviewer/tester; preferences for the manager. Returns a compact Persian/English block withinbudget_chars; updateshits/last_used_at.on_task_finished(TaskOutcome): one cheap model call (through the existing ai_control routing in "cheap" mode andModelProviderPort, no new adapter) asking for ≤ 2 lessons + a 1-paragraph code-map delta. It stores them withsource=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 aconfirmaction (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.pyendpoints 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. Publishmemory.changedonorg:<id>and refresh live. - 4.7 Wire the real
MemoryPortincontainer_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 throughapp/core/redact.pybefore 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 likesmo_<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.pywith the officialmcpSDK (streamable HTTP, bound toMCP_GATEWAY_HOST:MCP_GATEWAY_PORT) andmain()forpnpm 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 publicWorkforceService.create_tasksignature; 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, kindsfact|preferenceonly,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 throughredact.py.
- 5.4
services/mcp-gateway/README.md: how to run it, the Caddy host suggestionmcp.<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 yourorg_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.