agent-harness-aibom 0.1.0__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.
- agent_harness_aibom-0.1.0/LICENSE +21 -0
- agent_harness_aibom-0.1.0/PKG-INFO +124 -0
- agent_harness_aibom-0.1.0/README.md +110 -0
- agent_harness_aibom-0.1.0/pyproject.toml +26 -0
- agent_harness_aibom-0.1.0/setup.cfg +4 -0
- agent_harness_aibom-0.1.0/src/agent_harness_aibom.egg-info/PKG-INFO +124 -0
- agent_harness_aibom-0.1.0/src/agent_harness_aibom.egg-info/SOURCES.txt +30 -0
- agent_harness_aibom-0.1.0/src/agent_harness_aibom.egg-info/dependency_links.txt +1 -0
- agent_harness_aibom-0.1.0/src/agent_harness_aibom.egg-info/entry_points.txt +2 -0
- agent_harness_aibom-0.1.0/src/agent_harness_aibom.egg-info/requires.txt +4 -0
- agent_harness_aibom-0.1.0/src/agent_harness_aibom.egg-info/top_level.txt +1 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/__init__.py +10 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/cli.py +109 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/collectors/__init__.py +2 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/collectors/base.py +33 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/collectors/hermes.py +206 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/collectors/mcp.py +36 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/collectors/ollama.py +50 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/collectors/openclaw.py +148 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/collectors/secrets.py +35 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/cyclonedx.py +84 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/diff.py +50 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/fingerprint.py +32 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/model.py +110 -0
- agent_harness_aibom-0.1.0/src/harness_aibom/validate.py +44 -0
- agent_harness_aibom-0.1.0/tests/test_cli.py +43 -0
- agent_harness_aibom-0.1.0/tests/test_cyclonedx.py +44 -0
- agent_harness_aibom-0.1.0/tests/test_diff.py +42 -0
- agent_harness_aibom-0.1.0/tests/test_fingerprint.py +18 -0
- agent_harness_aibom-0.1.0/tests/test_hermes_collector.py +132 -0
- agent_harness_aibom-0.1.0/tests/test_openclaw_collector.py +91 -0
- agent_harness_aibom-0.1.0/tests/test_validate.py +46 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the agent-harness-aibom contributors
|
|
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,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-harness-aibom
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AIBOM generator for AI agent harnesses (Hermes, OpenClaw) and their model/skill/MCP/hook surface
|
|
5
|
+
Author: Practical DevSecOps
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: PyYAML>=6.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# agent-harness-aibom
|
|
16
|
+
|
|
17
|
+
An AIBOM (AI Bill of Materials) generator for AI agent harnesses —
|
|
18
|
+
currently Hermes and [OpenClaw](https://docs.openclaw.ai) — that emits standard CycloneDX 1.6
|
|
19
|
+
JSON extended with harness-specific fields (skill fingerprints, MCP server
|
|
20
|
+
TLS/auth posture, hook approval state, model provenance). See
|
|
21
|
+
[`SPEC.md`](SPEC.md) for the full data model and its rationale.
|
|
22
|
+
|
|
23
|
+
This is the "ground truth" layer for a broader exercise: scan a real
|
|
24
|
+
harness with this tool, scan the same harness with a general-purpose
|
|
25
|
+
scanner (e.g. Cisco AI BOM), and diff what each one saw.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e ".[dev]" # from a checkout, for development
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(Not yet published to PyPI.)
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Auto-detect and scan whatever's installed under $HOME
|
|
39
|
+
harness-aibom scan --runtime auto -o aibom.json
|
|
40
|
+
|
|
41
|
+
# Scan a specific runtime explicitly
|
|
42
|
+
harness-aibom scan --runtime hermes -o hermes-aibom.json
|
|
43
|
+
harness-aibom scan --runtime openclaw -o openclaw-aibom.json
|
|
44
|
+
|
|
45
|
+
# Check a document's shape
|
|
46
|
+
harness-aibom validate aibom.json
|
|
47
|
+
|
|
48
|
+
# Compare two scans -- e.g. before/after a suspected skill compromise
|
|
49
|
+
harness-aibom diff before.json after.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`scan` runs entirely against the local filesystem and local subprocesses/
|
|
53
|
+
HTTP calls (`hermes`/`openclaw` CLIs, Ollama's `/api/tags`). To scan a
|
|
54
|
+
remote lab VM, install the package there (or SSH in and run it) — there's
|
|
55
|
+
no built-in remote transport yet.
|
|
56
|
+
|
|
57
|
+
Missing pieces are never fatal: if `hermes` isn't on `PATH`, or Ollama
|
|
58
|
+
isn't reachable, the scan still completes and prints a `warning[...]` line
|
|
59
|
+
to stderr explaining what it skipped, so students can see exactly what
|
|
60
|
+
partial information looks like.
|
|
61
|
+
|
|
62
|
+
## Example output
|
|
63
|
+
|
|
64
|
+
[`examples/hermes-aibom.example.json`](examples/hermes-aibom.example.json)
|
|
65
|
+
and [`examples/openclaw-aibom.example.json`](examples/openclaw-aibom.example.json)
|
|
66
|
+
were generated by running `scan` against the test fixtures in
|
|
67
|
+
`tests/fixtures/` (a synthetic `config.yaml`/`openclaw.json`, no live
|
|
68
|
+
`hermes`/`openclaw`/`ollama` on this machine) — a realistic example of
|
|
69
|
+
graceful degradation when the runtime binary isn't present.
|
|
70
|
+
|
|
71
|
+
## Project layout
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/harness_aibom/
|
|
75
|
+
├── model.py # Component / HarnessDocument -- the in-memory data model
|
|
76
|
+
├── cyclonedx.py # model.py -> CycloneDX 1.6 JSON
|
|
77
|
+
├── fingerprint.py # sha256 helpers
|
|
78
|
+
├── validate.py # structural checks on a harness-aibom document
|
|
79
|
+
├── diff.py # before/after comparison
|
|
80
|
+
├── cli.py # `harness-aibom` entrypoint
|
|
81
|
+
└── collectors/
|
|
82
|
+
├── base.py # Collector ABC
|
|
83
|
+
├── ollama.py # shared: model discovery via Ollama's HTTP API
|
|
84
|
+
├── mcp.py # shared: MCP server extraction from a config dict
|
|
85
|
+
├── secrets.py # shared: secrets-surface discovery (paths/perms only)
|
|
86
|
+
├── hermes.py # Hermes collector
|
|
87
|
+
└── openclaw.py # OpenClaw collector
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## CI and publishing
|
|
91
|
+
|
|
92
|
+
- `.github/workflows/ci.yml` runs the test suite and a CLI smoke test on
|
|
93
|
+
every push and pull request, on Python 3.10, 3.11, and 3.12.
|
|
94
|
+
- `.github/workflows/publish.yml` builds and publishes the package to PyPI
|
|
95
|
+
when you publish a GitHub Release. It uses PyPI Trusted Publishing, so no
|
|
96
|
+
password lives in this repo. One-time setup before your first release:
|
|
97
|
+
1. Go to <https://pypi.org/manage/account/publishing/>.
|
|
98
|
+
2. Add a new trusted publisher for project `agent-harness-aibom`.
|
|
99
|
+
3. Set the owner and repo name to match this GitHub repo.
|
|
100
|
+
4. Set the workflow name to `publish.yml`.
|
|
101
|
+
5. Set the environment name to `pypi`.
|
|
102
|
+
|
|
103
|
+
## Testing
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pytest -q
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Collector tests run entirely against fixtures under `tests/fixtures/`
|
|
110
|
+
(`hermes_home/`, `openclaw_home/`) with injected fake `run`/`fetch`
|
|
111
|
+
functions — no real `hermes`/`openclaw`/`ollama` needed to run the suite.
|
|
112
|
+
|
|
113
|
+
## Roadmap
|
|
114
|
+
|
|
115
|
+
This is v0.1 — the spec plus two working collectors. Natural next steps,
|
|
116
|
+
roughly in order:
|
|
117
|
+
|
|
118
|
+
1. Harden hook parsing against a live Hermes box (see `SPEC.md` §5).
|
|
119
|
+
2. Point this at the actual CAASP lab VMs and fix whatever the real output
|
|
120
|
+
shapes get wrong.
|
|
121
|
+
3. Scan the same harness with Cisco AI BOM and diff the two documents —
|
|
122
|
+
the gap-analysis exercise this package exists to support.
|
|
123
|
+
4. cosign signing/provenance for the emitted AIBOM.
|
|
124
|
+
5. Package for PyPI.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# agent-harness-aibom
|
|
2
|
+
|
|
3
|
+
An AIBOM (AI Bill of Materials) generator for AI agent harnesses —
|
|
4
|
+
currently Hermes and [OpenClaw](https://docs.openclaw.ai) — that emits standard CycloneDX 1.6
|
|
5
|
+
JSON extended with harness-specific fields (skill fingerprints, MCP server
|
|
6
|
+
TLS/auth posture, hook approval state, model provenance). See
|
|
7
|
+
[`SPEC.md`](SPEC.md) for the full data model and its rationale.
|
|
8
|
+
|
|
9
|
+
This is the "ground truth" layer for a broader exercise: scan a real
|
|
10
|
+
harness with this tool, scan the same harness with a general-purpose
|
|
11
|
+
scanner (e.g. Cisco AI BOM), and diff what each one saw.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e ".[dev]" # from a checkout, for development
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
(Not yet published to PyPI.)
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Auto-detect and scan whatever's installed under $HOME
|
|
25
|
+
harness-aibom scan --runtime auto -o aibom.json
|
|
26
|
+
|
|
27
|
+
# Scan a specific runtime explicitly
|
|
28
|
+
harness-aibom scan --runtime hermes -o hermes-aibom.json
|
|
29
|
+
harness-aibom scan --runtime openclaw -o openclaw-aibom.json
|
|
30
|
+
|
|
31
|
+
# Check a document's shape
|
|
32
|
+
harness-aibom validate aibom.json
|
|
33
|
+
|
|
34
|
+
# Compare two scans -- e.g. before/after a suspected skill compromise
|
|
35
|
+
harness-aibom diff before.json after.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`scan` runs entirely against the local filesystem and local subprocesses/
|
|
39
|
+
HTTP calls (`hermes`/`openclaw` CLIs, Ollama's `/api/tags`). To scan a
|
|
40
|
+
remote lab VM, install the package there (or SSH in and run it) — there's
|
|
41
|
+
no built-in remote transport yet.
|
|
42
|
+
|
|
43
|
+
Missing pieces are never fatal: if `hermes` isn't on `PATH`, or Ollama
|
|
44
|
+
isn't reachable, the scan still completes and prints a `warning[...]` line
|
|
45
|
+
to stderr explaining what it skipped, so students can see exactly what
|
|
46
|
+
partial information looks like.
|
|
47
|
+
|
|
48
|
+
## Example output
|
|
49
|
+
|
|
50
|
+
[`examples/hermes-aibom.example.json`](examples/hermes-aibom.example.json)
|
|
51
|
+
and [`examples/openclaw-aibom.example.json`](examples/openclaw-aibom.example.json)
|
|
52
|
+
were generated by running `scan` against the test fixtures in
|
|
53
|
+
`tests/fixtures/` (a synthetic `config.yaml`/`openclaw.json`, no live
|
|
54
|
+
`hermes`/`openclaw`/`ollama` on this machine) — a realistic example of
|
|
55
|
+
graceful degradation when the runtime binary isn't present.
|
|
56
|
+
|
|
57
|
+
## Project layout
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
src/harness_aibom/
|
|
61
|
+
├── model.py # Component / HarnessDocument -- the in-memory data model
|
|
62
|
+
├── cyclonedx.py # model.py -> CycloneDX 1.6 JSON
|
|
63
|
+
├── fingerprint.py # sha256 helpers
|
|
64
|
+
├── validate.py # structural checks on a harness-aibom document
|
|
65
|
+
├── diff.py # before/after comparison
|
|
66
|
+
├── cli.py # `harness-aibom` entrypoint
|
|
67
|
+
└── collectors/
|
|
68
|
+
├── base.py # Collector ABC
|
|
69
|
+
├── ollama.py # shared: model discovery via Ollama's HTTP API
|
|
70
|
+
├── mcp.py # shared: MCP server extraction from a config dict
|
|
71
|
+
├── secrets.py # shared: secrets-surface discovery (paths/perms only)
|
|
72
|
+
├── hermes.py # Hermes collector
|
|
73
|
+
└── openclaw.py # OpenClaw collector
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## CI and publishing
|
|
77
|
+
|
|
78
|
+
- `.github/workflows/ci.yml` runs the test suite and a CLI smoke test on
|
|
79
|
+
every push and pull request, on Python 3.10, 3.11, and 3.12.
|
|
80
|
+
- `.github/workflows/publish.yml` builds and publishes the package to PyPI
|
|
81
|
+
when you publish a GitHub Release. It uses PyPI Trusted Publishing, so no
|
|
82
|
+
password lives in this repo. One-time setup before your first release:
|
|
83
|
+
1. Go to <https://pypi.org/manage/account/publishing/>.
|
|
84
|
+
2. Add a new trusted publisher for project `agent-harness-aibom`.
|
|
85
|
+
3. Set the owner and repo name to match this GitHub repo.
|
|
86
|
+
4. Set the workflow name to `publish.yml`.
|
|
87
|
+
5. Set the environment name to `pypi`.
|
|
88
|
+
|
|
89
|
+
## Testing
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pytest -q
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Collector tests run entirely against fixtures under `tests/fixtures/`
|
|
96
|
+
(`hermes_home/`, `openclaw_home/`) with injected fake `run`/`fetch`
|
|
97
|
+
functions — no real `hermes`/`openclaw`/`ollama` needed to run the suite.
|
|
98
|
+
|
|
99
|
+
## Roadmap
|
|
100
|
+
|
|
101
|
+
This is v0.1 — the spec plus two working collectors. Natural next steps,
|
|
102
|
+
roughly in order:
|
|
103
|
+
|
|
104
|
+
1. Harden hook parsing against a live Hermes box (see `SPEC.md` §5).
|
|
105
|
+
2. Point this at the actual CAASP lab VMs and fix whatever the real output
|
|
106
|
+
shapes get wrong.
|
|
107
|
+
3. Scan the same harness with Cisco AI BOM and diff the two documents —
|
|
108
|
+
the gap-analysis exercise this package exists to support.
|
|
109
|
+
4. cosign signing/provenance for the emitted AIBOM.
|
|
110
|
+
5. Package for PyPI.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agent-harness-aibom"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "AIBOM generator for AI agent harnesses (Hermes, OpenClaw) and their model/skill/MCP/hook surface"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Practical DevSecOps" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"PyYAML>=6.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = [
|
|
15
|
+
"pytest>=8.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
harness-aibom = "harness_aibom.cli:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["setuptools>=68", "wheel"]
|
|
23
|
+
build-backend = "setuptools.build_meta"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-harness-aibom
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AIBOM generator for AI agent harnesses (Hermes, OpenClaw) and their model/skill/MCP/hook surface
|
|
5
|
+
Author: Practical DevSecOps
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: PyYAML>=6.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# agent-harness-aibom
|
|
16
|
+
|
|
17
|
+
An AIBOM (AI Bill of Materials) generator for AI agent harnesses —
|
|
18
|
+
currently Hermes and [OpenClaw](https://docs.openclaw.ai) — that emits standard CycloneDX 1.6
|
|
19
|
+
JSON extended with harness-specific fields (skill fingerprints, MCP server
|
|
20
|
+
TLS/auth posture, hook approval state, model provenance). See
|
|
21
|
+
[`SPEC.md`](SPEC.md) for the full data model and its rationale.
|
|
22
|
+
|
|
23
|
+
This is the "ground truth" layer for a broader exercise: scan a real
|
|
24
|
+
harness with this tool, scan the same harness with a general-purpose
|
|
25
|
+
scanner (e.g. Cisco AI BOM), and diff what each one saw.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e ".[dev]" # from a checkout, for development
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(Not yet published to PyPI.)
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Auto-detect and scan whatever's installed under $HOME
|
|
39
|
+
harness-aibom scan --runtime auto -o aibom.json
|
|
40
|
+
|
|
41
|
+
# Scan a specific runtime explicitly
|
|
42
|
+
harness-aibom scan --runtime hermes -o hermes-aibom.json
|
|
43
|
+
harness-aibom scan --runtime openclaw -o openclaw-aibom.json
|
|
44
|
+
|
|
45
|
+
# Check a document's shape
|
|
46
|
+
harness-aibom validate aibom.json
|
|
47
|
+
|
|
48
|
+
# Compare two scans -- e.g. before/after a suspected skill compromise
|
|
49
|
+
harness-aibom diff before.json after.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`scan` runs entirely against the local filesystem and local subprocesses/
|
|
53
|
+
HTTP calls (`hermes`/`openclaw` CLIs, Ollama's `/api/tags`). To scan a
|
|
54
|
+
remote lab VM, install the package there (or SSH in and run it) — there's
|
|
55
|
+
no built-in remote transport yet.
|
|
56
|
+
|
|
57
|
+
Missing pieces are never fatal: if `hermes` isn't on `PATH`, or Ollama
|
|
58
|
+
isn't reachable, the scan still completes and prints a `warning[...]` line
|
|
59
|
+
to stderr explaining what it skipped, so students can see exactly what
|
|
60
|
+
partial information looks like.
|
|
61
|
+
|
|
62
|
+
## Example output
|
|
63
|
+
|
|
64
|
+
[`examples/hermes-aibom.example.json`](examples/hermes-aibom.example.json)
|
|
65
|
+
and [`examples/openclaw-aibom.example.json`](examples/openclaw-aibom.example.json)
|
|
66
|
+
were generated by running `scan` against the test fixtures in
|
|
67
|
+
`tests/fixtures/` (a synthetic `config.yaml`/`openclaw.json`, no live
|
|
68
|
+
`hermes`/`openclaw`/`ollama` on this machine) — a realistic example of
|
|
69
|
+
graceful degradation when the runtime binary isn't present.
|
|
70
|
+
|
|
71
|
+
## Project layout
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/harness_aibom/
|
|
75
|
+
├── model.py # Component / HarnessDocument -- the in-memory data model
|
|
76
|
+
├── cyclonedx.py # model.py -> CycloneDX 1.6 JSON
|
|
77
|
+
├── fingerprint.py # sha256 helpers
|
|
78
|
+
├── validate.py # structural checks on a harness-aibom document
|
|
79
|
+
├── diff.py # before/after comparison
|
|
80
|
+
├── cli.py # `harness-aibom` entrypoint
|
|
81
|
+
└── collectors/
|
|
82
|
+
├── base.py # Collector ABC
|
|
83
|
+
├── ollama.py # shared: model discovery via Ollama's HTTP API
|
|
84
|
+
├── mcp.py # shared: MCP server extraction from a config dict
|
|
85
|
+
├── secrets.py # shared: secrets-surface discovery (paths/perms only)
|
|
86
|
+
├── hermes.py # Hermes collector
|
|
87
|
+
└── openclaw.py # OpenClaw collector
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## CI and publishing
|
|
91
|
+
|
|
92
|
+
- `.github/workflows/ci.yml` runs the test suite and a CLI smoke test on
|
|
93
|
+
every push and pull request, on Python 3.10, 3.11, and 3.12.
|
|
94
|
+
- `.github/workflows/publish.yml` builds and publishes the package to PyPI
|
|
95
|
+
when you publish a GitHub Release. It uses PyPI Trusted Publishing, so no
|
|
96
|
+
password lives in this repo. One-time setup before your first release:
|
|
97
|
+
1. Go to <https://pypi.org/manage/account/publishing/>.
|
|
98
|
+
2. Add a new trusted publisher for project `agent-harness-aibom`.
|
|
99
|
+
3. Set the owner and repo name to match this GitHub repo.
|
|
100
|
+
4. Set the workflow name to `publish.yml`.
|
|
101
|
+
5. Set the environment name to `pypi`.
|
|
102
|
+
|
|
103
|
+
## Testing
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pytest -q
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Collector tests run entirely against fixtures under `tests/fixtures/`
|
|
110
|
+
(`hermes_home/`, `openclaw_home/`) with injected fake `run`/`fetch`
|
|
111
|
+
functions — no real `hermes`/`openclaw`/`ollama` needed to run the suite.
|
|
112
|
+
|
|
113
|
+
## Roadmap
|
|
114
|
+
|
|
115
|
+
This is v0.1 — the spec plus two working collectors. Natural next steps,
|
|
116
|
+
roughly in order:
|
|
117
|
+
|
|
118
|
+
1. Harden hook parsing against a live Hermes box (see `SPEC.md` §5).
|
|
119
|
+
2. Point this at the actual CAASP lab VMs and fix whatever the real output
|
|
120
|
+
shapes get wrong.
|
|
121
|
+
3. Scan the same harness with Cisco AI BOM and diff the two documents —
|
|
122
|
+
the gap-analysis exercise this package exists to support.
|
|
123
|
+
4. cosign signing/provenance for the emitted AIBOM.
|
|
124
|
+
5. Package for PyPI.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/agent_harness_aibom.egg-info/PKG-INFO
|
|
5
|
+
src/agent_harness_aibom.egg-info/SOURCES.txt
|
|
6
|
+
src/agent_harness_aibom.egg-info/dependency_links.txt
|
|
7
|
+
src/agent_harness_aibom.egg-info/entry_points.txt
|
|
8
|
+
src/agent_harness_aibom.egg-info/requires.txt
|
|
9
|
+
src/agent_harness_aibom.egg-info/top_level.txt
|
|
10
|
+
src/harness_aibom/__init__.py
|
|
11
|
+
src/harness_aibom/cli.py
|
|
12
|
+
src/harness_aibom/cyclonedx.py
|
|
13
|
+
src/harness_aibom/diff.py
|
|
14
|
+
src/harness_aibom/fingerprint.py
|
|
15
|
+
src/harness_aibom/model.py
|
|
16
|
+
src/harness_aibom/validate.py
|
|
17
|
+
src/harness_aibom/collectors/__init__.py
|
|
18
|
+
src/harness_aibom/collectors/base.py
|
|
19
|
+
src/harness_aibom/collectors/hermes.py
|
|
20
|
+
src/harness_aibom/collectors/mcp.py
|
|
21
|
+
src/harness_aibom/collectors/ollama.py
|
|
22
|
+
src/harness_aibom/collectors/openclaw.py
|
|
23
|
+
src/harness_aibom/collectors/secrets.py
|
|
24
|
+
tests/test_cli.py
|
|
25
|
+
tests/test_cyclonedx.py
|
|
26
|
+
tests/test_diff.py
|
|
27
|
+
tests/test_fingerprint.py
|
|
28
|
+
tests/test_hermes_collector.py
|
|
29
|
+
tests/test_openclaw_collector.py
|
|
30
|
+
tests/test_validate.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
harness_aibom
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""agent-harness-aibom: an AIBOM generator for AI agent harnesses.
|
|
2
|
+
|
|
3
|
+
This package answers a narrower question than a general-purpose AI BOM
|
|
4
|
+
scanner: what exactly constitutes a *deployed agent harness* -- its runtime
|
|
5
|
+
binary, the model endpoint(s) it talks to, the configuration and skills it
|
|
6
|
+
loads, the MCP servers and hooks it can reach, and the secrets surface
|
|
7
|
+
around all of that. See SPEC.md at the repo root for the full data model.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""`harness-aibom` command-line entrypoint: scan / validate / diff."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .collectors.hermes import HermesCollector
|
|
12
|
+
from .collectors.openclaw import OpenClawCollector
|
|
13
|
+
from .cyclonedx import current_hostname, to_cyclonedx
|
|
14
|
+
from .diff import diff_documents
|
|
15
|
+
from .model import HarnessDocument
|
|
16
|
+
from .validate import validate_document
|
|
17
|
+
|
|
18
|
+
COLLECTORS = {
|
|
19
|
+
"hermes": HermesCollector,
|
|
20
|
+
"openclaw": OpenClawCollector,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _run_scan(args: argparse.Namespace) -> int:
|
|
25
|
+
home = Path(args.home).expanduser() if args.home else Path.home()
|
|
26
|
+
|
|
27
|
+
if args.runtime == "auto":
|
|
28
|
+
candidates = [cls(home=home) for cls in COLLECTORS.values()]
|
|
29
|
+
active = [c for c in candidates if c.is_present()]
|
|
30
|
+
if not active:
|
|
31
|
+
print(f"no supported runtime found under {home}", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
else:
|
|
34
|
+
active = [COLLECTORS[args.runtime](home=home)]
|
|
35
|
+
|
|
36
|
+
for collector in active:
|
|
37
|
+
doc = HarnessDocument(
|
|
38
|
+
harness_name=f"{collector.runtime_kind}@{current_hostname()}",
|
|
39
|
+
runtime_kind=collector.runtime_kind,
|
|
40
|
+
hostname=current_hostname(),
|
|
41
|
+
)
|
|
42
|
+
collector.collect(doc)
|
|
43
|
+
for w in doc.warnings:
|
|
44
|
+
print(f"warning[{collector.runtime_kind}]: {w}", file=sys.stderr)
|
|
45
|
+
|
|
46
|
+
bom = to_cyclonedx(doc)
|
|
47
|
+
text = json.dumps(bom, indent=2 if args.pretty else None)
|
|
48
|
+
|
|
49
|
+
if args.output:
|
|
50
|
+
out_path = Path(args.output)
|
|
51
|
+
if len(active) > 1:
|
|
52
|
+
out_path = out_path.with_name(f"{collector.runtime_kind}-{out_path.name}")
|
|
53
|
+
out_path.write_text(text + "\n")
|
|
54
|
+
print(f"wrote {out_path}")
|
|
55
|
+
else:
|
|
56
|
+
print(text)
|
|
57
|
+
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _run_validate(args: argparse.Namespace) -> int:
|
|
62
|
+
data = json.loads(Path(args.file).read_text())
|
|
63
|
+
errors = validate_document(data)
|
|
64
|
+
if errors:
|
|
65
|
+
for e in errors:
|
|
66
|
+
print(f"error: {e}", file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
print(f"{args.file}: valid")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _run_diff(args: argparse.Namespace) -> int:
|
|
73
|
+
before = json.loads(Path(args.before).read_text())
|
|
74
|
+
after = json.loads(Path(args.after).read_text())
|
|
75
|
+
print(json.dumps(diff_documents(before, after), indent=2))
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
80
|
+
parser = argparse.ArgumentParser(prog="harness-aibom")
|
|
81
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
82
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
83
|
+
|
|
84
|
+
scan = sub.add_parser("scan", help="scan a running harness and emit a CycloneDX AIBOM")
|
|
85
|
+
scan.add_argument("--runtime", choices=["auto", *COLLECTORS], default="auto")
|
|
86
|
+
scan.add_argument("--home", help="home directory to scan under (default: current user's)")
|
|
87
|
+
scan.add_argument("--output", "-o", help="write to this file instead of stdout")
|
|
88
|
+
scan.add_argument("--pretty", action="store_true", default=True, help="pretty-print JSON (default: on)")
|
|
89
|
+
scan.set_defaults(func=_run_scan)
|
|
90
|
+
|
|
91
|
+
validate = sub.add_parser("validate", help="check a harness-aibom JSON document's shape")
|
|
92
|
+
validate.add_argument("file")
|
|
93
|
+
validate.set_defaults(func=_run_validate)
|
|
94
|
+
|
|
95
|
+
diff = sub.add_parser("diff", help="compare two harness-aibom documents, e.g. before/after a suspected compromise")
|
|
96
|
+
diff.add_argument("before")
|
|
97
|
+
diff.add_argument("after")
|
|
98
|
+
diff.set_defaults(func=_run_diff)
|
|
99
|
+
|
|
100
|
+
return parser
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main(argv: list[str] | None = None) -> int:
|
|
104
|
+
args = build_parser().parse_args(argv)
|
|
105
|
+
return args.func(args)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Collector base class shared by every runtime-specific collector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..model import HarnessDocument
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Collector(ABC):
|
|
12
|
+
"""One collector = one runtime kind (hermes, openclaw, ...).
|
|
13
|
+
|
|
14
|
+
A collector never raises just because the runtime isn't installed --
|
|
15
|
+
it records that via `doc.warn()` and adds nothing. It should only raise
|
|
16
|
+
for genuine programmer errors (e.g. a broken test fixture), not for
|
|
17
|
+
"this piece is missing on this machine".
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
runtime_kind: str
|
|
21
|
+
|
|
22
|
+
def __init__(self, home: Path | None = None):
|
|
23
|
+
self.home = home or Path.home()
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def is_present(self) -> bool:
|
|
27
|
+
"""Cheap, side-effect-free check: does this runtime look installed
|
|
28
|
+
under self.home? Used by `scan --runtime auto`."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def collect(self, doc: HarnessDocument) -> None:
|
|
32
|
+
"""Populate doc with every component/relationship this collector
|
|
33
|
+
can find. Must not raise for missing-but-optional pieces."""
|