Skip to content

feat(#6042): support Prow OWNERS file authorization for slash commands - #6051

Open
RaphaelBut wants to merge 11 commits into
fullsend-ai:mainfrom
RaphaelBut:feat/owners-file-authorization
Open

feat(#6042): support Prow OWNERS file authorization for slash commands#6051
RaphaelBut wants to merge 11 commits into
fullsend-ai:mainfrom
RaphaelBut:feat/owners-file-authorization

Conversation

@RaphaelBut

@RaphaelBut RaphaelBut commented Aug 10, 2026

Copy link
Copy Markdown

Summary

Adds opt-in Prow OWNERS file authorization for slash command dispatch. Repos that use Prow/OWNERS instead of GitHub collaborator roles can add owners_file to the authorization providers list in .fullsend/config.yaml to let
OWNERS-listed users trigger fullsend agents without needing GitHub write/triage collaborator access. Approvers get write-equivalent access; reviewers get triage-equivalent only. OWNERS is checked first; if it authorizes the user,
the collaborator API is not consulted. If the user isn't in OWNERS, it falls through to the collaborator API.

authorization:
  - provider: owners_file

Changes

  • Provider-list config schema: authorization is a []AuthorizationProvider with validation for unknown/duplicate providers. Overlay-only — intentionally no base-layer fallback
  • OWNERS authorization path in has_repo_permission (reusable-dispatch.yml): opt-in gate via config, case-insensitive username matching (lc_user), direct + alias membership checks, ::notice:: audit logging, case guard with ;;&
    fallthrough mapping the role hierarchy
  • Checkout ref pin extended to pull_request_review events on all 9 checkout refs — PR authors cannot self-authorize on any path (base branch SHA for PR-scoped events, default-branch head otherwise)
  • Sparse-checkout anchoring: /OWNERS and /OWNERS_ALIASES anchored to repo root to avoid pulling nested per-directory OWNERS files on Prow-style repos
  • Scaffold parity: _owners_has_user and OWNERS auth block mirrored into internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml
  • Harness dispatch wiring (core.go): owners.Resolve computes an effective role for the IsAuthorized gate without mutating the caller's event — downstream CEL evaluation sees the original collaborator-API role. OWNERS resolved via
    filepath.Dir(ConfigDir), not bare working-directory paths
  • Slot-health check: ValidateSlotClean runs before every behaviour scenario to catch stale config state (kill switch, OWNERS auth) from failed cleanups — prevents silent false positives in auth tests
  • Unit tests: Dispatch() integration tests for OWNERS role upgrade/denial/disabled, config round-trip tests, provider validation tests
  • E2E scenarios: outsider-driven issue-opened scenarios proving OWNERS approver/reviewer/alias paths, write-denial scenario proving reviewer cannot escalate, collaborator-fallthrough scenario, opt-out scenario
  • Documentation: ADR 0054 updated for harness dispatch scope, non-mutation semantics, v1 limitation, and checkout ref semantics; layered-config reference expanded with provider list, overlay-only behavior, fail-closed behavior;
    workflow contracts documents the OWNERS sync contract and parity test coverage

Known limitations

  • v1 OWNERS schema: only repo-root flat approvers/reviewers lists are read. Prow filters: blocks and nested per-directory OWNERS files are not supported
  • OWNERS-authorized users without GitHub collaborator write access use the fork PR path

Related

Testing

  • make lint passes (stage changes first, then run)
  • Tests added/updated for new or modified logic

Checklist

  • PR title follows Conventional Commits (correct type, ! for breaking changes)
  • Commits are signed off (DCO) — human and human-directed agent sessions only
  • I wrote this contribution myself and can explain all changes in it

@RaphaelBut
RaphaelBut requested a review from a team as a code owner August 10, 2026 18:06
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

See E2E testing guide for details.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Support opt-in Prow OWNERS authorization for slash-command dispatch

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add opt-in OWNERS/OWNERS_ALIASES authorization for dispatch slash commands via
 authorization.owners_file.
• Grant approvers write-equivalent access and reviewers triage-equivalent, with audit notices.
• Add e2e scenarios, step definitions, and docs describing the new authorization backend.
Diagram

graph TD
  IC(["Issue comment / label"]) --> WD["Dispatch workflow"] --> AB{"Auth backend?"}
  AB -->|"owners_file=true"| OC["OWNERS check"] --> AL["Allow stage"]
  AB -->|"fallback"| GH{{"GitHub collaborator API"}} --> AL
  T["Godog e2e tests"] --> IC
  OC --> F1[/".fullsend/config.yaml"/] --> F2[/"OWNERS"/] --> F3[/"OWNERS_ALIASES"/]

  subgraph Legend
    direction LR
    _wf["Workflow/Test"] ~~~ _file[/"Repo file"/] ~~~ _ext{{"External"}} ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Implement OWNERS authorization as a Go helper invoked by the workflow
  • ➕ Typed parsing/validation of OWNERS and OWNERS_ALIASES with explicit errors
  • ➕ Unit-testable without relying on live GitHub Actions logs
  • ➕ Easier to evolve permission mapping and alias resolution safely
  • ➖ Requires CLI/build pipeline integration and caching changes
  • ➖ More moving parts for a first increment compared to inline bash/yq
2. Precompute authorization via a GitHub App/service and store results
  • ➕ Moves security logic out of bash and into a controlled runtime
  • ➕ Centralized policy with better observability and auditing
  • ➖ Introduces new infrastructure/service dependencies
  • ➖ More complex deployment and maintenance footprint
3. Rely on GitHub-native roles/teams instead of OWNERS
  • ➕ Simpler, already supported by existing collaborator API logic
  • ➕ Avoids parsing YAML files and alias semantics
  • ➖ Does not work for Prow-centric repos where OWNERS is the source of truth
  • ➖ May force repos to duplicate permission data in GitHub roles

Recommendation: The PR’s approach (opt-in OWNERS-file check with a safe fallback to the collaborator API) is a reasonable incremental step for Prow-based repos, especially given the base-SHA pinning and username/alias validation to reduce self-authorization and injection risk. If merged, it should be treated as a stepping stone: follow up by migrating the OWNERS parsing/decision logic into a Go helper with unit tests and explicit error handling, keeping the workflow as a thin caller.

Files changed (8) +459 / -1

Enhancement (1) +55 / -1
reusable-dispatch.ymlAdd opt-in OWNERS/alias authorization in has_repo_permission +55/-1

Add opt-in OWNERS/alias authorization in has_repo_permission

• Extends sparse-checkout to include OWNERS and OWNERS_ALIASES. Adds bash helpers to resolve direct and alias membership and gates the new authorization path behind 'authorization.owners_file' in '.fullsend/config.yaml', mapping approvers to write/triage and reviewers to triage only with audit notices.

.github/workflows/reusable-dispatch.yml

Tests (5) +365 / -0
owners-auth.featureAdd e2e feature scenarios for OWNERS authorization behavior +65/-0

Add e2e feature scenarios for OWNERS authorization behavior

• Introduces behaviour tests covering: approver authorization, alias resolution, reviewer triage grant, reviewer write denial, and opt-in gating. Asserts expected audit log messages in workflow/dispatch logs to confirm the OWNERS path is exercised.

e2e/behaviour/features/dispatch/owners-auth.feature

cleanup.goCleanup OWNERS auth artifacts after scenarios +5/-0

Cleanup OWNERS auth artifacts after scenarios

• Hooks scenario cleanup to remove/neutralize OWNERS auth test changes when activated. Ensures subsequent scenarios in the same repo slot are not impacted.

pkg/behaviourtest/steps/cleanup.go

owners.goAdd Godog step definitions to manage OWNERS/config and assert logs +289/-0

Add Godog step definitions to manage OWNERS/config and assert logs

• Adds steps to commit OWNERS and OWNERS_ALIASES fixtures, enable 'authorization.owners_file', post slash commands, and assert workflow/dispatch logs. Includes polling logic to find issue_comment-triggered dispatch runs and cleanup logic to revert config and clear OWNERS files.

pkg/behaviourtest/steps/owners.go

registry.goRegister OWNERS authorization step definitions +1/-0

Register OWNERS authorization step definitions

• Registers the new OWNERS-related Godog steps so the feature scenarios execute.

pkg/behaviourtest/steps/registry.go

world.goTrack OWNERS auth activation in test world state +5/-0

Track OWNERS auth activation in test world state

• Adds an 'OwnersAuthActivated' flag to the shared scenario state so cleanup can reliably revert OWNERS/config mutations.

pkg/behaviourtest/world/world.go

Documentation (2) +39 / -0
0054-require-authorization-on-all-agent-dispatch-paths.mdDocument OWNERS-file authorization extension in ADR 0054 +15/-0

Document OWNERS-file authorization extension in ADR 0054

• Amends ADR 0054 to describe the new opt-in OWNERS/OWNERS_ALIASES authorization backend, its permission mapping, and the base-SHA pinning rationale. Notes the scope limitation that harness agents are unaffected.

docs/ADRs/0054-require-authorization-on-all-agent-dispatch-paths.md

layered-config-reference.mdAdd 'authorization.owners_file' to layered config reference +24/-0

Add 'authorization.owners_file' to layered config reference

• Documents a new 'authorization' object and the 'owners_file' boolean flag, including an example configuration. Clarifies that this field is consumed by workflow bash/yq rather than the Go config package.

docs/guides/infrastructure/layered-config-reference.md

@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Guide file in infrastructure/ ✗ Dismissed 📜 Skill insight ⌂ Architecture
Description
A modified guide lives at docs/guides/infrastructure/... instead of being placed under either
docs/guides/admin/ or docs/guides/user/, which violates the required guides directory structure.
This makes the guide set inconsistent and harder to navigate/enforce uniformly.
Code

docs/guides/infrastructure/layered-config-reference.md[82]

+| `authorization` | `object` | Replace whole object if set | `nil` |
Relevance

●● Moderate

Infrastructure guides have been modified/accepted before; unclear if admin/user-only structure is
enforced.

PR-#5763

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062077 requires guide files under docs/guides/ to be placed in either the
admin/ or user/ subdirectory. This PR modifies
docs/guides/infrastructure/layered-config-reference.md, which is neither, so it violates the
directory requirement.

docs/guides/infrastructure/layered-config-reference.md[1-15]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`docs/guides/infrastructure/layered-config-reference.md` is a documentation guide but it is not located under either `docs/guides/admin/` or `docs/guides/user/`, which is required.

## Issue Context
This PR modifies the guide, so its placement is in-scope for compliance.

## Fix Focus Areas
- docs/guides/infrastructure/layered-config-reference.md[1-20]
- docs/guides/admin/layered-config-reference.md[1-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. owners.go lacks unit tests 📘 Rule violation ▣ Testability
Description
New Go logic was added in pkg/behaviourtest/steps/owners.go without adding or updating any
corresponding _test.go coverage for the new behaviors. This increases regression risk for the
OWNERS auth behaviour-test helpers.
Code

pkg/behaviourtest/steps/owners.go[R119-122]

+func givenOwnersAuthEnabled(w *world.World) error {
+	cfgPath := ".fullsend/config.yaml"
+	cfgData, err := w.SCM.GetFileContent(context.Background(),
+		w.Install.ConfigOwner(), w.Install.ConfigRepo(), cfgPath)
Relevance

●●● Strong

Repo often accepts adding/updating tests to cover new Go behavior and meet coverage expectations.

PR-#5225

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062049 requires that new or modified Go logic be accompanied by tests updated in
the same change. This PR adds substantial new logic in pkg/behaviourtest/steps/owners.go but does
not add/modify any _test.go files for these new behaviors.

Rule 1062049: Require tests for new or modified Go logic
pkg/behaviourtest/steps/owners.go[1-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New non-test Go logic was added in `pkg/behaviourtest/steps/owners.go` without adding/updating unit tests that exercise it.

## Issue Context
The compliance rule requires tests for new or modified Go logic (typically via `_test.go` files updated in the same change).

## Fix Focus Areas
- pkg/behaviourtest/steps/owners.go[119-205]
- pkg/behaviourtest/steps/owners_test.go[1-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Scaffold dispatch missing OWNERS ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new OWNERS-file authorization logic was added to .github/workflows/reusable-dispatch.yml but
not mirrored into the scaffolded internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml,
creating divergent stage-routing/authorization behavior. This violates the requirement to keep
dispatch workflow routing logic in sync.
Code

.github/workflows/reusable-dispatch.yml[R160-163]

+            # OWNERS-file authorization (opt-in via authorization.owners_file in config.yaml).
+            # Approvers get write-equivalent access; reviewers get triage-equivalent.
+            # Safe: sparse-checkout pins to base branch SHA, so PR authors cannot
+            # self-authorize by adding themselves to OWNERS.
Relevance

●●● Strong

Team has accepted keeping reusable+scaffold dispatch logic in sync via mirrored changes.

PR-#1688

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062045 requires dispatch workflow stage routing and related logic to be kept in
sync between the scaffold dispatch workflow and the reusable dispatch workflow. The reusable
workflow now implements OWNERS-file authorization, while the scaffold workflow’s corresponding
has_repo_permission implementation lacks that logic, so they diverge.

Rule 1062045: Keep jq payload, stage routing, and secret threading logic in dispatch workflows in sync
.github/workflows/reusable-dispatch.yml[131-186]
internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[58-90]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Dispatch stage-routing/authorization logic is now inconsistent between `.github/workflows/reusable-dispatch.yml` and the scaffold workflow `internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml`.

## Issue Context
This PR adds an opt-in OWNERS-file auth path (and sparse-checkout of `OWNERS` files) in the reusable workflow, but the scaffold workflow’s `has_repo_permission` still only uses the collaborator API.

## Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[131-186]
- internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[58-90]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Aliases cleanup flag missing ✓ Resolved 🐞 Bug ☼ Reliability
Description
givenOwnersAliasesFile commits OWNERS_ALIASES but never sets w.OwnersAuthActivated, so using
that step without an OWNERS/config step can skip cleanupOwnersAuth and leave repo state modified
for later scenarios.
Code

pkg/behaviourtest/steps/owners.go[R93-96]

+		[]byte(aliases)); err != nil {
+		return fmt.Errorf("committing OWNERS_ALIASES file: %w", err)
+	}
+	return nil
Relevance

●●● Strong

They’ve accepted ensuring cleanup triggers are set when repo state is mutated to avoid leaks.

PR-#5309

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The alias step commits OWNERS_ALIASES but returns without setting the cleanup flag, while
CleanupScenario only runs OWNERS cleanup when OwnersAuthActivated is true.

pkg/behaviourtest/steps/owners.go[80-97]
pkg/behaviourtest/steps/cleanup.go[90-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`givenOwnersAliasesFile` mutates the repo by committing `OWNERS_ALIASES`, but it does not set `w.OwnersAuthActivated = true`. Cleanup is gated on that flag, so this step is not self-contained and can leave the repository dirty if used alone or reordered.

### Issue Context
Cleanup for OWNERS auth runs only when `w.OwnersAuthActivated` is true.

### Fix Focus Areas
- pkg/behaviourtest/steps/owners.go[80-97]
- pkg/behaviourtest/steps/cleanup.go[90-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
5. OWNERS flag not reset ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
OwnersAuthActivated is scenario state that gates repo-mutating cleanup, but resetScenarioWorld
does not clear it; this makes cleanup behavior depend on template/cloned state and risks stale-state
test flakiness if the template ever becomes non-zero.
Code

pkg/behaviourtest/steps/cleanup.go[R90-93]

+	// --- OWNERS auth cleanup ---
+	if w.OwnersAuthActivated {
+		cleanupOwnersAuth(w)
+	}
Relevance

●●● Strong

Resetting scenario/world fields to prevent stale-state flakiness matches previously accepted fixes.

PR-#4901

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
CleanupScenario newly uses OwnersAuthActivated to decide whether to run cleanupOwnersAuth.
World.Clone() copies scenario fields verbatim and resetScenarioWorld is responsible for clearing
scenario state, but it does not reset this new field (unlike similar flags such as
KillSwitchActivated). This matches a previously-accepted stale-state failure pattern in the
behaviour suite.

pkg/behaviourtest/steps/cleanup.go[90-93]
pkg/behaviourtest/world/world.go[83-100]
pkg/behaviourtest/suite/init.go[81-103]
PR-#4901

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
A new scenario-level boolean (`OwnersAuthActivated`) controls whether cleanup commits repo changes. The suite’s standard scenario reset helper should clear all scenario fields, but it currently does not clear this new flag.

### Issue Context
`World.Clone()` is a shallow copy; scenario fields are copied verbatim and are expected to be zeroed by `resetScenarioWorld`.

### Fix Focus Areas
- pkg/behaviourtest/suite/init.go[81-103]
- pkg/behaviourtest/world/world.go[83-100]
- pkg/behaviourtest/steps/cleanup.go[90-93]
- pkg/behaviourtest/suite/init_test.go[79-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 54 rules

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/guides/infrastructure/layered-config-reference.md Outdated
Comment thread .github/workflows/reusable-dispatch.yml Outdated
Comment thread pkg/behaviourtest/steps/owners.go Outdated
Comment thread pkg/behaviourtest/steps/owners.go Outdated
Comment thread pkg/behaviourtest/steps/cleanup.go
@ralphbean ralphbean self-assigned this Aug 10, 2026
@ralphbean

Copy link
Copy Markdown
Member

Adding more bash is always technical debt (we're on a journey to move much of it into the go binary), but - that said, the cli entrypoint you'd need to use here (imagine - a new fullsend authz check-permission ... command) doesn't exist yet, and adding one significantly expands the scope of this PR. Notably, we'd need to reason about how that should work in the presence of a NormalizedEvent coming from JIRA, rather than from a GH issue or PR.

I think we should file a backlog issue to centralize that in a new subcommand and replace this - meaning that, I think I'm ok with moving forwards with adding more bash here for now. I'd love to hear from others in @fullsend-ai/core before moving forwards though.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-model review (Claude ×2 + Grok, with maintainer verification). Review-only — no approve/request-changes action. Posting the unique MEDIUM-and-above findings inline; qodo's #4 (givenOwnersAliasesFile missing OwnersAuthActivated) and #5 (resetScenarioWorld doesn't reset it) are confirmed and deepened — a stale/true flag makes later scenarios commit empty OWNERS files to their leased slot — so they're not re-posted here. qodo #1 (guide placement) is a false positive: there is no docs/guides/admin/ and the file pre-dates this PR.

The gate itself is sound

The has_repo_permission / _owners_has_user bash was traced by all three reviewers and executed against yq v4 with injection probes: username (^[a-zA-Z0-9-]+$) and alias-entry (^[a-zA-Z0-9_-]+$) are validated before any yq interpolation, key is always a literal, it fails closed on every malformed input, and the ;;& fallthrough correctly gives approvers write+triage while reviewers can never reach write. The inline-bash choice is not itself a defect — @RaphaelBut, your instinct to ask was right, and the answer is that the logic is safe as written.

Must-fix before merge

  • H1 — the pull_request_review checkout ref makes the "cannot self-authorize" invariant false on that path (workflow-level; not fixed by any refactor). One-line pin fix inline.
  • H2 — the OWNERS-auth e2e scenarios exercise the auth path in zero cases and have never run on this fork PR. Inline.
  • M1 — case-sensitive matching silently denies the non-collaborator Prow users this feature targets. Inline.

Direction — bash vs. Go package vs. CEL

This is the real decision, and it needs a core-team call (see @ifireball below). Summary of the analysis:

  • CEL is not reachable for this feature yet. CEL authorization runs host-side in Go, but only in the harness-dispatch job for custom agents, and its env exposes only event (no OWNERS data / API). The built-in stages this PR targets (triage/code/fix/review) are routed by the bash Route job, which is checkout + one bash step and has no fullsend binary. Routing OWNERS auth through CEL would first require migrating built-in stages onto harness-dispatch — the "default agents not on CEL" gap.
  • The Go package is the right home for the logic (parsing, alias resolution, actor→role, policy): typed, unit-testable, and it makes M1 and the M5/typo gap correct by construction. Its durable consumer is the IsAuthorized / normalized-event layer that already runs Go — after the built-in-stages migration.
  • But wiring a fullsend owners-check subcommand into the Route job now would add a binary install/build step to the highest-frequency path (every comment/label/review across all repos), and it's throwaway once the migration lands — the exact cost @RaphaelBut flagged in the PR description.

Recommended sequencing: land this as the reviewed bash interim after H1 + H2 + M1; extract internal/owners/ (logic + tests) as the immediate follow-up so the resolver exists and is reusable; let the built-in-stages→harness-dispatch/CEL migration — not this PR — be what moves the call site into Go. That path fixes the review's real concerns without gating a first contribution behind a core dispatch-path migration.

@ifireball — flagging you as fullsend core / CEL owner

The substantive open question here isn't the bash correctness (verified) but the direction above: whether OWNERS authorization should be built toward the CEL / harness-dispatch authorization path — and whether/when built-in stages migrate onto it — or ship as the interim bash gate and be revisited during that migration. That's your call as CEL feature owner; the recommendation above is a proposal, not a decision. H1 (the security-invariant/checkout-ref fix) also warrants a core-team eye since it touches the dispatch trust boundary.

Comment thread .github/workflows/reusable-dispatch.yml Outdated
Comment thread e2e/behaviour/features/dispatch/owners-auth.feature Outdated
Comment thread .github/workflows/reusable-dispatch.yml Outdated
Comment thread pkg/behaviourtest/steps/owners.go Outdated
Comment thread docs/guides/infrastructure/layered-config-reference.md Outdated

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few notes inline, and replies on a couple of the existing qodo comments. Filed #6072 to track the bash→Go follow-up from the PR description and my earlier comment.

Comment thread docs/guides/infrastructure/layered-config-reference.md Outdated
Comment thread pkg/behaviourtest/steps/owners.go Outdated
Comment thread pkg/behaviourtest/steps/owners.go Outdated
@RaphaelBut
RaphaelBut force-pushed the feat/owners-file-authorization branch from 7621199 to 4c93fe3 Compare August 11, 2026 11:58
@RaphaelBut

Copy link
Copy Markdown
Author

Thanks @waynesun09 and @ralphbean for the thorough review!
I tried to address all the feedback and rebased onto current main to resolve the conflicts.

Highlights beyond what's visible in the diff:

  • Removed the write-denial E2E scenario (was scenario 4). It was vacuous — the bot's COMMENT_USER_TYPE is "Bot" which short-circuits before auth, and the bot has collaborator access so the API fallback always grants. The
    ;;& security invariant (reviewer can't escalate to write) is now covered by string assertions in TestDispatchPerStageAuthorization against both workflow files instead.

  • Went further than yaml.Node on M4 — rather than the bespoke tree walker I originally had, added AuthorizationConfig and SetAuthorizationOwnersFile(bool) to the config package. givenOwnersAuthEnabled and
    cleanupOwnersAuth now follow the exact SetKillSwitch pattern from dispatch.go. This also makes cleanup idempotent and parse-based as requested.

  • M6 (clock skew buffer) resolved structurallywaitForDispatchRunAnyConclusion no longer exists since the scenarios now trigger via issues.opened and use ensureTriageWorkflowComplete, which already handles retries
    with issueOpenDrainSkewBuffer.

  • lc_user instead of mutating username — the lowercased form is used only for OWNERS matching, so the API fallback and ::notice:: logs preserve the original casing.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review-only sweep — no approve/request-changes action taken. Posting one additional finding not already covered by existing review threads; the OWNERS-auth CI-coverage gap I'd flagged is already tracked in more depth by the existing HIGH comment on e2e/behaviour/features/dispatch/owners-auth.feature (skipped as a substantive duplicate).

Comment thread internal/config/interfaces.go Outdated
@RaphaelBut

Copy link
Copy Markdown
Author

im working on the OWNERS-auth CI-coverage gap

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Site preview

Preview: https://a843ed08-site.fullsend-ai.workers.dev

Commit: 946e397402440a6dd30ce82a0edfef7fc7c6ef28

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Re-reviewed at 62efb214: H1 (pull_request_review checkout ref) is fixed in both workflow files, and M1/M3/M4/M5 plus the config-setter finding are all resolved — with scaffold parity and unit tests added on top.

I also verified the OWNERS grant/deny logic executes correctly against the shipped bash locally: approver→write, reviewer→triage-only (no escalation), alias resolution, case-insensitive matching, and injection blocked. The ;;& privilege boundary is now pinned by a running test across both workflow files.

Only remaining item is live behaviour/e2e coverage, which is fail-closed and non-blocking — fine as a follow-up alongside the CI-coverage work you're already on. Nice work for a first contribution.

@ifireball ifireball left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to add an implementation of this to the custom agent dispatch path as well (inside the Go binary)

To @waynesun09 about wither CEL expressions should come into play - wither the hard-wired auth will be replaced by the expressions is an open question (has to do with wither we ever want to allow agent use by less privileged actors). I do not think it is in scope for this.

Comment thread pkg/behaviourtest/steps/owners.go Outdated
Comment thread internal/config/config.go Outdated
// AuthorizationConfig controls opt-in authorization mechanisms that
// extend the default collaborator-API permission check.
type AuthorizationConfig struct {
OwnersFile bool `yaml:"owners_file,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps define this in a more flexible way with:

authorization:
  - provider: native  # what we do now
  - provider: owners_file
  - provider: LDAP   # future speculation
    ldap-url: ...

Where the checks AND all providers, and the default is to use the native.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked a bit at this part and the provider list is definitely the right way to go.
Right now the native collaborator API always runs as a fallback. The owners path is an additional check on top, not a replacement. The boolean flag does match the behavior: "also check owners" rather than "switch to owners instead of native."

It's not pretty but it's honest about what the code does. Happy to rework it to the list schema in this PR if you want, or we can land as-is and move this to #6072 when the Go migration removes the bash side and a second provider makes sense to be added. Let me know which you'd prefer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be happy if we can do the list now, to save us user migration and deprecation headache later. We can define that whatever is in the list is always on top of the native auth if that makes more sense. (I was under the impression that since Prow can do stuff on behalf of the users it can effectively bypass the native auth)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! That makes a lot of sense, I did not think about the migration :D
Added the provider list in 58aa53c and updated documentation accordingly.

For now, native auth is the implicit fallback, not a provider in the list, meaning owners is checked first, if it authorizes the user, the collaborator API is not consulted.
If the user isn't in owners, it falls through to native auth.

  authorization:
    - provider: owners_file

And the default (no list / empty list) is just collaborator API:

  # No authorization list — collaborator API only
  authorization: []

Let me know what you think!

@RaphaelBut

Copy link
Copy Markdown
Author

Thank you so much for all of your reviews <3

Pushed 659e568 to adress review comments and add a denial test, although I am not quite sure if we should go a bit further still with adding more tests around this.

  • Harness dispatch wiring: OWNERS resolution in harnessdispatch.Dispatch so custom agents get the same authorization as built-in stages. harness-dispatch checkout ref pinned to base SHA for pull_request_review events (same gap as H1, introduced when wiring OWNERS into the Go path). Added TestOwnersCheckoutRefPin to prevent recurrence.
  • E2E denial test: outsider-driven scenario proving OWNERS reviewer cannot escalate to write — uses fstest-outsider, ActorLogin filtering, dual assertion (no OWNERS grant + no stage matched)

If you decide this PR is far enough to let the CI run, we can take a look at how my tests break :D
Thank you all anyway!

@ralphbean ralphbean added the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 12, 2026

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review-only sweep (multi-agent, verified against the live PR head and CI). No approve/request-changes action taken. Findings below are all newly introduced or newly-relevant as of the final commit (659e568c) and were checked for overlap with existing review threads before posting; the write-denial CI failure was reproduced live against the exact head SHA.

And the agent will succeed to Prove execution
And the triage workflow logs contain "authorized via OWNERS file (reviewer"

Scenario: OWNERS reviewer is denied write-level access

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL — CI is red on the current head; the write-denial security-invariant test is actually failing

Verified live: the behaviour check on head 659e568c (run 31616854589) has conclusion=failure. The raw job log shows this exact scenario failing with after scenario hook failed: dispatch run logs unexpectedly contain OWNERS authorization — i.e. the E2E run observed the outsider's reviewer-only OWNERS entry being treated as authorized for a write-level command, which is exactly the escalation this scenario exists to disprove.

Three other new OWNERS scenarios also fail (artifact fullsend-triage not found), though those look like they may share a common upstream flake (a harness-agent workflow run failing before producing an artifact) rather than being a second independent bug. This has not been fixed/re-run since; it is the current, unresolved state of the exact head SHA of the open PR.

Suggestion: Do not merge until the behaviour suite is green on this exact head. Root-cause the reviewer-denial failure specifically (check for state leakage between godog scenarios in a shared repo slot) with a clean, isolated repro before re-requesting review.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The root cause was that dummy agents wrote to custom output paths (e.g. output/owners-ok.json)
but the triage validation script expects output/agent-result.json with triage-schema content. In addition the log assertion was matching to widely.

Commit 08c93cc fixed this "fix OWNERS E2E tests -- wrong fixture path and log assertions matching source display"
The CI run after that commit were successful for this failing test.

State leakage is of critical concern, so I added a check for stale config before any scenario starts. See ValidateSlotClean added in commit 8670676

Needs a behaviour CI run on the current head to confirm green.

Comment thread internal/harnessdispatch/core.go
Comment thread .github/workflows/reusable-dispatch.yml Outdated
Comment thread internal/owners/owners.go
w.RepoOwner, w.Install.TriageWorkflowRepo(), w.WorkflowRun.ID)
}

func requireOutsider(w *world.World) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM — OWNERS write-denial E2E scenario has no capability-skip guard; will hard-fail rather than skip when the outsider PAT is unavailable

Verified: requireOutsider() (lines 194-198) returns a hard fmt.Errorf("TEST_ACTOR_OUTSIDER_PAT not set") when the PAT is empty, and the "OWNERS reviewer is denied write-level access" scenario (owners-auth.feature:50-55) that depends on it carries no tag at all. This repo has an established mechanism for exactly this situation — scenarios tagged @requires:capability:<name> are skipped via godog.ErrSkip in SkipErrorForTagNames (pkg/behaviourtest/suite/init.go) when the capability is not declared via BEHAVIOUR_CAPABILITIES (confirmed precedent: @requires:capability:applier-branch-namespace in branch-namespace.feature) — but no outsider-pat capability was ever wired into env.RunnerConfig/HasCapability, and this scenario does not use the tag. If TEST_ACTOR_OUTSIDER_PAT is ever unset in a given CI context, this scenario fails the whole suite instead of skipping cleanly.

Suggestion: Add a @requires:capability:outsider-pat tag to the scenario and extend env.LoadRunnerConfig/HasCapability to report that capability based on TEST_ACTOR_OUTSIDER_PAT presence, consistent with the existing skip pattern.

@RaphaelBut RaphaelBut Aug 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tagging this with requires:capability:outsider-pat would allow the write-denial test to potentially be silently skipped.
IIUC hard-fail is preferred here unless there is a CI guardrail that catches silently-skipped capability scenarios?

Comment thread internal/config/interfaces.go
@waynesun09

Copy link
Copy Markdown
Member

@RaphaelBut rebase is needed to resolve conflicts

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work carrying this through so many rounds of review — the OWNERS/alias resolution logic and the new Go package are solid, and the earlier H1/H2/M1 findings all look genuinely fixed.

One thing I noticed poking around outside the diff: route and harness-dispatch now pin their config checkout to the base SHA for pull_request_review events, but fix (and triage/code/review/retro/prioritize/harness-run) still only pin for pull_request_target. fix is the one stage pull_request_review actually dispatches, so its kill-switch/config checks can still read from the PR head there. It's not part of this diff so I can't point at a line directly, but it'd be worth fixing.

A couple more notes inline.

Comment thread docs/ADRs/0054-require-authorization-on-all-agent-dispatch-paths.md Outdated
Comment thread internal/harnessdispatch/core.go
@RaphaelBut
RaphaelBut force-pushed the feat/owners-file-authorization branch from 659e568 to 75cb622 Compare August 13, 2026 16:57
@github-actions github-actions Bot removed the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 13, 2026
@RaphaelBut

Copy link
Copy Markdown
Author

Thanks again! Rebased on main and addressed all review comments in a single commit. I humbly request another CI run :D

Summary of changes:

Security fixes:

  • Pin all 9 checkout refs for pull_request_review events to base SHA (pre-existing gap, not introduced by this PR)
  • Anchor sparse-checkout patterns to /OWNERS and /OWNERS_ALIASES to avoid pulling nested OWNERS files on Prow-style repos

Test hardening:

  • Switch scenarios 1-3 from the bot to the outsider identity -- authorization now succeeds only through OWNERS, not the collaborator API fallback
  • Add a collaborator-fallthrough scenario (unlisted collaborator authorized via API)
  • Rewrite the denial test to use WaitForWorkflow with the standard retry pattern (was a custom polling loop with a 30s upfront buffer that could match stale runs)
  • Parameterize step definitions by actor (bot|outsider) per ifireball's feedback
  • Avoid mutating the caller's event in Dispatch() -- OWNERS role upgrade is used only for the IsAuthorized gate, not leaked to downstream CEL

Code:

  • Resolve OWNERS via filepath.Dir(ConfigDir) instead of bare working-directory paths
  • Add Dispatch() integration tests for OWNERS role upgrade (per ralphbean)
  • Add AuthorizationOwnersFile no-parent-fallback unit test (per waynesun09)

Docs:

  • ADR 0054: fix stale "harness agents are unaffected" claim
  • Layered config reference: fix footnote, expand authorization section with OWNERS_ALIASES, fail-closed behavior, v1 limitation
  • Workflow contracts: add OWNERS authorization paragraph

Deferred: Provider-list config schema tracked in #6072.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review-only pass (no approve/request-changes action) — one additional MEDIUM finding not covered by the existing threads on this PR.

Comment thread internal/forge/forge.go Outdated

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review-only pass (no approve/request-changes action taken) — 2 findings not covered by existing threads on this PR.

Comment thread docs/contributing/workflow-contracts.md Outdated
Comment thread internal/harnessdispatch/core.go
@ralphbean ralphbean added the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 17, 2026
@github-actions github-actions Bot removed the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 18, 2026
@ralphbean ralphbean assigned ifireball and unassigned ralphbean Aug 19, 2026
@ifireball

Copy link
Copy Markdown
Member

Re-assigning to author to deal with comments.

@ifireball ifireball assigned RaphaelBut and unassigned ifireball Aug 20, 2026
@rh-hemartin rh-hemartin added the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 20, 2026
@github-actions github-actions Bot removed the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Aug 20, 2026
RaphaelBut and others added 11 commits August 20, 2026 18:10
…ash commands

Add an opt-in OWNERS-file authorization path to has_repo_permission in
reusable-dispatch.yml. When authorization.owners_file is set to true in
.fullsend/config.yaml, the dispatch routing checks OWNERS and
OWNERS_ALIASES before falling back to the GitHub collaborator API.

Approvers get write-equivalent access; reviewers get triage-equivalent.
Sparse-checkout pins to the base branch SHA to prevent PR-based
self-authorization. Username and alias entry names are validated before
yq interpolation. Audit notices are emitted on every OWNERS-granted
authorization.

Includes five e2e behaviour scenarios (direct approver, alias
resolution, reviewer triage, reviewer write denial, opt-in gate) and
documentation updates to ADR 0054 and the layered config reference.

Limitations:
- OWNERS auth applies to built-in stages only; harness agents are
  unaffected (they resolve roles via Go code in ghaevent.go).
- OWNERS-authorized users without GitHub write access will use the
  fork PR path via commit.go, requiring /ok-to-test for CI.
- Authorization logic is implemented in bash/yq rather than Go.
  See PR description for discussion and migration path.

Signed-off-by: RaphaelBut <rbut@redhat.com>
…zation

  Pin checkout ref to base SHA for pull_request_review events, closing a
  self-authorization gap. Switch E2E scenarios to issues.opened trigger
  so they exercise has_repo_permission. Add case-insensitive OWNERS
  matching via lc_user without leaking lowercase into the API fallback.
  Mirror OWNERS auth into scaffold dispatch.yml for parity. Replace
  yaml.Node config manipulation with SetAuthorizationOwnersFile on the
  config writer, matching the SetKillSwitch pattern. Add workflow
  alignment assertions for the role-mapping security invariant.

  Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: RaphaelBut <rbut@redhat.com>
…Authorization struct

  SetAuthorizationOwnersFile(false) was niling the entire Authorization
  pointer, which would silently wipe future sibling fields. Now clears
  only OwnersFile and nils the struct only when all fields are zero.

  Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: RaphaelBut <rbut@redhat.com>
…g, and E2E denial test

  Add internal/owners/ package with OWNERS file parser, alias resolver,
  case-insensitive role mapping, and username validation matching the
  bash regex guard (18 unit tests). Wire into harnessdispatch.Dispatch
  to upgrade actor role before authorization when owners_file is enabled.
  Pin harness-dispatch checkout ref to base SHA for pull_request_review
  events and add OWNERS to its sparse-checkout. Add TestOwnersCheckoutRefPin
  to catch future checkout-without-pin bugs. Add AuthorizationOwnersFile()
  accessor to ConfigReader. Simplify behaviour step definitions with
  shared helpers and parameterized role step. Apply clock-skew buffer to
  issue-open trigger timestamp. Add outsider-driven E2E scenario proving
  OWNERS reviewer cannot escalate to write-level access, with ActorLogin
  filtering and dual log-line assertions.

  Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: RaphaelBut <rbut@redhat.com>
…t hardening, docs

Pin all 9 checkout refs for pull_request_review events to base SHA,
closing a pre-existing gap where downstream stage jobs read config
from the PR merge commit instead of the trusted base branch.

Harden E2E tests: switch scenarios 1-3 from the bot to the outsider
identity so authorization succeeds only through OWNERS (not the API
fallback). Add a collaborator-fallthrough scenario proving unlisted
collaborators are still authorized via the API. Rewrite the denial
test to use WaitForWorkflow with the standard retry pattern instead
of a custom polling loop. Parameterize step definitions by actor
(bot|outsider) to eliminate duplicated functions.

Resolve OWNERS relative to filepath.Dir(ConfigDir) instead of bare
working-directory paths. Avoid mutating the callers event -- the
OWNERS-upgraded role is used only for the IsAuthorized gate, not
leaked to downstream CEL evaluation. Anchor sparse-checkout to
/OWNERS and /OWNERS_ALIASES.

Update ADR 0054, layered-config reference, and workflow contracts.
Add Dispatch integration tests and a no-parent-fallback unit test.
Document the v1 flat-schema limitation.

Provider-list config schema deferred to fullsend-ai#6072.

Signed-off-by: RaphaelBut <rbut@redhat.com>
… log assertions matching source display

Dummy agents wrote to custom output paths (e.g. output/owners-ok.json)
but the triage validation script expects output/agent-result.json with
triage-schema content. Log assertions matched "OWNERS file resolved
user" in GitHub Actions' bash source code display, not just runtime
output -- use ##[notice] prefix and expanded parameter values to
distinguish.

Drop the "No stage matched" echo-filtering heuristic from the denial
scenario. The heuristic was brittle and inconsistent with the ##[notice]
approach. The assertion tested a test-environment precondition (outsider
has no collaborator access), not the feature under test (OWNERS reviewer
cannot escalate to write). The ##[notice] negative check alone is
sufficient.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: RaphaelBut <rbut@redhat.com>
…, fix E2E actor

Refactor the authorization config from a boolean field to a provider
list to avoid future migrations:

  authorization:
    - provider: owners_file

The config interfaces (AuthorizationOwnersFile/SetAuthorizationOwnersFile)
keep the same signatures -- only the storage format changes. Native
collaborator-API auth remains implicit and always runs; the list names
additional providers. Includes validation for unknown/duplicate providers.

Fix the two failing OWNERS E2E scenarios ("Unlisted collaborator falls
through to API authorization" and "Triage dispatches without OWNERS path
when not opted in") by switching from the bot actor to the write actor
(TEST_ACTOR_WRITE_PAT). The bot cannot pass has_repo_permission because
GitHub App bots are not collaborators and their [bot] username fails the
OWNERS regex. The write actor has write-level collaborator access but is
not in OWNERS, correctly testing the API fallthrough path.

Also removes the unused ActorLogin field from forge.WorkflowRun and
fixes a stale claim in workflow-contracts.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: RaphaelBut <rbut@redhat.com>
…ness dispatch Options

Signed-off-by: RaphaelBut <rbut@redhat.com>
…cenario

OWNERS auth cleanup failure can silently grant authorization in
unrelated scenarios, causing false positives. Validate repo slot
config is clean before every scenario so stale state fails loudly.

Signed-off-by: RaphaelBut <rbut@redhat.com>
Signed-off-by: RaphaelBut <rbut@redhat.com>
@RaphaelBut
RaphaelBut force-pushed the feat/owners-file-authorization branch from 8670676 to 55666c3 Compare August 20, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Prow/OWNERS file permission model for slash command authorization

5 participants