Skip to content

fix(agents): an own non-catalog repo is no longer treated as a shared catalog template (#3545) - #3647

Merged
obasilakis merged 2 commits into
devfrom
feature/3545-own-repo-pull-only
Oct 13, 2026
Merged

obasilakis merged 2 commits into
devfrom
feature/3545-own-repo-pull-only

Conversation

@obasilakis

Copy link
Copy Markdown
Contributor

Summary

  • Creating an agent from your own repository with your own GitHub token returned pull-only with "is a shared catalog template". The create path passed catalog_template=bool(gh_template), and get_github_template() builds a template for every github: 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 bare owner/repo.
  • Matching ignores case and a trailing .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: real template_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

  • Admin and registry override matching in GET /api/templates/{id} is now case- and .git-insensitive.
  • A registry that cannot be read lists nothing, so its templates read as not-catalog. A working branch still requires the token's own account to own the repository and a passing push probe.

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)
  • Mutation: passing template_lookup to the predicate turns test_3545_catalog_membership_is_asked_about_the_bare_repo red
  • Full unit suite: CI
  • Manual: POST /api/agents with github:<you>/<private-repo> and a personal token returns git_mode.pushes: true

Fixes #3545

🤖 Generated with Claude Code

obasilakis and others added 2 commits October 13, 2026 11:37
… 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 dolho 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.

/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 repo PAT (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)

  1. A respelled id resolves to the canonical override (confidence 6/10): matching ignores case and a trailing .git, so GET /api/templates/github:Abilityai/Cornelius.git now 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.
  2. 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.
  3. 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

@obasilakis
obasilakis merged commit 3cc2bb7 into dev Oct 13, 2026
25 checks passed
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.

2 participants