From 9b898c3640a59cc43b852aa0cadb38a559b865e7 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 07:26:19 +0000 Subject: [PATCH] docs(adr): add ADR 0065 for human-gated permission adjustments Records the three-layer design (CLI policy, mint mechanics, automation privileges) and threat model for label-gated elevated agent permissions. Updates architecture.md to reflect the decision. Closes #2821 Co-Authored-By: Claude Opus 4.6 --- ...0065-human-gated-permission-adjustments.md | 169 ++++++++++++++++++ docs/architecture.md | 1 + 2 files changed, 170 insertions(+) create mode 100644 docs/ADRs/0065-human-gated-permission-adjustments.md diff --git a/docs/ADRs/0065-human-gated-permission-adjustments.md b/docs/ADRs/0065-human-gated-permission-adjustments.md new file mode 100644 index 000000000..87f4c5599 --- /dev/null +++ b/docs/ADRs/0065-human-gated-permission-adjustments.md @@ -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 + + + +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. diff --git a/docs/architecture.md b/docs/architecture.md index 9f05754cc..5aa6960f3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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__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.