@0dai-dev/cli 4.3.6 → 4.3.7
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.
- package/README.md +12 -11
- package/bin/0dai.js +127 -30
- package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
- package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
- package/lib/ai/registry/mcp-catalog.json +98 -0
- package/lib/commands/auth.js +2 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/doctor.js +506 -12
- package/lib/commands/experience.js +40 -5
- package/lib/commands/feedback.js +157 -15
- package/lib/commands/gh.js +26 -0
- package/lib/commands/graph.js +9 -4
- package/lib/commands/heatmap.js +1 -1
- package/lib/commands/init.js +209 -27
- package/lib/commands/mcp.js +111 -33
- package/lib/commands/models.js +138 -41
- package/lib/commands/provider.js +30 -59
- package/lib/commands/quota.js +1 -1
- package/lib/commands/receipt.js +1 -1
- package/lib/commands/run.js +14 -6
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +38 -10
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/update.js +184 -38
- package/lib/commands/usage.js +1 -1
- package/lib/commands/validate.js +32 -3
- package/lib/commands/vault.js +43 -8
- package/lib/python/__init__.py +0 -0
- package/lib/python/agent_quotas.py +525 -0
- package/lib/python/anomaly_alert.py +397 -0
- package/lib/python/anti_pattern_detector.py +799 -0
- package/lib/python/auth.py +443 -0
- package/lib/python/capi_profile_guard.py +477 -0
- package/lib/python/compliance_report.py +581 -0
- package/lib/python/drift_detector.py +388 -0
- package/lib/python/experience_pipeline.py +1130 -0
- package/lib/python/graph.py +19 -0
- package/lib/python/graph_core.py +293 -0
- package/lib/python/graph_io.py +179 -0
- package/lib/python/graph_legacy.py +2052 -0
- package/lib/python/graph_legacy_helpers.py +221 -0
- package/lib/python/graph_outcomes_core.py +85 -0
- package/lib/python/graph_queries.py +171 -0
- package/lib/python/graph_slice.py +198 -0
- package/lib/python/graph_slicer.py +576 -0
- package/lib/python/graph_slicer_cli.py +60 -0
- package/lib/python/graph_validation.py +64 -0
- package/lib/python/heatmap.py +934 -0
- package/lib/python/json_utils.py +193 -0
- package/lib/python/mcp_exposure_check.py +247 -0
- package/lib/python/model_router.py +1434 -0
- package/lib/python/project_manager.py +621 -0
- package/lib/python/provider_profiles.py +1618 -0
- package/lib/python/provider_registry.py +1211 -0
- package/lib/python/provider_registry_cli.py +125 -0
- package/lib/python/receipt_png.py +727 -0
- package/lib/python/structural_memory.py +325 -0
- package/lib/python/swarm_cost.py +177 -0
- package/lib/python/usage_ledger.py +569 -0
- package/lib/scripts/mcp_tier_config.py +240 -0
- package/lib/shared.js +95 -12
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +1 -4
- package/lib/utils/constants.js +7 -1
- package/lib/utils/identity.js +184 -0
- package/lib/utils/mcp-auth.js +81 -15
- package/lib/utils/plan.js +1 -1
- package/lib/vault/index.js +19 -3
- package/lib/vault/storage.js +21 -2
- package/lib/wizard.js +5 -2
- package/package.json +9 -3
- package/scripts/build-python-bundle.js +106 -0
- package/scripts/build-tui.js +14 -1
- package/scripts/harvest_experience.py +523 -0
- package/scripts/postinstall.js +15 -9
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Structured anomaly emitter and optional operator alert bridge.
|
|
3
|
+
|
|
4
|
+
This module is intentionally usable both as a library from guards and as a
|
|
5
|
+
small CLI from session scripts. It records every anomaly as JSONL and only
|
|
6
|
+
calls the operator alert path when explicitly enabled, so tests and local dry
|
|
7
|
+
runs do not send Telegram messages accidentally.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import pathlib
|
|
16
|
+
import re
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
REPO_ROOT = pathlib.Path(
|
|
23
|
+
os.environ.get("ODAI_REPO_ROOT", str(pathlib.Path(__file__).resolve().parent.parent))
|
|
24
|
+
)
|
|
25
|
+
STATE_ROOT = pathlib.Path(os.environ.get("ODAI_STATE_ROOT", "/srv/0dai-state"))
|
|
26
|
+
TAXONOMY_PATH = pathlib.Path(
|
|
27
|
+
os.environ.get(
|
|
28
|
+
"ODAI_ANOMALY_TAXONOMY",
|
|
29
|
+
str(REPO_ROOT / "ai" / "manifest" / "agent-anomaly-taxonomy.json"),
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
ANOMALY_LOG = pathlib.Path(
|
|
33
|
+
os.environ.get("ODAI_ANOMALY_LOG", str(STATE_ROOT / "anomalies" / "events.jsonl"))
|
|
34
|
+
)
|
|
35
|
+
ALERT_STATE = pathlib.Path(
|
|
36
|
+
os.environ.get("ODAI_ANOMALY_ALERT_STATE", str(STATE_ROOT / "anomalies" / "alert-state.json"))
|
|
37
|
+
)
|
|
38
|
+
ASK_OPERATOR = pathlib.Path(
|
|
39
|
+
os.environ.get("ODAI_ASK_OPERATOR", str(REPO_ROOT / "scripts" / "ask_operator.py"))
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
TRUTHY = {"1", "true", "yes", "on"}
|
|
43
|
+
DEFAULT_ALERT_TTL_SECONDS = 30 * 60
|
|
44
|
+
MAX_SUMMARY_LEN = 240
|
|
45
|
+
GIT_STATUS_PATH_OFFSET = 3
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _now_iso() -> str:
|
|
49
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _truthy_env(name: str) -> bool:
|
|
53
|
+
return os.environ.get(name, "").strip().lower() in TRUTHY
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_json(path: pathlib.Path, default: Any) -> Any:
|
|
57
|
+
try:
|
|
58
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
59
|
+
except (json.JSONDecodeError, OSError):
|
|
60
|
+
return default
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_taxonomy(path: pathlib.Path | None = None) -> dict[str, Any]:
|
|
64
|
+
data = _read_json(path or TAXONOMY_PATH, {})
|
|
65
|
+
return data if isinstance(data, dict) else {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_anomaly_type(anomaly_type: str, taxonomy: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
69
|
+
taxonomy = taxonomy or load_taxonomy()
|
|
70
|
+
types = taxonomy.get("types", {})
|
|
71
|
+
if isinstance(types, dict) and isinstance(types.get(anomaly_type), dict):
|
|
72
|
+
return dict(types[anomaly_type])
|
|
73
|
+
category = anomaly_type.split(".", 1)[0] if "." in anomaly_type else "unknown"
|
|
74
|
+
return {
|
|
75
|
+
"category": category,
|
|
76
|
+
"severity": "warning",
|
|
77
|
+
"response": ["telemetry", "review_required"],
|
|
78
|
+
"description": "Unregistered anomaly type; update agent-anomaly-taxonomy.json.",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _shorten_summary(summary: str) -> str:
|
|
83
|
+
summary = " ".join(summary.split())
|
|
84
|
+
if len(summary) <= MAX_SUMMARY_LEN:
|
|
85
|
+
return summary
|
|
86
|
+
return summary[: MAX_SUMMARY_LEN - 3] + "..."
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _current_agent() -> str:
|
|
90
|
+
return (
|
|
91
|
+
os.environ.get("AGENT_ID")
|
|
92
|
+
or os.environ.get("TMUX_SESSION")
|
|
93
|
+
or os.environ.get("HOSTNAME")
|
|
94
|
+
or "unknown"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _dedupe_key(record: dict[str, Any]) -> str:
|
|
99
|
+
context = record.get("context") if isinstance(record.get("context"), dict) else {}
|
|
100
|
+
stable_context = {
|
|
101
|
+
key: context.get(key)
|
|
102
|
+
for key in ("command_hash", "rule_id", "working_tree", "task", "outside_scope")
|
|
103
|
+
if context.get(key)
|
|
104
|
+
}
|
|
105
|
+
basis = {
|
|
106
|
+
"type": record.get("type"),
|
|
107
|
+
"agent_id": record.get("agent_id"),
|
|
108
|
+
"summary": record.get("summary"),
|
|
109
|
+
"context": stable_context,
|
|
110
|
+
}
|
|
111
|
+
raw = json.dumps(basis, sort_keys=True, separators=(",", ":"))
|
|
112
|
+
return hashlib.sha256(raw.encode("utf-8", errors="replace")).hexdigest()[:16]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _write_jsonl(path: pathlib.Path, payload: dict[str, Any]) -> None:
|
|
116
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
118
|
+
fh.write(json.dumps(payload, ensure_ascii=True, sort_keys=True) + "\n")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _load_alert_state() -> dict[str, Any]:
|
|
122
|
+
data = _read_json(ALERT_STATE, {})
|
|
123
|
+
return data if isinstance(data, dict) else {}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _save_alert_state(state: dict[str, Any]) -> None:
|
|
127
|
+
ALERT_STATE.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
ALERT_STATE.write_text(json.dumps(state, ensure_ascii=True, sort_keys=True) + "\n", encoding="utf-8")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _timestamp(value: str) -> float:
|
|
132
|
+
try:
|
|
133
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
|
|
134
|
+
except ValueError:
|
|
135
|
+
return 0.0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _alert_ttl_seconds() -> int:
|
|
139
|
+
raw = os.environ.get("ODAI_ANOMALY_ALERT_TTL_SECONDS", str(DEFAULT_ALERT_TTL_SECONDS))
|
|
140
|
+
try:
|
|
141
|
+
return max(0, int(raw))
|
|
142
|
+
except ValueError:
|
|
143
|
+
return DEFAULT_ALERT_TTL_SECONDS
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _send_operator_alert(record: dict[str, Any], *, dry_run: bool) -> dict[str, Any]:
|
|
147
|
+
if dry_run:
|
|
148
|
+
return {"status": "suppressed", "reason": "dry_run"}
|
|
149
|
+
if not _truthy_env("ODAI_ANOMALY_ALERTS"):
|
|
150
|
+
return {"status": "suppressed", "reason": "alerts_disabled"}
|
|
151
|
+
|
|
152
|
+
dedupe_key = str(record["dedupe_key"])
|
|
153
|
+
state = _load_alert_state()
|
|
154
|
+
now = datetime.now(timezone.utc).timestamp()
|
|
155
|
+
last = _timestamp(str(state.get(dedupe_key, "")))
|
|
156
|
+
ttl = _alert_ttl_seconds()
|
|
157
|
+
if ttl and last and now - last < ttl:
|
|
158
|
+
return {"status": "suppressed", "reason": "duplicate", "ttl_seconds": ttl}
|
|
159
|
+
|
|
160
|
+
if not ASK_OPERATOR.is_file():
|
|
161
|
+
return {"status": "failed", "reason": "ask_operator_missing", "path": str(ASK_OPERATOR)}
|
|
162
|
+
|
|
163
|
+
title = f"Agent anomaly: {record['type']}"
|
|
164
|
+
body = "\n".join(
|
|
165
|
+
[
|
|
166
|
+
f"- severity: {record['severity']}",
|
|
167
|
+
f"- agent: {record['agent_id']}",
|
|
168
|
+
f"- source: {record['source']}",
|
|
169
|
+
f"- summary: {record['summary']}",
|
|
170
|
+
f"- dedupe: {dedupe_key}",
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
proc = subprocess.run( # noqa: S603 - fixed local helper path and arguments.
|
|
174
|
+
[
|
|
175
|
+
sys.executable,
|
|
176
|
+
str(ASK_OPERATOR),
|
|
177
|
+
"--kind",
|
|
178
|
+
"ALERT",
|
|
179
|
+
"--id",
|
|
180
|
+
f"anomaly-{dedupe_key}",
|
|
181
|
+
title,
|
|
182
|
+
body,
|
|
183
|
+
],
|
|
184
|
+
capture_output=True,
|
|
185
|
+
check=False,
|
|
186
|
+
text=True,
|
|
187
|
+
timeout=20,
|
|
188
|
+
)
|
|
189
|
+
if proc.returncode != 0:
|
|
190
|
+
return {
|
|
191
|
+
"status": "failed",
|
|
192
|
+
"reason": "ask_operator_failed",
|
|
193
|
+
"returncode": proc.returncode,
|
|
194
|
+
"stderr": proc.stderr[-500:],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
state[dedupe_key] = _now_iso()
|
|
198
|
+
_save_alert_state(state)
|
|
199
|
+
return {"status": "sent", "tool": str(ASK_OPERATOR)}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def emit_anomaly(
|
|
203
|
+
anomaly_type: str,
|
|
204
|
+
summary: str,
|
|
205
|
+
*,
|
|
206
|
+
source: str = "unknown",
|
|
207
|
+
severity: str = "",
|
|
208
|
+
agent_id: str = "",
|
|
209
|
+
session: str = "",
|
|
210
|
+
task: str = "",
|
|
211
|
+
context: dict[str, Any] | None = None,
|
|
212
|
+
alert: bool | None = None,
|
|
213
|
+
dry_run: bool = False,
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
"""Append a structured anomaly event and optionally alert the operator."""
|
|
216
|
+
taxonomy = load_taxonomy()
|
|
217
|
+
meta = get_anomaly_type(anomaly_type, taxonomy)
|
|
218
|
+
response = [str(item) for item in meta.get("response", ["telemetry"])]
|
|
219
|
+
effective_severity = severity or str(meta.get("severity") or "warning")
|
|
220
|
+
record: dict[str, Any] = {
|
|
221
|
+
"ts": _now_iso(),
|
|
222
|
+
"event": "anomaly",
|
|
223
|
+
"type": anomaly_type,
|
|
224
|
+
"category": str(meta.get("category") or anomaly_type.split(".", 1)[0]),
|
|
225
|
+
"severity": effective_severity,
|
|
226
|
+
"response": response,
|
|
227
|
+
"source": source,
|
|
228
|
+
"agent_id": agent_id or _current_agent(),
|
|
229
|
+
"session": session or os.environ.get("TMUX_SESSION", ""),
|
|
230
|
+
"task": task,
|
|
231
|
+
"summary": _shorten_summary(summary),
|
|
232
|
+
"context": context or {},
|
|
233
|
+
}
|
|
234
|
+
record["dedupe_key"] = _dedupe_key(record)
|
|
235
|
+
|
|
236
|
+
should_alert = alert if alert is not None else "operator_alert" in response
|
|
237
|
+
if should_alert:
|
|
238
|
+
record["operator_alert"] = _send_operator_alert(record, dry_run=dry_run)
|
|
239
|
+
|
|
240
|
+
_write_jsonl(ANOMALY_LOG, record)
|
|
241
|
+
return record
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _split_scope(scope: str) -> list[str]:
|
|
245
|
+
tokens = [item for item in re.split(r"[\s,;]+", scope.strip()) if item]
|
|
246
|
+
return [token.strip("./") for token in tokens if token not in {"-", "*"}]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _path_in_scope(path: str, scope_tokens: list[str]) -> bool:
|
|
250
|
+
normalized = path.strip().strip("./")
|
|
251
|
+
for token in scope_tokens:
|
|
252
|
+
scope_token = token.rstrip("/")
|
|
253
|
+
if normalized == scope_token or normalized.startswith(scope_token + "/"):
|
|
254
|
+
return True
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _git_dirty_paths(working_tree: pathlib.Path) -> list[str]:
|
|
259
|
+
proc = subprocess.run( # noqa: S603 - read-only git status in caller-selected worktree.
|
|
260
|
+
["git", "-C", str(working_tree), "status", "--short", "--untracked-files=all"], # noqa: S607
|
|
261
|
+
capture_output=True,
|
|
262
|
+
check=False,
|
|
263
|
+
text=True,
|
|
264
|
+
timeout=10,
|
|
265
|
+
)
|
|
266
|
+
if proc.returncode != 0:
|
|
267
|
+
return []
|
|
268
|
+
paths: list[str] = []
|
|
269
|
+
for line in proc.stdout.splitlines():
|
|
270
|
+
if len(line) < GIT_STATUS_PATH_OFFSET + 1:
|
|
271
|
+
continue
|
|
272
|
+
path = line[GIT_STATUS_PATH_OFFSET:]
|
|
273
|
+
if " -> " in path:
|
|
274
|
+
path = path.rsplit(" -> ", 1)[-1]
|
|
275
|
+
paths.append(path)
|
|
276
|
+
return paths
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def dirty_scan(
|
|
280
|
+
*,
|
|
281
|
+
working_tree: pathlib.Path,
|
|
282
|
+
file_scope: str,
|
|
283
|
+
source: str = "dirty-scan",
|
|
284
|
+
agent_id: str = "",
|
|
285
|
+
task: str = "",
|
|
286
|
+
dry_run: bool = False,
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
dirty_paths = _git_dirty_paths(working_tree)
|
|
289
|
+
if not dirty_paths:
|
|
290
|
+
return {"ok": True, "dirty_paths": [], "anomaly": None}
|
|
291
|
+
|
|
292
|
+
scope_tokens = _split_scope(file_scope)
|
|
293
|
+
if not scope_tokens:
|
|
294
|
+
anomaly = emit_anomaly(
|
|
295
|
+
"workspace.dirty_without_claim_scope",
|
|
296
|
+
"Dirty worktree observed without declared file scope",
|
|
297
|
+
source=source,
|
|
298
|
+
agent_id=agent_id,
|
|
299
|
+
task=task,
|
|
300
|
+
context={"working_tree": str(working_tree), "dirty_paths": dirty_paths},
|
|
301
|
+
dry_run=dry_run,
|
|
302
|
+
)
|
|
303
|
+
return {"ok": False, "dirty_paths": dirty_paths, "outside_scope": dirty_paths, "anomaly": anomaly}
|
|
304
|
+
|
|
305
|
+
outside = [path for path in dirty_paths if not _path_in_scope(path, scope_tokens)]
|
|
306
|
+
if not outside:
|
|
307
|
+
return {"ok": True, "dirty_paths": dirty_paths, "outside_scope": [], "anomaly": None}
|
|
308
|
+
|
|
309
|
+
anomaly = emit_anomaly(
|
|
310
|
+
"workspace.unowned_change_observed",
|
|
311
|
+
"Dirty paths observed outside declared file scope",
|
|
312
|
+
source=source,
|
|
313
|
+
agent_id=agent_id,
|
|
314
|
+
task=task,
|
|
315
|
+
context={
|
|
316
|
+
"working_tree": str(working_tree),
|
|
317
|
+
"file_scope": file_scope,
|
|
318
|
+
"dirty_paths": dirty_paths,
|
|
319
|
+
"outside_scope": outside,
|
|
320
|
+
},
|
|
321
|
+
dry_run=dry_run,
|
|
322
|
+
)
|
|
323
|
+
return {"ok": False, "dirty_paths": dirty_paths, "outside_scope": outside, "anomaly": anomaly}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _json_context(values: list[str]) -> dict[str, Any]:
|
|
327
|
+
context: dict[str, Any] = {}
|
|
328
|
+
for item in values:
|
|
329
|
+
if "=" not in item:
|
|
330
|
+
continue
|
|
331
|
+
key, value = item.split("=", 1)
|
|
332
|
+
context[key] = value
|
|
333
|
+
return context
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
337
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
338
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
339
|
+
|
|
340
|
+
emit = sub.add_parser("emit", help="emit one anomaly event")
|
|
341
|
+
emit.add_argument("--type", required=True, dest="anomaly_type")
|
|
342
|
+
emit.add_argument("--summary", required=True)
|
|
343
|
+
emit.add_argument("--source", default="manual")
|
|
344
|
+
emit.add_argument("--severity", default="")
|
|
345
|
+
emit.add_argument("--agent", default="")
|
|
346
|
+
emit.add_argument("--session", default="")
|
|
347
|
+
emit.add_argument("--task", default="")
|
|
348
|
+
emit.add_argument("--context", action="append", default=[], help="key=value context item")
|
|
349
|
+
emit.add_argument("--alert", action="store_true")
|
|
350
|
+
emit.add_argument("--dry-run", action="store_true")
|
|
351
|
+
|
|
352
|
+
scan = sub.add_parser("dirty-scan", help="detect dirty paths outside declared file scope")
|
|
353
|
+
scan.add_argument("--working-tree", default=".")
|
|
354
|
+
scan.add_argument("--file-scope", default="")
|
|
355
|
+
scan.add_argument("--source", default="dirty-scan")
|
|
356
|
+
scan.add_argument("--agent", default="")
|
|
357
|
+
scan.add_argument("--task", default="")
|
|
358
|
+
scan.add_argument("--dry-run", action="store_true")
|
|
359
|
+
scan.add_argument("--fail-on-anomaly", action="store_true")
|
|
360
|
+
return parser
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def main(argv: list[str] | None = None) -> int:
|
|
364
|
+
args = _build_parser().parse_args(argv)
|
|
365
|
+
if args.command == "emit":
|
|
366
|
+
record = emit_anomaly(
|
|
367
|
+
args.anomaly_type,
|
|
368
|
+
args.summary,
|
|
369
|
+
source=args.source,
|
|
370
|
+
severity=args.severity,
|
|
371
|
+
agent_id=args.agent,
|
|
372
|
+
session=args.session,
|
|
373
|
+
task=args.task,
|
|
374
|
+
context=_json_context(args.context),
|
|
375
|
+
alert=args.alert or None,
|
|
376
|
+
dry_run=args.dry_run,
|
|
377
|
+
)
|
|
378
|
+
sys.stdout.write(json.dumps(record, ensure_ascii=True, sort_keys=True) + "\n")
|
|
379
|
+
return 0
|
|
380
|
+
|
|
381
|
+
if args.command == "dirty-scan":
|
|
382
|
+
result = dirty_scan(
|
|
383
|
+
working_tree=pathlib.Path(args.working_tree),
|
|
384
|
+
file_scope=args.file_scope,
|
|
385
|
+
source=args.source,
|
|
386
|
+
agent_id=args.agent,
|
|
387
|
+
task=args.task,
|
|
388
|
+
dry_run=args.dry_run,
|
|
389
|
+
)
|
|
390
|
+
sys.stdout.write(json.dumps(result, ensure_ascii=True, sort_keys=True) + "\n")
|
|
391
|
+
return 1 if args.fail_on_anomaly and not result.get("ok") else 0
|
|
392
|
+
|
|
393
|
+
return 2
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
if __name__ == "__main__":
|
|
397
|
+
raise SystemExit(main())
|