Skip to content

Open pull requests

A recipe with delivery.pullRequest turns a successful run’s commits into a GitHub pull request. The agent never holds a token that can push. It commits in its clone, and a separate courier container in the same pod pushes the branch and opens the pull request. The pull request’s URL ends up in the Agent’s status.prUrl.

Without delivery, the agent container gets the repo’s token as its token_secret variable and as GH_TOKEN, and the clone keeps push auth in its config. An agent that a prompt injection has taken over can then push anywhere that token reaches. With delivery, it can only commit.

apiVersion: agents.re-cinq.com/v1beta1
kind: AgentDefinition
metadata: { name: bug-fixer, namespace: ai-agents }
spec:
model: claude-sonnet-4-6
prompt: "Fix {ticket} and commit the fix."
resources:
repos:
- name: app
url: https://github.com/acme/app.git
token_secret: GH_TOKEN_APP # a key in the agent-secrets Secret
delivery:
pullRequest:
base: main
titleTemplate: "Fix {ticket}"
draft: true
FieldNotes
baseRequired. The branch the pull request merges into.
titleTemplateRequired. The title. {placeholder} tokens are filled from the Agent’s parameters, as the prompt’s are.
draftOpen the pull request as a draft. Default false.
apiUrlGitHub REST API base URL. Default https://api.github.com; set it for GitHub Enterprise (https://ghe.example.com/api/v3).

The pull request comes from the first entry of resources.repos. Its token is that repo’s token_secret. When the Agent’s parameters carry git_credential and git_credential_url, the credential broker mints the token instead, for both the push and the API call.

apiVersion: agents.re-cinq.com/v1beta1
kind: Agent
metadata: { generateName: fix-, namespace: ai-agents }
spec:
stationRef: bug-fixer
parameters: { ticket: ENG-1 }
branch: fix/eng-1 # optional; default agent/<Agent name>
targetRepo: acme/app # optional; default the repo's own owner/name

The work is pushed to branch, or to agent/<Agent name> when the Agent names none. The pull request opens on targetRepo, or on the owner/name of the repo’s GitHub URL. Once the run ends:

Terminal window
kubectl -n ai-agents get agent fix-abc12 -o jsonpath='{.status.prUrl}'
  1. The init container clones the repo with the token and writes no push auth into the clone.
  2. The agent runs and commits. Its container has no git token, no GH_TOKEN, and no broker credential.
  3. When the agent exits, the supervisor writes a git bundle of the clone’s HEAD to the outbox volume, and then a marker that says whether there is anything to deliver.
  4. The courier container waits for the marker. It fetches the bundle into a bare repository of its own and never runs git in the agent’s clone. It pushes HEAD to refs/heads/<branch>, then calls POST /repos/{owner}/{repo}/pulls. When a pull request from that branch is already open (a 422 saying so), it looks that one up and reports it.
  5. The courier raises a delivery event on its stdout and on the recipe’s sinks. The controller reads the courier container’s log, not the agent’s, so the agent cannot forge the URL, and records it as status.prUrl.

The courier is an ordinary container, not a native sidecar, because it has to outlive the agent. The Job completes once both have exited. It runs the agent image’s ai-agent-init binary as ai-agent-init courier, so no extra image is needed.

The runOutcome
Exits 0 with new commitsBranch pushed, pull request opened, status.prUrl set.
Exits non-zeroNothing pushed. The delivery event says skipped, and the Agent fails because the agent did.
Exits 0 without a new commitNothing pushed, skipped with no new commits. The Agent succeeds.
Push or API call failsThe courier exits 1, so the Agent fails, with failureReason set to pull request delivery failed: ….
The agent container dies before the supervisor writes the markerThe courier gives up five seconds after the supervisor’s deadline and the Agent fails.

The branch is pushed without --force. A rerun onto a branch whose history has moved on is rejected.

  • execution must be interactive. Batch and realtime recipes with delivery fail the Agent before a Job is created, as does a recipe with no resources.repos, a repo without token_secret when there is no broker, a branch name git would refuse, or no GitHub owner/name to open the pull request on.
  • The Station’s image needs git, since the supervisor bundles the work there. The courier uses the agent image, which ships it.
  • The agent has no credential, so it cannot git fetch from a private repo or use gh against it during the run. Anything it needs has to be in the clone the init made.
  • A Station template that already has a container named courier, or a volume named outbox or courier, is rejected or overridden; those names are reserved.