Phase 2 of TODO-agent-2: one employee, one turn, real tools in the sandbox.
- engine/prompt.py: the stable, cache-friendly prefix in the plan's order
(rules → role protocol → toolkit → app profile → memory), a task brief
capped at 1200 chars, role-specific stage context (spec for the developer,
the diff only for the reviewer, test output for the tester) and a hard
prompt cap that drops the oldest tool results first.
- engine/tools.py: READ/WRITE/EDIT/LIST_DIR/SEARCH/RUN executed inside the
sandbox through SandboxProviderPort, rooted at the worktree's sandbox path.
Path escapes, .. and .git are refused; git push/remote/config are refused
with "the platform does git"; RUN clamps its timeout (120s default, 600s
max) and keeps the tail of its output; every execution writes a tool_calls
row so the approvals UI shows real work.
- engine/runner.py: the loop — model call, parse tags, execute tool tags,
feed results back — until a terminal tag, the tool cap, the turn timeout or
a cancellation. The employee's model is checked against ModelGrantPort
before the first call (ungranted → error_kind=auth, no spend), errors are
classified transient vs fatal, and every model call writes usage events
through the one shared record_model_usage path (extracted from
WorkforceService so manager chat counts too). Writes engine_turns, run_steps
and tool_calls.
- engine/events.py: RunFeed batches run.log (≤1 event / 500 ms / stream, ≤200
lines) and emits run.tool and task.gate through the generic envelope.
- tests: engine_fakes.py (ScriptedModel, FakeSandbox, FakeGrants,
RecordingMemory) plus 54 new tests in test_engine_runner,
test_engine_prompt and test_engine_tools.
Suite: 648 passed, ruff clean, mypy clean.
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.
Gap-closing on top of the merged A3-3..A3-6 work:
- ProjectService.create_preview_domain now refuses a label that is a reserved
platform name (git, mcp, api, panel, ...) via PROTECTED_HOST_LABELS. The rules
set existed but only the owner-facing validate_extra_host consulted it, so the
tenant path could reserve one.
- process.py: signal.SIGKILL is resolved through getattr with a SIGTERM
fallback, so the module imports and type-checks on Windows where SIGKILL does
not exist.
- Three more Windows test repairs: a git-created .git must be unlinked and
recreated rather than overwritten in place; POSIX-only chmod and symlink
assertions are gated on os.name (the security property they assert is checked
everywhere, the platform-specific mechanics only where they apply).
- docs/plan/reports/agent-3-final.md: the integration hand-off — host
requirements, every setting, every endpoint added or changed, the tables, the
open CHANGE REQUESTS, a 12-step manual QA script and the known limitations.
Checks: ruff clean, mypy clean (136 files), 521 API tests pass, 83 web tests
pass, tsc clean, next lint clean.
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
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
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
- 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 تمیز.
- حذف کامل providerهای mock و openai_compatible: دامنه، آداپوترها، رجیستری، کانفیگ، seed و رابطٔ مدیریت
- بخش «ارائهدهندهها» از کنسول حذف شد؛ ورود اکانتها فقط از صفحهٔ اکانتهای agy
- صفحهٔ مدلها بازطراحی شد: ارائهدهنده → اکانت → مدل (کاتالوگ لحظهای و زندهٔ همان اکانت از Google) → افورت استدلال؛ بدون هیچ ورود دستی نام مدل
- مدلها ستون reasoning_effort گرفتند؛ آداپتور آن را به thinkingConfig (بودجهٔ ۱۰۲۴ تا ۳۲۷۶۸) ترجمه میکند و فقط به مدلهای استدلالی میفرستد
- مهاجرت c2d3e4f5b6a7: افزودن reasoning_effort و حذف ردیفهای providerهای قدیمی
- تستها: ۲۴۶ تست بکاند و ۵۱ تست فرانت سبز؛ ruff و mypy و tsc و eslint تمیز
- Adapter اکانتمحور با چرخش round-robin و بنچشدن خودکار روی ۴۲۹/۴۰۱
- ورود ادمین از طریق URL بازگشتی گوگل (PKCE) و ذخیرهٔ رمزنگاریشدهٔ refresh token
- صفحهٔ مدیریت اکانتهای agy در پنل ادمین به همراه تست و فعال/غیرفعالسازی
- مهاجرت Alembic b1c2d3e4f5a6 برای ستونهای اکانت agy
- seed همیشه کاتالوگ agy را میسازد و فقط با AI_DEFAULT_PROVIDER=agy مسیریابی میشود
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.