Repository navigation
feat(git-sync): an agent created as an agent owns its repository (trinity-enterprise#705) - #3020
Conversation
…ew default driver SQLAlchemy 2.1.0 (released 2026-09-24) resolves a bare postgresql:// URL to the psycopg (v3) driver. tests/requirements-test.txt carried an uncapped sqlalchemy>=2.0.0, so every run since resolved 2.1.0 and the pg-migrations job's alembic upgrade died with ModuleNotFoundError: No module named 'psycopg'. The backend image pins 2.0.36 + psycopg2-binary, so production was never affected; CI was running a stack prod never does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…efuses on a shared source-mode branch (#3011) The heartbeat was add -A -> commit -> push origin HEAD with no fetch, so the first foreign push to the agent's branch failed every later cycle non-fast-forward, forever, with the agent's commits piling up locally. - fetch origin <branch>; if behind, rebase --autostash onto it and push with --force-with-lease pinned to the fetched sha (a racing push is rejected, never overwritten); a plain push otherwise; a branch not yet on the remote is created by the push - rebase conflict (or timeout): rebase --abort, repo left as it was, record 'diverged: rebase conflict on <branch>' for the existing sync_failing path; never resolve, overwrite or reset - source-mode agent on the default branch (origin/HEAD, else main/master) refuses before committing: 'refused: source-mode on <branch>'; fork-to-own agents (GIT_UPSTREAM_REPO or an upstream remote) exempt - sync-state.json gains behind_after_fetch and last_successful_push_at Fixes #3011 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PUT /git/auto-sync wrote the DB row and nothing else: the container gated on GIT_SYNC_AUTO read once at startup, and every recreate re-derived it as DB flag OR baked env. OFF never took effect on an agent that baked the env at creation; ON waited for the next recreate. - agent loop: starts whenever it can ask the platform and reads the owner's flag every cycle via GET /git/auto-sync with its own MCP key; OFF skips the cycle, ON runs it, no recreate. 404 'Git not configured' -> off; platform unreachable / 5xx / auth refusal -> the env fallback. /api/git/status reports the value the loop runs with. - one writer: _apply_git_env_from_db derives GIT_SYNC_AUTO from the DB flag alone (OR + log removed); creation writes the flag from the same _git_auto_sync_baked predicate that bakes the env, ghosts included. - one-shot backfill (SQLite + Alembic 0075): live non-source-mode ghosts, the env-true/DB-0 slice the DB can identify. - Settings -> Git sync panel with both toggles (auto-sync, pause schedules while sync is failing). Fixes #3010 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…omments (#3010) Found on a live dev agent: the first cycle waits a full interval, so /api/git/status reported the env fallback (off) for 15 minutes while the owner's flag said on. Resolve once before the first sleep. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Contents: read-only fine-grained PAT passed every check (ls-remote talks to upload-pack; REST permissions.push is the user's role, not the token's grant) and then failed every auto-sync for the agent's life. - git_service.probe_push_access: git push --dry-run of a throwaway ref from an empty scratch repo, PAT via git_auth_env (never argv); asks receive-pack, creates nothing. ok / denied / transient. - creation: agents that will auto-push (_git_auto_sync_baked) are probed; denied -> 400 naming GitHub's reason and the fix, before any branch is reserved or container created; transient -> logged, non-blocking. - sync health: a sync_failing item whose error is a refused push is titled 'Git token can't push', says it won't recover on its own, and carries cause=push_denied + remediation. Fixes #2107 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…2107) CodeQL py/clear-text-logging-sensitive-data: the line comes from a child that held the token. It is still returned (scrubbed) to the caller. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…feature/ent705-agent-defaults
…to feature/ent705-agent-defaults
…to feature/ent705-agent-defaults # Conflicts: # docs/memory/architecture/agent-lifecycle.md # tests/registry.json
…to feature/ent705-agent-defaults # Conflicts: # tests/registry.json # tests/unit/test_1484_create_agent_characterization.py
…nity-enterprise#705) Create-time kind: agent (default) | deployment, on POST /api/agents and MCP create_agent. An explicit source_mode always wins. An agent gets a working branch + auto_sync_enabled + freeze-on-sync- failure, but only when the #2107 push probe says its token can push to that repo. A deployment, an ephemeral ghost, a tokenless create, a refused or unverifiable probe all stay pull-only, with the reason on the create response's git_mode (never on the /ws broadcast). A template someone else owns therefore never receives an agent's branches (the ent#162 class); Cornelius, built from a shared public upstream, is pinned pull-only. Fork-to-own gets the trio. Existing agents are not flipped. Operator runbook for migrating a live agent: docs/migrations/AGENT_WORKING_BRANCH_DEFAULT_2026-09.md. Stacked on #3016, #3017, #3018 (+#3015): merges only after them. Related to Abilityai/trinity-enterprise#705 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#705) - a fork asked for as a deployment is reported as one (the freeze write reads the same kind), pinned by a test - say what explicit source_mode wins: the mode; every auto-pushing agent also gets freeze-on-failure - state the pre-#706 freeze trigger (3 failed syncs) in the requirement - restore the blank line between models Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
/review ReportScope reviewed: the ent#705 commits ( Plan completion (ent#705 AC):
Execution coverage
Mutation: with Critical FindingsNone. Informational Findings (fixed in
|
…s (trinity-enterprise#705) The ent#705 kind default reads config.kind, .model_fields_set and .name; the fork gate's SimpleNamespace stand-in had none of them. It sets source_mode=True explicitly, so its model_fields_set says so - exactly what a real AgentConfig(source_mode=True) reports - and the default never probes push access (the gate still costs no extra GitHub call). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
merge-train: ejected from this train (and #3021 / #3022 with it, since they are stacked on this branch). #3016, #3017 and #3018 are riding without it. Needs a ruling, probe vs. ownership: Possible directions: require a per-user or per-agent PAT tier and/or a private repo before defaulting to push. If the risk is accepted as-is, this is otherwise READY. Explicit |
|
✅ Nightly unit-suite clean when this PR is merged into |
|
Resolve by merging |
…efaults # Conflicts: # src/backend/db/migrations.py # src/backend/services/agent_service/crud.py # tests/registry.json # tests/unit/test_1484_create_agent_characterization.py
…ing-branch default (trinity-enterprise#705) Ruling on the probe-vs-ownership question in PR #3020: being able to push is not owning the repo. resolve_github_pat can return the global platform PAT for any creator, so on an install where that token can write a shared template repo, every default create was granted a working branch and auto-pushed the agent's workspace to it (the ent#162 class). _apply_agent_kind_default now takes the resolved tier and grants only for per_user / per_agent; the global tier is pull-only with a reason that names the fix (add your own GitHub token), and is never probed. The test that asserted the old behaviour (shared template + platform PAT) now runs on a creator-owned token; a new test pins the platform-token refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own (trinity-enterprise#705) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n (trinity-enterprise#704) Follows the #3020 ruling: the platform-wide GitHub token never earns the working-branch default. The picker no longer promises one for any token that can push, and no longer hard-codes the 15-minute interval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Ruling on probe vs. ownership (vybe, 09-25): being able to push is not owning the repo.
Tests: 133 in #1484 + fork files. 2,775 across the git, sync, fork and create suites; the only 4 failures are the ent14 IPv4-mapped SSRF cases that fail locally on py3.12 on every branch. |
AndriiPasternak31
left a comment
There was a problem hiding this comment.
The shape is right: explicit source_mode wins, ghosts and deployments stay pull-only, git_mode is set after the /ws broadcast, and the probe is only run once. One real blocker is left. The tier gate from the 09-25 ruling still treats "can push" as "owns the repo" for any creator who has their own token. Everything else is should-fix or smaller. #3021 builds on this default, so the fix here carries into it.
-
[blocking] A creator's own token still gets a working branch on a template they don't own.
src/backend/services/agent_service/crud.py:943-950
The ruling says being able to push is not owning the repo. The fix only drops theglobaltier. Aper_usertoken that passes the probe still gets the full trio. A classic PAT withreposcope, or an org-wide fine-grained token, can write every repo in the member's orgs. So a member who creates an agent from their org's shared public template gets atrinity/<agent>/<id>branch on that template. The heartbeat then pushes the agent's workspace there every 15 minutes, publicly. That is the ent#162 class that §11.17 and the flow table say is excluded ("a template someone else owns never receives an agent's branches").
The PR's own test pins it.test_ent705_agent_default_with_push_access_gets_the_trio(tests/unit/test_1484_create_agent_characterization.py:1319-1332) creates fromAbilityai/corneliuswith aper_usertoken and asserts a working branch. That is the same shared public upstream Cornelius is pinned away from incornelius_agent_service.py.
Fix: add an ownership signal next to the tier, and fall back to pull-only when it fails. The cheapest one is already fetched:_validate_github_accessgetsrepo_info.privatefromcheck_repo_exists, so a public repo would stay pull-only. The stronger check is repo owner == the token's login. A catalog template (gh_templateset) should never qualify. Then flip that test so it asserts pull-only for a shared public repo. -
[should-fix] Following the runbook moves a live agent onto
mainand auto-pushes there.docs/migrations/AGENT_WORKING_BRANCH_DEFAULT_2026-09.md:25-33- Step 2 flips the row to
source_mode = 0. - Step 3 recreates the container. On a recreate,
lifecycle.py:878clearsGIT_SOURCE_MODE.startup.sh:254keeps the branch already checked out on the volume, which is the default branch for a source-mode agent.GIT_WORKING_BRANCHis only read when cloning, so nothing checks out the working branch. - Step 4 turns auto-sync on. #3011's refusal (
agent_server/routers/git.py:771,:882) only fires whenGIT_SOURCE_MODE=true, so the heartbeat runsgit push origin HEADstraight tomain. The issue says this must never happen for repos that deploy on push. - Step 2 also has no supported path. No endpoint writes
source_modeon an existing agent, so an operator would be editing the DB by hand.
Fix: add a step that checks out the working branch in the workspace before auto-sync is enabled, and a check that fails closed if HEAD is still the default branch. Name the exact supported way to flip the row, or say this waits for ent#230.
- Step 2 flips the row to
-
[should-fix] Two create paths can't say "deployment", so they silently get the agent default.
- System manifests:
SystemAgentConfig(src/backend/models.py:1234) has nokind.system_service.py:1921buildsAgentConfigwithoutsource_mode, so every manifest agent from a shared template takes the new default with no way to opt out. It also runs one push probe per agent, one after another. - CLI:
trinity deploy --repo(src/cli/trinity_cli/commands/deploy.py:232) is by name a deployment of a codebase, but it now creates anagent.
Fix: addkindtoSystemAgentConfigand pass it through, and sendkind: "deployment"fromtrinity deploy. If that's out of scope, list both in §11.17 as known gaps.
- System manifests:
-
[nit] The
per_agenttier can't happen at create._resolve_github_repo_and_patcallsresolve_github_pat(owner_id=...)withoutagent_name(crud.py:704,:722), so the tier is neverper_agenton this path.test_ent705_an_agents_own_token_earns_it_like_the_creatorscalls the helper directly with a tier production can't produce. Either drop "or the agent's own" from the docs and theAgentConfig.kindcomment, or say it's there for future callers. -
[nit] Stale status line.
docs/memory/requirements/github.md:820still says "Merges only after #3016, #3017 and #3018". Those have landed.
What I checked
- Scope: the PR's own diff only (
origin/dev...8b20c171, 11 files), not the merged-in dev commits. - Auth: there's no new endpoint or gate. An agent-scoped key creating a child resolves to the owner's
per_usertoken, same as before. That could already ask forsource_mode=falseover REST, so it's not a new grant. - Data and ordering: no schema change and no new trigger, so Invariant #18 doesn't apply.
git_modeis set after the broadcast (#918). The MCPkindfield matches the backend. - Tests: 12 related suites (1484, fork_to_own, ent14, models_centralized, 2107, 3010, 3011, ent123, 2069, 69 ephemeral, ent109 seam and extraction) pass with seeds 12345 and 99999: 362/362 each run. MCP:
tsc --noEmitclean,npm test550/550.gh pr checksis green.
…eport as forks (trinity-enterprise#705) A creator's own token that can push is still not ownership: a classic `repo` PAT or an org-wide fine-grained token can write an org's shared public template, and the agent default then pushed the workspace to a branch on it every cycle (the ent#162 class). The default now also requires a non-catalog template whose owner equals the token's GitHub login (GET /user, case-insensitive); anything else stays pull-only and is never probed. Fork-to-own is checked before the explicit-source_mode branch: _apply_fork_to_own assigns config.source_mode, which Pydantic v2 records in model_fields_set, so every fork was reported as "source_mode set explicitly". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oyment (trinity-enterprise#705) The live-agent runbook flipped the binding with no supported endpoint, and a recreate keeps the checked-out default branch, so enabling auto-sync pushed straight to main. The runbook now waits for ent#230, checks out the working branch explicitly and fails closed while HEAD is the default branch. `trinity deploy --repo` sends kind "deployment" so a codebase deploy never takes the agent default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of 09-28 addressed in 1. [blocking] Pushable ≠ owned.
Fork reason (from the #3022 review). 2. [should-fix] Runbook.
3. [should-fix] Create paths that can't say "deployment".
4. [nit] 5. [nit] Stale status line. Fixed. Verification
🤖 Generated with Claude Code |
…#705) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efaults # Conflicts: # tests/registry.json
obasilakis
left a comment
There was a problem hiding this comment.
Approving.
Checked locally on c370041a (py3.13, seed 12345): the PR's suites pass 145/145. Mutating each of the tier gate, catalog-template gate, owner==login gate and the freeze-on write turns at least one test red, so the new tests execute the decision path. CI is green, merge with dev is clean.
Two things to be aware of, neither blocking:
- Org-owned repos (private included) never get the working-branch default; only repos owned by the token's own login do. Worth an explicit product call from vybe, since most team repos are org-owned.
- Freeze-on-sync-failure is now written for every auto-pushing create whose
kindis unset, which includes existing callers that sendsource_mode=falseexplicitly, and ghosts.
After merge, ent#705 needs status-in-dev set by hand (cross-tracker ref). #3021 and #3022 merge after this.
Resolve tests/registry.json: dev's file plus this branch's entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rise#703) (#3021) * feat(git-sync): the container pulls origin on its own (trinity-enterprise#703) Invariant G3: human and fleet work reaches the agent within a bound. Nothing inside the container ever pulled; the 2026-09-24 audit found agents up to 31 commits behind. - agent server: a pull loop beside the push loop, gated per cycle on the owner's pull_sync_enabled (read live, GIT_SYNC_PULL fallback), interval GIT_SYNC_PULL_INTERVAL_SECONDS (defaults to the push one). Under _REPO_LOCK, never while an execution runs (checked again right before the tree is touched). Fast-forward, or rebase aborted on conflict. Uncommitted edits are stashed explicitly and a pull that collides with them is undone - not --autostash, which strands them in the stash while reporting success. - pull_sync_enabled + last_pull_at/_status/behind_after_pull on both migration tracks; backfill on only where auto-sync is on; new github agents get it at creation (source mode included), GIT_SYNC_PULL env derived from the DB flag alone at recreate. - GET/PUT /api/agents/{name}/git/pull-sync; sync-health persists the pull outcome (bounded); Settings -> Git sync gains the toggle. Stacked on #3020 (+#3015-#3018): merges after it. Related to Abilityai/trinity-enterprise#703 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(schema): keep the FK strip working on agent_git_config for PostgreSQL (trinity-enterprise#703) The inline comment on `pull_sync_enabled` sat between the comma and the FOREIGN KEY clause. `_PG_TABLE_SUBS` strips that clause only when it directly follows the comma, so the clause survived without its REFERENCES and PostgreSQL rejected the table ("syntax error at or near ')'"), failing 18 requires_postgres tests in schema-parity. The comment moves above the column. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(git-sync): pull cycle never strands local work, counts queued turns, brings main to working branches (trinity-enterprise#703) PR #3021 review: - _with_stash wraps every post-stash step: a git child that times out (run_registered raises TimeoutExpired) goes back to the pre-pull HEAD and re-applies the stash; when that is impossible the error says the edits are kept in `git stash`. - reset --hard's return code is checked. A failed undo stops with an error naming it instead of leaving UU files behind silently. - Both cycles refuse to run over unmerged paths. The push cycle checks before `git add -A`, which would stage conflict markers and push them to origin as a successful sync. - The execution gate counts register_pending entries (#2433), so a turn queued on the chat lock or the headless pool is busy, not idle. - The pull reaps stale lock litter first, as the push cycle does; a pull-only agent has no push cycle to do it. - sync-state gains last_pull_error streak fields: consecutive pull failures / skips and last_successful_pull_at. Ruling on the intent question: a trinity/* working branch only ever pulled itself, so human pushes to main never arrived (G3). The cycle now also merges origin/main (via _get_pull_branch) into the working branch — a merge, not a rebase, since the branch is already pushed; a conflict is aborted and recorded. Nine real-repo tests, all red before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(git-sync): persist pull health; renumber to 0080_pull_sync; docs + UI copy (trinity-enterprise#703) - Alembic 0076_pull_sync -> 0080_pull_sync <- 0079_telegram_group_context (one head, 81 revisions); SQLite entry re-appended after dev's. - The same migration pair adds agent_sync_state.last_pull_error, last_successful_pull_at, consecutive_pull_failures and consecutive_pull_skips, persisted by the sync-health poller (bounded, coerced; a success clears the error). ent#706/#707 read these, so they land here rather than as a second migration on the same table. - agent-runtime.md says three loops; the flow and requirement describe the main merge, the queued-turn gate, the unmerged-path refusal, what behind_after_pull measures, and that auto-sync agents already rebase in the push cycle. - Settings copy no longer hard-codes 15 minutes (GIT_SYNC_PULL_INTERVAL_SECONDS) and no longer claims the push cycle never touches the tree while the agent works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(git-sync): pull records merge conflicts as conflicts; rebases keep merges (trinity-enterprise#703) PR #3021 re-review: - A conflict merging main was recorded as "Auto-merging <file>": git prints it on stdout with an empty stderr. Unmerged paths are read before the abort; the error is "diverged: merge conflict with main (<files>)", and a failed merge --abort is named. - `git rebase` flattened the merge the pull made into copies of main's commits; both cycles now rebase with --rebase-merges. - A failed strict ahead/behind count fails the pull instead of reading as up to date. - A timed-out step is reset to the pre-pull HEAD even with nothing stashed; a reset blocked by the killed child's index.lock says so. - "Never runs while the agent is working" is check-then-act: the copy and docs now say "never starts" and document the admission window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(git-sync): pull-sync write is person-only; classify the agent's read #2996's route census (merged to dev) requires every new route to be classified. The agent's pull loop reads GET .../git/pull-sync with its own key each cycle (AGENT_CALLABLE); the PUT is a setting write, so it takes Depends(require_person) like the other #2996 settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(git-sync): the push and pull cycles share the repo lock without starving each other (#3021 review) The two background loops started together on one interval and took _REPO_LOCK non-blocking, so the loser skipped silently every tick and the effective pull bound could quietly double. Background cycles now wait up to GIT_SYNC_LOCK_WAIT_SECONDS (default 120 s) for the lock, and the pull loop's first tick is half an interval after the push loop's. Operator endpoints keep their immediate 409. The pull's reset --hard undo re-checks that no execution started meanwhile; if one did, the tree is left as it is (conflict markers block the next push and pull) instead of discarding the turn's writes. merge --abort runs only when MERGE_HEAD exists, so a merge that refused to start no longer records "There is no merge to abort". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * merge-train: narrow the "never discards local work" claims to what _safe_to_reset guarantees (#3021) — mechanical, per the merge-train note on the PR The undo paths no longer reset over a registered execution, but writes outside the process registry (Files API, docker exec, web terminal) made during the integrate window are not protected, as the author's e6cb8b5 note concedes. Wording only: two git.py docstrings, the auto_sync.py comment, requirements/github.md and the GitSyncSettingsPanel.vue copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Andrii Pasternak <44377756+AndriiPasternak31@users.noreply.github.com> Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.com>
Summary
Operator ruling 2026-09-24: the repository is the agent; the container is a cache of it.
kind:agent(the default) ordeployment, onPOST /api/agentsand MCPcreate_agent. This absorbs the API half of ent#704; its UI half stays there. An explicitsource_modealways wins, so every existing caller keeps its behaviour. No UI, MCP or CLI caller sent one, which is why the default matters.auto_sync_enabled=1andfreeze_schedules_if_sync_failing=1. It only gets them when the bug: git-sync write access is never validated — a read-only credential fails silently forever (ls-remoteand the RESTpermissions.pushfield both lie) #2107 push probe says its token can push to that repo.git_mode(set after the/wsbroadcast, never on it; feat: agent-reported structured reports via MCP + dashboard display #918). The cases:kind=deploymentfor exactly that reason.sync_failingitem)Changes
src/backend/models.py:AgentConfig.kind,AgentStatus.git_modesrc/backend/services/agent_service/crud.py:_apply_agent_kind_default, called before validation and branch reservation_validate_github_access(push_verified=), which skips the duplicate probesrc/backend/services/cornelius_agent_service.py: pinned pull-onlysrc/mcp-server/src/tools/agents.ts:create_agentgainskindrequirements/github.md§11.17feature-flows/github-sync.md("Which mode a new agent gets")architecture/agent-lifecycle.mddocs/migrations/AGENT_WORKING_BRANCH_DEFAULT_2026-09.md: migrate a live agent (get the disk into git first, switch, recreate, verify), plus the exclusions stated genericallytest_1484_create_agent_characterization.py, through the fullcreate_agent_internal):source_mode→ never probedls-remoteand the RESTpermissions.pushfield both lie) #2107's 400 still standskind="deployment"explicitly, with their assertions unchanged.Test Plan
148458,fork_to_own40,ent12353,206935,69ephemeral 42,1759,2107,3010,3011,ent109seam, models-centralized, gitignore doc sync.lint_sys_modulesand root placement clean.crud.pyand the Cornelius pin reverted, 8 of 9 new tests fail. The one that stays green is the bug: git-sync write access is never validated — a read-only credential fails silently forever (ls-remoteand the RESTpermissions.pushfield both lie) #2107 explicit-request 400, which is preserved on purpose.tsc --noEmitcleandevRelated to abilityai/trinity-enterprise#705 (cross-repo; close at release)
🤖 Generated with Claude Code