atlas-iac/testing/tests/test_hermes_handoff_policy.py

392 lines
15 KiB
Python
Raw Normal View History

hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
"""Contracts for the fail-closed command policy of the handoff harness."""
from __future__ import annotations
import pytest
from testing.tests.test_hermes_handoff_support import load_handoff_module
policy = load_handoff_module("hermes_handoff_policy")
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
def check(*argv: str, mode: str | None = None) -> None:
policy.check_argv(argv, mode or policy.READ_ONLY)
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
@pytest.mark.parametrize(
"argv",
[
("kubectl", "--namespace", "hermes", "get", "pods", "-o", "name"),
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
("kubectl", "auth", "can-i", "get", "secrets", "--all-namespaces"),
("kubectl", "get", "nodes", "-o", "name"),
(
"kubectl",
"get",
"pods",
"-o",
policy.projection("jsonpath={.status.phase}"),
),
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
("git", "merge-base", "--is-ancestor", "a", "b"),
("git", "ls-remote", "origin", "refs/heads/main"),
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
("hermes", "kanban", "list", "--json"),
("hermes", "sessions", "list", "--source", "telegram"),
("hermes", "status"),
hermes: source handoff forge evidence through the scm broker The acceptance harness pinned a forge client that has never existed in any commit or pod (/opt/coordinator/gitea_api.py, digest f0943db4..., GIT/POST grammar, an askpass helper). Every Gitea-backed check was therefore unrunnable as merged. Point the harness at the credential-isolated SCM broker client that actually ships in the agent pod. - policy: GITEA_CLIENT=/opt/scm/gitea_api.py; trust /opt/scm/ instead of the phantom /opt/coordinator/; admit the client's real grammar (`read <api-path>`, exactly one path) with the same atlas/titan-iac pin and dot-segment rejection; bare HTTP methods are refused in every mode. The armed POST/PATCH/DELETE windows remain but are documented as deferred: the deployed client cannot execute them. - exec: pin the client digest to the sha256 of services/hermes/scm-common/scripts/gitea_api.py — the exact file the hermes-scm-boundary-v2 ConfigMap mounts at /opt/scm/gitea_api.py — so the pin is derivable from merged source and equal to the deployed client. gitea_api.py gains a narrow /api/v1/user identity read in _authorize_read (see below), so the pin is the NEW source hash 76efd16dedbeb74425b12fbbdbfaa391854771292077e0463bf22706855ae6dc. Drop the dangling GIT_ASKPASS (no helper exists; broker git needs none) and swap /opt/coordinator for /opt/scm in SAFE_PATH. - checks: all forge/baseline/lineage probes use (client, "read", path). The SELF-vantage identity checks now truthfully assert the *broker's* forge identity (the only one the platform can exercise) is not an administrator and holds push-scoped, non-administrative repository authority; the administrative-route check asserts the broker read allowlist's live refusal of branch_protections. The remote-main step keeps `origin` (the broker remote exists only in pool workspaces and the broker origin is cluster-local); its rationale now tells the operator to ensure origin fetchability. - gitea_api.py/_authorize_read: allow exactly `/api/v1/user` (no query, no sibling routes) as operation "identity" so the harness can prove the broker identity is not an administrator. The broker imports the same module, so one reviewed edit covers both sides of the boundary. - rules: DENIAL_MARKERS now match the client's real refusal lines ("SCM broker request failed with HTTP 400/403" and the no-credential rejection) and drop "gitea api returned http 403", which the client never emits; a broker 404 is deliberately not denial evidence. - ephemeral: index/verification reads use the real grammar; manual cleanup guidance now says close/delete require operator forge credentials (the client exposes no mutation besides create-draft); armed mode is documented as deferred until the probes are rebuilt on the broker's bounded mutation surface. - docs: broker vantage/evidence section, operator prerequisites (broker healthy, no /vault/secrets/gitea-token anywhere on the harness path, current ConfigMap mount, operator-side client + origin fetchability), armed-mode deferral. - tests: read-grammar accepted / GET refused in every mode, /opt/scm attestation pin proven equal to the merged source digest, real denial-marker matching, /api/v1/user identity route bounds; the repository-pin mutant probe speaks the new grammar. Full handoff + gitea + broker families pass (952 tests), mutation gate 13/13, per-file line+branch coverage >=95%, all touched sources within the 500-line cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:43:13 -03:00
(policy.GITEA_CLIENT, "read", "/api/v1/user"),
(policy.GITEA_CLIENT, "read", "/api/v1/repos/atlas/titan-iac/pulls/19"),
("/opt/scm/gitea_api.py", "read", "/api/v1/repos/atlas/titan-iac"),
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
],
)
def test_read_only_commands_are_permitted(argv: tuple[str, ...]) -> None:
check(*argv)
@pytest.mark.parametrize(
"argv",
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
[
(),
("vault", "read", "kv/x"),
("curl", "https://example.dev"),
("rm", "-rf", "/"),
("kubectl", "get", "pods", "--token", "abc"),
("kubectl", "get", "pods"),
("kubectl", "get", "pods", "--as=system:admin"),
("kubectl", "get", "secrets", "-o", "name"),
("kubectl", "get", "pods,secrets/x", "-o", "name"),
("kubectl", "describe", "pod/x"),
("kubectl", "config", "view", "--raw"),
("kubectl", "auth", "reconcile", "-f", "x"),
("kubectl", "delete", "pod/x", "--dry-run=client"),
("kubectl", "delete", "pod/x", "--", "--dry-run=server"),
("kubectl", "get", "pods", "-o=json"),
("kubectl", "get", "pods", "-o", "jsonpath={.spec.containers[*].env[*].value}"),
("kubectl", "port-forward", "svc/x", "80"),
("kubectl", "exec", "pod"),
("kubectl", "exec", "pod", "--"),
("kubectl",),
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
("flux", "get", "kustomizations", "--all-namespaces"),
("flux", "reconcile", "kustomization", "x"),
("flux",),
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
("helm", "list", "--all-namespaces"),
("helm", "upgrade", "x"),
("helm", "get", "values", "x"),
("git", "commit", "-m", "x"),
("git", "push", "origin", "HEAD:refs/heads/x"),
("git", "fetch", "origin"),
("git", "config", "core.sshCommand", "x"),
("git", "-ccore.sshCommand=x", "ls-remote", "origin"),
("git", "--git-dir=/tmp/x", "status"),
("hermes", "kanban", "complete", "t_1"),
("hermes",),
(policy.GITEA_CLIENT, "POST", "/api/v1/x"),
(policy.GITEA_CLIENT, "GET"),
hermes: source handoff forge evidence through the scm broker The acceptance harness pinned a forge client that has never existed in any commit or pod (/opt/coordinator/gitea_api.py, digest f0943db4..., GIT/POST grammar, an askpass helper). Every Gitea-backed check was therefore unrunnable as merged. Point the harness at the credential-isolated SCM broker client that actually ships in the agent pod. - policy: GITEA_CLIENT=/opt/scm/gitea_api.py; trust /opt/scm/ instead of the phantom /opt/coordinator/; admit the client's real grammar (`read <api-path>`, exactly one path) with the same atlas/titan-iac pin and dot-segment rejection; bare HTTP methods are refused in every mode. The armed POST/PATCH/DELETE windows remain but are documented as deferred: the deployed client cannot execute them. - exec: pin the client digest to the sha256 of services/hermes/scm-common/scripts/gitea_api.py — the exact file the hermes-scm-boundary-v2 ConfigMap mounts at /opt/scm/gitea_api.py — so the pin is derivable from merged source and equal to the deployed client. gitea_api.py gains a narrow /api/v1/user identity read in _authorize_read (see below), so the pin is the NEW source hash 76efd16dedbeb74425b12fbbdbfaa391854771292077e0463bf22706855ae6dc. Drop the dangling GIT_ASKPASS (no helper exists; broker git needs none) and swap /opt/coordinator for /opt/scm in SAFE_PATH. - checks: all forge/baseline/lineage probes use (client, "read", path). The SELF-vantage identity checks now truthfully assert the *broker's* forge identity (the only one the platform can exercise) is not an administrator and holds push-scoped, non-administrative repository authority; the administrative-route check asserts the broker read allowlist's live refusal of branch_protections. The remote-main step keeps `origin` (the broker remote exists only in pool workspaces and the broker origin is cluster-local); its rationale now tells the operator to ensure origin fetchability. - gitea_api.py/_authorize_read: allow exactly `/api/v1/user` (no query, no sibling routes) as operation "identity" so the harness can prove the broker identity is not an administrator. The broker imports the same module, so one reviewed edit covers both sides of the boundary. - rules: DENIAL_MARKERS now match the client's real refusal lines ("SCM broker request failed with HTTP 400/403" and the no-credential rejection) and drop "gitea api returned http 403", which the client never emits; a broker 404 is deliberately not denial evidence. - ephemeral: index/verification reads use the real grammar; manual cleanup guidance now says close/delete require operator forge credentials (the client exposes no mutation besides create-draft); armed mode is documented as deferred until the probes are rebuilt on the broker's bounded mutation surface. - docs: broker vantage/evidence section, operator prerequisites (broker healthy, no /vault/secrets/gitea-token anywhere on the harness path, current ConfigMap mount, operator-side client + origin fetchability), armed-mode deferral. - tests: read-grammar accepted / GET refused in every mode, /opt/scm attestation pin proven equal to the merged source digest, real denial-marker matching, /api/v1/user identity route bounds; the repository-pin mutant probe speaks the new grammar. Full handoff + gitea + broker families pass (952 tests), mutation gate 13/13, per-file line+branch coverage >=95%, all touched sources within the 500-line cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:43:13 -03:00
(policy.GITEA_CLIENT, "GET", "/api/v1/user"),
(policy.GITEA_CLIENT, "GET", "/api/v1/repos/atlas/titan-iac"),
(policy.GITEA_CLIENT, "read", "/api/v1/user", "extra"),
(policy.GITEA_CLIENT, "create-draft", "/api/v1/repos/atlas/titan-iac/pulls"),
("/opt/coordinator/gitea_api.py", "read", "/api/v1/user"),
("sh", "-c", "rm -rf /"),
("sh", "echo"),
("git", "log", "/etc/shadow"),
hermes: source handoff forge evidence through the scm broker The acceptance harness pinned a forge client that has never existed in any commit or pod (/opt/coordinator/gitea_api.py, digest f0943db4..., GIT/POST grammar, an askpass helper). Every Gitea-backed check was therefore unrunnable as merged. Point the harness at the credential-isolated SCM broker client that actually ships in the agent pod. - policy: GITEA_CLIENT=/opt/scm/gitea_api.py; trust /opt/scm/ instead of the phantom /opt/coordinator/; admit the client's real grammar (`read <api-path>`, exactly one path) with the same atlas/titan-iac pin and dot-segment rejection; bare HTTP methods are refused in every mode. The armed POST/PATCH/DELETE windows remain but are documented as deferred: the deployed client cannot execute them. - exec: pin the client digest to the sha256 of services/hermes/scm-common/scripts/gitea_api.py — the exact file the hermes-scm-boundary-v2 ConfigMap mounts at /opt/scm/gitea_api.py — so the pin is derivable from merged source and equal to the deployed client. gitea_api.py gains a narrow /api/v1/user identity read in _authorize_read (see below), so the pin is the NEW source hash 76efd16dedbeb74425b12fbbdbfaa391854771292077e0463bf22706855ae6dc. Drop the dangling GIT_ASKPASS (no helper exists; broker git needs none) and swap /opt/coordinator for /opt/scm in SAFE_PATH. - checks: all forge/baseline/lineage probes use (client, "read", path). The SELF-vantage identity checks now truthfully assert the *broker's* forge identity (the only one the platform can exercise) is not an administrator and holds push-scoped, non-administrative repository authority; the administrative-route check asserts the broker read allowlist's live refusal of branch_protections. The remote-main step keeps `origin` (the broker remote exists only in pool workspaces and the broker origin is cluster-local); its rationale now tells the operator to ensure origin fetchability. - gitea_api.py/_authorize_read: allow exactly `/api/v1/user` (no query, no sibling routes) as operation "identity" so the harness can prove the broker identity is not an administrator. The broker imports the same module, so one reviewed edit covers both sides of the boundary. - rules: DENIAL_MARKERS now match the client's real refusal lines ("SCM broker request failed with HTTP 400/403" and the no-credential rejection) and drop "gitea api returned http 403", which the client never emits; a broker 404 is deliberately not denial evidence. - ephemeral: index/verification reads use the real grammar; manual cleanup guidance now says close/delete require operator forge credentials (the client exposes no mutation besides create-draft); armed mode is documented as deferred until the probes are rebuilt on the broker's bounded mutation surface. - docs: broker vantage/evidence section, operator prerequisites (broker healthy, no /vault/secrets/gitea-token anywhere on the harness path, current ConfigMap mount, operator-side client + origin fetchability), armed-mode deferral. - tests: read-grammar accepted / GET refused in every mode, /opt/scm attestation pin proven equal to the merged source digest, real denial-marker matching, /api/v1/user identity route bounds; the repository-pin mutant probe speaks the new grammar. Full handoff + gitea + broker families pass (952 tests), mutation gate 13/13, per-file line+branch coverage >=95%, all touched sources within the 500-line cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:43:13 -03:00
("git",),
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
],
)
def test_unsafe_commands_are_refused(argv: tuple[str, ...]) -> None:
with pytest.raises(policy.PolicyError):
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
check(*argv)
def test_unknown_mode_is_refused() -> None:
with pytest.raises(policy.PolicyError, match="unknown mode"):
policy.check_argv(("kubectl", "get", "pods"), "whatever")
def test_dry_run_mutation_commands_are_never_constructible() -> None:
with pytest.raises(policy.PolicyError, match="dry-run"):
check("kubectl", "patch", "deploy/x", "--dry-run=server")
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
def test_a_resource_named_after_a_subcommand_cannot_smuggle_a_delete() -> None:
"""`-n get` must not make a delete look like a read."""
with pytest.raises(policy.PolicyError):
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
check("kubectl", "delete", "-n", "get", "configmap", "x")
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
@pytest.mark.parametrize("flag", ["--as", "--as-group", "--as-uid"])
@pytest.mark.parametrize("mode", ["read-only", "ephemeral-armed"])
def test_impersonation_is_refused_from_every_vantage_and_mode(
flag: str, mode: str
) -> None:
"""No vantage may impersonate: not the operator, and not inside a pod.
The inner command of ``kubectl exec`` used to be granted impersonation
unconditionally, so a self/switchyard/node/chat step carrying ``--as`` was
accepted. The inner argv is now re-checked under the same rule.
"""
for argv in (
("kubectl", "get", "namespaces", "-o", "name", flag, "system:admin"),
("kubectl", "get", "ns", "-o", "name", f"{flag}=system:admin"),
):
with pytest.raises(policy.PolicyError, match="forbidden argument"):
check(*argv, mode=mode)
inner = (
"kubectl",
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
"exec",
"pod",
"--",
"/usr/local/bin/kubectl",
"get",
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
"ns",
"-o",
"name",
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
flag,
"system:admin",
)
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
with pytest.raises(policy.PolicyError, match="forbidden argument"):
check(*inner, mode=mode)
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
def test_exec_validates_its_inner_command() -> None:
script = policy.render_shell("env_names")
policy._RENDERED.add(script)
check(
"kubectl", "exec", "--namespace", "hermes", "pod", "--", "/bin/sh", "-c", script
)
check(
"kubectl",
"exec",
"pod",
"--",
"/usr/local/bin/kubectl",
"get",
"ns",
"-o",
"name",
)
with pytest.raises(policy.PolicyError):
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
check("kubectl", "exec", "pod", "--", "bash", "-c", "id")
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
def test_the_repository_pin_has_an_exact_boundary_and_rejects_dot_segments() -> None:
"""A prefix match without a boundary let a look-alike repository through."""
for path in (
"/api/v1/repos/atlas/titan-iac-evil/pulls",
"/api/v1/repos/atlas/titan-iacx",
"/api/v1/repos/atlas/titan-iac../pulls",
):
with pytest.raises(policy.PolicyError):
hermes: source handoff forge evidence through the scm broker The acceptance harness pinned a forge client that has never existed in any commit or pod (/opt/coordinator/gitea_api.py, digest f0943db4..., GIT/POST grammar, an askpass helper). Every Gitea-backed check was therefore unrunnable as merged. Point the harness at the credential-isolated SCM broker client that actually ships in the agent pod. - policy: GITEA_CLIENT=/opt/scm/gitea_api.py; trust /opt/scm/ instead of the phantom /opt/coordinator/; admit the client's real grammar (`read <api-path>`, exactly one path) with the same atlas/titan-iac pin and dot-segment rejection; bare HTTP methods are refused in every mode. The armed POST/PATCH/DELETE windows remain but are documented as deferred: the deployed client cannot execute them. - exec: pin the client digest to the sha256 of services/hermes/scm-common/scripts/gitea_api.py — the exact file the hermes-scm-boundary-v2 ConfigMap mounts at /opt/scm/gitea_api.py — so the pin is derivable from merged source and equal to the deployed client. gitea_api.py gains a narrow /api/v1/user identity read in _authorize_read (see below), so the pin is the NEW source hash 76efd16dedbeb74425b12fbbdbfaa391854771292077e0463bf22706855ae6dc. Drop the dangling GIT_ASKPASS (no helper exists; broker git needs none) and swap /opt/coordinator for /opt/scm in SAFE_PATH. - checks: all forge/baseline/lineage probes use (client, "read", path). The SELF-vantage identity checks now truthfully assert the *broker's* forge identity (the only one the platform can exercise) is not an administrator and holds push-scoped, non-administrative repository authority; the administrative-route check asserts the broker read allowlist's live refusal of branch_protections. The remote-main step keeps `origin` (the broker remote exists only in pool workspaces and the broker origin is cluster-local); its rationale now tells the operator to ensure origin fetchability. - gitea_api.py/_authorize_read: allow exactly `/api/v1/user` (no query, no sibling routes) as operation "identity" so the harness can prove the broker identity is not an administrator. The broker imports the same module, so one reviewed edit covers both sides of the boundary. - rules: DENIAL_MARKERS now match the client's real refusal lines ("SCM broker request failed with HTTP 400/403" and the no-credential rejection) and drop "gitea api returned http 403", which the client never emits; a broker 404 is deliberately not denial evidence. - ephemeral: index/verification reads use the real grammar; manual cleanup guidance now says close/delete require operator forge credentials (the client exposes no mutation besides create-draft); armed mode is documented as deferred until the probes are rebuilt on the broker's bounded mutation surface. - docs: broker vantage/evidence section, operator prerequisites (broker healthy, no /vault/secrets/gitea-token anywhere on the harness path, current ConfigMap mount, operator-side client + origin fetchability), armed-mode deferral. - tests: read-grammar accepted / GET refused in every mode, /opt/scm attestation pin proven equal to the merged source digest, real denial-marker matching, /api/v1/user identity route bounds; the repository-pin mutant probe speaks the new grammar. Full handoff + gitea + broker families pass (952 tests), mutation gate 13/13, per-file line+branch coverage >=95%, all touched sources within the 500-line cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:43:13 -03:00
check(policy.GITEA_CLIENT, "read", path)
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
for path in (
"/api/v1/repos/atlas/titan-iac/../../user/tokens",
"/api/v1/repos/atlas/titan-iac/%2e%2e/%2e%2e/user/tokens",
"/api/v1/repos/atlas/titan-iac/%252e%252e/user/tokens",
"/api/v1/repos/atlas/titan-iac/./pulls",
"/api/v1/repos/atlas/titan-iac//pulls",
):
with pytest.raises(policy.PolicyError, match="relative segments"):
hermes: source handoff forge evidence through the scm broker The acceptance harness pinned a forge client that has never existed in any commit or pod (/opt/coordinator/gitea_api.py, digest f0943db4..., GIT/POST grammar, an askpass helper). Every Gitea-backed check was therefore unrunnable as merged. Point the harness at the credential-isolated SCM broker client that actually ships in the agent pod. - policy: GITEA_CLIENT=/opt/scm/gitea_api.py; trust /opt/scm/ instead of the phantom /opt/coordinator/; admit the client's real grammar (`read <api-path>`, exactly one path) with the same atlas/titan-iac pin and dot-segment rejection; bare HTTP methods are refused in every mode. The armed POST/PATCH/DELETE windows remain but are documented as deferred: the deployed client cannot execute them. - exec: pin the client digest to the sha256 of services/hermes/scm-common/scripts/gitea_api.py — the exact file the hermes-scm-boundary-v2 ConfigMap mounts at /opt/scm/gitea_api.py — so the pin is derivable from merged source and equal to the deployed client. gitea_api.py gains a narrow /api/v1/user identity read in _authorize_read (see below), so the pin is the NEW source hash 76efd16dedbeb74425b12fbbdbfaa391854771292077e0463bf22706855ae6dc. Drop the dangling GIT_ASKPASS (no helper exists; broker git needs none) and swap /opt/coordinator for /opt/scm in SAFE_PATH. - checks: all forge/baseline/lineage probes use (client, "read", path). The SELF-vantage identity checks now truthfully assert the *broker's* forge identity (the only one the platform can exercise) is not an administrator and holds push-scoped, non-administrative repository authority; the administrative-route check asserts the broker read allowlist's live refusal of branch_protections. The remote-main step keeps `origin` (the broker remote exists only in pool workspaces and the broker origin is cluster-local); its rationale now tells the operator to ensure origin fetchability. - gitea_api.py/_authorize_read: allow exactly `/api/v1/user` (no query, no sibling routes) as operation "identity" so the harness can prove the broker identity is not an administrator. The broker imports the same module, so one reviewed edit covers both sides of the boundary. - rules: DENIAL_MARKERS now match the client's real refusal lines ("SCM broker request failed with HTTP 400/403" and the no-credential rejection) and drop "gitea api returned http 403", which the client never emits; a broker 404 is deliberately not denial evidence. - ephemeral: index/verification reads use the real grammar; manual cleanup guidance now says close/delete require operator forge credentials (the client exposes no mutation besides create-draft); armed mode is documented as deferred until the probes are rebuilt on the broker's bounded mutation surface. - docs: broker vantage/evidence section, operator prerequisites (broker healthy, no /vault/secrets/gitea-token anywhere on the harness path, current ConfigMap mount, operator-side client + origin fetchability), armed-mode deferral. - tests: read-grammar accepted / GET refused in every mode, /opt/scm attestation pin proven equal to the merged source digest, real denial-marker matching, /api/v1/user identity route bounds; the repository-pin mutant probe speaks the new grammar. Full handoff + gitea + broker families pass (952 tests), mutation gate 13/13, per-file line+branch coverage >=95%, all touched sources within the 500-line cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:43:13 -03:00
check(policy.GITEA_CLIENT, "read", path)
check(policy.GITEA_CLIENT, "read", "/api/v1/repos/atlas/titan-iac")
check(policy.GITEA_CLIENT, "read", "/api/v1/repos/atlas/titan-iac/pulls?state=all")
def test_the_client_grammar_is_read_not_http_methods_in_every_mode() -> None:
"""The deployed broker client speaks ``read <path>``, never a bare method."""
for mode in (policy.READ_ONLY, policy.ARMED):
check(policy.GITEA_CLIENT, "read", "/api/v1/user", mode=mode)
with pytest.raises(policy.PolicyError, match="not permitted"):
check(policy.GITEA_CLIENT, "GET", "/api/v1/user", mode=mode)
with pytest.raises(policy.PolicyError, match="exactly one API path"):
check(policy.GITEA_CLIENT, "read", "/api/v1/user", "--field", "x=y")
def test_the_client_is_pinned_to_the_scm_boundary_mount() -> None:
assert policy.GITEA_CLIENT == "/opt/scm/gitea_api.py"
assert "/opt/scm/" in policy.TRUSTED_EXECUTABLE_ROOTS
assert "/opt/coordinator/" not in policy.TRUSTED_EXECUTABLE_ROOTS
assert policy.TRUSTED_INNER_PATHS[policy.GITEA_CLIENT] == policy.GITEA_CLIENT
fix(hermes): close the zero-evidence fail-open in absence checks evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:37 +00:00
def test_every_allowlisted_binary_is_release_attestable() -> None:
"""An allowlisted binary with no pinned digest is dead, misleading config."""
exec_module = load_handoff_module("hermes_handoff_exec")
assert set(exec_module.EXPECTED_SHA256) == set(policy.ALLOWED_BINARIES)
assert set(exec_module.EXPECTED_PATHS) == set(policy.ALLOWED_BINARIES)
assert set(exec_module.POD_COMMAND_PATHS) == set(policy.ALLOWED_BINARIES)
assert set(exec_module.CONTEXT_AWARE_BINARIES) <= set(policy.ALLOWED_BINARIES)
def test_shell_templates_reject_credential_roots() -> None:
with pytest.raises(policy.PolicyError, match="credential paths"):
policy.shell("json_fields", path="/runtime-access/token", fields="state")
with pytest.raises(policy.PolicyError, match="credential-bearing"):
policy.shell("json_fields", path="/tmp/health.json", fields="state,token")
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
def test_armed_mode_opens_exactly_the_mutations_it_should() -> None:
ref = "ephemeral/hermes-handoff-acceptance/acceptance1"
policy.arm_ephemeral_policy(ref)
policy.register_ephemeral_pull(9)
check("git", "push", "origin", f"HEAD:refs/heads/{ref}", mode=policy.ARMED)
root = "/api/v1/repos/atlas/titan-iac"
check(
policy.GITEA_CLIENT,
"POST",
f"{root}/pulls",
"--field",
"title=WIP: Hermes handoff acceptance ephemeral probe",
"--field",
f"head={ref}",
"--field",
"base=main",
"--field",
"body=Ephemeral acceptance probe. Closed and deleted by the harness.",
mode=policy.ARMED,
)
check(
policy.GITEA_CLIENT,
"PATCH",
f"{root}/pulls/9",
"--field",
"state=closed",
mode=policy.ARMED,
)
check(policy.GITEA_CLIENT, "DELETE", f"{root}/branches/{ref}", mode=policy.ARMED)
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
with pytest.raises(policy.PolicyError, match="may not force"):
check(
"git",
"push",
"--force",
"origin",
f"HEAD:refs/heads/{ref}",
mode=policy.ARMED,
)
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
with pytest.raises(policy.PolicyError, match="may not force"):
check("git", "push", "--all", "origin", mode=policy.ARMED)
with pytest.raises(policy.PolicyError, match="not permitted"):
check(policy.GITEA_CLIENT, "PUT", f"{root}/pulls/9", mode=policy.ARMED)
def test_arming_and_projection_registration_fail_closed() -> None:
with pytest.raises(policy.PolicyError, match="exact ephemeral ref"):
policy.arm_ephemeral_policy("main")
policy._ARMED_REF = ""
with pytest.raises(policy.PolicyError, match="registration is malformed"):
policy.register_ephemeral_pull(9)
policy.arm_ephemeral_policy("ephemeral/hermes-handoff-acceptance/acceptance1")
for candidate in (
"wide",
"jsonpath={.status.token}",
"jsonpath={.metadata.annotations}",
"jsonpath={.metadata.labels.other}",
"jsonpath={.spec.containers[*].env[*].value}",
):
with pytest.raises(policy.PolicyError):
policy.projection(candidate)
with pytest.raises(policy.PolicyError):
check("/usr/bin/awk", "x")
with pytest.raises(policy.PolicyError):
check("git", "--exec-path=/tmp", "status")
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
def test_every_frozen_template_renders_and_is_accepted() -> None:
parameters = {
"account": "hermes-agent",
"fields": "state,model",
"limit": "200",
"path": "/host-etc/passwd",
"url": "https://scm.example.dev/atlas/repo.git",
}
for name in policy.SHELL_TEMPLATES:
required = policy.template_parameters(name)
argv = policy.shell(name, **{key: parameters[key] for key in required})
policy.check_argv(argv)
assert argv[0] == "sh"
def test_template_rendering_rejects_unknown_names_and_unsafe_parameters() -> None:
with pytest.raises(policy.PolicyError, match="unknown shell template"):
policy.render_shell("nope")
with pytest.raises(policy.PolicyError, match="expects parameters"):
policy.render_shell("json_fields")
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
with pytest.raises(policy.PolicyError, match="unsafe shell parameter"):
policy.render_shell("json_fields", path="/tmp;rm", fields="state")
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
with pytest.raises(policy.PolicyError, match="unsafe shell parameter"):
policy.render_shell("json_fields", path="$(id)", fields="state")
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
def test_positionals_skip_flags_and_stop_at_the_boundary() -> None:
argv = ("kubectl", "--namespace", "hermes", "get", "pods", "--", "ignored")
assert list(policy.positionals(argv)) == ["get", "pods"]
assert list(policy.positionals(("kubectl", "--all-namespaces", "get"))) == ["get"]
def test_a_subcommand_must_precede_its_resource_arguments() -> None:
with pytest.raises(policy.PolicyError):
hermes: add a fail-closed full-handoff acceptance harness Decides whether the Hermes platform handoff is fit to release, and refuses to round an absence of evidence up to a pass. The harness is read-only by default and classifies 71 checks PASS / FAIL / NOT_RUN / NOT_APPLICABLE. Any mandatory FAIL or NOT_RUN is NO_GO, and so is a harness-level problem: an unreachable vantage, a catalog entry whose evidence no longer exists, an expired deadline, or an evaluator that raised. Evidence comes from two vantages that cannot cover for each other: an external read-only operator kubeconfig, and the Hermes agent probing itself from inside its own pod. Before any check runs, the harness asks each vantage who it is and stops if they are the same principal, because dual-vantage evidence from one identity is a restatement rather than a corroboration. `--as` is rejected for every operator-side command and reachable only as the inner command of a `kubectl exec`, so impersonation can never stand in for a real self-probe. A deny check needs a live refused request, not only an authorization review. Two safety properties are structural rather than conventional, enforced where an argv becomes a subprocess: the default mode mutates nothing (mutating verbs require a server dry run; there is deliberately no live TokenRequest probe, because a successful one would mint a real credential), and no probe can pull a credential value into a report (no vault/sops/curl, secrets readable only with -o name, environment probes list names, shell only through frozen reviewed templates). Captures are bounded before they are screened, and the rendered report is re-screened before it is written. Mutation lives behind a separate arming flag with an exact confirmation phrase, a caller-supplied unique ref, a preflight that refuses a protected push target before any network call, and a cleanup whose verification is itself mandatory. A default run reports those four checks NOT_RUN. The catalog is declarative so a reviewer reads what is asserted rather than how it is plumbed, and so structural properties can be proven over every entry before a run. Catalog drift surfaces as NOT_RUN, which stops the release. docs/hermes_full_handoff_acceptance.md carries the merge order for PRs #14-#18 on top of the merged #13 baseline, the image build and Flux rollout, the rollback point for each step, the go/no-go checklist, and the limits that are asserted rather than exercised. Validation: 295 handoff tests pass with 100% line coverage on all 15 new modules; the full unit suite is 647 passed with two failures that reproduce unchanged on origin/main; Ruff, py_compile, kustomize render, and a diff credential screen are clean; a live read-only run against Atlas returns NO_GO for the pre-merge cluster with no unscreened fields in the report.
2026-08-17 10:14:17 +00:00
check("kubectl", "pods", "get")
@pytest.mark.parametrize(
"argv",
[
("/usr/bin/../../tmp/kubectl", "get", "pods"),
("/tmp/reviewer/kubectl", "get", "pods"),
("kubectl", "get", "pods", "-o", "go-template=x"),
("kubectl", "get", "pods", "-o", "jsonpath={.status.token}"),
("kubectl", "get", "pods", "-o", "jsonpath={.metadata.annotations}"),
("kubectl", "get", "pods", "-o", "jsonpath={.spec.containers[*].env[*].value}"),
("git", "-C", "/tmp", "status"),
("git", "--work-tree=/tmp", "status"),
hermes: source handoff forge evidence through the scm broker The acceptance harness pinned a forge client that has never existed in any commit or pod (/opt/coordinator/gitea_api.py, digest f0943db4..., GIT/POST grammar, an askpass helper). Every Gitea-backed check was therefore unrunnable as merged. Point the harness at the credential-isolated SCM broker client that actually ships in the agent pod. - policy: GITEA_CLIENT=/opt/scm/gitea_api.py; trust /opt/scm/ instead of the phantom /opt/coordinator/; admit the client's real grammar (`read <api-path>`, exactly one path) with the same atlas/titan-iac pin and dot-segment rejection; bare HTTP methods are refused in every mode. The armed POST/PATCH/DELETE windows remain but are documented as deferred: the deployed client cannot execute them. - exec: pin the client digest to the sha256 of services/hermes/scm-common/scripts/gitea_api.py — the exact file the hermes-scm-boundary-v2 ConfigMap mounts at /opt/scm/gitea_api.py — so the pin is derivable from merged source and equal to the deployed client. gitea_api.py gains a narrow /api/v1/user identity read in _authorize_read (see below), so the pin is the NEW source hash 76efd16dedbeb74425b12fbbdbfaa391854771292077e0463bf22706855ae6dc. Drop the dangling GIT_ASKPASS (no helper exists; broker git needs none) and swap /opt/coordinator for /opt/scm in SAFE_PATH. - checks: all forge/baseline/lineage probes use (client, "read", path). The SELF-vantage identity checks now truthfully assert the *broker's* forge identity (the only one the platform can exercise) is not an administrator and holds push-scoped, non-administrative repository authority; the administrative-route check asserts the broker read allowlist's live refusal of branch_protections. The remote-main step keeps `origin` (the broker remote exists only in pool workspaces and the broker origin is cluster-local); its rationale now tells the operator to ensure origin fetchability. - gitea_api.py/_authorize_read: allow exactly `/api/v1/user` (no query, no sibling routes) as operation "identity" so the harness can prove the broker identity is not an administrator. The broker imports the same module, so one reviewed edit covers both sides of the boundary. - rules: DENIAL_MARKERS now match the client's real refusal lines ("SCM broker request failed with HTTP 400/403" and the no-credential rejection) and drop "gitea api returned http 403", which the client never emits; a broker 404 is deliberately not denial evidence. - ephemeral: index/verification reads use the real grammar; manual cleanup guidance now says close/delete require operator forge credentials (the client exposes no mutation besides create-draft); armed mode is documented as deferred until the probes are rebuilt on the broker's bounded mutation surface. - docs: broker vantage/evidence section, operator prerequisites (broker healthy, no /vault/secrets/gitea-token anywhere on the harness path, current ConfigMap mount, operator-side client + origin fetchability), armed-mode deferral. - tests: read-grammar accepted / GET refused in every mode, /opt/scm attestation pin proven equal to the merged source digest, real denial-marker matching, /api/v1/user identity route bounds; the repository-pin mutant probe speaks the new grammar. Full handoff + gitea + broker families pass (952 tests), mutation gate 13/13, per-file line+branch coverage >=95%, all touched sources within the 500-line cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 17:43:13 -03:00
(policy.GITEA_CLIENT, "read", "not-an-api-path"),
(policy.GITEA_CLIENT, "read", "/api/v1/repos/atlas/other"),
],
)
def test_provenance_projection_and_repository_bypasses_are_rejected(argv) -> None:
with pytest.raises(policy.PolicyError):
check(*argv)
def test_armed_writes_are_pinned_to_exact_remote_ref_and_endpoints() -> None:
ref = "ephemeral/hermes-handoff-acceptance/acceptance1"
policy.arm_ephemeral_policy(ref)
policy.register_ephemeral_pull(9)
with pytest.raises(policy.PolicyError, match="pinned to origin"):
check(
"git",
"push",
"upstream",
"HEAD:refs/heads/ephemeral/hermes-handoff-acceptance/acceptance1",
mode=policy.ARMED,
)
with pytest.raises(policy.PolicyError, match="preflighted ephemeral ref"):
check("git", "push", "origin", "HEAD:refs/heads/other", mode=policy.ARMED)
with pytest.raises(policy.PolicyError):
check(
policy.GITEA_CLIENT,
"DELETE",
"/api/v1/repos/atlas/titan-iac",
mode=policy.ARMED,
)
with pytest.raises(policy.PolicyError):
check(
policy.GITEA_CLIENT,
"PATCH",
"/api/v1/repos/atlas/titan-iac/pulls/8",
"--field",
"state=closed",
mode=policy.ARMED,
)
with pytest.raises(policy.PolicyError):
check(
policy.GITEA_CLIENT,
"POST",
"/api/v1/repos/atlas/titan-iac/pulls",
"--field",
"title=WIP: wrong branch",
"--field",
"head=main",
"--field",
"base=main",
mode=policy.ARMED,
)