Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: "1.27.2"
GO_VERSION: "1.27.3"
# Module resolution: relay is the only GrayCodeAI module dependency and it is
# published on the public proxy. Direct-VCS fallback is retained for anything
# else.
Expand All @@ -37,6 +37,20 @@ env:
GOWORK: "off"

jobs:
commit-policy:
name: commit policy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Enforce signed commits and no AI co-authors
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PUSH_BEFORE: ${{ github.event.before }}
PUSH_AFTER: ${{ github.event.after }}
run: bash scripts/check-commit-policy.sh

version:
name: Canonical version metadata
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion internal/features/shellmode/classify.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ var (
cmdCache = make(map[string]bool)
)

// lookPath is exec.LookPath, indirected so tests can make command lookup
// deterministic instead of depending on which binaries the host has installed.
var lookPath = exec.LookPath

// isValidCommand checks if a word exists as a command in PATH (cached).
func isValidCommand(word string) bool {
cmdCacheMu.RLock()
Expand All @@ -167,7 +171,7 @@ func isValidCommand(word string) bool {
return v
}
cmdCacheMu.RUnlock()
_, err := exec.LookPath(word)
_, err := lookPath(word)
result := err == nil
cmdCacheMu.Lock()
cmdCache[word] = result
Expand Down
48 changes: 47 additions & 1 deletion internal/features/shellmode/classify_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,45 @@
package shellmode

import "testing"
import (
"os/exec"
"testing"
)

// stubCommands replaces PATH lookup for the duration of a test, so routing
// results depend only on the input rather than on which binaries the host
// happens to have installed. It also clears the package-level lookup cache so
// earlier tests cannot leak their results into this one.
func stubCommands(t *testing.T, names ...string) {
t.Helper()
allowed := make(map[string]bool, len(names))
for _, name := range names {
allowed[name] = true
}

oldLookPath := lookPath
lookPath = func(name string) (string, error) {
if allowed[name] {
return "/usr/bin/" + name, nil
}
return "", exec.ErrNotFound
}

cmdCacheMu.Lock()
oldCache := cmdCache
cmdCache = make(map[string]bool)
cmdCacheMu.Unlock()

t.Cleanup(func() {
lookPath = oldLookPath
cmdCacheMu.Lock()
cmdCache = oldCache
cmdCacheMu.Unlock()
})
}

func TestClassifyInput(t *testing.T) {
stubCommands(t, "ls", "git", "echo", "find", "kill", "make", "sort")

tests := []struct {
input string
want Classification
Expand Down Expand Up @@ -57,6 +94,15 @@ func TestClassifyInput(t *testing.T) {
// shell-syntax evidence (flags/paths/operators) for these ambiguous
// command names before trusting them as shell.
func TestClassifyInput_AmbiguousWordsWithoutShellSyntax(t *testing.T) {
// Stub every ambiguous verb so this test exercises the shell-syntax
// gate, not whether the host happens to ship these binaries.
stubCommands(
t,
"make", "test", "find", "time", "kill", "man", "more", "less",
"true", "date", "file", "look", "expr", "link", "mount",
"sort", "diff", "patch",
)

sentences := []string{
"make sure this works",
"test the login flow",
Expand Down
80 changes: 80 additions & 0 deletions scripts/check-commit-policy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Enforce the repository commit policy:
# 1. Every commit in the change set has a verified signature.
# 2. No commit credits an AI assistant as a co-author or generator.
#
# Works for pull_request and push events. Requires GH_TOKEN (the workflow's
# GITHUB_TOKEN is enough) and GITHUB_REPOSITORY.
set -euo pipefail

repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
event="${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}"

ai_author_re='^[[:space:]]*co-authored-by:.*(claude|anthropic|openai|chatgpt|gpt|copilot|cursor|gemini|codex|aider|devin|windsurf|tabnine|cody|replit|amazon[ -]?q|jetbrains[ -]?ai|sourcegraph)'
ai_generated_re='generated (with|by).*(claude|anthropic|openai|chatgpt|gpt|copilot|cursor|gemini|codex|aider|devin|windsurf|tabnine|cody|replit|amazon[ -]?q)'

commit_shas() {
case "$event" in
pull_request | pull_request_target)
gh api --paginate "repos/$repo/pulls/${PR_NUMBER:?PR_NUMBER is required}/commits" --jq '.[].sha'
;;
push)
local before="${PUSH_BEFORE:-}" after="${PUSH_AFTER:?PUSH_AFTER is required}"
if [[ -z "$before" || "$before" =~ ^0+$ ]]; then
# New branch: check the pushed head only; its history predates
# this policy.
printf '%s\n' "$after"
else
gh api "repos/$repo/compare/${before}...${after}" --jq '.commits[].sha'
fi
;;
*)
echo "commit policy: unsupported event '$event'" >&2
exit 2
;;
esac
}

failed=0
checked=0

while IFS= read -r sha; do
[[ -z "$sha" ]] && continue
checked=$((checked + 1))

commit_json=$(gh api "repos/$repo/commits/$sha")
verified=$(jq -r '.commit.verification.verified' <<<"$commit_json")
message=$(jq -r '.commit.message' <<<"$commit_json")

if [[ "$verified" != "true" ]]; then
echo "::error::Commit ${sha:0:12} is not signed with a verified key."
failed=1
fi

if grep -Eiq "$ai_author_re" <<<"$message"; then
echo "::error::Commit ${sha:0:12} credits an AI assistant as co-author."
grep -Ein "$ai_author_re" <<<"$message" | sed 's/^/ /'
failed=1
fi

if grep -Eiq "$ai_generated_re" <<<"$message"; then
echo "::error::Commit ${sha:0:12} contains an AI generation attribution."
grep -Ein "$ai_generated_re" <<<"$message" | sed 's/^/ /'
failed=1
fi
done < <(commit_shas)

if [[ "$checked" -eq 0 ]]; then
echo "commit policy: no commits to check"
exit 0
fi

if [[ "$failed" -ne 0 ]]; then
echo
echo "Commit policy failed."
echo " - Sign commits with a key registered on GitHub (git commit -S)."
echo " - Remove AI co-author / generation trailers from the message."
exit 1
fi

echo "commit policy: $checked commit(s) verified, no AI co-author trailers"
Loading