Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions docs/ADRs/0065-human-gated-permission-adjustments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: "65. Human-gated permission adjustments for agent tokens"
status: Accepted
relates_to:
- security-threat-model
- agent-architecture
topics:
- authorization
- permissions
- mint
- workflows
---

# 65. Human-gated permission adjustments for agent tokens

Date: 2026-07-01

## Status

Accepted

<!-- ADRs are point-in-time records, but not fully frozen after acceptance.
Minor annotations are welcome: cross-references to related ADRs, short
notes linking to newer decisions, or clarifying remarks. However, do not
substantially rewrite the Context, Decision, or Consequences sections. If
the decision itself needs to change, write a new ADR that supersedes this
one. For evolving design narrative, use docs/architecture.md. -->

Supersedes the automated secret-inventory gating approach proposed in
[#1739](https://github.com/fullsend-ai/fullsend/issues/1739).

## Context

The code agent cannot push `.github/workflows/` changes because
`workflows: write` is intentionally withheld from default coder tokens
([ADR 0007](0007-per-role-github-apps.md),
[ADR 0017](0017-credential-isolation-for-sandboxed-agents.md)). This blocks
[#470](https://github.com/fullsend-ai/fullsend/issues/470). The problem
applies to any permission that is too dangerous for routine agent runs but
sometimes required for legitimate work.

An earlier approach
([#1739](https://github.com/fullsend-ai/fullsend/issues/1739)) proposed
automated secret-inventory validation as the gate for `workflows: write` —
the agent would scan the repository and only receive elevated permissions if
no secrets were detected. This was abandoned because automated readiness
checks cannot substitute for human judgment about when elevated access is
appropriate, and because the threat model
([security-threat-model.md](../problems/security-threat-model.md)) prioritizes
external injection over automated heuristic bypass.

[PR #2548](https://github.com/fullsend-ai/fullsend/pull/2548) prototyped a
label-based human-authorization flow with a layered design. It was closed
without merging after
[#2389](https://github.com/fullsend-ai/fullsend/pull/2389) moved token
minting into the binary, changing the integration surface. This ADR records
the design decisions from that work so implementation can proceed.

## Decision

Permission adjustments use a three-layer design where each layer has a
single responsibility:

### Layer 1: Policy (CLI)

The `fullsend` CLI owns all policy decisions about when a permission
adjustment is allowed. Policy inputs are deterministic forge state — labels,
collaborator status, comment timestamps — not LLM output.

For the initial `workflow-change` gate:

- **`workflow-change-needed`** — triage signals anticipated workflow edits.
When set without authorization, `ready-to-code` is withheld.
- **`workflow-change-allowed`** — a repository collaborator explicitly
authorizes the elevation.

The CLI verifies labels, checks that the authorizing user is a collaborator,
and invalidates stale authorization (e.g., when an agent-influencing comment
appears after the authorization label).

`fullsend auth check` runs at three phases:

| Phase | When | Outcome if not authorized |
|-------|------|----|
| `mint` | Before token mint | Token minted without elevation |
| `pre-run` | Before sandbox creation | Agent run skipped |
| `pre-push` | Before git push | Push blocked; `workflow-change-needed` applied |

### Layer 2: Mint (mechanical)

The mint service applies caller-requested permission adjustments
mechanically on top of a role baseline. When the workflow passes
`elevations: ["workflow-change"]` after a successful CLI auth check,
mint merges the gate's permissions (e.g., `workflows: write`) into the
token. The role's GitHub App manifest includes elevated permissions as
a **ceiling**; mint grants them only when explicitly requested.

Mint does not read issues, labels, or comments. It does not make policy
decisions. It trusts that the caller (an enrolled workflow verified by
OIDC audience and `job_workflow_ref`) has already performed authorization.

### Layer 3: Automation (privileged operations)

Privileged git and GitHub operations — pushing branches, creating PRs,
applying labels — remain in the deterministic post-script on the runner,
never in the LLM sandbox. The elevated token is available only to the
post-script; the sandbox agent never holds write-capable credentials
([ADR 0017](0017-credential-isolation-for-sandboxed-agents.md)).

### Threat model

Three vectors are addressed, ordered by the project's threat priority
([security-threat-model.md](../problems/security-threat-model.md)):

1. **TOCTOU between authorization and token mint.** A gap between when
a human authorizes elevation and when the token is minted could allow
conditions to change (e.g., a malicious comment injected between
label application and the mint call). Mitigated by performing the
CLI auth check and mint request in a single workflow invocation —
the check and the mint are adjacent steps in the same job, not
separate events.

2. **Post-grant prompt injection expanding scope.** After authorization,
the agent might be influenced (via injected content in issues,
comments, or code) to believe it needs broader access than authorized.
Mitigated by using deterministic gate inputs (labels, collaborator
API) rather than agent tool discovery or LLM reasoning to decide
what permissions are needed. The agent does not choose its own
permissions.

3. **Post-grant injection via privileged tools.** Even with an elevated
token, a compromised agent could misuse `workflows: write` to deploy
arbitrary workflow files. Mitigated by keeping write-capable tokens
out of the LLM sandbox entirely
([ADR 0017](0017-credential-isolation-for-sandboxed-agents.md),
[ADR 0032](0032-safe-push-wrapper-for-sandboxed-agents.md)). The
elevated token is used only by the post-script, which applies its
own protected-path checks and review gates before pushing.

### Design decisions

- **Human authorization over automated gating.** Automated repo-readiness
checks ([#788](https://github.com/fullsend-ai/fullsend/issues/788))
may remain as optional advisory signals but are not a blocker for
elevation. The authorization decision is a human collaborator applying
a label.
- **Reusable gate pattern.** The `workflow-change` gate is the first
instance of a general `authorization.Gate` registry. Future elevated
permissions (e.g., `admin` scope for CI config changes) reuse the
same label + CLI + elevations pattern without mint changes.
- **The automated secret-inventory approach from
[#1739](https://github.com/fullsend-ai/fullsend/issues/1739) is
superseded.** Human judgment replaces automated heuristics for
elevation decisions.

## Consequences

- Unblocks [#470](https://github.com/fullsend-ai/fullsend/issues/470):
agents can push workflow changes when a collaborator authorizes it.
- Default agent tokens remain unchanged — no org-wide `workflows: write`
on every code run.
- Orgs must approve updated coder app permissions (adding `workflows: write`
as a ceiling) in GitHub org settings; `fullsend admin` surfaces
stale-permission warnings.
- Future elevated permissions reuse the same gate + CLI + elevations
pattern without modifying mint logic.
- Defense in depth: label gate (CLI) + mint downscoping + credential
isolation (sandbox never holds write token) + post-script
protected-path checks + review agent protected-path rules.
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Identity is not the same as trust. An agent's identity lets it authenticate to e
- Per-role GitHub Apps with manifest-based creation. Each agent role gets its own app with scoped permissions. PEMs stored in Secret Manager as `fullsend-{role}-app-pem` — one secret per role, shared across orgs on a mint. `ROLE_APP_IDS` uses the same shared-per-role model (`coder` → app ID). Org isolation is enforced via `ALLOWED_ORGS`, WIF conditions, and installation verification ([ADR 0007](ADRs/0007-per-role-github-apps.md), [ADR 0033](ADRs/0033-per-repo-installation-mode.md)). Public multi-tenant mint (`ALLOWED_ORGS=*`) with upstream-only workflow provenance is defined in [ADR 0059](ADRs/0059-public-mint-mode-with-wildcard-allowlists.md); upstream-only provenance limits which workflows can call the mint, complementing [ADR 0029](ADRs/0029-central-token-mint-secretless-fullsend.md) multi-tenant blast-radius concerns.
- Cross-org mint authorization: workflows may request tokens for a different org via optional `target_org` when the target org installs the role App and sets `FULLSEND_FOREIGN_<role>_REPOS`. Empty `repos` yields installation-wide tokens on either path; cross-org adds FOREIGN gating, same-org relies on WIF/OIDC enrollment ([ADR 0060](ADRs/0060-cross-org-mint-authorization-via-org-variables.md)).
- Standalone mint deployment: `cmd/mint/` provides a self-contained HTTP server that uses direct JWKS verification and filesystem PEM storage instead of GCP infrastructure. It shares the `internal/mintcore/` library with the GCF mint and adds support for custom role permissions and a fallback proxy to an upstream mint. Custom role permissions live in mintcore (not `cmd/mint/`) so that `RolePermissionsFor`, `HasRole`, and `CreateInstallationToken` return a unified view without callers needing to distinguish built-in from custom roles. The GCF mint never calls `RegisterCustomRolePermissions`, so the code is inert there. See the [standalone mint guide](guides/infrastructure/standalone-mint.md).
- Human-gated permission adjustments: elevated permissions (e.g., `workflows: write`) are granted via a label-based authorization flow. Policy decisions (label checks, collaborator verification, stale invalidation) live in the CLI (`fullsend auth check`); mint mechanically merges caller-requested `elevations` into the role baseline without reading issues or labels. Privileged operations remain in the deterministic post-script, never in the LLM sandbox ([ADR 0065](ADRs/0065-human-gated-permission-adjustments.md)).

One concrete implementation option is [`oidcx`](https://github.com/oxidecomputer/oidcx): a service that accepts OIDC identity tokens and exchanges them for short-lived access tokens. It can mint tokens scoped to selected GitHub repositories and permissions, or to selected Oxide silos and permissions, and it also ships with a GitHub Action wrapper. In a Fullsend deployment, this can be used by the sandbox entrypoint to narrow a broad GitHub App identity down to only the specific permissions an agent needs for the current run.

Expand Down
Loading