agent-diff-guard 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_diff_guard/__init__.py +0 -0
- agent_diff_guard/analyze.py +80 -0
- agent_diff_guard/cli.py +158 -0
- agent_diff_guard/git_diff.py +28 -0
- agent_diff_guard/license.py +44 -0
- agent_diff_guard/sarif.py +41 -0
- agent_diff_guard-0.1.0.dist-info/METADATA +122 -0
- agent_diff_guard-0.1.0.dist-info/RECORD +12 -0
- agent_diff_guard-0.1.0.dist-info/WHEEL +5 -0
- agent_diff_guard-0.1.0.dist-info/entry_points.txt +2 -0
- agent_diff_guard-0.1.0.dist-info/licenses/LICENSE +21 -0
- agent_diff_guard-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
SAFETY_KEYWORDS = ["never", "must not", "do not", "refuse"]
|
|
4
|
+
|
|
5
|
+
DANGEROUS_PERMISSIONS = [
|
|
6
|
+
"shell_exec",
|
|
7
|
+
"sudo",
|
|
8
|
+
"rm -rf",
|
|
9
|
+
"exec(",
|
|
10
|
+
"eval(",
|
|
11
|
+
"admin_override",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
# Coarse relative cost tier per model family, not exact pricing - just enough
|
|
15
|
+
# to catch an obvious cheap-to-expensive swap.
|
|
16
|
+
MODEL_COST_TIER = {
|
|
17
|
+
"claude-haiku-4-5": 1,
|
|
18
|
+
"claude-sonnet-5": 2,
|
|
19
|
+
"claude-opus-4-8": 3,
|
|
20
|
+
"gpt-4o-mini": 1,
|
|
21
|
+
"gpt-4o": 2,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Finding:
|
|
27
|
+
severity: str
|
|
28
|
+
message: str
|
|
29
|
+
rule_id: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def analyze(before: str, after: str) -> list[Finding]:
|
|
33
|
+
findings = []
|
|
34
|
+
before_lower = before.lower()
|
|
35
|
+
after_lower = after.lower()
|
|
36
|
+
|
|
37
|
+
for keyword in SAFETY_KEYWORDS:
|
|
38
|
+
if keyword in before_lower and keyword not in after_lower:
|
|
39
|
+
findings.append(
|
|
40
|
+
Finding(
|
|
41
|
+
severity="HIGH",
|
|
42
|
+
message=f"Safety instruction containing '{keyword}' was removed",
|
|
43
|
+
rule_id="safety-keyword-removed",
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
for permission in DANGEROUS_PERMISSIONS:
|
|
48
|
+
if permission not in before_lower and permission in after_lower:
|
|
49
|
+
findings.append(
|
|
50
|
+
Finding(
|
|
51
|
+
severity="HIGH",
|
|
52
|
+
message=f"New dangerous permission added: '{permission}'",
|
|
53
|
+
rule_id="dangerous-permission-added",
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
before_model = _find_model(before_lower)
|
|
58
|
+
after_model = _find_model(after_lower)
|
|
59
|
+
if (
|
|
60
|
+
before_model
|
|
61
|
+
and after_model
|
|
62
|
+
and before_model != after_model
|
|
63
|
+
and MODEL_COST_TIER[after_model] > MODEL_COST_TIER[before_model]
|
|
64
|
+
):
|
|
65
|
+
findings.append(
|
|
66
|
+
Finding(
|
|
67
|
+
severity="MEDIUM",
|
|
68
|
+
message=f"Model changed from '{before_model}' to pricier '{after_model}'",
|
|
69
|
+
rule_id="model-cost-increase",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return findings
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _find_model(text: str) -> str | None:
|
|
77
|
+
for model in MODEL_COST_TIER:
|
|
78
|
+
if model in text:
|
|
79
|
+
return model
|
|
80
|
+
return None
|
agent_diff_guard/cli.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .analyze import Finding, analyze
|
|
8
|
+
from .license import verify_license, PRO_PRODUCT_URL
|
|
9
|
+
from .sarif import build_sarif
|
|
10
|
+
from . import git_diff
|
|
11
|
+
|
|
12
|
+
FREE_FILE_DETAIL_LIMIT = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_pro(args: argparse.Namespace) -> bool:
|
|
16
|
+
if not args.license_key:
|
|
17
|
+
return False
|
|
18
|
+
if verify_license(args.license_key):
|
|
19
|
+
return True
|
|
20
|
+
print("License key provided but could not be verified - falling back to free tier", file=sys.stderr)
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _visible_findings(findings: list[Finding], pro: bool) -> tuple[list[Finding], int]:
|
|
25
|
+
visible = findings if pro else [f for f in findings if f.severity == "HIGH"]
|
|
26
|
+
return visible, len(findings) - len(visible)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _print_findings(visible: list[Finding], hidden_count: int) -> None:
|
|
30
|
+
for finding in visible:
|
|
31
|
+
print(f"{finding.severity}: {finding.message}")
|
|
32
|
+
if hidden_count:
|
|
33
|
+
print(
|
|
34
|
+
f"{hidden_count} additional finding(s) hidden - upgrade to Pro to see them: {PRO_PRODUCT_URL}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _diff(args: argparse.Namespace) -> int:
|
|
39
|
+
pro = _is_pro(args)
|
|
40
|
+
before = Path(args.before).read_text()
|
|
41
|
+
after = Path(args.after).read_text()
|
|
42
|
+
|
|
43
|
+
findings = analyze(before, after)
|
|
44
|
+
visible, hidden_count = _visible_findings(findings, pro)
|
|
45
|
+
_print_findings(visible, hidden_count)
|
|
46
|
+
|
|
47
|
+
return 1 if any(f.severity == "HIGH" for f in findings) else 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _check(args: argparse.Namespace) -> int:
|
|
51
|
+
if args.format == "sarif":
|
|
52
|
+
return _check_sarif(args)
|
|
53
|
+
|
|
54
|
+
pro = _is_pro(args)
|
|
55
|
+
repo_dir = Path(args.repo).resolve()
|
|
56
|
+
paths = args.paths or git_diff.changed_files(repo_dir, args.against)
|
|
57
|
+
|
|
58
|
+
exit_code = 0
|
|
59
|
+
detail_shown = 0
|
|
60
|
+
hidden_high_files = 0
|
|
61
|
+
hidden_high_findings = 0
|
|
62
|
+
|
|
63
|
+
for path in paths:
|
|
64
|
+
before = git_diff.read_before(repo_dir, args.against, path)
|
|
65
|
+
after = git_diff.read_after(repo_dir, path)
|
|
66
|
+
|
|
67
|
+
findings = analyze(before, after)
|
|
68
|
+
has_high = any(f.severity == "HIGH" for f in findings)
|
|
69
|
+
if has_high:
|
|
70
|
+
exit_code = 1
|
|
71
|
+
|
|
72
|
+
if not findings:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if pro or detail_shown < FREE_FILE_DETAIL_LIMIT:
|
|
76
|
+
visible, hidden_count = _visible_findings(findings, pro)
|
|
77
|
+
print(f"== {path} ==")
|
|
78
|
+
_print_findings(visible, hidden_count)
|
|
79
|
+
detail_shown += 1
|
|
80
|
+
elif has_high:
|
|
81
|
+
hidden_high_files += 1
|
|
82
|
+
hidden_high_findings += sum(1 for f in findings if f.severity == "HIGH")
|
|
83
|
+
|
|
84
|
+
if hidden_high_files:
|
|
85
|
+
print(
|
|
86
|
+
f"{hidden_high_files} more file(s) with {hidden_high_findings} HIGH finding(s) not shown "
|
|
87
|
+
f"in detail - upgrade to Pro for full per-file detail: {PRO_PRODUCT_URL}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return exit_code
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _check_sarif(args: argparse.Namespace) -> int:
|
|
94
|
+
if not _is_pro(args):
|
|
95
|
+
print(f"SARIF output requires a valid Pro license key. Get one: {PRO_PRODUCT_URL}", file=sys.stderr)
|
|
96
|
+
return 2
|
|
97
|
+
|
|
98
|
+
repo_dir = Path(args.repo).resolve()
|
|
99
|
+
paths = args.paths or git_diff.changed_files(repo_dir, args.against)
|
|
100
|
+
|
|
101
|
+
findings_by_path = {}
|
|
102
|
+
exit_code = 0
|
|
103
|
+
for path in paths:
|
|
104
|
+
before = git_diff.read_before(repo_dir, args.against, path)
|
|
105
|
+
after = git_diff.read_after(repo_dir, path)
|
|
106
|
+
findings = analyze(before, after)
|
|
107
|
+
if findings:
|
|
108
|
+
findings_by_path[path] = findings
|
|
109
|
+
if any(f.severity == "HIGH" for f in findings):
|
|
110
|
+
exit_code = 1
|
|
111
|
+
|
|
112
|
+
print(json.dumps(build_sarif(findings_by_path), indent=2))
|
|
113
|
+
return exit_code
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run(argv: list[str]) -> int:
|
|
117
|
+
parser = argparse.ArgumentParser(
|
|
118
|
+
description="Flag risky changes to an AI agent config (prompt/tool-permissions/model choice)."
|
|
119
|
+
)
|
|
120
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
121
|
+
|
|
122
|
+
diff_parser = subparsers.add_parser("diff", help="Compare two file versions directly")
|
|
123
|
+
diff_parser.add_argument("before", help="Path to the file before the change")
|
|
124
|
+
diff_parser.add_argument("after", help="Path to the file after the change")
|
|
125
|
+
diff_parser.add_argument(
|
|
126
|
+
"--license-key",
|
|
127
|
+
default=os.environ.get("AGENT_DIFF_GUARD_LICENSE", ""),
|
|
128
|
+
help="Pro license key (or set AGENT_DIFF_GUARD_LICENSE). Verified against Gumroad.",
|
|
129
|
+
)
|
|
130
|
+
diff_parser.set_defaults(handler=_diff)
|
|
131
|
+
|
|
132
|
+
check_parser = subparsers.add_parser(
|
|
133
|
+
"check", help="Check files changed vs. a git ref (default: HEAD) - no manual diffing needed"
|
|
134
|
+
)
|
|
135
|
+
check_parser.add_argument(
|
|
136
|
+
"paths", nargs="*", help="Specific file(s) to check (default: all files changed vs. --against)"
|
|
137
|
+
)
|
|
138
|
+
check_parser.add_argument("--against", default="HEAD", help="Git ref to diff against (default: HEAD)")
|
|
139
|
+
check_parser.add_argument("--repo", default=".", help="Path to the git repo (default: current directory)")
|
|
140
|
+
check_parser.add_argument(
|
|
141
|
+
"--license-key",
|
|
142
|
+
default=os.environ.get("AGENT_DIFF_GUARD_LICENSE", ""),
|
|
143
|
+
help="Pro license key (or set AGENT_DIFF_GUARD_LICENSE). Verified against Gumroad.",
|
|
144
|
+
)
|
|
145
|
+
check_parser.add_argument(
|
|
146
|
+
"--format",
|
|
147
|
+
choices=["text", "sarif"],
|
|
148
|
+
default="text",
|
|
149
|
+
help="Output format. 'sarif' requires a valid Pro license key.",
|
|
150
|
+
)
|
|
151
|
+
check_parser.set_defaults(handler=_check)
|
|
152
|
+
|
|
153
|
+
args = parser.parse_args(argv)
|
|
154
|
+
return args.handler(args)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main() -> None:
|
|
158
|
+
sys.exit(run(sys.argv[1:]))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def changed_files(repo_dir: Path, against: str) -> list[str]:
|
|
6
|
+
result = subprocess.run(
|
|
7
|
+
["git", "diff", "--name-only", against],
|
|
8
|
+
cwd=repo_dir,
|
|
9
|
+
capture_output=True,
|
|
10
|
+
text=True,
|
|
11
|
+
check=True,
|
|
12
|
+
)
|
|
13
|
+
return [line for line in result.stdout.splitlines() if line]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_before(repo_dir: Path, against: str, path: str) -> str:
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
["git", "show", f"{against}:{path}"],
|
|
19
|
+
cwd=repo_dir,
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
return result.stdout if result.returncode == 0 else ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_after(repo_dir: Path, path: str) -> str:
|
|
27
|
+
file_path = repo_dir / path
|
|
28
|
+
return file_path.read_text() if file_path.exists() else ""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import ssl
|
|
3
|
+
import urllib.error
|
|
4
|
+
import urllib.parse
|
|
5
|
+
import urllib.request
|
|
6
|
+
|
|
7
|
+
import certifi
|
|
8
|
+
|
|
9
|
+
GUMROAD_VERIFY_URL = "https://api.gumroad.com/v2/licenses/verify"
|
|
10
|
+
PRODUCT_ID = "Wpncwq3CMyYG2AGLxfbD7A=="
|
|
11
|
+
PRO_PRODUCT_URL = "https://skitamaze.gumroad.com/l/dkybda"
|
|
12
|
+
|
|
13
|
+
# Explicit CA bundle, not the system default: some Python installs (notably
|
|
14
|
+
# macOS python.org builds) ship without a working local trust store, which
|
|
15
|
+
# would otherwise make a genuinely valid, paid license fail closed to "invalid".
|
|
16
|
+
_SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def verify_license(license_key: str, product_id: str = PRODUCT_ID) -> bool:
|
|
20
|
+
if not license_key:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
data = urllib.parse.urlencode(
|
|
24
|
+
{
|
|
25
|
+
"product_id": product_id,
|
|
26
|
+
"license_key": license_key,
|
|
27
|
+
"increment_uses_count": "false",
|
|
28
|
+
}
|
|
29
|
+
).encode()
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
with urllib.request.urlopen(
|
|
33
|
+
GUMROAD_VERIFY_URL, data=data, timeout=10, context=_SSL_CONTEXT
|
|
34
|
+
) as response:
|
|
35
|
+
body = json.loads(response.read())
|
|
36
|
+
except urllib.error.HTTPError as error:
|
|
37
|
+
try:
|
|
38
|
+
body = json.loads(error.read())
|
|
39
|
+
except (ValueError, OSError):
|
|
40
|
+
return False
|
|
41
|
+
except (urllib.error.URLError, TimeoutError, OSError, ValueError):
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
return bool(body.get("success"))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from .analyze import Finding
|
|
2
|
+
|
|
3
|
+
SARIF_LEVEL = {"HIGH": "error", "MEDIUM": "warning"}
|
|
4
|
+
|
|
5
|
+
INFORMATION_URI = "https://github.com/agent-diff-guard/agent-diff-guard"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_sarif(findings_by_path: dict[str, list[Finding]]) -> dict:
|
|
9
|
+
results = []
|
|
10
|
+
rule_ids = set()
|
|
11
|
+
|
|
12
|
+
for path, findings in findings_by_path.items():
|
|
13
|
+
for finding in findings:
|
|
14
|
+
rule_ids.add(finding.rule_id)
|
|
15
|
+
results.append(
|
|
16
|
+
{
|
|
17
|
+
"ruleId": finding.rule_id,
|
|
18
|
+
"level": SARIF_LEVEL.get(finding.severity, "warning"),
|
|
19
|
+
"message": {"text": finding.message},
|
|
20
|
+
"locations": [
|
|
21
|
+
{"physicalLocation": {"artifactLocation": {"uri": path}}}
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"version": "2.1.0",
|
|
28
|
+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
29
|
+
"runs": [
|
|
30
|
+
{
|
|
31
|
+
"tool": {
|
|
32
|
+
"driver": {
|
|
33
|
+
"name": "agent-diff-guard",
|
|
34
|
+
"informationUri": INFORMATION_URI,
|
|
35
|
+
"rules": [{"id": rule_id} for rule_id in sorted(rule_ids)],
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"results": results,
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-diff-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flags risky changes to AI agent configs (prompts, tool permissions, model choice) in a diff.
|
|
5
|
+
Author: Wally
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jwa-wa/agent-diff-guard
|
|
8
|
+
Project-URL: Pro license, https://skitamaze.gumroad.com/l/dkybda
|
|
9
|
+
Keywords: ai-agents,llm-security,prompt-injection,ci,github-actions
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
13
|
+
Classifier: Topic :: Security
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: certifi
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# agent-diff-guard
|
|
22
|
+
|
|
23
|
+
Catches risky changes to an AI agent's config — system prompt, tool permissions, model choice —
|
|
24
|
+
before they merge. A narrow, single-purpose CI check, not a full observability platform: point it
|
|
25
|
+
at a repo and it scans every changed file against your base branch automatically.
|
|
26
|
+
|
|
27
|
+
**[Get a Pro license →](https://skitamaze.gumroad.com/l/dkybda)** — full per-file detail, model
|
|
28
|
+
cost-tier detection, SARIF output for GitHub code scanning.
|
|
29
|
+
|
|
30
|
+
## What it catches
|
|
31
|
+
|
|
32
|
+
- **Safety instructions removed** — a phrase like "must never" / "do not" / "refuse" present
|
|
33
|
+
before a change and missing after it.
|
|
34
|
+
- **Dangerous tool permissions added** — `shell_exec`, `sudo`, `rm -rf`, `exec(`, `eval(`,
|
|
35
|
+
`admin_override` appearing for the first time.
|
|
36
|
+
- **Model swapped to a pricier tier** (Pro) — e.g. haiku → opus, gpt-4o-mini → gpt-4o.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install agent-diff-guard
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### `check` — scans changed files against a git ref (recommended)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
agent-diff-guard check # all files changed vs. HEAD, in the current repo
|
|
50
|
+
agent-diff-guard check --against origin/main # all files changed vs. another ref
|
|
51
|
+
agent-diff-guard check path/to/agent_config.yaml # restrict to one file
|
|
52
|
+
agent-diff-guard check --repo /path/to/repo # run against a repo other than cwd
|
|
53
|
+
agent-diff-guard check --license-key "$LICENSE" # unlock Pro (or set AGENT_DIFF_GUARD_LICENSE)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Exit code is `1` if any file has a HIGH severity finding, `0` otherwise — safe to use as a CI gate.
|
|
57
|
+
The exit code always reflects every scanned file, on the free tier too.
|
|
58
|
+
|
|
59
|
+
Free tier shows full detail (path + findings) for the first 3 files with findings; beyond that it
|
|
60
|
+
collapses into a one-line summary — the CI gate still fails correctly either way, Pro just gives
|
|
61
|
+
the full per-file breakdown.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
agent-diff-guard check --format sarif --license-key "$LICENSE" # Pro: SARIF output
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A Pro license key is verified live against Gumroad on every run. If it's invalid or unreachable,
|
|
68
|
+
the tool falls back to the free tier rather than erroring out, so a network hiccup never breaks
|
|
69
|
+
your CI.
|
|
70
|
+
|
|
71
|
+
### `diff` — compare two file versions directly
|
|
72
|
+
|
|
73
|
+
For comparing arbitrary snapshots outside a git checkout:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
agent-diff-guard diff path/to/before.txt path/to/after.txt
|
|
77
|
+
agent-diff-guard diff path/to/before.txt path/to/after.txt --license-key "$LICENSE"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## GitHub Action
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
- uses: jwa-wa/agent-diff-guard@v1
|
|
84
|
+
with:
|
|
85
|
+
license-key: ${{ secrets.AGENT_DIFF_GUARD_LICENSE }} # optional, omit for free tier
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That's the whole setup for a `pull_request` workflow — it reads the PR's base branch automatically
|
|
89
|
+
and works against a standard (shallow) checkout, no extra config needed.
|
|
90
|
+
|
|
91
|
+
For a `push`-triggered workflow (no PR context), it compares against the previous commit instead;
|
|
92
|
+
your checkout step needs `fetch-depth: 2` (or `0`) for that:
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
- uses: actions/checkout@v4
|
|
96
|
+
with:
|
|
97
|
+
fetch-depth: 2
|
|
98
|
+
- uses: jwa-wa/agent-diff-guard@v1
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Store your license key as a repo/org secret — the Action passes it through as an environment
|
|
102
|
+
variable, never a command-line argument, so it doesn't end up in job logs.
|
|
103
|
+
|
|
104
|
+
| Input | Default | Purpose |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `base-ref` | PR base branch | Branch to diff against. Empty = compare against previous commit. |
|
|
107
|
+
| `paths` | *(all changed files)* | Space-separated list to restrict the scan to specific files. |
|
|
108
|
+
| `license-key` | *(none, free tier)* | Pro license key, verified live against Gumroad. |
|
|
109
|
+
|
|
110
|
+
## Pricing
|
|
111
|
+
|
|
112
|
+
**Free** — the CI-gate result (pass/fail) always covers every scanned file. Full per-file detail
|
|
113
|
+
for the first 3 files with findings each run.
|
|
114
|
+
|
|
115
|
+
**Pro** — [get a license](https://skitamaze.gumroad.com/l/dkybda):
|
|
116
|
+
- Full per-file detail for every file, not just the first 3.
|
|
117
|
+
- Model cost-tier change detection.
|
|
118
|
+
- SARIF output — findings show up natively in GitHub's Security tab.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
agent_diff_guard/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
agent_diff_guard/analyze.py,sha256=rNk4Jujt-ozVwUeP9T8EZV_UOcY17h56Vrp06yrpNzE,2121
|
|
3
|
+
agent_diff_guard/cli.py,sha256=NnXqpi_hkx2L-V5jE_KpEonuSK7It9KCxABLtiwxgzE,5400
|
|
4
|
+
agent_diff_guard/git_diff.py,sha256=nSgjKPz4U6LPZQc17MGS3UCyAeG83Ec7P4B_W4Q8aiI,770
|
|
5
|
+
agent_diff_guard/license.py,sha256=UFQdfCKgLYxPdIc00gYpYIQzhSrS6atu34KtPRarAyM,1370
|
|
6
|
+
agent_diff_guard/sarif.py,sha256=rrgeY3AR6vkHvrzzHqJccbzRwNdqLgEH_60D8k8jvIs,1344
|
|
7
|
+
agent_diff_guard-0.1.0.dist-info/licenses/LICENSE,sha256=ITHwfuJ2PzIdOttUb260qcqFtr2AzAJl3bAJiJdkmGo,1062
|
|
8
|
+
agent_diff_guard-0.1.0.dist-info/METADATA,sha256=emgcCIP0P50pI1ZWHYh0h-6lGGrqrGGelHz6SjN2xQo,4672
|
|
9
|
+
agent_diff_guard-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
agent_diff_guard-0.1.0.dist-info/entry_points.txt,sha256=2Tr-rjYpUFWAYnj-9Wa96a8JsPyGQ4priWnwRA9PcR4,63
|
|
11
|
+
agent_diff_guard-0.1.0.dist-info/top_level.txt,sha256=ehM7z1UtZd1JjQ0k5bOzlwj7vBsszns-1-NNm5wdHBE,17
|
|
12
|
+
agent_diff_guard-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wally
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent_diff_guard
|