Agent Hands: First-Party Forge Tools¶
Agent hands are the first-party, connection-gated tools an agent uses to land its own work on a code forge: read a repo, open and comment on issues and pull requests, review with inline diff-anchored comments, drive CI, and push a branch or file so a pull request can be opened. They are built on the native connection catalog and the forge agent-client registry, not a third-party MCP server, so credential brokering, the approval gate, and the egress host-pin are the ones already in place.
Tool surface¶
All tools live under synthorg/tools/forge/, subclass the shared
_BaseForgeTool (itself a GovernedConnectionTool), and dispatch on an
action field. Egress is pinned to the bound connection's host by
construction, so an agent can never redirect a call to another host.
| Tool | Actions | Writes |
|---|---|---|
forge_repo |
get_repo, read_file, list_dir |
none (read-only) |
forge_issue |
get, list, open, comment |
open, comment |
forge_pull_request |
get, list, open, comment, review, merge |
all but get/list |
forge_push |
create_branch, write_file |
all |
forge_ci |
list_runs, get_run, trigger, rerun |
trigger, rerun |
Agent-authored titles, bodies, and commit messages pass through the
output-style policy (_guard_forge_text) before any write reaches the
forge, so a hard-rule violation (the em-dash ban) is rejected or rewritten
at the boundary.
Branch and file on-ramp (forge_push)¶
forge_push is the on-ramp for an agent landing its own work through the
forge API: create_branch opens a feature branch, write_file commits
file contents to it, and forge_pull_request open then raises the PR.
forge_push binds its own ActionType.VCS_PUSH (never the shared
comms:external), so the existing GitAccess sub-constraint ladder and
the DEFAULT_RISK_MAP HIGH tier govern it, and an autonomy grant written
for chat can never auto-approve a push. A force-push pattern in the commit
message is auto-escalated by the security rules engine's
DestructiveOpDetector.
Inline review comments and CI¶
A forge_pull_request review carries optional comments, each anchored to
a path / line / side on the diff; the forge attaches them to the
review. forge_ci trigger / rerun mutate CI state and route through the
approval gate; a forge without a CI API (Gitea/Forgejo) fails loud with
ForgeUnsupportedError rather than faking a result.
Forge clients¶
The forge_agent_api_client registry (engine/workspace/git_backend/
forge_api/) is keyed by ConnectionType:
- GitHub and the Gitea family (Gitea + Forgejo) share the
GitHub-compatible read surface on
ForgeAgentBase; each supplies its own write divergences (review event vocabulary, one-shot vs resolve-then-ref branch creation, label-by-id resolution). - GitLab implements the
ForgeAgentApiClientprotocol directly on the GitLab transport: its v4 API addresses repositories as a URL-encodednamespace/projectpath, keys issues and merge requests by a per-projectiid, models a review as an approval plus diff-anchored discussions, and runs CI as pipelines. Its files API returns no commitsha, so the client resolves the branch head after a write.
A forge without a wired agent client surfaces a typed
ForgeUnsupportedError at the tool boundary.
Repository scoping (fail-closed, least-privilege)¶
An org-wide token can reach many repositories; agent hands must act only on
the ones an operator selected. Scope is expressed as the connection's
allowed_repos (owner/repo entries, owner/* globs permitted) and
enforced once in _BaseForgeTool._resolve_connection, which rejects any
owner/repo outside the scope with ForgeRepoScopeError before the
approval gate (an out-of-scope call is refused outright, never parked).
The scope is fail-closed: an empty allowed_repos denies every
repository. An operator populates it through a discovery flow rather than
guessing globs:
GET /connections/{name}/accessible-reposscans the repositories the connection's token can reach (list_accessible_reposon the client).- The operator selects the in-scope repositories in the dashboard connection editor.
- The selection persists on the
Connectionrecord (allowed_repos), round-tripped on SQLite and Postgres.
The check_forge_repo_scoped.py convention gate guards the enforcement:
_BaseForgeTool._resolve_connection in tools/forge/_base.py must
raise ForgeRepoScopeError from a reachable statement, and no forge tool may override
_resolve_connection without re-enforcing scope (opt out per-class with
# lint-allow: forge-repo-scoped -- <reason>). The base check is bound to
that path, so a class merely named _BaseForgeTool in another forge module
cannot stand in for the real enforcement site; elsewhere the name carries no
privilege and such a class is checked as an ordinary tool.
Governance summary¶
- Reads on a
sensitiveconnection and every write route through the identity-boundConnectionApprovalGate. forge_pushcarriesvcs:push; other forge writes carrycomms:external; each is risk-classified independently.- Credentials are brokered per call from the connection catalog, travel in
the
Authorizationheader only, and are never logged. - Repo scope is checked before the gate; out-of-scope calls are refused.