Skip to content

Commit b0250c9

Browse files
ampagento-az
andcommitted
feat: add Hermes configuration audit skill
Amp-Thread-ID: https://ampcode.com/threads/T-019fd296-da7a-7667-9bcc-b3e704438f27 Co-authored-by: rekt <omaraziz.dev@gmail.com>
1 parent 0269d11 commit b0250c9

7 files changed

Lines changed: 2077 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
name: auditing-hermes-config
3+
description: Audits revision-pinned Hermes runtime configuration, native NixOS module options, and host configuration. Use when reviewing Hermes defaults, migrations, validity, module wiring, or revision drift.
4+
compatibility: Requires Node.js 20+. Nix is optional; static fallback is explicitly limited.
5+
---
6+
7+
# Auditing Hermes Config
8+
9+
Keep three evidence surfaces separate: Hermes runtime configuration, the exact
10+
`hermes-agent` NixOS module pinned in `flake.lock`, and target host-effective
11+
configuration. Absence from defaults alone does not prove invalidity.
12+
13+
## Run
14+
15+
Set the installed skill directory and always identify the target explicitly:
16+
17+
```sh
18+
SKILL_DIR=/absolute/path/to/skills/auditing-hermes-config
19+
node "$SKILL_DIR/scripts/hermes-config-audit.mjs" discover --target-repo /path/to/nixos-config --source /exact/worktree --cache /outside/cache
20+
node "$SKILL_DIR/scripts/hermes-config-audit.mjs" index --target-repo /path/to/nixos-config --source /exact/worktree --cache /outside/cache
21+
node "$SKILL_DIR/scripts/hermes-config-audit.mjs" audit --target-repo /path/to/nixos-config --host marley --source /exact/worktree --cache /outside/cache --json /outside/reports/audit.json --markdown /outside/reports/audit.md
22+
node "$SKILL_DIR/scripts/hermes-config-audit.mjs" compare --base /outside/base.json --target /outside/target.json
23+
```
24+
25+
`--target-repo` is canonical. `HERMES_TARGET_REPO` and upward discovery are
26+
fallbacks; `--repo` is a compatibility alias. Outputs and cache must be outside
27+
the target, skills repository, and source worktree. Read
28+
[surfaces](reference/surfaces.md) before interpreting results and the
29+
[output schema](reference/output-schema.md) before consuming JSON.
30+
31+
## Discipline
32+
33+
Report **Proper**, **Repo today**, **Gap**, and **Path**. Treat unresolved values
34+
and incomplete dynamic schemas as uncertainty, never as proof of invalidity.
35+
`configFile` may bypass generated `settings`; do not realize derivations merely
36+
to inspect it.
37+
38+
The tool is read-only. Never run Python, build, switch, deploy, rebuild, restart,
39+
or edit target configuration. Snapshot permits at most one read-only `nix eval`
40+
per host invocation, disables import-from-derivation, redacts scalar host values,
41+
and falls back honestly when evaluation fails. Add
42+
`--host-adapter hosts/marley/hermes/settings.nix` for tracked static local evidence;
43+
without it static host evidence remains unresolved.
44+
45+
Static analysis is not effective configuration. Dynamic contracts and the
46+
structural Nix parser can be incomplete; `configFile` content stays unresolved
47+
without realization; offline Nix can fail. `--latest` is tested with a
48+
deterministic fake `gh`; live GitHub availability is not tested. Target dirty
49+
state is reported.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://example.invalid/hermes-config-audit.schema.json",
4+
"type": "object",
5+
"required": [
6+
"schemaVersion",
7+
"command",
8+
"generatedAt",
9+
"readOnly",
10+
"provenance",
11+
"surfaces",
12+
"findings",
13+
"moduleSummary",
14+
"limits",
15+
"safety"
16+
],
17+
"properties": {
18+
"schemaVersion": { "const": 4 },
19+
"command": { "const": "audit" },
20+
"generatedAt": { "type": "string", "format": "date-time" },
21+
"readOnly": { "const": true },
22+
"provenance": {
23+
"type": "object",
24+
"required": [
25+
"selection",
26+
"lockedSha",
27+
"resolvedSha",
28+
"owner",
29+
"repo",
30+
"targetRepo",
31+
"targetDirty",
32+
"host"
33+
],
34+
"properties": {
35+
"selection": { "enum": ["locked", "latest"] },
36+
"lockedSha": { "$ref": "#/$defs/sha" },
37+
"resolvedSha": { "$ref": "#/$defs/sha" },
38+
"owner": { "type": "string", "minLength": 1 },
39+
"repo": { "type": "string", "minLength": 1 },
40+
"targetRepo": { "type": "string", "minLength": 1 },
41+
"targetDirty": { "type": "boolean" },
42+
"host": { "type": "string", "minLength": 1 },
43+
"resolvedAt": { "type": "string", "format": "date-time" }
44+
},
45+
"allOf": [
46+
{
47+
"if": { "properties": { "selection": { "const": "latest" } } },
48+
"then": { "required": ["resolvedAt"] }
49+
}
50+
]
51+
},
52+
"surfaces": {
53+
"type": "object",
54+
"required": ["runtime", "module", "host"],
55+
"properties": {
56+
"runtime": {
57+
"type": "object",
58+
"required": ["contracts", "defaultCount"],
59+
"properties": {
60+
"contracts": { "type": "object" },
61+
"defaultCount": { "type": "integer", "minimum": 0 }
62+
}
63+
},
64+
"module": {
65+
"type": "object",
66+
"required": [
67+
"source",
68+
"optionCount",
69+
"options",
70+
"completeness",
71+
"diagnostics",
72+
"generation"
73+
],
74+
"properties": {
75+
"source": { "type": "string" },
76+
"optionCount": { "type": "integer" },
77+
"options": { "type": "array", "items": { "$ref": "#/$defs/option" } },
78+
"completeness": { "type": "string" },
79+
"diagnostics": { "type": "array", "items": { "type": "string" } },
80+
"generation": { "type": "object" }
81+
}
82+
},
83+
"host": {
84+
"type": "object",
85+
"required": [
86+
"host",
87+
"provenance",
88+
"evaluated",
89+
"runtimeDocument",
90+
"moduleValues",
91+
"localAssignments",
92+
"mode",
93+
"settingsEmpty",
94+
"configFileBypassesSettings",
95+
"limits"
96+
]
97+
}
98+
}
99+
},
100+
"findings": {
101+
"type": "array",
102+
"items": { "$ref": "#/$defs/finding" }
103+
},
104+
"moduleSummary": {
105+
"type": "object",
106+
"required": ["effectiveTopLevelValues", "optionCoverage"],
107+
"properties": {
108+
"effectiveTopLevelValues": { "type": "array" },
109+
"optionCoverage": { "const": "unavailable" }
110+
}
111+
},
112+
"limits": { "type": "array", "items": { "type": "string" } },
113+
"safety": { "type": "object", "required": ["performed", "forbidden"] }
114+
},
115+
"$defs": {
116+
"sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
117+
"evidence": {
118+
"type": "object",
119+
"required": ["source", "line", "url", "excerpt"],
120+
"properties": {
121+
"source": { "type": "string" },
122+
"line": { "type": "integer", "minimum": 1 },
123+
"url": { "type": "string" },
124+
"excerpt": { "type": "string" }
125+
}
126+
},
127+
"option": {
128+
"type": "object",
129+
"required": [
130+
"path",
131+
"typeExpression",
132+
"default",
133+
"example",
134+
"description",
135+
"declaration",
136+
"mappingEvidence"
137+
],
138+
"properties": {
139+
"path": { "type": "string" },
140+
"typeExpression": { "type": ["string", "null"] },
141+
"declaration": { "$ref": "#/$defs/evidence" },
142+
"mappingEvidence": { "type": "array" }
143+
}
144+
},
145+
"finding": {
146+
"type": "object",
147+
"required": ["path", "classification", "reason"],
148+
"properties": {
149+
"path": { "type": "string" },
150+
"classification": {
151+
"enum": [
152+
"current",
153+
"valid-dynamic-extension",
154+
"obsolete-or-wrong-shape",
155+
"ignored-or-nonexistent",
156+
"intentional-semantic-exception",
157+
"uncertain-needs-targeted-review"
158+
]
159+
},
160+
"reason": { "type": "string" },
161+
"evidence": { "$ref": "#/$defs/evidence" }
162+
}
163+
}
164+
}
165+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Output schema (schemaVersion 4)
2+
3+
Every document has `schemaVersion`, `command`, and command-specific data. Audit
4+
documents exactly contain:
5+
6+
- `schemaVersion`, `command`, `generatedAt`, `readOnly`
7+
- `provenance`: `{ selection, lockedSha, resolvedSha, owner, repo, targetRepo,
8+
targetDirty, host }`, plus `resolvedAt` for `latest`
9+
- `surfaces`: `{ runtime: { contracts, defaultCount }, module, host }`
10+
- `findings[]`: `path`, `classification`, `reason`, plus optional `type`,
11+
`unresolved`, and `evidence`; scalar setting values are never emitted
12+
- `moduleSummary`: `{ effectiveTopLevelValues, optionCoverage: "unavailable" }`
13+
- `limits[]`
14+
- `safety`: `{ performed, forbidden }`
15+
16+
Evidence is `{ source, line, url, excerpt }`; upstream URLs include the exact SHA.
17+
Static or unresolved Markdown explicitly says it is not a complete audit.
18+
Host documents preserve key/type/unresolved shape but redact scalar values. Report
19+
files are mode `0600`.
20+
The machine-readable schema is [audit.schema.json](audit.schema.json).
21+
22+
Useful `jq` queries:
23+
24+
```sh
25+
jq -r '.provenance.resolvedSha' audit.json
26+
jq -r '.findings[] | select(.classification == "uncertain-needs-targeted-review") | .path' audit.json
27+
jq -r '.surfaces.module.options[] | [.path, .typeExpression] | @tsv' audit.json
28+
jq -r '.limits[]' audit.json
29+
```
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Evidence surfaces and limits
2+
3+
## Boundaries
4+
5+
The target is selected by `--target-repo`, then `HERMES_TARGET_REPO`, then upward
6+
discovery of a `flake.lock` with `nodes.hermes-agent.locked.rev`. `flake.lock`
7+
and host adapters must be Git-tracked. Every upstream source read is from a
8+
required path present in the selected commit's `git ls-tree`; paths containing a
9+
component exactly `_` are refused.
10+
The exact source worktree's `HEAD` must equal the requested SHA. Paths are
11+
enumerated from that object with `git ls-tree` and read with `git show`; dirty
12+
source worktree files therefore cannot affect evidence.
13+
14+
Cache, indexes, temporary outputs, snapshots, and reports stay outside target,
15+
skills repository, and source worktree. Indexes are recomputed from immutable Git
16+
objects rather than trusted from mutable cache files. Python is parsed, never
17+
executed.
18+
19+
## Runtime and platform contracts
20+
21+
The JavaScript literal lexer ignores comments and understands quoted literals,
22+
collections, booleans, null, and simple numeric arithmetic. It extracts defaults,
23+
root sets, provider fields/aliases, platform container keys and concrete schemas
24+
where present. Provider unknown fields are called ignored only when the pinned
25+
normalizer proves rejection. Platform extraction records completeness; unknown
26+
fields remain uncertain if rejection cannot be proven.
27+
28+
MCP shapes fail closed: known top-level server fields and explicitly supported
29+
nested leaves can be valid; unsupported ancestors or nested leaves are uncertain.
30+
Collision-safe `toolUnresolved: true` tool records remain atomic uncertainty
31+
records. A genuine `$unresolved` key is ordinary user data.
32+
33+
## Native module and host
34+
35+
Structural Nix extraction recognizes `mkOption`, `mkEnableOption`, nested attrsets,
36+
and wildcard `mcpServers.<name>` options without assuming a fixed option count.
37+
Metadata and exact-SHA citations retain line, URL, and excerpt.
38+
39+
Snapshot makes at most one `nix eval` call. It asks the target flake for one host,
40+
uses JSON-safe values, disables import-from-derivation, is offline/read-only, and
41+
never deliberately realizes a build. Reports retain key/type shape but redact
42+
all scalar host values. Static evidence
43+
requires an explicit tracked `--host-adapter` and never guesses target layout.
44+
Derivation-backed
45+
`configFile` contents remain unresolved. Failed/disabled evaluation uses tracked
46+
static host evidence and explicitly limits every host-effective conclusion.

0 commit comments

Comments
 (0)