abom-cli 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.
abom/scan.py ADDED
@@ -0,0 +1,207 @@
1
+ """abom scan — statically detect an AI agent's components from a repository.
2
+
3
+ Produces the `agent`, `components`, and `controls` of an ABOM Composition
4
+ Manifest by inspecting dependency manifests, source, prompt files, and MCP
5
+ configs. Pure stdlib. Best-effort and conservative: it reports what it
6
+ can see, with where it saw it (`detected_from`).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import re
13
+ import tomllib
14
+ from pathlib import Path
15
+
16
+ # --- known signals -----------------------------------------------------------
17
+ FRAMEWORKS = {
18
+ "langchain": "LangChain", "langchain-core": "LangChain", "langgraph": "LangGraph",
19
+ "crewai": "CrewAI", "llama-index": "LlamaIndex", "llama_index": "LlamaIndex",
20
+ "autogen": "AutoGen", "pyautogen": "AutoGen", "autogen-agentchat": "AutoGen",
21
+ "semantic-kernel": "Semantic Kernel", "haystack-ai": "Haystack",
22
+ "smolagents": "smolagents", "openai-agents": "OpenAI Agents SDK",
23
+ "google-adk": "Google ADK", "google-genai": "Google GenAI", "mcp": "Model Context Protocol",
24
+ "dspy": "DSPy", "dspy-ai": "DSPy", "pydantic-ai": "Pydantic AI",
25
+ }
26
+ MODEL_SDKS = {
27
+ "openai": "OpenAI", "anthropic": "Anthropic", "mistralai": "Mistral",
28
+ "cohere": "Cohere", "google-generativeai": "Google", "vertexai": "Google Vertex",
29
+ "boto3": "AWS Bedrock", "ollama": "Ollama", "vllm": "vLLM",
30
+ "huggingface-hub": "Hugging Face", "transformers": "Hugging Face",
31
+ "groq": "Groq", "together": "Together", "replicate": "Replicate",
32
+ }
33
+ VECTOR_STORES = {
34
+ "chromadb": "Chroma", "pinecone-client": "Pinecone", "pinecone": "Pinecone",
35
+ "weaviate-client": "Weaviate", "qdrant-client": "Qdrant", "faiss-cpu": "FAISS",
36
+ "faiss-gpu": "FAISS", "pgvector": "pgvector", "pymilvus": "Milvus", "lancedb": "LanceDB",
37
+ }
38
+ GUARDRAILS = {
39
+ "guardrails-ai": "Guardrails AI", "nemoguardrails": "NeMo Guardrails",
40
+ "llm-guard": "LLM Guard", "presidio-analyzer": "Presidio", "rebuff": "Rebuff",
41
+ }
42
+
43
+ MODEL_NAME_RE = re.compile(
44
+ r"\b(gpt-4[\w.\-]*|gpt-3\.5[\w.\-]*|o[134][\w.\-]*|chatgpt[\w.\-]*|"
45
+ r"claude-[\w.\-]+|gemini-[\w.\-]+|mistral[\w.\-]*|mixtral[\w.\-]*|"
46
+ r"llama-?[23][\w.\-]*|qwen[\w.\-]*|command-r[\w.\-]*|deepseek[\w.\-]*|phi-?[34][\w.\-]*)\b",
47
+ re.IGNORECASE,
48
+ )
49
+ TOOL_RE = re.compile(r"@(?:tool|function_tool|tool\([^)]*\))\s*(?:\n\s*)*def\s+(\w+)|"
50
+ r"@(?:tool|function_tool)\b[\s\S]{0,80}?def\s+(\w+)")
51
+ MCP_CONFIG_NAMES = {"mcp.json", ".mcp.json", "claude_desktop_config.json", "mcp_config.json", ".cursor/mcp.json"}
52
+ IGNORE_DIRS = {".git", "node_modules", ".venv", "venv", "env", "__pycache__", "dist", "build",
53
+ ".mypy_cache", ".pytest_cache", ".tox", ".ruff_cache", "site-packages", ".next"}
54
+ TEXT_EXT = {".py", ".js", ".ts", ".tsx", ".yaml", ".yml", ".json", ".toml", ".env", ".cfg", ".ini", ".md", ".txt"}
55
+ MAX_FILE = 1_000_000
56
+
57
+
58
+ def _read(p: Path) -> str:
59
+ try:
60
+ return p.read_text(encoding="utf-8", errors="ignore")
61
+ except Exception:
62
+ return ""
63
+
64
+
65
+ def _sha256(text: str) -> str:
66
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
67
+
68
+
69
+ def _iter_files(root: Path):
70
+ for p in root.rglob("*"):
71
+ if not p.is_file():
72
+ continue
73
+ if any(part in IGNORE_DIRS for part in p.parts):
74
+ continue
75
+ try:
76
+ if p.stat().st_size > MAX_FILE:
77
+ continue
78
+ except OSError:
79
+ continue
80
+ yield p
81
+
82
+
83
+ def _norm_dep(name: str) -> str:
84
+ return re.split(r"[<>=!~ ;\[]", name.strip(), maxsplit=1)[0].strip().lower().replace("_", "-")
85
+
86
+
87
+ def parse_dependencies(root: Path) -> set[str]:
88
+ deps: set[str] = set()
89
+ for req in list(root.glob("requirements*.txt")) + list(root.glob("**/requirements*.txt")):
90
+ if any(part in IGNORE_DIRS for part in req.parts):
91
+ continue
92
+ for line in _read(req).splitlines():
93
+ line = line.strip()
94
+ if line and not line.startswith(("#", "-")):
95
+ deps.add(_norm_dep(line))
96
+ pp = root / "pyproject.toml"
97
+ if pp.exists():
98
+ try:
99
+ data = tomllib.loads(_read(pp))
100
+ for d in data.get("project", {}).get("dependencies", []) or []:
101
+ deps.add(_norm_dep(d))
102
+ poetry = data.get("tool", {}).get("poetry", {}).get("dependencies", {})
103
+ for d in poetry:
104
+ deps.add(_norm_dep(d))
105
+ except Exception:
106
+ pass
107
+ pkg = root / "package.json"
108
+ if pkg.exists():
109
+ try:
110
+ data = json.loads(_read(pkg))
111
+ for section in ("dependencies", "devDependencies"):
112
+ deps.update(_norm_dep(k) for k in data.get(section, {}))
113
+ except Exception:
114
+ pass
115
+ deps.discard("")
116
+ return deps
117
+
118
+
119
+ def _agent_meta(root: Path) -> dict:
120
+ name, version = root.resolve().name, "0.0.0"
121
+ pp = root / "pyproject.toml"
122
+ if pp.exists():
123
+ try:
124
+ data = tomllib.loads(_read(pp))
125
+ proj = data.get("project", {})
126
+ name = proj.get("name", name)
127
+ version = proj.get("version", version)
128
+ except Exception:
129
+ pass
130
+ pkg = root / "package.json"
131
+ if pkg.exists():
132
+ try:
133
+ data = json.loads(_read(pkg))
134
+ name = data.get("name", name)
135
+ version = data.get("version", version)
136
+ except Exception:
137
+ pass
138
+ return {"name": name, "version": version}
139
+
140
+
141
+ def scan(root: Path) -> dict:
142
+ root = Path(root)
143
+ deps = parse_dependencies(root)
144
+ components: list[dict] = []
145
+ seen: set[tuple] = set()
146
+
147
+ def add(comp: dict):
148
+ key = (comp["type"], comp.get("name"))
149
+ if key not in seen:
150
+ seen.add(key)
151
+ components.append(comp)
152
+
153
+ # 1. from dependencies
154
+ for d in sorted(deps):
155
+ if d in FRAMEWORKS:
156
+ add({"type": "framework", "name": FRAMEWORKS[d], "detected_from": f"dependency:{d}"})
157
+ if d in MODEL_SDKS:
158
+ add({"type": "model", "name": f"{MODEL_SDKS[d]} (SDK)", "provider": MODEL_SDKS[d],
159
+ "egress": d not in ("vllm", "ollama", "transformers"),
160
+ "detected_from": f"dependency:{d}"})
161
+ if d in VECTOR_STORES:
162
+ add({"type": "dataSource", "kind": "vector-store", "name": VECTOR_STORES[d],
163
+ "detected_from": f"dependency:{d}"})
164
+ if d in GUARDRAILS:
165
+ add({"type": "policy", "kind": "guardrail", "name": GUARDRAILS[d],
166
+ "detected_from": f"dependency:{d}"})
167
+
168
+ # 2. concrete model names, prompt files, tools, MCP servers — from source
169
+ model_names: set[str] = set()
170
+ for p in _iter_files(root):
171
+ suffix = p.suffix.lower()
172
+ rel = str(p.relative_to(root))
173
+ # prompt files
174
+ if suffix == ".prompt" or "prompt" in p.parent.name.lower() and suffix in (".txt", ".md"):
175
+ text = _read(p)
176
+ add({"type": "prompt", "name": rel, "sha256": _sha256(text),
177
+ "detected_from": f"file:{rel}"})
178
+ # mcp configs
179
+ if p.name in MCP_CONFIG_NAMES or rel in MCP_CONFIG_NAMES:
180
+ try:
181
+ cfg = json.loads(_read(p))
182
+ for server in (cfg.get("mcpServers") or {}):
183
+ add({"type": "mcpServer", "name": server, "detected_from": f"file:{rel}"})
184
+ except Exception:
185
+ pass
186
+ if suffix not in TEXT_EXT:
187
+ continue
188
+ text = _read(p)
189
+ for m in MODEL_NAME_RE.findall(text):
190
+ model_names.add(m if isinstance(m, str) else next(filter(None, m)))
191
+ if suffix == ".py" and ("@tool" in text or "@function_tool" in text):
192
+ for groups in TOOL_RE.findall(text):
193
+ fn = next((g for g in groups if g), None)
194
+ if fn:
195
+ add({"type": "tool", "name": fn, "detected_from": f"file:{rel}"})
196
+
197
+ for name in sorted(model_names):
198
+ provider = "external" if not name.lower().startswith(("llama", "qwen", "mistral", "mixtral", "phi", "deepseek")) else "open-weight"
199
+ add({"type": "model", "name": name, "provenance": "referenced in source",
200
+ "egress": provider == "external", "detected_from": "source"})
201
+
202
+ return {
203
+ "agent": _agent_meta(root),
204
+ "components": components,
205
+ "controls": {"scan": "static", "scanned_path": str(root.resolve().name),
206
+ "egress": "unknown (static scan)"},
207
+ }
abom/schemas.py ADDED
@@ -0,0 +1,79 @@
1
+ """Pydantic request/response models for the Control API."""
2
+ from __future__ import annotations
3
+
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class ProjectCreate(BaseModel):
12
+ name: str
13
+ repo_url: str
14
+ test_command: str = "pytest -q"
15
+
16
+
17
+ class ProjectOut(BaseModel):
18
+ id: uuid.UUID
19
+ name: str
20
+ repo_url: str
21
+ test_command: str
22
+
23
+ model_config = {"from_attributes": True}
24
+
25
+
26
+ class RunCreate(BaseModel):
27
+ project_id: uuid.UUID
28
+ intent: str = Field(min_length=3)
29
+ workload_type: Literal["dev_agent"] = "dev_agent"
30
+ max_iterations: int = Field(default=4, ge=1, le=8)
31
+
32
+
33
+ class RunOut(BaseModel):
34
+ id: uuid.UUID
35
+ project_id: uuid.UUID
36
+ intent: str
37
+ status: str
38
+ model: str
39
+ max_iterations: int
40
+ prompt_tokens: int
41
+ completion_tokens: int
42
+ created_at: datetime
43
+ updated_at: datetime
44
+
45
+ model_config = {"from_attributes": True}
46
+
47
+
48
+ class StepOut(BaseModel):
49
+ seq: int
50
+ type: str
51
+ payload: dict[str, Any]
52
+ created_at: datetime
53
+
54
+ model_config = {"from_attributes": True}
55
+
56
+
57
+ class ApprovalDecision(BaseModel):
58
+ decision: Literal["approved", "rejected"]
59
+ comment: str = ""
60
+
61
+
62
+ class AuditEventOut(BaseModel):
63
+ seq: int
64
+ event_type: str
65
+ actor: str
66
+ data: dict[str, Any]
67
+ prev_hash: str
68
+ hash: str
69
+ created_at: datetime
70
+
71
+ model_config = {"from_attributes": True}
72
+
73
+
74
+ class VerifyResult(BaseModel):
75
+ run_id: uuid.UUID
76
+ valid: bool
77
+ broken_seq: int | None = None
78
+ reason: str | None = None
79
+ event_count: int
abom/sign.py ADDED
@@ -0,0 +1,91 @@
1
+ """ed25519 signing for ABOM — real detached signatures over canonical JSON.
2
+
3
+ A signing key lives at ``~/.abom/signing_key.pem`` (override with ``ABOM_KEY``).
4
+ The public key and a short key id are embedded in the signature, so verification
5
+ is self-contained. In production a Notary / key registry pins the set of trusted
6
+ key ids; ``verify_obj(obj, trusted_keys=...)`` enforces that.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import hashlib
12
+ import os
13
+ from pathlib import Path
14
+
15
+ from cryptography.hazmat.primitives import serialization
16
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
17
+ Ed25519PrivateKey,
18
+ Ed25519PublicKey,
19
+ )
20
+
21
+ from .audit import canonical_json
22
+
23
+
24
+ def default_key_path() -> Path:
25
+ return Path(os.environ.get("ABOM_KEY", str(Path.home() / ".abom" / "signing_key.pem")))
26
+
27
+
28
+ def load_or_create_key(path: Path | None = None) -> Ed25519PrivateKey:
29
+ path = Path(path or default_key_path())
30
+ if path.exists():
31
+ return serialization.load_pem_private_key(path.read_bytes(), password=None)
32
+ key = Ed25519PrivateKey.generate()
33
+ path.parent.mkdir(parents=True, exist_ok=True)
34
+ path.write_bytes(
35
+ key.private_bytes(
36
+ serialization.Encoding.PEM,
37
+ serialization.PrivateFormat.PKCS8,
38
+ serialization.NoEncryption(),
39
+ )
40
+ )
41
+ try:
42
+ os.chmod(path, 0o600)
43
+ except OSError:
44
+ pass
45
+ return key
46
+
47
+
48
+ def _pub_b64(pub: Ed25519PublicKey) -> str:
49
+ raw = pub.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
50
+ return base64.b64encode(raw).decode()
51
+
52
+
53
+ def key_id(pub_b64: str) -> str:
54
+ return hashlib.sha256(base64.b64decode(pub_b64)).hexdigest()[:16]
55
+
56
+
57
+ def sign_obj(obj: dict, key: Ed25519PrivateKey | None = None) -> dict:
58
+ """Return ``obj`` with a detached ed25519 ``signature`` over its canonical form."""
59
+ key = key or load_or_create_key()
60
+ body = {k: v for k, v in obj.items() if k != "signature"}
61
+ sig = key.sign(canonical_json(body).encode("utf-8"))
62
+ pub_b64 = _pub_b64(key.public_key())
63
+ return {
64
+ **obj,
65
+ "signature": {
66
+ "alg": "ed25519",
67
+ "public_key": pub_b64,
68
+ "key_id": key_id(pub_b64),
69
+ "value": base64.b64encode(sig).decode(),
70
+ },
71
+ }
72
+
73
+
74
+ def verify_obj(obj: dict, trusted_keys: set[str] | None = None) -> bool:
75
+ """Verify the embedded ed25519 signature. If ``trusted_keys`` is given, the
76
+ signer's key id must be in it."""
77
+ sig = obj.get("signature") or {}
78
+ if sig.get("alg") != "ed25519":
79
+ return False
80
+ pub_b64, value = sig.get("public_key"), sig.get("value")
81
+ if not pub_b64 or not value:
82
+ return False
83
+ if trusted_keys is not None and key_id(pub_b64) not in trusted_keys:
84
+ return False
85
+ try:
86
+ pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(pub_b64))
87
+ body = {k: v for k, v in obj.items() if k != "signature"}
88
+ pub.verify(base64.b64decode(value), canonical_json(body).encode("utf-8"))
89
+ return True
90
+ except Exception:
91
+ return False
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: abom-cli
3
+ Version: 0.1.0
4
+ Summary: ABOM — the Agent Bill of Materials. Scan, sign, and verify what your AI agents are made of.
5
+ Project-URL: Homepage, https://abom.ai
6
+ Project-URL: Repository, https://github.com/josephassiga/abom
7
+ Project-URL: Specification, https://github.com/josephassiga/abom/tree/main/spec
8
+ Author: ABOM Contributors
9
+ License-Expression: Apache-2.0
10
+ Keywords: agent,ai,ai-security,cyclonedx,llm,ml-bom,provenance,sbom,supply-chain
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: cryptography>=42.0
19
+ Requires-Dist: typer>=0.12
20
+ Provides-Extra: dev
21
+ Requires-Dist: build>=1.2; extra == 'dev'
22
+ Requires-Dist: jsonschema>=4.21; extra == 'dev'
23
+ Requires-Dist: pytest>=8.3; extra == 'dev'
24
+ Requires-Dist: ruff>=0.7; extra == 'dev'
25
+ Requires-Dist: twine>=5.0; extra == 'dev'
26
+ Provides-Extra: server
27
+ Requires-Dist: alembic>=1.13; extra == 'server'
28
+ Requires-Dist: asyncpg>=0.30; extra == 'server'
29
+ Requires-Dist: boto3>=1.35; extra == 'server'
30
+ Requires-Dist: fastapi>=0.115; extra == 'server'
31
+ Requires-Dist: httpx>=0.27; extra == 'server'
32
+ Requires-Dist: pydantic-settings>=2.6; extra == 'server'
33
+ Requires-Dist: pydantic>=2.9; extra == 'server'
34
+ Requires-Dist: python-jose[cryptography]>=3.3; extra == 'server'
35
+ Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'server'
36
+ Requires-Dist: temporalio>=1.8; extra == 'server'
37
+ Requires-Dist: uvicorn[standard]>=0.32; extra == 'server'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # abom-cli
41
+
42
+ The reference implementation of [ABOM](../spec/) — the Agent Bill of Materials.
43
+ Scan a repo, emit a **signed** Composition Manifest, and **verify** it.
44
+
45
+ ```bash
46
+ pip install abom-cli # (until published: pip install -e .)
47
+ abom scan . # → abom.json (signed with ed25519)
48
+ abom verify abom.json # check signature
49
+ abom verify abom.json --policy policy.json # + enforce a policy (exit 1 on violations)
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ | Command | What it does |
55
+ |---|---|
56
+ | `abom scan [PATH]` | Detect agent components (models, prompts, tools, MCP servers, frameworks, vector stores, guardrails) and emit a signed Composition Manifest. `-o -` writes to stdout. |
57
+ | `abom verify [FILE]` | Verify the ed25519 signature; with `--policy`, enforce model allowlist / residency / egress / approval rules. Non-zero exit on findings (CI-friendly). |
58
+ | `abom keygen` | Show (or create) the local ed25519 signing key (`~/.abom/signing_key.pem`, override with `ABOM_KEY`). |
59
+ | `abom version` | Print the tool and spec versions. |
60
+
61
+ ### Example
62
+
63
+ ```
64
+ $ abom scan .
65
+ ABOM · my-agent @ 1.2.0
66
+ models 3 gpt-4o-mini, claude-3-5-sonnet, OpenAI (SDK)
67
+ frameworks 2 LangChain, LangGraph
68
+ MCP servers 2 filesystem, github
69
+ tools 1 lookup_customer
70
+ prompts 1 prompts/system.txt
71
+ signed: ed25519 · key 5846eabc738b3542
72
+ → wrote abom.json
73
+ ```
74
+
75
+ ## How detection works
76
+
77
+ `abom scan` is a static scanner (pure stdlib + `cryptography`):
78
+ - **Dependencies** (`requirements*.txt`, `pyproject.toml`, `package.json`) → frameworks, model SDKs, vector stores, guardrails.
79
+ - **Source** → concrete model names (`gpt-4o`, `claude-*`, …) and `@tool`-decorated functions.
80
+ - **Prompt files** (`*.prompt`, `prompts/*.txt|md`) → hashed.
81
+ - **MCP configs** (`mcp.json`, `claude_desktop_config.json`, …) → MCP servers.
82
+
83
+ Each component records `detected_from` so the manifest is auditable. The output
84
+ validates against [`spec/abom-0.1.schema.json`](../spec/abom-0.1.schema.json).
85
+
86
+ ## Signing
87
+
88
+ `abom scan` signs with **ed25519** (`cryptography`). The key lives at
89
+ `~/.abom/signing_key.pem` (override with `ABOM_KEY`); the public key + a short
90
+ `key_id` are embedded so `abom verify` is self-contained. A Notary / key registry
91
+ pins trusted key ids in production.
92
+
93
+ ## Dev
94
+
95
+ ```bash
96
+ make install # pip install -e ".[dev]"
97
+ make test # pytest (audit chain, scanner, signing)
98
+ make scan && make verify
99
+ make build # wheel + sdist + twine check
100
+ python demo/demo.py # generate → verify → tamper-evidence walkthrough
101
+ ```
102
+
103
+ ## What else is in this package
104
+
105
+ `src/abom/` also contains a **prototype control-plane** (`api.py`, `db.py`,
106
+ `orchestration.py`, the Notary) behind the optional `[server]` extra — the
107
+ beginnings of the commercial layer. It is **not** required for `scan`/`verify`
108
+ and is not part of the v0.1 spec. See [MVP_SPEC.md](MVP_SPEC.md).
@@ -0,0 +1,19 @@
1
+ abom/__init__.py,sha256=ZMeFiJTyfJkr7vXP-G2aGc3a6iIVrmMTeGX5auXz-v0,101
2
+ abom/agents.py,sha256=kgzkSIukMybcCDOyZoN8Xm45Ly2kolP9fUvmMvWyGp0,1999
3
+ abom/api.py,sha256=hek2SCygDxq6r17xMbOIIjU2UXAvkrjVhSvgUUkPR2Q,6769
4
+ abom/audit.py,sha256=ZzNjvJNNMXK3KYMdcgUzP5fVi5qePP3lp_2Jk48OBcg,4128
5
+ abom/bom.py,sha256=ot5cKd1ngkXxkNGpiGHr3kmjXmyMRK-p1CDe7PjY-bY,6771
6
+ abom/cli.py,sha256=0zToq9X8xvJgdlWbnY4UE-3RXLfVqh98Tutgnd9kJC8,4976
7
+ abom/config.py,sha256=4tEwFglXjVKA82f8hOrG_nv96WN3TQ_tKua24X5gW4I,1233
8
+ abom/db.py,sha256=Qq65ExI5Hvoosn8MCm1hpTXePnjuKK6TX82bOPYy5-o,5769
9
+ abom/execution.py,sha256=V-pijLQVwu25c3nta8377Ykm1yRpPoCwHhJZ99SxAe8,2699
10
+ abom/models_router.py,sha256=OfbE0cFHVejAe4gUno_uIBxDyjyg6rNsRSJxNdl0hPo,2927
11
+ abom/orchestration.py,sha256=s0IhJSVYyRDJRoLSTTSD6VhrffgS7V_CYiNOQ8uCeZE,10135
12
+ abom/policy.py,sha256=cgad67d8ycEHiELaELed8tyf1-i0S3citG6AV1OUXng,1471
13
+ abom/scan.py,sha256=1Kijg7FSSejd4IHwo11CUzaCbZT-QqvMsrEdhVYp_xQ,8379
14
+ abom/schemas.py,sha256=L46khKUR4pKfXiWFAXSPkUNlKgDOBBWhWLqyPQTN1o0,1577
15
+ abom/sign.py,sha256=Aze5qykBTAJ2wQcaeppfcbTogQP2qLZ4xcsbqWmoIMs,3068
16
+ abom_cli-0.1.0.dist-info/METADATA,sha256=K0-hPwTqTLmPD4T89utfbC8D5PfU2vrYPBd3ZA6xLGI,4690
17
+ abom_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
18
+ abom_cli-0.1.0.dist-info/entry_points.txt,sha256=BW8BE6sSWrJAnjvPKN-VtT8ah_LnuhaXIFRYGzE39lQ,38
19
+ abom_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ abom = abom.cli:app