Development record
abcd audits and applies a repository's GitHub settings
Summary
Delivers itd-2610290746077716, which resolves iss-2610121535283764. abcd ahoy --remote grows from two secret-scanning toggles into a read-only audit of the whole baseline the intent's Decisions name. abcd ahoy remote apply applies the differences the audit can apply, after one confirmation that names each of them. It writes what it applied into the committed mirror.
Grounding, read on 29 October 2026:
internal/core/ahoy/remote.goalready holds the gates every remote verb passes: a managed repo, the origin's own github.com repository, the opt-out, and a confirmation. It also holds theghrunner (internal/core/github) and the content-keyed mirror at.abcd/work/rulesets/repo-settings.json.scaffold.DeriveCIChecks(internal/core/launch/scaffold/cichecks.go) already derives a repository's check names from its workflows, and omits what it can't know (matrix jobs, expression names, reusable-workflow calls).- The API answers were read live on abcd's own repository and two managed ones:
actions/permissionscarriesallowed_actionsandsha_pinning_required;actions/permissions/workflowcarriesdefault_workflow_permissionsandcan_approve_pull_request_reviews;vulnerability-alertsanswers 204 when on, 404 when off;rules/branches/{branch}returns the effective rules from every active ruleset;rulesets?targets=tag&includes_parents=truelists the tag rulesets;orgs/{org}carriesplan.name;orgs/{org}/actions/permissionsanswers 403 to anyone who is not an org admin.
Approach
- The audit model. A
SettingsAuditonRemoteResultholds the declared and real visibility, the plan, and oneFindingper baseline setting. Each finding has a setting, current value, target and state:ok;differs: the apply can set it;advisory: reported, never applied, with the reason;owed: the plan or the visibility does not offer it yet;kept: named ingithub.keep;unknown: the read failed or the answer did not carry it, with the reason, and the missing scope or role when that was the cause. An unknown is never reported as off and never applied. The two existing toggles are findings too. On a private repository whose answer does not carry them, they areowed, not written: that write is the 422 the PG-Engine report met.
- Reads, one per endpoint above. All go through
github.API, are bounded, and are judged as follows:- A 403 or 404 whose message names an upgrade, on a private repository, makes the setting
owed. - A 403 on an org endpoint is
unknown. Its note names the org admin role and thegh auth refresh -h github.com -s admin:orgstep. - A tag ruleset is read in detail only when the listing names it, and at most 20 are read.
- A 403 or 404 whose message names an upgrade, on a private repository, makes the setting
- Workflow facts, read from the tree. One exported derivation in
scaffoldreturns the required-check names for a repository. It reusesworkflowChecksand keeps only workflows that:- run on
pull_request; - also run on
merge_groupwhen the effective rules hold a merge queue; - have no
paths/paths-ignorefilter; - have no base-branch filter that leaves out the default branch.
A workflow it leaves out is named in a note. A small reader in
ahoycounts theuses:references not pinned to a 40-hex commit (local./anddocker://excepted). It also names the workflows with no top-levelpermissions:key.
- run on
- Overrides.
.abcd/config.jsongains agithubblock:keep: a list of setting keys abcd leaves exactly as they are;required_checks: a list that replaces the derived check names. Both are validated, and an unknown key inkeeprefuses the audit, naming the key, so a typo cannot silently keep nothing. An absent block is the baseline. The existing opt-outscan.native_secret_scanning: falsenow keeps only the two toggles it names, reported askept. The rest of the baseline is still audited and applied. Before this, it stood the whole remote verb down and abcd contacted nothing. The coordinator agreed this on 29 October 2026.
- The apply (step 2). The confirmation lists every
differsfinding, and nothing is written without the yes. Then, in order:- the security toggles: secret scanning before push protection, Dependabot alerts before security updates;
- the repository object (
delete_branch_on_merge,has_wiki,has_projects) in one PATCH; - abcd's own three rulesets, created or updated by name and never another ruleset:
abcd: default branch(deletion, non_fast_forward, required checks),abcd: code-owner review(pull_request,require_code_owner_review, count 0), andabcd: release tags(refs/tags/v*: deletion, update, non_fast_forward); none of them has a bypass actor; - the Actions policy and the workflow token, each keeping the fields it does not drive; A failed step stops the sequence, and the refusal names what already landed (as today). Visibility is in no write's body.
- The mirror.
repo-settings.jsonkeeps its shape and gains the managed settings abcd applied and anowedlist. Each ruleset abcd applied is mirrored, normalised, as.abcd/work/rulesets/abcd-<name>.json. Writes go throughos.Rootand are content-keyed, so a re-run writes nothing. No file abcd writes carries a person's login or name (product thinker, 29 October 2026). A ruleset's bypass actors are mirrored by actor type and the id GitHub returns. A repository owned by a person is recorded by its name alone, neverowner/name. This rule binds the existingrepo-settings.jsonwrite too. The text and JSON output a person reads may show logins. - The surfaces. The text render prints one
setting / current / targetline per finding that is notok, then a count of the ok ones.--jsoncarries the whole audit.commands/ahoy.md, the ahoy brief and the configuration brief describe the audit, the apply, thegithubblock and the mirror.
How each criterion is met
- Differences listed, nothing changed. Approaches 1–3. Test: a routed fake
ghserving a Dessau-shaped repository; the read lists each difference, and the call log holds no PATCH, PUT or POST and the tree is unchanged. - Apply then clean, apply again changes nothing. Approach 5. Test: the fake records writes and serves the post-apply state; the second read reports only
okandkept, and the second apply makes no write and rewrites no file. - Unanswered apply. The existing RefusingPrompter path. Test: no write, status
aborted. - Private Free. Approaches 1–2. Test: private repository, no secret-scanning fields, rulesets 403 with the upgrade message; the apply writes only the Dependabot toggles and the repository object, and the mirror lists the owed settings.
- Visibility mismatch. Approach 1. Test: declared
public, realprivate; reported, and no write body namesvisibilityorprivate. - Only the checks that run. Approach 3. Test: workflows with
buildandlinton pull_request, one path-filtered workflow, one withoutmerge_groupunder a merge queue; the ruleset body requires exactlybuildandlint, and a stale required context is named. github.keep. Approach 4. Test:has_wikikept; reportedkept, absent from the PATCH.- Missing scope. Approach 2. Test: org actions endpoint 403;
unknown, the note namesadmin:org. - No login in a written file (requirement, approach 6). Test: a user-owned repository whose owner login is a distinctive token, and an existing abcd ruleset with a user bypass actor. After the apply, no file under
.abcd/work/rulesets/contains the token, and the bypass actor is mirrored by type and id. - Unpinned actions. Approach 3. Test: a
uses: actions/checkout@v4;advisorywith the count, and no Actions-policy write.
Out of scope, recorded elsewhere: writing CODEOWNERS (iss-2610290755587946), which waits on the maintainer definition of iss-2610290730539347 (until then the audit reports a missing or partial file as advisory, naming the paths it should cover); the starter workflow for a repository with no CI (iss-2610290748300390); organization-wide settings writes; classic branch protection, which is not read (a repository on it gets abcd's rulesets beside it, and rules add up).
Footprint
- packages: internal/core/ahoy/remote.go, internal/core/ahoy/remote_baseline.go, internal/core/ahoy/remote_baseline_apply.go, internal/core/ahoy/remote_workflows.go, internal/core/launch/scaffold/cichecks.go, internal/surface/cli/cli.go, commands/ahoy.md, .abcd/development/brief/04-surfaces/01-ahoy.md, .abcd/development/brief/05-internals/03-configuration.md, .abcd/work/rulesets/README.md, .abcd/work/rulesets/main-review.json
- tests: internal/core/ahoy/remote_baseline_test.go, internal/core/ahoy/remote_baseline_apply_test.go, internal/core/ahoy/remote_test.go, internal/core/launch/scaffold/cichecks_required_test.go, internal/surface/cli/ahoy_remote_test.go
Steps
- The read-only audit
- packages: internal/core/ahoy/remote.go, internal/core/ahoy/remote_baseline.go, internal/core/ahoy/remote_workflows.go, internal/core/launch/scaffold/cichecks.go, internal/surface/cli/cli.go
- tests: internal/core/ahoy/remote_baseline_test.go, internal/core/launch/scaffold/cichecks_required_test.go, internal/surface/cli/ahoy_remote_test.go
- The opt-in apply and its mirror
- needs: 1
- packages: internal/core/ahoy/remote.go, internal/core/ahoy/remote_baseline_apply.go, commands/ahoy.md, .abcd/development/brief/04-surfaces/01-ahoy.md, .abcd/development/brief/05-internals/03-configuration.md
- tests: internal/core/ahoy/remote_baseline_apply_test.go, internal/core/ahoy/remote_test.go
Frontmatter
| id | spc-2610290749113317 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-2610290749113317-abcd-audits-a-managed-repository-s.md
open on GitHub ↗ · commit history ↗
References1
- itd-2610290746077716abcd audits and applies a repository's GitHub settingsimplements
mentions4
- iss-2610121535283764abcd should audit and apply GitHub security settings for every repository it manages
- iss-2610290730539347Nothing limits who may make a private lab or ideate verdict public: once a promote path exists, any account able to run it could move private material into the committed research store. A check inside abcd alone cannot hold, because a contributor controls their own machine and can copy the files by hand; the boundary that holds is the forge, where only an account with write access can merge. A contributor's pull request is also public the moment it is opened, before any review, so the forge keeps such material out of the project's record but cannot stop a contributor disclosing it there.
- iss-2610290748300390A managed repository with no CI workflow gets no required status check from abcd ahoy remote apply (itd-2610290746077716), because no workflow reports one. iss-2610121535283764 proposed that abcd offer such a repository a minimal starter workflow, gitleaks plus abcd lint, and then require those checks. That was split out of itd-2610290746077716 because nothing in abcd writes a CI workflow yet. A correct one needs a gitleaks binary pinned by version and verified by checksum (the gitleaks Action needs a paid licence on organization repositories), abcd itself installed and verified in CI, and every action pinned by commit SHA so it passes the SHA-pinning baseline it would sit under.
- iss-2610290755587946abcd ahoy remote apply (itd-2610290746077716) reports a missing or partial CODEOWNERS file as advisory and does not write it. Whether a managed repository keeps a CODEOWNERS file at all is now part of the maintainer-gate record iss-2610290730539347, which is not yet on the default branch. A maintainer is anyone with Write access or above, and GitHub counts approvals only from those, so the approval gate does not depend on CODEOWNERS naming a team. <!-- record-lint: forward-looking -->