agent-write-gate 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_write_gate-0.1.0.dist-info/METADATA +276 -0
- agent_write_gate-0.1.0.dist-info/RECORD +18 -0
- agent_write_gate-0.1.0.dist-info/WHEEL +5 -0
- agent_write_gate-0.1.0.dist-info/entry_points.txt +2 -0
- agent_write_gate-0.1.0.dist-info/licenses/LICENSE +21 -0
- agent_write_gate-0.1.0.dist-info/top_level.txt +1 -0
- agentgate/__init__.py +5 -0
- agentgate/adapter.py +142 -0
- agentgate/apply_patch.py +74 -0
- agentgate/checks/__init__.py +1 -0
- agentgate/checks/cjk.py +81 -0
- agentgate/checks/unicode_safety.py +277 -0
- agentgate/cli.py +550 -0
- agentgate/config.py +171 -0
- agentgate/model.py +34 -0
- agentgate/policy.py +94 -0
- agentgate/registry.py +61 -0
- agentgate/report.py +247 -0
agentgate/cli.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""cli.py -- agentgate command-line interface.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
agentgate hook --stdin Primary: agent hook entrypoint (Pre/PostToolUse)
|
|
5
|
+
agentgate scan PATH... Same checks over files (CI / manual); tty|json|sarif
|
|
6
|
+
agentgate checks List checks + enabled state + missing deps
|
|
7
|
+
agentgate --version
|
|
8
|
+
|
|
9
|
+
Exit codes:
|
|
10
|
+
hook: 0 = allow; 2 = block (deny in Pre / feedback in Post) or error
|
|
11
|
+
scan: 0 = no blocking findings; 1 = blocking findings; 2 = error
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
from .config import load_config
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# UTF-8 stdio fix (Windows cp932)
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def _enable_utf8_io() -> None:
|
|
29
|
+
"""Force UTF-8 on stdio so Unicode output is not mangled on Windows."""
|
|
30
|
+
out = getattr(sys.stdout, "reconfigure", None)
|
|
31
|
+
if out is not None:
|
|
32
|
+
try:
|
|
33
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
err = getattr(sys.stderr, "reconfigure", None)
|
|
37
|
+
if err is not None:
|
|
38
|
+
try:
|
|
39
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
inp = getattr(sys.stdin, "reconfigure", None)
|
|
43
|
+
if inp is not None:
|
|
44
|
+
try:
|
|
45
|
+
sys.stdin.reconfigure(encoding="utf-8")
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Skip dirs / binary detection (mirrors mojihen)
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
_SKIP_DIRS = {
|
|
55
|
+
".git", "node_modules", ".venv", "__pycache__", ".mypy_cache",
|
|
56
|
+
".pytest_cache", "dist", "build", ".tox",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_binary(path: Path) -> bool:
|
|
61
|
+
try:
|
|
62
|
+
with open(path, "rb") as fh:
|
|
63
|
+
chunk = fh.read(8192)
|
|
64
|
+
return b"\x00" in chunk
|
|
65
|
+
except OSError:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_source(path: Path) -> Optional[str]:
|
|
70
|
+
if _is_binary(path):
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
74
|
+
except OSError:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def collect_files(paths: List[Path]) -> List[Path]:
|
|
79
|
+
result: List[Path] = []
|
|
80
|
+
for p in paths:
|
|
81
|
+
if p.is_file():
|
|
82
|
+
result.append(p)
|
|
83
|
+
elif p.is_dir():
|
|
84
|
+
for root, dirs, files in os.walk(p):
|
|
85
|
+
dirs[:] = [
|
|
86
|
+
d for d in dirs
|
|
87
|
+
if d not in _SKIP_DIRS and not d.startswith(".")
|
|
88
|
+
]
|
|
89
|
+
for fname in files:
|
|
90
|
+
result.append(Path(root) / fname)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# hook subcommand
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def cmd_hook(args: List[str]) -> int:
|
|
99
|
+
"""agentgate hook --stdin
|
|
100
|
+
|
|
101
|
+
Reads a JSON hook payload from stdin, runs enabled checks, applies policy.
|
|
102
|
+
|
|
103
|
+
Exit codes:
|
|
104
|
+
0 = allow
|
|
105
|
+
2 = block (deny in PreToolUse; feedback in PostToolUse)
|
|
106
|
+
"""
|
|
107
|
+
if "--stdin" not in args:
|
|
108
|
+
print("agentgate hook: expected --stdin flag", file=sys.stderr)
|
|
109
|
+
return 2
|
|
110
|
+
|
|
111
|
+
# Parse optional --config
|
|
112
|
+
config_path: Optional[Path] = None
|
|
113
|
+
for i, arg in enumerate(args):
|
|
114
|
+
if arg in ("--config", "-c") and i + 1 < len(args):
|
|
115
|
+
config_path = Path(args[i + 1])
|
|
116
|
+
elif arg.startswith("--config="):
|
|
117
|
+
config_path = Path(arg.split("=", 1)[1])
|
|
118
|
+
|
|
119
|
+
# Load config (with startup dep check)
|
|
120
|
+
cfg = load_config(explicit_path=config_path)
|
|
121
|
+
|
|
122
|
+
# Startup check: if cjk is enabled, ensure mojihen is importable
|
|
123
|
+
if cfg.cjk.enabled:
|
|
124
|
+
try:
|
|
125
|
+
import mojihen # type: ignore # noqa: F401
|
|
126
|
+
except ImportError:
|
|
127
|
+
# Surface as clean error, not traceback
|
|
128
|
+
print(
|
|
129
|
+
"agentgate: ERROR -- cjk check enabled but mojihen is not installed.\n"
|
|
130
|
+
" Install it with: pip install agent-write-gate[cjk]\n"
|
|
131
|
+
" Or disable it: set [checks] cjk = false in agentgate.toml",
|
|
132
|
+
file=sys.stderr,
|
|
133
|
+
)
|
|
134
|
+
return 2
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
payload_raw = sys.stdin.read()
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
print(f"agentgate hook: failed to read stdin: {exc}", file=sys.stderr)
|
|
140
|
+
return 2
|
|
141
|
+
|
|
142
|
+
if not payload_raw or not payload_raw.strip():
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
# Try JSON parse; non-JSON -> fail open
|
|
146
|
+
try:
|
|
147
|
+
json.loads(payload_raw)
|
|
148
|
+
except (json.JSONDecodeError, ValueError):
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
from .adapter import from_stdin_json
|
|
152
|
+
from .registry import get_enabled
|
|
153
|
+
from .policy import apply_suppression, decide_block, filter_actionable
|
|
154
|
+
from .report import block_report, warn_report
|
|
155
|
+
|
|
156
|
+
event = from_stdin_json(payload_raw)
|
|
157
|
+
|
|
158
|
+
if not event.content:
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
# Run enabled checks
|
|
162
|
+
all_issues = []
|
|
163
|
+
enabled = get_enabled(cfg)
|
|
164
|
+
for name, check_fn in enabled:
|
|
165
|
+
try:
|
|
166
|
+
found = check_fn(event, cfg)
|
|
167
|
+
all_issues.extend(found)
|
|
168
|
+
except RuntimeError as exc:
|
|
169
|
+
# e.g. mojihen missing despite startup check passing (race or monkeypatch)
|
|
170
|
+
print(f"agentgate: ERROR in check '{name}': {exc}", file=sys.stderr)
|
|
171
|
+
return 2
|
|
172
|
+
|
|
173
|
+
# Apply suppression
|
|
174
|
+
surviving = apply_suppression(all_issues, event.content, cfg)
|
|
175
|
+
|
|
176
|
+
# Filter to actionable (not 'ignore')
|
|
177
|
+
actionable = filter_actionable(surviving, cfg)
|
|
178
|
+
|
|
179
|
+
if decide_block(actionable, cfg):
|
|
180
|
+
report = block_report(actionable, file_path=event.file_path)
|
|
181
|
+
print(report, file=sys.stderr)
|
|
182
|
+
return 2
|
|
183
|
+
|
|
184
|
+
# Non-blocking warn-level issues: surface to stderr but do not block (exit 0).
|
|
185
|
+
if actionable:
|
|
186
|
+
print(warn_report(actionable, file_path=event.file_path), file=sys.stderr)
|
|
187
|
+
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# scan subcommand
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def cmd_scan(args: List[str]) -> int:
|
|
196
|
+
"""agentgate scan PATH...
|
|
197
|
+
|
|
198
|
+
Walk paths, run checks on each file, output in chosen format.
|
|
199
|
+
|
|
200
|
+
Exit codes:
|
|
201
|
+
0 = no blocking findings
|
|
202
|
+
1 = blocking findings found
|
|
203
|
+
2 = error
|
|
204
|
+
"""
|
|
205
|
+
fmt = "tty"
|
|
206
|
+
config_path: Optional[Path] = None
|
|
207
|
+
paths: List[str] = []
|
|
208
|
+
|
|
209
|
+
i = 0
|
|
210
|
+
while i < len(args):
|
|
211
|
+
arg = args[i]
|
|
212
|
+
if arg in ("--format", "-f") and i + 1 < len(args):
|
|
213
|
+
fmt = args[i + 1]
|
|
214
|
+
i += 2
|
|
215
|
+
elif arg.startswith("--format="):
|
|
216
|
+
fmt = arg.split("=", 1)[1]
|
|
217
|
+
i += 1
|
|
218
|
+
elif arg in ("--config", "-c") and i + 1 < len(args):
|
|
219
|
+
config_path = Path(args[i + 1])
|
|
220
|
+
i += 2
|
|
221
|
+
elif arg.startswith("--config="):
|
|
222
|
+
config_path = Path(arg.split("=", 1)[1])
|
|
223
|
+
i += 1
|
|
224
|
+
elif arg.startswith("-"):
|
|
225
|
+
print(f"agentgate scan: unknown flag {arg!r}", file=sys.stderr)
|
|
226
|
+
return 2
|
|
227
|
+
else:
|
|
228
|
+
paths.append(arg)
|
|
229
|
+
i += 1
|
|
230
|
+
|
|
231
|
+
if not paths:
|
|
232
|
+
print("agentgate scan: no paths specified", file=sys.stderr)
|
|
233
|
+
return 2
|
|
234
|
+
|
|
235
|
+
if fmt not in ("tty", "json", "sarif"):
|
|
236
|
+
print(f"agentgate scan: --format must be tty|json|sarif, got {fmt!r}", file=sys.stderr)
|
|
237
|
+
return 2
|
|
238
|
+
|
|
239
|
+
cfg = load_config(explicit_path=config_path)
|
|
240
|
+
|
|
241
|
+
# Startup dep check
|
|
242
|
+
if cfg.cjk.enabled:
|
|
243
|
+
try:
|
|
244
|
+
import mojihen # type: ignore # noqa: F401
|
|
245
|
+
except ImportError:
|
|
246
|
+
print(
|
|
247
|
+
"agentgate: ERROR -- cjk check enabled but mojihen is not installed.\n"
|
|
248
|
+
" Install it with: pip install agent-write-gate[cjk]\n"
|
|
249
|
+
" Or disable it: set [checks] cjk = false in agentgate.toml",
|
|
250
|
+
file=sys.stderr,
|
|
251
|
+
)
|
|
252
|
+
return 2
|
|
253
|
+
|
|
254
|
+
from .adapter import from_stdin_json
|
|
255
|
+
from .model import WriteEvent, Issue
|
|
256
|
+
from .registry import get_enabled
|
|
257
|
+
from .policy import apply_suppression, decide_block, filter_actionable
|
|
258
|
+
from .report import format_tty, format_json, format_sarif
|
|
259
|
+
from .checks.unicode_safety import run as unicode_run
|
|
260
|
+
|
|
261
|
+
file_list = collect_files([Path(p) for p in paths])
|
|
262
|
+
if not file_list:
|
|
263
|
+
print(f"agentgate scan: no files found in {paths}", file=sys.stderr)
|
|
264
|
+
return 2
|
|
265
|
+
|
|
266
|
+
all_file_issues: List[tuple] = [] # (file_path_str, issue)
|
|
267
|
+
any_blocking = False
|
|
268
|
+
|
|
269
|
+
enabled = get_enabled(cfg)
|
|
270
|
+
|
|
271
|
+
for fp in file_list:
|
|
272
|
+
source = _read_source(fp)
|
|
273
|
+
if source is None:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Build a synthetic WriteEvent for this file
|
|
277
|
+
event = WriteEvent(
|
|
278
|
+
agent="generic",
|
|
279
|
+
phase="unknown",
|
|
280
|
+
tool="unknown",
|
|
281
|
+
file_path=str(fp),
|
|
282
|
+
content=source,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
file_issues = []
|
|
286
|
+
for name, check_fn in enabled:
|
|
287
|
+
try:
|
|
288
|
+
found = check_fn(event, cfg)
|
|
289
|
+
file_issues.extend(found)
|
|
290
|
+
except RuntimeError as exc:
|
|
291
|
+
print(f"agentgate: ERROR in check '{name}' on {fp}: {exc}", file=sys.stderr)
|
|
292
|
+
return 2
|
|
293
|
+
|
|
294
|
+
# Apply suppression
|
|
295
|
+
surviving = apply_suppression(file_issues, source, cfg)
|
|
296
|
+
actionable = filter_actionable(surviving, cfg)
|
|
297
|
+
|
|
298
|
+
for issue in actionable:
|
|
299
|
+
all_file_issues.append((str(fp), issue))
|
|
300
|
+
if decide_block([issue], cfg):
|
|
301
|
+
any_blocking = True
|
|
302
|
+
|
|
303
|
+
# Render output
|
|
304
|
+
all_issues_flat = [issue for _, issue in all_file_issues]
|
|
305
|
+
|
|
306
|
+
if fmt == "json":
|
|
307
|
+
# Build combined JSON with file paths per issue
|
|
308
|
+
import json as _json
|
|
309
|
+
payload = {
|
|
310
|
+
"version": "1",
|
|
311
|
+
"tool": "agentgate",
|
|
312
|
+
"issues": [
|
|
313
|
+
{
|
|
314
|
+
"check": issue.check,
|
|
315
|
+
"rule_id": issue.rule_id,
|
|
316
|
+
"severity": issue.severity,
|
|
317
|
+
"file": fp_str,
|
|
318
|
+
"line": issue.line,
|
|
319
|
+
"col": issue.col,
|
|
320
|
+
"message": issue.message,
|
|
321
|
+
"excerpt": issue.excerpt,
|
|
322
|
+
"suggestion": issue.suggestion,
|
|
323
|
+
}
|
|
324
|
+
for fp_str, issue in all_file_issues
|
|
325
|
+
],
|
|
326
|
+
}
|
|
327
|
+
_safe_print(_json.dumps(payload, ensure_ascii=False, indent=2))
|
|
328
|
+
elif fmt == "sarif":
|
|
329
|
+
# Build SARIF with file paths
|
|
330
|
+
_print_sarif(all_file_issues)
|
|
331
|
+
else:
|
|
332
|
+
# TTY: group by file
|
|
333
|
+
_print_tty(all_file_issues)
|
|
334
|
+
|
|
335
|
+
return 1 if any_blocking else 0
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _print_tty(file_issues: List[tuple]) -> None:
|
|
339
|
+
lines: List[str] = []
|
|
340
|
+
for fp_str, issue in file_issues:
|
|
341
|
+
lines.append(
|
|
342
|
+
f"{fp_str}:{issue.line}:{issue.col} "
|
|
343
|
+
f"{issue.check}/{issue.rule_id} {issue.severity.upper()} "
|
|
344
|
+
f"'{issue.excerpt}'"
|
|
345
|
+
+ (f" -> {issue.suggestion}" if issue.suggestion else "")
|
|
346
|
+
)
|
|
347
|
+
lines.append(f" {issue.message}")
|
|
348
|
+
|
|
349
|
+
n = len(file_issues)
|
|
350
|
+
if n == 0:
|
|
351
|
+
lines.append("agentgate: no issues")
|
|
352
|
+
else:
|
|
353
|
+
plural = "issue" if n == 1 else "issues"
|
|
354
|
+
lines.append(f"\nagentgate: {n} {plural}")
|
|
355
|
+
_safe_print("\n".join(lines))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _print_sarif(file_issues: List[tuple]) -> None:
|
|
359
|
+
import json as _json
|
|
360
|
+
|
|
361
|
+
rule_ids_seen = sorted({issue.rule_id for _, issue in file_issues})
|
|
362
|
+
|
|
363
|
+
_RULE_DESCRIPTIONS = {
|
|
364
|
+
"AG-BIDI": "Bidi control character that can visually reorder source code (Trojan-Source).",
|
|
365
|
+
"AG-INVIS": "Invisible character inside identifier or string.",
|
|
366
|
+
"AG-HOMO": "Homoglyph: Cyrillic or Greek character that looks like ASCII.",
|
|
367
|
+
"MH001": "Known LLM CJK corruption.",
|
|
368
|
+
"MH002": "Mixed-script token.",
|
|
369
|
+
"MH003": "Isolated CJK in ASCII context.",
|
|
370
|
+
}
|
|
371
|
+
_RULE_NAMES = {
|
|
372
|
+
"AG-BIDI": "bidi-control",
|
|
373
|
+
"AG-INVIS": "invisible-char",
|
|
374
|
+
"AG-HOMO": "homoglyph",
|
|
375
|
+
"MH001": "known-cjk-corruption",
|
|
376
|
+
"MH002": "mixed-script-token",
|
|
377
|
+
"MH003": "isolated-cjk",
|
|
378
|
+
}
|
|
379
|
+
_SARIF_LEVEL = {"high": "error", "medium": "warning", "low": "note"}
|
|
380
|
+
|
|
381
|
+
rules = [
|
|
382
|
+
{
|
|
383
|
+
"id": rid,
|
|
384
|
+
"name": _RULE_NAMES.get(rid, rid),
|
|
385
|
+
"shortDescription": {"text": _RULE_DESCRIPTIONS.get(rid, rid)},
|
|
386
|
+
"defaultConfiguration": {"level": "error"},
|
|
387
|
+
}
|
|
388
|
+
for rid in rule_ids_seen
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
results = [
|
|
392
|
+
{
|
|
393
|
+
"ruleId": issue.rule_id,
|
|
394
|
+
"level": _SARIF_LEVEL.get(issue.severity, "warning"),
|
|
395
|
+
"message": {"text": issue.message},
|
|
396
|
+
"locations": [
|
|
397
|
+
{
|
|
398
|
+
"physicalLocation": {
|
|
399
|
+
"artifactLocation": {"uri": fp_str},
|
|
400
|
+
"region": {
|
|
401
|
+
"startLine": issue.line,
|
|
402
|
+
"startColumn": issue.col,
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
}
|
|
408
|
+
for fp_str, issue in file_issues
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
sarif = {
|
|
412
|
+
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json",
|
|
413
|
+
"version": "2.1.0",
|
|
414
|
+
"runs": [
|
|
415
|
+
{
|
|
416
|
+
"tool": {
|
|
417
|
+
"driver": {
|
|
418
|
+
"name": "agentgate",
|
|
419
|
+
"version": "0.1.0",
|
|
420
|
+
"informationUri": "https://github.com/hryoma1217/agentgate",
|
|
421
|
+
"rules": rules,
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
"results": results,
|
|
425
|
+
}
|
|
426
|
+
],
|
|
427
|
+
}
|
|
428
|
+
_safe_print(_json.dumps(sarif, ensure_ascii=False, indent=2))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
# checks subcommand
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
def cmd_checks(args: List[str]) -> int:
|
|
436
|
+
"""agentgate checks -- list registered checks, enabled state, and dep status."""
|
|
437
|
+
from .registry import get_all
|
|
438
|
+
|
|
439
|
+
config_path: Optional[Path] = None
|
|
440
|
+
for i, arg in enumerate(args):
|
|
441
|
+
if arg in ("--config", "-c") and i + 1 < len(args):
|
|
442
|
+
config_path = Path(args[i + 1])
|
|
443
|
+
elif arg.startswith("--config="):
|
|
444
|
+
config_path = Path(arg.split("=", 1)[1])
|
|
445
|
+
|
|
446
|
+
cfg = load_config(explicit_path=config_path)
|
|
447
|
+
registry = get_all()
|
|
448
|
+
|
|
449
|
+
print("agentgate checks:")
|
|
450
|
+
print()
|
|
451
|
+
|
|
452
|
+
# unicode check
|
|
453
|
+
unicode_enabled = cfg.unicode.enabled
|
|
454
|
+
unicode_status = "enabled" if unicode_enabled else "disabled"
|
|
455
|
+
print(f" unicode {unicode_status} (stdlib, no extra deps)")
|
|
456
|
+
|
|
457
|
+
# cjk check
|
|
458
|
+
cjk_enabled = cfg.cjk.enabled
|
|
459
|
+
cjk_status = "enabled" if cjk_enabled else "disabled"
|
|
460
|
+
mojihen_ok = False
|
|
461
|
+
try:
|
|
462
|
+
import mojihen # type: ignore # noqa: F401
|
|
463
|
+
mojihen_ok = True
|
|
464
|
+
except ImportError:
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
if cjk_enabled and not mojihen_ok:
|
|
468
|
+
print(f" cjk {cjk_status} [MISSING: mojihen -- run: pip install agent-write-gate[cjk]]")
|
|
469
|
+
elif cjk_enabled and mojihen_ok:
|
|
470
|
+
print(f" cjk {cjk_status} (mojihen installed)")
|
|
471
|
+
else:
|
|
472
|
+
mojihen_note = "(mojihen installed)" if mojihen_ok else "(mojihen not installed)"
|
|
473
|
+
print(f" cjk {cjk_status} {mojihen_note}")
|
|
474
|
+
|
|
475
|
+
# Third-party checks
|
|
476
|
+
for name in registry:
|
|
477
|
+
if name not in ("unicode", "cjk"):
|
|
478
|
+
print(f" {name:<10} enabled (third-party)")
|
|
479
|
+
|
|
480
|
+
print()
|
|
481
|
+
return 0
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
# Main entry point
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
def _safe_print(text: str) -> None:
|
|
489
|
+
try:
|
|
490
|
+
print(text)
|
|
491
|
+
except UnicodeEncodeError:
|
|
492
|
+
enc = sys.stdout.encoding or "utf-8"
|
|
493
|
+
print(text.encode(enc, errors="replace").decode(enc, errors="replace"))
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _print_help() -> None:
|
|
497
|
+
msg = (
|
|
498
|
+
"agentgate -- agent-hook safety gate for AI-written code\n\n"
|
|
499
|
+
"Usage:\n"
|
|
500
|
+
" agentgate hook --stdin Hook entrypoint (pipe JSON from agent)\n"
|
|
501
|
+
" agentgate scan PATH... [--format tty|json|sarif] Scan files\n"
|
|
502
|
+
" agentgate checks List checks and their status\n"
|
|
503
|
+
" agentgate --version\n\n"
|
|
504
|
+
"Options:\n"
|
|
505
|
+
" --config PATH Explicit config file (agentgate.toml or pyproject.toml)\n"
|
|
506
|
+
" -h, --help Show this help\n\n"
|
|
507
|
+
"Exit codes (hook): 0 = allow; 2 = block or error\n"
|
|
508
|
+
"Exit codes (scan): 0 = no blocking issues; 1 = blocking issues; 2 = error\n"
|
|
509
|
+
)
|
|
510
|
+
try:
|
|
511
|
+
print(msg, file=sys.stderr)
|
|
512
|
+
except UnicodeEncodeError:
|
|
513
|
+
print(msg.encode("ascii", errors="replace").decode("ascii"), file=sys.stderr)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
517
|
+
_enable_utf8_io()
|
|
518
|
+
if argv is None:
|
|
519
|
+
argv = sys.argv[1:]
|
|
520
|
+
|
|
521
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
522
|
+
_print_help()
|
|
523
|
+
return 0 if argv else 2
|
|
524
|
+
|
|
525
|
+
if argv[0] == "--version":
|
|
526
|
+
from . import __version__
|
|
527
|
+
print(f"agentgate {__version__}")
|
|
528
|
+
return 0
|
|
529
|
+
|
|
530
|
+
from .config import ConfigError
|
|
531
|
+
try:
|
|
532
|
+
if argv[0] == "hook":
|
|
533
|
+
return cmd_hook(argv[1:])
|
|
534
|
+
|
|
535
|
+
if argv[0] == "scan":
|
|
536
|
+
return cmd_scan(argv[1:])
|
|
537
|
+
|
|
538
|
+
if argv[0] == "checks":
|
|
539
|
+
return cmd_checks(argv[1:])
|
|
540
|
+
except ConfigError as exc:
|
|
541
|
+
print(f"agentgate: config error -- {exc}", file=sys.stderr)
|
|
542
|
+
return 2
|
|
543
|
+
|
|
544
|
+
print(f"agentgate: unknown subcommand {argv[0]!r}", file=sys.stderr)
|
|
545
|
+
_print_help()
|
|
546
|
+
return 2
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
if __name__ == "__main__":
|
|
550
|
+
sys.exit(main())
|