Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
37 changes: 29 additions & 8 deletions .agents/agent-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
base_branch: main
repo_prefix: "AW"
follow_up_prefix: "Follow-up:"
review_gate: "independent code review for non-trivial workflow or helper changes"
review_gate: "current-head approval from a human collaborator; administrator code-owner approval for executable or agent-instruction surfaces"
automation_reviewers: "claude-review (GitHub Action; in-flight and terminal state observable via the checks API) and coderabbitai (advisory; may fail on rate limits)"
approval_exempt: "docs, workflow text, helper scripts, skill metadata, and validation fixtures when the change remains portable"
approval_exempt: "ordinary root documentation and validation fixtures only; agent instructions, executable helpers, distribution metadata, and CI workflows are never exempt"
# This source repo runs in single-operator mode; consumer repos pick their own
# coordination backend in their local seam.
coordination_backend: "n/a"
Expand All @@ -22,15 +22,36 @@ autonomous_merge:
max_commits: 9
max_reviewed_heads: 3
human_review_paths:
- id: github-workflows
pattern: ".github/workflows/**"
- id: repository-agent-policy
pattern: "AGENTS.md"
reason: security
- id: repository-agent-contract
pattern: ".agents/**"
reason: security
- id: claude-plugin-metadata
pattern: ".claude-plugin/**"
reason: security
- id: codex-plugin-metadata
pattern: ".codex-plugin/**"
reason: security
- id: github-policy-and-workflows
pattern: ".github/**"
reason: infrastructure
- id: distribution-tools
pattern: "bin/*agent-workflows"
reason: release
- id: downstream-publishing
pattern: "bin/push-downstream"
pattern: "bin/**"
reason: release
- id: agent-skills
pattern: "skills/**"
reason: security
- id: fallback-workflows
pattern: "workflows/**"
reason: security
- id: shell-scripts
pattern: "**/*.sh"
reason: security
- id: bash-scripts
pattern: "**/*.bash"
reason: security
policy_paths:
- "skills/pr-batch/bin/autonomous-merge-*"
- "skills/pr-batch/lib/autonomous_merge_*"
Expand Down
12 changes: 12 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Executable helpers and agent instructions are a security boundary. Require an
# administrator code owner in addition to the exact-head human review gate.
/AGENTS.md @shakacode/admins
/.agents/ @shakacode/admins
/.claude-plugin/ @shakacode/admins
/.codex-plugin/ @shakacode/admins
/.github/ @shakacode/admins
/bin/ @shakacode/admins
/skills/ @shakacode/admins
/workflows/ @shakacode/admins
**/*.sh @shakacode/admins
**/*.bash @shakacode/admins
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: github-actions
Comment thread
justin808 marked this conversation as resolved.
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
4 changes: 2 additions & 2 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
Comment thread
justin808 marked this conversation as resolved.
with:
fetch-depth: 1

- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
with:
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/human-security-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Human Security Review

on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
# Review events from public forks deliberately receive a read-only token.
# Re-evaluate open PRs from trusted default-branch code instead.
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:

permissions:
contents: read
pull-requests: read
statuses: write

concurrency:
group: human-security-review
cancel-in-progress: false

jobs:
gate:
name: Exact-head human approval
runs-on: ubuntu-latest
steps:
# The helper and workflow come from the trusted base. Never check out or
# execute the pull request branch in this privileged event context.
- name: Checkout trusted base
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.event.pull_request.base.sha || github.sha }}
persist-credentials: false

- name: Require current-head human approval for execution surfaces
env:
GH_TOKEN: ${{ github.token }}
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

pairs="$RUNNER_TEMP/human-security-review-pairs"
if [[ -n "$EVENT_PR_NUMBER" ]]; then
printf '%s %s\n' "$EVENT_PR_NUMBER" "$EVENT_HEAD_SHA" > "$pairs"
else
gh api --paginate \
"repos/$GITHUB_REPOSITORY/pulls?state=open&per_page=100" \
--jq '.[] | "\(.number) \(.head.sha)"' > "$pairs"
fi

infrastructure_error=0
while read -r PR_NUMBER HEAD_SHA; do
[[ -z "$PR_NUMBER" ]] && continue
if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || [[ ! "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid pull request identity" >&2
infrastructure_error=1
continue
fi

set +e
bin/human-security-review-gate \
--repo "$GITHUB_REPOSITORY" \
--pr "$PR_NUMBER" \
--expected-head "$HEAD_SHA" \
>"$RUNNER_TEMP/human-security-review-$PR_NUMBER.out" 2>&1
gate_status=$?
set -e
cat "$RUNNER_TEMP/human-security-review-$PR_NUMBER.out"

case "$gate_status" in
0)
state=success
description="Current-head human review satisfied or not required"
;;
1)
state=failure
description="Current-head human approval is required"
;;
*)
state=error
description="Human security review could not be verified"
infrastructure_error=1
;;
esac

gh api --method POST \
"repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \
-f state="$state" \
-f context=human-security-review/exact-head \
Comment thread
justin808 marked this conversation as resolved.
Outdated
-f description="$description" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
done < "$pairs"

exit "$infrastructure_error"
6 changes: 3 additions & 3 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.4"

Expand All @@ -25,7 +25,7 @@ jobs:
run: echo "dir=$(gem env gemdir)" >> "$GITHUB_OUTPUT"

- name: Cache RuboCop gems
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ${{ steps.gem-cache.outputs.dir }}
key: ${{ runner.os }}-ruby-3.4-rubocop-${{ hashFiles('.rubocop-version') }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to this portable workflow pack are documented here.

#### Added

- **Require exact-current-head human approval for executable helpers, agent instructions, plugin metadata, and repository automation; protect renames and policy files, assign administrator code owners, route the same surfaces out of autonomous merge, pin every GitHub Action to a full commit SHA, and document the branch/ruleset controls that make the boundary enforceable.**
- **Add machine-enforced batch-plan preflight and merge-assurance receipt gates, including scoped exact-head CI evidence and a historical React on Rails Wave A fixture replay that proves an unsafe plan is rejected before dispatch.**
- **Add `$pr-walkthrough`, a read-only exact-diff PR tour that builds a complete conceptual change map, explains one change and its rationale at a time, pauses for questions before continuing, adapts depth for large or complex PRs, tracks diff coverage, and keeps understanding separate from review or merge approval.**
- **Add a portable, fail-closed `qa-evidence v2` gate for user-visible UI changes: durable reviewer-visible before/after links, non-blank paint inspection, interaction clips or measured substitutes, unfixed negative controls for visual fixes, and repo-seam performance evidence that distinguishes bundle hygiene from a measured metric. GitHub-only runs use an authenticated GitHub UI uploader when available and otherwise remain explicitly blocked on human attachment; configured trackers such as Linear or repo artifact stores can host evidence when every reviewer has access. Historical `qa-evidence v1` receipts remain replayable, while current UI audits can require v2 explicitly.** [issue 261](https://github.com/shakacode/agent-workflows/issues/261).
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ default.
- Installer, status, upgrade, trust-audit, and seam-doctor helpers under `bin/`.
- Security preflight for public issue and PR batches so untrusted GitHub text
cannot quietly become agent instructions.
- An exact-head human approval boundary for executable helpers, agent
instructions, plugin metadata, and repository automation; automated reviews
remain advisory. See
[Repository Supply-Chain Policy](docs/repository-supply-chain.md).
- Site-ready Markdown docs under the
[ShakaCode Agent Workflow Playbook](docs/README.md).

Expand Down
136 changes: 136 additions & 0 deletions bin/human-security-review-gate
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "json"
require "open3"
require "optparse"

module HumanSecurityReviewGate
class InfrastructureError < StandardError; end

HIGH_RISK_PATHS = [
%r{\A(?:\.agents|\.claude-plugin|\.codex-plugin)/},
%r{\A\.github/},
/\AAGENTS\.md\z/,
%r{\Abin/},
%r{\Askills/},
%r{\Aworkflows/},
/\.(?:bash|sh)\z/
].freeze
REVIEW_PERMISSIONS = %w[admin maintain write].freeze
DECISIVE_REVIEW_STATES = %w[APPROVED CHANGES_REQUESTED DISMISSED].freeze
PAGE_SIZE = 100
MAX_PAGES = 30

module_function

def gh_json(executable, endpoint)
stdout, stderr, status = Open3.capture3(executable, "api", endpoint)
return JSON.parse(stdout) if status.success?

raise InfrastructureError, "GitHub API failed for #{endpoint}: #{stderr.strip}"
rescue JSON::ParserError => e
raise InfrastructureError, "invalid GitHub API response for #{endpoint}: #{e.message}"
end

def high_risk_path?(path)
HIGH_RISK_PATHS.any? { |pattern| pattern.match?(path) }
end

def gh_paginated_json(executable, endpoint)
records = []
1.upto(MAX_PAGES) do |page|
separator = endpoint.include?("?") ? "&" : "?"
batch = gh_json(executable, "#{endpoint}#{separator}per_page=#{PAGE_SIZE}&page=#{page}")
raise InfrastructureError, "expected an array from #{endpoint}" unless batch.is_a?(Array)

records.concat(batch)
return records if batch.length < PAGE_SIZE
end

raise InfrastructureError, "GitHub API coverage exceeded #{MAX_PAGES} pages for #{endpoint}"
end

def run(argv)
options = {}
OptionParser.new do |parser|
parser.on("--repo OWNER/REPO") { |value| options[:repo] = value }
parser.on("--pr NUMBER", Integer) { |value| options[:pr] = value }
parser.on("--expected-head SHA") { |value| options[:expected_head] = value }
end.parse!(argv)

repo = options[:repo]
number = options[:pr]
raise OptionParser::MissingArgument, "--repo" if repo.nil?
raise OptionParser::MissingArgument, "--pr" if number.nil?
raise OptionParser::InvalidArgument, "--repo must be OWNER/REPO" unless repo.match?(%r{\A[^/\s]+/[^/\s]+\z})
raise OptionParser::InvalidArgument, "--pr must be positive" unless number.positive?

gh = ENV.fetch("AGENT_WORKFLOWS_GH_EXECUTABLE", "gh")
pull_request = gh_json(gh, "repos/#{repo}/pulls/#{number}")
head_sha = pull_request.dig("head", "sha")
author = pull_request.dig("user", "login")
unless head_sha.is_a?(String) && head_sha.match?(/\A[0-9a-f]{40}\z/) &&
author.is_a?(String) && !author.empty?
raise InfrastructureError, "pull request identity is missing or malformed"
end

expected_head = options[:expected_head]
if expected_head && !expected_head.match?(/\A[0-9a-f]{40}\z/)
raise OptionParser::InvalidArgument, "--expected-head must be a full commit SHA"
end

if expected_head && expected_head != head_sha
raise InfrastructureError, "pull request head changed: expected #{expected_head}, found #{head_sha}"
end

files = gh_paginated_json(gh, "repos/#{repo}/pulls/#{number}/files")
changed_paths = files.flat_map do |file|
[file.fetch("filename"), file["previous_filename"]]
end
risky_paths = changed_paths.compact.select { |path| high_risk_path?(path) }.uniq.sort

if risky_paths.empty?
puts "HUMAN_SECURITY_REVIEW_NOT_REQUIRED head=#{head_sha}"
return 0
end

reviews = gh_paginated_json(gh, "repos/#{repo}/pulls/#{number}/reviews")
current_reviews = reviews.select do |review|
review["commit_id"] == head_sha &&
DECISIVE_REVIEW_STATES.include?(review.fetch("state", "").upcase)
end
latest_reviews = current_reviews.group_by { |review| review.dig("user", "login").to_s.downcase }.values.map do |reviewer_reviews|
reviewer_reviews.max_by do |review|
[review.fetch("submitted_at", "").to_s, review.fetch("id", 0).to_i]
end
end
approved_reviewer = latest_reviews.find do |review|
reviewer = review.dig("user", "login")
next false unless review.fetch("state", "").upcase == "APPROVED"
next false unless review.dig("user", "type") == "User"
next false if reviewer.nil? || reviewer.casecmp?(author) || reviewer.downcase.end_with?("[bot]")

permission = gh_json(gh, "repos/#{repo}/collaborators/#{reviewer}/permission").fetch("permission")
REVIEW_PERMISSIONS.include?(permission)
end

if approved_reviewer
reviewer = approved_reviewer.dig("user", "login")
puts "HUMAN_SECURITY_REVIEW_OK reviewer=@#{reviewer} head=#{head_sha}"
return 0
end

warn "HUMAN_SECURITY_REVIEW_REQUIRED head=#{head_sha}"
risky_paths.each { |path| warn "- #{path}" }
1
rescue InfrastructureError, KeyError => e
warn "HUMAN_SECURITY_REVIEW_ERROR #{e.message}"
70
rescue OptionParser::ParseError => e
warn "HUMAN_SECURITY_REVIEW_ERROR #{e.message}"
64
end
end

exit HumanSecurityReviewGate.run(ARGV) if $PROGRAM_NAME == __FILE__
Loading
Loading