abom-cli 0.1.1__tar.gz → 0.1.3__tar.gz

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.
Files changed (40) hide show
  1. {abom_cli-0.1.1 → abom_cli-0.1.3}/PKG-INFO +3 -1
  2. {abom_cli-0.1.1 → abom_cli-0.1.3}/README.md +2 -0
  3. {abom_cli-0.1.1 → abom_cli-0.1.3}/pyproject.toml +1 -1
  4. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/__init__.py +1 -1
  5. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/bom.py +12 -0
  6. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/cli.py +16 -2
  7. abom_cli-0.1.3/src/abom/log.py +66 -0
  8. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/scan.py +12 -0
  9. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/sign.py +11 -0
  10. abom_cli-0.1.3/tests/test_log.py +36 -0
  11. {abom_cli-0.1.1 → abom_cli-0.1.3}/.env.example +0 -0
  12. {abom_cli-0.1.1 → abom_cli-0.1.3}/.gitignore +0 -0
  13. {abom_cli-0.1.1 → abom_cli-0.1.3}/Dockerfile +0 -0
  14. {abom_cli-0.1.1 → abom_cli-0.1.3}/MVP_SPEC.md +0 -0
  15. {abom_cli-0.1.1 → abom_cli-0.1.3}/Makefile +0 -0
  16. {abom_cli-0.1.1 → abom_cli-0.1.3}/charts/abom/Chart.yaml +0 -0
  17. {abom_cli-0.1.1 → abom_cli-0.1.3}/charts/abom/values.yaml +0 -0
  18. {abom_cli-0.1.1 → abom_cli-0.1.3}/demo/README.md +0 -0
  19. {abom_cli-0.1.1 → abom_cli-0.1.3}/demo/demo.py +0 -0
  20. {abom_cli-0.1.1 → abom_cli-0.1.3}/demo/sample_repo/tests.py +0 -0
  21. {abom_cli-0.1.1 → abom_cli-0.1.3}/demo/sample_repo/validator.py +0 -0
  22. {abom_cli-0.1.1 → abom_cli-0.1.3}/docker-compose.yml +0 -0
  23. {abom_cli-0.1.1 → abom_cli-0.1.3}/scripts/init_db.py +0 -0
  24. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/agents.py +0 -0
  25. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/api.py +0 -0
  26. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/audit.py +0 -0
  27. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/config.py +0 -0
  28. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/db.py +0 -0
  29. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/execution.py +0 -0
  30. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/models_router.py +0 -0
  31. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/orchestration.py +0 -0
  32. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/policy.py +0 -0
  33. {abom_cli-0.1.1 → abom_cli-0.1.3}/src/abom/schemas.py +0 -0
  34. {abom_cli-0.1.1 → abom_cli-0.1.3}/tests/fixtures/sample-agent/agent.py +0 -0
  35. {abom_cli-0.1.1 → abom_cli-0.1.3}/tests/fixtures/sample-agent/mcp.json +0 -0
  36. {abom_cli-0.1.1 → abom_cli-0.1.3}/tests/fixtures/sample-agent/prompts/system.txt +0 -0
  37. {abom_cli-0.1.1 → abom_cli-0.1.3}/tests/fixtures/sample-agent/requirements.txt +0 -0
  38. {abom_cli-0.1.1 → abom_cli-0.1.3}/tests/test_audit_chain.py +0 -0
  39. {abom_cli-0.1.1 → abom_cli-0.1.3}/tests/test_scan.py +0 -0
  40. {abom_cli-0.1.1 → abom_cli-0.1.3}/tests/test_sign.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abom-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: ABOM — the Agent Bill of Materials. Scan, sign, and verify what your AI agents are made of.
5
5
  Project-URL: Homepage, https://abom.ai
6
6
  Project-URL: Repository, https://github.com/josephassiga/abom
@@ -59,6 +59,8 @@ abom verify abom.json --policy policy.json # + enforce a policy (exit 1 on vio
59
59
  | `abom keygen` | Show (or create) the local ed25519 signing key (`~/.abom/signing_key.pem`, override with `ABOM_KEY`). |
60
60
  | `abom version` | Print the tool and spec versions. |
61
61
 
62
+ All commands accept **`-v`** (info) / **`-vv`** (debug) / **`-q`** (errors only) / **`--json-logs`** (NDJSON for CI) — logs go to stderr, so the ABOM on stdout stays clean.
63
+
62
64
  ### Example
63
65
 
64
66
  ```
@@ -19,6 +19,8 @@ abom verify abom.json --policy policy.json # + enforce a policy (exit 1 on vio
19
19
  | `abom keygen` | Show (or create) the local ed25519 signing key (`~/.abom/signing_key.pem`, override with `ABOM_KEY`). |
20
20
  | `abom version` | Print the tool and spec versions. |
21
21
 
22
+ All commands accept **`-v`** (info) / **`-vv`** (debug) / **`-q`** (errors only) / **`--json-logs`** (NDJSON for CI) — logs go to stderr, so the ABOM on stdout stays clean.
23
+
22
24
  ### Example
23
25
 
24
26
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "abom-cli"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "ABOM — the Agent Bill of Materials. Scan, sign, and verify what your AI agents are made of."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """ABOM — the Agent Bill of Materials. Scan, sign, and verify AI agents."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.2"
@@ -14,11 +14,13 @@ from __future__ import annotations
14
14
 
15
15
  import dataclasses
16
16
  import hashlib
17
+ import logging
17
18
 
18
19
  from . import sign as _sign
19
20
  from .audit import GENESIS, canonical_json, make_record, verify_chain
20
21
 
21
22
  ABOM_VERSION = "0.1"
23
+ log = logging.getLogger("abom.bom")
22
24
 
23
25
 
24
26
  def _sha256(obj) -> str:
@@ -44,6 +46,8 @@ def build_composition(agent: dict, components: list[dict], controls: dict,
44
46
  "agent": agent, "components": components, "controls": controls,
45
47
  }
46
48
  body["composition_sha256"] = _sha256(body)
49
+ log.debug("composition built: %d components, sha256=%s…",
50
+ len(components), body["composition_sha256"][:16])
47
51
  return _sign.sign_obj(body, key) if sign else body
48
52
 
49
53
 
@@ -143,5 +147,13 @@ def verify_abom(composition: dict, chain: list | None = None, policy: dict | Non
143
147
  findings.append({"rule": "approval_coverage", "seq": seq, "severity": "high",
144
148
  "detail": "consequential action without approval"})
145
149
 
150
+ log.info("verify: %d finding(s) over %d components / %d action(s)",
151
+ len(findings), len(composition.get("components", [])), len(chain),
152
+ extra={"event": "verify_done", "findings": len(findings),
153
+ "components": len(composition.get("components", [])), "actions": len(chain)})
154
+ for f in findings:
155
+ log.debug("finding: [%s] %s — %s", f.get("severity"), f.get("rule"), f.get("detail"),
156
+ extra={"event": "finding", "rule": f.get("rule"),
157
+ "severity": f.get("severity"), "detail": f.get("detail")})
146
158
  return {"ok": len(findings) == 0, "findings": findings,
147
159
  "actions": len(chain), "components": len(composition.get("components", []))}
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
 
15
15
  import typer
16
16
 
17
- from . import __version__, bom, scan as scanner, sign
17
+ from . import __version__, bom, log, scan as scanner, sign
18
18
 
19
19
  app = typer.Typer(
20
20
  add_completion=False,
@@ -35,8 +35,12 @@ def scan(
35
35
  sign_manifest: bool = typer.Option(True, "--sign/--no-sign", help="ed25519-sign the manifest."),
36
36
  name: str = typer.Option(None, "--name", help="Override the detected agent name."),
37
37
  version: str = typer.Option(None, "--agent-version", help="Override the detected agent version."),
38
+ verbose: int = typer.Option(0, "-v", "--verbose", count=True, help="-v info, -vv debug (to stderr)."),
39
+ quiet: bool = typer.Option(False, "-q", "--quiet", help="Errors only."),
40
+ json_logs: bool = typer.Option(False, "--json-logs", help="Structured NDJSON logs to stderr (CI)."),
38
41
  ):
39
42
  """Scan a repo and emit a signed ABOM Composition Manifest."""
43
+ log.setup(verbose, quiet, json_logs)
40
44
  root = Path(path)
41
45
  if not root.exists():
42
46
  typer.secho(f"path not found: {path}", fg="red", err=True)
@@ -79,8 +83,12 @@ def scan(
79
83
  def verify(
80
84
  abom_file: str = typer.Argument("abom.json", help="ABOM file to verify."),
81
85
  policy_file: str = typer.Option(None, "--policy", "-p", help="Policy JSON to enforce."),
86
+ verbose: int = typer.Option(0, "-v", "--verbose", count=True, help="-v info, -vv debug (to stderr)."),
87
+ quiet: bool = typer.Option(False, "-q", "--quiet", help="Errors only."),
88
+ json_logs: bool = typer.Option(False, "--json-logs", help="Structured NDJSON logs to stderr (CI)."),
82
89
  ):
83
90
  """Verify an ABOM's signature, and (with --policy) its compliance."""
91
+ log.setup(verbose, quiet, json_logs)
84
92
  try:
85
93
  doc = json.loads(Path(abom_file).read_text())
86
94
  except Exception as exc:
@@ -107,8 +115,14 @@ def verify(
107
115
 
108
116
 
109
117
  @app.command()
110
- def keygen(show_private: bool = typer.Option(False, "--show-private", help="Print the private key path.")):
118
+ def keygen(
119
+ show_private: bool = typer.Option(False, "--show-private", help="Print the private key path."),
120
+ verbose: int = typer.Option(0, "-v", "--verbose", count=True, help="-v info, -vv debug (to stderr)."),
121
+ quiet: bool = typer.Option(False, "-q", "--quiet", help="Errors only."),
122
+ json_logs: bool = typer.Option(False, "--json-logs", help="Structured NDJSON logs to stderr (CI)."),
123
+ ):
111
124
  """Show (or create) the local ed25519 signing key."""
125
+ log.setup(verbose, quiet, json_logs)
112
126
  key = sign.load_or_create_key()
113
127
  pub_b64 = sign._pub_b64(key.public_key())
114
128
  path = sign.default_key_path()
@@ -0,0 +1,66 @@
1
+ """Logging for the abom CLI.
2
+
3
+ Logs go to **stderr** so they never mix into the ABOM written to stdout.
4
+
5
+ (default) warnings + errors only
6
+ -v INFO — high-level steps (what was scanned / verified)
7
+ -vv DEBUG — per-dependency / per-file / per-check detail
8
+ -q / --quiet ERROR — errors only
9
+ --json-logs one NDJSON object per line on stderr (for CI pipelines)
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import datetime as _dt
14
+ import json
15
+ import logging
16
+ import sys
17
+
18
+ log = logging.getLogger("abom")
19
+
20
+ # Standard LogRecord attributes — everything else is treated as structured extra.
21
+ _STD = {
22
+ "name", "msg", "args", "levelname", "levelno", "pathname", "filename",
23
+ "module", "exc_info", "exc_text", "stack_info", "lineno", "funcName",
24
+ "created", "msecs", "relativeCreated", "thread", "threadName",
25
+ "processName", "process", "taskName", "message", "asctime",
26
+ }
27
+
28
+
29
+ class JsonFormatter(logging.Formatter):
30
+ """Render each record as a single JSON line, including any ``extra=`` fields."""
31
+
32
+ def format(self, record: logging.LogRecord) -> str:
33
+ data = {
34
+ "ts": _dt.datetime.fromtimestamp(record.created, _dt.timezone.utc)
35
+ .isoformat(timespec="milliseconds"),
36
+ "level": record.levelname,
37
+ "logger": record.name,
38
+ "msg": record.getMessage(),
39
+ }
40
+ for key, value in record.__dict__.items():
41
+ if key not in _STD and not key.startswith("_"):
42
+ data[key] = value
43
+ if record.exc_info:
44
+ data["exc"] = self.formatException(record.exc_info)
45
+ return json.dumps(data, default=str)
46
+
47
+
48
+ def setup(verbose: int = 0, quiet: bool = False, json_logs: bool = False) -> None:
49
+ if quiet:
50
+ level = logging.ERROR
51
+ elif verbose >= 2:
52
+ level = logging.DEBUG
53
+ elif verbose == 1:
54
+ level = logging.INFO
55
+ else:
56
+ level = logging.WARNING
57
+
58
+ handler = logging.StreamHandler(sys.stderr)
59
+ handler.setFormatter(
60
+ JsonFormatter() if json_logs
61
+ else logging.Formatter("%(levelname).1s %(name)s | %(message)s")
62
+ )
63
+ root = logging.getLogger("abom")
64
+ root.handlers[:] = [handler]
65
+ root.setLevel(level)
66
+ root.propagate = False
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import hashlib
11
11
  import json
12
+ import logging
12
13
  import re
13
14
  from pathlib import Path
14
15
 
@@ -17,6 +18,8 @@ try:
17
18
  except ModuleNotFoundError: # Python 3.10
18
19
  import tomli as tomllib
19
20
 
21
+ log = logging.getLogger("abom.scan")
22
+
20
23
  # --- known signals -----------------------------------------------------------
21
24
  FRAMEWORKS = {
22
25
  "langchain": "LangChain", "langchain-core": "LangChain", "langgraph": "LangGraph",
@@ -144,7 +147,10 @@ def _agent_meta(root: Path) -> dict:
144
147
 
145
148
  def scan(root: Path) -> dict:
146
149
  root = Path(root)
150
+ log.info("scanning %s", root.resolve())
147
151
  deps = parse_dependencies(root)
152
+ log.info("parsed %d dependencies", len(deps))
153
+ log.debug("dependencies: %s", ", ".join(sorted(deps)) or "(none)")
148
154
  components: list[dict] = []
149
155
  seen: set[tuple] = set()
150
156
 
@@ -153,6 +159,10 @@ def scan(root: Path) -> dict:
153
159
  if key not in seen:
154
160
  seen.add(key)
155
161
  components.append(comp)
162
+ log.debug("+ %-10s %-28s [%s]", comp["type"], comp.get("name", "?"),
163
+ comp.get("detected_from", "?"),
164
+ extra={"event": "component", "comp_type": comp["type"],
165
+ "comp_name": comp.get("name"), "source": comp.get("detected_from")})
156
166
 
157
167
  # 1. from dependencies
158
168
  for d in sorted(deps):
@@ -203,6 +213,8 @@ def scan(root: Path) -> dict:
203
213
  add({"type": "model", "name": name, "provenance": "referenced in source",
204
214
  "egress": provider == "external", "detected_from": "source"})
205
215
 
216
+ log.info("detected %d components", len(components),
217
+ extra={"event": "scan_done", "components": len(components)})
206
218
  return {
207
219
  "agent": _agent_meta(root),
208
220
  "components": components,
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import base64
11
11
  import hashlib
12
+ import logging
12
13
  import os
13
14
  from pathlib import Path
14
15
 
@@ -20,6 +21,8 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
20
21
 
21
22
  from .audit import canonical_json
22
23
 
24
+ log = logging.getLogger("abom.sign")
25
+
23
26
 
24
27
  def default_key_path() -> Path:
25
28
  return Path(os.environ.get("ABOM_KEY", str(Path.home() / ".abom" / "signing_key.pem")))
@@ -28,6 +31,7 @@ def default_key_path() -> Path:
28
31
  def load_or_create_key(path: Path | None = None) -> Ed25519PrivateKey:
29
32
  path = Path(path or default_key_path())
30
33
  if path.exists():
34
+ log.debug("loaded signing key from %s", path)
31
35
  return serialization.load_pem_private_key(path.read_bytes(), password=None)
32
36
  key = Ed25519PrivateKey.generate()
33
37
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -42,6 +46,7 @@ def load_or_create_key(path: Path | None = None) -> Ed25519PrivateKey:
42
46
  os.chmod(path, 0o600)
43
47
  except OSError:
44
48
  pass
49
+ log.info("generated new ed25519 signing key at %s", path)
45
50
  return key
46
51
 
47
52
 
@@ -60,6 +65,7 @@ def sign_obj(obj: dict, key: Ed25519PrivateKey | None = None) -> dict:
60
65
  body = {k: v for k, v in obj.items() if k != "signature"}
61
66
  sig = key.sign(canonical_json(body).encode("utf-8"))
62
67
  pub_b64 = _pub_b64(key.public_key())
68
+ log.debug("signed with key_id=%s", key_id(pub_b64))
63
69
  return {
64
70
  **obj,
65
71
  "signature": {
@@ -76,16 +82,21 @@ def verify_obj(obj: dict, trusted_keys: set[str] | None = None) -> bool:
76
82
  signer's key id must be in it."""
77
83
  sig = obj.get("signature") or {}
78
84
  if sig.get("alg") != "ed25519":
85
+ log.debug("verify: unsupported or missing signature alg %r", sig.get("alg"))
79
86
  return False
80
87
  pub_b64, value = sig.get("public_key"), sig.get("value")
81
88
  if not pub_b64 or not value:
89
+ log.debug("verify: signature missing public_key or value")
82
90
  return False
83
91
  if trusted_keys is not None and key_id(pub_b64) not in trusted_keys:
92
+ log.debug("verify: signer key_id %s not in trusted set", key_id(pub_b64))
84
93
  return False
85
94
  try:
86
95
  pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(pub_b64))
87
96
  body = {k: v for k, v in obj.items() if k != "signature"}
88
97
  pub.verify(base64.b64decode(value), canonical_json(body).encode("utf-8"))
98
+ log.debug("verify: signature OK (key_id=%s)", key_id(pub_b64))
89
99
  return True
90
100
  except Exception:
101
+ log.debug("verify: signature INVALID (key_id=%s)", key_id(pub_b64))
91
102
  return False
@@ -0,0 +1,36 @@
1
+ """Tests for the logging verbosity setup."""
2
+ import logging
3
+
4
+ from abom import log as logmod
5
+
6
+
7
+ def test_levels():
8
+ logmod.setup(verbose=0)
9
+ assert logging.getLogger("abom").level == logging.WARNING
10
+ logmod.setup(verbose=1)
11
+ assert logging.getLogger("abom").level == logging.INFO
12
+ logmod.setup(verbose=2)
13
+ assert logging.getLogger("abom").level == logging.DEBUG
14
+ logmod.setup(quiet=True)
15
+ assert logging.getLogger("abom").level == logging.ERROR
16
+
17
+
18
+ def test_handler_goes_to_stderr():
19
+ logmod.setup(verbose=1)
20
+ handlers = logging.getLogger("abom").handlers
21
+ assert len(handlers) == 1
22
+ import sys
23
+ assert handlers[0].stream is sys.stderr
24
+
25
+
26
+ def test_json_logs_are_parseable(capsys):
27
+ import json
28
+ logmod.setup(verbose=1, json_logs=True)
29
+ logging.getLogger("abom.t").info("hi there", extra={"event": "x", "n": 3})
30
+ line = capsys.readouterr().err.strip().splitlines()[-1]
31
+ rec = json.loads(line) # valid JSON per line
32
+ assert rec["level"] == "INFO"
33
+ assert rec["logger"] == "abom.t"
34
+ assert rec["msg"] == "hi there"
35
+ assert rec["event"] == "x" and rec["n"] == 3
36
+ assert "ts" in rec
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes