Skip to content

feat(git-sync): an agent created as an agent owns its repository (trinity-enterprise#705) - #3020

Merged
dolho merged 22 commits into
devfrom
feature/ent705-agent-defaults
Sep 29, 2026
Merged

dolho merged 22 commits into
devfrom
feature/ent705-agent-defaults

Conversation

@dolho

@dolho dolho commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Merge order: after #3016, #3017 and #3018 (and #3015). ent#705 says this default must not ship before those blockers land. The branch is stacked on them, so until they merge the diff against dev includes their commits; this PR's own changes are the last two commits (5369fa1f, 5a32230a). After they land: rebase onto dev, then merge.

Summary

Operator ruling 2026-09-24: the repository is the agent; the container is a cache of it.

  • Create-time kind: agent (the default) or deployment, on POST /api/agents and MCP create_agent. This absorbs the API half of ent#704; its UI half stays there. An explicit source_mode always wins, so every existing caller keeps its behaviour. No UI, MCP or CLI caller sent one, which is why the default matters.
  • An agent gets a working branch it alone writes, auto_sync_enabled=1 and freeze_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-remote and the REST permissions.push field both lie) #2107 push probe says its token can push to that repo.
  • Everything else stays pull-only, with the reason on the create response's git_mode (set after the /ws broadcast, never on it; feat: agent-reported structured reports via MCP + dashboard display #918). The cases:
    • a deployment
    • an ephemeral ghost (ent#69)
    • no token (was admitted before, stays admitted, not ent#123's 400)
    • a refused probe (the reason points to fork-to-own)
    • an unverifiable probe
  • Why the probe gate is load-bearing: without it, a create from a template someone else owns (a shared public upstream above all) with a platform token that happens to have write there would push the agent's branches into that repo. That's the ent#162 class.
  • Cornelius is pinned kind=deployment for exactly that reason.
  • Fork-to-own gets the trio; it owns its fork.
  • Not changed:
    • existing agents: the default applies to new creates, and the per-agent migration is an explicit runbook
    • freeze paging (the sync_failing item)
    • the pull cycle: on when ent#703 lands
    • divergence-age freeze semantics: ent#706
  • Open-core by operator ruling (recorded on ent#705).

Changes

Test Plan

Related to abilityai/trinity-enterprise#705 (cross-repo; close at release)

🤖 Generated with Claude Code

dolho and others added 12 commits September 25, 2026 11:06
…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>
…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>
@dolho

dolho commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

/review Report

Scope reviewed: the ent#705 commits (5369fa1f, review fixes in 5a32230a). The stacked commits of #3016, #3017, #3018 and #3015 were reviewed on their own PRs.
Scope check: CLEAN. The changes are the create-time kind, the default resolver, freeze-on at creation, the Cornelius pin, MCP kind, and docs plus the runbook. Nothing unrelated.

Plan completion (ent#705 AC):

AC Status
Agent default → working branch + auto_sync_enabled=1 + freeze on; deployment → source, no push, freeze off DONE: crud.py::_apply_agent_kind_default and freeze-on in _materialize_agent_files. Changed: granted only when the push probe says ok (see the PR description)
Pull cycle on NOT DONE, by the operator's scope ruling: ent#703
Fork-to-own gets the trio DONE
Existing fleet not flipped DONE: the default applies at create only; kind is not persisted
Migration runbook for a live agent DONE: docs/migrations/AGENT_WORKING_BRANCH_DEFAULT_2026-09.md
Exclusions recorded and enforced DONE in the runbook, stated generically (public repo); enforcement is #3016's shared-branch refusal
Freeze paging unchanged; freeze keys on divergence age Paging unchanged. Divergence age is ent#706; the current trigger is now stated in the requirement
Wizards / template.yaml gain nothing DONE: nothing added

Execution coverage

changed symbol executed by live consumer verdict
_apply_agent_kind_default 11 test_ent705_* through the full create_agent_internal _resolve_template → every github: create (routers/agents.py, MCP create_agent) ✅ executed
push_verified …gets_the_trio (probe awaited once) _validate_github_access ✅ executed
freeze-on write …gets_the_trio, …deployment…never_probed, …without_push_access… scheduler freeze check (routers/internal.py) ✅ executed
AgentStatus.git_mode asserted on the create result create response (REST + MCP) ✅ executed
Cornelius pin AST guard over its AgentConfig the startup seed 🛡 guard (the behaviour it selects, explicit source_mode, is executed)
MCP kind tsc --noEmit backend AgentConfig.kind ⚠️ typecheck only

Mutation: with crud.py and the Cornelius pin reverted, 8 of 9 new tests fail. The survivor is the #2107 explicit-request 400, which is preserved on purpose.

Critical Findings

None.

Informational Findings (fixed in 5a32230a)

  • [I1] Doc overclaim (8/10): "an explicit source_mode wins, every caller unchanged". The mode is unchanged, but an explicit working branch and fork-to-own now also get freeze-on. That's intended; the wording now says so.
  • [I2] Edge inconsistency (7/10): a fork asked for as kind="deployment" was reported as kind: agent while the freeze write read deployment. It's now reported as asked, pinned by a new test.
  • [I3] A blank line between models was dropped; restored.
  • [I4] Freeze trigger before ent#706 (8/10): freeze-on means schedules pause after 3 consecutive failed syncs (~45 min), including a rebase conflict. That's consistent with ruling 2 ("not only after three failed pushes"), and it's now stated in the requirement.

Noted, not changed

  • Every default github: create with a token now makes one extra receive-pack round trip (the probe; 20 s timeout at worst). There's no second probe, because validation reuses the result.
  • The MCP kind is covered by a typecheck, not a test; it's a straight pass-through.

Clean categories

  • SQL: no new queries.
  • Auth: kind can only lower capability. agent requires a verified push, and ghosts are excluded.
  • Credential exposure: the git_mode reason names the repo, never the token or the probe's stderr. It's set after the /ws broadcast, which only sends name, status and port anyway.
  • Enterprise-docs guard: clean.

Summary

🤖 Generated with Claude Code

@dolho
dolho marked this pull request as ready for review September 25, 2026 12:28
…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>
@vybe

vybe commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

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: _apply_agent_kind_default in crud.py (the outcome == "ok" branch) grants a working branch plus auto_sync_enabled=1 whenever the push probe succeeds. Being able to push is not owning the repo. The token comes from resolve_github_pat, which can return the global platform PAT for any creator (the tier is never checked). So on an install whose platform PAT can write to a shared template repo, every UI/MCP/CLI/manifest create would auto-push that agent's workspace to a branch on that repo. That is the ent#162 class the docstring says is excluded. test_ent705_agent_default_with_push_access_gets_the_trio asserts exactly this for a shared template + platform PAT.

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 source_mode wins, ghosts and deployments stay pull-only, git_mode is attached after the broadcast, and 131 own tests plus 786 related tests pass.

Once #3016–#3018 land, rebase this onto dev.

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

✅ Nightly unit-suite clean when this PR is merged into dev, all 3 seeds (head_sha: c370041a71a11948be1be9c7979a7dbae6c46f56).

@github-actions

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

dolho and others added 3 commits September 28, 2026 10:42
…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>
dolho added a commit that referenced this pull request Sep 28, 2026
…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>
@dolho

dolho commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

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. tsc clean. CI green.

#3021 and #3022 are re-merged on top of this.

@AndriiPasternak31 AndriiPasternak31 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. [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 the global tier. A per_user token that passes the probe still gets the full trio. A classic PAT with repo scope, 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 a trinity/<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 from Abilityai/cornelius with a per_user token and asserts a working branch. That is the same shared public upstream Cornelius is pinned away from in cornelius_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_access gets repo_info.private from check_repo_exists, so a public repo would stay pull-only. The stronger check is repo owner == the token's login. A catalog template (gh_template set) should never qualify. Then flip that test so it asserts pull-only for a shared public repo.

  2. [should-fix] Following the runbook moves a live agent onto main and 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:878 clears GIT_SOURCE_MODE. startup.sh:254 keeps the branch already checked out on the volume, which is the default branch for a source-mode agent. GIT_WORKING_BRANCH is 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 when GIT_SOURCE_MODE=true, so the heartbeat runs git push origin HEAD straight to main. The issue says this must never happen for repos that deploy on push.
    • Step 2 also has no supported path. No endpoint writes source_mode on 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.
  3. [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 no kind. system_service.py:1921 builds AgentConfig without source_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 an agent.
      Fix: add kind to SystemAgentConfig and pass it through, and send kind: "deployment" from trinity deploy. If that's out of scope, list both in §11.17 as known gaps.
  4. [nit] The per_agent tier can't happen at create. _resolve_github_repo_and_pat calls resolve_github_pat(owner_id=...) without agent_name (crud.py:704, :722), so the tier is never per_agent on this path. test_ent705_an_agents_own_token_earns_it_like_the_creators calls the helper directly with a tier production can't produce. Either drop "or the agent's own" from the docs and the AgentConfig.kind comment, or say it's there for future callers.

  5. [nit] Stale status line. docs/memory/requirements/github.md:820 still 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_user token, same as before. That could already ask for source_mode=false over 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_mode is set after the broadcast (#918). The MCP kind field 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 --noEmit clean, npm test 550/550. gh pr checks is green.

dolho and others added 2 commits September 29, 2026 10:09
…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>
@dolho

dolho commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

Review of 09-28 addressed in d8a9fd0f8 and 325be3ae8.

1. [blocking] Pushable ≠ owned. d8a9fd0f8. I went with the stronger signal: repo owner == the token's GitHub login.

  • The login costs one GET /user (GitHubService.validate_token_detailed, the same call Settings uses to validate a personal token). The check runs only after the tier gate passes, so it adds one call on the per_user path.
  • A catalog template (gh_template set) never qualifies and is never probed.
  • If the login can't be read (invalid, unreachable, empty), the agent is pull-only: "could not confirm the repository is yours".
  • If the owner isn't the login, the agent is pull-only, and the reason names the owner and the login and points to fork-to-own.
  • Tradeoff to flag: an org-owned repo stays pull-only by default, private ones included. An explicit source_mode=false over REST or MCP still asks for a working branch, and bug: git-sync write access is never validated — a read-only credential fails silently forever (ls-remote and the REST permissions.push field both lie) #2107's 400 still guards that path. If you'd rather also admit private org repos, it's a one-line or repo_info.private. I left it out because a private org repo is still a repo someone else owns.
  • Tests: …with_push_access_gets_the_trio is replaced.
    • …a_shared_catalog_template_stays_pull_only_even_when_pushable: Abilityai/cornelius with a per_user token and probe=ok stays pull-only and is never probed.
    • …a_repo_someone_else_owns_stays_pull_only_even_when_pushable: the same repo named directly.
    • Positive case: …on_the_creators_own_repo_gets_the_trio (alice/brain, login alice).
    • Also added: a case-insensitive login match and three unknown-login cases.
    • The 5 new negative cases fail on 8b20c1714.

Fork reason (from the #3022 review). d8a9fd0f8. The fork branch now runs before the explicit-source_mode branch, because _apply_fork_to_own's assignment lands in model_fields_set. test_ent705_a_fork_is_reported_as_a_fork_after_fork_to_own_ran assigns source_mode the way the fork does, asserts the trap, and then asserts the fork reason. It fails before the fix.

2. [should-fix] Runbook. 325be3ae8.

3. [should-fix] Create paths that can't say "deployment".

  • CLI: 325be3ae8. trinity deploy --repo sends kind: "deployment", guarded by an AST test, tests/unit/test_ent705_cli_deploy_kind.py. It's static because the unit job doesn't install click/rich.
  • Manifests: the per-agent kind is feat(git-sync): ask "what is this repository?" at create (trinity-enterprise#704) #3022's manifest passthrough. Until that lands, the ownership rule already keeps manifest agents off shared templates. §11.17 now says both.

4. [nit] per_agent. The docs, the helper docstring and the test now say it's accepted for future callers; no create path resolves it today.

5. [nit] Stale status line. Fixed.

Verification

  • The 1484, fork_to_own, ent14, models_centralized, 2107, 3010, 3011, ent123, 2069, 69-ephemeral, ent109, ent162 and new CLI suites: 791 passed on seeds 12345 and 99999.
  • This excludes test_ent14_registry_url_ssrf.py, whose 4 IPv4-mapped cases fail locally on py3.12 on the unmodified branch too.
  • MCP tsc --noEmit is clean. Merging with dev is clean (merge-tree).

🤖 Generated with Claude Code

dolho and others added 2 commits September 29, 2026 10:34
…#705)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@obasilakis obasilakis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 kind is unset, which includes existing callers that send source_mode=false explicitly, and ghosts.

After merge, ent#705 needs status-in-dev set by hand (cross-tracker ref). #3021 and #3022 merge after this.

@obasilakis
obasilakis dismissed AndriiPasternak31’s stale review September 29, 2026 12:09

All five points from this review are addressed in d8a9fd0 and 325be3a (owner == token login gate, runbook waits for ent#230, CLI sends kind=deployment, per_agent documented, stale line fixed). Re-verified and approved; dismissing to unblock merge.

dolho and others added 2 commits September 29, 2026 16:09
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>
@dolho
dolho merged commit 7408498 into dev Sep 29, 2026
21 checks passed
vybe pushed a commit that referenced this pull request Oct 2, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants