Commit Graph
9 Commits
Author SHA1 Message Date
mjalmoudy94 5095a29df9 feat(org): platform invites, roles, org sharing (v2 phase A1-1)
- New platform_invites table (migration a1b2c3d4e5f1) + PlatformInviteRow
- Owner-only endpoints in admin_users.py: issue/list/revoke platform invites
- invites.py: two-kind resolution (org + platform), email-supplied accept path
- auth.py /me gains needs_onboarding (manager, 0 orgs, not owner)
- organizations.py: transfer-ownership, leave (last owner guard), list/revoke org invites
- contracts + web UI: admin/users invite modal, accept-invite platform variant, onboarding gating
- Tests: test_org_platform_invites.py (18), org-sharing.test.tsx; report under docs/plan/reports

Note: (app)/settings "اشتراک‌گذاری سازمان" section (1.10) remains presentational work;
endpoints are done and tested.
2026-10-14 09:14:09 +03:30
mjalmoudy94 3c8296e354 feat(delivery): the Application page + owner consoles (v2 phase A3-6)
The user-visible payoff of the whole delivery workstream.

- (app)/app: four tabs — pipeline (runs + the eight steps + a live console),
  releases (with demo rollback), production (target summary, confirmed deploy,
  history), Git. The header answers "is it live": demo URL, live release,
  health state.
- Live console: batches onto an animation frame, stops following the tail the
  moment the user scrolls away from the bottom, strips ANSI colour codes and
  carriage-return redraws. A console that yanks the view back is worse than none.
- Production panel: the target is shown as the operator recognises their server
  plus has_secret/ثبت شده. No field anywhere could render a credential.
- contracts: the v2 delivery types and seven new WsEventType members.
- /admin/sandbox (owner): per-role container counts, running containers, per-org
  limits, image mapping, stop with the id typed back (audited). A provider without
  the capability reports an empty list rather than an error.
- /admin/domains (owner): every route with its owning app, attach/detach (host
  typed back), idempotent re-sync.
- /projects removed entirely, with a permanent redirect to /app.

Tests: 502 API (+3), 78 web (+11); ruff, mypy, tsc and lint clean.

Report: docs/plan/reports/agent-3-phase-6.md
2026-10-14 02:03:38 +03:30
mjalmoudy94 72408a0a4f feat(delivery): production deploy over SSH/SFTP/FTP/FTPS (v2 phase A3-5)
The one path that touches a customer's server, and the one the platform cannot
undo. It is human-triggered, confirmed and audited; there is deliberately no
code path from a pipeline run into it (D13).

- deploy_ssh.py: SshDeployer. Release lands in releases/<ts>-<sha7>/, the
  post-deploy command runs with cwd there, and only then does `current` move —
  atomically (relative symlink + mv -T). A failed hook or extraction never moves
  it, so the previous release keeps serving. Host key is pinned on first success
  (TOFU); a mismatch is a hard failure. Prunes to the newest five.
- deploy_ftp.py: FtpDeployer. FTP has no atomic switch and no rollback except a
  backup, so a backup is mandatory: every file about to be overwritten is
  archived under ARTIFACTS_ROOT before anything is uploaded. The artifact is
  extracted, then the build dir (dist/build/public/out) is chosen — a static
  deploy must not publish the repository. Archive members are sanitised: absolute
  paths, `..` and escaping links are refused.
- deploy_target.py: the tester that dispatches on `kind`.
- deploy_service.py + production_deploys (migration a3b4c5d6e7f4) + 4 routes.
  One deploy per project at a time; the org slug must be typed back; the target
  summary reports has_secret, never the secret.

Tests: 499 API (+26) with fake SSH/FTP transports; ruff, mypy, alembic clean.

Report: docs/plan/reports/agent-3-phase-5.md
2026-10-14 01:16:23 +03:30
mjalmoudy94 82cea1e690 feat(delivery): owner-only domains + idempotent route re-sync (v2 phase A3-4)
Domains are infrastructure, and in v2 infrastructure is the platform owner's
alone (D9). A tenant gets one automatic demo host and nothing else.

- admin_domains.py (owner-only): list every route with its owning app, attach an
  extra host, detach with the host typed back as confirmation (audited), re-sync
  the router from the database, and read the suggested MCP gateway route.
- domain/platform/rules.py: validate_extra_host. Two shapes only — a single
  label under the managed root, or a full custom FQDN when ALLOW_CUSTOM_DOMAINS
  is on. A wildcard is refused in both (it would match another tenant's
  subdomain), and so are 30 protected platform labels (git, mcp, panel, api...).
  Enforced in the router *and* again in the adapter.
- Both router adapters gained routes_fingerprint / list_all_routes /
  sync_routes. Re-sync writes and reloads only when the rendered state differs:
  a reload drops every in-flight connection on the host, including live demos.
  Adapter-level extras, not additions to the frozen DomainRouterPort.
- The member-facing preview-domain routes are owner-only now. They are kept
  because the demo host is still created through them at app-creation time.

Tests: 473 API (+58); ruff and mypy clean.

Report: docs/plan/reports/agent-3-phase-4.md
2026-10-14 00:57:08 +03:30
mjalmoudy94 a1b8a28dda feat(delivery): live CI/CD pipeline + demo runtime (v2 phase A3-3)
A push to `dev` now becomes a live demo, and a failed build never takes the
demo down.

- Four tables (pipeline_runs, pipeline_steps, releases, app_runtimes) and their
  migration; alembic upgrade/downgrade/check all clean.
- DeliveryService implements DeliveryHooks: on_app_created provisions the demo
  port + host + first build (idempotently); on_branch_pushed queues a run for
  `dev` and supersedes older *queued* runs, leaving a running one alone.
- The 8-step pipeline. Blue/green is real: the replacement starts on a staging
  port, is healthchecked, and only then does the route move — then the previous
  container is stopped. A failure removes the replacement, releases its port and
  leaves the old container serving.
- The demo port ledger is the DATABASE, not the router adapters' in-memory maps:
  the API and the delivery worker are separate processes.
- Runs are claimed with a conditional UPDATE, so two workers never build the
  same run.
- Live logs are batched (1 event / 500 ms, 200 lines) and redacted as a whole
  tail — per-chunk redaction misses a secret split across pipe reads, and the
  *stored* tail leaked until this was fixed.
- delivery worker: drain the queue, then poll remotes for pushes made outside
  the engine.
- 7 routes; reads need membership, actions need projects.manage and are audited.

Tests: 415 API (+21, 1 docker-marked skipped); ruff, mypy, alembic check clean.

Report: docs/plan/reports/agent-3-phase-3.md
2026-10-14 00:44:02 +03:30
mjalmoudy94 d41e00ad10 feat(delivery): git workspaces + SSRF host check (v2 phase A3-2)
The platform's side of D6: git runs on the host, in a gitdir that is never
mounted into a container, so repository credentials never reach the model.

- adapters/git_workspace.py: LocalGitWorkspaceAdapter implementing
  RepoWorkspacePort. Bare shared gitdir + a dev work tree mounted at
  /workspace + one linked worktree per task under work/.worktrees/<id>
  (excluded via the gitdir's info/exclude, not the project's .gitignore).
  Explicit --git-dir/--work-tree, core.hooksPath=/dev/null, fsmonitor off,
  quotePath off, GIT_CONFIG_NOSYSTEM, a private HOME per project, prompts off,
  one asyncio.Lock per project. Token via GIT_ASKPASS reading a 0600 file; SSH
  key as a 0600 temp file with a pinned known_hosts. Everything deleted in a
  finally; only a *path* ever travels through the environment.
- The bug this phase existed to find: --git-dir=<shared> does not address a
  linked worktree, so `git commit` in a task worktree committed straight to
  `dev`. Operations now address <gitdir>/worktrees/<task_id> explicitly — and
  never read the worktree's .git file, which a repository can rewrite.
- domain/platform/rules.py: is_blocked_address (pure) + assert_remote_host_allowed
  (resolves, refuses if any address is loopback/private/link-local/metadata).
  Shared by the git remote test and, later, every deploy target.
- ProjectExternalGit domain model + repository; container_delivery wires the
  real adapter behind GIT_WORKSPACE=local with a resolver that joins the
  project, its repo and its (decrypted) credentials.

Tests: 394 API (+48) against real git and a local bare repo; ruff and mypy clean.

Report: docs/plan/reports/agent-3-phase-2.md
2026-10-14 00:18:13 +03:30
mjalmoudy94 a0c8473eb8 feat(delivery): docker sandbox adapter + owner-only infrastructure (v2 phase A3-1)
The execution boundary for v2. Adds the local docker/podman sandbox adapter
and the hardened host subprocess runner every other delivery adapter will use.

- adapters/process.py: the one place the platform shells out. argv-only (no
  shell), start_new_session + killpg on timeout, concurrent stdout/stderr
  drains with a byte cap, explicit minimal env (never os.environ), and a
  missing binary reported as spawn_failed rather than an exception.
- adapters/docker_sandbox.py: DockerSandboxAdapter over SandboxProviderPort.
  --user 1000:1000, --cap-drop ALL, no-new-privileges, pids/cpu/memory limits,
  --read-only with tmpfs scratch, one bind mount (the work tree, never the git
  dir), labels, no published ports. Path-safe file access; secrets refused in
  `docker exec -e`. Extras for later phases: start_demo_container (loopback
  publish only), list_managed_containers, remove_container.
- registry: SANDBOX_PROVIDER=docker, cached singleton so the workspace registry
  survives create_workspace -> start_session.
- sandbox_gateway worker: reap_orphans() removes labelled containers no live
  session points at, audited as sandbox.reap; run_pass() does both passes.
- sandbox router: every mutating route now requires the platform owner
  (PLAN-v2 §7.7); read-only status routes stay open to org members.
- sandbox-panel: canManageInfra prop (fails closed) hides the create form,
  the session panel and the action buttons from non-owners.

Tests: 346 API (+37), 67 web (+3); ruff, mypy, tsc and lint clean.
Three existing tests now drive the sandbox as the platform owner, since that
is who may touch it.

Report: docs/plan/reports/agent-3-phase-1.md
2026-10-13 23:57:09 +03:30
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
mjalmoudy94andClaude Sonnet 5.5 3a68b0fa07 docs: v2 plan (shared plan + foundation + 3 agent TODO files)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 22:57:06 +03:30