Skip to content

feat(hermes-attestation-guardian): harden attestation verification and drift controls#192

Merged
davida-ps merged 9 commits intoprompt-security:mainfrom
abutbul:skill/hermes-attestation-guardian-hardening-v0.0.1
Apr 16, 2026
Merged

feat(hermes-attestation-guardian): harden attestation verification and drift controls#192
davida-ps merged 9 commits intoprompt-security:mainfrom
abutbul:skill/hermes-attestation-guardian-hardening-v0.0.1

Conversation

@abutbul
Copy link
Copy Markdown
Contributor

@abutbul abutbul commented Apr 15, 2026

User description

This PR introduces the new hermes-attestation-guardian skill and hardens Hermes runtime attestation verification and drift detection controls.

Initial release note

  • This skill starts at version v0.0.1.
  • skills/hermes-attestation-guardian/CHANGELOG.md is populated with a 0.0.1 entry dated 2026-04-15.

Security fixes and hardening summary

  • Adds deterministic attestation generation with canonicalized payload digesting.
  • Enforces fail-closed verification for schema, digest, optional expected checksum, and detached signatures.
  • Adds baseline authenticity and drift-severity classification for risky toggles, feed verification regressions, trust anchor drift, and watched file drift.
  • Adds Hermes-only cron setup helper with managed marker block and print-only default.
  • Includes output-scope/path guardrails for attestation artifacts and policy parsing safeguards.

Validation run (all passing)

  • python utils/validate_skill.py skills/hermes-attestation-guardian
    • Validation PASSED - all checks passed
  • node skills/hermes-attestation-guardian/test/attestation_schema.test.mjs
    • attestation_schema.test.mjs: ok
  • node skills/hermes-attestation-guardian/test/attestation_diff.test.mjs
    • attestation_diff.test.mjs: ok
  • node skills/hermes-attestation-guardian/test/attestation_cli.test.mjs
    • attestation_cli.test.mjs: ok
  • node skills/hermes-attestation-guardian/test/setup_attestation_cron.test.mjs
    • setup_attestation_cron.test.mjs: ok

Overlap check findings
Searched upstream issues and PRs for:

  • hermes-attestation-guardian
  • baseline authenticity
  • attestation cron marker
  • output scope guard
  • digest binding

Result: no matching open/closed upstream issues or PRs found for these keywords at search time.


Generated description

Below is a concise technical summary of the changes proposed in this PR:
Introduce the Hermes attestation guardian skill that generates canonicalized Hermes posture attestations, verifies them with fail-closed schema/digest/signature checks, and diffing-driven drift severity logic plus cron scheduling guardrails. Document the manifest, docs, and quickstart guidance for the Hermes-only flow so operators can run or schedule the skill confidently.

TopicDetails
Docs & discovery Document the new Hermes skill with changelog/metadata, operator playbooks, wiki narratives, and discovery links so the hardened flow is visible to operators.
Modified files (9)
  • pages/Home.tsx
  • skills/hermes-attestation-guardian/CHANGELOG.md
  • skills/hermes-attestation-guardian/README.md
  • skills/hermes-attestation-guardian/SKILL.md
  • skills/hermes-attestation-guardian/skill.json
  • wiki/GENERATION.md
  • wiki/INDEX.md
  • wiki/modules/hermes-attestation-guardian-draft-history.md
  • wiki/modules/hermes-attestation-guardian.md
Latest Contributors(2)
UserCommitDate
david.a@prompt.securityfeat: enhance support ...February 25, 2026
David.a@prompt.securitychore(docs): correct i...February 06, 2026
Attestation tooling Implement deterministic Hermes attestation generation, fail-closed verification, baseline diffing, and cron guardrails by building the shared lib/attestation.mjs/lib/diff.mjs helpers, CLI scripts, regression sandbox, and supporting tests.
Modified files (10)
  • scripts/hermes_attestation_sandbox_regression.sh
  • skills/hermes-attestation-guardian/lib/attestation.mjs
  • skills/hermes-attestation-guardian/lib/diff.mjs
  • skills/hermes-attestation-guardian/scripts/generate_attestation.mjs
  • skills/hermes-attestation-guardian/scripts/setup_attestation_cron.mjs
  • skills/hermes-attestation-guardian/scripts/verify_attestation.mjs
  • skills/hermes-attestation-guardian/test/attestation_cli.test.mjs
  • skills/hermes-attestation-guardian/test/attestation_diff.test.mjs
  • skills/hermes-attestation-guardian/test/attestation_schema.test.mjs
  • skills/hermes-attestation-guardian/test/setup_attestation_cron.test.mjs
Latest Contributors(0)
UserCommitDate
This pull request is reviewed by Baz. Review like a pro on (Baz).

Comment on lines +171 to +175
function bool(value, defaultValue = false) {
if (value === undefined || value === null) {
return defaultValue;
}
return !!value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since bool() coerces non-null config.json strings to true in buildAttestation() for gateways.*.enabled and security.*, should we normalize/validate config booleans like readEnvBool() before attesting so values like "false" don’t get treated as enabled?

Finding type: Logical Bugs | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In
skills/hermes-attestation-guardian/lib/attestation.mjs around lines 171-176 (`bool`) and
lines 209-229 (`buildAttestation()` where `gateways.*.enabled` and `security.*` are
computed), fix the logical bug where `bool()` treats any non-null string as truthy (so
config values like "false" become enabled). Refactor by either (a) updating `bool()` to
normalize boolean-like strings using the same rules as `readEnvBool()` (trim +
lowercase; accept 1/true/yes/on/enabled and 0/false/no/off/disabled) before coercing, or
(b) validating that the config fields are actual booleans and otherwise ignoring them in
favor of the env fallback/default. Ensure malformed/incorrect config types in
detectHermesConfig() do not flip posture; if the config type is invalid, default to the
provided env fallback.

Comment on lines +342 to +344
if (!isPlainObject(attestation.generator)) {
errors.push("generator object is required");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generator is only validated as an object, but diffAttestations() reads generator.version and defaults to "unknown", so malformed attestations can slip through and skew diff output—should we require generator.version as a string in the schema?

Finding type: Type Inconsistency | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In
skills/hermes-attestation-guardian/lib/attestation.mjs around lines 342-344 inside
`validateAttestationSchema()`, the code only checks that `attestation.generator` is a
plain object and does not validate `attestation.generator.version`. Refactor the
validation to additionally require `generator.version` to be a string (and ideally
non-empty/trimmed) and push a clear schema error if it’s missing or not a string. This
will prevent malformed attestations from passing schema checks and producing misleading
diff results that hide version changes behind the `

Comment on lines +352 to +354
if (!isPlainObject(attestation.posture.runtime)) {
errors.push("posture.runtime object is required");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

posture.runtime only checks object-ness, so diffAttestations() can misread posture.runtime.gateways.* and posture.runtime.risky_toggles.* with !! on malformed shapes; can we validate both sub-objects and their leaf boolean values here?

Finding type: Type Inconsistency | Severity: 🟠 Medium


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In
skills/hermes-attestation-guardian/lib/attestation.mjs around lines 352-354 inside
`validateAttestationSchema`, extend validation for `attestation.posture.runtime`:
currently it only checks that `posture.runtime` is a plain object, but it never
validates `posture.runtime.gateways` and `posture.runtime.risky_toggles`. Refactor this
block to also assert both `gateways` and `risky_toggles` are plain objects and that each
expected leaf key exists (telegram/matrix/discord and
allow_unsigned_mode/bypass_verification) and each corresponding value is a boolean; if
values are missing or not booleans, add descriptive errors. Keep the existing `errors`
array behavior and ensure any malformed shapes that could later affect
`diffAttestations()` are rejected at schema validation time.

Comment on lines +368 to +372
if (!Array.isArray(integrity.watched_files)) {
errors.push("posture.integrity.watched_files must be an array");
}
if (!Array.isArray(integrity.trust_anchors)) {
errors.push("posture.integrity.trust_anchors must be an array");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

posture.integrity.watched_files and trust_anchors only get array checks in validateAttestationSchema(), so objects missing a string path can slip through and be dropped later by mapByPath() in diffAttestations()—should we validate each integrity item shape here (e.g. path string, sha256 string|null, and required fields like exists) before accepting the attestation?

Finding type: Logical Bugs | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In
skills/hermes-attestation-guardian/lib/attestation.mjs around lines 323-375 inside
validateAttestationSchema(), tighten validation of posture.integrity.watched_files and
posture.integrity.trust_anchors: after confirming they are arrays, also iterate each
element and ensure it has the expected object shape (path must be a non-empty string;
sha256 must be either null or a 64-char hex string; and any required fields your
verifier/diff logic expects, e.g. exists boolean). If any element fails, push a specific
error message and return the accumulated errors. This prevents malformed integrity
entries from passing schema validation and then being silently dropped later in
diffAttestations().

Comment on lines +170 to +174
const schemaErrors = validateAttestationSchema(attestation);
for (const message of schemaErrors) {
verificationFindings.push({ severity: "critical", code: "SCHEMA_INVALID", message });
failures.push(message);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema/digest validation flow (including SCHEMA_INVALID/CANONICAL_DIGEST_MISMATCH and BASELINE_SCHEMA_INVALID/BASELINE_CANONICAL_DIGEST_MISMATCH) is duplicated later, so changes could drift—should we extract something like validateAttestationDocument({ bytes, findings, failureList, codePrefix }) and reuse it for both flows before computing the diff?

Finding type: Code Dedup and Conventions | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

@abutbul abutbul force-pushed the skill/hermes-attestation-guardian-hardening-v0.0.1 branch from 59eef65 to aa30e7f Compare April 15, 2026 20:59
@abutbul abutbul force-pushed the skill/hermes-attestation-guardian-hardening-v0.0.1 branch from aa30e7f to 1f1dde4 Compare April 15, 2026 21:00
Comment on lines +113 to +117
const outPath = resolveHermesScopedOutputPath(args.output);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
const body = stableStringify(attestation, args.compact ? 0 : 2);
fs.writeFileSync(outPath, `${body}\n`, "utf8");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_attestation should write current.json atomically instead of directly to outPath, so a crash during fs.writeFileSync doesn’t leave partial invalid JSON — can we write to a temp file in the same dir and rename it into place?

Finding type: Architecture soundness design | Severity: 🟠 Medium


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In
skills/hermes-attestation-guardian/scripts/generate_attestation.mjs around lines 113-117
inside the run() logic that computes outPath and calls fs.writeFileSync, change the
output write to be atomic. Refactor to write the JSON to a temporary file in the same
directory (e.g., prefix/suffix the filename), flush it to disk (fsync) after writing,
and then replace the target outPath via a rename/move that is atomic on the same
filesystem. Also ensure the .sha256 file is written only after the rename succeeds so
watchers never observe a digest paired with a corrupted/partial current.json.

Comment on lines +183 to +189
if (typeof value === "string") {
const norm = value.trim().toLowerCase();
if (["1", "true", "yes", "on", "enabled"].includes(norm)) return true;
if (["0", "false", "no", "off", "disabled"].includes(norm)) return false;
return defaultValue;
}
return defaultValue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool() silently falls back to readEnvBool(...) for malformed values like {} or unknown strings, and also duplicates readEnvBool's normalization logic — should we refactor bool() to explicitly accept only booleans, 0/1, and a fixed set of strings, returning false for anything else, and have readEnvBool reuse it as the single normalization source?

Finding types: Logical Bugs Code Dedup and Conventions | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In skills/hermes-attestation-guardian/lib/attestation.mjs around lines 175-189, refactor
`bool()` to explicitly accept only boolean/number (0/1) and a fixed set of normalized
strings; for anything else, return false or throw an error — do not fall back to
reading `HERMES_*` env vars. Then have `readEnvBool` reuse `bool()` as the single
normalization source of truth. Adjust all call sites (especially the fail-closed
attestation path) so that a present-but-malformed config value is rejected/forced false
rather than triggering an env fallback.

Comment on lines +116 to +120
try {
if (fs.lstatSync(resolvedOutput).isSymbolicLink()) {
throw new Error(`output path must not be a symlink: ${resolvedOutput}`);
}
} catch (error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ENOENT is swallowed on the preflight lstat, should we avoid the TOCTOU gap by opening/creating resolvedOutput atomically with no-follow semantics (or rechecking right before fs.writeFileSync)?

Finding type: Basic Security Patterns | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In
skills/hermes-attestation-guardian/lib/attestation.mjs around lines 116-120, the catch
block that ignores error.code === "ENOENT" causes a TOCTOU issue: a missing path is
treated as safe, but later fs.writeFileSync will follow a symlink if another process
creates one between the check and the write. Refactor this logic so the file is
created/opened in a single atomic step with no-follow semantics (e.g., open with
O_NOFOLLOW and use the resulting file descriptor for writing), or alternatively re-check
for symlink immediately before the write and fail closed if it appears. Ensure the final
write path enforcement is applied on the handle you write to, not only on a prior
lstatSync preflight.

Comment on lines +91 to +102
function isSymlinkPath(filePath) {
try {
return fs.lstatSync(filePath).isSymbolicLink();
} catch (error) {
if (error?.code === "ENOENT") {
return false;
}
throw error;
}
}

function writeAtomically(outPath, body) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeAtomically duplicates the symlink guard from resolveHermesScopedOutputPath (both lstatSync().isSymbolicLink() and throw the same error), should we consolidate to a shared helper or rely only on the resolveHermesScopedOutputPath export?

Finding type: Code Dedup and Conventions | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

Comment on lines +73 to +77
INSTALL_OUT=\$(hermes skills install \"well-known:http://127.0.0.1:$WELL_KNOWN_PORT/.well-known/skills/hermes-attestation-guardian\" --yes 2>&1)
echo \"\$INSTALL_OUT\"

echo \"\$INSTALL_OUT\" | grep -q \"Verdict: SAFE\"
echo \"\$INSTALL_OUT\" | grep -q \"Decision: ALLOWED\"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hermes skills install success currently depends on grepping logs for Verdict: SAFE and Decision: ALLOWED (not documented as part of the contract), so can we assert a stable machine-readable signal from INSTALL_OUT instead of parsing prose?

Finding type: Logical Bugs | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In
scripts/hermes_attestation_sandbox_regression.sh around lines 73-77 (inside the docker
run bash -lc block), the sandbox determines install success by grepping INSTALL_OUT for
the literal strings “Verdict: SAFE” and “Decision: ALLOWED”. Refactor this to
use a stable, machine-readable success signal: first check the hermes CLI for an
existing option/contract (e.g., a JSON output or a documented flag) and assert on that;
if none exists, change the logic to assert on the hermes skills install command exit
code and then verify the skill was actually installed by checking for the expected
installed directory/files under the configured HERMES_HOME. Remove the brittle prose
greps so the test doesn’t fail when wording changes.

@davida-ps davida-ps merged commit 600c945 into prompt-security:main Apr 16, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants