# PAT Pool

Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting.

**This is a stop-gap workaround.** As soon as organization/enterprise billing is available to the dotnet org, this approach will be removed from our workflows.

## Repository Onboarding

To use Agentic Workflows in a dotnet org repository:

1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo]. Use `gh aw` **v0.71.5 or newer**, which supports the agent job dependencies required for this implementation.
2. Copy the `pat_pool.md` and `pat_pool.README.md` files into the repository under `.github/workflows/shared`.
3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below.

**Install or upgrade the `gh aw` CLI and check the version**

```sh
gh extension install github/gh-aw --force
gh aw --version
```

### Environment

Create an environment for the agentic workflows:
  - _Configuring these settings requires repo admin permission_
  - https://github.com/dotnet/{repo}/settings/environments
  - Recommended Name: **copilot-pat-pool**
  - Recommended Deployment branches and tags: **Protected branches only**

This environment is used for all agentic workflows, restricting agentic workflows to the repo's protected branches and preventing the workflows from accessing secrets defined for other environments.

## PAT Management

Team members provide PATs into the pool with secret names matching the pattern of `<pool_name>_<0-9>`, such as `COPILOT_PAT_0`.

[Use this link to prefill the PAT creation form with the required settings][create-pat]:

1. **Resource owner** is your **user account**, not an organization.
2. **Copilot Requests (Read)** must be the only permission granted.
3. **8-day expiration** must be used, which enforces a weekly renewal.
4. **Repository access** set to **Public repositories** only.

The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference.

Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the PAT pool. With an 8-day expiration, renewal can be done on the same day each week.

## PAT Pool Secrets

For a PAT pool that is specific to an environment, PATs can be added to repositories as **Environment Secrets** for the environment created above. _This requires repo admin permission_.

* **Settings** >
   * **Environments** >
      * **copilot-pat-pool** (or other environment name) >
         * **Add environment secret** (or edit your existing secret)
            * Enter your secret name of `COPILOT_PAT_{0-9}` and paste in your PAT

This can also be accomplished using the `gh` CLI, specifying the repo and environment arguments.

```sh
# Register the PAT secret. This will prompt for you to paste the PAT.
gh secret set "<pool_name>_<0-9>" --repo <org>/<repo> --env "copilot-pat-pool"
```

It's also helpful to record who owns each PAT within the pool. To capture which team member is associated with each PAT, a `<pool_name>_<0-9>_<username>` "sidecar secret" can be added alongside the PAT secret to make the username for the PAT pool entry visible. This sidecar secret must have a non-empty value, but it's never consumed, so any value is sufficient.

```sh
# Record a sidecar secret that presents who owns this PAT.
gh secret set "<pool_name>_<0-9>_<username>" --body "<username>" --repo <org>/<repo> --env "copilot-pat-pool"
```

## Workflow Output Attribution

Team members' PATs are _only_ used for the Copilot requests from within the agentic portion of the workflow. All outputs from the workflow use the `github-actions[bot]` account token. Issues, PRs, comments, and all other content generated by the workflow will be attributed to `github-actions[bot]`--not the team member's account or token.

## Usage

The [`pat_pool.md`](./pat_pool.md) workflow import defines a custom job with a `pat_number` output. Consuming workflows need two additions to their frontmatter to import this job and use the PAT number to override the `COPILOT_GITHUB_TOKEN` passed to the workflow's agent job.

```yml
# ###############################################################
# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN.
# Run agentic jobs in an isolated `copilot-pat-pool` environment.
#
# When org-level billing is available, this will be removed.
# See `shared/pat_pool.README.md` for more information.
# ###############################################################
imports:
  - uses: shared/pat_pool.md
    with:
      environment: copilot-pat-pool

environment: copilot-pat-pool

engine:
  id: copilot
  env:
    COPILOT_GITHUB_TOKEN: |
      ${{ case(
        needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0,
        needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1,
        needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2,
        needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3,
        needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4,
        needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5,
        needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6,
        needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7,
        needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8,
        needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9,
        'NO COPILOT PAT AVAILABLE')
      }}
```

The `COPILOT_GITHUB_TOKEN` expression can be collapsed onto a single line if desired. `gh-aw compile` automatically wires `pat_pool` into the activation and agent jobs' `needs:` graph because of the `needs.pat_pool.` references within the `engine.env` property.

```sh
gh aw compile <workflow-name> --schedule-seed <org>/<repo>
```

### Specifying the environment

The `environment` must be specified both to the `pat_pool.md` import and to the containing workflow to ensure both jobs access the PAT pool from the same environment. The `copilot-pat-pool` environment name is recommended as the isolated environment for agentic workflows that use the PAT pool.

### Customizing the pool

The import declares 10 optional inputs (`COPILOT_PAT_0` through `COPILOT_PAT_9`), each defaulting to `secrets.COPILOT_PAT_#` of the matching number. To point a workflow at a different pool of repository secrets, use the parameterized `uses`/`with` form when importing and pass the substitute secrets as the `COPILOT_PAT_#` inputs:

```yml
imports:
  - uses: shared/pat_pool.md
    with:
      COPILOT_PAT_0: ${{ secrets.MY_TEAM_PAT_0 }}
      COPILOT_PAT_1: ${{ secrets.MY_TEAM_PAT_1 }}
      # Unspecified inputs default to `secrets.COPILOT_PAT_#` lookups
```

The secrets passed via `with:` must match the secrets referenced in the consuming workflow's `case` expression that overrides `COPILOT_GITHUB_TOKEN`--both sides need to agree on which secret backs each `COPILOT_PAT_#` slot. Update the `case` expression accordingly:

```yml
engine:
  id: copilot
  env:
    COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.MY_TEAM_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.MY_TEAM_PAT_1, ..., 'NO COPILOT PAT AVAILABLE') }}
```

This approach aligns with GitHub's documented guidance for [passing secrets][passing-secrets] between workflows, where the `pat_pool` job returns a PAT number and the `case` statement acts as a secret store to look the PAT secret up based on the selected number.

## Design / Security

There are several details of this implementation that keep our workflows and repositories safe.

1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is
   provided to a dedicated step within the `pat_pool` job. That job runs
   after `pre_activation` and contains only the trusted checkout and action
   steps--no untrusted context or input is within scope. The
   `select-pat-number` action only references the secret values to determine
   which are non-empty, filtering the secret numbers to those with values.
1. **The `pat_pool` job emits only a number, never a secret.** Its sole output,
   `pat_number`, is the 0-9 index of the selected PAT (or empty when the pool
   is empty). The actual secret materializes only later, in the activation
   job's `engine.env` mapping, where the `case()` expression resolves the
   number to the matching secret. This follows GitHub's guidance for
   [passing secrets][passing-secrets] between jobs or workflows, with the
   `case` statement acting as a very simple secret store.
1. **The `select-pat-number` action does not require any permissions.** It
   reads only the `COPILOT_PAT_#` environment variables passed to it and writes
   only to `GITHUB_OUTPUT`. The job that hosts it sets `permissions:` to the
   workflow defaults (no elevated scopes).
1. **The implementation uses supported Agentic Workflow extensibility hooks.**
   Defining a custom job inside an [imported workflow file][imports] is
   supported by `gh aw compile`. gh-aw automatically
   wires `pat_pool` into the activation job's `needs:` graph based on the
   `needs.pat_pool.outputs.pat_number` references in `engine.env`. The
   [secret override][secret-override] capability supplies the `COPILOT_GITHUB_TOKEN`
   value via `engine.env` rather than the default secret of the same name.

Each of the references below contributed to the design and implementation to ensure a secure and reliable design.

## Known Issues

### Workflows must produce a `pre_activation` job

The `pat_pool` import integration requires that the workflow's compilation results in a `pre_activation` job. If nothing in your workflow definition produces a `pre_activation` job, a compilation error will be received.

```text
✗ Failed workflows:
  ✗ <workflow-name>.md

.github\workflows\<workflow-name>.md:1:1: error: failed to generate YAML: failed to build and validate jobs: job dependency validation failed: job 'pat_pool' depends on non-existent job 'pre_activation'
```

To work around this, add `on.permissions: {}` to your workflow, which forces a no-op `pre_activation` job to be generated.

```yml
on:
  permissions: {}
```

See: [Activation 'needs' does not incorporate jobs in engine.env expressions (github/gh-aw#30790)](https://github.com/github/gh-aw/issues/30790)

### Fork guards must be wrapped in parentheses

A workflow-level `if:` is applied to the generated `pre_activation` job -- the same job produced by the `on.permissions: {}` workaround above. When that condition is a single expression beginning with `!`, such as a fork guard, `gh aw compile` emits it inline and produces invalid YAML, because a leading `!` is a YAML tag indicator.

```yml
# Frontmatter fork guard
if: ${{ !github.event.repository.fork }}
```

```yml
# ...emitted as an invalid `pre_activation` condition in <workflow-name>.lock.yml
  pre_activation:
    if: !github.event.repository.fork
```

GitHub then rejects the compiled workflow with a tag-construction error (`could not determine a constructor for the tag '!github.event.repository.fork'`). Only single-term conditions are affected; a compound condition -- for example, a fork guard combined with a `pull_request` head-repo check -- is emitted in an already-parenthesized form that parses cleanly.

Wrap the expression in parentheses so the emitted condition starts with `(` instead of `!`. The `${{ }}` wrapper is optional -- `gh aw compile` strips it -- so it can be dropped.

```yml
if: (!github.event.repository.fork)
```

## References

- [Agentic Workflows CLI Extension][cli-setup]
- [Agentic Authoring][configure-repo]
- [Authentication][authentication]
- [Agentic Workflow Imports][imports]
- [Custom Steps][steps]
- [Custom Jobs][jobs]
- [Job Outputs][job-outputs]
- [Engine Configuration][engine]
- [Engine Environment Variables][engine-vars]
- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override]
- [Case Function in Workflow Expressions][case-expression]
- [Passing a secret between jobs or workflows][passing-secrets]

[cli-setup]: https://github.github.com/gh-aw/setup/cli/
[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository
[authentication]: https://github.github.com/gh-aw/reference/auth/
[create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8
[imports]: https://github.github.com/gh-aw/reference/imports/
[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps
[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs
[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs
[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine
[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables
[secret-override]: https://github.com/github/gh-aw/pull/18017
[case-expression]: https://docs.github.com/actions/reference/workflows-and-actions/expressions#case
[passing-secrets]: https://docs.github.com/actions/reference/workflows-and-actions/workflow-commands#example-masking-and-passing-a-secret-between-jobs-or-workflows
