You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On 2026-05-28 an external account OyaAIProd — production account of "Oya.ai / SafeSkill" — opened a drive-by PR in pleme-io/kotoba (#1) adding a "SafeSkill 91/100 Verified Safe" shields.io badge to the README. The PR pattern is:
FIRST_TIME_CONTRIBUTOR, fork created 6 seconds before the PR.
Branch name is a unix timestamp suffix (safeskill-scan-1779933979225) — clearly script-generated.
The "scan" admits in its own info finding it skipped all Rust source (it's a JS/TS scanner).
The "high finding" is a false positive against a URL inside CLAUDE.md documentation.
The badge URL safeskill.dev/scan/pleme-io-kotoba is operator-controlled — they can mutate what it shows at any time (score, redirect, tracking).
Verified blast radius: this account has hit ~100 repos in the past 2 weeks with the same template, targeting projects whose README mentions "MCP server". Several merged.
Sweep org confirmed clean: no other OyaAIProd PRs, no merged safeskill/safeskill.dev references anywhere in pleme-io code
This issue tracks the long-term fix — a typed primitive that makes this class of drive-by impossible to land again.
Destination (the long-term shape)
A typed ExternalContributorPolicy declared in Pangea Ruby, reconciled by the live pangea-operator on the rio cluster (the same controller that already manages repo settings via pleme-io-github-posture).
Yes — one shim per repo, via the existing repo-posture FluxCD pipeline
Branch-protection: restricted-paths
GitHub Branch Protection API — require CODEOWNERS review when a non-allowlisted author touches restricted_paths
Yes — per repo
Gate logic (the action this issue creates)
pull-request-gate is a new tlisp-based action under pleme-io/actions/pull-request-gate/ following the canonical 2-file shape (action.yml + run.tlisp, see argocd-sync/ for the model). Runs on pull_request_target:
pleme-io/actions/pull-request-gate/action.yml — composite action declaring inputs (allowlist, bot-allowlist, restricted-paths, rejection-action), wraps stdlib + run.tlisp via the standard pleme-io/actions/tatara-script@v1 pattern.
pleme-io/actions/pull-request-gate/run.tlisp — gate logic using _tlisp-stdlib/stdlib.tlisp helpers + gh api calls for author lookup, close, lock, label, comment.
pleme-io/actions/pull-request-gate/README.md — usage docs, four behavior branches, examples.
Add to pleme-actions-catalog.lisp (typed catalog entry per the catalog-reflection invariant).
Dogfood: add .github/workflows/external-contributor-gate.yml shim to 3 high-traffic repos — kotoba, caixa-cargo-watch (10 open bot PRs = good load test), hanabi. Verify a week of runs.
Add ExternalContributorPolicy Dry::Struct + (defexternalcontributorpolicy …) Lisp form to pleme-io-github-posture (or whichever pangea-architectures sub-gem owns repo settings — TBD on first read of the gem).
Extend the github-posture reconciler in pangea-operator to:
PUT /orgs/{org}/blocks/{user} for each blocklist entry
PUT the gate workflow file via the existing per-repo posture pipeline (FluxCD-driven git commits to each repo)
PUT the branch-protection restricted-paths rule
Declare :pleme_io_default instance in the org's pangea-architectures workspace.
Roll out fleet-wide: ~120 repos get the workflow shim mechanically.
Phase 3 — BlocklistDriftController (proactive)
Add a typed BlocklistDriftController to pangea-operator that runs weekly:
Sweep gh search prs --author <new-account> against the org (paginated, samba-rate-limited)
Cluster new external accounts by PR template hash (catches future "ScanAndBadgeCorp" before they hit us)
Emit a ntfy alert via the existing observability path when an unknown vendor pattern appears
Status receipts emitted via OutcomeChain (peer of other pangea-operator controllers).
Design choices already made
Choice
Decision
Why
Gate aggressiveness
Strict — auto-close+lock first-time contributors who only touch restricted paths
Kills badge-trojans outright; real typo-fix PRs to README still need to coordinate (acceptable trade-off for a hardened fleet)
Tracking issue home
pleme-io/actions
The action ships here; this issue is the single source of truth for both Phase 1 (action) and Phase 2 (typed primitive in github-posture)
Blocklist storage
In-repo, typed Ruby value reconciled by pangea-operator
Reversible by git revert; auditable via OutcomeChain; reconciled automatically every controller tick
Pillar alignment
Pillar 7 (Kubernetes control) — pangea-operator on rio is the reconciler
Pillar 12 (generation over composition) — one typed declaration generates 120 workflow shims
Context
On 2026-05-28 an external account
OyaAIProd— production account of "Oya.ai / SafeSkill" — opened a drive-by PR inpleme-io/kotoba(#1) adding a "SafeSkill 91/100 Verified Safe" shields.io badge to the README. The PR pattern is:FIRST_TIME_CONTRIBUTOR, fork created 6 seconds before the PR.safeskill-scan-1779933979225) — clearly script-generated.CLAUDE.mddocumentation.safeskill.dev/scan/pleme-io-kotobais operator-controlled — they can mutate what it shows at any time (score, redirect, tracking).Immediate response landed (2026-05-28):
not plannedspamOyaAIProdat thepleme-ioorg levelsafeskill/safeskill.devreferences anywhere inpleme-iocodeThis issue tracks the long-term fix — a typed primitive that makes this class of drive-by impossible to land again.
Destination (the long-term shape)
A typed
ExternalContributorPolicydeclared in Pangea Ruby, reconciled by the livepangea-operatoron the rio cluster (the same controller that already manages repo settings viapleme-io-github-posture).Reconciles to three artifacts
PUT /orgs/{org}/blocks/{user}for each blocklist entry.github/workflows/external-contributor-gate.ymlconsumingpleme-io/actions/pull-request-gate@v1restricted_pathsGate logic (the action this issue creates)
pull-request-gateis a new tlisp-based action underpleme-io/actions/pull-request-gate/following the canonical 2-file shape (action.yml+run.tlisp, seeargocd-sync/for the model). Runs onpull_request_target:The OyaAIProd PR shape —
FIRST_TIME_CONTRIBUTOR+ onlyREADME.md+ one badge line — falls in branch 3 and is auto-rejected without human intervention.Path (the three phases)
Phase 1 — ship
pull-request-gate@v1action (this issue's primary deliverable)pleme-io/actions/pull-request-gate/action.yml— composite action declaring inputs (allowlist,bot-allowlist,restricted-paths,rejection-action), wraps stdlib +run.tlispvia the standardpleme-io/actions/tatara-script@v1pattern.pleme-io/actions/pull-request-gate/run.tlisp— gate logic using_tlisp-stdlib/stdlib.tlisphelpers +gh apicalls for author lookup, close, lock, label, comment.pleme-io/actions/pull-request-gate/README.md— usage docs, four behavior branches, examples.pleme-actions-catalog.lisp(typed catalog entry per the catalog-reflection invariant)..github/workflows/external-contributor-gate.ymlshim to 3 high-traffic repos —kotoba,caixa-cargo-watch(10 open bot PRs = good load test),hanabi. Verify a week of runs.Phase 2 — typed
ExternalContributorPolicyprimitiveExternalContributorPolicyDry::Struct +(defexternalcontributorpolicy …)Lisp form topleme-io-github-posture(or whichever pangea-architectures sub-gem owns repo settings — TBD on first read of the gem).pangea-operatorto:/orgs/{org}/blocks/{user}for each blocklist entry:pleme_io_defaultinstance in the org's pangea-architectures workspace.Phase 3 —
BlocklistDriftController(proactive)BlocklistDriftControllerto pangea-operator that runs weekly:gh search prs --author <new-account>against the org (paginated, samba-rate-limited)OutcomeChain(peer of other pangea-operator controllers).Design choices already made
Pillar alignment
pleme-io-github-posture(existing primitive), does not parallel itReferences
~/.claude/projects/-Users-drzzln-code-github-pleme-io-nix/memory/incident_oyaai_safeskill_drive_by_2026_05_28.mdpleme-io-github-posturepangea-operator-authorpleme-actions(action-authoring conventions)pleme-io-pattern-core(canonical action shape)pleme-io/actions/argocd-sync/pleme-io-auto-releaseskill