Skip to content
Draft
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
77 changes: 70 additions & 7 deletions .github/scripts/Set-RepoGovernance.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
.SYNOPSIS
Applies this repository's governance settings on GitHub with the gh CLI:
security features, merge policy, Actions permissions, a 'release'
environment, and a ruleset protecting the default branch. Idempotent -
environment with required reviewers, a ruleset protecting the default
branch and a ruleset protecting release tags. Idempotent -
re-run after changing it. Run as a repo admin: `task repo:govern`.

.DESCRIPTION
Expand All @@ -20,19 +21,37 @@
opens the generated-docs PR); fork PR workflows need approval from a
maintainer for first-time contributors.
- 'release' environment (required by release.yaml) restricted to the
default branch - the only place the packages:write token exists.
default branch - the only place the packages:write token exists - and
with a required reviewer (the repository owner, the Operator, or
-ReleaseReviewer): a release job waits for their approval. Self-review
is allowed, so a solo maintainer can still release.
- Ruleset 'protect-main': no deletion/force-push, linear history, PRs
required with 1 approval incl. code owners, stale reviews dismissed,
conversations resolved, required checks validate / lint / tests.
Repo admins may bypass *only through a PR* (solo-maintainer escape
hatch: own PRs can be merged without a second human).
hatch: own PRs can be merged without a second human). Kept on
purpose for now (least-privilege step 1, decision 4): the agent's
fine-grained token acts as the Operator, so it can still merge.
- Ruleset 'protect-release-tags': tags 'feature_*' (what the release
workflow publishes: feature_<id>_<version>) cannot be deleted,
moved (update) or force-pushed, and can be created only by the
GitHub Actions app (the release workflow) or a repo admin. If the API
refuses the Actions app as a bypass actor, the ruleset is applied with
admins only and a warning says so.

.PARAMETER Repo
owner/name. Defaults to the repo of the current directory.

.PARAMETER ReleaseReviewer
GitHub login required to approve runs of the 'release' environment.
Defaults to the repository owner when that is a user (read at run time,
never stored in this file). When neither resolves, reviewers are left as
they are and a warning says so.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$Repo
[string]$Repo,
[string]$ReleaseReviewer
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -99,14 +118,27 @@ if ($PSCmdlet.ShouldProcess($Repo, 'Actions permissions')) {
}

if ($PSCmdlet.ShouldProcess($Repo, "environment 'release'")) {
Invoke-Gh PUT "repos/$Repo/environments/release" @{
$envBody = @{
deployment_branch_policy = @{ protected_branches = $false; custom_branch_policies = $true }
} | Out-Null
}
if (-not $ReleaseReviewer) {
$owner = Invoke-Gh GET "repos/$Repo" | ConvertFrom-Json | Select-Object -ExpandProperty owner
if ($owner.type -eq 'User') { $ReleaseReviewer = $owner.login }
}
if ($ReleaseReviewer) {
$reviewerId = [int](Invoke-Gh GET "users/$ReleaseReviewer" | ConvertFrom-Json).id
$envBody.reviewers = @(@{ type = 'User'; id = $reviewerId })
$envBody.prevent_self_review = $false
}
else {
Write-Warning "No release reviewer resolved (the owner is not a user); pass -ReleaseReviewer <login>. Reviewers left unchanged."
}
Invoke-Gh PUT "repos/$Repo/environments/release" $envBody | Out-Null
$existing = Invoke-Gh GET "repos/$Repo/environments/release/deployment-branch-policies" | ConvertFrom-Json
if (-not ($existing.branch_policies | Where-Object { $_.name -eq 'main' })) {
Invoke-Gh POST "repos/$Repo/environments/release/deployment-branch-policies" @{ name = 'main'; type = 'branch' } | Out-Null
}
Write-Host " environment 'release': deployable from main only"
Write-Host " environment 'release': deployable from main only$(if ($ReleaseReviewer) { ', one required reviewer' })"
}

if ($PSCmdlet.ShouldProcess($Repo, "ruleset 'protect-main'")) {
Expand Down Expand Up @@ -153,6 +185,37 @@ if ($PSCmdlet.ShouldProcess($Repo, "ruleset 'protect-main'")) {
}
}

if ($PSCmdlet.ShouldProcess($Repo, "ruleset 'protect-release-tags'")) {
# RepositoryRole 5 = admin. Integration 15368 = the GitHub Actions app,
# which the release workflow's GITHUB_TOKEN acts as.
$admin = @{ actor_id = 5; actor_type = 'RepositoryRole'; bypass_mode = 'always' }
$actions = @{ actor_id = 15368; actor_type = 'Integration'; bypass_mode = 'always' }
$tagRuleset = @{
name = 'protect-release-tags'
target = 'tag'
enforcement = 'active'
conditions = @{ ref_name = @{ include = @('refs/tags/feature_*'); exclude = @() } }
bypass_actors = @($admin, $actions)
rules = @(
@{ type = 'creation' },
@{ type = 'update' },
@{ type = 'deletion' },
@{ type = 'non_fast_forward' }
)
}
$all = Invoke-Gh GET "repos/$Repo/rulesets" | ConvertFrom-Json
$current = $all | Where-Object { $_.name -eq 'protect-release-tags' } | Select-Object -First 1
$method = if ($current) { 'PUT' } else { 'POST' }
$endpoint = if ($current) { "repos/$Repo/rulesets/$($current.id)" } else { "repos/$Repo/rulesets" }
$result = Invoke-Gh $method $endpoint $tagRuleset -IgnoreError
if (-not $result) {
Write-Warning 'The API refused the GitHub Actions app as a bypass actor; applying with repo admins only (the release workflow then cannot create tags: add the actor by hand).'
$tagRuleset.bypass_actors = @($admin)
$result = Invoke-Gh $method $endpoint $tagRuleset
}
Write-Host " ruleset 'protect-release-tags': $(if ($current) { 'updated' } else { 'created' }) (feature_* tags: no delete, update or force-push; created by the release workflow or admins)"
}

Write-Host @'
Done. Manual follow-ups GitHub does not expose via API:
- Install the DCO app on this repo (https://github.com/apps/dco) - the
Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,6 @@ tasks:
- git config --get core.hooksPath

repo:govern:
desc: 'Apply the GitHub governance settings (security features, squash-only merges, Actions allowlist, release environment, protect-main ruleset) with gh - see .github/scripts/Set-RepoGovernance.ps1. Idempotent; needs gh logged in as a repo admin.'
desc: 'Apply the GitHub governance settings (security features, squash-only merges, Actions allowlist, release environment with a required reviewer, protect-main and protect-release-tags rulesets) with gh - see .github/scripts/Set-RepoGovernance.ps1. Idempotent; needs gh logged in as a repo admin.'
cmds:
- pwsh -NoLogo -NoProfile -File '{{.ROOT_DIR}}/.github/scripts/Set-RepoGovernance.ps1'
1 change: 1 addition & 0 deletions docs/proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ Drafts the Operator decides on. Before a ruling a proposal carries an "Architect
| [OpenBao key file](openbao-key-file.md) | proposed 2026-11-04; a DPAPI key file on the workstation replaces the typed unseal and the Operator's password: five shares (two in the file, three in custody) and an AppRole login with a periodic token, fed by the launcher at every `task dc:*`; the static seal was tested and is the fallback; ten decisions and the architecture review await; build not started |
| [Least privilege for agents, ending in one agent Linux user](agent-linux-user.md) | proposed 2026-11-06 as agents as their own Linux users (one per identity and per back); re-scoped the same day to least privilege first (the Operator's ruling; brokers held): an MVP of eight opt-in steps from the inventory, configuration before build: a GitHub ruleset and a narrow fine-grained token, Claude Code's sandbox and locked permissions in the claude-code managed drop-in, write-deny on hooks, `Taskfile.yml`, `.devhost` and the git dir's `config`, the backs' `claude -p` narrowed and the tester under `bwrap`, a per-agent OpenBao AppRole, one `agent` user without `sudo` or `docker`, the shared folder's agent access and no default SSH keys, the Codex equivalent; tests T1 and T3 gate the sandbox step; the first design's per-identity users are under "Later"; accepted 2026-11-08 (all sixteen recommendations; step 2 with the two-list rule after T1 and T3 came out partial); build not started; architecture review 2026-11-06 (of the earlier design): fit with changes |
| [Least privilege of agents and backs: an inventory](agent-least-privilege-inventory.md) | draft 2026-11-06; read-only inventory of what the host, a con, each back and a consumer's agent can reach today and need, the existing control for each gap (Claude Code's sandbox and managed settings, Codex's sandbox, GitHub rulesets and narrow tokens, OpenBao AppRoles, no `sudo` and no `docker`), the residuals with no native control, both architecture reviews mapped, an MVP list in delivery order and nine tests; the input for re-scoping agent-linux-user; nothing built |
| [Least privilege: the two consumers' BRDs mapped](agent-least-privilege-brd-mapping.md) | draft 2026-11-08; every requirement and acceptance test of the office's and AOF's BRDs traced onto the accepted least-privilege proposal (covered, partly, gap), where they agree, differ and run into accepted decisions (D7 one agent user, D8, D10, the brokers hold); the revision they imply: step 6 as accepted with a shared `dcf-work` group, then users per identity made at start from the agent list, declared groups, a Claude home per user, per-user ssh-agent keys, the hand-over to back users, a helper gate by task name with the caller from `SO_PEERCRED` (the reused part of #187), `agents check` and `migrate`; owners and versions, twelve open questions, eighteen decisions (six overturn or amend accepted ones); awaits the architecture review and the Operator; agent-linux-user.md not edited; architecture review 2026-11-08: fit with changes |
Loading
Loading