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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "4.4.25",
"version": "4.4.26",
"source": "./plugins/agentic-engineering"
},
{
Expand Down
2 changes: 1 addition & 1 deletion .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "4.4.25",
"version": "4.4.26",
"source": "./plugins/agentic-engineering"
},
{
Expand Down
2 changes: 1 addition & 1 deletion plugins/agentic-engineering/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "4.4.25",
"version": "4.4.26",
"author": {
"name": "devantler-tech",
"url": "https://github.com/devantler-tech"
Expand Down
2 changes: 1 addition & 1 deletion plugins/agentic-engineering/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "4.4.25",
"version": "4.4.26",
"author": {
"name": "devantler-tech",
"url": "https://github.com/devantler-tech"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
{
"path": "scripts/forge-readonly-guard.sh",
"sha256": "2e0754b6e003e739ced37b27bf35f3afd6caed2a257afdc535f9d04948525e83",
"sha256": "9591835448efc0d8f08f83da48deacc9a50079f4df79e9db7ed91d9146dd3649",
"executable": true
},
{
Expand Down
54 changes: 53 additions & 1 deletion plugins/agentic-engineering/scripts/forge-readonly-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,28 @@ GH_VERB_VALUE_FLAGS=" -R --repo --state --limit -L --json --jq -q --search --aut
# independent rules that fire earlier, and neither is widened by an explicit GET.
GH_API_FIELD_FLAGS=" -f --raw-field -F --field --input "

# Value grammars that legitimately begin with a dash. A value-taking flag consumes the
# next word whatever it looks like — gh's flag parser and git's alike — so `--repo --web`
# never reaches gh as a flag. That inertness lives in a parser the guard does not assert,
# and it stops holding the day any value flag grows an optional-value grammar. So the
# guard classifies the consumed word itself (check_consumed_value): a flag-shaped value
# is denied by name unless its flag is listed here, where a leading dash is part of the
# value's own grammar and the value can name no program, host, or file:
# --search a server-side query expression (`-label:foo` negates a qualifier)
# --label, --milestone free-form server-side filter strings — a label or milestone
# title may itself begin with a dash (`--label -bug`)
# --jq / -q a jq program (`-1`, `-(.x)`) run by gh's embedded jq; its reach into
# the process environment is refused separately by check_gh_flag_value
# --template/-t a Go template rendered locally, with no exec or file primitive
# git --grep, -S, -G, --author, --committer a regex or pickaxe string matched
# against history (`--author -bot` matches authors containing `-bot`)
# git --since, --until, --after, --before a date expression, which git's
# approxidate parser accepts in free form (`--since -yesterday`)
# Everything else — a count, a path, a ref, a format name — has a grammar no
# dash-letter word can satisfy, so a flag-shaped value there is denied by name.
GH_DASH_VALUE_FLAGS=" --search --label --milestone --jq -q --template -t "
GIT_DASH_VALUE_FLAGS=" --grep -S -G --author --committer --since --until --after --before "

# git options. Read verbs are not enough on their own: several options make git
# write a file or execute a program without any shell syntax for the scanner to
# catch, so options are allowlisted like everything else.
Expand Down Expand Up @@ -743,6 +765,7 @@ classify_gh_api() {
if [ "$FLAG_HAS_VALUE" -eq 0 ]; then
if [ $((i + 1)) -ge "$n" ]; then deny "gh api $name needs a value"; fi
val=${WORDS[$((i + 1))]}
check_consumed_value 'gh api' "$GH_DASH_VALUE_FLAGS" "$name" "$val"
i=$((i + 1))
fi

Expand Down Expand Up @@ -849,6 +872,29 @@ check_gh_flag_value() {
esac
}

# A word consumed as a flag's value that is itself flag-shaped. Only a flag whose value
# grammar begins with a dash ($2 names that family's set) may take it; for every other
# flag the guard would be handing a flag it never classified to a parser it does not
# assert. Denied by name, so the message points at the word to remove.
#
# Flag-shaped means a dash followed by a letter or a second dash. A dash followed by a
# digit is a NUMBER, never a flag — `--max-count -1` (unlimited), `-n -1`, and the
# relative date `--since -1.day` are all documented, read-only git grammars — so it is
# admitted for every value flag: no flag's name starts with a digit, so nothing the
# guard classifies can hide behind one.
check_consumed_value() {
local prog=$1 dash_flags=$2 name=$3 val=$4
case "$val" in
-[0-9]*) ;;
-?*)
case "$dash_flags" in
*" $name "*) ;;
*) deny "$prog $name consumed '$val' as its value, but that word is flag-shaped and $name takes no dash-leading value; the guard classifies it as a flag rather than trusting the parser to keep it inert" ;;
esac
;;
esac
}

check_gh_verb_flags() {
local i=1 w name val
local n=${#WORDS[@]}
Expand Down Expand Up @@ -903,6 +949,7 @@ check_gh_verb_flags() {
fi
if [ $((i + 1)) -ge "$n" ]; then deny "gh $name needs a value"; fi
val=${WORDS[$((i + 1))]}
check_consumed_value gh "$GH_DASH_VALUE_FLAGS" "$name" "$val"
check_gh_flag_value "$name" "$val"
i=$((i + 1))
;;
Expand Down Expand Up @@ -1192,7 +1239,12 @@ classify_git() {

if [ "$FLAG_HAS_VALUE" -eq 0 ]; then
case "$GIT_OK_VALUE_FLAGS" in
*" $name "*) i=$((i + 1)) ;;
*" $name "*)
if [ $((i + 1)) -lt "${#WORDS[@]}" ]; then
check_consumed_value git "$GIT_DASH_VALUE_FLAGS" "$name" "${WORDS[$((i + 1))]}"
fi
i=$((i + 1))
;;
esac
fi
i=$((i + 1))
Expand Down
51 changes: 51 additions & 0 deletions plugins/agentic-engineering/scripts/forge-readonly-guard.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,57 @@ expect_deny 'trailing --json does not license redirection' "gh pr list --json >
expect_deny 'a different value flag left bare is still denied' "gh pr list --repo"
expect_deny 'trailing --jq left bare is still denied' "gh pr list --json number --jq"
expect_deny 'the probe does not make a mutation verb readable' "gh pr merge --json"

# A value-taking flag must not swallow a flag-shaped word (#181). gh's parser does
# consume the next word as the value whatever it looks like, so `--repo --web` never
# opens a browser — but that inertness belongs to a downstream parser the guard does
# not assert. The guard classifies the word itself: consumed by a flag whose value
# grammar cannot begin with a dash, a flag-shaped word is denied by name.
expect_deny_names 'a value flag does not swallow --web (pr list --repo)' \
"gh pr list --repo --web" "--web"
expect_deny_names 'a value flag does not swallow --web (pr list --state)' \
"gh pr list --state --web" "--web"
expect_deny_names 'a value flag does not swallow --web (pr view --limit)' \
"gh pr view 1 --limit --web" "--web"
expect_deny_names 'gh api --method does not swallow --web' \
"gh api repos/devantler-tech/monorepo/pulls --method --web" "--web"
expect_deny_names 'gh api -H does not swallow --hostname' \
"gh api repos/devantler-tech/monorepo/pulls -H --hostname" "--hostname"
expect_deny_names 'git --max-count does not swallow --work-tree' \
"git log --oneline --max-count --work-tree" "--work-tree"
# The rule is per flag family, not a blanket ban on dash-leading values: a search
# expression, a jq program and a git grep pattern legitimately begin with a dash, and
# none of them can name a program, a host, or a file.
expect_allow 'a --search expression may begin with a dash' \
"gh pr list --repo devantler-tech/platform --search -label:blocked --limit 10"
expect_allow 'a --jq program may begin with a dash' \
"gh api repos/devantler-tech/platform/pulls --jq -1"
# A label or milestone title is a free-form server-side filter and may itself begin
# with a dash; neither can name a program, a host, or a file.
expect_allow 'a --label filter may begin with a dash' \
"gh issue list --repo devantler-tech/platform --label -bug --limit 10"
expect_allow 'a --milestone filter may begin with a dash' \
"gh pr list --repo devantler-tech/platform --milestone -v2 --limit 10"
expect_allow 'a git --grep pattern may begin with a dash' \
"git log --oneline --grep -x"
# A dash followed by a digit is a number, not a flag: git documents `--max-count -1`
# and `-n -1` as "no limit", and `--since -1.day` as a relative date. No flag name
# starts with a digit, so admitting these hides nothing the guard classifies.
expect_allow 'git --max-count takes a negative number' "git log --oneline --max-count -1"
expect_allow 'git -n takes a negative number' "git log --oneline -n -1"
expect_allow 'git --since takes a relative date beginning with a dash' \
"git log --oneline --since -1.day"
expect_deny_names 'a negative number does not widen the rule to letters' \
"git log --oneline --max-count -x --work-tree" "-x"
# git's pattern and date flags take free-form values that may begin with a dash:
# `--author -bot` is a regex over authors and `--since -yesterday` is an approxidate
# expression, both accepted by git as separated forms. A count is not free-form.
expect_allow 'git --author takes a dash-leading regex' "git log --oneline --author -bot"
expect_allow 'git --committer takes a dash-leading regex' "git log --oneline --committer -bot"
expect_allow 'git --since takes a dash-leading date expression' \
"git log --oneline --since -yesterday"
expect_allow 'git --until takes a dash-leading date expression' \
"git log --oneline --until -yesterday"
expect_allow 'issue list' "gh issue list --repo devantler-tech/ksail --state open --limit 200"
expect_allow 'search issues' "gh search issues --owner devantler-tech --state open --limit 300"
expect_allow 'search prs' "gh search prs --owner devantler-tech --state open"
Expand Down
Loading