Repository navigation
fix(agents): an own non-catalog repo is no longer treated as a shared catalog template (#3545) - #3647
Conversation
… catalog template (#3545) Agent creation decided the git mode with `catalog_template=bool(gh_template)`. `get_github_template()` builds a template for every `github:` id, listed or unlisted, so every create with a personal GitHub token returned pull-only with "is a shared catalog template" and the ownership and push checks never ran. `template_service.is_catalog_github_template(repo)` answers catalog membership on the ladder `get_github_template()` already walks: the admin list, else the registry, plus the bundled defaults. Matching ignores case and a trailing `.git`, as GitHub does. The create path passes the bare `owner/repo`. An agent created from the creator's own repository with their own token now gets a working branch, auto-sync and pause-schedules-on-sync-failure. A catalog template stays pull-only. Tests run the real `template_service` for an unlisted repo and for each rung of the ladder. The create-path stubs in test_1484 now return what production returns for an unlisted repo. Fixes #3545 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion can produce (#3545) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dolho
left a comment
There was a problem hiding this comment.
/review on 1c58ecfda: approve
Scope: clean. The one-line root cause (catalog_template=bool(gh_template), which is truthy for every github: repo, because get_github_template() builds a dynamic template for unlisted repos) is replaced by a real membership question, is_catalog_github_template(repo_path). get_github_template() now reads the same _catalog_entry ladder, so list, detail and create can't disagree about membership. That's the ent#14 "one ladder" rule, kept.
Safety: what "not catalog" lets through
I read _apply_agent_kind_default on the branch. Answering "not catalog" only gets past the catalog rung. A working branch still needs all of the following:
- the creator's own token (
per_user; the global PAT stays pull-only); - the token's own GitHub login equal to the repo owner, so an org-owned shared template stays pull-only even for a member with a classic
repoPAT (the ent#162 / PR #3020 class); - a passing push probe.
So the fail-open direction stated in the body (an unreadable registry lists nothing, so its templates read as not-catalog) can only grant push to a repo that a person's own account owns and can push to. That's the case the issue asks to allow. Acceptable.
Execution coverage
| Changed | Executed by | Live consumer | Verdict |
|---|---|---|---|
is_catalog_github_template / _catalog_entry |
test_3545_own_repo_not_catalog.py (real template_service, every rung + unlisted) |
crud._resolve_template → _apply_agent_kind_default |
✅ |
get_github_template on the shared ladder |
test_ent14_catalog_failopen.py, test_1931_empty_github_defaults.py |
GET /api/templates/{id}, create |
✅ |
| crud call site | test_1484_create_agent_characterization.py (stubs now return what production returns for an unlisted repo, plus the bare-repo argument pin) |
POST /api/agents |
✅ |
My mutation: I restored catalog_template=bool(gh_template) and 10 tests went red, including test_ent704_git_mode_pushes_for_an_agent_on_its_own_repo and the ent#705 pull-only cases. They pass with the fix: 152 passed locally on the four suites. The stub-default fix in test_1484 is the real catch here, and it's worth its learnings fragment: the old stubs returned a value the real function never returns, which is how this shipped green.
Informational (non-blocking)
- A respelled id resolves to the canonical override (confidence 6/10): matching ignores case and a trailing
.git, soGET /api/templates/github:Abilityai/Cornelius.gitnow returns the catalog entry's display data. That's intended, and stated in the body. The template id keeps the caller's spelling, so two ids can show the same card. Cosmetic. - Registry outage also changes the create-time reason, not just the mode (confidence 5/10, low): with the registry unreadable, a registry-only template owned by a person's account (not an org) reads as own-repo, so the creator gets a working branch where they'd otherwise get "shared catalog template". It's bounded by the owner and probe checks above. A registry read failure could be told apart from "not listed" if this ever matters.
- CI: the
pytest (head, seed 12345)lane is still running at the time of review. Everything else is green. Approving on the targeted suites and the mutation; merge once that lane lands.
The manual box in the test plan (POST /api/agents with your own private repo + personal token → git_mode.pushes: true) is still unticked. The unit coverage pins the decision, but a live run against a real GitHub repo is the only check of the probe and login path end to end.
🤖 Generated with Claude Code
Summary
catalog_template=bool(gh_template), andget_github_template()builds a template for everygithub:id.template_service.is_catalog_github_template(repo)now answers catalog membership: the admin list, else the registry, plus the bundled defaults. The create path calls it with the bareowner/repo..git, so a respelled catalog repository is still the catalog repository.Changes
src/backend/services/template_service.py:_catalog_entry(),is_catalog_github_template();get_github_template()reads the same ladder.src/backend/services/agent_service/crud.py:catalog_template=is_catalog_github_template(repo_path).tests/unit/test_3545_own_repo_not_catalog.py: realtemplate_service, unlisted repo and each rung of the ladder.tests/unit/test_1484_create_agent_characterization.py: create-path stubs return what production returns for an unlisted repo; one test pins the argument passed to the predicate.docs/security-reports/cso-diff-2026-10-13-3545-own-repo-not-catalog.*: diff audit, 0 findings at the 8/10 gate.Behaviour notes
GET /api/templates/{id}is now case- and.git-insensitive.Test Plan
cd tests && pytest unit/test_3545_own_repo_not_catalog.py unit/test_1484_create_agent_characterization.py unit/test_1931_empty_github_defaults.py unit/test_ent14_catalog_failopen.py -v(152 passed)template_lookupto the predicate turnstest_3545_catalog_membership_is_asked_about_the_bare_reporedPOST /api/agentswithgithub:<you>/<private-repo>and a personal token returnsgit_mode.pushes: trueFixes #3545
🤖 Generated with Claude Code