103 lines
3.7 KiB
Bash
103 lines
3.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Static guardrail checks for game.js. Run from repo root.
|
|
#
|
|
# Each check prints a PASS/FAIL line and contributes to the final exit code.
|
|
# The rules enforced here come from AGENTS.md — keep the two files in sync.
|
|
#
|
|
# Some rules are marked PENDING: they describe invariants we've agreed on but
|
|
# haven't reached on main yet (because another open PR is landing the fix).
|
|
# PENDING rules print their current violation count without failing the job;
|
|
# convert them to hard failures once the blocking PR merges.
|
|
|
|
set -u
|
|
fail=0
|
|
|
|
say() { printf '%s\n' "$*"; }
|
|
banner() { say ""; say "==== $* ===="; }
|
|
|
|
# ---------- Rule 1: no *Boost mutation inside applyFn blocks ----------
|
|
# Persistent multipliers (codeBoost, computeBoost, ...) must not be written
|
|
# from any function that runs per tick. The `applyFn` of a debuff is invoked
|
|
# on every updateRates() call, so `G.codeBoost *= 0.7` inside applyFn compounds
|
|
# and silently zeros code production. See AGENTS.md rule 1.
|
|
banner "Rule 1: no *Boost mutation inside applyFn"
|
|
rule1_hits=$(awk '
|
|
/applyFn:/ { inFn=1; brace=0; next }
|
|
inFn {
|
|
n = gsub(/\{/, "{")
|
|
brace += n
|
|
if ($0 ~ /(codeBoost|computeBoost|knowledgeBoost|userBoost|impactBoost)[[:space:]]*([*\/+\-]=|=)/) {
|
|
print FILENAME ":" NR ": " $0
|
|
}
|
|
n = gsub(/\}/, "}")
|
|
brace -= n
|
|
if (brace <= 0) inFn = 0
|
|
}
|
|
' game.js)
|
|
if [ -z "$rule1_hits" ]; then
|
|
say " PASS"
|
|
else
|
|
say " FAIL — see AGENTS.md rule 1"
|
|
say "$rule1_hits"
|
|
fail=1
|
|
fi
|
|
|
|
# ---------- Rule 2: click power has a single source (getClickPower) ----------
|
|
# The formula should live only inside getClickPower(). If it appears anywhere
|
|
# else, the sites will drift when someone changes the formula.
|
|
banner "Rule 2: click power formula has one source"
|
|
rule2_hits=$(grep -nE 'Math\.floor\(G\.buildings\.autocoder \* 0\.5\)' game.js || true)
|
|
rule2_count=0
|
|
if [ -n "$rule2_hits" ]; then
|
|
rule2_count=$(printf '%s\n' "$rule2_hits" | grep -c .)
|
|
fi
|
|
if [ "$rule2_count" -le 1 ]; then
|
|
say " PASS ($rule2_count site)"
|
|
else
|
|
say " FAIL — $rule2_count sites; inline into getClickPower() only"
|
|
printf '%s\n' "$rule2_hits"
|
|
fail=1
|
|
fi
|
|
|
|
# ---------- Rule 3: loadGame uses a whitelist, not Object.assign ----------
|
|
# Object.assign(G, data) lets a malicious or corrupted save file set any G
|
|
# field, and hides drift when saveGame's explicit list diverges from what
|
|
# the game actually reads. See AGENTS.md rule 3.
|
|
banner "Rule 3: loadGame uses a whitelist"
|
|
rule3_hits=$(grep -nE 'Object\.assign\(G,[[:space:]]*data\)' game.js || true)
|
|
if [ -z "$rule3_hits" ]; then
|
|
say " PASS"
|
|
else
|
|
say " FAIL — see AGENTS.md rule 3"
|
|
printf '%s\n' "$rule3_hits"
|
|
fail=1
|
|
fi
|
|
|
|
# ---------- Rule 7: no secrets in the tree ----------
|
|
# Scans for common token prefixes. Expand the pattern list when new key
|
|
# formats appear in the fleet. See AGENTS.md rule 7.
|
|
banner "Rule 7: secret scan"
|
|
secret_hits=$(grep -rnE 'sk-ant-[a-zA-Z0-9_-]{6,}|sk-or-[a-zA-Z0-9_-]{6,}|ghp_[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16}' \
|
|
--include='*.js' --include='*.json' --include='*.md' --include='*.html' \
|
|
--include='*.yml' --include='*.yaml' --include='*.py' --include='*.sh' \
|
|
--exclude-dir=.git --exclude-dir=.gitea . || true)
|
|
# Strip our own literal-prefix patterns (this file, AGENTS.md, workflow) so the
|
|
# check doesn't match the very grep that implements it.
|
|
secret_hits=$(printf '%s\n' "$secret_hits" | grep -v -E '(AGENTS\.md|guardrails\.sh|guardrails\.yml)' || true)
|
|
if [ -z "$secret_hits" ]; then
|
|
say " PASS"
|
|
else
|
|
say " FAIL"
|
|
printf '%s\n' "$secret_hits"
|
|
fail=1
|
|
fi
|
|
|
|
banner "result"
|
|
if [ "$fail" = "0" ]; then
|
|
say "all guardrails passed"
|
|
exit 0
|
|
else
|
|
say "one or more guardrails failed"
|
|
exit 1
|
|
fi
|