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
7 changes: 3 additions & 4 deletions agentguard.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ShellForge — AgentGuard Governance Policy
# Mode: monitor (log but don't block) — switch to enforce when ready
# Mode: enforce (block violations)

mode: enforce

Expand All @@ -13,12 +13,11 @@ policies:
message: "Force push is not allowed by governance policy"

- name: no-destructive-rm
description: Block recursive force deletion
description: Block all rm invocations (plain rm, -r, -rf, etc.)
match:
command: "rm"
args_contain: ["-rf", "-fr", "--recursive --force", "--force --recursive", "-r -f", "-f -r"]
action: deny
message: "Destructive rm blocked by policy"
message: "rm is not allowed by governance policy — use targeted file operations instead"

- name: file-write-constraints
description: Agents may only write to outputs/ and agents' own workspace
Expand Down
10 changes: 8 additions & 2 deletions cmd/shellforge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ func cmdEvaluate() {
// Used by Crush fork to check actions before execution.
data, err := io.ReadAll(os.Stdin)
if err != nil {
json.NewEncoder(os.Stdout).Encode(map[string]any{"allowed": true, "reason": "stdin read error"})
json.NewEncoder(os.Stdout).Encode(map[string]any{"allowed": false, "reason": "stdin read error"})
return
}

Expand All @@ -810,7 +810,13 @@ Tool string `json:"tool"`
Action string `json:"action"`
Path string `json:"path"`
}
json.Unmarshal(data, &input)
if err := json.Unmarshal(data, &input); err != nil {
json.NewEncoder(os.Stdout).Encode(map[string]any{
"allowed": false,
"reason": "malformed governance request: " + err.Error(),
})
return
}

configPath := findGovernanceConfig()
if configPath == "" {
Expand Down
4 changes: 3 additions & 1 deletion internal/governance/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ if m.Command == "*" {
if len(m.ArgsContain) > 0 {
return containsAny(cmd, m.ArgsContain)
}
return p.Timeout > 0
// Wildcard with no args_contain is a budget/monitoring policy only.
// Timeout is enforced separately via GetTimeout(); don't match every command.
return false
}
if !strings.Contains(cmd, m.Command) {
return false
Expand Down
20 changes: 16 additions & 4 deletions scripts/govern-shell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,22 @@ if [ "${1:-}" = "-c" ]; then
COMMAND="$*"

# Evaluate through AgentGuard
RESULT=$(printf '{"tool":"run_shell","action":"%s","path":"."}' "$COMMAND" | shellforge evaluate 2>/dev/null || echo '{"allowed":true}')

if echo "$RESULT" | grep -q '"allowed":false'; then
REASON=$(echo "$RESULT" | sed 's/.*"reason":"\([^"]*\)".*/\1/')
# Use jq --arg for safe JSON construction: handles quotes, backslashes, control chars in $COMMAND
if command -v jq &>/dev/null; then
JSON_PAYLOAD=$(jq -n --arg cmd "$COMMAND" '{"tool":"run_shell","action":$cmd,"path":"."}')
else
# Fallback: basic escaping (covers common cases; jq preferred)
ESCAPED=$(printf '%s' "$COMMAND" | sed 's/\\/\\\\/g; s/"/\\"/g')
JSON_PAYLOAD="{\"tool\":\"run_shell\",\"action\":\"$ESCAPED\",\"path\":\".\"}"
fi
RESULT=$(printf '%s' "$JSON_PAYLOAD" | shellforge evaluate 2>/dev/null || echo '{"allowed":false,"reason":"governance unavailable"}')

if printf '%s' "$RESULT" | grep -q '"allowed":false'; then
if command -v jq &>/dev/null; then
REASON=$(printf '%s' "$RESULT" | jq -r '.reason // "policy violation"')
else
REASON=$(printf '%s' "$RESULT" | sed 's/.*"reason":"\([^"]*\)".*/\1/')
fi
echo "[AgentGuard] DENIED: $REASON" >&2
exit 1
fi
Expand Down
Loading