Commit Graph
36 Commits
Author SHA1 Message Date
mjalmoudy94 df7da8c75c feat(engine): workforce engine core — tag protocol, pipelines, optimistic state
Phase 1 of TODO-agent-2: pure logic plus the tables it writes to.

- domain/workforce/pipeline.py: Stage/PipelineKey/Outcome, the four pipelines,
  TRANSITIONS, next_stage, STAGE_ROLE (role+mode), resolve_pipeline with the
  no-reviewer fallback, task-kind defaults, stage -> coarse tasks.status, and
  the TaskPipelineState record (with the optimistic version).
- application/engine/protocol.py: the [[TAG attr="v"]]body[[/TAG]] parser and
  its self-closing form, the per-role/mode allow-list, literal bodies (a
  bracket inside a body is not a tag), typed readers (ASSIGN/CREATE_TASK/
  team changes/KNOWLEDGE/EDIT_FILE blocks) and the per-reply limits
  (3 tasks, 3 team changes, 3 knowledge items).
- application/engine/state.py: advance() with the optimistic compare-and-set
  and park_dependents() so a failed dependency never deadlocks its children.
- db/models_engine.py + migration a2b3c4d5e6f2: task_pipeline_state,
  employee_profiles, engine_turns, gate_results as side tables (tasks and
  employees stay locked). Linear head; alembic check reports no drift.
- repositories/engine.py + 58 new tests (test_engine_protocol,
  test_engine_pipeline) covering every pipeline's happy path, illegal
  transitions, exactly-one-winner concurrent advance, dependency parking and
  all parser edge cases.

Suite: 594 passed, ruff clean, mypy clean.
2026-10-14 15:35:31 +03:30
mjalmoudy94 cfcc520f46 Merge remote-tracking branch 'origin/dev' into dev 2026-10-14 12:08:23 +03:30
mjalmoudy94 5fb85d6eed feat(org): org creation wizard + app profile (v2 phase A1-2) 2026-10-14 10:46:47 +03:30
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
mjalmoudy94 8eb631aedf ساختار مالکیت پلتفرم: یک مالک، مدیرانِ ساختهٔ مالک، سازمان‌های مدیریت‌شده
- users.is_platform_owner → users.platform_role (owner|manager|user) با مهاجرت
  d4e5f6a7b8c9 و backfill. ایندکس یکتای جزئی uq_single_platform_owner تضمین
  می‌کند دقیقاً یک مالک وجود داشته باشد — در سطح دیتابیس، نه فقط در کد.
- ثبت‌نام عمومی حذف شد (POST /auth/signup). حساب‌ها فقط با POST /admin/users
  (فقط مالک، با رمز موقت یک‌بارمصرف) یا با پذیرش لینک دعوت ساخته می‌شوند.
  در مسیر دعوت، اگر حساب وجود نداشته باشد خودِ دعوت‌شده رمز تعیین می‌کند؛ اگر
  وجود داشته باشد لینک هرگز جای reset رمز را نمی‌گیرد.
- ساخت سازمان فقط برای مالک و مدیران؛ کاربر عادی با دعوت عضو می‌شود.
- تنزل یا حذف مدیر، سازمان‌هایش را به مالک سامانه منتقل می‌کند (مالکیت + عضویت،
  با audit). حساب مالک از API تغییرناپذیر است؛ جابه‌جایی صندلی فقط با
  POST /admin/transfer-ownership و در یک تراکنش انجام می‌شود.
- افزودن POST /auth/change-password برای تبدیل رمز موقت به رمز خصوصی.
- فرانت: صفحهٔ /admin/users، صفحهٔ /account، بازنویسی /accept-invite، حذف /signup،
  گارد /onboarding برای کاربر عادی، و نشان نقش پلتفرم در پوستهٔ اپ.
- PRAGMA foreign_keys=ON برای SQLite تا ON DELETE CASCADE در dev و prod یکسان
  رفتار کند (پیش‌تر حذف کاربر، عضویت‌هایش را یتیم می‌گذاشت).
- تست: ۳۰۰ تست بک‌اند (شامل tests/test_platform_hierarchy.py با ۲۲ تست) و ۶۴ تست
  فرانت‌اند؛ ruff، mypy، tsc، eslint و build تمیز.
2026-10-13 21:58:41 +03:30
mjalmoudy94 6937b88ca7 Merge origin/dev: لایهٔ realtime (WebSocket/RPC) + فلو دعوت عضو
ادغام ۱۲ کامیت ریموت (لایهٔ realtime روی WebSocket، حذف REST عمومی، رفع
باگ‌های صفحهٔ مدل‌ها) با ۸ کامیت محلی (ارائه‌دهندهٔ agy + رفع فلو دعوت).

تنها کانفلیکت واقعی در `app/main.py` بود و ماهوی بود، نه متنی:
ریموت اپ عمومی را به «فقط /health + /api/ws» کاهش داده و همهٔ هندلرهای HTTP
را به اپ داخلی `interfaces/realtime/rpc_app.py` منتقل کرده است. بنابراین
روتر `invites` من نباید در اپ عمومی mount شود.

راه‌حل:
- `app/main.py`: فقط `health` و `realtime_router` می‌مانند؛ ایمپورت‌های اضافی
  حذف شدند (نسخهٔ ریموت).
- `interfaces/realtime/rpc_app.py`: روتر `invites` به اپ داخلی اضافه شد تا
  `GET /api/invites/{token}` و `POST /api/invites/accept` از مسیر WS-RPC
  قابل دسترس باشند.

`api.ts` و `contracts/index.ts` خودکار و بدون کانفلیکت ادغام شدند: کلاینت
`previewInvite`/`acceptInvite` حالا از `getRealtimeClient().rpc()` رد می‌شود،
پس هیچ تغییری در فراخوانی‌ها لازم نبود.

رفع تست شکستهٔ ریموت: `src/test/version.test.tsx` مقدار `AppShell` را بدون
`RealtimeProvider` رندر می‌کرد و `useRealtime` استثنا می‌داد. حالا اسکلت
همان‌طور که در `app/layout.tsx` سیم‌کشی شده رندر می‌شود و سوکت در تست stub
شده است.

اعتبارسنجی روی درخت ادغام‌شده:
- بک‌اند ۲۷۰ تست سبز (شامل ۶ تست دعوت روی معماری جدید)؛ mypy تمیز.
- فرانت ۶۱ تست سبز؛ tsc و eslint تمیز.
- تأیید شد روترهای invite روی اپ داخلی RPC حاضرند و سطح عمومی تغییر نکرده.

نکته: ۵ خطای ruff در `tests/conftest.py`، `tests/test_error_log.py` و
`tests/test_realtime.py` از خود `origin/dev` می‌آیند و به این ادغام ربطی
ندارند؛ دست‌نخورده رها شدند.
2026-10-13 18:09:03 +03:30
mjalmoudy94 e56f105db3 رفع فلو دعوت عضو: لینک کلیک‌پذیر و صفحهٔ پذیرش دعوت
لینک دعوت هم کلیک‌پذیر نبود و هم کار نمی‌کرد؛ چهار نقص هم‌زمان:

- `invite_url` نسبی بود (`/accept-invite?token=…`) و host نداشت → حالا از
  `settings.app_url` آدرس مطلق ساخته می‌شود.
- صفحهٔ `/accept-invite` در اپ Next وجود نداشت → ۴۰۴.
- هیچ اندپوینت پذیرش دعوت در بک‌اند نبود؛ `InviteRepository.mark_accepted`
  نوشته شده بود ولی هرگز صدا زده نمی‌شد، پس توکن مصرف نمی‌شد.
- در تنظیمات، لینک به‌صورت متن ساده در `<p>` رندر می‌شد.

بک‌اند:
- روتر `app/interfaces/http/routers/invites.py`:
  `GET /api/invites/{token}` (پیش‌نمایش) و `POST /api/invites/accept`.
  عمداً زیر `/organizations/{org_id}` نیست: دعوت‌شونده هنوز عضو نیست و
  `get_org_context` ردش می‌کند، پس خود توکن مجوز است. accept ایدمپوتنت است.
- `AuditAction.MEMBER_JOIN = "member.join"`.
- رفع باگ نهفته: `Invite.is_expired()` بین `expires_at` خوانده‌شده از SQLite
  (naive) و `utcnow()` (aware) خطای مقایسه می‌داد. طبق قرارداد موجود پروژه
  (`_aware` در `adapters/agy.py`)، در `repositories/identity.py` تابع `_aware`
  اضافه و روی `expires_at`/`accepted_at` اعمال شد.

فرانت:
- صفحهٔ `app/accept-invite/page.tsx` با پیش‌نمایش سازمان/نقش و پذیرش؛ پس از
  پذیرش `setActiveOrganization` می‌کند تا در همان سازمان لند کند.
- قراردادهای `InvitePreview`/`AcceptInviteResult` و کلاینت
  `previewInvite`/`acceptInvite`.
- `login`/`signup` پارامتر `?next=` را می‌فهمند (فقط same-origin) تا توکن دعوت
  در مسیر احراز هویت گم نشود.
- `settings`: لینک به `<a>` واقعی + دکمهٔ «کپی لینک».

تست: `tests/test_invites.py` (۶) و `src/test/accept-invite.test.tsx` (۵).
بک‌اند ۲۵۲ سبز + ruff/mypy تمیز؛ فرانت ۶۱ سبز + tsc/eslint تمیز.
2026-10-13 17:47:26 +03:30
mjalmoudy94 d5c9917371 feat(admin/models): delete an added model with inline confirm
Backend: AiModelRepository.delete() + DELETE /api/admin/models/{id} (owner-only,
audited) in the RPC-served admin router; session commits per request.
Frontend: api.adminDeleteModel() (WebSocket RPC DELETE) and a per-row حذف
action with a two-step inline confirm (تأیید حذف / انصراف) that removes the
row from state on success. tsc clean, py_compile clean.
2026-10-13 17:40:16 +03:30
mjalmoudy94 f2983afaec fix(admin/models): make account->model and model->effort cascade fire on first pick
The account and model <select>s had no value="" placeholder option, so a
controlled value of "" made the browser display the first real entry as if
it were already chosen. Clicking that entry was not a change, so onChange
never fired and the cascade (account->catalog, model->efforts) only ran after
a *second*, different selection.

Prepend a real value="" placeholder option to both dropdowns so the displayed
value matches the controlled state and the first real selection now triggers
onChange as expected.
2026-10-13 17:29:12 +03:30
mjalmoudy94 3711ca5513 fix(admin/models): make add-to-list visible + clarify account->model loading
- Newly added model is highlighted (2.5s) and the list scrolls into
  view so the owner sees it land immediately after create.
- Model dropdown shows an explicit 'loading catalog…' placeholder while
  the selected account's live catalogue is fetched, instead of a
  misleading 'no catalog' message during the request.
2026-10-13 17:11:56 +03:30
mjalmoudy94 91a8ae6c63 fix(admin/models): collapse agy effort variants into one clean option
The model dropdown labeled each option as "{display} ({id})", and since
agy's display name already bakes in the effort, entries rendered like
"Gemini 3.8 Flash (High) (gemini-3.8-flash-high)" — the name, its effort,
AND the raw id all in one, redundant with the separate effort field below.

Group the live catalog into families by base name (effort qualifier
stripped): "Gemini 3.8 Flash" is now one option, and the reasoning effort
is chosen in the existing effort dropdown. On create, the family + chosen
effort resolves back to the concrete agy catalog entry
(gemini-3.8-flash-medium etc.), so the saved internal_name and
reasoning_effort are byte-for-byte what a direct pick produced before.

Frontend-only; no backend/catalog change. tsc --noEmit clean.
2026-10-13 16:54:33 +03:30
mjalmoudy94 e50c86c451 fix(web): stop authenticating WebSocket reconnect storm
Console evidence: on the authenticated layout, api.me() opened
wss://.../api/ws?token=<JWT> and the browser logged "WebSocket is closed
before the connection is established", then an uncaught
connection_closed rejection — a self-inflicted reconnect storm.

Root cause (two halves):
1. connectedToken was only assigned inside onopen, so during the CONNECTING
   window every other caller (RealtimeProvider's 2s poll, rpc's whenOpen)
   saw connectedToken !== token and called openWith again, tearing down the
   socket it had just dialled.
2. Replacing a socket left its late onclose to run onSocketClosed, which nulled
   this.socket and connectedToken — clobbering the successor and firing a
   spurious reconnect + rejected openPromise.

Fix:
- Set connectedToken the moment we commit to a dial (in openWith), so
  concurrent callers wait on the same openPromise instead of re-dialling.
- Guard onopen/onmessage/onclose with `this.socket !== socket` so a replaced
  socket's late callbacks are ignored and cannot clobber its successor.

Anonymous login escaped this (both tokens null); an authenticated connection
(a real JWT) triggered it, matching the reported failure.
2026-10-13 16:07:37 +03:30
mjalmoudy94 22034fd90a fix(web): make WebSocket RPC self-heal against half-open sockets
A login over the socket could hang the full 60s timeout when the
reverse proxy allowed the WebSocket handshake but dropped the
server->client direction without closing the socket (so onclose never
fired and the client saw a live-but-unresponsive link).

- Track last inbound frame time (any frame, incl. hello/pong) as proof
  the link is bidirectional.
- ensureLive(): before sending an RPC, replace a socket that has been
  silent for >30s (SOCKET_STALE_MS).
- On timeout, recycle the connection and retry once on a fresh socket
  instead of hard-failing; only idempotent-damage-free flows are affected
  since the first attempt is torn down first.

Reduces the observed "درخواست از طریق وب‌سوکت زمان‌بر شد" on login to a
single reconnect+retry rather than a hard 60s wait.
2026-10-13 14:23:40 +03:30
mjalmoudy94 8066e9d055 feat: remove public REST API; route all app calls over WebSocket RPC
The platform no longer exposes a public /api/* REST surface. The original
HTTP route handlers are preserved verbatim as the single source of truth
inside an internal, never-socket-bound FastAPI app (rpc_app), and are now
reached only via RPC-over-WebSocket dispatch.

- realtime router: anonymous connections allowed only to /api/auth/login
  and /api/auth/signup; all other RPCs/subscribes refused until token auth.
- RPC dispatch re-injects the bearer token and X-Request-ID, then forwards
  through httpx.ASGITransport against the internal app (no network hop).
- Public app now serves only /, /health, /health/ready and /api/ws;
  docs/openapi disabled.
- Frontend: apiRequest routes /api/* to ws.rpc(); /health probes keep fetch.
  RealtimeClient gains rpc() with id correlation, timeout and AbortSignal.
- Tests repointed to get_rpc_app(); anonymous-auth behaviour covered.

Result: zero REST on the public surface; 264 backend tests + frontend
typecheck pass.
2026-10-13 13:54:56 +03:30
mjalmoudy94 f1e5df29ce feat: realtime WebSocket layer for live events and streaming logs
- Backend: ConnectionManager, Redis-backed bus with in-process fallback,
  channel authorization, and typed emit helpers wired into workers
  (ai_runtime, sandbox_gateway), workforce/sandbox/billing/audit/error services.
- Frontend: single shared WS client with backoff + heartbeat, RealtimeProvider,
  useLiveChannel/useLiveEvent hooks, live indicators on task detail, manager
  chat, dashboard, traces, sandbox terminal, admin errors, and billing.
- Contracts: WsEvent/WsServerFrame/WsClientFrame types mirroring the port.

All 255 backend tests pass; web typecheck and build pass.
2026-10-13 12:56:36 +03:30
mjalmoudy94 afdef4b444 به‌روزرسانی: استخراج خودکار و پویای فهرست مدل‌ها و سطوح تلاش پردازش از ابزار خط فرمان و ارتقای نسخه به ۰.۱.۴ 2026-10-13 09:00:18 +00:00
mjalmoudy94 f21abf53ee به‌روزرسانی: پشتیبانی پویا از مدل‌های جدید و قالب‌بندی خودکار عناوین کاتالوگ ارائه‌دهنده 2026-10-13 08:35:09 +00:00
mjalmoudy94 5a64ebf560 رفع باگ: تصحیح مسیردهی ارتباط کلاینت با سرویس و حل خطای دسترسی شبکه به همراه افزودن آیکون سامانه 2026-10-13 08:05:58 +00:00
mjalmoudy94 0b72c4ec6d به‌روزرسانی: دریافت زندهٔ کاتالوگ مدل‌ها و افورت‌های استدلال پشتیبانی‌شده بر اساس اکانت انتخابی 2026-10-13 07:41:00 +00:00
mjalmoudy94 da5a2a5d1a agy تنها ارائه‌دهندهٔ مدل؛ حذف بخش ارائه‌دهنده‌ها و افزودن زنجیرهٔ انتخاب مدل
- حذف کامل providerهای mock و openai_compatible: دامنه، آداپوترها، رجیستری، کانفیگ، seed و رابطٔ مدیریت
- بخش «ارائه‌دهنده‌ها» از کنسول حذف شد؛ ورود اکانت‌ها فقط از صفحهٔ اکانت‌های agy
- صفحهٔ مدل‌ها بازطراحی شد: ارائه‌دهنده → اکانت → مدل (کاتالوگ لحظه‌ای و زندهٔ همان اکانت از Google) → افورت استدلال؛ بدون هیچ ورود دستی نام مدل
- مدل‌ها ستون reasoning_effort گرفتند؛ آداپتور آن را به thinkingConfig (بودجهٔ ۱۰۲۴ تا ۳۲۷۶۸) ترجمه می‌کند و فقط به مدل‌های استدلالی می‌فرستد
- مهاجرت c2d3e4f5b6a7: افزودن reasoning_effort و حذف ردیف‌های providerهای قدیمی
- تست‌ها: ۲۴۶ تست بک‌اند و ۵۱ تست فرانت سبز؛ ruff و mypy و tsc و eslint تمیز
2026-10-13 10:01:11 +03:30
mjalmoudy94 a2284db4f4 اعمال motion بر صفحهٔ اکانت‌های agy
صفحهٔ `/admin/agy-accounts` بعد از پاس motion ساخته شد، بنابراین ریشهٔ
خودش `.anim-stagger` نداشت و کل صفحه یک‌جا ظاهر می‌شد در حالی که بقیهٔ
پنل آبشاری وارد می‌شوند. فهرست اکانت‌ها هم که async لود می‌شود stagger
نگرفته بود.

هر صفحهٔ جدیدی که بعد از این ساخته شود باید همین دو کلاس را بگیرد؛
`motion.test.tsx` واژگان را محافظت می‌کند اما اعمال آن روی صفحه‌های
تازه را نمی‌گیرد.
2026-10-13 08:35:56 +03:30
mjalmoudy94 0fd50d94fc افزودن نشان نسخه به پایین بخش کناری
یک حباب کوچک و شیشه‌ای با فونت تک‌عرض در فوتر ستون کناری، کنار دکمهٔ خروج:

- `font-mono` — پشتهٔ فونت‌های سیستمی (ui-monospace، SFMono، Menlo،
  Consolas) که همیشه نصب‌اند؛ پس نسخه روی هر دستگاه یکسان رندر می‌شود،
  بدون انتظار برای وب‌فونت، و رقم‌ها جابه‌جا نمی‌شوند.
- شیشه‌ای با translucent و blur: `bg-white/5` + `border-white/10` +
  `backdrop-blur-sm`. بدون gradient و بدون shadow، مطابق §12 پلن.
- `ltr-inline` تا الگوریتم bidi در پوستهٔ RTL رشتهٔ لاتین را جابه‌جا نکند،
  و `tabular-nums` تا عرض numeralها ثابت بماند.

نسخه از `src/lib/version.ts` می‌آید — به‌جای import کردن کل
`package.json` که کل manifest را برای یک رشته به باندل کلاینت می‌کشید.
هزینهٔ این انتخاب drift است، پس `version.test.tsx` می‌سنجد که با
`apps/web/package.json` یکی باشد.

تست‌های جدید: ۵ (نسخه و نشان نسخه در پوسته).
2026-10-13 08:35:29 +03:30
mjalmoudy94 3d1e98f4fb افزودن ارائه‌دهندهٔ agy (Google Antigravity) با لاگین headless و پول چرخشی اکانت
- Adapter اکانت‌محور با چرخش round-robin و بنچ‌شدن خودکار روی ۴۲۹/۴۰۱
- ورود ادمین از طریق URL بازگشتی گوگل (PKCE) و ذخیرهٔ رمزنگاری‌شدهٔ refresh token
- صفحهٔ مدیریت اکانت‌های agy در پنل ادمین به همراه تست و فعال/غیرفعال‌سازی
- مهاجرت Alembic b1c2d3e4f5a6 برای ستون‌های اکانت agy
- seed همیشه کاتالوگ agy را می‌سازد و فقط با AI_DEFAULT_PROVIDER=agy مسیریابی می‌شود
2026-10-13 08:20:27 +03:30
mjalmoudy94 54074f10c3 اضافه کردن transition-colors به همهٔ حالت‌های hover
چندین سطح تعاملی (منوی سازمان، منوی کاربر، لینک‌های بردکرامب، آیتم‌های
پالت فرمان، دکمهٔ ساخت سازمان، لینک ردیابی) رنگ را در hover یک‌باره
تغییر می‌دادند و بقیهٔ رابط نرم بود. اکنون همه یکسان transition
می‌گیرند.
2026-10-13 02:55:49 +03:30
mjalmoudy94 df2c8546db افزودن سیستم Motion به تمام بخش‌های رابط کاربری
واژگان مشترک حرکت در globals.css: شش @keyframes و شش کلاس .anim-*
به‌همراه .anim-stagger برای ورود آبشاری. همهٔ مدت‌زمان‌ها پشت custom
property هستند تا یک override کل اپ را تنظیم کند.

- packages/ui: Button، Card، Stat، Loading/Empty/ErrorState، UsageBar،
  Timeline، Modal، Drawer، Table، ToastStack و ArtifactViewer
- ریشهٔ هر صفحهٔ (app) با .anim-stagger وارد می‌شود
- AppShell: محتوا fade، منوی کنار stagger، منوهای کشویی slide-up
- prefers-reduced-motion مدت‌زمان‌ها را به ۰.۰۱ms می‌رساند تا
  وضعیت پایانی حفظ شود
- قرارداد motion در src/test/motion.test.tsx تست می‌شود تا
  بازنویسی بعدی آن را بی‌صدا حذف نکند
- تست‌های فرانت‌اند: ۳۵ ← ۴۵
2026-10-13 02:48:59 +03:30
mjalmoudy94 55b9055b50 افزودن پالت فرمان (⌘K) و صفحهٔ مصرف سراسری پنل مالک
دو شکاف رسیدگی‌شده از بازبینی PLAN.md و TODO.md:

- پالت فرمان: جست‌وجو و رفتن سریع بین بخش‌ها با ⌘K/Ctrl+K، پیمایش با
  کلیدهای جهت‌دار، Enter برای رفتن، و Escape برای بستن. گروه اختصاصی
  «کنسول مالک سامانه» فقط برای مالک سامانه نمایش داده می‌شود.
- صفحهٔ /admin/usage: گزارش مصرف سراسری به تفکیک سازمان. اندپوینت
  `GET /admin/usage` و کلاینت `adminUsage()` از قبل وجود داشتند و توسط
  صفحات نمای کلی و سازمان‌ها مصرف می‌شدند؛ این صفحهٔ اختصاصی آن است.

همچنین `NAV_ITEMS` به `lib/nav.ts` منتقل شد تا sidebar و پالت فرمان
هرگز از هم واگرا نشوند.

تست: ۱۰ تست جدید برای پالت فرمان. مجموع تست‌های فرانت‌اند از ۲۵ به ۳۵
رسید. tsc، eslint و ruff تمیز.
2026-10-13 02:28:30 +03:30
mjalmoudy94 0b28a9f365 تغییر نام اپلیکیشن به Smart Organization
نام نمایشی و هویت اپلیکیشن در همهٔ سطوح کاربر-پسند از «AGY Web» به
«Smart Organization» تغییر کرد:

- UI: عنوان تب مرورگر، صفحهٔ ورود، برند و لوگوی پوستهٔ اپ (AG → SO)
- بک‌اند: مقدار پیش‌فرض app_name، نام و توضیح توزیع پایتون
- مانیفست‌ها: package.json ریشه و چهار پکیج، pyproject.toml
- مستندات: README، PLAN، TODO و دو سند docs

شناسه‌های داخلی عمداً دست‌نخورده مانده‌اند: scope پکیج‌های @agy/*،
کلیدهای localStorage، نام‌های logger، نام فایل پایگاهٔ داده، و اطلاعات
اتصال پایگاه داده در .env.example و infra/docker/docker-compose.yml.
2026-10-13 01:26:44 +03:30
mjalmoudy94 717e53a7ce نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
2026-10-13 00:39:28 +03:30