Skip to content

feat: Maintainer reaction endorsement for integrity promotion/demotion#3666

Merged
lpcox merged 7 commits intomainfrom
copilot/feature-maintainer-reaction-endorsement
Apr 12, 2026
Merged

feat: Maintainer reaction endorsement for integrity promotion/demotion#3666
lpcox merged 7 commits intomainfrom
copilot/feature-maintainer-reaction-endorsement

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 12, 2026

Adds two new integrity mechanisms to the AllowOnly guard: maintainer 👍/❤️ reactions can promote an item to approved integrity, and maintainer 👎/😕 reactions can cap it at a configured level (default none). Disapproval always overrides endorsement.

Config

{
  "allow-only": {
    "min-integrity": "approved",
    "endorsement-reactions": ["THUMBS_UP", "HEART"],
    "disapproval-reactions": ["THUMBS_DOWN", "CONFUSED"],
    "disapproval-integrity": "none",
    "endorser-min-integrity": "approved"
  }
}
  • endorsement-reactions / disapproval-reactions: ReactionContent enum values. Empty = disabled.
  • disapproval-integrity: integrity cap when disapproval detected. Default none.
  • endorser-min-integrity: minimum collaborator permission for a reactor to count. Default approved (write/maintain/admin).

Precedence (new steps 3 & 4)

blocked_users > author_association > trusted_users > merged > approval_labels > endorsement > disapproval

Rust guard (helpers.rs, lib.rs)

  • 4 new PolicyContext fields
  • has_maintainer_reaction_with_callback(): extracts reactions.nodes[]{user.login, content} (GraphQL proxy shape), checks up to 20 reactions, resolves reactor integrity via get_collaborator_permission
  • apply_endorsement_promotion() / apply_disapproval_demotion() wired as steps 3/4 in issue_integrity() and pr_integrity()
  • cap_integrity(): min-of-two for demotion (inverse of max_integrity)
  • AllowOnlyPolicy deserialization extended for the 4 new fields

Gateway-mode degradation

When reactions field is present but has no nodes array (MCP server returns counts only, not per-user data), each reaction kind logs a warning once per process lifetime and skips evaluation entirely — no promotion or demotion occurs.

Backend (backend.rs)

  • Per-request cache for get_collaborator_permission keyed owner/repo:username (case-insensitive) to bound API calls when multiple items share the same reactor

Go config (guard_policy.go, wasm.go)

  • AllowOnlyPolicy + NormalizedGuardPolicy structs extended with 4 new fields
  • NormalizeGuardPolicy: uppercases reaction content values, deduplicates, validates integrity level strings
  • buildStrictLabelAgentPayload: allows and validates the 4 new allow-only keys

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build2564705994/b514/launcher.test /tmp/go-build2564705994/b514/launcher.test -test.testlogfile=/tmp/go-build2564705994/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2564705994/b422/vet.cfg (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2193583002/b001/config.test /tmp/go-build2193583002/b001/config.test -test.testlogfile=/tmp/go-build2193583002/b001/testlog.txt -test.paniconexit0 -test.timeout=10m0s aw-m�� go aw-mcpg/guards/g--64 x_amd64/asm aw-mcpg/guards/g/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet aw-mcpg/guards/g-unsafeptr=false aw-mcpg/guards/g-unreachable=false x_amd64/asm aw-m�� g_.a Lj2wtMmGc x_amd64/compile k/gh-aw-mcpg/gh-/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet (dns block)
    • Triggering command: /tmp/go-build2936037720/b001/config.test /tmp/go-build2936037720/b001/config.test -test.testlogfile=/tmp/go-build2936037720/b001/testlog.txt -test.paniconexit0 -test.timeout=10m0s --uid-owner 0 -j ACCEPT /home/REDACTED/wor/usr/lib/sysstat/sadc /home/REDACTED/wor-F /home/REDACTED/wor-L x_amd64/vet go_.�� 64/src/net ache/go/1.25.8/x1 64/pkg/tool/linu/var/log/sysstat -p internal/runtime-w -lang=go1.25 64/pkg/tool/linusecurity (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build2564705994/b514/launcher.test /tmp/go-build2564705994/b514/launcher.test -test.testlogfile=/tmp/go-build2564705994/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2564705994/b422/vet.cfg (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build2564705994/b514/launcher.test /tmp/go-build2564705994/b514/launcher.test -test.testlogfile=/tmp/go-build2564705994/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2564705994/b422/vet.cfg (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2564705994/b523/mcp.test /tmp/go-build2564705994/b523/mcp.test -test.testlogfile=/tmp/go-build2564705994/b523/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2564705994/b446/vet.cfg g_.a /home/REDACTED/go/pkg/mod/github.c-nolocalimports x_amd64/vet 64/src/runtime/c/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet json x_amd64/asm x_amd64/vet --no�� 1.80.0/grpclog/c-errorsas 1.80.0/grpclog/g-ifaceassert x_amd64/vet 64/src/runtime/c/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet k/gh-aw-mcpg/gh--atomic x_amd64/compile x_amd64/vet (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI and others added 2 commits April 12, 2026 19:04
- Add endorsement_reactions, disapproval_reactions, disapproval_integrity,
  endorser_min_integrity fields to PolicyContext in Rust guard
- Implement has_maintainer_reaction_with_callback() core evaluation function
  with gateway-mode graceful degradation (log once, skip safely)
- Add has_maintainer_endorsement() and has_maintainer_disapproval() functions
- Add apply_endorsement_promotion() and apply_disapproval_demotion() functions
- Wire endorsement (step 3) and disapproval (step 4, always wins) into
  issue_integrity() and pr_integrity() after approval-labels promotion
- Add cap_integrity() helper for disapproval demotion (min of current and cap)
- Add collaborator permission caching in backend.rs (keyed owner/repo:user)
- Update AllowOnlyPolicy in lib.rs to deserialize new fields
- Update Go config AllowOnlyPolicy and NormalizedGuardPolicy structs
- Update JSON marshal/unmarshal with new field support
- Update NormalizeGuardPolicy with validation for reaction fields
- Update wasm.go buildStrictLabelAgentPayload to allow new allow-only keys
- Add comprehensive unit tests (Rust and Go) for all new functionality

Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/1cd99fee-78d0-43bd-bc83-1d39e17d9de3

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
- Use separate AtomicBool flags for endorsement vs disapproval gateway warnings
  so each reaction kind logs its own warning at most once per process lifetime
- Add warning log for unrecognised integrity level values in integrity_level_rank()
  (defaults to 'approved' per spec, now with explicit warning)
- Document cache key username lowercasing behavior in get_collaborator_permission_with_callback

Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/1cd99fee-78d0-43bd-bc83-1d39e17d9de3

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Add maintainer reaction endorsement for integrity promotion feat: Maintainer reaction endorsement for integrity promotion/demotion Apr 12, 2026
Copilot AI requested a review from lpcox April 12, 2026 19:10
@lpcox lpcox marked this pull request as ready for review April 12, 2026 19:22
Copilot AI review requested due to automatic review settings April 12, 2026 19:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds reaction-based maintainer endorsement/disapproval signals to the GitHub guard’s integrity evaluation, with corresponding policy/config plumbing and validation across the Go gateway and Rust WASM guard.

Changes:

  • Extend allow-only policy schema to support endorsement/disapproval reaction lists and integrity thresholds/caps.
  • Implement reaction evaluation in the Rust guard (promotion to approved; demotion cap with disapproval override) with gateway-mode degradation behavior.
  • Add Go-side normalization/validation and expand unit tests for the new policy fields and behavior.
Show a summary per file
File Description
internal/guard/wasm.go Allows/validates the new allow-only keys in the strict payload builder.
internal/guard/wasm_test.go Adds payload-shape tests for new reaction + integrity config fields.
internal/config/guard_policy.go Extends policy structs + JSON (un)marshal; normalizes reaction lists and validates new integrity fields.
internal/config/guard_policy_test.go Adds normalization + round-trip tests for the new allow-only fields.
guards/github-guard/rust-guard/src/lib.rs Wires new allow-only fields into runtime PolicyContext.
guards/github-guard/rust-guard/src/labels/mod.rs Adds Rust tests covering reaction-related integrity edge cases/degradation.
guards/github-guard/rust-guard/src/labels/helpers.rs Implements reaction-based endorsement/disapproval evaluation and integrity promotion/demotion helpers.
guards/github-guard/rust-guard/src/labels/backend.rs Adds caching for collaborator permission lookups to reduce repeated backend calls.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 8/8 changed files
  • Comments generated: 3

Comment on lines +27 to +33
/// Cache for collaborator permission lookups keyed by "owner/repo:username".
/// Caches the raw permission string so it can be reused across multiple items
/// that share the same reactor within a single gateway request.
fn collaborator_permission_cache() -> &'static Mutex<HashMap<String, Option<String>>> {
static CACHE: OnceLock<Mutex<HashMap<String, Option<String>>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

The collaborator-permission cache is implemented as a process-wide static (OnceLock + Mutex) and is never cleared/evicted, but the comment states it is per-request. This can lead to unbounded growth (unique user/repo combos) and stale permission decisions affecting integrity promotion/demotion; consider scoping this cache to a single gateway request (or adding TTL/LRU + explicit reset at request boundaries) and updating the comment accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +490 to +505
// Cache key uses lowercase username because GitHub usernames are case-insensitive.
// The original case-sensitive username is preserved in the returned CollaboratorPermission
// struct (via `username.to_string()`) so callers see the canonical display form.
let cache_key = format!("{}/{}:{}", owner, repo, username.to_ascii_lowercase());

// Return cached permission if available.
if let Some(cached) = get_cached_collaborator_permission(&cache_key) {
crate::log_debug(&format!(
"get_collaborator_permission: cache hit for {}/{} user {} → permission={:?}",
owner, repo, username, cached
));
return cached.map(|permission| CollaboratorPermission {
permission: Some(permission),
login: Some(username.to_string()),
});
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

The cache key only normalizes username casing; owner/repo are also case-insensitive on GitHub, so differing case in inputs will reduce cache hits and increase cache size. Consider normalizing owner/repo to a canonical form (e.g., lowercasing) when building the cache key.

Copilot uses AI. Check for mistakes.
Comment on lines +370 to +384
/// Convert an integrity level name to its rank (0 = unknown, 1 = none, 2 = reader, 3 = writer, 4 = merged).
fn integrity_level_rank(level: &str) -> u8 {
match level.trim().to_ascii_lowercase().as_str() {
"none" => 1,
"unapproved" => 2,
"approved" => 3,
"merged" => 4,
other => {
crate::log_warn(&format!(
"integrity_level_rank: unrecognised level {:?}, defaulting to 'approved'",
other
));
3 // unrecognised → safe default is "approved" (matches endorser_min_integrity default)
}
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

The doc comment for integrity_level_rank says rank 0 represents "unknown", but the implementation never returns 0 and instead defaults unrecognized levels to rank 3 (approved). Update the comment to match the actual behavior (or adjust the function) to avoid misleading future changes.

Copilot uses AI. Check for mistakes.
lpcox and others added 4 commits April 12, 2026 12:36
…ring

- Update collaborator permission cache comment to accurately describe
  it as process-wide static (not per-request).
- Normalize owner/repo to lowercase in cache keys since GitHub treats
  them as case-insensitive, improving cache hit rate.
- Fix integrity_level_rank docstring: remove incorrect '0 = unknown'
  and 'reader/writer' terminology; document actual return values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The function was defined in helpers.rs tests but never called (the
equivalent helper in mod.rs tests is the one actually used). Fixes
rust-guard-test CI failure: -D dead-code triggered by RUSTFLAGS.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tests

- Add critical test: disapproval overrides endorsement on same item
  (helpers.rs) verifying the precedence rule via mock callbacks
- Add tests for disapproval with admin/read permission thresholds
- Fix misleading Go comments claiming defaults are set by normalization
  (defaults are applied in Rust, not Go)
- Add Go tests: empty disapproval-reactions entry rejected,
  disapproval-reactions deduplication

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The test_has_maintainer_reaction_backend_error_skips test was flaky
because it used login 'alice' which could be cached by prior tests
using admin_permission_callback. The global permission cache returned
the cached 'admin' permission instead of invoking error_callback.

Use 'error-test-user' to guarantee a cache miss.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lpcox lpcox merged commit 9e91827 into main Apr 12, 2026
14 checks passed
@lpcox lpcox deleted the copilot/feature-maintainer-reaction-endorsement branch April 12, 2026 20:17
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.

Feature: Maintainer reaction endorsement for integrity promotion

3 participants