# Secure forge interactions

> Agents clone, ship pull requests, file issues and review code on GitHub, GitLab and Radicle, without ever holding a git key.

An agent in Ryph works on your repositories the way a developer does. It clones them, commits, opens pull requests, keeps a branch up to date with main, files issues, comments on threads and reviews pull requests, on GitHub, on GitLab and on Radicle. On GitLab, a pull request is a merge request.

It does all of that without a credential for your forge. Every request goes through Ryph's forge, which holds the keys and checks each one against what the agent was allowed to do.

| What | The run asks | The run holds | Ryph’s forge | Your forge |
| --- | --- | --- | --- | --- |
| Read code | git clone, git fetch | A read-only key for this run’s repos | Serves the clone from its own cache. The gateway only serves clones. | Fetched with Ryph’s credential |
| Ship work | submit_patch | Uploads a git bundle of the branch | Checks the bundle carries the run’s own branch on top of the base, then stores it. | Pushed and opened as a pull or merge request, after approval if the repo needs it |
| Issues and reviews | create_issue, add_comment, submit_review… | A token for Ryph’s tools | Checks the agent’s grant, its limits and the repo’s visibility, and signs the write. | Written as Ryph’s GitHub App, GitLab bot or Radicle node |

Everything an agent does with your repositories crosses into Ryph’s forge first. The forge holds the GitHub App’s key, the GitLab bot’s token or the Radicle node’s key, and checks each request against what the agent was granted before anything reaches your forge.

## The run holds no key

An agent that reads a hostile issue or web page can be talked into anything, so a push token in its environment is a risk. In Ryph, the run only gets these:

- **A read-only key for cloning.** It works for the repositories this run checks out, and only while the run is going. The git gateway behind it serves clones and nothing else, so there's no way to push with it.
- **A bundle upload for shipping.** When the agent ships a branch, the runner packs the new commits into a git bundle and uploads it. Ryph checks that the bundle carries the run's own branch and applies to the base, and stores it before it answers. From that moment the work is safe, even if the forge is down.
- **A token for Ryph's tools.** Issues, comments and reviews go through Ryph's tool gateway to the forge, which checks each call. On GitHub, it writes with a token made for that one call: one repository, one permission. The GitHub App's private key, the GitLab bot's token and the Radicle node's key never leave the forge. GitLab has no token that can be narrowed per call, so there the checks below are the fence, and the bot's Developer role keeps it off protected branches.

So an agent can't push to main, merge a pull request, delete a branch or change a file through the API, however it's prompted. Pushing to the forge is something only Ryph's forge does, after the repository's approval policy says yes.

Git inside the run is locked down as well. The repository's own git hooks never run, and git commands never see the run's credentials.

## Working with code

Inside its checkout, the agent uses plain git: branches, commits, `git log`, a rebase. A few tools from Ryph cover what leaves the checkout:

| Tool                      | What it does                                                                                                                      |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `submit_patch`            | Ships a branch as a patch, which becomes a pull request. The agent keeps working, and can submit again.                           |
| `update_from_base`        | Merges the latest base branch, usually main, into the branch. Conflicts are left for the agent to resolve before its next submit. |
| `checkout_change_request` | Moves the checkout onto an existing pull request, so the agent's commits go onto it.                                              |
| `patches`                 | Lists what the run has shipped and where each patch stands.                                                                       |
| `run_checks`              | Runs the repository's [checks](https://ryph.ai/docs/checks.md) on a commit.                                                                        |

### Patches

A patch is one repository's share of a run's work on its way to the forge. It exists the moment Ryph has the bundle, before anyone has approved anything. Then the repository's submission policy decides what happens. You set it on the repository's page under **Repos**, in **Submitting work**:

- **Submit automatically:** Ryph opens the pull request straight away.
- **Wait for approval:** the patch waits under **Patches** until someone opens it and clicks **Approve**, or **Reject**. A patch that failed to submit gets a **Retry** button there.

An agent only ships patches while **Let this agent open patches** is on, in the **Permissions** step of its settings.

If GitHub, GitLab or Radicle can't take it right then, Ryph retries with backoff, starting at 30 seconds and up to 30 minutes apart, eight times before it asks a person. A run that changed several repositories ships one patch for each, and their pull requests link to each other.

The agent hears what happens to every patch as a message: opened, approved, merged, closed or rejected, with the reason. It can follow up, wait or revise without checking back.

### Working on an existing pull request

Asked with `/ryph` on a pull request, or after `checkout_change_request`, the agent pushes its commits onto that pull request's branch. Ryph reads the pull request again right before it pushes: it has to still be open, on the same branch, and that branch can't be the repository's default. The push is fast-forward only, from the tip Ryph just read, so commits someone else pushed meanwhile are never overwritten.

### Generations

Once a pull request merges, its branch is done. The agent can keep working, and its next submit starts a new **generation**:

1. **The pull request merges**

   Ryph tells the agent, and moves the repository's base to where the pull request merged.

2. **The agent keeps committing**

   New commits go on the same local branch, as before.

3. **The next submit moves them onto the new base**

   Ryph rebases the new commits onto the merged base and ships them on a fresh branch, such as `…/work-2`. A conflict comes back with the files to resolve, and the agent resolves them and submits again.

4. **A fresh pull request opens**

   It holds only the new work, and reviewers never see the merged commits again.

Within a generation, every submit adds to the same pull request. If a merge left out some of the agent's commits, the agent is told, and they ship with its next submit.

### Lines

An agent that fixes two unrelated things in one repository can ship them as two pull requests. Each independent stream of work is a **line**: its own branch, its own generations and its own pull request.

The agent starts a line with plain git, from the base:

```sh
git switch -c fix-login origin/main
```

and ships it with `submit_patch`, naming the branch. Every branch of a run lives under the run's own namespace, such as `ryph/ada/r-7f3a/fix-login`, and the default line is `work`. A run that never starts another line ships exactly as before.

A run's branches in one repo. Every branch of the run sits under `ryph/ada/r-7f3a/`.

- **work** (The default line): `…/work`, PR #212 merged, then `…/work-2`, PR #219 open
- **fix-login** (git switch -c fix-login origin/main): `…/fix-login`, PR #213 open

Two lines in one repo, so two pull requests that can merge on their own. When PR #212 merged, the next submit on `work` moved the new commits onto the merged base and opened PR #219 with only the new work.

- Up to 8 lines per repository in one run.
- Each line starts from the base. A branch started from another submitted line is refused, so two pull requests never carry the same commits.
- When a line's pull request merges, only that line moves on to its next generation. The others carry on untouched.
- When the run ends, Ryph ships the default line and every line the agent submitted. If the agent left a local branch it never submitted, `finish` names it, so the agent can still ship it.
- In a task working on an existing pull request, the default line goes onto that pull request, and every other line opens its own against the same base.

## Working with issues and pull requests

With forge tools on, an agent reads and writes threads on your forge:

| Tool                     | What it does                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `get_issue`              | Reads an issue with all its comments.                                                                        |
| `list_issues`            | Lists open or closed issues, filtered by label.                                                              |
| `get_change_request`     | Reads a pull request with its comments, changed files, the lines a review can comment on, and its CI status. |
| `compare_change_request` | Compares two revisions of a pull request for a second review. Radicle only for now.                          |
| `list_change_requests`   | Lists open or closed pull requests.                                                                          |
| `create_issue`           | Opens an issue, with labels from the agent's allowed list.                                                   |
| `add_comment`            | Comments on an issue or a pull request, or replies to one review comment and can mark its thread resolved.   |
| `add_labels`             | Adds allowed labels to a thread.                                                                             |
| `submit_review`          | Posts one review of a pull request: a summary and up to 30 comments on specific lines.                       |
| `close_issue`            | Closes an issue as completed or not planned, with an optional note.                                          |

Agents can't merge, approve or close pull requests, edit or delete anything, assign people or create labels. Reviews only comment.

Text the read tools return was written by anyone with a forge account, so the agent gets it quoted and marked as material to read.

### What an agent may do

An admin sets this per agent. Go to **Agents**, open the agent, and go to the **Permissions** step. The settings are under **Issues and change requests**:

| Setting                         | What it controls                                                                                                                |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Repositories                    | **Checked out repos**, the ones the run works on, or **Chosen repositories**, connected ones the run may not check out.         |
| Read issues and change requests | Whether the agent gets the five read tools.                                                                                     |
| Open issues                     | Whether it may open issues, and how many per run.                                                                               |
| Comment                         | Where it may comment, **The run's own thread** and issues it opened, or **Any thread of the repositories above**. And how many. |
| Review change requests          | Where it may review, by the same rule, and how many reviews per run.                                                            |
| Close issues                    | Where it may close issues, by the same rule, and how many.                                                                      |
| Labels it may apply             | The labels it may use. They have to exist on the repository already.                                                            |

Click **Save changes** when you're done.

Every limit is a number, up to 50 per run. The form suggests 5 issues, 20 comments and 5 reviews. A run keeps the policy it started with, so editing an agent never changes a run that's already going.

### What Ryph checks on every call

1. **The run is still going**

   A finished run's grant is gone, and the tool says so.

2. **The request is in shape**

   Titles, bodies and arguments are checked against their limits.

3. **The grant allows it**

   The repository is in scope, the capability is on, the thread fits the rule and the labels are on the list.

4. **Nothing private goes public**

   A run that holds a private repository can't write to a public one, except on the thread it was asked from.

5. **The run is under its limit**

   Ryph reserves the write against the run's limit before anything is sent.

6. **The write goes out, signed**

   Ryph writes it as its own GitHub App, with a token for that one repository and permission, or as its GitLab bot or Radicle node, and records it on the run's page with a link.

When a check fails, the agent reads why, so it can correct the call.

### Every write is signed

Ryph signs everything an agent writes, review comments included, with a line at the top: the agent's name, a link to the run and who asked for it. The agent's text follows as written, and nothing in it can hide the signature. On GitLab, nothing an agent writes runs a quick action.

If the forge accepts a write but the answer gets lost on the way back, Ryph looks for its own signature before it reports a failure, so a retry doesn't post twice. Comments written by bots, Ryph's own included, never start a run, so an agent can't summon another one with a `/ryph` in its comment.

> [!WARNING]
> **A write capability lends the agent Ryph's hand**
>
> Within its grant, an agent that was talked into it can post what the grant allows. That's why every write capability has a limit, every write carries the signature and lands on the run's trail, and only admins can grant forge tools. `@mentions` in an agent's comment are posted as written.

## Turn on forge tools

Forge tools act as Ryph's GitHub App, its GitLab account or its Radicle node, so they come with one:

- The `ryph` binary and Docker Compose turn them on whenever GitHub, GitLab or a Radicle node is connected.
- The [Kubernetes](https://ryph.ai/docs/kubernetes.md) Helm chart does the same with `forgeTools: auto`, the default.
- [Networked Compose](https://ryph.ai/docs/networked-compose.md) turns them on once `RYPH_FORGE_GATEWAY_TOKEN` is set in `.env`.

If they're not available, the agent form says which settings are missing. On Radicle, Ryph can only add labels and close issues in repositories where its node is a delegate. It can always close issues it opened itself. On GitLab, confidential issues and internal notes are kept from agents.

## Follow the implementation

See [the git gateway](https://github.com/ryph-hq/ryph/blob/main/packages/forge/docs/git-gateway.md), [the patch lifecycle](https://github.com/ryph-hq/ryph/blob/main/packages/orchestrator/docs/patch-lifecycle.md), [the runner's git flow](https://github.com/ryph-hq/ryph/blob/main/packages/runner/docs/git-flow.md) and [the forge's architecture](https://github.com/ryph-hq/ryph/blob/main/packages/forge/docs/architecture.md).
