Compare commits
4 Commits
3cd8a17e77
...
3fec1cd5dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fec1cd5dc | ||
|
|
b0812bbe9f | ||
|
|
bf777066fb | ||
|
|
36031958a4 |
@ -1,19 +0,0 @@
|
||||
# clusters/atlas/flux-system/applications/hermes-observer-bindings/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: hermes-observer-bindings
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./services/hermes-observer-bindings
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
wait: true
|
||||
timeout: 5m
|
||||
dependsOn:
|
||||
- name: hermes
|
||||
- name: hermes-observer-rbac
|
||||
@ -31,7 +31,6 @@ resources:
|
||||
- hermes-scm-broker-code/kustomization.yaml
|
||||
- hermes/kustomization.yaml
|
||||
- hermes-observer-rbac/kustomization.yaml
|
||||
- hermes-observer-bindings/kustomization.yaml
|
||||
- hermes-scm-broker/kustomization.yaml
|
||||
- hermes-chat/kustomization.yaml
|
||||
- hermes-triage-demo/kustomization.yaml
|
||||
|
||||
@ -629,125 +629,16 @@ source = source[:runs_start] + runs_source
|
||||
path.write_text(source)
|
||||
PY
|
||||
|
||||
# ``create_task(initial_status="blocked")`` is an explicit operator park, but
|
||||
# upstream only treats later block_task() events as sticky. Recognize the
|
||||
# existing created(status=blocked) event too. This also protects tasks created
|
||||
# before this image patch without making dependency or circuit-breaker blocks
|
||||
# depend on a new persisted field.
|
||||
RUN python - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path("/opt/hermes/hermes_cli/kanban_db.py")
|
||||
db_source = db_path.read_text()
|
||||
|
||||
# The rewritten sticky-block gate below classifies a task from the ``created``
|
||||
# event payload that ``create_task`` writes. That producer is upstream code we
|
||||
# do not own, so anchor the exact contract the consumer depends on instead of
|
||||
# trusting it: an ``initial_status="blocked"`` park must resolve ``task_status``
|
||||
# to ``"blocked"``, every other creation must resolve it to something else, and
|
||||
# the ``created`` event must carry that same variable under the ``"status"``
|
||||
# key. If upstream renames the key, drops it, hardcodes it, or stops deriving it
|
||||
# from ``initial_status``, fail the build here rather than ship a consumer that
|
||||
# silently mis-classifies every task it reads. These assert only -- they
|
||||
# deliberately do not rewrite the producer. The regression suite copied in below
|
||||
# is the second net: it proves the same contract against the real create_task,
|
||||
# for the drift these textual anchors cannot see.
|
||||
producer_park_anchor = ''' if initial_status == "blocked":
|
||||
task_status = "blocked"
|
||||
'''
|
||||
if db_source.count(producer_park_anchor) != 1:
|
||||
raise SystemExit(
|
||||
"Hermes Kanban create_task park semantics changed: expected 1, "
|
||||
f"found {db_source.count(producer_park_anchor)}"
|
||||
)
|
||||
# The mirror of the park branch: every non-park creation must resolve to a
|
||||
# status the gate does *not* read as a park. Without this, drift that widened
|
||||
# the ladder to park ordinary work would leave both other anchors intact.
|
||||
producer_unparked_anchor = ''' elif triage:
|
||||
task_status = "triage"
|
||||
else:
|
||||
task_status = "ready"
|
||||
'''
|
||||
if db_source.count(producer_unparked_anchor) != 1:
|
||||
raise SystemExit(
|
||||
"Hermes Kanban create_task non-park status ladder changed: expected 1, "
|
||||
f"found {db_source.count(producer_unparked_anchor)}"
|
||||
)
|
||||
producer_event_anchor = ''' _append_event(
|
||||
conn,
|
||||
task_id,
|
||||
"created",
|
||||
{
|
||||
"assignee": assignee,
|
||||
"status": task_status,
|
||||
'''
|
||||
if db_source.count(producer_event_anchor) != 1:
|
||||
raise SystemExit(
|
||||
"Hermes Kanban created-event status payload changed: expected 1, "
|
||||
f"found {db_source.count(producer_event_anchor)}"
|
||||
)
|
||||
|
||||
sticky_doc_before = ''' The cheapest signal that distinguishes the two is the most recent
|
||||
``"blocked"`` / ``"unblocked"`` event for the task. If the most
|
||||
recent one is ``"blocked"`` (or there is a ``"blocked"`` event and
|
||||
no ``"unblocked"`` event has fired since), the task is sticky and
|
||||
``recompute_ready`` must *not* auto-promote it.
|
||||
|
||||
Returns ``False`` when there is no such event at all (e.g. the task
|
||||
'''
|
||||
sticky_doc_after = ''' The cheapest signal that distinguishes the two is the most recent
|
||||
``"created"`` / ``"blocked"`` / ``"unblocked"`` event for the task.
|
||||
A ``"created"`` event whose payload has ``status="blocked"`` is the
|
||||
explicit park requested by ``create_task(initial_status="blocked")``.
|
||||
A later ``"unblocked"`` event clears either kind of sticky block.
|
||||
|
||||
Returns ``False`` when there is no such event at all (e.g. the task
|
||||
'''
|
||||
if db_source.count(sticky_doc_before) != 1:
|
||||
raise SystemExit(
|
||||
"Hermes Kanban sticky-block documentation changed: expected 1, "
|
||||
f"found {db_source.count(sticky_doc_before)}"
|
||||
)
|
||||
db_source = db_source.replace(sticky_doc_before, sticky_doc_after, 1)
|
||||
|
||||
sticky_before = ''' row = conn.execute(
|
||||
"SELECT kind FROM task_events "
|
||||
"WHERE task_id = ? AND kind IN ('blocked', 'unblocked') "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(task_id,),
|
||||
).fetchone()
|
||||
return bool(row) and row["kind"] == "blocked"
|
||||
'''
|
||||
sticky_after = ''' row = conn.execute(
|
||||
"SELECT kind, payload FROM task_events "
|
||||
"WHERE task_id = ? AND kind IN ('created', 'blocked', 'unblocked') "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(task_id,),
|
||||
).fetchone()
|
||||
if not row or row["kind"] == "unblocked":
|
||||
return False
|
||||
if row["kind"] == "blocked":
|
||||
return True
|
||||
try:
|
||||
payload = json.loads(row["payload"] or "{}")
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if not isinstance(payload, dict):
|
||||
return False
|
||||
return payload.get("status") == "blocked"
|
||||
'''
|
||||
if db_source.count(sticky_before) != 1:
|
||||
raise SystemExit(
|
||||
"Hermes Kanban sticky-block gate changed: expected 1, "
|
||||
f"found {db_source.count(sticky_before)}"
|
||||
)
|
||||
db_source = db_source.replace(sticky_before, sticky_after, 1)
|
||||
|
||||
# A manual evidence-based close must not expose a parked task to the dispatcher
|
||||
# between separate `unblock` and `complete` commands. Upstream already permits
|
||||
# direct completion from `blocked`; extend the same atomic path to `scheduled`
|
||||
# and make that contract visible in CLI help so agents do not create a ready
|
||||
# window that can launch redundant work.
|
||||
RUN python - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path("/opt/hermes/hermes_cli/kanban_db.py")
|
||||
db_source = db_path.read_text()
|
||||
state_before = ''' WHERE id = ?
|
||||
AND status IN ('running', 'ready', 'blocked')
|
||||
'''
|
||||
@ -780,10 +671,6 @@ if cli_source.count(help_before) != 1:
|
||||
cli_path.write_text(cli_source.replace(help_before, help_after, 1))
|
||||
PY
|
||||
|
||||
COPY dockerfiles/hermes-kanban-blocked-regression.py /tmp/hermes-kanban-blocked-regression.py
|
||||
RUN /opt/hermes/.venv/bin/python /tmp/hermes-kanban-blocked-regression.py \
|
||||
&& rm /tmp/hermes-kanban-blocked-regression.py
|
||||
|
||||
COPY dockerfiles/hermes-python-sandbox-tool.py /opt/hermes/tools/python_sandbox_tool.py
|
||||
COPY dockerfiles/hermes-public-extract/__init__.py /opt/hermes/plugins/web/public_extract/__init__.py
|
||||
COPY dockerfiles/hermes-public-extract/plugin.yaml /opt/hermes/plugins/web/public_extract/plugin.yaml
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
**
|
||||
!dockerfiles/hermes-kanban-blocked-regression.py
|
||||
!dockerfiles/hermes-python-sandbox-tool.py
|
||||
!dockerfiles/hermes-public-extract/
|
||||
!dockerfiles/hermes-public-extract/**
|
||||
|
||||
@ -1,249 +0,0 @@
|
||||
"""Regression tests for Hermes Kanban blocked-task scheduling semantics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_HERMES_HOME = tempfile.TemporaryDirectory(prefix="hermes-kanban-test-home-")
|
||||
os.environ["HERMES_HOME"] = _HERMES_HOME.name
|
||||
|
||||
from hermes_cli import kanban_db # noqa: E402
|
||||
|
||||
|
||||
class BlockedTaskSchedulingTests(unittest.TestCase):
|
||||
"""Exercise the real upstream database API against isolated databases."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self._database_dir = tempfile.TemporaryDirectory(
|
||||
prefix="hermes-kanban-test-db-"
|
||||
)
|
||||
self.connection = kanban_db.connect(
|
||||
Path(self._database_dir.name) / "kanban.db"
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.connection.close()
|
||||
self._database_dir.cleanup()
|
||||
|
||||
def create_task(self, title: str, **kwargs: object) -> str:
|
||||
return kanban_db.create_task(self.connection, title=title, **kwargs)
|
||||
|
||||
def status(self, task_id: str) -> str:
|
||||
task = kanban_db.get_task(self.connection, task_id)
|
||||
self.assertIsNotNone(task)
|
||||
return task.status
|
||||
|
||||
def created_payload(self, task_id: str) -> dict:
|
||||
"""Return the decoded payload of the task's ``created`` event."""
|
||||
row = self.connection.execute(
|
||||
"SELECT payload FROM task_events "
|
||||
"WHERE task_id = ? AND kind = 'created' ORDER BY id LIMIT 1",
|
||||
(task_id,),
|
||||
).fetchone()
|
||||
self.assertIsNotNone(row, "create_task emitted no 'created' event")
|
||||
return json.loads(row["payload"] or "{}")
|
||||
|
||||
def set_created_payload(self, task_id: str, payload: object) -> None:
|
||||
"""Overwrite a ``created`` event payload to simulate producer drift."""
|
||||
with self.connection:
|
||||
self.connection.execute(
|
||||
"UPDATE task_events SET payload = ? "
|
||||
"WHERE task_id = ? AND kind = 'created'",
|
||||
(payload, task_id),
|
||||
)
|
||||
|
||||
def block_via_circuit_breaker(self, title: str) -> str:
|
||||
"""Create + claim a normal task, then trip the breaker once on it.
|
||||
|
||||
The resulting task is ``blocked`` with exactly one recorded failure and
|
||||
-- critically -- no ``blocked`` event, because the breaker emits
|
||||
``gave_up``. Its most recent created/blocked/unblocked event is
|
||||
therefore its own ``created`` event, which is what makes it the right
|
||||
probe for the created-event branch of the sticky gate.
|
||||
"""
|
||||
task_id = self.create_task(title)
|
||||
self.assertIsNotNone(kanban_db.claim_task(self.connection, task_id))
|
||||
self.assertTrue(
|
||||
kanban_db._record_spawn_failure(
|
||||
self.connection,
|
||||
task_id,
|
||||
"worker failed",
|
||||
failure_limit=1,
|
||||
)
|
||||
)
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
return task_id
|
||||
|
||||
def test_initial_block_without_parents_is_sticky(self) -> None:
|
||||
task_id = self.create_task("operator parked", initial_status="blocked")
|
||||
|
||||
self.assertEqual(kanban_db.recompute_ready(self.connection), 0)
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
|
||||
def test_initial_block_with_complete_parents_is_sticky(self) -> None:
|
||||
parent_id = self.create_task("completed prerequisite")
|
||||
self.assertTrue(kanban_db.complete_task(self.connection, parent_id))
|
||||
task_id = self.create_task(
|
||||
"operator parked after prerequisite",
|
||||
initial_status="blocked",
|
||||
parents=[parent_id],
|
||||
)
|
||||
|
||||
self.assertEqual(kanban_db.recompute_ready(self.connection), 0)
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
|
||||
def test_initial_block_remains_sticky_when_parent_later_completes(self) -> None:
|
||||
parent_id = self.create_task("incomplete prerequisite")
|
||||
task_id = self.create_task(
|
||||
"operator parked before prerequisite completes",
|
||||
initial_status="blocked",
|
||||
parents=[parent_id],
|
||||
)
|
||||
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
self.assertTrue(kanban_db.complete_task(self.connection, parent_id))
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
|
||||
def test_initial_block_is_released_by_an_explicit_unblock(self) -> None:
|
||||
task_id = self.create_task("operator parked", initial_status="blocked")
|
||||
|
||||
self.assertTrue(kanban_db.unblock_task(self.connection, task_id))
|
||||
self.assertEqual(self.status(task_id), "ready")
|
||||
|
||||
def test_dependency_block_promotes_after_parent_completes(self) -> None:
|
||||
parent_id = self.create_task("incomplete prerequisite")
|
||||
task_id = self.create_task("dependency gated", parents=[parent_id])
|
||||
self.assertEqual(self.status(task_id), "todo")
|
||||
|
||||
self.assertTrue(kanban_db.complete_task(self.connection, parent_id))
|
||||
self.assertEqual(self.status(task_id), "ready")
|
||||
|
||||
def test_explicit_block_and_unblock_remain_sticky_and_reversible(self) -> None:
|
||||
task_id = self.create_task("worker handoff")
|
||||
|
||||
self.assertTrue(
|
||||
kanban_db.block_task(
|
||||
self.connection,
|
||||
task_id,
|
||||
reason="human review required",
|
||||
)
|
||||
)
|
||||
self.assertEqual(kanban_db.recompute_ready(self.connection), 0)
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
self.assertTrue(kanban_db.unblock_task(self.connection, task_id))
|
||||
self.assertEqual(self.status(task_id), "ready")
|
||||
|
||||
def test_circuit_breaker_block_is_not_auto_promoted(self) -> None:
|
||||
task_id = self.create_task("repeated worker failure")
|
||||
self.assertIsNotNone(kanban_db.claim_task(self.connection, task_id))
|
||||
|
||||
# Upstream has no public circuit-breaker entry point; exercise the
|
||||
# dispatcher helper that owns its persisted failure-limit semantics.
|
||||
self.assertTrue(
|
||||
kanban_db._record_spawn_failure(
|
||||
self.connection,
|
||||
task_id,
|
||||
"worker failed",
|
||||
failure_limit=1,
|
||||
)
|
||||
)
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
self.assertEqual(
|
||||
kanban_db.recompute_ready(self.connection, failure_limit=1),
|
||||
0,
|
||||
)
|
||||
self.assertEqual(self.status(task_id), "blocked")
|
||||
|
||||
def test_circuit_breaker_block_is_promoted_below_the_failure_limit(self) -> None:
|
||||
"""Non-sticky oracle for the created-event branch of the sticky gate.
|
||||
|
||||
A circuit-breaker block must still auto-recover once the effective
|
||||
failure limit rises above the recorded failure count -- that is the
|
||||
pre-existing upstream contract the sticky-block patch must not widen.
|
||||
|
||||
This task's most recent created/blocked/unblocked event is its own
|
||||
``created`` event carrying ``status="ready"``, so it is exactly the
|
||||
case that a mis-classifying gate strands. It fails if the gate treats
|
||||
any ``created`` event as a park (over-broad consumer) and it fails if
|
||||
``create_task`` drifts to labelling every ``created`` event
|
||||
``"blocked"`` (over-broad producer), while the
|
||||
circuit-breaker-at-current-limit case above still holds.
|
||||
"""
|
||||
task_id = self.block_via_circuit_breaker("recoverable worker failure")
|
||||
|
||||
self.assertFalse(
|
||||
kanban_db._has_sticky_block(self.connection, task_id),
|
||||
"a circuit-breaker block must never be classified as a park",
|
||||
)
|
||||
self.assertEqual(
|
||||
kanban_db.recompute_ready(self.connection, failure_limit=2),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(self.status(task_id), "ready")
|
||||
|
||||
def test_created_event_carries_the_status_the_sticky_gate_reads(self) -> None:
|
||||
"""Producer contract asserted directly against the real create_task.
|
||||
|
||||
The sticky gate reads ``created`` payload ``status``. Pin both poles of
|
||||
that contract so producer drift is a test failure here as well as a
|
||||
build failure at the Dockerfile source anchor.
|
||||
"""
|
||||
parked_id = self.create_task("operator parked", initial_status="blocked")
|
||||
normal_id = self.create_task("ordinary work")
|
||||
|
||||
self.assertEqual(self.created_payload(parked_id).get("status"), "blocked")
|
||||
self.assertEqual(self.created_payload(normal_id).get("status"), "ready")
|
||||
|
||||
def test_non_blocked_created_payloads_are_not_treated_as_a_park(self) -> None:
|
||||
"""Only ``status="blocked"`` parks; every other value must promote."""
|
||||
for payload_status in ("ready", "todo", "running", "triage"):
|
||||
with self.subTest(payload_status=payload_status):
|
||||
task_id = self.block_via_circuit_breaker(
|
||||
f"breaker trip with {payload_status} created payload"
|
||||
)
|
||||
self.set_created_payload(
|
||||
task_id, json.dumps({"status": payload_status})
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
kanban_db._has_sticky_block(self.connection, task_id)
|
||||
)
|
||||
self.assertEqual(
|
||||
kanban_db.recompute_ready(self.connection, failure_limit=2),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(self.status(task_id), "ready")
|
||||
|
||||
def test_malformed_created_payloads_fail_closed_to_not_parked(self) -> None:
|
||||
"""Undecodable or non-object payloads must not park -- or crash.
|
||||
|
||||
``recompute_ready`` walks every blocked task in one pass, so an
|
||||
exception escaping the gate would stall promotion board-wide. Falling
|
||||
back to "not parked" preserves upstream's pre-patch auto-recover
|
||||
default for rows the gate cannot interpret.
|
||||
"""
|
||||
malformed = (None, "", " ", "{not json", "[]", "null", '"blocked"', "17")
|
||||
for payload in malformed:
|
||||
with self.subTest(payload=payload):
|
||||
task_id = self.block_via_circuit_breaker(
|
||||
f"breaker trip with payload {payload!r}"
|
||||
)
|
||||
self.set_created_payload(task_id, payload)
|
||||
|
||||
self.assertFalse(
|
||||
kanban_db._has_sticky_block(self.connection, task_id)
|
||||
)
|
||||
self.assertEqual(
|
||||
kanban_db.recompute_ready(self.connection, failure_limit=2),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(self.status(task_id), "ready")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@ -416,3 +416,101 @@ def test_jobs_dashboard_bar_gauges_use_solid_threshold_colors():
|
||||
|
||||
assert {"color": "dark-yellow", "value": 93} in threshold_steps
|
||||
assert {"color": "dark-blue", "value": 100} in threshold_steps
|
||||
|
||||
|
||||
def test_jobs_dashboard_collapses_heavy_drilldowns_for_light_first_paint():
|
||||
mod = load_module()
|
||||
dashboard = mod.build_jobs_dashboard()
|
||||
panels = dashboard["panels"]
|
||||
rows = [panel for panel in panels if panel["type"] == "row"]
|
||||
visible_query_panels = [panel for panel in panels if panel["type"] != "row"]
|
||||
nested_panels_by_title = {
|
||||
child["title"]: child
|
||||
for row in rows
|
||||
for child in row.get("panels", [])
|
||||
}
|
||||
|
||||
assert len(panels) == 18
|
||||
assert len(visible_query_panels) == 12
|
||||
assert sum(len(panel.get("targets", [])) for panel in visible_query_panels) == 12
|
||||
assert all(
|
||||
panel["title"] != "Coverage Gap to 95% by Suite"
|
||||
for panel in visible_query_panels
|
||||
)
|
||||
assert [row["title"] for row in rows] == [
|
||||
"CI Runs And Test Result History",
|
||||
"Check Failure Rates By Suite",
|
||||
"Check Healthy Rates By Suite",
|
||||
"Test Drilldowns And Problem Tests",
|
||||
"Telemetry Completeness And Branches",
|
||||
"SonarQube Project Health",
|
||||
]
|
||||
assert all(row["collapsed"] for row in rows)
|
||||
|
||||
assert "Coverage Failure Rate" in nested_panels_by_title
|
||||
assert "Supply Chain Healthy Rate" in nested_panels_by_title
|
||||
assert "Test Category Health History" in nested_panels_by_title
|
||||
assert "Selected Test Pass Rate History" in nested_panels_by_title
|
||||
assert "Coverage Metrics Present by Suite" in nested_panels_by_title
|
||||
assert "SonarQube API Up" in nested_panels_by_title
|
||||
|
||||
failure_rate_panel = nested_panels_by_title["Coverage Failure Rate"]
|
||||
assert failure_rate_panel["type"] == "state-timeline"
|
||||
assert failure_rate_panel["fieldConfig"]["defaults"]["unit"] == "percent"
|
||||
assert failure_rate_panel["fieldConfig"]["defaults"]["max"] == 100
|
||||
assert failure_rate_panel["fieldConfig"]["defaults"]["thresholds"]["steps"][0]["color"] == "dark-blue"
|
||||
assert "increase(" not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "platform_quality:check_failed_flag:present_1h" in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "platform_quality:check_seen_flag:present_1h" in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "platform_quality:check_status:present_1h" not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert '.*_quality_gate_checks_total' not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "last_over_time" not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert 'label_replace' not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "0 *" in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "and on(suite)" not in failure_rate_panel["targets"][0]["expr"]
|
||||
|
||||
pass_rate_panel = nested_panels_by_title["Selected Test Pass Rate History"]
|
||||
assert pass_rate_panel["type"] == "state-timeline"
|
||||
assert "platform_quality:test_case_pass_rate:percent_1h" in pass_rate_panel["targets"][0]["expr"]
|
||||
assert "platform_quality_gate_test_case_result" not in pass_rate_panel["targets"][0]["expr"]
|
||||
|
||||
pass_fail_panel = nested_panels_by_title["Selected Test Pass/Fail History"]
|
||||
assert pass_fail_panel["fieldConfig"]["defaults"]["custom"]["drawStyle"] == "bars"
|
||||
assert all(
|
||||
"platform_quality:test_case_status:count_1h" in target["expr"]
|
||||
for target in pass_fail_panel["targets"]
|
||||
)
|
||||
|
||||
problematic_panel = nested_panels_by_title["Problematic Tests Over Time (Top failures)"]
|
||||
assert problematic_panel["type"] == "state-timeline"
|
||||
assert problematic_panel["gridPos"]["w"] == 24
|
||||
assert 'test!=""' in problematic_panel["targets"][0]["expr"]
|
||||
assert "vector(0)" not in problematic_panel["targets"][0]["expr"]
|
||||
assert problematic_panel["fieldConfig"]["defaults"]["thresholds"]["steps"] == [
|
||||
{"color": "dark-blue", "value": None},
|
||||
{"color": "dark-green", "value": 2},
|
||||
{"color": "dark-yellow", "value": 3},
|
||||
{"color": "dark-orange", "value": 5},
|
||||
{"color": "dark-red", "value": 8},
|
||||
]
|
||||
assert "rolling 24h failure count" in problematic_panel["description"]
|
||||
assert "at least two recent failures" in problematic_panel["description"]
|
||||
|
||||
sonar_mix_panel = nested_panels_by_title["Sonar Gate Status Mix (Selected)"]
|
||||
sonar_health_panel = nested_panels_by_title["Sonar Gate Health by Project"]
|
||||
assert sonar_mix_panel["gridPos"]["w"] == 4
|
||||
assert sonar_health_panel["gridPos"]["w"] == 8
|
||||
assert sonar_health_panel["type"] == "state-timeline"
|
||||
assert "platform_quality:sonar_gate_health_percent:latest_1h" in sonar_health_panel["targets"][0]["expr"]
|
||||
assert "sonarqube_project_quality_gate_pass" not in sonar_health_panel["targets"][0]["expr"]
|
||||
|
||||
branch_panel = nested_panels_by_title["Primary Branch Clean by Suite (7d)"]
|
||||
recent_branch_panel = nested_panels_by_title["Recent Branch Evidence by Suite (7d)"]
|
||||
assert branch_panel["gridPos"]["x"] == 12
|
||||
assert recent_branch_panel["gridPos"]["x"] == 18
|
||||
assert "[7d:1h]" in recent_branch_panel["targets"][0]["expr"]
|
||||
assert "[7d:1h]" in branch_panel["targets"][0]["expr"]
|
||||
assert branch_panel["fieldConfig"]["defaults"]["unit"] == "percent"
|
||||
assert "unless on(suite)" in branch_panel["targets"][0]["expr"]
|
||||
assert "> bool 0" in branch_panel["targets"][0]["expr"]
|
||||
assert branch_panel["targets"][0]["expr"].startswith("sort(")
|
||||
|
||||
@ -1,127 +0,0 @@
|
||||
"""Detailed Atlas Jobs dashboard collapse and drilldown contracts."""
|
||||
|
||||
from scripts.tests.test_dashboards_render_atlas import load_module
|
||||
|
||||
|
||||
def test_jobs_dashboard_collapses_heavy_drilldowns_for_light_first_paint():
|
||||
mod = load_module()
|
||||
dashboard = mod.build_jobs_dashboard()
|
||||
panels = dashboard["panels"]
|
||||
rows = [panel for panel in panels if panel["type"] == "row"]
|
||||
visible_query_panels = [panel for panel in panels if panel["type"] != "row"]
|
||||
nested_panels_by_title = {
|
||||
child["title"]: child for row in rows for child in row.get("panels", [])
|
||||
}
|
||||
|
||||
assert len(panels) == 18
|
||||
assert len(visible_query_panels) == 12
|
||||
assert sum(len(panel.get("targets", [])) for panel in visible_query_panels) == 12
|
||||
assert all(
|
||||
panel["title"] != "Coverage Gap to 95% by Suite"
|
||||
for panel in visible_query_panels
|
||||
)
|
||||
assert [row["title"] for row in rows] == [
|
||||
"CI Runs And Test Result History",
|
||||
"Check Failure Rates By Suite",
|
||||
"Check Healthy Rates By Suite",
|
||||
"Test Drilldowns And Problem Tests",
|
||||
"Telemetry Completeness And Branches",
|
||||
"SonarQube Project Health",
|
||||
]
|
||||
assert all(row["collapsed"] for row in rows)
|
||||
|
||||
assert "Coverage Failure Rate" in nested_panels_by_title
|
||||
assert "Supply Chain Healthy Rate" in nested_panels_by_title
|
||||
assert "Test Category Health History" in nested_panels_by_title
|
||||
assert "Selected Test Pass Rate History" in nested_panels_by_title
|
||||
assert "Coverage Metrics Present by Suite" in nested_panels_by_title
|
||||
assert "SonarQube API Up" in nested_panels_by_title
|
||||
|
||||
failure_rate_panel = nested_panels_by_title["Coverage Failure Rate"]
|
||||
assert failure_rate_panel["type"] == "state-timeline"
|
||||
assert failure_rate_panel["fieldConfig"]["defaults"]["unit"] == "percent"
|
||||
assert failure_rate_panel["fieldConfig"]["defaults"]["max"] == 100
|
||||
assert (
|
||||
failure_rate_panel["fieldConfig"]["defaults"]["thresholds"]["steps"][0]["color"]
|
||||
== "dark-blue"
|
||||
)
|
||||
assert "increase(" not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert (
|
||||
"platform_quality:check_failed_flag:present_1h"
|
||||
in failure_rate_panel["targets"][0]["expr"]
|
||||
)
|
||||
assert (
|
||||
"platform_quality:check_seen_flag:present_1h"
|
||||
in failure_rate_panel["targets"][0]["expr"]
|
||||
)
|
||||
assert (
|
||||
"platform_quality:check_status:present_1h"
|
||||
not in failure_rate_panel["targets"][0]["expr"]
|
||||
)
|
||||
assert (
|
||||
".*_quality_gate_checks_total" not in failure_rate_panel["targets"][0]["expr"]
|
||||
)
|
||||
assert "last_over_time" not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "label_replace" not in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "0 *" in failure_rate_panel["targets"][0]["expr"]
|
||||
assert "and on(suite)" not in failure_rate_panel["targets"][0]["expr"]
|
||||
|
||||
pass_rate_panel = nested_panels_by_title["Selected Test Pass Rate History"]
|
||||
assert pass_rate_panel["type"] == "state-timeline"
|
||||
assert (
|
||||
"platform_quality:test_case_pass_rate:percent_1h"
|
||||
in pass_rate_panel["targets"][0]["expr"]
|
||||
)
|
||||
assert (
|
||||
"platform_quality_gate_test_case_result"
|
||||
not in pass_rate_panel["targets"][0]["expr"]
|
||||
)
|
||||
|
||||
pass_fail_panel = nested_panels_by_title["Selected Test Pass/Fail History"]
|
||||
assert pass_fail_panel["fieldConfig"]["defaults"]["custom"]["drawStyle"] == "bars"
|
||||
assert all(
|
||||
"platform_quality:test_case_status:count_1h" in target["expr"]
|
||||
for target in pass_fail_panel["targets"]
|
||||
)
|
||||
|
||||
problematic_panel = nested_panels_by_title[
|
||||
"Problematic Tests Over Time (Top failures)"
|
||||
]
|
||||
assert problematic_panel["type"] == "state-timeline"
|
||||
assert problematic_panel["gridPos"]["w"] == 24
|
||||
assert 'test!=""' in problematic_panel["targets"][0]["expr"]
|
||||
assert "vector(0)" not in problematic_panel["targets"][0]["expr"]
|
||||
assert problematic_panel["fieldConfig"]["defaults"]["thresholds"]["steps"] == [
|
||||
{"color": "dark-blue", "value": None},
|
||||
{"color": "dark-green", "value": 2},
|
||||
{"color": "dark-yellow", "value": 3},
|
||||
{"color": "dark-orange", "value": 5},
|
||||
{"color": "dark-red", "value": 8},
|
||||
]
|
||||
assert "rolling 24h failure count" in problematic_panel["description"]
|
||||
assert "at least two recent failures" in problematic_panel["description"]
|
||||
|
||||
sonar_mix_panel = nested_panels_by_title["Sonar Gate Status Mix (Selected)"]
|
||||
sonar_health_panel = nested_panels_by_title["Sonar Gate Health by Project"]
|
||||
assert sonar_mix_panel["gridPos"]["w"] == 4
|
||||
assert sonar_health_panel["gridPos"]["w"] == 8
|
||||
assert sonar_health_panel["type"] == "state-timeline"
|
||||
assert (
|
||||
"platform_quality:sonar_gate_health_percent:latest_1h"
|
||||
in sonar_health_panel["targets"][0]["expr"]
|
||||
)
|
||||
assert (
|
||||
"sonarqube_project_quality_gate_pass"
|
||||
not in sonar_health_panel["targets"][0]["expr"]
|
||||
)
|
||||
|
||||
branch_panel = nested_panels_by_title["Primary Branch Clean by Suite (7d)"]
|
||||
recent_branch_panel = nested_panels_by_title["Recent Branch Evidence by Suite (7d)"]
|
||||
assert branch_panel["gridPos"]["x"] == 12
|
||||
assert recent_branch_panel["gridPos"]["x"] == 18
|
||||
assert "[7d:1h]" in recent_branch_panel["targets"][0]["expr"]
|
||||
assert "[7d:1h]" in branch_panel["targets"][0]["expr"]
|
||||
assert branch_panel["fieldConfig"]["defaults"]["unit"] == "percent"
|
||||
assert "unless on(suite)" in branch_panel["targets"][0]["expr"]
|
||||
assert "> bool 0" in branch_panel["targets"][0]["expr"]
|
||||
assert branch_panel["targets"][0]["expr"].startswith("sort(")
|
||||
@ -4,17 +4,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
MAX_INPUT = 1024 * 1024
|
||||
MAX_RULES = 100
|
||||
MAX_RULE_NAME = 255
|
||||
SPECIAL = frozenset("*?\\[]{}")
|
||||
|
||||
|
||||
class PolicyError(RuntimeError):
|
||||
@ -35,102 +33,35 @@ def _read_bounded(path: Path) -> bytes:
|
||||
return value
|
||||
|
||||
|
||||
def _glob_regex(pattern: str, position: int = 0, terminators: str = "") -> tuple[str, int]:
|
||||
"""Compile the gobwas/glob syntax Gitea v1.23.8 uses."""
|
||||
pieces: list[str] = []
|
||||
while position < len(pattern):
|
||||
character = pattern[position]
|
||||
if character in terminators:
|
||||
break
|
||||
if character == "\\":
|
||||
position += 1
|
||||
if position >= len(pattern):
|
||||
raise ValueError("trailing escape")
|
||||
pieces.append(re.escape(pattern[position]))
|
||||
elif character == "*":
|
||||
if position + 1 < len(pattern) and pattern[position + 1] == "*":
|
||||
pieces.append(".*")
|
||||
position += 1
|
||||
else:
|
||||
pieces.append("[^/]*")
|
||||
elif character == "?":
|
||||
pieces.append("[^/]")
|
||||
elif character == "[":
|
||||
end = pattern.find("]", position + 1)
|
||||
if end < 0:
|
||||
raise ValueError("unterminated range")
|
||||
value = pattern[position + 1 : end]
|
||||
if not value:
|
||||
raise ValueError("empty range")
|
||||
negate = value.startswith("!")
|
||||
value = value[1:] if negate else value
|
||||
if not value or "[" in value or "\\" in value:
|
||||
raise ValueError("invalid range")
|
||||
if "-" in value:
|
||||
if value.count("-") != 1 or value.startswith("-") or value.endswith("-"):
|
||||
raise ValueError("invalid range")
|
||||
low, high = value.split("-")
|
||||
if len(low) != 1 or len(high) != 1 or ord(high) < ord(low):
|
||||
raise ValueError("invalid range")
|
||||
content = re.escape(low) + "-" + re.escape(high)
|
||||
else:
|
||||
content = re.escape(value)
|
||||
pieces.append("[" + ("^" if negate else "") + content + "]")
|
||||
position = end
|
||||
elif character == "{":
|
||||
alternatives: list[str] = []
|
||||
position += 1
|
||||
while True:
|
||||
alternative, position = _glob_regex(pattern, position, ",}")
|
||||
alternatives.append(alternative)
|
||||
if position >= len(pattern):
|
||||
raise ValueError("unterminated alternatives")
|
||||
if pattern[position] == "}":
|
||||
break
|
||||
position += 1
|
||||
if len(alternatives) < 2 or any(not item for item in alternatives):
|
||||
raise ValueError("invalid alternatives")
|
||||
pieces.append("(?:" + "|".join(alternatives) + ")")
|
||||
elif character in "]}":
|
||||
# A closing delimiter outside its grammar context is literal in
|
||||
# gobwas/glob's lexer.
|
||||
pieces.append(re.escape(character))
|
||||
else:
|
||||
pieces.append(re.escape(character))
|
||||
position += 1
|
||||
return "".join(pieces), position
|
||||
|
||||
|
||||
def _is_plain(pattern: str) -> bool:
|
||||
return not any(character in SPECIAL for character in pattern)
|
||||
|
||||
|
||||
def _matches(pattern: str, branch: str) -> bool:
|
||||
if not pattern.isascii() or not 1 <= len(pattern) <= MAX_RULE_NAME:
|
||||
raise PolicyError("branch protection rule name is invalid")
|
||||
if _is_plain(pattern):
|
||||
return pattern.casefold() == branch.casefold()
|
||||
try:
|
||||
expression, position = _glob_regex(pattern)
|
||||
if position != len(pattern):
|
||||
raise ValueError("incomplete glob")
|
||||
except (re.error, ValueError):
|
||||
# Gitea quotes an invalid special pattern and matches it literally.
|
||||
# A literal containing a special byte cannot equal main or master.
|
||||
return False
|
||||
return re.fullmatch(expression, branch) is not None
|
||||
|
||||
|
||||
def _created(value: object) -> datetime.datetime:
|
||||
if not isinstance(value, str) or len(value) > 64:
|
||||
raise PolicyError("branch protection creation time is invalid")
|
||||
try:
|
||||
result = datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise PolicyError("branch protection creation time is invalid") from exc
|
||||
if result.tzinfo is None:
|
||||
raise PolicyError("branch protection creation time is invalid")
|
||||
return result
|
||||
if pattern.count("[") != pattern.count("]") or "\\" in pattern:
|
||||
raise PolicyError("branch protection glob is ambiguous")
|
||||
# Gitea matches ordinary names case-insensitively and compiles wildcard
|
||||
# names as globs with '/' as a separator. Primary branches have no slash,
|
||||
# so fnmatchcase is equivalent for the only two targets managed here.
|
||||
patterns = [pattern]
|
||||
while any("{" in item or "}" in item for item in patterns):
|
||||
expanded: list[str] = []
|
||||
for item in patterns:
|
||||
if "{" not in item and "}" not in item:
|
||||
expanded.append(item)
|
||||
continue
|
||||
start = item.find("{")
|
||||
end = item.find("}", start + 1)
|
||||
if start < 0 or end < 0 or "{" in item[start + 1 : end]:
|
||||
raise PolicyError("branch protection glob is ambiguous")
|
||||
choices = item[start + 1 : end].split(",")
|
||||
if len(choices) < 2 or any(not choice for choice in choices):
|
||||
raise PolicyError("branch protection glob is ambiguous")
|
||||
expanded.extend(item[:start] + choice + item[end + 1 :] for choice in choices)
|
||||
patterns = expanded
|
||||
if len(patterns) > 32:
|
||||
raise PolicyError("branch protection glob expands beyond the safe limit")
|
||||
return any(
|
||||
fnmatch.fnmatchcase(branch.casefold(), item.casefold()) for item in patterns
|
||||
)
|
||||
|
||||
|
||||
def _required(reviewer: str) -> dict[str, object]:
|
||||
@ -159,7 +90,8 @@ def evaluate(value: bytes, branch: str, reviewer: str) -> str:
|
||||
data = json.loads(value)
|
||||
if not isinstance(data, list) or len(data) > MAX_RULES:
|
||||
raise PolicyError("branch protection response is invalid")
|
||||
ordered: list[tuple[int, bool, datetime.datetime, int, dict[str, object]]] = []
|
||||
ordered: list[tuple[int, int, dict[str, object]]] = []
|
||||
priorities: set[int] = set()
|
||||
for index, raw in enumerate(data):
|
||||
if not isinstance(raw, dict):
|
||||
raise PolicyError("branch protection entry is invalid")
|
||||
@ -169,17 +101,16 @@ def evaluate(value: bytes, branch: str, reviewer: str) -> str:
|
||||
not isinstance(priority, int)
|
||||
or isinstance(priority, bool)
|
||||
or not 1 <= priority <= 1_000_000
|
||||
or priority in priorities
|
||||
or not isinstance(rule_name, str)
|
||||
):
|
||||
raise PolicyError("branch protection priority is ambiguous")
|
||||
created = _created(raw.get("created_at"))
|
||||
priorities.add(priority)
|
||||
if _matches(rule_name, branch):
|
||||
# Gitea v1.23.8 sorts by priority, then puts plain names before
|
||||
# globs, then uses creation time. The API exposes every component.
|
||||
ordered.append((priority, not _is_plain(rule_name), created, index, raw))
|
||||
ordered.append((priority, index, raw))
|
||||
if not ordered:
|
||||
return "ABSENT"
|
||||
effective = min(ordered)[4]
|
||||
effective = min(ordered)[2]
|
||||
for key, expected in _required(reviewer).items():
|
||||
if effective.get(key) != expected:
|
||||
raise PolicyError(
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
# services/hermes-observer-bindings/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- rolebindings.yaml
|
||||
@ -3,3 +3,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- rbac.yaml
|
||||
- rolebindings.yaml
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# services/hermes-observer-bindings/rolebindings.yaml
|
||||
# services/hermes-observer-rbac/rolebindings.yaml
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
items:
|
||||
@ -81,7 +81,6 @@ spec:
|
||||
vault.hashicorp.com/agent-limits-mem: 128Mi
|
||||
spec:
|
||||
serviceAccountName: hermes-agent
|
||||
enableServiceLinks: false
|
||||
automountServiceAccountToken: true
|
||||
securityContext:
|
||||
fsGroup: 10000
|
||||
@ -101,7 +100,7 @@ spec:
|
||||
values: ["true"]
|
||||
- key: kubernetes.io/hostname
|
||||
operator: NotIn
|
||||
values: [titan-04, titan-08, titan-13, titan-14, titan-17, titan-18, titan-19]
|
||||
values: [titan-08, titan-13, titan-14, titan-17, titan-18]
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
@ -681,7 +680,7 @@ spec:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
resources:
|
||||
requests: {cpu: 300m, memory: 768Mi}
|
||||
requests: {cpu: 350m, memory: 768Mi}
|
||||
limits: {cpu: "3", memory: 6Gi}
|
||||
- name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561
|
||||
@ -867,7 +866,7 @@ spec:
|
||||
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
|
||||
- {name: PYTHONPATH, value: /opt/hermes}
|
||||
- {name: HERMES_CLI_LANE_CONCURRENCY, value: "2"}
|
||||
- {name: HERMES_CLI_LANE_CONCURRENCY, value: "4"}
|
||||
- {name: HERMES_AUTO_ROUTER_PROFILE, value: agent}
|
||||
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
|
||||
securityContext:
|
||||
@ -892,7 +891,7 @@ spec:
|
||||
- {name: tmp, mountPath: /tmp}
|
||||
resources:
|
||||
requests: {cpu: 100m, memory: 256Mi}
|
||||
limits: {cpu: "2", memory: 6Gi}
|
||||
limits: {cpu: "3", memory: 6Gi}
|
||||
- name: model-steward
|
||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
@ -25,7 +25,7 @@ from gitea_api import (
|
||||
)
|
||||
from gitea_api_policy import _reject_forbidden, _validate_ref, _validate_repo
|
||||
from scm_broker_io import spool_response
|
||||
from scm_broker_server import AbsoluteHeaderDeadlineMixin, BoundedThreadingHTTPServer
|
||||
from scm_broker_server import BoundedThreadingHTTPServer
|
||||
|
||||
BROKER_PORT = 9081
|
||||
GIT_USER = "hermes-automation"
|
||||
@ -319,12 +319,15 @@ def _upstream_git_request(
|
||||
result.close()
|
||||
|
||||
|
||||
class BrokerHandler(AbsoluteHeaderDeadlineMixin, BaseHTTPRequestHandler):
|
||||
class BrokerHandler(BaseHTTPRequestHandler):
|
||||
"""Expose only bounded metadata, draft creation, and smart-HTTP Git."""
|
||||
|
||||
server_version = "HermesSCMBroker/1"
|
||||
sys_version = ""
|
||||
header_deadline_seconds = INBOUND_HEADER_TIMEOUT
|
||||
|
||||
def setup(self) -> None:
|
||||
super().setup()
|
||||
self.connection.settimeout(INBOUND_HEADER_TIMEOUT)
|
||||
|
||||
def _validate_headers(self) -> None:
|
||||
items = list(self.headers.items())
|
||||
|
||||
@ -4,83 +4,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from http.server import ThreadingHTTPServer
|
||||
|
||||
MAX_CONCURRENT_REQUESTS = 3
|
||||
|
||||
|
||||
class _AbsoluteDeadlineReader:
|
||||
"""Read header lines against one wall-clock deadline, not idle timeouts."""
|
||||
|
||||
def __init__(self, stream, connection):
|
||||
self._stream = stream
|
||||
self._connection = connection
|
||||
self._deadline: float | None = None
|
||||
|
||||
def begin(self, timeout: float) -> None:
|
||||
self._deadline = time.monotonic() + timeout
|
||||
|
||||
def end(self) -> None:
|
||||
self._deadline = None
|
||||
|
||||
def readline(self, limit: int = -1) -> bytes:
|
||||
if self._deadline is None:
|
||||
return self._stream.readline(limit)
|
||||
value = bytearray()
|
||||
while limit < 0 or len(value) < limit:
|
||||
remaining = self._deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError("absolute SCM header deadline exceeded")
|
||||
self._connection.settimeout(remaining)
|
||||
character = self._stream.read(1)
|
||||
if not character:
|
||||
break
|
||||
value.extend(character)
|
||||
if character == b"\n":
|
||||
break
|
||||
return bytes(value)
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
return getattr(self._stream, name)
|
||||
|
||||
|
||||
class AbsoluteHeaderDeadlineMixin:
|
||||
"""Apply an absolute request-line plus header deadline to HTTP handlers."""
|
||||
|
||||
header_deadline_seconds = 10.0
|
||||
|
||||
def setup(self) -> None:
|
||||
super().setup()
|
||||
self._header_reader = _AbsoluteDeadlineReader(self.rfile, self.connection)
|
||||
self.rfile = self._header_reader
|
||||
|
||||
def handle_one_request(self) -> None:
|
||||
try:
|
||||
self._header_reader.begin(self.header_deadline_seconds)
|
||||
try:
|
||||
self.raw_requestline = self.rfile.readline(65537)
|
||||
if len(self.raw_requestline) > 65536:
|
||||
self.requestline = ""
|
||||
self.request_version = ""
|
||||
self.command = ""
|
||||
self.send_error(414)
|
||||
return
|
||||
if not self.raw_requestline:
|
||||
self.close_connection = True
|
||||
return
|
||||
if not self.parse_request():
|
||||
return
|
||||
finally:
|
||||
self._header_reader.end()
|
||||
method_name = "do_" + self.command
|
||||
if not hasattr(self, method_name):
|
||||
self.send_error(501, "Unsupported method")
|
||||
return
|
||||
getattr(self, method_name)()
|
||||
self.wfile.flush()
|
||||
except TimeoutError:
|
||||
self.close_connection = True
|
||||
MAX_CONCURRENT_REQUESTS = 2
|
||||
|
||||
|
||||
class BoundedThreadingHTTPServer(ThreadingHTTPServer):
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import stat
|
||||
import struct
|
||||
from pathlib import Path
|
||||
@ -81,33 +80,6 @@ def _audit_policy_metadata(snapshot, root_uid: int, account_uid: int) -> None:
|
||||
raise HardeningError("sudo/polkit policy ACL grants Hermes write access")
|
||||
|
||||
|
||||
def _active_sudo_policy(text: str) -> str:
|
||||
"""Remove comments without discarding sudoers' numeric-UID principals."""
|
||||
active: list[str] = []
|
||||
for line in text.splitlines():
|
||||
stripped = line.lstrip()
|
||||
if not stripped:
|
||||
continue
|
||||
include = re.fullmatch(
|
||||
r"(?i)([#@]include|[#@]includedir)\s+([^\s]+)", stripped
|
||||
)
|
||||
if include:
|
||||
directive, target = include.groups()
|
||||
if directive.lower().endswith("includedir") and target == "/etc/sudoers.d":
|
||||
continue
|
||||
raise HardeningError("sudo policy includes an unaudited authority source")
|
||||
if stripped.startswith("#") and not re.match(r"#\d+(?:\s|$)", stripped):
|
||||
continue
|
||||
active.append(line)
|
||||
return "\n".join(active)
|
||||
|
||||
|
||||
def _mentions_dedicated_identity(text: str, account: str, account_uid: int) -> bool:
|
||||
account_pattern = rf"(?<![A-Za-z0-9_.-])%?{re.escape(account)}(?![A-Za-z0-9_.-])"
|
||||
numeric_pattern = rf"(?<![0-9])#?{account_uid}(?![0-9])"
|
||||
return bool(re.search(account_pattern, text) or re.search(numeric_pattern, text))
|
||||
|
||||
|
||||
def audit_privilege_policies(
|
||||
account: str,
|
||||
account_uid: int,
|
||||
@ -132,18 +104,12 @@ def audit_privilege_policies(
|
||||
raise HardeningError("nsswitch policy is not UTF-8") from exc
|
||||
for line in text.splitlines():
|
||||
name, separator, sources = line.partition(":")
|
||||
if name.strip() in {
|
||||
"passwd",
|
||||
"group",
|
||||
"initgroups",
|
||||
"shadow",
|
||||
"sudoers",
|
||||
} and separator:
|
||||
if name.strip() in {"group", "initgroups", "sudoers"} and separator:
|
||||
active_sources = [item for item in sources.split() if not item.startswith("[")]
|
||||
if not active_sources or any(
|
||||
item not in {"files", "systemd"} for item in active_sources
|
||||
):
|
||||
raise HardeningError("external group/account/sudo authority source is unsafe")
|
||||
raise HardeningError("external group/sudo authority source is unsafe")
|
||||
total = 0
|
||||
for path in _policy_files(host_etc, polkit_share, root_uid):
|
||||
snapshot = read_regular(path, 256 * 1024)
|
||||
@ -156,23 +122,17 @@ def audit_privilege_policies(
|
||||
text = value.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise HardeningError("sudo/polkit policy is not UTF-8") from exc
|
||||
active = "\n".join(
|
||||
line for line in text.splitlines() if not line.lstrip().startswith("#")
|
||||
)
|
||||
if account in active or f"%{account}" in active:
|
||||
raise HardeningError("dedicated Hermes account has sudo/polkit authority")
|
||||
if path.name == "sudoers" or "sudoers.d" in path.parts:
|
||||
active = _active_sudo_policy(text)
|
||||
if _mentions_dedicated_identity(active, account, account_uid):
|
||||
raise HardeningError("dedicated Hermes account has sudo authority")
|
||||
for line in active.splitlines():
|
||||
fields = line.split()
|
||||
if fields and fields[0] in {"ALL", "%ALL"} and "=" in line:
|
||||
raise HardeningError("broad sudo authority includes Hermes")
|
||||
continue
|
||||
active = "\n".join(
|
||||
line for line in text.splitlines() if not line.lstrip().startswith("#")
|
||||
)
|
||||
grants = "polkit.Result.YES" in active or re.search(
|
||||
r"(?im)^\s*Result(?:Any|Inactive|Active)\s*=\s*yes\s*$", active
|
||||
)
|
||||
if grants and _mentions_dedicated_identity(active, account, account_uid):
|
||||
raise HardeningError("dedicated Hermes account has polkit authority")
|
||||
broad = "unix-user:*" in active or "Identity=unix-user:*" in active
|
||||
grants = "polkit.Result.YES" in active or "ResultActive=yes" in active
|
||||
if broad and grants and any(item in active for item in dangerous_polkit):
|
||||
raise HardeningError("root-equivalent broad polkit authority includes Hermes")
|
||||
|
||||
@ -11,7 +11,7 @@ resources:
|
||||
- networking
|
||||
images:
|
||||
- name: registry.bstein.dev/bstein/ariadne
|
||||
newTag: 0.1.0-461 # {"$imagepolicy": "maintenance:ariadne:tag"}
|
||||
newTag: 0.1.0-460 # {"$imagepolicy": "maintenance:ariadne:tag"}
|
||||
- name: registry.bstein.dev/bstein/metis
|
||||
newTag: 0.1.0-295-arm64 # {"$imagepolicy": "maintenance:metis-arm64:tag"}
|
||||
- name: registry.bstein.dev/bstein/soteria
|
||||
|
||||
@ -1,282 +0,0 @@
|
||||
"""Image generation and sandbox-boundary contracts for Hermes chat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_chat_support import (
|
||||
HERMES,
|
||||
VAULT,
|
||||
_documents,
|
||||
)
|
||||
|
||||
|
||||
def test_chat_image_generation_uses_private_owner_broker():
|
||||
"""Family pods get image bytes without receiving the owner's OAuth file."""
|
||||
configmap = _documents(HERMES / "chat-configmap.yaml")[0]
|
||||
assert "shared desktop/Wolf lane" in configmap["data"]["SOUL.md"]
|
||||
assert "local FLUX waits" in configmap["data"]["SOUL.md"]
|
||||
assert "Use `image_generate_local`" in configmap["data"]["SOUL.md"]
|
||||
assert "Use `image_generate_hosted`" in configmap["data"]["SOUL.md"]
|
||||
assert "ComfyUI endpoint" in configmap["data"]["SOUL.md"]
|
||||
assert "`MEDIA:` path" in configmap["data"]["SOUL.md"]
|
||||
assert "`image_edit_latest`" in configmap["data"]["SOUL.md"]
|
||||
assert (
|
||||
"Preserve the most recently selected image lane" in configmap["data"]["SOUL.md"]
|
||||
)
|
||||
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
||||
assert config["image_gen"] == {
|
||||
"provider": "atlas-broker",
|
||||
"model": "atlas-image-auto-high",
|
||||
}
|
||||
assert config["plugins"]["enabled"] == ["atlas-broker", "auto-router"]
|
||||
|
||||
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
||||
pod = statefulset["spec"]["template"]["spec"]
|
||||
hermes = next(item for item in pod["containers"] if item["name"] == "hermes")
|
||||
mounts = {item["name"]: item for item in hermes["volumeMounts"]}
|
||||
assert mounts["image-plugin"]["mountPath"] == (
|
||||
"/opt/hermes/plugins/image_gen/atlas-broker"
|
||||
)
|
||||
assert mounts["runtime-access"]["mountPath"] == "/runtime-access"
|
||||
assert "provider-auth" not in mounts
|
||||
assert not any(
|
||||
mount["name"] == "home" and "agent" in str(mount)
|
||||
for mount in hermes["volumeMounts"]
|
||||
)
|
||||
env = {item["name"]: item["value"] for item in hermes["env"]}
|
||||
assert env["HERMES_IMAGE_BROKER_URL"].startswith("http://hermes-image-broker.")
|
||||
|
||||
plugin = (HERMES / "plugins" / "image-gen-broker" / "__init__.py").read_text()
|
||||
assert '"local": "flux-2-klein-4b-local"' in plugin
|
||||
assert '"hosted": "gpt-image-2-high"' in plugin
|
||||
assert 'name="image_generate_local"' in plugin
|
||||
assert 'name="image_generate_hosted"' in plugin
|
||||
assert '"name": "image_edit_latest"' in plugin
|
||||
assert '"name": "image_edit_latest_local"' in plugin
|
||||
assert '"name": "image_edit_latest_hosted"' in plugin
|
||||
assert "def _latest_generated_image" in plugin
|
||||
assert "newest MEDIA: path from the conversation" in plugin
|
||||
assert 'candidate.upper().startswith("MEDIA:")' in plugin
|
||||
assert "override=True" not in plugin
|
||||
|
||||
agent = _documents(HERMES / "agent-deployment.yaml")[0]
|
||||
containers = agent["spec"]["template"]["spec"]["containers"]
|
||||
broker = next(item for item in containers if item["name"] == "image-broker")
|
||||
assert broker["ports"] == [
|
||||
{"name": "image-broker", "containerPort": 9002, "protocol": "TCP"}
|
||||
]
|
||||
assert broker["securityContext"]["readOnlyRootFilesystem"] is True
|
||||
assert broker["securityContext"]["runAsNonRoot"] is True
|
||||
|
||||
services = _documents(HERMES / "service.yaml")
|
||||
service = next(
|
||||
item for item in services if item["metadata"]["name"] == "hermes-image-broker"
|
||||
)
|
||||
assert service["spec"]["selector"] == {"app": "hermes-agent"}
|
||||
|
||||
oauth_store = _documents(HERMES / "oauth-session-store.yaml")
|
||||
redis = next(item for item in oauth_store if item["kind"] == "Deployment")
|
||||
assert redis["spec"]["strategy"]["type"] == "Recreate"
|
||||
assert "--appendonly" in redis["spec"]["template"]["spec"]["containers"][0]["args"]
|
||||
|
||||
policies = _documents(HERMES / "networkpolicy.yaml")
|
||||
agent_policy = next(
|
||||
item
|
||||
for item in policies
|
||||
if item["metadata"]["name"] == "hermes-agent-isolation"
|
||||
)
|
||||
broker_ingress = next(
|
||||
rule
|
||||
for rule in agent_policy["spec"]["ingress"]
|
||||
if {port["port"] for port in rule["ports"]} == {9002, 9003}
|
||||
)
|
||||
assert broker_ingress["from"][0]["podSelector"]["matchLabels"] == {
|
||||
"app": "hermes-chat-tenant"
|
||||
}
|
||||
vault_policy = (VAULT / "scripts" / "vault_k8s_auth_configure.sh").read_text()
|
||||
agent_role = vault_policy[
|
||||
vault_policy.index('write_policy_and_role "hermes-agent"') : vault_policy.index(
|
||||
"write_policy_and_role",
|
||||
vault_policy.index('write_policy_and_role "hermes-agent"') + 1,
|
||||
)
|
||||
]
|
||||
assert "hermes/developer-gitea" not in agent_role
|
||||
assert 'write_policy_and_role "hermes-scm-broker" "hermes-scm"' in vault_policy
|
||||
assert (
|
||||
'write_policy_and_role "hermes-node-ssh" "hermes" '
|
||||
'"hermes-node-ssh-access"' in vault_policy
|
||||
)
|
||||
|
||||
|
||||
def test_compact_image_edit_resolves_latest_tenant_artifact(tmp_path, monkeypatch):
|
||||
"""Follow-up edits resolve the source server-side and keep tool JSON small."""
|
||||
provider_module = SimpleNamespace(
|
||||
DEFAULT_ASPECT_RATIO="square",
|
||||
ImageGenProvider=object,
|
||||
error_response=lambda **value: value,
|
||||
normalize_reference_images=lambda value: value,
|
||||
resolve_aspect_ratio=lambda value: value,
|
||||
save_b64_image=lambda *_args, **_kwargs: tmp_path / "saved.png",
|
||||
success_response=lambda **value: value,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "agent", SimpleNamespace())
|
||||
monkeypatch.setitem(sys.modules, "agent.image_gen_provider", provider_module)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"hermes_image_plugin",
|
||||
HERMES / "plugins" / "image-gen-broker" / "__init__.py",
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
image_dir = tmp_path / "cache" / "images"
|
||||
image_dir.mkdir(parents=True)
|
||||
older = image_dir / "atlas_flux-old.png"
|
||||
newest = image_dir / "atlas_gpt-image-new.png"
|
||||
older.write_bytes(b"older")
|
||||
newest.write_bytes(b"newest")
|
||||
older.touch()
|
||||
time.sleep(0.001)
|
||||
newest.touch()
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_handle_image_generate",
|
||||
lambda args, route: calls.append((args, route)) or "ok",
|
||||
)
|
||||
assert module._handle_hosted_edit({"prompt": "make it a clown"}) == "ok"
|
||||
assert calls == [
|
||||
(
|
||||
{
|
||||
"prompt": "make it a clown",
|
||||
"image_url": str(newest.resolve()),
|
||||
},
|
||||
"hosted",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_chat_reasoning_uses_switchyard_without_owner_credentials():
|
||||
"""Family pods use AUTO/manual routes without mounting owner credentials."""
|
||||
configmap = _documents(HERMES / "chat-configmap.yaml")[0]
|
||||
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
||||
assert config["model"] == {
|
||||
"provider": "atlas-switchyard",
|
||||
"default": "atlas/auto/fast",
|
||||
"model": "atlas/auto/fast",
|
||||
}
|
||||
assert config["providers"]["atlas-switchyard"] == {
|
||||
"name": "Automatic Router",
|
||||
"api": "http://hermes-switchyard.hermes.svc.cluster.local:9005/v1",
|
||||
"api_key": "atlas-switchyard",
|
||||
"default_model": "atlas/auto/fast",
|
||||
"transport": "chat_completions",
|
||||
}
|
||||
assert config["platforms"]["api_server"]["extra"]["model_routes"] == {
|
||||
route: {"provider": "atlas-switchyard", "model": route}
|
||||
for route in [
|
||||
"atlas/auto/fast",
|
||||
"atlas/auto/balanced",
|
||||
"atlas/auto/deep",
|
||||
"atlas/auto/maximum",
|
||||
"atlas/manual/codex/luna",
|
||||
"atlas/manual/codex/terra",
|
||||
"atlas/manual/codex/sol",
|
||||
"atlas/manual/claude/haiku",
|
||||
"atlas/manual/claude/fable",
|
||||
"atlas/manual/claude/sonnet",
|
||||
"atlas/manual/claude/opus",
|
||||
"atlas/manual/local/qwen-14b",
|
||||
]
|
||||
}
|
||||
|
||||
agent = _documents(HERMES / "agent-deployment.yaml")[0]
|
||||
containers = agent["spec"]["template"]["spec"]["containers"]
|
||||
broker = next(item for item in containers if item["name"] == "codex-broker")
|
||||
assert broker["ports"] == [
|
||||
{"name": "codex-broker", "containerPort": 9003, "protocol": "TCP"}
|
||||
]
|
||||
assert broker["securityContext"]["readOnlyRootFilesystem"] is True
|
||||
assert broker["securityContext"]["runAsNonRoot"] is True
|
||||
assert {item["name"]: item["value"] for item in broker["env"]}.items() >= {
|
||||
"PYTHONPATH": "/opt/hermes",
|
||||
"HERMES_CODEX_BROKER_LISTEN_PORT": "9003",
|
||||
"HERMES_ROUTING_CATALOG_PATH": "/routing-catalog/catalog.json",
|
||||
}.items()
|
||||
|
||||
services = _documents(HERMES / "service.yaml")
|
||||
service = next(
|
||||
item for item in services if item["metadata"]["name"] == "hermes-codex-broker"
|
||||
)
|
||||
assert service["spec"]["selector"] == {"app": "hermes-agent"}
|
||||
assert service["spec"]["ports"] == [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 9003,
|
||||
"targetPort": "codex-broker",
|
||||
"protocol": "TCP",
|
||||
}
|
||||
]
|
||||
|
||||
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
||||
assert (
|
||||
statefulset["spec"]["template"]["metadata"]["annotations"][
|
||||
"ai.bstein.dev/config-rev"
|
||||
]
|
||||
== "20260816-telegram-topics"
|
||||
)
|
||||
pod_spec = statefulset["spec"]["template"]["spec"]
|
||||
patch_init = next(
|
||||
item
|
||||
for item in pod_spec["initContainers"]
|
||||
if item["name"] == "patch-stream-recovery"
|
||||
)
|
||||
assert patch_init["command"][-2:] == [
|
||||
"/opt/hermes/agent/conversation_loop.py",
|
||||
"/patched/conversation_loop.py",
|
||||
]
|
||||
hermes = next(item for item in pod_spec["containers"] if item["name"] == "hermes")
|
||||
assert {
|
||||
"name": "stream-recovery-patch",
|
||||
"mountPath": "/opt/hermes/agent/conversation_loop.py",
|
||||
"subPath": "conversation_loop.py",
|
||||
} in hermes["volumeMounts"]
|
||||
api_session_init = next(
|
||||
item
|
||||
for item in pod_spec["initContainers"]
|
||||
if item["name"] == "patch-api-server-sessions"
|
||||
)
|
||||
assert "patch_api_server_sessions.py" in api_session_init["args"][0]
|
||||
assert "migrate_telegram_api_sessions.py" in api_session_init["args"][0]
|
||||
assert {
|
||||
"name": "api-server-patch",
|
||||
"mountPath": "/opt/hermes/gateway/platforms/api_server.py",
|
||||
"subPath": "api_server.py",
|
||||
} in hermes["volumeMounts"]
|
||||
assert any(volume["name"] == "api-server-patch" for volume in pod_spec["volumes"])
|
||||
assert not any(
|
||||
mount["mountPath"].endswith("/.codex") for mount in hermes["volumeMounts"]
|
||||
)
|
||||
|
||||
policies = _documents(HERMES / "networkpolicy.yaml")
|
||||
agent_policy = next(
|
||||
item
|
||||
for item in policies
|
||||
if item["metadata"]["name"] == "hermes-agent-isolation"
|
||||
)
|
||||
broker_ingress = next(
|
||||
rule
|
||||
for rule in agent_policy["spec"]["ingress"]
|
||||
if {port["port"] for port in rule["ports"]} == {9002, 9003}
|
||||
)
|
||||
assert broker_ingress["from"][0]["podSelector"]["matchLabels"] == {
|
||||
"app": "hermes-chat-tenant"
|
||||
}
|
||||
@ -1,172 +0,0 @@
|
||||
"""Local inference and sandbox execution contracts for Hermes chat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
from testing.tests.test_hermes_chat_support import (
|
||||
HERMES,
|
||||
ROOT,
|
||||
_documents,
|
||||
)
|
||||
|
||||
|
||||
def test_titan20_serializes_classifier_and_local_chat_model_residency():
|
||||
"""Classifier and local chat share one serialized resident Qwen weight."""
|
||||
deployment = _documents(
|
||||
Path(__file__).parents[2] / "services/ai-llm/deployment.yaml"
|
||||
)[0]
|
||||
pod = deployment["spec"]["template"]["spec"]
|
||||
required = pod["affinity"]["nodeAffinity"][
|
||||
"requiredDuringSchedulingIgnoredDuringExecution"
|
||||
]["nodeSelectorTerms"][0]["matchExpressions"][0]
|
||||
assert required["values"] == ["titan-20"]
|
||||
container = pod["containers"][0]
|
||||
env = {item["name"]: item["value"] for item in container["env"]}
|
||||
assert env["OLLAMA_MAX_LOADED_MODELS"] == "1"
|
||||
assert env["OLLAMA_NUM_PARALLEL"] == "1"
|
||||
assert env["OLLAMA_KEEP_ALIVE"] == "-1"
|
||||
assert env["OLLAMA_CONTEXT_LENGTH"] == "8192"
|
||||
warm_command = " ".join(container["command"])
|
||||
assert "--keepalive=-1" not in warm_command
|
||||
models = next(item for item in pod["volumes"] if item["name"] == "models")
|
||||
assert models["persistentVolumeClaim"]["claimName"] == ("ollama-models-titan20")
|
||||
|
||||
|
||||
def test_local_image_gpu_guard_distinguishes_background_and_saturated_gpu(monkeypatch):
|
||||
"""Lease-idle desktop spikes may coexist, but saturation still blocks FLUX."""
|
||||
source = ROOT / "dockerfiles" / "hermes-local-image-server.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_local_image_server", source)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
monkeypatch.setattr(module, "GPU_ACTIVITY_NODE", "titan-24")
|
||||
monkeypatch.setattr(module, "GPU_ACTIVE_SM_PERCENT", 80.0)
|
||||
monkeypatch.setattr(module, "GPU_MAX_EXTERNAL_MEMORY_BYTES", 3 << 30)
|
||||
|
||||
idle = module._parse_gpu_activity(
|
||||
"\n".join(
|
||||
[
|
||||
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="Xorg"} 0',
|
||||
'nvidia_process_gpu_memory_used_bytes{node="titan-24",namespace="host",process="Xorg"} 1900000000',
|
||||
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="game-stream",process="wolf"} 3',
|
||||
'nvidia_process_gpu_memory_used_bytes{node="titan-24",namespace="hermes",process="python"} 9000000000',
|
||||
]
|
||||
)
|
||||
)
|
||||
assert idle["interactive_active"] is False
|
||||
assert idle["external_gpu_memory_bytes"] == 1900000000
|
||||
assert idle["external_gpu_sm_percent"] == 3
|
||||
|
||||
background_spike = module._parse_gpu_activity(
|
||||
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="sway"} 41\n'
|
||||
)
|
||||
assert background_spike["interactive_active"] is False
|
||||
|
||||
active = module._parse_gpu_activity(
|
||||
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="steam"} 91\n'
|
||||
)
|
||||
assert active["interactive_active"] is True
|
||||
assert "91%" in active["gpu_guard_reason"]
|
||||
|
||||
|
||||
def test_local_flux_renderer_uses_a_disposable_cuda_worker(monkeypatch):
|
||||
"""A completed render must not leave its CUDA context in the API process."""
|
||||
source = ROOT / "dockerfiles" / "hermes-local-image-server.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_local_image_worker", source)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
calls = []
|
||||
|
||||
def run(command, **kwargs):
|
||||
calls.append((command, kwargs))
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout=b'{"success":true,"route":"local","image_b64":"cG5n"}',
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(module.subprocess, "run", run)
|
||||
result = module._render({"prompt": "black cat", "aspect_ratio": "square"})
|
||||
|
||||
assert result["route"] == "local"
|
||||
command, options = calls[0]
|
||||
assert command[-1] == "--render-worker"
|
||||
assert json.loads(options["input"]) == {
|
||||
"prompt": "black cat",
|
||||
"aspect_ratio": "square",
|
||||
}
|
||||
assert options["timeout"] == module.RENDER_TIMEOUT_SECONDS
|
||||
assert options["check"] is False
|
||||
|
||||
|
||||
def test_local_flux_uses_low_vram_offload_without_reducing_resolution():
|
||||
"""The shared 3080 lane must trade time, not image size, for headroom."""
|
||||
source = (ROOT / "dockerfiles" / "hermes-local-image-server.py").read_text()
|
||||
assert "OFFLOAD_MODE = os.environ.get(" in source
|
||||
assert '"HERMES_LOCAL_IMAGE_OFFLOAD_MODE", "sequential"' in source
|
||||
assert "pipe.enable_sequential_cpu_offload()" in source
|
||||
assert '"square": (1024, 1024)' in source
|
||||
|
||||
|
||||
def test_chat_auth_and_relay_are_pod_lifetime_only():
|
||||
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
||||
pod = statefulset["spec"]["template"]["spec"]
|
||||
containers = statefulset["spec"]["template"]["spec"]["containers"]
|
||||
|
||||
for name in ("hermes", "webui"):
|
||||
container = next(item for item in containers if item["name"] == name)
|
||||
env = {item["name"]: item["value"] for item in container["env"]}
|
||||
assert env["HERMES_AUTH_FILE"] == "/runtime-access/hermes-auth.json"
|
||||
mount = next(
|
||||
item
|
||||
for item in container["volumeMounts"]
|
||||
if item["name"] == "runtime-access"
|
||||
)
|
||||
assert mount["mountPath"] == "/runtime-access"
|
||||
assert "subPath" not in mount
|
||||
|
||||
hermes_env = {
|
||||
item["name"]: item["value"]
|
||||
for item in next(item for item in containers if item["name"] == "hermes")["env"]
|
||||
}
|
||||
runtime = next(item for item in pod["volumes"] if item["name"] == "runtime-access")
|
||||
assert runtime["emptyDir"] == {"medium": "Memory", "sizeLimit": "2Mi"}
|
||||
assert not any(item["name"] == "provider-auth" for item in pod["volumes"])
|
||||
init_command = next(
|
||||
item for item in pod["initContainers"] if item["name"] == "init-config"
|
||||
)["command"][2]
|
||||
for key in (
|
||||
"ANTHROPIC_API_KEY",
|
||||
"API_SERVER_KEY",
|
||||
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||
"GITEA_TOKEN",
|
||||
"HERMES_IMAGE_BROKER_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
):
|
||||
assert key in init_command
|
||||
assert "printf 'API_SERVER_KEY=%s" not in init_command
|
||||
assert hermes_env["AGENT_BROWSER_EXECUTABLE_PATH"].endswith(
|
||||
"/chrome-linux/headless_shell"
|
||||
)
|
||||
assert "--no-sandbox" in hermes_env["AGENT_BROWSER_ARGS"]
|
||||
|
||||
|
||||
def test_sandbox_executes_python_with_bounded_output(tmp_path: Path, monkeypatch):
|
||||
source = ROOT / "dockerfiles" / "hermes-chat-sandbox-server.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_chat_sandbox_server", source)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
monkeypatch.setattr(module, "WORKSPACE", tmp_path)
|
||||
|
||||
result = module._execute("import math\nprint(math.comb(10, 3))")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["stdout"] == "120\n"
|
||||
assert result["stderr"] == ""
|
||||
@ -1,476 +0,0 @@
|
||||
"""Provider subscription and OAuth contracts for Hermes chat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_chat_support import (
|
||||
_load_broker_module,
|
||||
)
|
||||
|
||||
|
||||
def test_codex_broker_auth_and_request_contract(tmp_path: Path, monkeypatch):
|
||||
"""The relay is bounded, stateless, and rejects unapproved models."""
|
||||
module = _load_broker_module("hermes_codex_broker", "codex_broker.py", monkeypatch)
|
||||
monkeypatch.setattr(module, "TOKEN", "relay-secret")
|
||||
|
||||
assert module._authorized("Bearer relay-secret") is True
|
||||
assert module._authorized("Bearer wrong") is False
|
||||
assert module._real_model("route/codex/gpt-5.6-sol/xhigh") == "gpt-5.6-sol"
|
||||
payload = module._validate_payload(
|
||||
{
|
||||
"model": "gpt-5.6-terra",
|
||||
"input": "route this chat turn",
|
||||
"store": True,
|
||||
"stream": False,
|
||||
"max_output_tokens": 96,
|
||||
"max_completion_tokens": 96,
|
||||
"max_tokens": 96,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.9,
|
||||
}
|
||||
)
|
||||
assert payload["store"] is False
|
||||
assert payload["stream"] is True
|
||||
assert "max_output_tokens" not in payload
|
||||
assert "max_completion_tokens" not in payload
|
||||
assert "max_tokens" not in payload
|
||||
assert "temperature" not in payload
|
||||
assert "top_p" not in payload
|
||||
assert payload["input"] == [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "route this chat turn"}],
|
||||
}
|
||||
]
|
||||
response_item = {
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "keep this item"}],
|
||||
}
|
||||
assert module._validate_payload({"model": "gpt-5.6-terra", "input": response_item})[
|
||||
"input"
|
||||
] == [response_item]
|
||||
response_items = [response_item]
|
||||
assert (
|
||||
module._validate_payload({"model": "gpt-5.6-terra", "input": response_items})[
|
||||
"input"
|
||||
]
|
||||
is response_items
|
||||
)
|
||||
image_items = [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What color is this?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
"detail": "high",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
assert module._validate_payload({"model": "gpt-5.6-terra", "input": image_items})[
|
||||
"input"
|
||||
][0]["content"][1] == {
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
"detail": "high",
|
||||
}
|
||||
switchyard_image_items = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What color is this?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
"detail": "auto",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
assert module._validate_payload(
|
||||
{"model": "gpt-5.6-terra", "input": switchyard_image_items}
|
||||
)["input"][0]["content"][1] == {
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
"detail": "auto",
|
||||
}
|
||||
switchyard_base64_items = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What color is this?"},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": "cHJpdmF0ZQ==",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
normalized_base64 = module._validate_payload(
|
||||
{"model": "gpt-5.6-terra", "input": switchyard_base64_items}
|
||||
)["input"][0]["content"][1]
|
||||
assert normalized_base64 == {
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
}
|
||||
switchyard_enum_items = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": {
|
||||
"type": "url",
|
||||
"data": {
|
||||
"url": "data:image/png;base64,cHJpdmF0ZQ==",
|
||||
"detail": "high",
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
nested_image = module._validate_payload(
|
||||
{"model": "gpt-5.6-terra", "input": switchyard_enum_items}
|
||||
)["input"][0]["content"][0]
|
||||
assert nested_image["image_url"] == "data:image/png;base64,cHJpdmF0ZQ=="
|
||||
assert nested_image["detail"] == "high"
|
||||
with pytest.raises(ValueError, match=r"non-empty Responses image URL.*str\[4\]"):
|
||||
module._validate_payload(
|
||||
{
|
||||
"model": "gpt-5.6-terra",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": {"detail": "high"},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
routed = module._validate_payload(
|
||||
{
|
||||
"model": "route/codex/gpt-5.6-luna/low",
|
||||
"input": "use the low route",
|
||||
"stream": False,
|
||||
}
|
||||
)
|
||||
assert routed["model"] == "gpt-5.6-luna"
|
||||
with pytest.raises(ValueError, match="unsupported Codex model"):
|
||||
module._validate_payload({"model": "unapproved-model", "input": "hello"})
|
||||
with pytest.raises(ValueError, match="non-empty Responses input"):
|
||||
module._validate_payload({"model": "gpt-5.6-terra", "input": ""})
|
||||
with pytest.raises(ValueError, match="non-empty Responses input list"):
|
||||
module._validate_payload({"model": "gpt-5.6-terra", "input": []})
|
||||
|
||||
completed = {
|
||||
"id": "resp_test",
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"output": [],
|
||||
}
|
||||
completed_item = {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"content": [{"type": "output_text", "text": "done"}],
|
||||
}
|
||||
assert module._completed_response(
|
||||
[
|
||||
"event: response.created",
|
||||
'data: {"type":"response.created","response":{}}',
|
||||
"event: response.output_item.done",
|
||||
"data: "
|
||||
+ json.dumps(
|
||||
{
|
||||
"type": "response.output_item.done",
|
||||
"output_index": 0,
|
||||
"item": completed_item,
|
||||
}
|
||||
),
|
||||
"event: response.completed",
|
||||
"data: "
|
||||
+ json.dumps({"type": "response.completed", "response": completed}),
|
||||
"data: [DONE]",
|
||||
]
|
||||
)["output"] == [completed_item]
|
||||
raw_stream = (
|
||||
"event: response.output_item.done\n"
|
||||
"data: "
|
||||
+ json.dumps(
|
||||
{
|
||||
"type": "response.output_item.done",
|
||||
"output_index": 0,
|
||||
"item": completed_item,
|
||||
}
|
||||
)
|
||||
+ "\n\nevent: response.completed\ndata: "
|
||||
+ json.dumps({"type": "response.completed", "response": completed})
|
||||
+ "\n\n"
|
||||
).encode()
|
||||
normalized = module._normalized_stream(
|
||||
raw_stream, {**completed, "output": [completed_item]}
|
||||
).decode()
|
||||
terminal_data = next(
|
||||
line for line in normalized.splitlines() if '"response.completed"' in line
|
||||
)
|
||||
assert json.loads(terminal_data.removeprefix("data: "))["response"]["output"] == [
|
||||
completed_item
|
||||
]
|
||||
assert normalized.endswith("\n\n")
|
||||
streamed_function_item = {
|
||||
"type": "function_call",
|
||||
"name": "read_file",
|
||||
"status": "completed",
|
||||
"arguments": '{"path":"/tmp"}',
|
||||
}
|
||||
streamed_function_body = (
|
||||
"event: response.function_call_arguments.delta\n"
|
||||
'data: {"type":"response.function_call_arguments.delta",'
|
||||
'"item_id":"call_1","delta":"{\\"path\\":\\"/tmp\\"}"}\n\n'
|
||||
"event: response.function_call_arguments.done\n"
|
||||
'data: {"type":"response.function_call_arguments.done",'
|
||||
'"item_id":"call_1","arguments":"{\\"path\\":\\"/tmp\\"}"}\n\n'
|
||||
"event: response.output_item.done\n"
|
||||
'data: {"type":"response.output_item.done","output_index":0,'
|
||||
'"item":{"type":"function_call","name":"read_file",'
|
||||
'"arguments":"{\\"path\\":\\"/tmp\\"}"}}\n\n'
|
||||
"event: response.completed\n"
|
||||
"data: "
|
||||
+ json.dumps(
|
||||
{
|
||||
"type": "response.completed",
|
||||
"response": {**completed, "output": [streamed_function_item]},
|
||||
}
|
||||
)
|
||||
+ "\n\n"
|
||||
).encode()
|
||||
normalized_function_stream = module._normalized_stream(
|
||||
streamed_function_body, {**completed, "output": [streamed_function_item]}
|
||||
).decode()
|
||||
assert "response.function_call_arguments.delta" in normalized_function_stream
|
||||
assert "response.function_call_arguments.done" not in normalized_function_stream
|
||||
assert "response.output_item.done" not in normalized_function_stream
|
||||
normalized_terminal = next(
|
||||
line
|
||||
for line in normalized_function_stream.splitlines()
|
||||
if '"response.completed"' in line
|
||||
)
|
||||
assert (
|
||||
json.loads(normalized_terminal.removeprefix("data: "))["response"]["output"]
|
||||
== []
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="retryable incomplete response"):
|
||||
module._completed_response(
|
||||
[
|
||||
"event: response.incomplete",
|
||||
'data: {"type":"response.incomplete","response":'
|
||||
'{"status":"incomplete","incomplete_details":'
|
||||
'{"reason":"max_output_tokens"}}}',
|
||||
]
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="provider unavailable"):
|
||||
module._completed_response(
|
||||
[
|
||||
"event: error",
|
||||
'data: {"type":"error","error":{"message":"provider unavailable"}}',
|
||||
]
|
||||
)
|
||||
malformed_tool_item = {
|
||||
"type": "function_call",
|
||||
"name": "search_files",
|
||||
"status": "completed",
|
||||
"arguments": '{"path":"","offset":',
|
||||
}
|
||||
with pytest.raises(RuntimeError, match="malformed function arguments"):
|
||||
module._completed_response(
|
||||
[
|
||||
"event: response.output_item.done",
|
||||
"data: "
|
||||
+ json.dumps(
|
||||
{
|
||||
"type": "response.output_item.done",
|
||||
"output_index": 0,
|
||||
"item": malformed_tool_item,
|
||||
}
|
||||
),
|
||||
"event: response.completed",
|
||||
"data: "
|
||||
+ json.dumps({"type": "response.completed", "response": completed}),
|
||||
]
|
||||
)
|
||||
valid_tool_item = {
|
||||
**malformed_tool_item,
|
||||
"arguments": '{"path":"","offset":0}',
|
||||
}
|
||||
assert module._completed_response(
|
||||
[
|
||||
"event: response.output_item.done",
|
||||
"data: "
|
||||
+ json.dumps(
|
||||
{
|
||||
"type": "response.output_item.done",
|
||||
"output_index": 0,
|
||||
"item": valid_tool_item,
|
||||
}
|
||||
),
|
||||
"event: response.completed",
|
||||
"data: "
|
||||
+ json.dumps({"type": "response.completed", "response": completed}),
|
||||
]
|
||||
)["output"] == [valid_tool_item]
|
||||
with pytest.raises(RuntimeError, match="malformed function arguments"):
|
||||
module._completed_response(
|
||||
[
|
||||
"event: response.function_call_arguments.delta",
|
||||
'data: {"type":"response.function_call_arguments.delta",'
|
||||
'"item_id":"call_1","output_index":0,'
|
||||
'"delta":"{\\"path\\":\\"/tmp\\",\\"offset\\":"}',
|
||||
"event: response.completed",
|
||||
"data: "
|
||||
+ json.dumps({"type": "response.completed", "response": completed}),
|
||||
]
|
||||
)
|
||||
streamed_tool = module._completed_response(
|
||||
[
|
||||
"event: response.function_call_arguments.delta",
|
||||
'data: {"type":"response.function_call_arguments.delta",'
|
||||
'"item_id":"call_2","output_index":0,'
|
||||
'"delta":"{\\"path\\":\\"/tmp\\",\\"offset\\":"}',
|
||||
"event: response.function_call_arguments.done",
|
||||
'data: {"type":"response.function_call_arguments.done",'
|
||||
'"item_id":"call_2","output_index":0,'
|
||||
'"arguments":"{\\"path\\":\\"/tmp\\",\\"offset\\":0}"}',
|
||||
"event: response.completed",
|
||||
"data: "
|
||||
+ json.dumps({"type": "response.completed", "response": completed}),
|
||||
]
|
||||
)
|
||||
assert streamed_tool["status"] == "completed"
|
||||
|
||||
auth_dir = tmp_path / ".codex"
|
||||
auth_dir.mkdir()
|
||||
# The token payload need only prove the broker reads CODEX_HOME directly.
|
||||
encoded = (
|
||||
base64.urlsafe_b64encode(json.dumps({"exp": time.time() + 3600}).encode())
|
||||
.decode()
|
||||
.rstrip("=")
|
||||
)
|
||||
(auth_dir / "auth.json").write_text(
|
||||
json.dumps({"tokens": {"access_token": f"header.{encoded}.signature"}})
|
||||
)
|
||||
monkeypatch.setenv("CODEX_HOME", str(auth_dir))
|
||||
assert module._access_token().startswith("header.")
|
||||
|
||||
|
||||
def test_codex_broker_refreshes_and_persists_first_party_oauth(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""Expired ChatGPT OAuth refreshes in the canonical Codex CLI store."""
|
||||
module = _load_broker_module(
|
||||
"hermes_codex_refresh_broker", "codex_broker.py", monkeypatch
|
||||
)
|
||||
auth_dir = tmp_path / ".codex"
|
||||
auth_dir.mkdir()
|
||||
|
||||
def jwt(expires_at: float) -> str:
|
||||
payload = (
|
||||
base64.urlsafe_b64encode(json.dumps({"exp": expires_at}).encode())
|
||||
.decode()
|
||||
.rstrip("=")
|
||||
)
|
||||
return f"header.{payload}.signature"
|
||||
|
||||
expired = jwt(time.time() - 60)
|
||||
live = jwt(time.time() + 3600)
|
||||
auth_path = auth_dir / "auth.json"
|
||||
auth_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"auth_mode": "chatgpt",
|
||||
"tokens": {
|
||||
"access_token": expired,
|
||||
"refresh_token": "refresh-old",
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
calls = []
|
||||
auth_module = ModuleType("hermes_cli.auth")
|
||||
|
||||
def refresh(access_token, refresh_token, *, timeout_seconds):
|
||||
calls.append((access_token, refresh_token, timeout_seconds))
|
||||
return {
|
||||
"access_token": live,
|
||||
"refresh_token": "refresh-new",
|
||||
"last_refresh": "2026-08-12T20:00:00Z",
|
||||
}
|
||||
|
||||
auth_module.refresh_codex_oauth_pure = refresh
|
||||
package = ModuleType("hermes_cli")
|
||||
package.auth = auth_module
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", package)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli.auth", auth_module)
|
||||
monkeypatch.setenv("CODEX_HOME", str(auth_dir))
|
||||
|
||||
assert module._access_token() == live
|
||||
persisted = json.loads(auth_path.read_text())
|
||||
assert persisted["tokens"]["access_token"] == live
|
||||
assert persisted["tokens"]["refresh_token"] == "refresh-new"
|
||||
assert persisted["last_refresh"] == "2026-08-12T20:00:00Z"
|
||||
assert calls == [(expired, "refresh-old", 30.0)]
|
||||
assert auth_path.stat().st_mode & 0o777 == 0o600
|
||||
|
||||
# A healthy token is reused, so repeated routed turns do not spend a
|
||||
# refresh token or create a second billing/authentication path.
|
||||
assert module._access_token() == live
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_claude_broker_uses_native_subscription_without_api_billing(monkeypatch):
|
||||
"""Claude traffic must use the native first-party CLI subscription lane."""
|
||||
module = _load_broker_module(
|
||||
"hermes_claude_broker", "claude_oauth_broker.py", monkeypatch
|
||||
)
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "must-not-leak")
|
||||
monkeypatch.setenv("CLAUDE_API_KEY", "must-not-leak")
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"resolve_route",
|
||||
lambda route: "claude-fable-5" if "/fable/" in route else route,
|
||||
)
|
||||
|
||||
model, effort = module._route(
|
||||
"route/claude/fable/xhigh", {"output_config": {"effort": "xhigh"}}
|
||||
)
|
||||
|
||||
assert (model, effort) == ("claude-fable-5", "xhigh")
|
||||
assert "ANTHROPIC_API_KEY" not in module._claude_environment()
|
||||
assert "CLAUDE_API_KEY" not in module._claude_environment()
|
||||
assert module.CAPACITY_PATTERN.search("weekly usage limit exhausted")
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,486 +0,0 @@
|
||||
"""Routing and runtime workload contracts for Hermes chat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
from testing.tests.test_hermes_chat_support import (
|
||||
HERMES,
|
||||
ROOT,
|
||||
_documents,
|
||||
)
|
||||
|
||||
|
||||
def test_stale_parent_linked_api_workers_close_on_startup(tmp_path: Path):
|
||||
"""A previous gateway lifetime cannot leave phantom active workers."""
|
||||
module_path = HERMES / "scripts" / "migrate_api_session_lineage.py"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"close_stale_api_workers", module_path
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
database = tmp_path / "state.db"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.execute(
|
||||
"CREATE TABLE sessions (id TEXT PRIMARY KEY, source TEXT, "
|
||||
"parent_session_id TEXT, title TEXT, started_at REAL, ended_at REAL, "
|
||||
"end_reason TEXT, archived INTEGER DEFAULT 0)"
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO sessions "
|
||||
"(id, source, parent_session_id, started_at, ended_at, end_reason) "
|
||||
"VALUES (?, ?, ?, 1, ?, ?)",
|
||||
(
|
||||
("stale", "api_server", "parent", None, None),
|
||||
("root", "api_server", None, None, None),
|
||||
("finished", "api_server", "parent", 2.0, "api_run_completed"),
|
||||
("interactive", "tui", "parent", None, None),
|
||||
),
|
||||
)
|
||||
|
||||
assert module.migrate(database) == 1
|
||||
assert module.migrate(database) == 0
|
||||
with sqlite3.connect(database) as connection:
|
||||
rows = connection.execute(
|
||||
"SELECT id, ended_at, end_reason FROM sessions ORDER BY id"
|
||||
).fetchall()
|
||||
by_id = {row[0]: row[1:] for row in rows}
|
||||
assert by_id["stale"][0] is not None
|
||||
assert by_id["stale"][1] == "api_run_recovered_stale"
|
||||
assert by_id["root"] == (None, None)
|
||||
assert by_id["finished"] == (2.0, "api_run_completed")
|
||||
assert by_id["interactive"] == (None, None)
|
||||
|
||||
|
||||
def test_switchyard_brokers_and_native_claude_lane_use_the_right_images():
|
||||
"""Thin brokers stay small while native Claude runs beside owner auth."""
|
||||
dockerfile = (
|
||||
ROOT / "dockerfiles" / "Dockerfile.hermes-switchyard-brokers"
|
||||
).read_text()
|
||||
assert "httpx==0.28.1" in dockerfile
|
||||
assert "worker_route_broker.py" in dockerfile
|
||||
assert "routing_catalog.py" in dockerfile
|
||||
|
||||
deployment = _documents(HERMES / "switchyard-deployment.yaml")[0]
|
||||
containers = {
|
||||
container["name"]: container
|
||||
for container in deployment["spec"]["template"]["spec"]["containers"]
|
||||
}
|
||||
expected = (
|
||||
"registry.bstein.dev/bstein/hermes-switchyard-brokers@"
|
||||
"sha256:ee7e95e060ef8083da505162d7e9030daba15fdd828cc047bbcbe6aa409d2083"
|
||||
)
|
||||
assert containers["worker-route-broker"]["image"] == expected
|
||||
assert containers["classifier-broker"]["image"] == expected
|
||||
assert "claude-oauth-broker" not in containers
|
||||
|
||||
agent = _documents(HERMES / "agent-deployment.yaml")[0]
|
||||
agent_containers = {
|
||||
container["name"]: container
|
||||
for container in agent["spec"]["template"]["spec"]["containers"]
|
||||
}
|
||||
for container_name in ("hermes", "terminal"):
|
||||
container = agent_containers[container_name]
|
||||
environment = {item["name"]: item["value"] for item in container["env"]}
|
||||
mounts = {item["name"]: item for item in container["volumeMounts"]}
|
||||
assert (
|
||||
environment["HERMES_ROUTING_CATALOG_PATH"]
|
||||
== "/routing-catalog/catalog.json"
|
||||
)
|
||||
assert (
|
||||
environment["HERMES_CODEX_HEALTH_PATH"]
|
||||
== "/opt/data/provider-health/codex.json"
|
||||
)
|
||||
assert (
|
||||
environment["HERMES_CLAUDE_HEALTH_PATH"]
|
||||
== "/opt/data/provider-health/claude.json"
|
||||
)
|
||||
assert mounts["routing-catalog"]["mountPath"] == "/routing-catalog"
|
||||
assert mounts["routing-catalog"]["readOnly"] is True
|
||||
codex = agent_containers["codex-broker"]
|
||||
codex_environment = {item["name"]: item["value"] for item in codex["env"]}
|
||||
assert (
|
||||
codex_environment["HERMES_CODEX_HEALTH_PATH"]
|
||||
== "/opt/data/provider-health/codex.json"
|
||||
)
|
||||
claude = agent_containers["claude-broker"]
|
||||
assert claude["image"].startswith("registry.bstein.dev/bstein/hermes-agent@")
|
||||
assert "unset ANTHROPIC_API_KEY CLAUDE_API_KEY" in claude["args"][0]
|
||||
assert any(
|
||||
mount["name"] == "home" and mount["mountPath"] == "/opt/data"
|
||||
for mount in claude["volumeMounts"]
|
||||
)
|
||||
|
||||
|
||||
def test_classifier_broker_bounds_history_without_losing_routing_intent(monkeypatch):
|
||||
"""AUTO classification must fit the Jetson context without losing intent."""
|
||||
broker_path = HERMES / "scripts" / "classifier_broker.py"
|
||||
monkeypatch.setitem(sys.modules, "httpx", SimpleNamespace())
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"hermes_classifier_broker", broker_path
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
payload = {
|
||||
"model": "qwen2.5:14b-instruct-q4_0",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "routing contract\n" + ("candidate policy " * 1000),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Build and verify the Cassandra release safely.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_large",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "large_tool",
|
||||
"arguments": '{"command":"' + ("x" * 5000) + '"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "unbounded test output " * 10000,
|
||||
"tool_call_id": "call_large",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/png;base64,private"},
|
||||
},
|
||||
{"type": "text", "text": "Turn this cat into a cute clown."},
|
||||
],
|
||||
},
|
||||
],
|
||||
"tools": [{"type": "function", "function": {"name": "large_tool"}}],
|
||||
"tool_choice": "auto",
|
||||
"parallel_tool_calls": True,
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
|
||||
compacted = module.compact_payload(payload)
|
||||
encoded = json.dumps(compacted)
|
||||
|
||||
assert compacted["model"] == payload["model"]
|
||||
assert compacted["response_format"] == payload["response_format"]
|
||||
assert "tools" not in compacted
|
||||
assert "tool_choice" not in compacted
|
||||
assert "parallel_tool_calls" not in compacted
|
||||
assert all(message.get("role") != "tool" for message in compacted["messages"])
|
||||
assert all("tool_call_id" not in message for message in compacted["messages"])
|
||||
assert all("tool_calls" not in message for message in compacted["messages"])
|
||||
assert "[tool evidence]" in encoded
|
||||
assert "[assistant requested an external tool]" in encoded
|
||||
assert "Build and verify the Cassandra release safely." in encoded
|
||||
assert "Turn this cat into a cute clown." in encoded
|
||||
assert "image attachment available to the selected worker" in encoded
|
||||
assert "data:image/png;base64" not in encoded
|
||||
assert "unbounded test output " * 100 not in encoded
|
||||
assert len(encoded) < 18_000
|
||||
|
||||
|
||||
def test_switchyard_classifier_is_bounded_and_fails_open_once():
|
||||
"""A sick local judge must not hold chat through repeated long retries."""
|
||||
config = tomllib.loads(
|
||||
_documents(HERMES / "switchyard-configmap.yaml")[0]["data"]["routes.toml"]
|
||||
)
|
||||
classifier = config["llm_clients"]["classifier"]
|
||||
assert classifier["base_url"] == "http://127.0.0.1:9008/v1"
|
||||
assert classifier["max_retries"] == 0
|
||||
for route in ("auto_fast", "auto_balanced"):
|
||||
assert config["routes"][route]["recent_turn_window"] == 4
|
||||
for route in ("auto_deep", "auto_maximum", "worker_auto_maximum"):
|
||||
assert config["routes"][route]["recent_turn_window"] == 6
|
||||
|
||||
deployment = _documents(HERMES / "switchyard-deployment.yaml")[0]
|
||||
containers = {
|
||||
item["name"]: item
|
||||
for item in deployment["spec"]["template"]["spec"]["containers"]
|
||||
}
|
||||
classifier_container = containers["classifier-broker"]
|
||||
env = {item["name"]: item["value"] for item in classifier_container["env"]}
|
||||
assert env["HERMES_CLASSIFIER_BROKER_READ_TIMEOUT"] == "60"
|
||||
assert classifier_container["readinessProbe"]["httpGet"]["port"] == "classifier"
|
||||
|
||||
|
||||
def test_image_broker_returns_bytes_and_removes_owner_cache(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""The broker must not retain a family user's generated image."""
|
||||
broker_path = HERMES / "scripts" / "image_broker.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_image_broker", broker_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
generated = tmp_path / "generated.png"
|
||||
generated.write_bytes(b"\x89PNG\r\n\x1a\nprivate-image")
|
||||
|
||||
class Provider:
|
||||
def generate(self, prompt, aspect, **kwargs):
|
||||
assert prompt == "paint a blue sphere"
|
||||
assert aspect == "square"
|
||||
return {
|
||||
"success": True,
|
||||
"image": str(generated),
|
||||
"model": "gpt-image-2-high",
|
||||
"quality": "high",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(module, "_PROVIDER", Provider())
|
||||
result = module._generate(
|
||||
{
|
||||
"prompt": "paint a blue sphere",
|
||||
"aspect_ratio": "square",
|
||||
"model": "gpt-image-2-high",
|
||||
}
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["image_b64"]
|
||||
assert "image" not in result
|
||||
assert not generated.exists()
|
||||
|
||||
|
||||
def test_image_broker_auto_falls_back_to_local_and_honors_explicit_routes(
|
||||
monkeypatch,
|
||||
):
|
||||
"""AUTO is hosted-first while explicit local never calls the hosted lane."""
|
||||
broker_path = HERMES / "scripts" / "image_broker.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_image_router", broker_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
calls = []
|
||||
|
||||
def hosted(payload, model, prompt, aspect):
|
||||
calls.append(("hosted", model, prompt, aspect))
|
||||
return {"success": False, "error": "hosted refusal"}
|
||||
|
||||
def local(payload, timeout=1800.0):
|
||||
calls.append(("local", payload["model"], timeout))
|
||||
return {
|
||||
"success": True,
|
||||
"image_b64": "aW1hZ2U=",
|
||||
"model": "flux-2-klein-4b-local",
|
||||
"route": "local",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(module, "_generate_hosted", hosted)
|
||||
monkeypatch.setattr(module, "_local_request", local)
|
||||
auto = module._generate(
|
||||
{
|
||||
"prompt": "colorize this family photograph",
|
||||
"aspect_ratio": "portrait",
|
||||
"model": "atlas-image-auto-high",
|
||||
}
|
||||
)
|
||||
assert auto["success"] is True
|
||||
assert auto["route"] == "local"
|
||||
assert auto["hosted_fallback_reason"] == "hosted refusal"
|
||||
assert [call[0] for call in calls] == ["hosted", "local"]
|
||||
|
||||
calls.clear()
|
||||
explicit = module._generate(
|
||||
{
|
||||
"prompt": "make a local landscape",
|
||||
"aspect_ratio": "landscape",
|
||||
"model": "flux-2-klein-4b-local",
|
||||
}
|
||||
)
|
||||
assert explicit["route"] == "local"
|
||||
assert [call[0] for call in calls] == ["local"]
|
||||
|
||||
|
||||
def test_image_broker_policy_is_narrow_and_operator_extensible(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""Family-photo restoration stays allowed while the hard boundary remains."""
|
||||
broker_path = HERMES / "scripts" / "image_broker.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_image_policy", broker_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
policy = tmp_path / "policy.json"
|
||||
policy.write_text('{"additional_blocked_phrases":["site-specific block"]}')
|
||||
monkeypatch.setattr(module, "POLICY_PATH", policy)
|
||||
|
||||
assert (
|
||||
module._policy_error(
|
||||
"Colorize my baby photograph with a lighter natural skin tone"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert "minors" in module._policy_error("Create a sexual image of a child")
|
||||
assert module._policy_error("A site-specific block request") == (
|
||||
"request is blocked by the operator image policy"
|
||||
)
|
||||
|
||||
|
||||
def test_local_flux_runtime_and_gpu_handoff_are_flux_managed():
|
||||
"""FLUX and Wolf share titan-24 while text stays on titan-20."""
|
||||
deployment = _documents(HERMES / "local-image-deployment.yaml")[0]
|
||||
assert deployment["metadata"]["name"] == "hermes-local-image"
|
||||
pod = deployment["spec"]["template"]["spec"]
|
||||
assert pod["serviceAccountName"] == "hermes-gpu-runtime"
|
||||
local = next(item for item in pod["containers"] if item["name"] == "local-image")
|
||||
assert len(pod["containers"]) == 1
|
||||
assert local["resources"]["requests"]["nvidia.com/gpu.shared"] == 1
|
||||
assert local["ports"] == [{"name": "local-image", "containerPort": 9004}]
|
||||
assert any(mount["mountPath"] == "/models" for mount in local["volumeMounts"])
|
||||
model_env = {item["name"]: item["value"] for item in local["env"]}
|
||||
assert model_env["HERMES_LOCAL_IMAGE_LISTEN_PORT"] == "9004"
|
||||
assert model_env["HERMES_LOCAL_IMAGE_REVISION"] == (
|
||||
"e7b7dc27f91deacad38e78976d1f2b499d76a294"
|
||||
)
|
||||
assert model_env["HERMES_LOCAL_IMAGE_GPU_ACTIVITY_NODE"] == "titan-24"
|
||||
assert model_env["HERMES_LOCAL_IMAGE_GPU_ACTIVE_SM_PERCENT"] == "80"
|
||||
assert model_env["HERMES_LOCAL_IMAGE_GPU_MAX_EXTERNAL_MEMORY_BYTES"] == (
|
||||
"3221225472"
|
||||
)
|
||||
assert model_env["HERMES_LOCAL_IMAGE_OFFLOAD_MODE"] == "sequential"
|
||||
assert (
|
||||
"nvidia-process-exporter-local.monitoring.svc.cluster.local"
|
||||
in model_env["HERMES_LOCAL_IMAGE_GPU_ACTIVITY_URL"]
|
||||
)
|
||||
models_volume = next(item for item in pod["volumes"] if item["name"] == "models")
|
||||
assert models_volume["persistentVolumeClaim"]["claimName"] == (
|
||||
"hermes-image-models"
|
||||
)
|
||||
|
||||
services = _documents(HERMES / "service.yaml")
|
||||
image_service = next(
|
||||
item for item in services if item["metadata"]["name"] == "hermes-local-image"
|
||||
)
|
||||
assert image_service["spec"]["selector"] == {"app": "hermes-local-image"}
|
||||
|
||||
handoff_services = _documents(HERMES / "model-gate-deployment.yaml")
|
||||
handoff = next(
|
||||
item
|
||||
for item in handoff_services
|
||||
if item["kind"] == "Service"
|
||||
and item["metadata"]["name"] == "hermes-gpu-handoff"
|
||||
)
|
||||
assert handoff["spec"]["ports"][0]["targetPort"] == "handoff"
|
||||
|
||||
ariadne = _documents(
|
||||
Path(__file__).parents[2] / "services/maintenance/apps/ariadne-deployment.yaml"
|
||||
)[0]
|
||||
env = {
|
||||
item["name"]: item["value"]
|
||||
for item in ariadne["spec"]["template"]["spec"]["containers"][0]["env"]
|
||||
if "value" in item
|
||||
}
|
||||
assert env["GAME_MODE_OLLAMA_URL"] == (
|
||||
"http://hermes-gpu-handoff.hermes.svc.cluster.local:11434"
|
||||
)
|
||||
assert env["GAME_MODE_OLLAMA_MODEL"] == "flux-2-klein-4b-local"
|
||||
|
||||
for config_name in (
|
||||
"configmap.yaml",
|
||||
"agent-configmap.yaml",
|
||||
"chat-configmap.yaml",
|
||||
):
|
||||
config = _documents(HERMES / config_name)[0]["data"]["config.yaml"]
|
||||
assert "gpt-oss:20b" not in config
|
||||
assert "atlas-switchyard" in config
|
||||
switchyard = _documents(HERMES / "switchyard-configmap.yaml")[0]["data"][
|
||||
"routes.toml"
|
||||
]
|
||||
assert 'id = "qwen2.5:14b-instruct-q4_0"' in switchyard
|
||||
assert "qwen2.5:3b-instruct-q4_0" not in switchyard
|
||||
assert "route/local/qwen2.5-14b/medium" in switchyard
|
||||
assert "Anthropic and Claude name the same provider" in switchyard
|
||||
assert "OpenAI and Codex name the same provider" in switchyard
|
||||
assert "Choose across every configured Codex and Claude family" in switchyard
|
||||
assert "Claude Fable" in switchyard
|
||||
assert switchyard.count('Treat "think hard"') == 4
|
||||
assert switchyard.count("Never choose below the") >= 5
|
||||
switchyard_config = tomllib.loads(switchyard)
|
||||
routes = switchyard_config["routes"]
|
||||
configured_targets = switchyard_config["targets"]
|
||||
for route_name in ("auto_fast", "auto_balanced", "auto_deep", "auto_maximum"):
|
||||
leading_targets = set(routes[route_name]["targets"][:2])
|
||||
assert leading_targets == {"codex_sol_xhigh", "claude_opus_xhigh"}
|
||||
assert "max_output_tokens" not in routes[route_name]
|
||||
for route_name in ("auto_fast", "auto_balanced", "auto_deep", "auto_maximum"):
|
||||
targets = routes[route_name]["targets"]
|
||||
selector_targets = routes[route_name]["response_schema"]
|
||||
assert not any(target.startswith("local_") for target in targets)
|
||||
assert any("fable" in target for target in targets)
|
||||
assert "local_qwen" not in selector_targets
|
||||
assert "not eligible for foreground" in routes[route_name]["prompt"]
|
||||
for route_name in ("auto_deep", "auto_maximum"):
|
||||
targets = routes[route_name]["targets"]
|
||||
selector_targets = routes[route_name]["response_schema"]
|
||||
assert not any(target.endswith("_low") for target in targets)
|
||||
assert "_low" not in selector_targets
|
||||
maximum_targets = routes["auto_maximum"]["targets"]
|
||||
maximum_selector_targets = routes["auto_maximum"]["response_schema"]
|
||||
assert not any(target.endswith("_medium") for target in maximum_targets)
|
||||
assert "_medium" not in maximum_selector_targets
|
||||
assert "absolute high effort floor" in routes["auto_maximum"]["prompt"]
|
||||
assert "quality mark was missed" in routes["auto_balanced"]["prompt"]
|
||||
assert "raises the next boundary to xhigh" in routes["auto_maximum"]["prompt"]
|
||||
assert (
|
||||
"Repeated quality misses require xhigh"
|
||||
in routes["worker_auto_maximum"]["prompt"]
|
||||
)
|
||||
assert any(
|
||||
target.startswith("local_") for target in routes["manual_local_qwen"]["targets"]
|
||||
)
|
||||
for route_name in (
|
||||
"manual_codex_luna",
|
||||
"manual_codex_terra",
|
||||
"manual_codex_sol",
|
||||
"manual_claude_haiku",
|
||||
"manual_claude_fable",
|
||||
"manual_claude_sonnet",
|
||||
"manual_claude_opus",
|
||||
):
|
||||
assert not any(
|
||||
target.startswith("local_") for target in routes[route_name]["targets"]
|
||||
)
|
||||
for provider, families in {
|
||||
"codex": ("luna", "terra", "sol"),
|
||||
"claude": ("haiku", "fable", "sonnet", "opus"),
|
||||
}.items():
|
||||
for family in families:
|
||||
for effort in ("low", "medium", "high", "xhigh"):
|
||||
route = routes[f"manual_{provider}_{family}_{effort}"]
|
||||
assert route["id"] == f"atlas/manual/{provider}/{family}/{effort}"
|
||||
assert route["targets"][0] == f"{provider}_{family}_{effort}"
|
||||
worker_target = f"worker_{provider}_{family}_{effort}"
|
||||
assert worker_target in routes["worker_auto_maximum"]["targets"]
|
||||
assert configured_targets[worker_target]["id"] == (
|
||||
f"worker/{provider}/{family}/{effort}"
|
||||
)
|
||||
for route_name in ("auto_fast", "auto_balanced"):
|
||||
prompt = routes[route_name]["prompt"]
|
||||
assert "image tool—not the conversational model" in prompt
|
||||
assert "Do not select a" in prompt
|
||||
assert "local Qwen or Claude target" in prompt
|
||||
assert "max_output_tokens" not in routes["worker_auto_maximum"]
|
||||
model_gate = _documents(HERMES / "model-gate-configmap.yaml")[0]["data"][
|
||||
"model_gate.py"
|
||||
]
|
||||
assert "qwen2.5:14b-instruct-q4_0" in model_gate
|
||||
@ -1,478 +0,0 @@
|
||||
"""Session lineage and continuity contracts for Hermes chat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_chat_support import (
|
||||
HERMES,
|
||||
)
|
||||
|
||||
|
||||
def test_codex_native_health_overrides_historical_router_errors(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""Fresh first-party health is authoritative over old Switchyard probes."""
|
||||
plugin_path = HERMES / "plugins" / "auto-router" / "provider_status.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_provider_status", plugin_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
health_path = tmp_path / "codex.json"
|
||||
health_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"state": "available",
|
||||
"authenticated": True,
|
||||
"transport": "codex-chatgpt-subscription",
|
||||
}
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(module, "CODEX_HEALTH_PATH", health_path)
|
||||
monkeypatch.setattr(module, "CLAUDE_HEALTH_PATH", tmp_path / "missing.json")
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_get_json",
|
||||
lambda url: {"status": "ok"}
|
||||
if url.endswith("/health")
|
||||
else {
|
||||
"models": {
|
||||
"route/codex/terra/medium": {
|
||||
"calls": 1,
|
||||
"errors": 99,
|
||||
"total_tokens": 12,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(module, "_codex_account", lambda: {})
|
||||
monkeypatch.setattr(module, "_claude_account", lambda: {})
|
||||
|
||||
codex = module.provider_status_payload()["providers"]["codex"]
|
||||
|
||||
assert codex["errors"] == 99
|
||||
assert codex["state"] == "available"
|
||||
assert codex["native_health"]["transport"] == "codex-chatgpt-subscription"
|
||||
|
||||
|
||||
def test_api_session_patch_accepts_parent_lineage(tmp_path: Path):
|
||||
"""API-created workers must persist the originating Hermes session."""
|
||||
module_path = HERMES / "scripts" / "patch_api_server_sessions.py"
|
||||
spec = importlib.util.spec_from_file_location("patch_api_sessions", module_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
source = tmp_path / "api_server.py"
|
||||
destination = tmp_path / "patched.py"
|
||||
source.write_text(
|
||||
"prefix\n"
|
||||
+ module.BEFORE
|
||||
+ "middle\n"
|
||||
+ module.RUNS_BEFORE
|
||||
+ "run body\n"
|
||||
+ module.RUN_CLOSE_BEFORE
|
||||
+ module.RESPONSES_SESSION_BEFORE
|
||||
+ module.EVENT_CALLBACK_SIGNATURE_BEFORE
|
||||
+ "callback docstring and push helper\n"
|
||||
+ module.EVENT_CALLBACK_BODY_BEFORE
|
||||
+ "tool start body\n"
|
||||
+ module.EVENT_CALLBACK_END_BEFORE
|
||||
+ module.EVENT_CALLBACK_CALL_BEFORE
|
||||
+ module.RUN_SWEEP_BEFORE
|
||||
+ "suffix\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
module.patch(source, destination)
|
||||
patched = destination.read_text(encoding="utf-8")
|
||||
|
||||
assert "X-Hermes-Parent-Session-Id" in patched
|
||||
assert "parent_session_id=parent_session_id" in patched
|
||||
assert "Parent session not found" in patched
|
||||
assert "HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES" in patched
|
||||
assert "user_message.startswith(default_prefixes)" in patched
|
||||
assert "session_parent_conflict" in patched
|
||||
assert "X-Hermes-Conversation-Platform" in patched
|
||||
assert "X-Hermes-Conversation-Title" in patched
|
||||
assert 'conversation_platform != "telegram"' in patched
|
||||
assert "db.record_gateway_session_peer(" in patched
|
||||
assert 'display_name="Telegram"' in patched
|
||||
assert "db.reopen_session(session_id)" in patched
|
||||
assert 'db.end_session(session_id, f"api_run_{terminal_status}")' in patched
|
||||
assert "def _record_run_activity(" in patched
|
||||
assert '"_thinking": "Hermes is reasoning"' in patched
|
||||
assert '"run.started": "Worker started"' in patched
|
||||
assert '"run.completed": "Worker completed"' in patched
|
||||
assert '"reasoning.available": "Hermes finished a reasoning step"' in patched
|
||||
assert '"subagent.progress": "Nested worker progress"' in patched
|
||||
assert "redact_sensitive_text" in patched
|
||||
assert 'getattr(os, "O_NOFOLLOW", 0)' in patched
|
||||
assert "os.fchmod(fd, 0o600)" in patched
|
||||
assert "session_id=session_id" in patched
|
||||
assert 'self._record_run_activity(session_id, "run.started")' in patched
|
||||
assert "detail = tool_name if event_type in {" in patched
|
||||
assert 'if event_type == "subagent.tool"' in patched
|
||||
assert "_RUN_ACTIVITY_HEARTBEAT_SECONDS = 15.0" in patched
|
||||
assert "heartbeats.get(session_id, 0.0)" in patched
|
||||
assert '"subagent.thinking",' in patched
|
||||
assert "Stream retention and run lifetime are separate" in patched
|
||||
assert 'terminal_status in {"completed", "failed", "cancelled"}' in patched
|
||||
assert patched.index("terminal_status = self._run_statuses") < patched.index(
|
||||
"self._active_run_tasks.pop(run_id, None)"
|
||||
)
|
||||
|
||||
|
||||
def test_telegram_api_session_migration_is_bounded_and_idempotent(tmp_path: Path):
|
||||
"""Only named Telegram conversations receive presentation metadata."""
|
||||
module_path = HERMES / "scripts" / "migrate_telegram_api_sessions.py"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"migrate_telegram_sessions", module_path
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
state = tmp_path / "state.db"
|
||||
responses = tmp_path / "response_store.db"
|
||||
with sqlite3.connect(state) as connection:
|
||||
connection.execute(
|
||||
"""CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY, source TEXT, session_key TEXT, chat_type TEXT,
|
||||
display_name TEXT, origin_json TEXT, title TEXT
|
||||
)"""
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO sessions (id, source) VALUES (?, ?)",
|
||||
(("telegram-session", "api_server"), ("other-session", "api_server")),
|
||||
)
|
||||
with sqlite3.connect(responses) as connection:
|
||||
connection.execute(
|
||||
"CREATE TABLE conversations (name TEXT PRIMARY KEY, response_id TEXT NOT NULL)"
|
||||
)
|
||||
connection.execute(
|
||||
"CREATE TABLE responses (response_id TEXT PRIMARY KEY, data TEXT NOT NULL, accessed_at REAL NOT NULL)"
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO responses VALUES (?, ?, 0)",
|
||||
(
|
||||
("telegram-response", json.dumps({"session_id": "telegram-session"})),
|
||||
("other-response", json.dumps({"session_id": "other-session"})),
|
||||
),
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO conversations VALUES (?, ?)",
|
||||
(("telegram", "telegram-response"), ("unrelated", "other-response")),
|
||||
)
|
||||
|
||||
assert module.migrate(state, responses) == 1
|
||||
assert module.migrate(state, responses) == 0
|
||||
with sqlite3.connect(state) as connection:
|
||||
telegram = connection.execute(
|
||||
"SELECT session_key, chat_type, display_name, origin_json, title "
|
||||
"FROM sessions WHERE id = 'telegram-session'"
|
||||
).fetchone()
|
||||
other = connection.execute(
|
||||
"SELECT session_key, title FROM sessions WHERE id = 'other-session'"
|
||||
).fetchone()
|
||||
assert telegram[:3] == ("telegram", "private", "Telegram")
|
||||
assert json.loads(telegram[3]) == {
|
||||
"platform": "telegram",
|
||||
"session_key": "telegram",
|
||||
}
|
||||
assert telegram[4] == "Telegram · General"
|
||||
assert other == (None, None)
|
||||
|
||||
|
||||
def test_web_session_activity_patch_projects_bounded_events(tmp_path: Path):
|
||||
"""The DOM transcript includes events without polluting agent history."""
|
||||
module_path = HERMES / "scripts" / "patch_web_session_activity.py"
|
||||
spec = importlib.util.spec_from_file_location("patch_web_activity", module_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
source = tmp_path / "web_server.py"
|
||||
destination = tmp_path / "patched.py"
|
||||
source.write_text(
|
||||
"prefix\n"
|
||||
+ module.LATEST_ROWS_BEFORE
|
||||
+ module.LATEST_SELECTION_BEFORE
|
||||
+ module.HELPER_MARKER
|
||||
+ " db = object()\n"
|
||||
+ module.MESSAGES_BEFORE
|
||||
+ "suffix\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
module.patch(source, destination)
|
||||
patched = destination.read_text(encoding="utf-8")
|
||||
|
||||
assert "def _run_activity_messages(" in patched
|
||||
assert 'root = get_hermes_home() / "run-activity"' in patched
|
||||
assert "path.stat().st_size > 600_000" in patched
|
||||
assert "entries[-1_000:]" in patched
|
||||
assert "*_run_activity_messages(sid)" in patched
|
||||
assert "messages.sort(" in patched
|
||||
assert "limit: Optional[int] = None" in patched
|
||||
assert "total_messages = len(messages)" in patched
|
||||
assert "min(int(limit), 10_000)" in patched
|
||||
assert '"total_messages": total_messages' in patched
|
||||
assert "SELECT id, parent_session_id, started_at, ended_at" in patched
|
||||
assert "newest still-open member of the lineage" in patched
|
||||
assert "immediate objective parent" in patched
|
||||
assert "mixes unrelated workstreams" in patched
|
||||
assert "orphaned children" in patched
|
||||
assert 'item[0].get("ended_at") is None' in patched
|
||||
|
||||
|
||||
def test_web_session_lineage_returns_to_resumed_parent():
|
||||
"""An ended reviewer must not strand the live view away from its parent."""
|
||||
module_path = HERMES / "scripts" / "patch_web_session_activity.py"
|
||||
spec = importlib.util.spec_from_file_location("patch_web_lineage", module_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
namespace: dict[str, object] = {}
|
||||
exec(
|
||||
"def select_active(sid, db, rows):\n" + module.LATEST_SELECTION_AFTER,
|
||||
namespace,
|
||||
)
|
||||
|
||||
class FakeDB:
|
||||
def get_session(self, session_id):
|
||||
return next((row for row in rows if row["id"] == session_id), None)
|
||||
|
||||
rows = [
|
||||
{
|
||||
"id": "umbrella",
|
||||
"parent_session_id": None,
|
||||
"started_at": 0.0,
|
||||
"ended_at": None,
|
||||
},
|
||||
{
|
||||
"id": "root",
|
||||
"parent_session_id": "umbrella",
|
||||
"started_at": 1.0,
|
||||
"ended_at": None,
|
||||
},
|
||||
{
|
||||
"id": "review-1",
|
||||
"parent_session_id": "root",
|
||||
"started_at": 2.0,
|
||||
"ended_at": 3.0,
|
||||
},
|
||||
{
|
||||
"id": "review-2",
|
||||
"parent_session_id": "root",
|
||||
"started_at": 4.0,
|
||||
"ended_at": None,
|
||||
},
|
||||
]
|
||||
select_active = namespace["select_active"]
|
||||
|
||||
assert select_active("root", FakeDB(), rows) == (
|
||||
"review-2",
|
||||
["root", "review-2"],
|
||||
)
|
||||
assert select_active("review-1", FakeDB(), rows) == (
|
||||
"review-2",
|
||||
["root", "review-2"],
|
||||
)
|
||||
rows[2]["ended_at"] = 5.0
|
||||
rows[3]["ended_at"] = 5.0
|
||||
assert select_active("root", FakeDB(), rows) == ("root", ["root"])
|
||||
|
||||
rows[1]["ended_at"] = 9.0
|
||||
rows.append(
|
||||
{
|
||||
"id": "orphaned-review",
|
||||
"parent_session_id": "root",
|
||||
"started_at": 6.0,
|
||||
"ended_at": None,
|
||||
}
|
||||
)
|
||||
rows.append(
|
||||
{
|
||||
"id": "accepted-review",
|
||||
"parent_session_id": "root",
|
||||
"started_at": 7.0,
|
||||
"ended_at": 8.0,
|
||||
}
|
||||
)
|
||||
assert select_active("review-1", FakeDB(), rows) == (
|
||||
"accepted-review",
|
||||
["root", "accepted-review"],
|
||||
)
|
||||
|
||||
|
||||
def test_api_activity_patch_coalesces_streaming_heartbeats(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""Token callbacks stay bounded while every tool transition is retained."""
|
||||
module_path = HERMES / "scripts" / "patch_api_server_sessions.py"
|
||||
spec = importlib.util.spec_from_file_location("patch_api_activity", module_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
activity_body = module.EVENT_CALLBACK_SIGNATURE_AFTER.split(
|
||||
" def _make_run_event_callback(", 1
|
||||
)[0]
|
||||
namespace: dict[str, object] = {}
|
||||
exec(
|
||||
"import hashlib, json, logging, os, time\n"
|
||||
"from pathlib import Path\n"
|
||||
"logger = logging.getLogger(__name__)\n"
|
||||
"def redact_sensitive_text(value): return value\n"
|
||||
"class ActivityRecorder:\n" + activity_body,
|
||||
namespace,
|
||||
)
|
||||
recorder = namespace["ActivityRecorder"]()
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
recorder._record_run_activity("session", "_thinking")
|
||||
recorder._record_run_activity("session", "reasoning.available")
|
||||
recorder._record_run_activity("session", "subagent.thinking")
|
||||
recorder._record_run_activity("session", "tool.started", tool_name="terminal")
|
||||
recorder._record_run_activity("session", "tool.completed", tool_name="terminal")
|
||||
|
||||
journals = list((tmp_path / "run-activity").glob("*.jsonl"))
|
||||
assert len(journals) == 1
|
||||
entries = [json.loads(line) for line in journals[0].read_text().splitlines()]
|
||||
assert [entry["activity_event"] for entry in entries] == [
|
||||
"_thinking",
|
||||
"tool.started",
|
||||
"tool.completed",
|
||||
]
|
||||
|
||||
|
||||
def test_legacy_api_sessions_are_nested_idempotently(tmp_path: Path):
|
||||
"""Known standalone API workers move under Cassandra without data loss."""
|
||||
module_path = HERMES / "scripts" / "migrate_api_session_lineage.py"
|
||||
spec = importlib.util.spec_from_file_location("migrate_api_sessions", module_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
database = tmp_path / "state.db"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.execute(
|
||||
"CREATE TABLE sessions (id TEXT PRIMARY KEY, source TEXT, "
|
||||
"parent_session_id TEXT, title TEXT, transcript TEXT, archived INTEGER DEFAULT 0)"
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO sessions (id, source, parent_session_id, title, transcript) "
|
||||
"VALUES (?, 'tui', NULL, 'Cassandra', 'parent-data')",
|
||||
(module.LEGACY_CASSANDRA_PARENT,),
|
||||
)
|
||||
worker_id = next(iter(module.LEGACY_CASSANDRA_WORKERS))
|
||||
connection.execute(
|
||||
"INSERT INTO sessions (id, source, parent_session_id, title, transcript) "
|
||||
"VALUES (?, 'api_server', NULL, 'old', 'keep-me')",
|
||||
(worker_id,),
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO sessions (id, source, parent_session_id, title, transcript) "
|
||||
"VALUES (?, 'api_server', NULL, 'old smoke', 'keep-smoke')",
|
||||
((orphan_id,) for orphan_id in module.LEGACY_ORPHANED_SMOKE_SESSIONS),
|
||||
)
|
||||
|
||||
assert module.migrate(database) == 1 + len(module.LEGACY_ORPHANED_SMOKE_SESSIONS)
|
||||
assert module.migrate(database) == 0
|
||||
with sqlite3.connect(database) as connection:
|
||||
row = connection.execute(
|
||||
"SELECT parent_session_id, title, transcript FROM sessions WHERE id = ?",
|
||||
(worker_id,),
|
||||
).fetchone()
|
||||
assert row == (
|
||||
module.LEGACY_CASSANDRA_PARENT,
|
||||
module.LEGACY_CASSANDRA_WORKERS[worker_id],
|
||||
"keep-me",
|
||||
)
|
||||
with sqlite3.connect(database) as connection:
|
||||
orphans = connection.execute(
|
||||
"SELECT id, archived, title, transcript FROM sessions "
|
||||
"WHERE id IN ({}) ORDER BY id".format(
|
||||
",".join("?" for _ in module.LEGACY_ORPHANED_SMOKE_SESSIONS)
|
||||
),
|
||||
tuple(module.LEGACY_ORPHANED_SMOKE_SESSIONS),
|
||||
).fetchall()
|
||||
assert orphans == sorted(
|
||||
(
|
||||
orphan_id,
|
||||
1,
|
||||
module.LEGACY_ORPHANED_SMOKE_SESSIONS[orphan_id],
|
||||
"keep-smoke",
|
||||
)
|
||||
for orphan_id in module.LEGACY_ORPHANED_SMOKE_SESSIONS
|
||||
)
|
||||
|
||||
|
||||
def test_automated_triage_sessions_are_grouped_without_touching_interactive_runs(
|
||||
tmp_path: Path,
|
||||
):
|
||||
"""Only the stable Ariadne contract moves below the triage parent."""
|
||||
module_path = HERMES / "scripts" / "migrate_api_session_lineage.py"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"migrate_triage_sessions", module_path
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
database = tmp_path / "state.db"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.execute(
|
||||
"CREATE TABLE sessions (id TEXT PRIMARY KEY, source TEXT, "
|
||||
"parent_session_id TEXT, title TEXT, started_at REAL, archived INTEGER DEFAULT 0)"
|
||||
)
|
||||
connection.execute(
|
||||
"CREATE TABLE messages (session_id TEXT, role TEXT, content TEXT)"
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO sessions (id, source, started_at) VALUES (?, 'api_server', ?)",
|
||||
(("triage-run", 1.0), ("jenkins-run", 1.5), ("interactive-run", 2.0)),
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO messages (session_id, role, content) VALUES (?, 'user', ?)",
|
||||
(
|
||||
(
|
||||
"triage-run",
|
||||
module.TRIAGE_MESSAGE_PREFIXES[0]
|
||||
+ " Fix for incident sonar/bstein_home/python:S2208/finding-key.",
|
||||
),
|
||||
(
|
||||
"jenkins-run",
|
||||
module.TRIAGE_MESSAGE_PREFIXES[1]
|
||||
+ "\nAnalyze incident soteria/291 for the Jenkins job soteria.",
|
||||
),
|
||||
("interactive-run", "Please explain this alert to me."),
|
||||
),
|
||||
)
|
||||
|
||||
assert module.migrate(database, group_triage=True) == 2
|
||||
assert module.migrate(database, group_triage=True) == 0
|
||||
with sqlite3.connect(database) as connection:
|
||||
parent = connection.execute(
|
||||
"SELECT title FROM sessions WHERE id = ?", (module.TRIAGE_PARENT,)
|
||||
).fetchone()
|
||||
triage = connection.execute(
|
||||
"SELECT parent_session_id, title FROM sessions WHERE id = 'triage-run'"
|
||||
).fetchone()
|
||||
interactive = connection.execute(
|
||||
"SELECT parent_session_id FROM sessions WHERE id = 'interactive-run'"
|
||||
).fetchone()
|
||||
jenkins = connection.execute(
|
||||
"SELECT parent_session_id, title FROM sessions WHERE id = 'jenkins-run'"
|
||||
).fetchone()
|
||||
assert parent == (module.TRIAGE_PARENT_TITLE,)
|
||||
assert triage == (
|
||||
module.TRIAGE_PARENT,
|
||||
"Sonar · bstein_home · python:S2208 · iage-run",
|
||||
)
|
||||
assert interactive == (None,)
|
||||
assert jenkins == (
|
||||
module.TRIAGE_PARENT,
|
||||
"Sonar · soteria/291 · kins-run",
|
||||
)
|
||||
@ -1,38 +0,0 @@
|
||||
"""Shared paths and loaders for Hermes chat capability contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
HERMES = ROOT / "services" / "hermes"
|
||||
VAULT = ROOT / "services" / "vault"
|
||||
|
||||
|
||||
def _documents(path: Path) -> list[dict]:
|
||||
return [doc for doc in yaml.safe_load_all(path.read_text()) if doc]
|
||||
|
||||
|
||||
def _load_broker_module(name: str, filename: str, monkeypatch):
|
||||
"""Load one broker with its mounted routing-catalog dependency."""
|
||||
catalog_path = HERMES / "scripts" / "routing_catalog.py"
|
||||
catalog_spec = importlib.util.spec_from_file_location(
|
||||
"routing_catalog", catalog_path
|
||||
)
|
||||
assert catalog_spec and catalog_spec.loader
|
||||
catalog = importlib.util.module_from_spec(catalog_spec)
|
||||
catalog_spec.loader.exec_module(catalog)
|
||||
monkeypatch.setitem(sys.modules, "routing_catalog", catalog)
|
||||
monkeypatch.setitem(sys.modules, "httpx", SimpleNamespace())
|
||||
|
||||
broker_path = HERMES / "scripts" / filename
|
||||
spec = importlib.util.spec_from_file_location(name, broker_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,378 +0,0 @@
|
||||
"""Network, OAuth, Kubernetes, and SSH access contracts for Hermes CLI lanes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_cli_lanes_support import (
|
||||
FLUX_HERMES,
|
||||
HERMES,
|
||||
KEYCLOAK,
|
||||
ROOT,
|
||||
_agent_deployment,
|
||||
_services,
|
||||
)
|
||||
|
||||
|
||||
def test_broker_services_survive_sibling_container_readiness_loss():
|
||||
services = _services()
|
||||
|
||||
for name in (
|
||||
"hermes-image-broker",
|
||||
"hermes-codex-broker",
|
||||
"hermes-local-image",
|
||||
"hermes-claude-broker",
|
||||
):
|
||||
assert services[name]["spec"]["publishNotReadyAddresses"] is True
|
||||
|
||||
|
||||
def test_agent_dashboard_reconnects_all_transient_websockets():
|
||||
dockerfile = (HERMES.parents[1] / "dockerfiles/Dockerfile.hermes-agent").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "eventsRetryAttempt.current" in dockerfile
|
||||
assert "if (!unmounting) setVersion((v) => v + 1);" in dockerfile
|
||||
assert "events feed rejected (${ev.code}) — reload the page" in dockerfile
|
||||
assert 'url = await api.buildWsUrl("/api/pty", params);' in dockerfile
|
||||
assert 'url = await buildWsUrl("/api/events", { channel });' in dockerfile
|
||||
assert dockerfile.count("' await api.getSessions(1, 0,") == 2
|
||||
assert ".then(() => gw.connect())" in dockerfile
|
||||
assert 'api.getSessions(1, 0, profile ?? "")' in dockerfile
|
||||
assert "dashboard token rotated by a server restart" in dockerfile
|
||||
|
||||
|
||||
def test_agent_refreshes_routes_after_restoring_cli_logins():
|
||||
deployment = _agent_deployment()
|
||||
init_containers = {
|
||||
item["name"]: item
|
||||
for item in deployment["spec"]["template"]["spec"]["initContainers"]
|
||||
}
|
||||
configure = init_containers["configure-agent-clients"]
|
||||
command = configure["command"][-1]
|
||||
assert "configure_agent_clients.py" in command
|
||||
assert command.index("configure_agent_clients.py") < command.index(
|
||||
"hermes_coordinator.py --once"
|
||||
)
|
||||
env = {item["name"]: item["value"] for item in configure["env"]}
|
||||
assert env["HERMES_AUTH_FILE"] == "/runtime-access/hermes-auth.json"
|
||||
assert env["PYTHONPATH"] == "/opt/hermes"
|
||||
for name in ("bootstrap-coordinator", "configure-agent-clients"):
|
||||
route_env = {
|
||||
item["name"]: item["value"] for item in init_containers[name]["env"]
|
||||
}
|
||||
assert route_env["CODEX_HOME"] == "/runtime-access/codex"
|
||||
assert route_env["CLAUDE_CONFIG_DIR"] == "/runtime-access/claude"
|
||||
assert "/opt/data/tools/bin" in route_env["PATH"]
|
||||
|
||||
assert "patch-web-session-activity" in init_containers
|
||||
web_patch = init_containers["patch-web-session-activity"]
|
||||
assert "/opt/coordinator/patch_web_session_activity.py" in web_patch["command"]
|
||||
|
||||
containers = {
|
||||
item["name"]: item
|
||||
for item in deployment["spec"]["template"]["spec"]["containers"]
|
||||
}
|
||||
steward_env = {
|
||||
item["name"]: item["value"] for item in containers["model-steward"]["env"]
|
||||
}
|
||||
assert steward_env["CODEX_HOME"] == "/runtime-access/codex"
|
||||
assert steward_env["CLAUDE_CONFIG_DIR"] == "/runtime-access/claude"
|
||||
assert "/opt/data/tools/bin" in steward_env["PATH"]
|
||||
hermes_mounts = {
|
||||
(item["name"], item["mountPath"], item.get("subPath"))
|
||||
for item in containers["hermes"]["volumeMounts"]
|
||||
}
|
||||
assert (
|
||||
"web-server-patch",
|
||||
"/opt/hermes/hermes_cli/web_server.py",
|
||||
"web_server.py",
|
||||
) in hermes_mounts
|
||||
|
||||
|
||||
def test_flux_health_checks_follow_the_owner_oauth_sidecar():
|
||||
flux = yaml.safe_load(FLUX_HERMES.read_text())
|
||||
checks = {(item["kind"], item["name"]) for item in flux["spec"]["healthChecks"]}
|
||||
assert ("Deployment", "hermes-agent") in checks
|
||||
assert ("DaemonSet", "hermes-node-ssh-access") in checks
|
||||
assert ("Deployment", "oauth2-proxy-hermes-agent") not in checks
|
||||
|
||||
|
||||
def test_agent_auth_is_bstein_group_and_email_bounded():
|
||||
deployment = _agent_deployment()
|
||||
oauth = next(
|
||||
item
|
||||
for item in deployment["spec"]["template"]["spec"]["containers"]
|
||||
if item["name"] == "oauth2-proxy"
|
||||
)
|
||||
args = oauth["args"]
|
||||
assert "--user-id-claim=sub" in args
|
||||
assert "--oidc-groups-claim=groups" in args
|
||||
assert "--allowed-group=/hermes-owner" in args
|
||||
assert "--authenticated-emails-file=/etc/oauth2-proxy/allowed-emails" in args
|
||||
script = (KEYCLOAK / "scripts/hermes_access_oidc_ensure.sh").read_text()
|
||||
assert 'group_name="hermes-owner"' in script
|
||||
assert "username=bstein&exact=true" in script
|
||||
assert '"full.path":"true"' in script
|
||||
|
||||
|
||||
def test_agent_network_boundary_allows_only_authenticated_and_metrics_surfaces():
|
||||
documents = [
|
||||
item
|
||||
for item in yaml.safe_load_all((HERMES / "networkpolicy.yaml").read_text())
|
||||
if item
|
||||
]
|
||||
isolation = next(
|
||||
item
|
||||
for item in documents
|
||||
if item.get("metadata", {}).get("name") == "hermes-agent-isolation"
|
||||
)
|
||||
assert isolation["spec"]["ingress"] == [
|
||||
{
|
||||
"from": [
|
||||
{
|
||||
"namespaceSelector": {
|
||||
"matchLabels": {"kubernetes.io/metadata.name": "traefik"}
|
||||
},
|
||||
"podSelector": {
|
||||
"matchLabels": {"app.kubernetes.io/name": "traefik"}
|
||||
},
|
||||
}
|
||||
],
|
||||
"ports": [{"protocol": "TCP", "port": 4180}],
|
||||
},
|
||||
{
|
||||
"from": [{"podSelector": {"matchLabels": {"app": "hermes-chat-tenant"}}}],
|
||||
"ports": [
|
||||
{"protocol": "TCP", "port": 9002},
|
||||
{"protocol": "TCP", "port": 9003},
|
||||
],
|
||||
},
|
||||
{
|
||||
"from": [{"podSelector": {"matchLabels": {"app": "hermes-switchyard"}}}],
|
||||
"ports": [
|
||||
{"protocol": "TCP", "port": 9003},
|
||||
{"protocol": "TCP", "port": 9006},
|
||||
],
|
||||
},
|
||||
{
|
||||
"from": [
|
||||
{
|
||||
"namespaceSelector": {
|
||||
"matchLabels": {"kubernetes.io/metadata.name": "monitoring"}
|
||||
},
|
||||
"podSelector": {"matchLabels": {"app": "server"}},
|
||||
}
|
||||
],
|
||||
"ports": [{"protocol": "TCP", "port": 9010}],
|
||||
},
|
||||
]
|
||||
egress = isolation["spec"]["egress"]
|
||||
assert any(
|
||||
rule.get("ports") == [{"protocol": "TCP", "port": 9081}] for rule in egress
|
||||
)
|
||||
assert "hermes-scm" in yaml.safe_dump(egress)
|
||||
assert "gitea" in yaml.safe_dump(egress)
|
||||
assert egress != [{}]
|
||||
|
||||
|
||||
def test_owner_agent_has_scoped_read_only_kubernetes_context():
|
||||
config = yaml.safe_load((HERMES / "agent-kubeconfig.yaml").read_text())
|
||||
assert config["current-context"] == "atlas-observer"
|
||||
assert config["contexts"][0]["context"]["namespace"] == "default"
|
||||
rbac_path = ROOT / "services/hermes-observer-rbac/rbac.yaml"
|
||||
documents = [item for item in yaml.safe_load_all(rbac_path.read_text()) if item]
|
||||
binding = next(item for item in documents if item["kind"] == "ClusterRoleBinding")
|
||||
assert binding["roleRef"] == {
|
||||
"apiGroup": "rbac.authorization.k8s.io",
|
||||
"kind": "ClusterRole",
|
||||
"name": "hermes-agent-cluster-observer-v2",
|
||||
}
|
||||
assert binding["subjects"] == [
|
||||
{"kind": "ServiceAccount", "name": "hermes-agent", "namespace": "hermes"}
|
||||
]
|
||||
|
||||
|
||||
def test_owner_agent_has_pinned_dedicated_node_ssh_access():
|
||||
deployment = _agent_deployment()
|
||||
annotations = deployment["spec"]["template"]["metadata"]["annotations"]
|
||||
assert (
|
||||
annotations["vault.hashicorp.com/agent-inject-secret-node-ssh-private-key"]
|
||||
== "kv/data/atlas/hermes/developer-ssh"
|
||||
)
|
||||
assert (
|
||||
annotations["vault.hashicorp.com/agent-inject-secret-node-ssh-config"]
|
||||
== "kv/data/atlas/hermes/developer-ssh"
|
||||
)
|
||||
assert (
|
||||
annotations["vault.hashicorp.com/agent-inject-secret-node-ssh-known-hosts"]
|
||||
== "kv/data/atlas/hermes/developer-ssh"
|
||||
)
|
||||
|
||||
init = next(
|
||||
item
|
||||
for item in deployment["spec"]["template"]["spec"]["initContainers"]
|
||||
if item["name"] == "init-config"
|
||||
)
|
||||
command = init["command"][2]
|
||||
assert "ln -s /runtime-access/node-ssh-config /opt/data/home/.ssh/config" in command
|
||||
assert (
|
||||
"ln -s /runtime-access/node-ssh-known-hosts /opt/data/home/.ssh/known_hosts"
|
||||
in command
|
||||
)
|
||||
assert "ln -s home/.ssh /opt/data/.ssh" in command
|
||||
assert "chmod 0700 /opt/data/home/.ssh" in command
|
||||
assert (
|
||||
"ln -s /runtime-access/node-ssh-private-key "
|
||||
"/opt/data/home/.ssh/id_ed25519_atlas_nodes"
|
||||
) in command
|
||||
|
||||
config = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())["data"]
|
||||
assert "ssh_config" not in config
|
||||
assert "ssh_known_hosts" not in config
|
||||
assert "ssh-ed25519" not in (HERMES / "agent-configmap.yaml").read_text()
|
||||
|
||||
resources = yaml.safe_load((HERMES / "kustomization.yaml").read_text())["resources"]
|
||||
assert "node-ssh-access.yaml" in resources
|
||||
access = [
|
||||
item
|
||||
for item in yaml.safe_load_all((HERMES / "node-ssh-access.yaml").read_text())
|
||||
if item
|
||||
]
|
||||
service_account = next(item for item in access if item["kind"] == "ServiceAccount")
|
||||
assert service_account["metadata"]["name"] == "hermes-node-ssh-access"
|
||||
provider = next(item for item in access if item["kind"] == "SecretProviderClass")
|
||||
assert provider["metadata"]["name"] == "hermes-node-ssh-access"
|
||||
assert provider["spec"]["provider"] == "vault"
|
||||
parameters = provider["spec"]["parameters"]
|
||||
assert parameters["roleName"] == "hermes-node-ssh"
|
||||
assert 'secretPath: "kv/data/atlas/hermes/developer-ssh"' in parameters["objects"]
|
||||
assert 'secretKey: "public_key"' in parameters["objects"]
|
||||
daemonset = next(item for item in access if item["kind"] == "DaemonSet")
|
||||
pod = daemonset["spec"]["template"]["spec"]
|
||||
assert pod["serviceAccountName"] == "hermes-node-ssh-access"
|
||||
assert pod["automountServiceAccountToken"] is True
|
||||
host_home = next(item for item in pod["volumes"] if item["name"] == "host-home")
|
||||
assert host_home["hostPath"] == {"path": "/home", "type": "Directory"}
|
||||
vault_secrets = next(
|
||||
item for item in pod["volumes"] if item["name"] == "vault-secrets"
|
||||
)
|
||||
assert vault_secrets["csi"]["driver"] == "secrets-store.csi.k8s.io"
|
||||
assert vault_secrets["csi"]["volumeAttributes"] == {
|
||||
"secretProviderClass": "hermes-node-ssh-access"
|
||||
}
|
||||
reconciler = pod["containers"][0]["args"][0]
|
||||
assert "/opt/node-hardener/node_account_hardening.py" in reconciler
|
||||
assert "--public-key-file /vault/secrets/node-ssh-public-key" in reconciler
|
||||
assert "sleep 300" in reconciler
|
||||
host_etc = next(item for item in pod["volumes"] if item["name"] == "host-etc")
|
||||
assert host_etc["hostPath"] == {"path": "/etc", "type": "Directory"}
|
||||
hardener = (HERMES / "scripts/node_account_hardening.py").read_text()
|
||||
assert 'ACCOUNT = "hermes-agent"' in hardener
|
||||
assert 'LEGACY_ACCOUNTS = ("atlas", "oceanus")' in hardener
|
||||
assert "ACCOUNT_UID = 1200" in hardener
|
||||
assert "ACCOUNT_GID = 1200" in hardener
|
||||
|
||||
|
||||
def test_owner_agent_tracks_no_ssh_identity_or_host_key_material():
|
||||
"""Vault references may be tracked; SSH identities and trust data may not."""
|
||||
forbidden = (
|
||||
"BEGIN OPENSSH PRIVATE KEY",
|
||||
"ssh-ed25519 AAAA",
|
||||
"ssh-rsa AAAA",
|
||||
"IdentityFile ",
|
||||
"UserKnownHostsFile ",
|
||||
"StrictHostKeyChecking ",
|
||||
"ssh_config:",
|
||||
"ssh_known_hosts:",
|
||||
)
|
||||
text_suffixes = {
|
||||
".conf",
|
||||
".json",
|
||||
".md",
|
||||
".py",
|
||||
".sh",
|
||||
".toml",
|
||||
".yaml",
|
||||
".yml",
|
||||
}
|
||||
tracked = "\n".join(
|
||||
path.read_text(encoding="utf-8")
|
||||
for path in HERMES.rglob("*")
|
||||
if path.is_file() and path.suffix in text_suffixes
|
||||
)
|
||||
for marker in forbidden:
|
||||
assert marker not in tracked
|
||||
|
||||
|
||||
def test_switchyard_has_a_dedicated_non_owner_identity_and_read_only_catalog():
|
||||
"""Routing must not inherit the owner agent's cluster-admin capability."""
|
||||
service_accounts = [
|
||||
item
|
||||
for item in yaml.safe_load_all(
|
||||
(HERMES / "vault-serviceaccount.yaml").read_text()
|
||||
)
|
||||
if item
|
||||
]
|
||||
assert any(
|
||||
item["kind"] == "ServiceAccount"
|
||||
and item["metadata"]["name"] == "hermes-switchyard"
|
||||
for item in service_accounts
|
||||
)
|
||||
|
||||
switchyard = yaml.safe_load((HERMES / "switchyard-deployment.yaml").read_text())
|
||||
switchyard_pod = switchyard["spec"]["template"]["spec"]
|
||||
assert switchyard_pod["serviceAccountName"] == "hermes-switchyard"
|
||||
agent_pod = _agent_deployment()["spec"]["template"]["spec"]
|
||||
for pod, container_name in (
|
||||
(switchyard_pod, "worker-route-broker"),
|
||||
(agent_pod, "claude-broker"),
|
||||
):
|
||||
container = next(
|
||||
item for item in pod["containers"] if item["name"] == container_name
|
||||
)
|
||||
catalog = next(
|
||||
item
|
||||
for item in container["volumeMounts"]
|
||||
if item["mountPath"] == "/routing-catalog"
|
||||
)
|
||||
assert catalog["readOnly"] is True
|
||||
|
||||
rbac = [
|
||||
item
|
||||
for item in yaml.safe_load_all(
|
||||
(ROOT / "services/hermes-observer-rbac/rbac.yaml").read_text()
|
||||
)
|
||||
if item
|
||||
]
|
||||
binding = next(item for item in rbac if item["kind"] == "ClusterRoleBinding")
|
||||
assert binding["subjects"] == [
|
||||
{"kind": "ServiceAccount", "name": "hermes-agent", "namespace": "hermes"}
|
||||
]
|
||||
|
||||
|
||||
def test_switchyard_active_state_uses_a_relocatable_rwx_claim():
|
||||
"""A stale node attachment must not strand the routing authority."""
|
||||
claims = [
|
||||
item
|
||||
for item in yaml.safe_load_all((HERMES / "switchyard-pvc.yaml").read_text())
|
||||
if item
|
||||
]
|
||||
active_claim = next(
|
||||
item
|
||||
for item in claims
|
||||
if item["metadata"]["name"] == "hermes-switchyard-state-rwx"
|
||||
)
|
||||
assert active_claim["spec"]["accessModes"] == ["ReadWriteMany"]
|
||||
|
||||
deployment = yaml.safe_load((HERMES / "switchyard-deployment.yaml").read_text())
|
||||
strategy = deployment["spec"]["strategy"]
|
||||
assert strategy == {
|
||||
"type": "RollingUpdate",
|
||||
"rollingUpdate": {"maxSurge": 1, "maxUnavailable": 0},
|
||||
}
|
||||
pod = deployment["spec"]["template"]["spec"]
|
||||
state = next(item for item in pod["volumes"] if item["name"] == "state")
|
||||
assert (
|
||||
state["persistentVolumeClaim"]["claimName"] == active_claim["metadata"]["name"]
|
||||
)
|
||||
@ -1,444 +0,0 @@
|
||||
"""Agent configuration and dashboard contracts for Hermes CLI lanes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_cli_lanes_support import (
|
||||
HERMES,
|
||||
_agent_deployment,
|
||||
client_config,
|
||||
lanes,
|
||||
migration,
|
||||
policy,
|
||||
)
|
||||
|
||||
|
||||
def test_workspace_preparation_failure_durably_blocks_the_claim(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
task = SimpleNamespace(id="t_bad_worktree", current_run_id=7, assignee="cli-auto")
|
||||
calls = []
|
||||
|
||||
class Connection:
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=lambda board: Connection(),
|
||||
get_task=lambda _conn, _task_id: task,
|
||||
worker_log_path=lambda _task_id, board: tmp_path / "worker.log",
|
||||
_resolve_worktree_workspace=lambda _task, board: (_ for _ in ()).throw(
|
||||
ValueError("not a Git repository")
|
||||
),
|
||||
block_task=lambda *_args, **kwargs: calls.append(kwargs),
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||||
monkeypatch.setattr(
|
||||
lanes, "state_path", lambda _board, _task_id: tmp_path / "state.json"
|
||||
)
|
||||
|
||||
lanes.execute_claim("cassandra", "t_bad_worktree")
|
||||
|
||||
assert calls[0]["kind"] == "capability"
|
||||
assert calls[0]["expected_run_id"] == 7
|
||||
assert "not a Git repository" in calls[0]["reason"]
|
||||
|
||||
|
||||
def test_artifacts_cannot_escape_the_task_worktree(tmp_path: Path):
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
inside = workspace / "report.json"
|
||||
outside = tmp_path / "auth.json"
|
||||
inside.write_text("{}\n", encoding="utf-8")
|
||||
outside.write_text("secret\n", encoding="utf-8")
|
||||
|
||||
assert lanes.workspace_artifacts(
|
||||
workspace,
|
||||
["report.json", str(outside), "missing.json"],
|
||||
) == [str(inside)]
|
||||
|
||||
|
||||
def test_restart_provider_change_includes_explicit_workspace_handoff(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
task = SimpleNamespace(
|
||||
id="t_resume",
|
||||
current_run_id=9,
|
||||
assignee="cli-auto",
|
||||
max_runtime_seconds=60,
|
||||
)
|
||||
state_file = tmp_path / "state.json"
|
||||
state_file.write_text(
|
||||
json.dumps({"current_route": {"provider": "claude"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "worker.log").write_text("prior provider evidence", encoding="utf-8")
|
||||
prompts = []
|
||||
|
||||
class Connection:
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=lambda board: Connection(),
|
||||
get_task=lambda _conn, _task_id: task,
|
||||
worker_log_path=lambda _task_id, board: tmp_path / "worker.log",
|
||||
_resolve_worktree_workspace=lambda _task, board: (tmp_path, "wt/t_resume"),
|
||||
set_branch_name=lambda *_args: None,
|
||||
set_workspace_path=lambda *_args: None,
|
||||
build_worker_context=lambda *_args: "resume objective",
|
||||
add_comment=lambda *_args: None,
|
||||
complete_task=lambda *_args, **_kwargs: None,
|
||||
block_task=lambda *_args, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||||
monkeypatch.setattr(lanes, "state_path", lambda _board, _task_id: state_file)
|
||||
monkeypatch.setattr(
|
||||
lanes,
|
||||
"select_route",
|
||||
lambda *_args, **_kwargs: lanes.Route(
|
||||
"codex", "gpt-5.6-sol", "high", "codex-high", "jetson", "vote", 1, ()
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
lanes,
|
||||
"git_handoff",
|
||||
lambda _workspace, output: f"HANDOFF:{output}",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
lanes,
|
||||
"run_provider",
|
||||
lambda _route, prompt, *_args, **_kwargs: (
|
||||
prompts.append(prompt)
|
||||
or lanes.ProcessResult(
|
||||
0,
|
||||
"",
|
||||
{
|
||||
"status": "completed",
|
||||
"summary": "done",
|
||||
"changed_files": [],
|
||||
"tests_run": [],
|
||||
"artifacts": [],
|
||||
"blockers": [],
|
||||
},
|
||||
False,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
lanes.execute_claim("cassandra", "t_resume")
|
||||
|
||||
assert "HANDOFF:prior provider evidence" in prompts[0]
|
||||
|
||||
|
||||
def test_provider_commands_are_structured_unattended_and_capped(tmp_path: Path):
|
||||
route = lanes.Route(
|
||||
"codex", "gpt-5.6-sol", "xhigh", "codex-xhigh", "jetson", "vote", 1, ()
|
||||
)
|
||||
command = lanes._codex_command(
|
||||
route, "Work.", tmp_path, {}, tmp_path / "result.json"
|
||||
)
|
||||
assert "--dangerously-bypass-approvals-and-sandbox" in command
|
||||
assert "--json" in command
|
||||
assert "--output-schema" in command
|
||||
assert 'model_reasoning_effort="xhigh"' in command
|
||||
|
||||
claude_state = {"claude_session_id": "13864642-2985-4f91-bef5-53f145f878e8"}
|
||||
claude = lanes._claude_command(
|
||||
lanes.Route(
|
||||
"claude", "claude-opus-5", "xhigh", "claude-xhigh", "jetson", "vote", 1, ()
|
||||
),
|
||||
"Review.",
|
||||
claude_state,
|
||||
False,
|
||||
)
|
||||
assert "--dangerously-skip-permissions" in claude
|
||||
assert "--output-format" in claude and "stream-json" in claude
|
||||
assert "--json-schema" in claude
|
||||
assert "--disallowedTools" in claude
|
||||
assert "Bash(kubectl apply *)" not in claude
|
||||
assert "Bash(flux reconcile *)" not in claude
|
||||
assert "max" not in claude
|
||||
|
||||
|
||||
def test_worker_contract_separates_review_findings_from_task_blockers(tmp_path: Path):
|
||||
prompt = lanes.build_prompt("Review the change.", tmp_path)
|
||||
|
||||
assert "put defects and risks in findings" in prompt
|
||||
assert "blockers array must be empty whenever status is completed" in prompt
|
||||
assert "findings" in lanes.RESULT_SCHEMA["properties"]
|
||||
assert set(lanes.RESULT_SCHEMA["required"]) == set(
|
||||
lanes.RESULT_SCHEMA["properties"]
|
||||
)
|
||||
assert (
|
||||
"assigned task itself"
|
||||
in lanes.RESULT_SCHEMA["properties"]["blockers"]["description"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
"git push --force origin main",
|
||||
"git reset --hard HEAD~1",
|
||||
"git clean -fd",
|
||||
],
|
||||
)
|
||||
def test_claude_pretool_hook_blocks_hard_denies(command: str):
|
||||
assert policy.denial_reason(command)
|
||||
|
||||
|
||||
def test_claude_pretool_hook_allows_normal_engineering():
|
||||
assert policy.denial_reason("pytest -q testing/tests") is None
|
||||
assert policy.denial_reason("git push origin feature/hermes") is None
|
||||
assert policy.denial_reason("kubectl delete pod -n cassandra stuck-worker") is None
|
||||
assert policy.denial_reason("flux reconcile kustomization hermes") is None
|
||||
assert policy.denial_reason("vault kv get kv/atlas/hermes") is None
|
||||
|
||||
|
||||
def test_claude_settings_preserve_state_and_install_three_guardrail_layers(
|
||||
tmp_path: Path,
|
||||
):
|
||||
state = tmp_path / ".claude.json"
|
||||
settings = tmp_path / "settings.json"
|
||||
state.write_text('{"promptQueueUseCount": 4}\n', encoding="utf-8")
|
||||
settings.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"theme": "dark",
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Bash(kubectl apply *)",
|
||||
"Bash(flux reconcile *)",
|
||||
"Bash(vault kv *)",
|
||||
"Bash(custom-owner-rule *)",
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
client_config.configure_claude_state(state)
|
||||
client_config.configure_claude_settings(settings)
|
||||
|
||||
state_value = json.loads(state.read_text())
|
||||
settings_value = json.loads(settings.read_text())
|
||||
assert state_value["promptQueueUseCount"] == 4
|
||||
assert state_value["bypassPermissionsModeAccepted"] is True
|
||||
assert settings_value["theme"] == "dark"
|
||||
assert "Bash(git reset --hard *)" in settings_value["permissions"]["deny"]
|
||||
assert "Bash(custom-owner-rule *)" in settings_value["permissions"]["deny"]
|
||||
assert "Bash(kubectl apply *)" not in settings_value["permissions"]["deny"]
|
||||
assert "Bash(flux reconcile *)" not in settings_value["permissions"]["deny"]
|
||||
assert "Bash(vault kv *)" not in settings_value["permissions"]["deny"]
|
||||
hook = settings_value["hooks"]["PreToolUse"][0]["hooks"][0]
|
||||
assert "claude_command_policy.py" in hook["command"]
|
||||
|
||||
|
||||
def test_legacy_state_is_archived_without_removing_provider_transcripts(tmp_path: Path):
|
||||
session = tmp_path / "home/.config/herdr/session.json"
|
||||
session.parent.mkdir(parents=True)
|
||||
session.write_text(
|
||||
'{"agents":[{"agent":"claude","session_id":"abc"}]}', encoding="utf-8"
|
||||
)
|
||||
binary = tmp_path / "tools/bin/herdr"
|
||||
binary.parent.mkdir(parents=True)
|
||||
binary.write_text("legacy", encoding="utf-8")
|
||||
(tmp_path / "home/.claude").mkdir()
|
||||
(tmp_path / "home/.codex").mkdir()
|
||||
|
||||
archive = migration.archive_legacy_state(tmp_path)
|
||||
|
||||
value = json.loads(archive.read_text())
|
||||
assert value["legacy_session"]["agents"][0]["session_id"] == "abc"
|
||||
assert (tmp_path / "home/.claude").is_dir()
|
||||
assert (tmp_path / "home/.codex").is_dir()
|
||||
assert not binary.exists()
|
||||
assert not (tmp_path / "home/.config/herdr").exists()
|
||||
|
||||
|
||||
def test_agent_uses_one_native_kanban_control_plane():
|
||||
configmap = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())
|
||||
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
||||
assert config["model"] == {
|
||||
"provider": "atlas-switchyard",
|
||||
"default": "atlas/auto/balanced",
|
||||
"model": "atlas/auto/balanced",
|
||||
}
|
||||
assert config["kanban"]["dispatch_in_gateway"] is True
|
||||
assert config["kanban"]["default_assignee"] == "cli-auto"
|
||||
assert config["plugins"]["enabled"] == ["auto-router"]
|
||||
|
||||
deployment = _agent_deployment()
|
||||
pod = deployment["spec"]["template"]["spec"]
|
||||
assert pod["enableServiceLinks"] is False
|
||||
names = {item["name"] for item in pod["containers"]}
|
||||
assert "cli-lane-runner" in names
|
||||
assert "terminal" in names
|
||||
assert not any("herdr" in name for name in names)
|
||||
rendered = (HERMES / "agent-deployment.yaml").read_text()
|
||||
assert "herdr server" not in rendered
|
||||
assert "herdr-dispatch" not in rendered
|
||||
|
||||
|
||||
def test_cli_lane_reserves_cpu_headroom_for_ui_and_auth():
|
||||
deployment = _agent_deployment()
|
||||
containers = {
|
||||
item["name"]: item
|
||||
for item in deployment["spec"]["template"]["spec"]["containers"]
|
||||
}
|
||||
lane = containers["cli-lane-runner"]
|
||||
environment = {item["name"]: item["value"] for item in lane["env"]}
|
||||
|
||||
assert environment["HERMES_CLI_LANE_CONCURRENCY"] == "2"
|
||||
assert lane["resources"] == {
|
||||
"requests": {"cpu": "100m", "memory": "256Mi"},
|
||||
"limits": {"cpu": "2", "memory": "6Gi"},
|
||||
}
|
||||
|
||||
|
||||
def test_agent_avoids_unhealthy_nodes_and_fits_its_remaining_capacity():
|
||||
"""Placement correction: keep the agent off nodes that cannot hold it.
|
||||
|
||||
titan-04 is cordoned after repeated kernel undervoltage and kubelet
|
||||
failure, and titan-19 was probe/Longhorn unstable under worker load, so
|
||||
both must join the existing hard exclusions. That leaves titan-05 as the
|
||||
healthy candidate, which is tight enough on requested CPU that the main
|
||||
container has to give back 50m to schedule there.
|
||||
"""
|
||||
pod = _agent_deployment()["spec"]["template"]["spec"]
|
||||
hostnames = next(
|
||||
item
|
||||
for item in pod["affinity"]["nodeAffinity"][
|
||||
"requiredDuringSchedulingIgnoredDuringExecution"
|
||||
]["nodeSelectorTerms"][0]["matchExpressions"]
|
||||
if item["key"] == "kubernetes.io/hostname"
|
||||
)
|
||||
|
||||
assert hostnames["operator"] == "NotIn"
|
||||
assert set(hostnames["values"]) >= {"titan-04", "titan-19"}
|
||||
|
||||
hermes = next(item for item in pod["containers"] if item["name"] == "hermes")
|
||||
assert hermes["resources"]["requests"]["cpu"] == "300m"
|
||||
|
||||
|
||||
def test_agent_root_is_stock_dashboard_and_terminal_is_a_separate_path():
|
||||
deployment = _agent_deployment()
|
||||
pod = deployment["spec"]["template"]["spec"]
|
||||
containers = {item["name"]: item for item in pod["containers"]}
|
||||
assert "webui" not in containers
|
||||
|
||||
assert "dashboard" not in containers
|
||||
hermes = containers["hermes"]
|
||||
hermes_env = {item["name"]: item["value"] for item in hermes["env"]}
|
||||
assert hermes_env["HERMES_STREAM_STALE_TIMEOUT"] == "600"
|
||||
assert hermes_env["HERMES_API_CALL_STALE_TIMEOUT"] == "600"
|
||||
assert hermes["command"] == ["/bin/sh", "-ec"]
|
||||
startup = hermes["args"][0]
|
||||
assert ". /opt/data/.env" in startup
|
||||
assert "exec /init /opt/hermes/docker/main-wrapper.sh gateway run" in startup
|
||||
hermes_env = {item["name"]: item["value"] for item in hermes["env"]}
|
||||
assert hermes_env["HERMES_DASHBOARD"] == "1"
|
||||
assert hermes_env["HERMES_DASHBOARD_HOST"] == "127.0.0.1"
|
||||
assert hermes_env["HERMES_DASHBOARD_PORT"] == "9119"
|
||||
assert hermes_env["HERMES_TUI_AGENT_INIT_TIMEOUT_S"] == "180"
|
||||
assert hermes["securityContext"]["runAsUser"] == 0
|
||||
assert hermes["securityContext"]["runAsGroup"] == 0
|
||||
for probe_name in ("startupProbe", "readinessProbe", "livenessProbe"):
|
||||
probe = hermes[probe_name]
|
||||
assert probe["exec"]["command"] == [
|
||||
"curl",
|
||||
"-fsS",
|
||||
"http://127.0.0.1:9119/api/status",
|
||||
]
|
||||
|
||||
terminal = containers["terminal"]
|
||||
command = terminal["args"][0]
|
||||
assert "--base-path /terminal" in command
|
||||
assert "--check-origin" not in command
|
||||
assert "/usr/bin/tmux new-session -A" in command
|
||||
assert "--continue" in command
|
||||
assert "--yolo" in command
|
||||
terminal_env = {item["name"]: item["value"] for item in terminal["env"]}
|
||||
assert terminal_env["HERMES_TUI_AGENT_INIT_TIMEOUT_S"] == "180"
|
||||
|
||||
claude_broker = containers["claude-broker"]
|
||||
claude_env = {item["name"]: item["value"] for item in claude_broker["env"]}
|
||||
assert claude_env["HERMES_CLAUDE_BROKER_CONCURRENCY"] == "2"
|
||||
assert claude_broker["readinessProbe"]["tcpSocket"] == {"port": "claude-broker"}
|
||||
assert claude_broker["livenessProbe"]["tcpSocket"] == {"port": "claude-broker"}
|
||||
|
||||
args = containers["oauth2-proxy"]["args"]
|
||||
terminal_upstream = "--upstream=http://127.0.0.1:7681/terminal/"
|
||||
dashboard_upstream = "--upstream=http://127.0.0.1:9119/"
|
||||
assert terminal_upstream in args
|
||||
assert dashboard_upstream in args
|
||||
assert args.index(terminal_upstream) < args.index(dashboard_upstream)
|
||||
assert "--pass-host-header=false" in args
|
||||
assert "--cookie-refresh=19m" in args
|
||||
assert "--session-store-type=redis" in args
|
||||
assert any(
|
||||
arg.startswith("--redis-connection-url=redis://hermes-oauth-sessions.")
|
||||
for arg in args
|
||||
)
|
||||
|
||||
patch_init = next(
|
||||
item for item in pod["initContainers"] if item["name"] == "patch-tui-gateway"
|
||||
)
|
||||
assert patch_init["command"][-1] == "/patched/server.py"
|
||||
for name in ("hermes", "terminal"):
|
||||
mounts = containers[name]["volumeMounts"]
|
||||
assert {
|
||||
"name": "tui-gateway-patch",
|
||||
"mountPath": "/opt/hermes/tui_gateway/server.py",
|
||||
"subPath": "server.py",
|
||||
} in mounts
|
||||
|
||||
ingress_documents = [
|
||||
item
|
||||
for item in yaml.safe_load_all((HERMES / "agent-ingress.yaml").read_text())
|
||||
if item
|
||||
]
|
||||
middlewares = {
|
||||
item["metadata"]["name"]: item
|
||||
for item in ingress_documents
|
||||
if item["kind"] == "Middleware"
|
||||
}
|
||||
assert middlewares["hermes-agent-terminal-slash"]["spec"]["redirectRegex"][
|
||||
"replacement"
|
||||
].endswith("/terminal/")
|
||||
assert (
|
||||
middlewares["hermes-agent-stock-dashboard-headers"]["spec"]["headers"][
|
||||
"customRequestHeaders"
|
||||
]["Origin"]
|
||||
== "http://127.0.0.1:9119"
|
||||
)
|
||||
ingresses = {
|
||||
item["metadata"]["name"]: item
|
||||
for item in ingress_documents
|
||||
if item["kind"] == "Ingress"
|
||||
}
|
||||
assert (
|
||||
ingresses["hermes-agent-dashboard"]["metadata"]["annotations"][
|
||||
"traefik.ingress.kubernetes.io/router.middlewares"
|
||||
]
|
||||
== "hermes-hermes-agent-stock-dashboard-headers@kubernetescrd"
|
||||
)
|
||||
assert (
|
||||
ingresses["hermes-agent-terminal"]["metadata"]["annotations"][
|
||||
"traefik.ingress.kubernetes.io/router.middlewares"
|
||||
]
|
||||
== "hermes-hermes-agent-terminal-slash@kubernetescrd"
|
||||
)
|
||||
@ -1,392 +0,0 @@
|
||||
"""Kanban claim and lifecycle contracts for Hermes CLI lanes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_cli_lanes_support import (
|
||||
lanes,
|
||||
)
|
||||
|
||||
|
||||
def test_unassigned_ready_task_is_persistently_routed_to_auto_lane(monkeypatch):
|
||||
task = SimpleNamespace(id="t_auto", assignee=None, status="ready")
|
||||
assigned = []
|
||||
|
||||
class Connection:
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
def assign_task(_conn, task_id, profile):
|
||||
assigned.append((task_id, profile))
|
||||
task.assignee = profile
|
||||
return True
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
list_boards=lambda include_archived=False: [{"slug": "cassandra"}],
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=lambda board: Connection(),
|
||||
recompute_ready=lambda _conn: None,
|
||||
list_tasks=lambda _conn: [task],
|
||||
assign_task=assign_task,
|
||||
get_task=lambda _conn, _task_id: task,
|
||||
claim_task=lambda _conn, _task_id, **_kwargs: task,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||||
|
||||
assert lanes.claim_ready(set(), 1) == [("cassandra", "t_auto")]
|
||||
assert assigned == [("t_auto", "cli-auto")]
|
||||
|
||||
|
||||
def test_corrupt_board_is_quarantined_without_stopping_healthy_lanes(
|
||||
monkeypatch, capsys
|
||||
):
|
||||
class CorruptBoardError(Exception):
|
||||
pass
|
||||
|
||||
task = SimpleNamespace(id="t_healthy", assignee="cli-auto", status="ready")
|
||||
|
||||
class Connection:
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
def connect(*, board):
|
||||
if board == "cassandra":
|
||||
raise CorruptBoardError("integrity_check failed")
|
||||
return Connection()
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
KanbanDbCorruptError=CorruptBoardError,
|
||||
list_boards=lambda include_archived=False: [
|
||||
{"slug": "cassandra"},
|
||||
{"slug": "healthy"},
|
||||
],
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=connect,
|
||||
recompute_ready=lambda _conn: None,
|
||||
list_tasks=lambda _conn: [task],
|
||||
claim_task=lambda _conn, _task_id, **_kwargs: task,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||||
lanes.BOARD_CORRUPTION_ERRORS.clear()
|
||||
|
||||
assert lanes.claim_ready(set(), 1) == [("healthy", "t_healthy")]
|
||||
assert "temporarily skipping Kanban board 'cassandra'" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_transient_board_scan_failure_does_not_stop_healthy_lanes(monkeypatch, capsys):
|
||||
task = SimpleNamespace(id="t_healthy", assignee="cli-auto", status="ready")
|
||||
|
||||
class Connection:
|
||||
def __init__(self, board):
|
||||
self.board = board
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
def recompute_ready(connection):
|
||||
if connection.board == "cassandra":
|
||||
raise lanes.sqlite3.OperationalError("disk I/O error")
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
list_boards=lambda include_archived=False: [
|
||||
{"slug": "cassandra"},
|
||||
{"slug": "healthy"},
|
||||
],
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=lambda board: Connection(board),
|
||||
recompute_ready=recompute_ready,
|
||||
list_tasks=lambda _conn: [task],
|
||||
claim_task=lambda _conn, _task_id, **_kwargs: task,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||||
lanes.BOARD_CORRUPTION_ERRORS.clear()
|
||||
|
||||
assert lanes.claim_ready(set(), 1) == [("healthy", "t_healthy")]
|
||||
error = capsys.readouterr().err
|
||||
assert "temporarily skipping Kanban board 'cassandra'" in error
|
||||
assert "storage OperationalError: disk I/O error" in error
|
||||
|
||||
|
||||
def test_board_call_retries_storage_faults_on_fresh_connections():
|
||||
connections = []
|
||||
|
||||
class Connection:
|
||||
def __init__(self):
|
||||
self.closed = False
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def connect(*, board):
|
||||
assert board == "cassandra"
|
||||
connection = Connection()
|
||||
connections.append(connection)
|
||||
return connection
|
||||
|
||||
attempts = []
|
||||
|
||||
def operation(_connection):
|
||||
attempts.append(1)
|
||||
if len(attempts) < 3:
|
||||
raise lanes.sqlite3.OperationalError("disk I/O error")
|
||||
return "healthy"
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=connect,
|
||||
)
|
||||
lanes.BOARD_CORRUPTION_ERRORS.clear()
|
||||
|
||||
assert lanes._board_call(fake_db, "cassandra", operation) == "healthy"
|
||||
assert len(connections) == 3
|
||||
assert all(connection.closed for connection in connections)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("result", "expected_action"),
|
||||
[
|
||||
(lanes.ProcessResult(0, "plain text only", None, False), "block"),
|
||||
(
|
||||
lanes.ProcessResult(
|
||||
0,
|
||||
"",
|
||||
{
|
||||
"status": "completed",
|
||||
"summary": "done",
|
||||
"changed_files": ["src/a.py"],
|
||||
"tests_run": ["pytest -q"],
|
||||
"artifacts": ["reports/result.json"],
|
||||
"blockers": [],
|
||||
},
|
||||
False,
|
||||
),
|
||||
"complete",
|
||||
),
|
||||
(
|
||||
lanes.ProcessResult(
|
||||
0,
|
||||
"",
|
||||
{
|
||||
"status": "completed",
|
||||
"summary": "The full test suite is still running.",
|
||||
"changed_files": ["src/a.py"],
|
||||
"tests_run": ["pytest -q — in progress"],
|
||||
"artifacts": [],
|
||||
"blockers": [],
|
||||
},
|
||||
False,
|
||||
),
|
||||
"block",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_claim_requires_structured_evidence_and_surfaces_artifacts(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
result,
|
||||
expected_action,
|
||||
):
|
||||
task = SimpleNamespace(
|
||||
id="t_worker",
|
||||
current_run_id=4,
|
||||
assignee="cli-auto",
|
||||
max_runtime_seconds=60,
|
||||
)
|
||||
calls = []
|
||||
heartbeats = []
|
||||
connections = []
|
||||
artifact = tmp_path / "reports/result.json"
|
||||
artifact.parent.mkdir()
|
||||
artifact.write_text("{}\n", encoding="utf-8")
|
||||
|
||||
class Connection:
|
||||
def __init__(self):
|
||||
self.closed = False
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def connect(*, board):
|
||||
assert board == "cassandra"
|
||||
connection = Connection()
|
||||
connections.append(connection)
|
||||
return connection
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=connect,
|
||||
get_task=lambda _conn, _task_id: task,
|
||||
worker_log_path=lambda _task_id, board: tmp_path / "worker.log",
|
||||
_resolve_worktree_workspace=lambda _task, board: (tmp_path, "wt/t_worker"),
|
||||
set_branch_name=lambda *_args: None,
|
||||
set_workspace_path=lambda *_args: None,
|
||||
build_worker_context=lambda *_args: "bounded objective",
|
||||
heartbeat_worker=lambda _conn, _task_id, *, note, expected_run_id: (
|
||||
heartbeats.append((note, expected_run_id)) or True
|
||||
),
|
||||
add_comment=lambda *_args: None,
|
||||
complete_task=lambda *_args, **kwargs: calls.append(("complete", kwargs)),
|
||||
block_task=lambda *_args, **kwargs: calls.append(("block", kwargs)),
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||||
monkeypatch.setattr(
|
||||
lanes, "state_path", lambda _board, _task_id: tmp_path / "state.json"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
lanes,
|
||||
"select_route",
|
||||
lambda *_args, **_kwargs: lanes.Route(
|
||||
"codex", "gpt-5.6-sol", "high", "codex-high", "jetson", "vote", 1, ()
|
||||
),
|
||||
)
|
||||
|
||||
def run_provider(*args, **_kwargs):
|
||||
assert connections[0].closed
|
||||
before_heartbeat = len(connections)
|
||||
assert args[6]("working") is True
|
||||
assert len(connections) == before_heartbeat + 1
|
||||
assert connections[-1].closed
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(lanes, "run_provider", run_provider)
|
||||
|
||||
lanes.execute_claim("cassandra", "t_worker")
|
||||
|
||||
assert calls[0][0] == expected_action
|
||||
assert heartbeats == [("working", 4)]
|
||||
if expected_action == "complete":
|
||||
assert calls[0][1]["metadata"]["artifacts"] == [str(artifact)]
|
||||
assert calls[0][1]["metadata"]["tests_run"] == ["pytest -q"]
|
||||
else:
|
||||
assert calls[0][1]["kind"] == "capability"
|
||||
assert all(connection.closed for connection in connections)
|
||||
|
||||
|
||||
def test_goal_card_continues_after_local_judge_rejects_progress(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
):
|
||||
task = SimpleNamespace(
|
||||
id="t_goal",
|
||||
current_run_id=12,
|
||||
assignee="cli-auto",
|
||||
max_runtime_seconds=300,
|
||||
goal_mode=True,
|
||||
goal_max_turns=3,
|
||||
)
|
||||
calls = []
|
||||
comments = []
|
||||
|
||||
class Connection:
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
scoped_current_board=lambda _board: nullcontext(),
|
||||
connect=lambda board: Connection(),
|
||||
get_task=lambda _conn, _task_id: task,
|
||||
worker_log_path=lambda _task_id, board: tmp_path / "worker.log",
|
||||
_resolve_worktree_workspace=lambda _task, board: (tmp_path, "wt/t_goal"),
|
||||
set_branch_name=lambda *_args: None,
|
||||
set_workspace_path=lambda *_args: None,
|
||||
build_worker_context=lambda *_args: "Run tests, commit, push, and verify remote HEAD.",
|
||||
heartbeat_worker=lambda *_args, **_kwargs: True,
|
||||
add_comment=lambda _conn, _task_id, _author, body: comments.append(body),
|
||||
complete_task=lambda *_args, **kwargs: calls.append(("complete", kwargs)),
|
||||
block_task=lambda *_args, **kwargs: calls.append(("block", kwargs)),
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
|
||||
monkeypatch.setattr(
|
||||
lanes,
|
||||
"state_path",
|
||||
lambda _board, _task_id: tmp_path / "state.json",
|
||||
)
|
||||
claude_low = lanes.Route(
|
||||
"claude", "claude-fable-5", "low", "claude-low", "jetson", "vote", 1, ()
|
||||
)
|
||||
codex_low = lanes.Route(
|
||||
"codex", "gpt-5.6-luna", "low", "codex-low", "manual", "fallback", 1, ()
|
||||
)
|
||||
codex_xhigh = lanes.Route(
|
||||
"codex", "gpt-5.6-sol", "xhigh", "codex-xhigh", "jetson", "escalated", 1, ()
|
||||
)
|
||||
route_calls = []
|
||||
|
||||
def select_route(_prompt, assignee, **kwargs):
|
||||
route_calls.append((assignee, kwargs))
|
||||
if assignee == "cli-codex-low":
|
||||
return codex_low
|
||||
if len(route_calls) == 1:
|
||||
return claude_low
|
||||
return codex_xhigh
|
||||
|
||||
monkeypatch.setattr(lanes, "select_route", select_route)
|
||||
monkeypatch.setattr(lanes, "fresh_unavailable_provider", lambda: None)
|
||||
reports = [
|
||||
lanes.ProcessResult(1, "authentication expired", None, True),
|
||||
lanes.ProcessResult(
|
||||
0,
|
||||
"first turn",
|
||||
{
|
||||
"status": "completed",
|
||||
"summary": "Focused tests passed.",
|
||||
"changed_files": ["src/a.py"],
|
||||
"tests_run": ["pytest focused: passed"],
|
||||
"artifacts": [],
|
||||
"blockers": [],
|
||||
},
|
||||
False,
|
||||
),
|
||||
lanes.ProcessResult(
|
||||
0,
|
||||
"second turn",
|
||||
{
|
||||
"status": "completed",
|
||||
"summary": "Full tests passed; commit pushed and remote HEAD verified.",
|
||||
"changed_files": ["src/a.py"],
|
||||
"tests_run": ["pytest full: passed"],
|
||||
"artifacts": [],
|
||||
"blockers": [],
|
||||
},
|
||||
False,
|
||||
),
|
||||
]
|
||||
monkeypatch.setattr(lanes, "run_provider", lambda *_args, **_kwargs: reports.pop(0))
|
||||
verdicts = iter(
|
||||
[
|
||||
(False, "commit, push, and remote verification are missing"),
|
||||
(True, "all explicit acceptance criteria have evidence"),
|
||||
]
|
||||
)
|
||||
judge_contexts = []
|
||||
|
||||
def judge_goal_completion(objective, *_args, **_kwargs):
|
||||
judge_contexts.append(objective)
|
||||
return next(verdicts)
|
||||
|
||||
monkeypatch.setattr(
|
||||
lanes.cli_lane_goal,
|
||||
"judge_goal_completion",
|
||||
judge_goal_completion,
|
||||
)
|
||||
|
||||
lanes.execute_claim("cassandra", "t_goal")
|
||||
|
||||
assert calls[0][0] == "complete"
|
||||
assert calls[0][1]["metadata"]["goal_turn"] == 2
|
||||
assert any(
|
||||
"Goal completion rejected; continuing turn 2/3" in item for item in comments
|
||||
)
|
||||
assert any(
|
||||
"Goal route 2/3: codex/gpt-5.6-sol at xhigh" in item for item in comments
|
||||
)
|
||||
assert route_calls[2][1]["exclude_provider"] == "claude"
|
||||
assert "prior rejected reports" in judge_contexts[1]
|
||||
assert "commit, push, and remote verification are missing" in judge_contexts[1]
|
||||
assert reports == []
|
||||
@ -1,79 +0,0 @@
|
||||
"""Shared loaders and manifest helpers for Hermes CLI-lane contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
SCRIPTS = ROOT / "services/hermes/scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
HERMES = ROOT / "services/hermes"
|
||||
KEYCLOAK = ROOT / "services/keycloak"
|
||||
FLUX_HERMES = ROOT / "clusters/atlas/flux-system/applications/hermes/kustomization.yaml"
|
||||
|
||||
|
||||
def _load(name: str):
|
||||
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
lanes = _load("cli_lane_runner")
|
||||
policy = _load("claude_command_policy")
|
||||
migration = _load("migrate_herdr_state")
|
||||
auth_patch = _load("patch_hermes_auth")
|
||||
tui_gateway_patch = _load("patch_tui_gateway")
|
||||
codex_runtime_patch = _load("patch_codex_runtime")
|
||||
ttyd_patch = _load("patch_ttyd_index")
|
||||
client_config = _load("configure_agent_clients")
|
||||
|
||||
|
||||
def _agent_deployment() -> dict:
|
||||
return yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
|
||||
|
||||
|
||||
def _services() -> dict[str, dict]:
|
||||
return {
|
||||
item["metadata"]["name"]: item
|
||||
for item in yaml.safe_load_all((HERMES / "service.yaml").read_text())
|
||||
if item
|
||||
}
|
||||
|
||||
|
||||
def _oauth_deployment(name: str) -> dict:
|
||||
documents = [
|
||||
item
|
||||
for item in yaml.safe_load_all((HERMES / "oauth2-proxy.yaml").read_text())
|
||||
if item
|
||||
]
|
||||
return next(
|
||||
item
|
||||
for item in documents
|
||||
if item["kind"] == "Deployment" and item["metadata"]["name"] == name
|
||||
)
|
||||
|
||||
|
||||
class _SwitchyardResponse:
|
||||
"""Minimal context-managed response used by routing contract tests."""
|
||||
|
||||
def __init__(self, selected: str, rationale: str = "local classifier vote"):
|
||||
self.headers = {
|
||||
"x-model-router-selected-model": selected,
|
||||
"x-model-router-rationale": rationale,
|
||||
}
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return b"{}"
|
||||
@ -1,386 +0,0 @@
|
||||
"""Operator toolchain and runtime-patch contracts for Hermes CLI lanes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_cli_lanes_support import (
|
||||
HERMES,
|
||||
SCRIPTS,
|
||||
_agent_deployment,
|
||||
auth_patch,
|
||||
client_config,
|
||||
codex_runtime_patch,
|
||||
tui_gateway_patch,
|
||||
ttyd_patch,
|
||||
)
|
||||
|
||||
|
||||
def test_worker_route_broker_accepts_pod_network_health_checks():
|
||||
"""Kubelet probes the pod IP, so the broker cannot bind to loopback only."""
|
||||
script = (SCRIPTS / "worker_route_broker.py").read_text()
|
||||
assert 'ThreadingHTTPServer(("0.0.0.0", PORT), Handler)' in script
|
||||
|
||||
|
||||
def test_switchyard_network_boundary_allows_vault_bootstrap():
|
||||
"""The pre-populate init container must reach Vault before routing starts."""
|
||||
documents = [
|
||||
item
|
||||
for item in yaml.safe_load_all((HERMES / "networkpolicy.yaml").read_text())
|
||||
if item
|
||||
]
|
||||
isolation = next(
|
||||
item
|
||||
for item in documents
|
||||
if item.get("metadata", {}).get("name") == "hermes-switchyard-isolation"
|
||||
)
|
||||
assert any(
|
||||
rule.get("to")
|
||||
== [
|
||||
{
|
||||
"namespaceSelector": {
|
||||
"matchLabels": {"kubernetes.io/metadata.name": "vault"}
|
||||
},
|
||||
"podSelector": {"matchLabels": {"app": "vault"}},
|
||||
}
|
||||
]
|
||||
and rule.get("ports") == [{"protocol": "TCP", "port": 8200}]
|
||||
for rule in isolation["spec"]["egress"]
|
||||
)
|
||||
|
||||
|
||||
def test_switchyard_network_boundary_allows_metrics_scraping():
|
||||
"""VictoriaMetrics may scrape Switchyard without widening its API boundary."""
|
||||
documents = [
|
||||
item
|
||||
for item in yaml.safe_load_all((HERMES / "networkpolicy.yaml").read_text())
|
||||
if item
|
||||
]
|
||||
isolation = next(
|
||||
item
|
||||
for item in documents
|
||||
if item.get("metadata", {}).get("name") == "hermes-switchyard-isolation"
|
||||
)
|
||||
assert any(
|
||||
rule.get("from")
|
||||
== [
|
||||
{
|
||||
"namespaceSelector": {
|
||||
"matchLabels": {"kubernetes.io/metadata.name": "monitoring"}
|
||||
},
|
||||
"podSelector": {"matchLabels": {"app": "server"}},
|
||||
}
|
||||
]
|
||||
and rule.get("ports") == [{"protocol": "TCP", "port": 9005}]
|
||||
for rule in isolation["spec"]["ingress"]
|
||||
)
|
||||
|
||||
|
||||
def test_owner_agent_installs_the_pinned_operator_toolchain():
|
||||
script = (SCRIPTS / "install_agent_tools.sh").read_text()
|
||||
for value in [
|
||||
"flux",
|
||||
"helm",
|
||||
"kustomize",
|
||||
"jq",
|
||||
"yq",
|
||||
"gh",
|
||||
"vault",
|
||||
"sops",
|
||||
"age",
|
||||
"age-keygen",
|
||||
"k9s",
|
||||
"terraform",
|
||||
"go",
|
||||
"gofmt",
|
||||
]:
|
||||
assert value in script
|
||||
assert "go1.26.5.linux-arm64.tar.gz" in script
|
||||
assert "fe4789e92b1f33358680864bbe8704289e7bb5fc207d80623c308935bd696d49" in script
|
||||
assert script.count("sha256sum -c -") == 1
|
||||
|
||||
deployment = _agent_deployment()
|
||||
installer = next(
|
||||
item
|
||||
for item in deployment["spec"]["template"]["spec"]["initContainers"]
|
||||
if item["name"] == "install-agent-tools"
|
||||
)
|
||||
assert "/bin/sh /opt/coordinator/install_agent_tools.sh" in installer["command"][2]
|
||||
assert any(mount["name"] == "coordinator" for mount in installer["volumeMounts"])
|
||||
init_config = next(
|
||||
item
|
||||
for item in deployment["spec"]["template"]["spec"]["initContainers"]
|
||||
if item["name"] == "init-config"
|
||||
)
|
||||
init_command = init_config["command"][2]
|
||||
assert "# Hermes managed operator PATH." in init_command
|
||||
assert "/opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin" in init_command
|
||||
assert 'chmod 0644 "${profile_file}"' in init_command
|
||||
|
||||
|
||||
def test_owner_agent_uses_only_the_canonical_hostname():
|
||||
paths = [
|
||||
HERMES / "agent-configmap.yaml",
|
||||
HERMES / "agent-deployment.yaml",
|
||||
HERMES / "agent-ingress.yaml",
|
||||
Path(__file__).parents[2] / "scripts/ops/hermes_triage_monitor.py",
|
||||
]
|
||||
for path in paths:
|
||||
content = path.read_text()
|
||||
assert "agent.bstein.dev" not in content
|
||||
assert "agent.hermes.bstein.dev" in content
|
||||
|
||||
|
||||
def test_agent_reconnect_retains_complete_history_and_long_tool_budget():
|
||||
configmap = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())
|
||||
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
||||
display = config["display"]
|
||||
assert display["resume_exchanges"] >= 10000
|
||||
assert display["resume_max_user_chars"] >= 10000000
|
||||
assert display["resume_max_assistant_chars"] >= 10000000
|
||||
assert config["agent"]["max_turns"] == 180
|
||||
assert config["delegation"]["max_iterations"] == 120
|
||||
|
||||
|
||||
def test_auth_patch_honors_explicit_shared_store(tmp_path: Path):
|
||||
source = tmp_path / "auth.py"
|
||||
destination = tmp_path / "patched/auth.py"
|
||||
source.write_text(
|
||||
'from pathlib import Path\nimport os\n\ndef _auth_file_path() -> Path:\n path = get_hermes_home() / "auth.json"\n return path\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
auth_patch.patch(source, destination)
|
||||
content = destination.read_text()
|
||||
assert 'os.environ.get("HERMES_AUTH_FILE"' in content
|
||||
|
||||
|
||||
def test_auth_patch_fails_closed_on_upstream_drift(tmp_path: Path):
|
||||
source = tmp_path / "auth.py"
|
||||
source.write_text("def changed():\n pass\n", encoding="utf-8")
|
||||
with pytest.raises(RuntimeError, match="context changed"):
|
||||
auth_patch.patch(source, tmp_path / "patched.py")
|
||||
|
||||
|
||||
def test_codex_runtime_patch_uses_cli_and_forwards_route(tmp_path: Path):
|
||||
provider = tmp_path / "runtime_provider.py"
|
||||
provider.write_text(codex_runtime_patch.PROVIDER_BEFORE, encoding="utf-8")
|
||||
provider_out = tmp_path / "patched/runtime_provider.py"
|
||||
codex_runtime_patch.patch_provider(provider, provider_out)
|
||||
assert '"api_mode": "codex_app_server"' in provider_out.read_text()
|
||||
|
||||
session = tmp_path / "codex_app_server_session.py"
|
||||
session.write_text(
|
||||
codex_runtime_patch.SESSION_SIGNATURE_BEFORE
|
||||
+ codex_runtime_patch.SESSION_REQUEST_BEFORE,
|
||||
encoding="utf-8",
|
||||
)
|
||||
session_out = tmp_path / "patched/codex_app_server_session.py"
|
||||
codex_runtime_patch.patch_session(session, session_out)
|
||||
session_content = session_out.read_text()
|
||||
assert 'turn_params["model"] = model' in session_content
|
||||
assert 'turn_params["effort"] = effort' in session_content
|
||||
assert '"approvalPolicy": "never"' in session_content
|
||||
assert '"sandboxPolicy": {"type": "dangerFullAccess"}' in session_content
|
||||
|
||||
turn = tmp_path / "codex_runtime.py"
|
||||
turn.write_text(
|
||||
codex_runtime_patch.FALLBACK_CONTEXT_BEFORE + codex_runtime_patch.TURN_BEFORE,
|
||||
encoding="utf-8",
|
||||
)
|
||||
turn_out = tmp_path / "patched/codex_runtime.py"
|
||||
codex_runtime_patch.patch_turn(turn, turn_out)
|
||||
turn_content = turn_out.read_text()
|
||||
assert "model=str(getattr(agent" in turn_content
|
||||
assert "build_cross_provider_codex_prompt" in turn_content
|
||||
|
||||
fallback = tmp_path / "chat_completion_helpers.py"
|
||||
fallback.write_text(
|
||||
codex_runtime_patch.FALLBACK_RESOLUTION_BEFORE,
|
||||
encoding="utf-8",
|
||||
)
|
||||
fallback_out = tmp_path / "patched/chat_completion_helpers.py"
|
||||
codex_runtime_patch.patch_fallback(fallback, fallback_out)
|
||||
fallback_content = fallback_out.read_text()
|
||||
assert 'agent.api_mode = "codex_app_server"' in fallback_content
|
||||
assert "agent._codex_cross_provider_fallback = True" in fallback_content
|
||||
|
||||
loop = tmp_path / "conversation_loop.py"
|
||||
loop.write_text(
|
||||
codex_runtime_patch.FALLBACK_DISPATCH_BEFORE
|
||||
+ codex_runtime_patch.RETRY_FALLBACK_DISPATCH_BEFORE
|
||||
+ codex_runtime_patch.STREAM_RECOVERY_BEFORE,
|
||||
encoding="utf-8",
|
||||
)
|
||||
loop_out = tmp_path / "patched/conversation_loop.py"
|
||||
codex_runtime_patch.patch_loop(loop, loop_out)
|
||||
loop_content = loop_out.read_text()
|
||||
assert loop_content.count('if agent.api_mode == "codex_app_server"') == 2
|
||||
assert "build_cross_provider_codex_prompt" in loop_content
|
||||
retry_dispatch = loop_content.index(
|
||||
"Fallback activation happens inside this retry loop"
|
||||
)
|
||||
api_kwargs = loop_content.find("agent._build_api_kwargs", retry_dispatch)
|
||||
assert api_kwargs == -1 or retry_dispatch < api_kwargs
|
||||
assert "Provider stream ended before a complete response" in loop_content
|
||||
assert "_is_transport_stub" in loop_content
|
||||
assert "rerouting ({truncated_tool_call_retries}/4)" in loop_content
|
||||
|
||||
auxiliary = tmp_path / "auxiliary_client.py"
|
||||
auxiliary.write_text(
|
||||
codex_runtime_patch.AUXILIARY_TOKEN_BEFORE,
|
||||
encoding="utf-8",
|
||||
)
|
||||
auxiliary_out = tmp_path / "patched/auxiliary_client.py"
|
||||
codex_runtime_patch.patch_auxiliary(auxiliary, auxiliary_out)
|
||||
auxiliary_content = auxiliary_out.read_text()
|
||||
assert 'os.environ.get("CODEX_HOME"' in auxiliary_content
|
||||
assert 'Path(codex_home).expanduser() / "auth.json"' in auxiliary_content
|
||||
assert "never creates a metered API-key lane" in auxiliary_content
|
||||
|
||||
|
||||
def test_agent_mounts_codex_auxiliary_runtime_patch():
|
||||
deployment = _agent_deployment()
|
||||
pod = deployment["spec"]["template"]["spec"]
|
||||
patch_init = next(
|
||||
item for item in pod["initContainers"] if item["name"] == "patch-codex-runtime"
|
||||
)
|
||||
assert patch_init["command"][-2:] == [
|
||||
"/opt/hermes/agent/auxiliary_client.py",
|
||||
"/patched/auxiliary_client.py",
|
||||
]
|
||||
expected_mount = {
|
||||
"name": "codex-runtime-patch",
|
||||
"mountPath": "/opt/hermes/agent/auxiliary_client.py",
|
||||
"subPath": "auxiliary_client.py",
|
||||
}
|
||||
containers = {item["name"]: item for item in pod["containers"]}
|
||||
for name in ("hermes", "terminal"):
|
||||
assert expected_mount in containers[name]["volumeMounts"]
|
||||
|
||||
|
||||
def test_codex_auxiliary_patch_reads_cli_token_without_copying_it(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
):
|
||||
codex_home = tmp_path / ".codex"
|
||||
codex_home.mkdir()
|
||||
(codex_home / "auth.json").write_text(
|
||||
json.dumps({"tokens": {"access_token": "cli-access-token"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
||||
|
||||
source = tmp_path / "auxiliary_client.py"
|
||||
source.write_text(
|
||||
"import json, logging, os, time\n"
|
||||
"from pathlib import Path\n"
|
||||
"logger = logging.getLogger(__name__)\n"
|
||||
"def read_token():\n"
|
||||
" try:\n"
|
||||
" raise RuntimeError('Hermes provider store intentionally empty')\n"
|
||||
+ codex_runtime_patch.AUXILIARY_TOKEN_BEFORE,
|
||||
encoding="utf-8",
|
||||
)
|
||||
destination = tmp_path / "patched/auxiliary_client.py"
|
||||
codex_runtime_patch.patch_auxiliary(source, destination)
|
||||
namespace: dict = {}
|
||||
exec(compile(destination.read_text(), str(destination), "exec"), namespace)
|
||||
|
||||
assert namespace["read_token"]() == "cli-access-token"
|
||||
|
||||
|
||||
def test_codex_runtime_patch_fails_closed_on_upstream_drift(tmp_path: Path):
|
||||
source = tmp_path / "runtime_provider.py"
|
||||
source.write_text("def changed():\n pass\n", encoding="utf-8")
|
||||
with pytest.raises(RuntimeError, match="context changed"):
|
||||
codex_runtime_patch.patch_provider(source, tmp_path / "patched.py")
|
||||
|
||||
|
||||
def test_codex_runtime_migration_uses_owner_unsafe_mode(tmp_path: Path):
|
||||
config = tmp_path / "config.yaml"
|
||||
config.write_text("model: {}\n", encoding="utf-8")
|
||||
codex_home = tmp_path / ".codex"
|
||||
calls = []
|
||||
|
||||
class Report:
|
||||
errors = []
|
||||
|
||||
@staticmethod
|
||||
def summary():
|
||||
return "configured"
|
||||
|
||||
def migrate(value, **kwargs):
|
||||
calls.append((value, kwargs))
|
||||
return Report()
|
||||
|
||||
client_config.configure_codex_runtime(config, migrate, codex_home)
|
||||
|
||||
assert calls[0][1]["default_permission_profile"] is None
|
||||
assert calls[0][1]["codex_home"] == codex_home
|
||||
content = (codex_home / "config.toml").read_text(encoding="utf-8")
|
||||
assert 'approval_policy = "never"' in content
|
||||
assert 'sandbox_mode = "danger-full-access"' in content
|
||||
assert "default_permissions" not in content
|
||||
|
||||
|
||||
def test_codex_owner_permissions_replace_stale_profile(tmp_path: Path):
|
||||
config = tmp_path / "config.toml"
|
||||
config.write_text(
|
||||
'default_permissions = ":danger-no-sandbox"\n\n[features]\nhooks = true\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
client_config.configure_codex_owner_permissions(config)
|
||||
client_config.configure_codex_owner_permissions(config)
|
||||
|
||||
content = config.read_text(encoding="utf-8")
|
||||
assert content.count(client_config.OWNER_PERMISSIONS_BEGIN) == 1
|
||||
assert content.count('approval_policy = "never"') == 1
|
||||
assert content.count('sandbox_mode = "danger-full-access"') == 1
|
||||
assert "default_permissions" not in content
|
||||
assert "[features]\nhooks = true" in content
|
||||
|
||||
|
||||
def test_tui_gateway_patch_extends_and_bounds_agent_startup(tmp_path: Path):
|
||||
source = tmp_path / "server.py"
|
||||
destination = tmp_path / "patched/server.py"
|
||||
source.write_text(
|
||||
"import os\n\n" + tui_gateway_patch.BEFORE + "\ndef unchanged():\n pass\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
tui_gateway_patch.patch(source, destination)
|
||||
|
||||
content = destination.read_text(encoding="utf-8")
|
||||
assert "HERMES_TUI_AGENT_INIT_TIMEOUT_S" in content
|
||||
assert "configured = 180.0" in content
|
||||
assert "return max(30.0, min(configured, 900.0))" in content
|
||||
assert "timeout: float | None = None" in content
|
||||
assert "ready.wait(timeout=wait_timeout)" in content
|
||||
assert "def unchanged():" in content
|
||||
|
||||
|
||||
def test_tui_gateway_patch_fails_closed_on_upstream_drift(tmp_path: Path):
|
||||
source = tmp_path / "server.py"
|
||||
source.write_text("def changed():\n pass\n", encoding="utf-8")
|
||||
with pytest.raises(RuntimeError, match="context changed"):
|
||||
tui_gateway_patch.patch(source, tmp_path / "patched.py")
|
||||
|
||||
|
||||
def test_ttyd_clipboard_and_reconnect_patch_remain_enabled():
|
||||
source = '<html><body><script>document.execCommand("copy")</script></body></html>'
|
||||
content = ttyd_patch.patch_html(source)
|
||||
assert 'id="atlas-ttyd-clipboard"' in content
|
||||
assert "navigator.clipboard.writeText(text)" in content
|
||||
assert "class AtlasRecoveringWebSocket" in content
|
||||
assert "window.location.reload()" in content
|
||||
assert "event.stopImmediatePropagation()" in content
|
||||
|
||||
|
||||
def test_ttyd_patch_fails_closed_on_upstream_drift():
|
||||
with pytest.raises(RuntimeError, match="context changed"):
|
||||
ttyd_patch.patch_html("<html><body>changed</body></html>")
|
||||
@ -2,20 +2,44 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_coordinator_support import (
|
||||
SCRIPT,
|
||||
_base_config,
|
||||
catalog_resolver,
|
||||
coordinator,
|
||||
routing,
|
||||
)
|
||||
|
||||
SCRIPT = Path(__file__).parents[2] / "services/hermes/scripts/hermes_coordinator.py"
|
||||
sys.path.insert(0, str(SCRIPT.parent))
|
||||
routing = importlib.import_module("hermes_model_routing")
|
||||
catalog_resolver = importlib.import_module("routing_catalog")
|
||||
SPEC = importlib.util.spec_from_file_location("hermes_coordinator", SCRIPT)
|
||||
assert SPEC and SPEC.loader
|
||||
coordinator = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = coordinator
|
||||
SPEC.loader.exec_module(coordinator)
|
||||
|
||||
|
||||
def _base_config() -> dict:
|
||||
"""Return the minimal coordinator configuration used by routing tests."""
|
||||
return {
|
||||
"model": {
|
||||
"provider": "openai-codex",
|
||||
"default": "gpt-5.6-terra",
|
||||
"model": "gpt-5.6-terra",
|
||||
},
|
||||
"fallback_providers": [
|
||||
{"provider": "anthropic", "model": "claude-opus-5"},
|
||||
dict(routing.LOCAL_FALLBACK),
|
||||
],
|
||||
"toolsets": ["kanban"],
|
||||
"terminal": {"cwd": "/opt/data/workspace"},
|
||||
}
|
||||
|
||||
|
||||
def test_model_version_and_quality_selection_handle_new_and_small_models():
|
||||
assert routing.model_version("claude-3-5-sonnet-20241022") == (3, 5)
|
||||
@ -310,3 +334,184 @@ def test_refresh_writes_non_secret_routing_status(tmp_path: Path, monkeypatch):
|
||||
assert status["projects"]["cassandra"]["state"] == "ready"
|
||||
assert status["projects"]["cassandra"]["board_state"] == {"state": "ready"}
|
||||
assert "do-not-report" not in status_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_corrupt_cassandra_board_is_preserved_without_blocking_refresh(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""A damaged project board degrades Kanban instead of stopping all services."""
|
||||
|
||||
class FakeKanbanDbCorruptError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("integrity check failed")
|
||||
self.db_path = tmp_path / "kanban.db"
|
||||
self.backup_path = tmp_path / "kanban.db.corrupt.backup"
|
||||
self.reason = "row out of order"
|
||||
|
||||
def raise_corruption(_root: Path) -> None:
|
||||
raise FakeKanbanDbCorruptError()
|
||||
|
||||
monkeypatch.setattr(coordinator, "bootstrap_cassandra", raise_corruption)
|
||||
monkeypatch.setattr(
|
||||
coordinator, "_kanban_corruption_type", lambda: FakeKanbanDbCorruptError
|
||||
)
|
||||
|
||||
status = coordinator.bootstrap_cassandra_state(tmp_path)
|
||||
|
||||
assert status == {
|
||||
"state": "corrupt-preserved",
|
||||
"database": str(tmp_path / "kanban.db"),
|
||||
"backup": str(tmp_path / "kanban.db.corrupt.backup"),
|
||||
"reason": "row out of order",
|
||||
}
|
||||
|
||||
|
||||
def test_unrelated_cassandra_bootstrap_failure_remains_fatal(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""Only the known fail-closed corruption state may be degraded."""
|
||||
|
||||
class FakeKanbanDbCorruptError(Exception):
|
||||
pass
|
||||
|
||||
def raise_permission_error(_root: Path) -> None:
|
||||
raise PermissionError("cannot access board")
|
||||
|
||||
monkeypatch.setattr(coordinator, "bootstrap_cassandra", raise_permission_error)
|
||||
monkeypatch.setattr(
|
||||
coordinator, "_kanban_corruption_type", lambda: FakeKanbanDbCorruptError
|
||||
)
|
||||
|
||||
try:
|
||||
coordinator.bootstrap_cassandra_state(tmp_path)
|
||||
except PermissionError as error:
|
||||
assert str(error) == "cannot access board"
|
||||
else:
|
||||
raise AssertionError("unrelated board errors must fail the coordinator refresh")
|
||||
|
||||
|
||||
def test_cassandra_workspace_uses_valid_configured_worktree(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""The coordinator selects an explicit Git worktree but rejects stale paths."""
|
||||
active = tmp_path / "cassandra-v69"
|
||||
active.mkdir()
|
||||
(active / ".git").write_text("gitdir: elsewhere\n", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_CASSANDRA_ACTIVE_WORKTREE", str(active))
|
||||
|
||||
assert coordinator.cassandra_workspace() == active
|
||||
|
||||
(active / ".git").unlink()
|
||||
assert coordinator.cassandra_workspace() == coordinator.CASSANDRA_BASE_PATH
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_remote", "repair_action"),
|
||||
[
|
||||
("https://scm.bstein.dev/bstein/cassandra.git", "set-url"),
|
||||
(None, "add"),
|
||||
(coordinator.CASSANDRA_REMOTE, None),
|
||||
],
|
||||
ids=["old-origin", "missing-origin", "canonical-origin"],
|
||||
)
|
||||
def test_cassandra_sync_repairs_existing_worktree(
|
||||
tmp_path: Path, monkeypatch, current_remote: str | None, repair_action: str | None
|
||||
):
|
||||
"""Existing worktrees use the canonical Atlas origin before fetching."""
|
||||
workspace = tmp_path / "cassandra"
|
||||
(workspace / ".git").mkdir(parents=True)
|
||||
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
|
||||
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
|
||||
token_path = tmp_path / "gitea-token"
|
||||
token_path.write_text("configured\n", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_GITEA_TOKEN_FILE", str(token_path))
|
||||
commands: list[list[str]] = []
|
||||
environments: list[dict[str, str]] = []
|
||||
|
||||
def run(command, **kwargs):
|
||||
commands.append(command)
|
||||
environments.append(kwargs.get("env", {}))
|
||||
if command[-2:] == ["get-url", "origin"]:
|
||||
return subprocess.CompletedProcess(
|
||||
command, 0 if current_remote else 2, stdout=f"{current_remote or ''}\n"
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 0)
|
||||
|
||||
monkeypatch.setattr(coordinator.subprocess, "run", run)
|
||||
assert coordinator.sync_cassandra_repo({"GITEA_TOKEN": "must-not-pass"}) == "ready"
|
||||
repair_commands = [command for command in commands if repair_action in command]
|
||||
assert bool(repair_commands) is bool(repair_action)
|
||||
assert commands[-1][-4:] == ["fetch", "--quiet", "--prune", "origin"]
|
||||
assert all("GITEA_TOKEN" not in environment for environment in environments)
|
||||
|
||||
|
||||
def test_cassandra_sync_repairs_origin_through_broker_without_token(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""Repository sync uses the credential-isolated broker remote."""
|
||||
workspace = tmp_path / "cassandra"
|
||||
(workspace / ".git").mkdir(parents=True)
|
||||
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
|
||||
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
|
||||
commands: list[list[str]] = []
|
||||
|
||||
def run(command, **_kwargs):
|
||||
commands.append(command)
|
||||
stdout = "old-origin\n" if command[-2:] == ["get-url", "origin"] else None
|
||||
return subprocess.CompletedProcess(command, 0, stdout=stdout)
|
||||
|
||||
monkeypatch.setattr(coordinator.subprocess, "run", run)
|
||||
state = coordinator.sync_cassandra_repo({})
|
||||
|
||||
assert state == "ready"
|
||||
assert any(
|
||||
"hermes-scm-broker.hermes-scm.svc.cluster.local" in " ".join(cmd)
|
||||
for cmd in commands
|
||||
)
|
||||
assert all("token" not in " ".join(cmd).lower() for cmd in commands)
|
||||
assert commands[-1][-4:] == ["fetch", "--quiet", "--prune", "origin"]
|
||||
|
||||
|
||||
def test_migrate_open_cassandra_tasks_preserves_running_and_done_tasks():
|
||||
"""A project switch moves queued work without relocating active evidence."""
|
||||
|
||||
class Task:
|
||||
def __init__(self, task_id: str, status: str):
|
||||
self.id = task_id
|
||||
self.status = status
|
||||
self.workspace_kind = "dir"
|
||||
self.workspace_path = str(coordinator.CASSANDRA_BASE_PATH)
|
||||
|
||||
class Closing:
|
||||
def __enter__(self):
|
||||
return object()
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return None
|
||||
|
||||
class FakeKanban:
|
||||
tasks = [
|
||||
Task("queued", "todo"),
|
||||
Task("active", "running"),
|
||||
Task("done", "done"),
|
||||
]
|
||||
moved: list[tuple[str, str]] = []
|
||||
|
||||
@staticmethod
|
||||
def connect_closing(*, board: str):
|
||||
assert board == "cassandra"
|
||||
return Closing()
|
||||
|
||||
@classmethod
|
||||
def list_tasks(cls, _connection, *, include_archived: bool):
|
||||
assert include_archived is False
|
||||
return cls.tasks
|
||||
|
||||
@classmethod
|
||||
def set_workspace_path(cls, _connection, task_id: str, path: Path):
|
||||
cls.moved.append((task_id, str(path)))
|
||||
|
||||
active = Path("/opt/data/workspace/projects/cassandra-hermes-v69")
|
||||
coordinator._migrate_open_cassandra_tasks(FakeKanban, active)
|
||||
|
||||
assert FakeKanban.moved == [("queued", str(active))]
|
||||
|
||||
@ -1,191 +0,0 @@
|
||||
"""Cassandra workspace and board contracts for the Hermes coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_coordinator_support import coordinator
|
||||
|
||||
|
||||
def test_corrupt_cassandra_board_is_preserved_without_blocking_refresh(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""A damaged project board degrades Kanban instead of stopping all services."""
|
||||
|
||||
class FakeKanbanDbCorruptError(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("integrity check failed")
|
||||
self.db_path = tmp_path / "kanban.db"
|
||||
self.backup_path = tmp_path / "kanban.db.corrupt.backup"
|
||||
self.reason = "row out of order"
|
||||
|
||||
def raise_corruption(_root: Path) -> None:
|
||||
raise FakeKanbanDbCorruptError()
|
||||
|
||||
monkeypatch.setattr(coordinator, "bootstrap_cassandra", raise_corruption)
|
||||
monkeypatch.setattr(
|
||||
coordinator, "_kanban_corruption_type", lambda: FakeKanbanDbCorruptError
|
||||
)
|
||||
|
||||
status = coordinator.bootstrap_cassandra_state(tmp_path)
|
||||
|
||||
assert status == {
|
||||
"state": "corrupt-preserved",
|
||||
"database": str(tmp_path / "kanban.db"),
|
||||
"backup": str(tmp_path / "kanban.db.corrupt.backup"),
|
||||
"reason": "row out of order",
|
||||
}
|
||||
|
||||
|
||||
def test_unrelated_cassandra_bootstrap_failure_remains_fatal(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""Only the known fail-closed corruption state may be degraded."""
|
||||
|
||||
class FakeKanbanDbCorruptError(Exception):
|
||||
pass
|
||||
|
||||
def raise_permission_error(_root: Path) -> None:
|
||||
raise PermissionError("cannot access board")
|
||||
|
||||
monkeypatch.setattr(coordinator, "bootstrap_cassandra", raise_permission_error)
|
||||
monkeypatch.setattr(
|
||||
coordinator, "_kanban_corruption_type", lambda: FakeKanbanDbCorruptError
|
||||
)
|
||||
|
||||
try:
|
||||
coordinator.bootstrap_cassandra_state(tmp_path)
|
||||
except PermissionError as error:
|
||||
assert str(error) == "cannot access board"
|
||||
else:
|
||||
raise AssertionError("unrelated board errors must fail the coordinator refresh")
|
||||
|
||||
|
||||
def test_cassandra_workspace_uses_valid_configured_worktree(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""The coordinator selects an explicit Git worktree but rejects stale paths."""
|
||||
active = tmp_path / "cassandra-v69"
|
||||
active.mkdir()
|
||||
(active / ".git").write_text("gitdir: elsewhere\n", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_CASSANDRA_ACTIVE_WORKTREE", str(active))
|
||||
|
||||
assert coordinator.cassandra_workspace() == active
|
||||
|
||||
(active / ".git").unlink()
|
||||
assert coordinator.cassandra_workspace() == coordinator.CASSANDRA_BASE_PATH
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_remote", "repair_action"),
|
||||
[
|
||||
("https://scm.bstein.dev/bstein/cassandra.git", "set-url"),
|
||||
(None, "add"),
|
||||
(coordinator.CASSANDRA_REMOTE, None),
|
||||
],
|
||||
ids=["old-origin", "missing-origin", "canonical-origin"],
|
||||
)
|
||||
def test_cassandra_sync_repairs_existing_worktree(
|
||||
tmp_path: Path, monkeypatch, current_remote: str | None, repair_action: str | None
|
||||
):
|
||||
"""Existing worktrees use the canonical Atlas origin before fetching."""
|
||||
workspace = tmp_path / "cassandra"
|
||||
(workspace / ".git").mkdir(parents=True)
|
||||
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
|
||||
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
|
||||
token_path = tmp_path / "gitea-token"
|
||||
token_path.write_text("configured\n", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_GITEA_TOKEN_FILE", str(token_path))
|
||||
commands: list[list[str]] = []
|
||||
environments: list[dict[str, str]] = []
|
||||
|
||||
def run(command, **kwargs):
|
||||
commands.append(command)
|
||||
environments.append(kwargs.get("env", {}))
|
||||
if command[-2:] == ["get-url", "origin"]:
|
||||
return subprocess.CompletedProcess(
|
||||
command, 0 if current_remote else 2, stdout=f"{current_remote or ''}\n"
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 0)
|
||||
|
||||
monkeypatch.setattr(coordinator.subprocess, "run", run)
|
||||
assert coordinator.sync_cassandra_repo({"GITEA_TOKEN": "must-not-pass"}) == "ready"
|
||||
repair_commands = [command for command in commands if repair_action in command]
|
||||
assert bool(repair_commands) is bool(repair_action)
|
||||
assert commands[-1][-4:] == ["fetch", "--quiet", "--prune", "origin"]
|
||||
assert all("GITEA_TOKEN" not in environment for environment in environments)
|
||||
|
||||
|
||||
def test_cassandra_sync_repairs_origin_through_broker_without_token(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
"""Repository sync uses the credential-isolated broker remote."""
|
||||
workspace = tmp_path / "cassandra"
|
||||
(workspace / ".git").mkdir(parents=True)
|
||||
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
|
||||
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
|
||||
commands: list[list[str]] = []
|
||||
|
||||
def run(command, **_kwargs):
|
||||
commands.append(command)
|
||||
stdout = "old-origin\n" if command[-2:] == ["get-url", "origin"] else None
|
||||
return subprocess.CompletedProcess(command, 0, stdout=stdout)
|
||||
|
||||
monkeypatch.setattr(coordinator.subprocess, "run", run)
|
||||
state = coordinator.sync_cassandra_repo({})
|
||||
|
||||
assert state == "ready"
|
||||
assert any(
|
||||
"hermes-scm-broker.hermes-scm.svc.cluster.local" in " ".join(cmd)
|
||||
for cmd in commands
|
||||
)
|
||||
assert all("token" not in " ".join(cmd).lower() for cmd in commands)
|
||||
assert commands[-1][-4:] == ["fetch", "--quiet", "--prune", "origin"]
|
||||
|
||||
|
||||
def test_migrate_open_cassandra_tasks_preserves_running_and_done_tasks():
|
||||
"""A project switch moves queued work without relocating active evidence."""
|
||||
|
||||
class Task:
|
||||
def __init__(self, task_id: str, status: str):
|
||||
self.id = task_id
|
||||
self.status = status
|
||||
self.workspace_kind = "dir"
|
||||
self.workspace_path = str(coordinator.CASSANDRA_BASE_PATH)
|
||||
|
||||
class Closing:
|
||||
def __enter__(self):
|
||||
return object()
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return None
|
||||
|
||||
class FakeKanban:
|
||||
tasks = [
|
||||
Task("queued", "todo"),
|
||||
Task("active", "running"),
|
||||
Task("done", "done"),
|
||||
]
|
||||
moved: list[tuple[str, str]] = []
|
||||
|
||||
@staticmethod
|
||||
def connect_closing(*, board: str):
|
||||
assert board == "cassandra"
|
||||
return Closing()
|
||||
|
||||
@classmethod
|
||||
def list_tasks(cls, _connection, *, include_archived: bool):
|
||||
assert include_archived is False
|
||||
return cls.tasks
|
||||
|
||||
@classmethod
|
||||
def set_workspace_path(cls, _connection, task_id: str, path: Path):
|
||||
cls.moved.append((task_id, str(path)))
|
||||
|
||||
active = Path("/opt/data/workspace/projects/cassandra-hermes-v69")
|
||||
coordinator._migrate_open_cassandra_tasks(FakeKanban, active)
|
||||
|
||||
assert FakeKanban.moved == [("queued", str(active))]
|
||||
@ -1,35 +0,0 @@
|
||||
"""Shared loader and configuration for Hermes coordinator contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).parents[2] / "services/hermes/scripts/hermes_coordinator.py"
|
||||
sys.path.insert(0, str(SCRIPT.parent))
|
||||
routing = importlib.import_module("hermes_model_routing")
|
||||
catalog_resolver = importlib.import_module("routing_catalog")
|
||||
SPEC = importlib.util.spec_from_file_location("hermes_coordinator", SCRIPT)
|
||||
assert SPEC and SPEC.loader
|
||||
coordinator = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = coordinator
|
||||
SPEC.loader.exec_module(coordinator)
|
||||
|
||||
|
||||
def _base_config() -> dict:
|
||||
"""Return the minimal coordinator configuration used by routing tests."""
|
||||
return {
|
||||
"model": {
|
||||
"provider": "openai-codex",
|
||||
"default": "gpt-5.6-terra",
|
||||
"model": "gpt-5.6-terra",
|
||||
},
|
||||
"fallback_providers": [
|
||||
{"provider": "anthropic", "model": "claude-opus-5"},
|
||||
dict(routing.LOCAL_FALLBACK),
|
||||
],
|
||||
"toolsets": ["kanban"],
|
||||
"terminal": {"cwd": "/opt/data/workspace"},
|
||||
}
|
||||
@ -1,12 +1,82 @@
|
||||
"""Route and request-boundary contracts for the Atlas Gitea client."""
|
||||
"""Adversarial contracts for the Atlas-only draft pull-request client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
from email.message import Message
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_gitea_support import HEAD_SHA, Response, _load
|
||||
ROOT = Path(__file__).parents[2]
|
||||
CLIENT_PATH = ROOT / "services/hermes/scm-common/scripts/gitea_api.py"
|
||||
HEAD_SHA = "465cf9146b05c174a2a8d310aff6c64be58277b6"
|
||||
if str(CLIENT_PATH.parent) not in sys.path:
|
||||
sys.path.insert(0, str(CLIENT_PATH.parent))
|
||||
|
||||
|
||||
def _load():
|
||||
spec = importlib.util.spec_from_file_location("safe_gitea_api", CLIENT_PATH)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _draft_payload(**updates):
|
||||
payload = {
|
||||
"base": "main",
|
||||
"body": "Review evidence",
|
||||
"head": "hermes/review-fix",
|
||||
"title": "WIP: Repair review findings",
|
||||
}
|
||||
payload.update(updates)
|
||||
return payload
|
||||
|
||||
|
||||
def _draft_response(**updates):
|
||||
response = {
|
||||
"number": 3,
|
||||
"state": "open",
|
||||
"draft": True,
|
||||
"merged": False,
|
||||
"html_url": "https://scm.bstein.dev/atlas/cassandra/pulls/3",
|
||||
"url": "https://scm.bstein.dev/atlas/cassandra/pulls/3",
|
||||
"title": "WIP: Focused fix",
|
||||
"body": "Review evidence",
|
||||
"base": {"ref": "main", "repo": {"full_name": "atlas/cassandra"}},
|
||||
"head": {
|
||||
"ref": "hermes/fix",
|
||||
"sha": HEAD_SHA,
|
||||
"repo": {"full_name": "atlas/cassandra"},
|
||||
},
|
||||
}
|
||||
response.update(updates)
|
||||
return response
|
||||
|
||||
|
||||
class Response:
|
||||
def __init__(self, body: object, status: int | None = None):
|
||||
self.body = body if isinstance(body, bytes) else json.dumps(body).encode()
|
||||
self.status = status if status is not None else (201 if isinstance(body, dict) else 200)
|
||||
self.headers = Message()
|
||||
self.headers["Content-Type"] = "application/json"
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def read(self, limit=-1):
|
||||
return self.body if limit < 0 else self.body[:limit]
|
||||
|
||||
|
||||
def test_redirect_handler_rejects_cross_origin_with_sentinel_authorization():
|
||||
client = _load()
|
||||
@ -287,3 +357,682 @@ def test_maximum_bounded_pr_number_is_readable():
|
||||
== b"{}"
|
||||
)
|
||||
assert called is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "path", "data"),
|
||||
[
|
||||
("DELETE", "/api/v1/repos/atlas/cassandra/pulls/4", None),
|
||||
("POST", "/api/v1/repos/atlas/cassandra/pulls/4/merge", {}),
|
||||
(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls/4/reviews",
|
||||
{"event": "APPROVED"},
|
||||
),
|
||||
("PATCH", "/api/v1/repos/atlas/cassandra/pulls/4", {"title": "WIP: x"}),
|
||||
("PUT", "/api/v1/repos/atlas/cassandra/branches/main", {}),
|
||||
],
|
||||
)
|
||||
def test_merge_approve_close_delete_update_and_other_mutations_are_rejected(
|
||||
method: str, path: str, data: object
|
||||
):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError):
|
||||
client.build_request(
|
||||
method, path, base_url=client.CANONICAL_BASE_URL, token="secret", data=data
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ref",
|
||||
[
|
||||
"foo/.bar",
|
||||
"foo/bar.lock/baz",
|
||||
"foo..bar",
|
||||
"foo@{bar",
|
||||
"foo//bar",
|
||||
"-danger",
|
||||
"danger.",
|
||||
"danger~one",
|
||||
"danger^one",
|
||||
"danger:one",
|
||||
"danger one",
|
||||
],
|
||||
)
|
||||
def test_complete_git_ref_validation_rejects_invalid_names(ref: str):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError):
|
||||
client._validate_ref(ref, "head")
|
||||
|
||||
|
||||
def test_git_ref_validation_uses_fixed_trusted_binary():
|
||||
client = _load()
|
||||
|
||||
assert client._validate_ref.__globals__["GIT_BIN"] == "/usr/bin/git"
|
||||
assert client._validate_ref("hermes/valid-fix", "head") == "hermes/valid-fix"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["base", "head"])
|
||||
@pytest.mark.parametrize("oversized", ["r" * 100_000, "🧪" * 128])
|
||||
def test_oversized_ref_never_invokes_git_request_or_opener(
|
||||
field: str, oversized: str, monkeypatch
|
||||
):
|
||||
client = _load()
|
||||
git_called = False
|
||||
request_built = False
|
||||
opener_called = False
|
||||
original_build_request = client.build_request
|
||||
|
||||
def git_run(*_args, **_kwargs):
|
||||
nonlocal git_called
|
||||
git_called = True
|
||||
raise AssertionError("Git must not receive an oversized ref")
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
client.build_request = build_request
|
||||
refs = {"base": "main", "head": "hermes/fix"}
|
||||
refs[field] = oversized
|
||||
with pytest.raises(client.PolicyError, match="branch-name limit"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base=refs["base"],
|
||||
head=refs["head"],
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=opener,
|
||||
)
|
||||
assert git_called is False
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "oversized"),
|
||||
[("repo", "r" * 101), ("title", "🧪" * 200), ("body", "🧪" * 9_000)],
|
||||
)
|
||||
def test_other_text_bounds_fail_before_git_request_or_opener(
|
||||
field: str, oversized: str, monkeypatch
|
||||
):
|
||||
client = _load()
|
||||
git_called = False
|
||||
request_built = False
|
||||
opener_called = False
|
||||
original_build_request = client.build_request
|
||||
|
||||
def git_run(*_args, **_kwargs):
|
||||
nonlocal git_called
|
||||
git_called = True
|
||||
raise AssertionError("Git must not run before cheap input bounds")
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
client.build_request = build_request
|
||||
values = {
|
||||
"repo": "cassandra",
|
||||
"title": "Focused fix",
|
||||
"body": "Review evidence",
|
||||
}
|
||||
values[field] = oversized
|
||||
with pytest.raises(client.PolicyError):
|
||||
client.create_draft(
|
||||
values["repo"],
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token="runtime",
|
||||
opener=opener,
|
||||
)
|
||||
assert git_called is False
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
def test_create_forces_draft_title_and_same_repository_branch_names():
|
||||
client = _load()
|
||||
assert (
|
||||
client.authorize_request(
|
||||
"POST", "/api/v1/repos/atlas/cassandra/pulls", _draft_payload()
|
||||
)
|
||||
== "create-draft"
|
||||
)
|
||||
with pytest.raises(client.PolicyError, match="draft-title prefix"):
|
||||
client.authorize_request(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls",
|
||||
_draft_payload(title="Not a draft"),
|
||||
)
|
||||
with pytest.raises(client.PolicyError):
|
||||
client.authorize_request(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls",
|
||||
_draft_payload(head="someone:branch"),
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_token_is_only_an_authorization_header():
|
||||
client = _load()
|
||||
request = client.build_request(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls",
|
||||
base_url=client.CANONICAL_BASE_URL,
|
||||
token="do-not-leak",
|
||||
data=_draft_payload(),
|
||||
)
|
||||
|
||||
assert "do-not-leak" not in request.full_url
|
||||
assert b"do-not-leak" not in request.data
|
||||
assert request.get_header("Authorization") == "token do-not-leak"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sensitive",
|
||||
[
|
||||
"client_" + "secret=not-a-real-value",
|
||||
'{"client_' + 'secret":\n"synthetic-value"}',
|
||||
'{"client_' + 'secret"\n:\n"synthetic-value"}',
|
||||
'{"client\\u005f' + 'secret":"synthetic-value"}',
|
||||
"client_" + "secret:\n synthetic-value",
|
||||
'{"client\\q' + 'secret":"synthetic-value"}',
|
||||
'{"client\n' + 'secret":"synthetic-value"}',
|
||||
'"client_' + 'secret"\x0b:\n"synthetic-value"',
|
||||
"ACCESS_" + "TOKEN = 'not-a-real-value'",
|
||||
'{"refresh_' + 'token": "not-a-real-value"}',
|
||||
"private_" + "key: not-a-real-value",
|
||||
"AWS_SECRET_ACCESS_" + "KEY=not-a-real-value",
|
||||
"aws_access_key_" + "id: not-a-real-value",
|
||||
"AWS_SESSION_" + "TOKEN = 'not-a-real-value'",
|
||||
'{"AccessKey' + 'Id":"not-a-real-value"}',
|
||||
'{"SecretAccess' + 'Key":"not-a-real-value"}',
|
||||
'{"Session' + 'Token":"not-a-real-value"}',
|
||||
"aws-security-" + "token: not-a-real-value",
|
||||
"Account" + "Key=not-a-real-value",
|
||||
"SharedAccess" + "Signature: not-a-real-value",
|
||||
"AZURE_STORAGE_CONNECTION_" + "STRING='not-a-real-value'",
|
||||
"DefaultEndpointsProtocol=https;AccountName=fake;Account"
|
||||
+ "Key=not-a-real-value;EndpointSuffix=example",
|
||||
"?sv=2024-11-04&ss=b&srt=sco&sp=rwdlac&se=2099-01-01&sig=" + "not-a-real-value",
|
||||
"DOCKER_AUTH_" + "CONFIG='not-a-real-value'",
|
||||
'{"auths":{"registry.example":{"auth":"bm90LXJlYWw="}}}',
|
||||
'{"identity' + 'token":"not-a-real-value"}',
|
||||
'{"type":"service_' + 'account","client_email":"fake@example.test"}',
|
||||
'{"private_key_' + 'id":"not-a-real-value"}',
|
||||
'{"client_' + 'email":"fake@example.test"}',
|
||||
"GOOGLE_CREDENTIALS" + "=not-a-real-value",
|
||||
"personal_access_" + "token: not-a-real-value",
|
||||
"GITEA_" + "TOKEN=not-a-real-value",
|
||||
"gitlab-token" + ": not-a-real-value",
|
||||
"pat" + "=not-a-real-value",
|
||||
"Authorization: " + "Bearer not-a-real-credential-value",
|
||||
"authorization = " + '"Basic not-a-real-credential-value"',
|
||||
"Bearer" + "=not-a-real-credential-value",
|
||||
"Basic" + ": not-a-real-credential-value",
|
||||
"pass" + "word=not-a-real-credential",
|
||||
"ghp_" + "notarealcredentialvalue123456",
|
||||
"github_pat_" + "notarealcredentialvalue123456",
|
||||
"glpat-" + "notarealcredentialvalue123456",
|
||||
"xoxb-" + "not-a-real-credential-value-123456",
|
||||
"sk-ant-" + "notarealcredentialvalue123456",
|
||||
"sk-proj-" + "notarealcredentialvalue123456",
|
||||
"sk_live_" + "notarealcredentialvalue123456",
|
||||
"ya29." + "notarealcredentialvalue123456",
|
||||
"gta_" + "notarealcredentialvalue123456",
|
||||
"whsec_" + "notarealcredentialvalue123456",
|
||||
"npm_" + "notarealcredentialvalue123456",
|
||||
"pypi-" + "notarealcredentialvalue123456789012345",
|
||||
"hf_" + "notarealcredentialvalue123456",
|
||||
"SG." + "notarealvalue1234" + ".notarealcredentialvalue123456",
|
||||
"SK" + "a" * 32,
|
||||
"https://hooks.slack.com/services/" + "T000/B000/notarealvalue123456",
|
||||
"https://discord.com/api/webhooks/123456789/" + "notarealcredentialvalue123456",
|
||||
"https://fake.webhook.office.com/" + "notarealcredentialvalue123456",
|
||||
"webhook_" + "url=https://example.test/not-real",
|
||||
"FutureCloudSigning" + "Credential=not-a-real-value",
|
||||
"future-client-signing-" + "key: not-a-real-value",
|
||||
"future_client_signing_" + "key='not-a-real-value'",
|
||||
'{"serviceAccountPrivate' + 'Key":"not-a-real-value"}',
|
||||
"CONTAINER_REGISTRY_" + "CREDENTIAL=not-a-real-value",
|
||||
"someWebhookSigning" + "Secret: not-a-real-value",
|
||||
"client" + "Key=not-a-real-value",
|
||||
"session" + "Key: not-a-real-value",
|
||||
"access" + "Id=not-a-real-value",
|
||||
"credentials" + ": {user: fake}",
|
||||
"private" + "Key: |",
|
||||
"nuget_api_" + "key=not-a-real-value",
|
||||
"oy2" + "a" * 44,
|
||||
"sk_test_" + "notarealcredentialvalue123456",
|
||||
"A1b2C3d4E5f6G7h8I9j0K_l-M+n/O=pQ2rS3tU4vW5xY6zZ7aB8cC9d",
|
||||
"AKIA" + "A" * 16,
|
||||
"AIza" + "a" * 35,
|
||||
"-----BEGIN OPENSSH " + "PRIVATE KEY-----",
|
||||
"ssh-ed25519 " + "bm90YXJlYWxjcmVkZW50aWFsdmFsdWU=",
|
||||
"eyJnotarealheader." + "notarealpayloadvalue." + "notarealsignature",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("field", ["title", "body"])
|
||||
def test_create_rejects_expanded_credential_shapes_before_network(
|
||||
sensitive: str, field: str
|
||||
):
|
||||
client = _load()
|
||||
called = False
|
||||
request_built = False
|
||||
|
||||
original_build_request = client.build_request
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
client.build_request = build_request
|
||||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||||
values[field] = sensitive
|
||||
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token="runtime-sentinel",
|
||||
opener=opener,
|
||||
)
|
||||
assert request_built is False
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_very_long_compact_body_is_rejected_before_request_or_network():
|
||||
client = _load()
|
||||
request_built = False
|
||||
opener_called = False
|
||||
original_build_request = client.build_request
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
client.build_request = build_request
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="a" * 300,
|
||||
token="runtime-sentinel",
|
||||
opener=opener,
|
||||
)
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"safe_text",
|
||||
[
|
||||
"AWS_SECRET_ACCESS_KEY is injected at runtime",
|
||||
"Token: reject empty values",
|
||||
"Authorization = preserve header behavior",
|
||||
"Password: add regression",
|
||||
"Review the Authorization header behavior",
|
||||
"Bearer authentication is required for this route",
|
||||
"Document Docker auths payload rejection",
|
||||
"The client_email field belongs to service accounts",
|
||||
"AccountKey assignments must be blocked",
|
||||
"This patch changes token validation without including a value",
|
||||
"FutureCloudSigningCredential handling needs a regression test",
|
||||
"The clientKey name is documented without an assigned value",
|
||||
"A SHA-256 digest 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef is evidence",
|
||||
],
|
||||
)
|
||||
def test_credential_policy_keeps_normal_engineering_prose_usable(safe_text: str):
|
||||
client = _load()
|
||||
|
||||
assert client._validate_body(safe_text) == safe_text
|
||||
assert client._draft_title(safe_text) == f"WIP: {safe_text}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["title", "body"])
|
||||
def test_create_rejects_exact_runtime_token_before_network(field: str):
|
||||
client = _load()
|
||||
called = False
|
||||
request_built = False
|
||||
|
||||
original_build_request = client.build_request
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
client.build_request = build_request
|
||||
runtime_token = "exact-random-runtime-sentinel-7b73ac61"
|
||||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||||
values[field] = f"Accidental {runtime_token} value"
|
||||
with pytest.raises(client.PolicyError, match="runtime credential"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token=runtime_token,
|
||||
opener=opener,
|
||||
)
|
||||
assert request_built is False
|
||||
assert called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["repo", "base", "head", "title", "body"])
|
||||
def test_every_public_field_rejects_exact_runtime_token_before_git_or_network(
|
||||
field: str, monkeypatch
|
||||
):
|
||||
client = _load()
|
||||
git_called = False
|
||||
request_built = False
|
||||
opener_called = False
|
||||
runtime_token = "runtime-sentinel"
|
||||
values = {
|
||||
"repo": "cassandra",
|
||||
"base": "main",
|
||||
"head": "hermes/fix",
|
||||
"title": "Focused fix",
|
||||
"body": "Review evidence",
|
||||
}
|
||||
values[field] = runtime_token
|
||||
|
||||
def git_run(*_args, **_kwargs):
|
||||
nonlocal git_called
|
||||
git_called = True
|
||||
raise AssertionError("Git must not run for a credential-bearing field")
|
||||
|
||||
def build_request(*_args, **_kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
raise AssertionError("a request must not be built")
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
raise AssertionError("the opener must not be called")
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
monkeypatch.setattr(client, "build_request", build_request)
|
||||
with pytest.raises(client.PolicyError, match="runtime credential"):
|
||||
client.create_draft(
|
||||
values["repo"],
|
||||
base=values["base"],
|
||||
head=values["head"],
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token=runtime_token,
|
||||
opener=opener,
|
||||
)
|
||||
assert git_called is False
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sensitive",
|
||||
[
|
||||
" ".join(["{}"] * 32) + ' {"client_secret":"synthetic-value"}',
|
||||
"prefix_" + "ghp_" + "notarealcredentialvalue123456_suffix",
|
||||
"client_secret: correct horse battery staple",
|
||||
"'client_secret':\n synthetic-value",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("field", ["title", "body"])
|
||||
def test_structured_scanner_closes_bounded_and_wrapped_token_bypasses(
|
||||
sensitive: str, field: str
|
||||
):
|
||||
client = _load()
|
||||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||||
values[field] = sensitive
|
||||
called = False
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token="runtime-sentinel",
|
||||
opener=opener,
|
||||
)
|
||||
assert called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [200, 202, 204, 206])
|
||||
def test_create_accepts_only_http_201(status: int):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="unexpected HTTP status"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(_draft_response(), status=status),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [201, 202, 204, 206])
|
||||
def test_read_accepts_only_http_200(status: int):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="unexpected HTTP status"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(b"{}", status=status),
|
||||
)
|
||||
|
||||
|
||||
def test_successful_create_validates_each_ref_once(monkeypatch):
|
||||
client = _load()
|
||||
calls = []
|
||||
|
||||
def git_run(command, **_kwargs):
|
||||
calls.append(command)
|
||||
return type("Result", (), {"returncode": 0})()
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(_draft_response()),
|
||||
)
|
||||
|
||||
assert [command[-1] for command in calls] == [
|
||||
"refs/heads/main",
|
||||
"refs/heads/hermes/fix",
|
||||
]
|
||||
|
||||
|
||||
def test_create_verifies_every_server_postcondition():
|
||||
client = _load()
|
||||
calls = []
|
||||
|
||||
def opener(request, timeout):
|
||||
calls.append((request, timeout))
|
||||
return Response(_draft_response())
|
||||
|
||||
result = client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=opener,
|
||||
)
|
||||
|
||||
assert json.loads(result) == _draft_response()
|
||||
payload = json.loads(calls[0][0].data)
|
||||
assert payload == {
|
||||
"base": "main",
|
||||
"body": "Review evidence",
|
||||
"head": "hermes/fix",
|
||||
"title": "WIP: Focused fix",
|
||||
}
|
||||
assert calls[0][1] == 30
|
||||
|
||||
|
||||
def test_create_postcondition_rejects_every_material_mismatch():
|
||||
client = _load()
|
||||
mutations = [
|
||||
("number", 0),
|
||||
("number", 2_147_483_648),
|
||||
("state", "closed"),
|
||||
("draft", False),
|
||||
("merged", True),
|
||||
("html_url", "https://evil.example/pulls/3"),
|
||||
("url", "https://evil.example/api/pulls/3"),
|
||||
("title", "Focused fix"),
|
||||
("body", "different"),
|
||||
]
|
||||
documents = []
|
||||
for key, value in mutations:
|
||||
document = _draft_response()
|
||||
document[key] = value
|
||||
documents.append(document)
|
||||
for path, value in [
|
||||
(("base", "ref"), "master"),
|
||||
(("base", "repo", "full_name"), "evil/cassandra"),
|
||||
(("head", "ref"), "other"),
|
||||
(("head", "sha"), "0" * 40),
|
||||
(("head", "repo", "full_name"), "evil/cassandra"),
|
||||
]:
|
||||
document = copy.deepcopy(_draft_response())
|
||||
target = document
|
||||
for key in path[:-1]:
|
||||
target = target[key]
|
||||
target[path[-1]] = value
|
||||
documents.append(document)
|
||||
|
||||
for document in documents:
|
||||
with pytest.raises(client.PolicyError):
|
||||
client._require_create_response(
|
||||
json.dumps(document).encode(),
|
||||
repo="cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="WIP: Focused fix",
|
||||
body="Review evidence",
|
||||
)
|
||||
|
||||
|
||||
def test_read_response_is_bounded():
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="safe size limit"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(b"x" * (client.MAX_RESPONSE_BYTES + 1)),
|
||||
)
|
||||
|
||||
|
||||
def test_direct_api_rejects_unexpected_success_content_type():
|
||||
client = _load()
|
||||
response = Response(b"{}")
|
||||
response.headers.replace_header("Content-Type", "text/html")
|
||||
|
||||
with pytest.raises(client.PolicyError, match="unexpected response type"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: response,
|
||||
)
|
||||
|
||||
|
||||
def test_output_redaction_covers_exact_token_and_authorization_header():
|
||||
client = _load()
|
||||
raw = b'{"message":"do-not-leak","debug":"Authorization: token do-not-leak"}'
|
||||
redacted = client.redact_bytes(raw, "do-not-leak")
|
||||
|
||||
assert b"do-not-leak" not in redacted
|
||||
assert redacted.count(b"[REDACTED]") >= 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reflected",
|
||||
[
|
||||
b"runtime-sentinel",
|
||||
base64.b64encode(b"runtime-sentinel"),
|
||||
base64.b64encode(b"hermes-automation:runtime-sentinel"),
|
||||
],
|
||||
)
|
||||
def test_direct_api_rejects_credential_reflection(reflected: bytes):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime-sentinel",
|
||||
opener=lambda *_a, **_k: Response(
|
||||
b'{"unexpected":"' + reflected + b'"}'
|
||||
),
|
||||
)
|
||||
|
||||
@ -1,203 +0,0 @@
|
||||
"""Draft-creation input contracts for the Atlas Gitea client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_gitea_support import (
|
||||
HEAD_SHA,
|
||||
Response,
|
||||
_draft_payload,
|
||||
_draft_response,
|
||||
_load,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "path", "data"),
|
||||
[
|
||||
("DELETE", "/api/v1/repos/atlas/cassandra/pulls/4", None),
|
||||
("POST", "/api/v1/repos/atlas/cassandra/pulls/4/merge", {}),
|
||||
(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls/4/reviews",
|
||||
{"event": "APPROVED"},
|
||||
),
|
||||
("PATCH", "/api/v1/repos/atlas/cassandra/pulls/4", {"title": "WIP: x"}),
|
||||
("PUT", "/api/v1/repos/atlas/cassandra/branches/main", {}),
|
||||
],
|
||||
)
|
||||
def test_merge_approve_close_delete_update_and_other_mutations_are_rejected(
|
||||
method: str, path: str, data: object
|
||||
):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError):
|
||||
client.build_request(
|
||||
method, path, base_url=client.CANONICAL_BASE_URL, token="secret", data=data
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ref",
|
||||
[
|
||||
"foo/.bar",
|
||||
"foo/bar.lock/baz",
|
||||
"foo..bar",
|
||||
"foo@{bar",
|
||||
"foo//bar",
|
||||
"-danger",
|
||||
"danger.",
|
||||
"danger~one",
|
||||
"danger^one",
|
||||
"danger:one",
|
||||
"danger one",
|
||||
],
|
||||
)
|
||||
def test_complete_git_ref_validation_rejects_invalid_names(ref: str):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError):
|
||||
client._validate_ref(ref, "head")
|
||||
|
||||
|
||||
def test_git_ref_validation_uses_fixed_trusted_binary():
|
||||
client = _load()
|
||||
|
||||
assert client._validate_ref.__globals__["GIT_BIN"] == "/usr/bin/git"
|
||||
assert client._validate_ref("hermes/valid-fix", "head") == "hermes/valid-fix"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["base", "head"])
|
||||
@pytest.mark.parametrize("oversized", ["r" * 100_000, "🧪" * 128])
|
||||
def test_oversized_ref_never_invokes_git_request_or_opener(
|
||||
field: str, oversized: str, monkeypatch
|
||||
):
|
||||
client = _load()
|
||||
git_called = False
|
||||
request_built = False
|
||||
opener_called = False
|
||||
original_build_request = client.build_request
|
||||
|
||||
def git_run(*_args, **_kwargs):
|
||||
nonlocal git_called
|
||||
git_called = True
|
||||
raise AssertionError("Git must not receive an oversized ref")
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
client.build_request = build_request
|
||||
refs = {"base": "main", "head": "hermes/fix"}
|
||||
refs[field] = oversized
|
||||
with pytest.raises(client.PolicyError, match="branch-name limit"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base=refs["base"],
|
||||
head=refs["head"],
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=opener,
|
||||
)
|
||||
assert git_called is False
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "oversized"),
|
||||
[("repo", "r" * 101), ("title", "🧪" * 200), ("body", "🧪" * 9_000)],
|
||||
)
|
||||
def test_other_text_bounds_fail_before_git_request_or_opener(
|
||||
field: str, oversized: str, monkeypatch
|
||||
):
|
||||
client = _load()
|
||||
git_called = False
|
||||
request_built = False
|
||||
opener_called = False
|
||||
original_build_request = client.build_request
|
||||
|
||||
def git_run(*_args, **_kwargs):
|
||||
nonlocal git_called
|
||||
git_called = True
|
||||
raise AssertionError("Git must not run before cheap input bounds")
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
client.build_request = build_request
|
||||
values = {
|
||||
"repo": "cassandra",
|
||||
"title": "Focused fix",
|
||||
"body": "Review evidence",
|
||||
}
|
||||
values[field] = oversized
|
||||
with pytest.raises(client.PolicyError):
|
||||
client.create_draft(
|
||||
values["repo"],
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token="runtime",
|
||||
opener=opener,
|
||||
)
|
||||
assert git_called is False
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
def test_create_forces_draft_title_and_same_repository_branch_names():
|
||||
client = _load()
|
||||
assert (
|
||||
client.authorize_request(
|
||||
"POST", "/api/v1/repos/atlas/cassandra/pulls", _draft_payload()
|
||||
)
|
||||
== "create-draft"
|
||||
)
|
||||
with pytest.raises(client.PolicyError, match="draft-title prefix"):
|
||||
client.authorize_request(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls",
|
||||
_draft_payload(title="Not a draft"),
|
||||
)
|
||||
with pytest.raises(client.PolicyError):
|
||||
client.authorize_request(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls",
|
||||
_draft_payload(head="someone:branch"),
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_token_is_only_an_authorization_header():
|
||||
client = _load()
|
||||
request = client.build_request(
|
||||
"POST",
|
||||
"/api/v1/repos/atlas/cassandra/pulls",
|
||||
base_url=client.CANONICAL_BASE_URL,
|
||||
token="do-not-leak",
|
||||
data=_draft_payload(),
|
||||
)
|
||||
|
||||
assert "do-not-leak" not in request.full_url
|
||||
assert b"do-not-leak" not in request.data
|
||||
assert request.get_header("Authorization") == "token do-not-leak"
|
||||
@ -1,317 +0,0 @@
|
||||
"""Credential-loss-prevention contracts for Atlas draft creation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_gitea_support import (
|
||||
HEAD_SHA,
|
||||
Response,
|
||||
_draft_response,
|
||||
_load,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sensitive",
|
||||
[
|
||||
"client_" + "secret=not-a-real-value",
|
||||
'{"client_' + 'secret":\n"synthetic-value"}',
|
||||
'{"client_' + 'secret"\n:\n"synthetic-value"}',
|
||||
'{"client\\u005f' + 'secret":"synthetic-value"}',
|
||||
"client_" + "secret:\n synthetic-value",
|
||||
'{"client\\q' + 'secret":"synthetic-value"}',
|
||||
'{"client\n' + 'secret":"synthetic-value"}',
|
||||
'"client_' + 'secret"\x0b:\n"synthetic-value"',
|
||||
"ACCESS_" + "TOKEN = 'not-a-real-value'",
|
||||
'{"refresh_' + 'token": "not-a-real-value"}',
|
||||
"private_" + "key: not-a-real-value",
|
||||
"AWS_SECRET_ACCESS_" + "KEY=not-a-real-value",
|
||||
"aws_access_key_" + "id: not-a-real-value",
|
||||
"AWS_SESSION_" + "TOKEN = 'not-a-real-value'",
|
||||
'{"AccessKey' + 'Id":"not-a-real-value"}',
|
||||
'{"SecretAccess' + 'Key":"not-a-real-value"}',
|
||||
'{"Session' + 'Token":"not-a-real-value"}',
|
||||
"aws-security-" + "token: not-a-real-value",
|
||||
"Account" + "Key=not-a-real-value",
|
||||
"SharedAccess" + "Signature: not-a-real-value",
|
||||
"AZURE_STORAGE_CONNECTION_" + "STRING='not-a-real-value'",
|
||||
"DefaultEndpointsProtocol=https;AccountName=fake;Account"
|
||||
+ "Key=not-a-real-value;EndpointSuffix=example",
|
||||
"?sv=2024-11-04&ss=b&srt=sco&sp=rwdlac&se=2099-01-01&sig=" + "not-a-real-value",
|
||||
"DOCKER_AUTH_" + "CONFIG='not-a-real-value'",
|
||||
'{"auths":{"registry.example":{"auth":"bm90LXJlYWw="}}}',
|
||||
'{"identity' + 'token":"not-a-real-value"}',
|
||||
'{"type":"service_' + 'account","client_email":"fake@example.test"}',
|
||||
'{"private_key_' + 'id":"not-a-real-value"}',
|
||||
'{"client_' + 'email":"fake@example.test"}',
|
||||
"GOOGLE_CREDENTIALS" + "=not-a-real-value",
|
||||
"personal_access_" + "token: not-a-real-value",
|
||||
"GITEA_" + "TOKEN=not-a-real-value",
|
||||
"gitlab-token" + ": not-a-real-value",
|
||||
"pat" + "=not-a-real-value",
|
||||
"Authorization: " + "Bearer not-a-real-credential-value",
|
||||
"authorization = " + '"Basic not-a-real-credential-value"',
|
||||
"Bearer" + "=not-a-real-credential-value",
|
||||
"Basic" + ": not-a-real-credential-value",
|
||||
"pass" + "word=not-a-real-credential",
|
||||
"ghp_" + "notarealcredentialvalue123456",
|
||||
"github_pat_" + "notarealcredentialvalue123456",
|
||||
"glpat-" + "notarealcredentialvalue123456",
|
||||
"xoxb-" + "not-a-real-credential-value-123456",
|
||||
"sk-ant-" + "notarealcredentialvalue123456",
|
||||
"sk-proj-" + "notarealcredentialvalue123456",
|
||||
"sk_live_" + "notarealcredentialvalue123456",
|
||||
"ya29." + "notarealcredentialvalue123456",
|
||||
"gta_" + "notarealcredentialvalue123456",
|
||||
"whsec_" + "notarealcredentialvalue123456",
|
||||
"npm_" + "notarealcredentialvalue123456",
|
||||
"pypi-" + "notarealcredentialvalue123456789012345",
|
||||
"hf_" + "notarealcredentialvalue123456",
|
||||
"SG." + "notarealvalue1234" + ".notarealcredentialvalue123456",
|
||||
"SK" + "a" * 32,
|
||||
"https://hooks.slack.com/services/" + "T000/B000/notarealvalue123456",
|
||||
"https://discord.com/api/webhooks/123456789/" + "notarealcredentialvalue123456",
|
||||
"https://fake.webhook.office.com/" + "notarealcredentialvalue123456",
|
||||
"webhook_" + "url=https://example.test/not-real",
|
||||
"FutureCloudSigning" + "Credential=not-a-real-value",
|
||||
"future-client-signing-" + "key: not-a-real-value",
|
||||
"future_client_signing_" + "key='not-a-real-value'",
|
||||
'{"serviceAccountPrivate' + 'Key":"not-a-real-value"}',
|
||||
"CONTAINER_REGISTRY_" + "CREDENTIAL=not-a-real-value",
|
||||
"someWebhookSigning" + "Secret: not-a-real-value",
|
||||
"client" + "Key=not-a-real-value",
|
||||
"session" + "Key: not-a-real-value",
|
||||
"access" + "Id=not-a-real-value",
|
||||
"credentials" + ": {user: fake}",
|
||||
"private" + "Key: |",
|
||||
"nuget_api_" + "key=not-a-real-value",
|
||||
"oy2" + "a" * 44,
|
||||
"sk_test_" + "notarealcredentialvalue123456",
|
||||
"A1b2C3d4E5f6G7h8I9j0K_l-M+n/O=pQ2rS3tU4vW5xY6zZ7aB8cC9d",
|
||||
"AKIA" + "A" * 16,
|
||||
"AIza" + "a" * 35,
|
||||
"-----BEGIN OPENSSH " + "PRIVATE KEY-----",
|
||||
"ssh-ed25519 " + "bm90YXJlYWxjcmVkZW50aWFsdmFsdWU=",
|
||||
"eyJnotarealheader." + "notarealpayloadvalue." + "notarealsignature",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("field", ["title", "body"])
|
||||
def test_create_rejects_expanded_credential_shapes_before_network(
|
||||
sensitive: str, field: str
|
||||
):
|
||||
client = _load()
|
||||
called = False
|
||||
request_built = False
|
||||
|
||||
original_build_request = client.build_request
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
client.build_request = build_request
|
||||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||||
values[field] = sensitive
|
||||
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token="runtime-sentinel",
|
||||
opener=opener,
|
||||
)
|
||||
assert request_built is False
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_very_long_compact_body_is_rejected_before_request_or_network():
|
||||
client = _load()
|
||||
request_built = False
|
||||
opener_called = False
|
||||
original_build_request = client.build_request
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
client.build_request = build_request
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="a" * 300,
|
||||
token="runtime-sentinel",
|
||||
opener=opener,
|
||||
)
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"safe_text",
|
||||
[
|
||||
"AWS_SECRET_ACCESS_KEY is injected at runtime",
|
||||
"Token: reject empty values",
|
||||
"Authorization = preserve header behavior",
|
||||
"Password: add regression",
|
||||
"Review the Authorization header behavior",
|
||||
"Bearer authentication is required for this route",
|
||||
"Document Docker auths payload rejection",
|
||||
"The client_email field belongs to service accounts",
|
||||
"AccountKey assignments must be blocked",
|
||||
"This patch changes token validation without including a value",
|
||||
"FutureCloudSigningCredential handling needs a regression test",
|
||||
"The clientKey name is documented without an assigned value",
|
||||
"A SHA-256 digest 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef is evidence",
|
||||
],
|
||||
)
|
||||
def test_credential_policy_keeps_normal_engineering_prose_usable(safe_text: str):
|
||||
client = _load()
|
||||
|
||||
assert client._validate_body(safe_text) == safe_text
|
||||
assert client._draft_title(safe_text) == f"WIP: {safe_text}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["title", "body"])
|
||||
def test_create_rejects_exact_runtime_token_before_network(field: str):
|
||||
client = _load()
|
||||
called = False
|
||||
request_built = False
|
||||
|
||||
original_build_request = client.build_request
|
||||
|
||||
def build_request(*args, **kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
return original_build_request(*args, **kwargs)
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
client.build_request = build_request
|
||||
runtime_token = "exact-random-runtime-sentinel-7b73ac61"
|
||||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||||
values[field] = f"Accidental {runtime_token} value"
|
||||
with pytest.raises(client.PolicyError, match="runtime credential"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token=runtime_token,
|
||||
opener=opener,
|
||||
)
|
||||
assert request_built is False
|
||||
assert called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["repo", "base", "head", "title", "body"])
|
||||
def test_every_public_field_rejects_exact_runtime_token_before_git_or_network(
|
||||
field: str, monkeypatch
|
||||
):
|
||||
client = _load()
|
||||
git_called = False
|
||||
request_built = False
|
||||
opener_called = False
|
||||
runtime_token = "runtime-sentinel"
|
||||
values = {
|
||||
"repo": "cassandra",
|
||||
"base": "main",
|
||||
"head": "hermes/fix",
|
||||
"title": "Focused fix",
|
||||
"body": "Review evidence",
|
||||
}
|
||||
values[field] = runtime_token
|
||||
|
||||
def git_run(*_args, **_kwargs):
|
||||
nonlocal git_called
|
||||
git_called = True
|
||||
raise AssertionError("Git must not run for a credential-bearing field")
|
||||
|
||||
def build_request(*_args, **_kwargs):
|
||||
nonlocal request_built
|
||||
request_built = True
|
||||
raise AssertionError("a request must not be built")
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal opener_called
|
||||
opener_called = True
|
||||
raise AssertionError("the opener must not be called")
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
monkeypatch.setattr(client, "build_request", build_request)
|
||||
with pytest.raises(client.PolicyError, match="runtime credential"):
|
||||
client.create_draft(
|
||||
values["repo"],
|
||||
base=values["base"],
|
||||
head=values["head"],
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token=runtime_token,
|
||||
opener=opener,
|
||||
)
|
||||
assert git_called is False
|
||||
assert request_built is False
|
||||
assert opener_called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sensitive",
|
||||
[
|
||||
" ".join(["{}"] * 32) + ' {"client_secret":"synthetic-value"}',
|
||||
"prefix_" + "ghp_" + "notarealcredentialvalue123456_suffix",
|
||||
"client_secret: correct horse battery staple",
|
||||
"'client_secret':\n synthetic-value",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("field", ["title", "body"])
|
||||
def test_structured_scanner_closes_bounded_and_wrapped_token_bypasses(
|
||||
sensitive: str, field: str
|
||||
):
|
||||
client = _load()
|
||||
values = {"title": "Focused fix", "body": "Review evidence"}
|
||||
values[field] = sensitive
|
||||
called = False
|
||||
|
||||
def opener(*_args, **_kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
return Response(_draft_response())
|
||||
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title=values["title"],
|
||||
body=values["body"],
|
||||
token="runtime-sentinel",
|
||||
opener=opener,
|
||||
)
|
||||
assert called is False
|
||||
@ -1,199 +0,0 @@
|
||||
"""Response and postcondition contracts for Atlas draft creation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_gitea_support import (
|
||||
HEAD_SHA,
|
||||
Response,
|
||||
_draft_response,
|
||||
_load,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("status", [200, 202, 204, 206])
|
||||
def test_create_accepts_only_http_201(status: int):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="unexpected HTTP status"):
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(_draft_response(), status=status),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [201, 202, 204, 206])
|
||||
def test_read_accepts_only_http_200(status: int):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="unexpected HTTP status"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(b"{}", status=status),
|
||||
)
|
||||
|
||||
|
||||
def test_successful_create_validates_each_ref_once(monkeypatch):
|
||||
client = _load()
|
||||
calls = []
|
||||
|
||||
def git_run(command, **_kwargs):
|
||||
calls.append(command)
|
||||
return type("Result", (), {"returncode": 0})()
|
||||
|
||||
monkeypatch.setattr(client._validate_ref.__globals__["subprocess"], "run", git_run)
|
||||
client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(_draft_response()),
|
||||
)
|
||||
|
||||
assert [command[-1] for command in calls] == [
|
||||
"refs/heads/main",
|
||||
"refs/heads/hermes/fix",
|
||||
]
|
||||
|
||||
|
||||
def test_create_verifies_every_server_postcondition():
|
||||
client = _load()
|
||||
calls = []
|
||||
|
||||
def opener(request, timeout):
|
||||
calls.append((request, timeout))
|
||||
return Response(_draft_response())
|
||||
|
||||
result = client.create_draft(
|
||||
"cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="Focused fix",
|
||||
body="Review evidence",
|
||||
token="runtime",
|
||||
opener=opener,
|
||||
)
|
||||
|
||||
assert json.loads(result) == _draft_response()
|
||||
payload = json.loads(calls[0][0].data)
|
||||
assert payload == {
|
||||
"base": "main",
|
||||
"body": "Review evidence",
|
||||
"head": "hermes/fix",
|
||||
"title": "WIP: Focused fix",
|
||||
}
|
||||
assert calls[0][1] == 30
|
||||
|
||||
|
||||
def test_create_postcondition_rejects_every_material_mismatch():
|
||||
client = _load()
|
||||
mutations = [
|
||||
("number", 0),
|
||||
("number", 2_147_483_648),
|
||||
("state", "closed"),
|
||||
("draft", False),
|
||||
("merged", True),
|
||||
("html_url", "https://evil.example/pulls/3"),
|
||||
("url", "https://evil.example/api/pulls/3"),
|
||||
("title", "Focused fix"),
|
||||
("body", "different"),
|
||||
]
|
||||
documents = []
|
||||
for key, value in mutations:
|
||||
document = _draft_response()
|
||||
document[key] = value
|
||||
documents.append(document)
|
||||
for path, value in [
|
||||
(("base", "ref"), "master"),
|
||||
(("base", "repo", "full_name"), "evil/cassandra"),
|
||||
(("head", "ref"), "other"),
|
||||
(("head", "sha"), "0" * 40),
|
||||
(("head", "repo", "full_name"), "evil/cassandra"),
|
||||
]:
|
||||
document = copy.deepcopy(_draft_response())
|
||||
target = document
|
||||
for key in path[:-1]:
|
||||
target = target[key]
|
||||
target[path[-1]] = value
|
||||
documents.append(document)
|
||||
|
||||
for document in documents:
|
||||
with pytest.raises(client.PolicyError):
|
||||
client._require_create_response(
|
||||
json.dumps(document).encode(),
|
||||
repo="cassandra",
|
||||
base="main",
|
||||
head="hermes/fix",
|
||||
head_sha=HEAD_SHA,
|
||||
title="WIP: Focused fix",
|
||||
body="Review evidence",
|
||||
)
|
||||
|
||||
|
||||
def test_read_response_is_bounded():
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="safe size limit"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: Response(b"x" * (client.MAX_RESPONSE_BYTES + 1)),
|
||||
)
|
||||
|
||||
|
||||
def test_direct_api_rejects_unexpected_success_content_type():
|
||||
client = _load()
|
||||
response = Response(b"{}")
|
||||
response.headers.replace_header("Content-Type", "text/html")
|
||||
|
||||
with pytest.raises(client.PolicyError, match="unexpected response type"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime",
|
||||
opener=lambda *_a, **_k: response,
|
||||
)
|
||||
|
||||
|
||||
def test_output_redaction_covers_exact_token_and_authorization_header():
|
||||
client = _load()
|
||||
raw = b'{"message":"do-not-leak","debug":"Authorization: token do-not-leak"}'
|
||||
redacted = client.redact_bytes(raw, "do-not-leak")
|
||||
|
||||
assert b"do-not-leak" not in redacted
|
||||
assert redacted.count(b"[REDACTED]") >= 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reflected",
|
||||
[
|
||||
b"runtime-sentinel",
|
||||
base64.b64encode(b"runtime-sentinel"),
|
||||
base64.b64encode(b"hermes-automation:runtime-sentinel"),
|
||||
],
|
||||
)
|
||||
def test_direct_api_rejects_credential_reflection(reflected: bytes):
|
||||
client = _load()
|
||||
|
||||
with pytest.raises(client.PolicyError, match="credential material"):
|
||||
client.read(
|
||||
"/api/v1/repos/atlas/cassandra",
|
||||
token="runtime-sentinel",
|
||||
opener=lambda *_a, **_k: Response(
|
||||
b'{"unexpected":"' + reflected + b'"}'
|
||||
),
|
||||
)
|
||||
@ -1,75 +0,0 @@
|
||||
"""Shared fixtures for the Atlas-only Gitea client contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from email.message import Message
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
CLIENT_PATH = ROOT / "services/hermes/scm-common/scripts/gitea_api.py"
|
||||
HEAD_SHA = "465cf9146b05c174a2a8d310aff6c64be58277b6"
|
||||
if str(CLIENT_PATH.parent) not in sys.path:
|
||||
sys.path.insert(0, str(CLIENT_PATH.parent))
|
||||
|
||||
|
||||
def _load():
|
||||
spec = importlib.util.spec_from_file_location("safe_gitea_api", CLIENT_PATH)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _draft_payload(**updates):
|
||||
payload = {
|
||||
"base": "main",
|
||||
"body": "Review evidence",
|
||||
"head": "hermes/review-fix",
|
||||
"title": "WIP: Repair review findings",
|
||||
}
|
||||
payload.update(updates)
|
||||
return payload
|
||||
|
||||
|
||||
def _draft_response(**updates):
|
||||
response = {
|
||||
"number": 3,
|
||||
"state": "open",
|
||||
"draft": True,
|
||||
"merged": False,
|
||||
"html_url": "https://scm.bstein.dev/atlas/cassandra/pulls/3",
|
||||
"url": "https://scm.bstein.dev/atlas/cassandra/pulls/3",
|
||||
"title": "WIP: Focused fix",
|
||||
"body": "Review evidence",
|
||||
"base": {"ref": "main", "repo": {"full_name": "atlas/cassandra"}},
|
||||
"head": {
|
||||
"ref": "hermes/fix",
|
||||
"sha": HEAD_SHA,
|
||||
"repo": {"full_name": "atlas/cassandra"},
|
||||
},
|
||||
}
|
||||
response.update(updates)
|
||||
return response
|
||||
|
||||
|
||||
class Response:
|
||||
def __init__(self, body: object, status: int | None = None):
|
||||
self.body = body if isinstance(body, bytes) else json.dumps(body).encode()
|
||||
self.status = (
|
||||
status if status is not None else (201 if isinstance(body, dict) else 200)
|
||||
)
|
||||
self.headers = Message()
|
||||
self.headers["Content-Type"] = "application/json"
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def read(self, limit=-1):
|
||||
return self.body if limit < 0 else self.body[:limit]
|
||||
@ -1,20 +1,99 @@
|
||||
"""Core contracts for the dedicated Hermes node SSH identity."""
|
||||
"""Contracts for the dedicated, unprivileged Hermes node SSH identity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_node_account_support import (
|
||||
ROOT,
|
||||
SCRIPT,
|
||||
_fixture,
|
||||
_key,
|
||||
_load,
|
||||
)
|
||||
ROOT = Path(__file__).parents[2]
|
||||
SCRIPT = ROOT / "services/hermes/scripts/node_account_hardening.py"
|
||||
sys.path.insert(0, str(SCRIPT.parent))
|
||||
|
||||
|
||||
def _key(material: bytes, comment: str = "") -> str:
|
||||
key_type = b"ssh-ed25519"
|
||||
blob = len(key_type).to_bytes(4, "big") + key_type + material
|
||||
suffix = f" {comment}" if comment else ""
|
||||
return "ssh-ed25519 " + base64.b64encode(blob).decode() + suffix
|
||||
|
||||
|
||||
def _typed_key(key_type: str, material: bytes, comment: str = "") -> str:
|
||||
encoded_type = key_type.encode()
|
||||
blob = len(encoded_type).to_bytes(4, "big") + encoded_type + material
|
||||
suffix = f" {comment}" if comment else ""
|
||||
return f"{key_type} {base64.b64encode(blob).decode()}{suffix}"
|
||||
|
||||
|
||||
def _load():
|
||||
spec = importlib.util.spec_from_file_location("node_account_hardening_test", SCRIPT)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _fixture(tmp_path: Path, monkeypatch):
|
||||
module = _load()
|
||||
host_etc = tmp_path / "etc"
|
||||
host_home = tmp_path / "home"
|
||||
host_etc.mkdir()
|
||||
host_home.mkdir()
|
||||
originals = {
|
||||
"passwd": (
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
"atlas:x:2000:2000:Atlas:/home/atlas:/bin/bash\n"
|
||||
"oceanus:x:2001:2001:Oceanus:/home/oceanus:/bin/bash\n"
|
||||
),
|
||||
"group": (
|
||||
"root:x:0:\n"
|
||||
"atlas:x:2000:\n"
|
||||
"oceanus:x:2001:\n"
|
||||
"disk:x:6:atlas\n"
|
||||
"sudo:x:27:oceanus\n"
|
||||
),
|
||||
"shadow": (
|
||||
"root:!:1:0:99999:7:::\n"
|
||||
"atlas:!:1:0:99999:7:::\n"
|
||||
"oceanus:!:1:0:99999:7:::\n"
|
||||
),
|
||||
"gshadow": (
|
||||
"root:!::\n"
|
||||
"atlas:!::\n"
|
||||
"oceanus:!::\n"
|
||||
"disk:!::atlas\n"
|
||||
"sudo:!::oceanus\n"
|
||||
),
|
||||
}
|
||||
for name, value in originals.items():
|
||||
(host_etc / name).write_text(value, encoding="utf-8")
|
||||
key = _key(b"synthetic-hermes-key")
|
||||
other = _key(b"human-operator-key")
|
||||
for user in ("atlas", "oceanus"):
|
||||
ssh = host_home / user / ".ssh"
|
||||
ssh.mkdir(parents=True)
|
||||
(ssh / "authorized_keys").write_text(
|
||||
f"{other} {user}\n{key}\n", encoding="utf-8"
|
||||
)
|
||||
public_key = tmp_path / "public-key"
|
||||
public_key.write_text(key + "\n", encoding="utf-8")
|
||||
monkeypatch.setattr(module, "HOST_ETC", host_etc)
|
||||
monkeypatch.setattr(module, "HOST_HOME", host_home)
|
||||
monkeypatch.setattr(module, "ACCOUNT_UID", os.getuid())
|
||||
monkeypatch.setattr(module, "ACCOUNT_GID", os.getgid())
|
||||
monkeypatch.setattr(module, "HOST_ROOT_UID", os.getuid())
|
||||
monkeypatch.setattr(module, "HOST_POLKIT_SHARE", tmp_path / "missing-polkit")
|
||||
# ACL behavior has its own executable test below. These account-database
|
||||
# tests must not depend on the host running pytest with CAP_FOWNER.
|
||||
monkeypatch.setattr(module, "_deny_sensitive_roots", lambda: None)
|
||||
return module, originals, key, other, public_key
|
||||
|
||||
|
||||
def test_reconciler_creates_locked_groupless_account_and_moves_only_exact_key(
|
||||
tmp_path: Path, monkeypatch
|
||||
@ -250,3 +329,267 @@ def test_runtime_ssh_config_forces_dedicated_account_for_every_titan():
|
||||
)
|
||||
assert "User atlas" not in yaml.safe_dump(agent)
|
||||
assert "User oceanus" not in yaml.safe_dump(agent)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("database,record", [
|
||||
("group", "disk:x:6:atlas,hermes-agent\n"),
|
||||
("gshadow", "disk:!:hermes-agent:atlas\n"),
|
||||
("gshadow", "disk:!::atlas,hermes-agent\n"),
|
||||
])
|
||||
def test_unexpected_group_or_gshadow_membership_fails_before_writes(
|
||||
tmp_path: Path, monkeypatch, database: str, record: str
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
path = module.HOST_ETC / database
|
||||
path.write_text(
|
||||
"\n".join(line for line in originals[database].splitlines() if not line.startswith("disk:"))
|
||||
+ "\n"
|
||||
+ record,
|
||||
encoding="utf-8",
|
||||
)
|
||||
before = {name: (module.HOST_ETC / name).read_bytes() for name in originals}
|
||||
|
||||
with pytest.raises(module.HardeningError, match="group access"):
|
||||
module.reconcile(public_key)
|
||||
|
||||
assert {name: (module.HOST_ETC / name).read_bytes() for name in originals} == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative,value",
|
||||
[
|
||||
("sudoers.d/hermes", "hermes-agent ALL=(ALL:ALL) NOPASSWD: ALL\n"),
|
||||
("sudoers", "ALL ALL=(ALL:ALL) ALL\n"),
|
||||
(
|
||||
"polkit-1/rules.d/90-root.rules",
|
||||
'if (subject.user == "hermes-agent") return polkit.Result.YES;\n',
|
||||
),
|
||||
(
|
||||
"polkit-1/rules.d/90-root.rules",
|
||||
'unix-user:* org.freedesktop.policykit.exec polkit.Result.YES\n',
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_sudo_and_root_equivalent_polkit_authority_fail_closed(
|
||||
tmp_path: Path, monkeypatch, relative: str, value: str
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
policy = module.HOST_ETC / relative
|
||||
policy.parent.mkdir(parents=True, exist_ok=True)
|
||||
policy.write_text(value, encoding="utf-8")
|
||||
|
||||
with pytest.raises(module.HardeningError, match="sudo|polkit"):
|
||||
module.reconcile(public_key)
|
||||
|
||||
assert all((module.HOST_ETC / name).read_text() == original for name, original in originals.items())
|
||||
|
||||
|
||||
def test_external_group_authority_source_is_rejected(tmp_path: Path, monkeypatch):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
(module.HOST_ETC / "nsswitch.conf").write_text("passwd: files\ngroup: files ldap\n")
|
||||
with pytest.raises(module.HardeningError, match="external group"):
|
||||
module.reconcile(public_key)
|
||||
assert all((module.HOST_ETC / name).read_text() == original for name, original in originals.items())
|
||||
|
||||
|
||||
def test_writable_privilege_policy_is_rejected(tmp_path: Path, monkeypatch):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
sudoers = module.HOST_ETC / "sudoers"
|
||||
sudoers.write_text("root ALL=(ALL:ALL) ALL\n")
|
||||
sudoers.chmod(0o666)
|
||||
with pytest.raises(module.HardeningError, match="unsafe mutation"):
|
||||
module.reconcile(public_key)
|
||||
assert all((module.HOST_ETC / name).read_text() == original for name, original in originals.items())
|
||||
|
||||
|
||||
def test_key_identity_ignores_options_and_comments_and_removes_legacy_first(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, _originals, key, other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
key_fields = key.split()
|
||||
decorated = f'restrict,command="echo denied" {key_fields[0]} {key_fields[1]} old-comment'
|
||||
for user in module.LEGACY_ACCOUNTS:
|
||||
path = module.HOST_HOME / user / ".ssh/authorized_keys"
|
||||
path.write_text(f"{other} {user}\n{decorated}\n", encoding="utf-8")
|
||||
writes: list[Path] = []
|
||||
real_write = module.atomic_write
|
||||
|
||||
def recording_write(path, value, metadata):
|
||||
writes.append(path)
|
||||
real_write(path, value, metadata)
|
||||
|
||||
monkeypatch.setattr(module, "atomic_write", recording_write)
|
||||
module.reconcile(public_key)
|
||||
|
||||
key_writes = [path for path in writes if path.name == "authorized_keys"]
|
||||
assert key_writes[-1].parts[-3:] == (module.ACCOUNT, ".ssh", "authorized_keys")
|
||||
assert all(module.ACCOUNT not in path.parts for path in key_writes[:-1])
|
||||
|
||||
|
||||
def test_unrelated_modern_human_key_type_is_preserved(tmp_path: Path, monkeypatch):
|
||||
module, _originals, key, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
modern = _typed_key("sk-ssh-ed25519@openssh.com", b"human-security-key", "human")
|
||||
for user in module.LEGACY_ACCOUNTS:
|
||||
path = module.HOST_HOME / user / ".ssh/authorized_keys"
|
||||
path.write_text(f"{modern}\n{key}\n", encoding="utf-8")
|
||||
|
||||
module.reconcile(public_key)
|
||||
|
||||
for user in module.LEGACY_ACCOUNTS:
|
||||
path = module.HOST_HOME / user / ".ssh/authorized_keys"
|
||||
assert path.read_text() == modern + "\n"
|
||||
|
||||
|
||||
def test_crash_during_legacy_key_removal_never_installs_duplicate_key(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, _originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
real_write = module.atomic_write
|
||||
key_writes = 0
|
||||
|
||||
def crash_on_second_key(path, value, metadata):
|
||||
nonlocal key_writes
|
||||
if path.name == "authorized_keys":
|
||||
key_writes += 1
|
||||
if key_writes == 2:
|
||||
raise module.HardeningError("synthetic crash")
|
||||
real_write(path, value, metadata)
|
||||
|
||||
monkeypatch.setattr(module, "atomic_write", crash_on_second_key)
|
||||
with pytest.raises(module.HardeningError, match="synthetic crash"):
|
||||
module.reconcile(public_key)
|
||||
assert not (module.HOST_HOME / module.ACCOUNT / ".ssh/authorized_keys").exists()
|
||||
|
||||
|
||||
def test_preexisting_target_key_is_removed_before_legacy_cleanup_and_reinstalled(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, _originals, key, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
target = module.HOST_HOME / module.ACCOUNT / ".ssh/authorized_keys"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text(key + " existing\n")
|
||||
writes: list[Path] = []
|
||||
real_write = module.atomic_write
|
||||
|
||||
def recording_write(path, value, metadata):
|
||||
if path.name == "authorized_keys":
|
||||
writes.append(path)
|
||||
real_write(path, value, metadata)
|
||||
|
||||
monkeypatch.setattr(module, "atomic_write", recording_write)
|
||||
module._move_key(public_key)
|
||||
|
||||
assert writes[0] == target
|
||||
assert writes[-1] == target
|
||||
assert all(path != target for path in writes[1:-1])
|
||||
assert target.read_text() == key + "\n"
|
||||
|
||||
|
||||
def test_account_database_write_preserves_mode_owner_and_xattrs(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
passwd = module.HOST_ETC / "passwd"
|
||||
passwd.chmod(0o640)
|
||||
os.setxattr(passwd, "user.hermes-test", b"preserve")
|
||||
before = passwd.stat()
|
||||
|
||||
module.reconcile(public_key)
|
||||
|
||||
after = passwd.stat()
|
||||
assert after.st_mode & 0o777 == 0o640
|
||||
assert (after.st_uid, after.st_gid) == (before.st_uid, before.st_gid)
|
||||
assert os.getxattr(passwd, "user.hermes-test") == b"preserve"
|
||||
backup = module.HOST_ETC / "passwd.hermes-boundary-backup"
|
||||
assert os.getxattr(backup, "user.hermes-test") == b"preserve"
|
||||
assert backup.read_text() == originals["passwd"]
|
||||
|
||||
|
||||
def test_atomic_write_restores_acl_and_security_label_xattrs(tmp_path: Path, monkeypatch):
|
||||
module = _load()
|
||||
io_module = sys.modules[module.atomic_write.__module__]
|
||||
target = tmp_path / "passwd"
|
||||
target.write_bytes(b"old\n")
|
||||
metadata = io_module.FileSnapshot(
|
||||
value=b"old\n",
|
||||
device=target.stat().st_dev,
|
||||
inode=target.stat().st_ino,
|
||||
mode=0o600,
|
||||
uid=os.getuid(),
|
||||
gid=os.getgid(),
|
||||
size=4,
|
||||
mtime_ns=target.stat().st_mtime_ns,
|
||||
ctime_ns=target.stat().st_ctime_ns,
|
||||
xattrs=(
|
||||
("system.posix_acl_access", b"synthetic-acl"),
|
||||
("security.selinux", b"synthetic-label"),
|
||||
("user.audit", b"synthetic-xattr"),
|
||||
),
|
||||
)
|
||||
restored: list[tuple[str, bytes]] = []
|
||||
monkeypatch.setattr(
|
||||
io_module.os,
|
||||
"setxattr",
|
||||
lambda _fd, name, value: restored.append((name, value)),
|
||||
)
|
||||
|
||||
io_module.atomic_write(target, b"new\n", metadata)
|
||||
|
||||
assert target.read_bytes() == b"new\n"
|
||||
assert restored == list(metadata.xattrs)
|
||||
|
||||
|
||||
def test_concurrent_database_change_is_detected_before_any_account_write(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
real_backup = module.backup_once
|
||||
backup_count = 0
|
||||
writes: list[Path] = []
|
||||
|
||||
def racing_backup(path, snapshot, maximum):
|
||||
nonlocal backup_count
|
||||
result = real_backup(path, snapshot, maximum)
|
||||
backup_count += 1
|
||||
if backup_count == 4:
|
||||
group = module.HOST_ETC / "group"
|
||||
group.write_text(originals["group"] + "race:x:4000:\n")
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(module, "backup_once", racing_backup)
|
||||
monkeypatch.setattr(module, "atomic_write", lambda path, *_args: writes.append(path))
|
||||
with pytest.raises(module.HardeningError, match="concurrent host account change"):
|
||||
module.reconcile(public_key)
|
||||
assert writes == []
|
||||
|
||||
|
||||
def test_flux_orders_observer_rbac_before_hermes_prunes_old_authority():
|
||||
observer = yaml.safe_load(
|
||||
(ROOT / "clusters/atlas/flux-system/applications/hermes-observer-rbac/kustomization.yaml").read_text()
|
||||
)
|
||||
hermes = yaml.safe_load(
|
||||
(ROOT / "clusters/atlas/flux-system/applications/hermes/kustomization.yaml").read_text()
|
||||
)
|
||||
assert "dependsOn" not in observer["spec"]
|
||||
assert {item["name"] for item in hermes["spec"]["dependsOn"]} >= {
|
||||
"hermes-observer-rbac",
|
||||
"hermes-scm-broker",
|
||||
}
|
||||
|
||||
|
||||
def test_sensitive_root_acl_backups_have_unique_host_path_names(monkeypatch):
|
||||
module = _load()
|
||||
seen = []
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_deny_sensitive_root",
|
||||
lambda path, backup_name=None: seen.append((path, backup_name)),
|
||||
)
|
||||
module._deny_sensitive_roots()
|
||||
assert [name for _path, name in seen] == [
|
||||
"var-lib-rancher-k3s",
|
||||
"var-lib-kubelet",
|
||||
"run-k3s",
|
||||
"run-containerd",
|
||||
]
|
||||
assert len({name for _path, name in seen}) == 4
|
||||
|
||||
@ -1,222 +0,0 @@
|
||||
"""Crash-safety and metadata contracts for Hermes node-account writes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_node_account_support import (
|
||||
ROOT,
|
||||
_fixture,
|
||||
_load,
|
||||
_typed_key,
|
||||
)
|
||||
|
||||
def test_key_identity_ignores_options_and_comments_and_removes_legacy_first(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, _originals, key, other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
key_fields = key.split()
|
||||
decorated = f'restrict,command="echo denied" {key_fields[0]} {key_fields[1]} old-comment'
|
||||
for user in module.LEGACY_ACCOUNTS:
|
||||
path = module.HOST_HOME / user / ".ssh/authorized_keys"
|
||||
path.write_text(f"{other} {user}\n{decorated}\n", encoding="utf-8")
|
||||
writes: list[Path] = []
|
||||
real_write = module.atomic_write
|
||||
|
||||
def recording_write(path, value, metadata):
|
||||
writes.append(path)
|
||||
real_write(path, value, metadata)
|
||||
|
||||
monkeypatch.setattr(module, "atomic_write", recording_write)
|
||||
module.reconcile(public_key)
|
||||
|
||||
key_writes = [path for path in writes if path.name == "authorized_keys"]
|
||||
assert key_writes[-1].parts[-3:] == (module.ACCOUNT, ".ssh", "authorized_keys")
|
||||
assert all(module.ACCOUNT not in path.parts for path in key_writes[:-1])
|
||||
|
||||
|
||||
def test_unrelated_modern_human_key_type_is_preserved(tmp_path: Path, monkeypatch):
|
||||
module, _originals, key, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
modern = _typed_key("sk-ssh-ed25519@openssh.com", b"human-security-key", "human")
|
||||
for user in module.LEGACY_ACCOUNTS:
|
||||
path = module.HOST_HOME / user / ".ssh/authorized_keys"
|
||||
path.write_text(f"{modern}\n{key}\n", encoding="utf-8")
|
||||
|
||||
module.reconcile(public_key)
|
||||
|
||||
for user in module.LEGACY_ACCOUNTS:
|
||||
path = module.HOST_HOME / user / ".ssh/authorized_keys"
|
||||
assert path.read_text() == modern + "\n"
|
||||
|
||||
|
||||
def test_crash_during_legacy_key_removal_never_installs_duplicate_key(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, _originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
real_write = module.atomic_write
|
||||
key_writes = 0
|
||||
|
||||
def crash_on_second_key(path, value, metadata):
|
||||
nonlocal key_writes
|
||||
if path.name == "authorized_keys":
|
||||
key_writes += 1
|
||||
if key_writes == 2:
|
||||
raise module.HardeningError("synthetic crash")
|
||||
real_write(path, value, metadata)
|
||||
|
||||
monkeypatch.setattr(module, "atomic_write", crash_on_second_key)
|
||||
with pytest.raises(module.HardeningError, match="synthetic crash"):
|
||||
module.reconcile(public_key)
|
||||
assert not (module.HOST_HOME / module.ACCOUNT / ".ssh/authorized_keys").exists()
|
||||
|
||||
|
||||
def test_preexisting_target_key_is_removed_before_legacy_cleanup_and_reinstalled(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, _originals, key, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
target = module.HOST_HOME / module.ACCOUNT / ".ssh/authorized_keys"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text(key + " existing\n")
|
||||
writes: list[Path] = []
|
||||
real_write = module.atomic_write
|
||||
|
||||
def recording_write(path, value, metadata):
|
||||
if path.name == "authorized_keys":
|
||||
writes.append(path)
|
||||
real_write(path, value, metadata)
|
||||
|
||||
monkeypatch.setattr(module, "atomic_write", recording_write)
|
||||
module._move_key(public_key)
|
||||
|
||||
assert writes[0] == target
|
||||
assert writes[-1] == target
|
||||
assert all(path != target for path in writes[1:-1])
|
||||
assert target.read_text() == key + "\n"
|
||||
|
||||
|
||||
def test_account_database_write_preserves_mode_owner_and_xattrs(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
passwd = module.HOST_ETC / "passwd"
|
||||
passwd.chmod(0o640)
|
||||
os.setxattr(passwd, "user.hermes-test", b"preserve")
|
||||
before = passwd.stat()
|
||||
|
||||
module.reconcile(public_key)
|
||||
|
||||
after = passwd.stat()
|
||||
assert after.st_mode & 0o777 == 0o640
|
||||
assert (after.st_uid, after.st_gid) == (before.st_uid, before.st_gid)
|
||||
assert os.getxattr(passwd, "user.hermes-test") == b"preserve"
|
||||
backup = module.HOST_ETC / "passwd.hermes-boundary-backup"
|
||||
assert os.getxattr(backup, "user.hermes-test") == b"preserve"
|
||||
assert backup.read_text() == originals["passwd"]
|
||||
|
||||
|
||||
def test_atomic_write_restores_acl_and_security_label_xattrs(tmp_path: Path, monkeypatch):
|
||||
module = _load()
|
||||
io_module = sys.modules[module.atomic_write.__module__]
|
||||
target = tmp_path / "passwd"
|
||||
target.write_bytes(b"old\n")
|
||||
metadata = io_module.FileSnapshot(
|
||||
value=b"old\n",
|
||||
device=target.stat().st_dev,
|
||||
inode=target.stat().st_ino,
|
||||
mode=0o600,
|
||||
uid=os.getuid(),
|
||||
gid=os.getgid(),
|
||||
size=4,
|
||||
mtime_ns=target.stat().st_mtime_ns,
|
||||
ctime_ns=target.stat().st_ctime_ns,
|
||||
xattrs=(
|
||||
("system.posix_acl_access", b"synthetic-acl"),
|
||||
("security.selinux", b"synthetic-label"),
|
||||
("user.audit", b"synthetic-xattr"),
|
||||
),
|
||||
)
|
||||
restored: list[tuple[str, bytes]] = []
|
||||
monkeypatch.setattr(
|
||||
io_module.os,
|
||||
"setxattr",
|
||||
lambda _fd, name, value: restored.append((name, value)),
|
||||
)
|
||||
|
||||
io_module.atomic_write(target, b"new\n", metadata)
|
||||
|
||||
assert target.read_bytes() == b"new\n"
|
||||
assert restored == list(metadata.xattrs)
|
||||
|
||||
|
||||
def test_concurrent_database_change_is_detected_before_any_account_write(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
real_backup = module.backup_once
|
||||
backup_count = 0
|
||||
writes: list[Path] = []
|
||||
|
||||
def racing_backup(path, snapshot, maximum):
|
||||
nonlocal backup_count
|
||||
result = real_backup(path, snapshot, maximum)
|
||||
backup_count += 1
|
||||
if backup_count == 4:
|
||||
group = module.HOST_ETC / "group"
|
||||
group.write_text(originals["group"] + "race:x:4000:\n")
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(module, "backup_once", racing_backup)
|
||||
monkeypatch.setattr(module, "atomic_write", lambda path, *_args: writes.append(path))
|
||||
with pytest.raises(module.HardeningError, match="concurrent host account change"):
|
||||
module.reconcile(public_key)
|
||||
assert writes == []
|
||||
|
||||
|
||||
def test_flux_orders_observer_rbac_before_hermes_prunes_old_authority():
|
||||
observer = yaml.safe_load(
|
||||
(ROOT / "clusters/atlas/flux-system/applications/hermes-observer-rbac/kustomization.yaml").read_text()
|
||||
)
|
||||
hermes = yaml.safe_load(
|
||||
(ROOT / "clusters/atlas/flux-system/applications/hermes/kustomization.yaml").read_text()
|
||||
)
|
||||
bindings = yaml.safe_load(
|
||||
(
|
||||
ROOT
|
||||
/ "clusters/atlas/flux-system/applications/hermes-observer-bindings/kustomization.yaml"
|
||||
).read_text()
|
||||
)
|
||||
assert "dependsOn" not in observer["spec"]
|
||||
assert {item["name"] for item in hermes["spec"]["dependsOn"]} >= {
|
||||
"hermes-observer-rbac",
|
||||
"hermes-scm-broker",
|
||||
}
|
||||
assert {item["name"] for item in bindings["spec"]["dependsOn"]} == {
|
||||
"hermes-observer-rbac",
|
||||
"hermes",
|
||||
}
|
||||
assert "hermes-observer-bindings" not in {
|
||||
item["name"] for item in hermes["spec"]["dependsOn"]
|
||||
}
|
||||
|
||||
|
||||
def test_sensitive_root_acl_backups_have_unique_host_path_names(monkeypatch):
|
||||
module = _load()
|
||||
seen = []
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_deny_sensitive_root",
|
||||
lambda path, backup_name=None: seen.append((path, backup_name)),
|
||||
)
|
||||
module._deny_sensitive_roots()
|
||||
assert [name for _path, name in seen] == [
|
||||
"var-lib-rancher-k3s",
|
||||
"var-lib-kubelet",
|
||||
"run-k3s",
|
||||
"run-containerd",
|
||||
]
|
||||
assert len({name for _path, name in seen}) == 4
|
||||
@ -1,196 +0,0 @@
|
||||
"""Host privilege-policy audits for the Hermes node SSH identity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_node_account_support import _fixture
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"database,record",
|
||||
[
|
||||
("group", "disk:x:6:atlas,hermes-agent\n"),
|
||||
("gshadow", "disk:!:hermes-agent:atlas\n"),
|
||||
("gshadow", "disk:!::atlas,hermes-agent\n"),
|
||||
],
|
||||
)
|
||||
def test_unexpected_group_or_gshadow_membership_fails_before_writes(
|
||||
tmp_path: Path, monkeypatch, database: str, record: str
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
path = module.HOST_ETC / database
|
||||
path.write_text(
|
||||
"\n".join(
|
||||
line
|
||||
for line in originals[database].splitlines()
|
||||
if not line.startswith("disk:")
|
||||
)
|
||||
+ "\n"
|
||||
+ record,
|
||||
encoding="utf-8",
|
||||
)
|
||||
before = {name: (module.HOST_ETC / name).read_bytes() for name in originals}
|
||||
|
||||
with pytest.raises(module.HardeningError, match="group access"):
|
||||
module.reconcile(public_key)
|
||||
|
||||
assert {name: (module.HOST_ETC / name).read_bytes() for name in originals} == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative,value",
|
||||
[
|
||||
("sudoers.d/hermes", "hermes-agent ALL=(ALL:ALL) NOPASSWD: ALL\n"),
|
||||
("sudoers", "ALL ALL=(ALL:ALL) ALL\n"),
|
||||
(
|
||||
"polkit-1/rules.d/90-root.rules",
|
||||
'if (subject.user == "hermes-agent") return polkit.Result.YES;\n',
|
||||
),
|
||||
(
|
||||
"polkit-1/rules.d/90-root.rules",
|
||||
"unix-user:* org.freedesktop.policykit.exec polkit.Result.YES\n",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_sudo_and_root_equivalent_polkit_authority_fail_closed(
|
||||
tmp_path: Path, monkeypatch, relative: str, value: str
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
policy = module.HOST_ETC / relative
|
||||
policy.parent.mkdir(parents=True, exist_ok=True)
|
||||
policy.write_text(value, encoding="utf-8")
|
||||
|
||||
with pytest.raises(module.HardeningError, match="sudo|polkit"):
|
||||
module.reconcile(public_key)
|
||||
|
||||
assert all(
|
||||
(module.HOST_ETC / name).read_text() == original
|
||||
for name, original in originals.items()
|
||||
)
|
||||
|
||||
|
||||
def test_external_group_authority_source_is_rejected(tmp_path: Path, monkeypatch):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
(module.HOST_ETC / "nsswitch.conf").write_text("passwd: files\ngroup: files ldap\n")
|
||||
with pytest.raises(module.HardeningError, match="external group"):
|
||||
module.reconcile(public_key)
|
||||
assert all(
|
||||
(module.HOST_ETC / name).read_text() == original
|
||||
for name, original in originals.items()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("database", ["passwd", "group", "initgroups", "shadow"])
|
||||
def test_external_identity_authority_sources_fail_closed(
|
||||
tmp_path: Path, monkeypatch, database: str
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
(module.HOST_ETC / "nsswitch.conf").write_text(
|
||||
f"passwd: files\n{database}: files ldap\n", encoding="utf-8"
|
||||
)
|
||||
with pytest.raises(module.HardeningError, match="external group/account"):
|
||||
module.reconcile(public_key)
|
||||
assert all(
|
||||
(module.HOST_ETC / name).read_text() == original
|
||||
for name, original in originals.items()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
"#1200 ALL=(ALL:ALL) NOPASSWD: ALL\n",
|
||||
"User_Alias HERMES_IDS = #1200\nHERMES_IDS ALL=(ALL:ALL) NOPASSWD: ALL\n",
|
||||
"User_Alias HERMES_GROUP = %#1200\nHERMES_GROUP ALL=(ALL:ALL) NOPASSWD: ALL\n",
|
||||
],
|
||||
)
|
||||
def test_visudo_valid_numeric_and_alias_grants_fail_closed(
|
||||
tmp_path: Path, monkeypatch, value: str
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr(module, "ACCOUNT_UID", 1200)
|
||||
policy = module.HOST_ETC / "sudoers"
|
||||
policy.write_text(value, encoding="utf-8")
|
||||
validation = subprocess.run(
|
||||
["/usr/bin/visudo", "-c", "-f", str(policy)],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert validation.returncode == 0, validation.stderr
|
||||
|
||||
with pytest.raises(module.HardeningError, match="sudo authority"):
|
||||
module.reconcile(public_key)
|
||||
|
||||
assert all(
|
||||
(module.HOST_ETC / name).read_text() == original
|
||||
for name, original in originals.items()
|
||||
)
|
||||
|
||||
|
||||
def test_sudo_include_is_bounded_to_the_audited_standard_directory(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
sudoers = module.HOST_ETC / "sudoers"
|
||||
sudoers.write_text("#includedir /opt/external-sudoers\n", encoding="utf-8")
|
||||
with pytest.raises(module.HardeningError, match="unaudited authority source"):
|
||||
module.reconcile(public_key)
|
||||
|
||||
sudoers.write_text("#includedir /etc/sudoers.d\n", encoding="utf-8")
|
||||
module.reconcile(public_key)
|
||||
assert all(
|
||||
(module.HOST_ETC / name).read_text().startswith(original)
|
||||
for name, original in originals.items()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative,value",
|
||||
[
|
||||
(
|
||||
"polkit-1/rules.d/90-hermes.rules",
|
||||
"polkit.addRule(function(action, subject) {\n"
|
||||
" if (subject.uid == 1200) return polkit.Result.YES;\n"
|
||||
"});\n",
|
||||
),
|
||||
(
|
||||
"polkit-1/localauthority/50-local.d/hermes.pkla",
|
||||
"[Hermes]\nIdentity=unix-user:1200\n"
|
||||
"Action=org.freedesktop.policykit.exec\nResultActive=yes\n",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_numeric_polkit_grants_fail_closed(
|
||||
tmp_path: Path, monkeypatch, relative: str, value: str
|
||||
):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr(module, "ACCOUNT_UID", 1200)
|
||||
policy = module.HOST_ETC / relative
|
||||
policy.parent.mkdir(parents=True, exist_ok=True)
|
||||
policy.write_text(value, encoding="utf-8")
|
||||
|
||||
with pytest.raises(module.HardeningError, match="polkit authority"):
|
||||
module.reconcile(public_key)
|
||||
|
||||
assert all(
|
||||
(module.HOST_ETC / name).read_text() == original
|
||||
for name, original in originals.items()
|
||||
)
|
||||
|
||||
|
||||
def test_writable_privilege_policy_is_rejected(tmp_path: Path, monkeypatch):
|
||||
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
||||
sudoers = module.HOST_ETC / "sudoers"
|
||||
sudoers.write_text("root ALL=(ALL:ALL) ALL\n")
|
||||
sudoers.chmod(0o666)
|
||||
with pytest.raises(module.HardeningError, match="unsafe mutation"):
|
||||
module.reconcile(public_key)
|
||||
assert all(
|
||||
(module.HOST_ETC / name).read_text() == original
|
||||
for name, original in originals.items()
|
||||
)
|
||||
@ -1,90 +0,0 @@
|
||||
"""Shared fixtures for dedicated Hermes node-account contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
SCRIPT = ROOT / "services/hermes/scripts/node_account_hardening.py"
|
||||
sys.path.insert(0, str(SCRIPT.parent))
|
||||
|
||||
|
||||
def _key(material: bytes, comment: str = "") -> str:
|
||||
key_type = b"ssh-ed25519"
|
||||
blob = len(key_type).to_bytes(4, "big") + key_type + material
|
||||
suffix = f" {comment}" if comment else ""
|
||||
return "ssh-ed25519 " + base64.b64encode(blob).decode() + suffix
|
||||
|
||||
|
||||
def _typed_key(key_type: str, material: bytes, comment: str = "") -> str:
|
||||
encoded_type = key_type.encode()
|
||||
blob = len(encoded_type).to_bytes(4, "big") + encoded_type + material
|
||||
suffix = f" {comment}" if comment else ""
|
||||
return f"{key_type} {base64.b64encode(blob).decode()}{suffix}"
|
||||
|
||||
|
||||
def _load():
|
||||
spec = importlib.util.spec_from_file_location("node_account_hardening_test", SCRIPT)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _fixture(tmp_path: Path, monkeypatch):
|
||||
module = _load()
|
||||
host_etc = tmp_path / "etc"
|
||||
host_home = tmp_path / "home"
|
||||
host_etc.mkdir()
|
||||
host_home.mkdir()
|
||||
originals = {
|
||||
"passwd": (
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
"atlas:x:2000:2000:Atlas:/home/atlas:/bin/bash\n"
|
||||
"oceanus:x:2001:2001:Oceanus:/home/oceanus:/bin/bash\n"
|
||||
),
|
||||
"group": (
|
||||
"root:x:0:\n"
|
||||
"atlas:x:2000:\n"
|
||||
"oceanus:x:2001:\n"
|
||||
"disk:x:6:atlas\n"
|
||||
"sudo:x:27:oceanus\n"
|
||||
),
|
||||
"shadow": (
|
||||
"root:!:1:0:99999:7:::\n"
|
||||
"atlas:!:1:0:99999:7:::\n"
|
||||
"oceanus:!:1:0:99999:7:::\n"
|
||||
),
|
||||
"gshadow": (
|
||||
"root:!::\n"
|
||||
"atlas:!::\n"
|
||||
"oceanus:!::\n"
|
||||
"disk:!::atlas\n"
|
||||
"sudo:!::oceanus\n"
|
||||
),
|
||||
}
|
||||
for name, value in originals.items():
|
||||
(host_etc / name).write_text(value, encoding="utf-8")
|
||||
key = _key(b"synthetic-hermes-key")
|
||||
other = _key(b"human-operator-key")
|
||||
for user in ("atlas", "oceanus"):
|
||||
ssh = host_home / user / ".ssh"
|
||||
ssh.mkdir(parents=True)
|
||||
(ssh / "authorized_keys").write_text(
|
||||
f"{other} {user}\n{key}\n", encoding="utf-8"
|
||||
)
|
||||
public_key = tmp_path / "public-key"
|
||||
public_key.write_text(key + "\n", encoding="utf-8")
|
||||
monkeypatch.setattr(module, "HOST_ETC", host_etc)
|
||||
monkeypatch.setattr(module, "HOST_HOME", host_home)
|
||||
monkeypatch.setattr(module, "ACCOUNT_UID", os.getuid())
|
||||
monkeypatch.setattr(module, "ACCOUNT_GID", os.getgid())
|
||||
monkeypatch.setattr(module, "HOST_ROOT_UID", os.getuid())
|
||||
monkeypatch.setattr(module, "HOST_POLKIT_SHARE", tmp_path / "missing-polkit")
|
||||
monkeypatch.setattr(module, "_deny_sensitive_roots", lambda: None)
|
||||
return module, originals, key, other, public_key
|
||||
@ -1,14 +1,63 @@
|
||||
"""Atlas route and Git protocol contracts for the Hermes SCM broker."""
|
||||
"""Executable contracts for the credential-isolated Hermes SCM broker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from email.message import Message
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
SCRIPTS = ROOT / "services/hermes/scm-common/scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
|
||||
def _load(name: str):
|
||||
path = SCRIPTS / f"{name}.py"
|
||||
spec = importlib.util.spec_from_file_location(f"test_{name}", path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _load_path(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class Response:
|
||||
def __init__(self, body: bytes, *, status: int = 200, content_type: str):
|
||||
self.body = body
|
||||
self.stream = io.BytesIO(body)
|
||||
self.status = status
|
||||
self.headers = Message()
|
||||
self.headers["Content-Type"] = content_type
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def read(self, limit=-1):
|
||||
return self.stream.read(limit)
|
||||
|
||||
|
||||
def _receive_command(old: bytes, new: bytes, ref: bytes) -> bytes:
|
||||
command = old + b" " + new + b" " + ref + b"\x00report-status\n"
|
||||
return f"{len(command) + 4:04x}".encode() + command + b"0000PACK"
|
||||
|
||||
from testing.tests.test_hermes_scm_broker_support import (
|
||||
Response,
|
||||
_load,
|
||||
_receive_command,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"target",
|
||||
@ -112,6 +161,7 @@ def test_git_proxy_uses_fixed_origin_and_never_reflects_credential():
|
||||
content_type="application/x-git-upload-pack-result",
|
||||
),
|
||||
)
|
||||
|
||||
encoded = broker._credential_forms("runtime-sentinel")[1]
|
||||
with pytest.raises(broker.PolicyError, match="credential material"):
|
||||
broker._upstream_git_request(
|
||||
@ -126,6 +176,8 @@ def test_git_proxy_uses_fixed_origin_and_never_reflects_credential():
|
||||
content_type="application/x-git-upload-pack-result",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [201, 202, 204, 206, 301, 302, 307, 308])
|
||||
def test_git_proxy_requires_exact_http_200(status: int):
|
||||
broker = _load("scm_broker")
|
||||
@ -144,3 +196,396 @@ def test_git_proxy_requires_exact_http_200(status: int):
|
||||
content_type="application/x-git-upload-pack-result",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_agent_broker_client_sends_no_credential_or_authorization_header():
|
||||
client = _load("scm_broker_client")
|
||||
seen = []
|
||||
|
||||
def opener(request, timeout):
|
||||
seen.append((request, timeout))
|
||||
return Response(b"{}", content_type="application/json")
|
||||
|
||||
assert client.read(
|
||||
"/api/v1/repos/atlas/cassandra", opener=opener
|
||||
) == b"{}"
|
||||
request = seen[0][0]
|
||||
assert request.full_url == client.BROKER_ORIGIN + "/v1/metadata"
|
||||
assert request.get_header("Authorization") is None
|
||||
assert json.loads(request.data) == {
|
||||
"path": "/api/v1/repos/atlas/cassandra"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", ["", "01", "9" * 4000, "134217729", "12"])
|
||||
def test_broker_content_length_is_canonical_and_bounded(raw: str):
|
||||
broker = _load("scm_broker")
|
||||
headers = Message()
|
||||
headers["Content-Length"] = raw
|
||||
|
||||
with pytest.raises(broker.PolicyError, match="request length"):
|
||||
broker._content_length(headers, broker.MAX_GIT_REQUEST)
|
||||
|
||||
|
||||
def test_large_git_exchange_rolls_to_disk_and_scans_chunk_boundaries():
|
||||
broker = _load("scm_broker")
|
||||
token = "runtime-sentinel"
|
||||
safe = b"x" * (broker.SPOOL_MEMORY_LIMIT + 1)
|
||||
inbound_timeouts = []
|
||||
spool, length = broker._spool_bounded(
|
||||
io.BytesIO(safe),
|
||||
broker.MAX_GIT_REQUEST,
|
||||
len(safe),
|
||||
token=token,
|
||||
context="Git request",
|
||||
deadline=broker.time.monotonic() + 30,
|
||||
set_timeout=inbound_timeouts.append,
|
||||
)
|
||||
try:
|
||||
assert length == len(safe)
|
||||
assert spool._rolled is True
|
||||
assert spool.read(4) == b"xxxx"
|
||||
assert inbound_timeouts
|
||||
assert all(0 < value <= 30 for value in inbound_timeouts)
|
||||
finally:
|
||||
spool.close()
|
||||
|
||||
crossing = b"x" * (broker.STREAM_CHUNK - 5) + token.encode() + b"tail"
|
||||
with pytest.raises(broker.PolicyError, match="credential material"):
|
||||
broker._spool_bounded(
|
||||
io.BytesIO(crossing),
|
||||
broker.MAX_GIT_REQUEST,
|
||||
len(crossing),
|
||||
token=token,
|
||||
context="Git request",
|
||||
deadline=broker.time.monotonic() + 30,
|
||||
)
|
||||
|
||||
|
||||
def test_large_upstream_response_is_streamed_through_bounded_spool():
|
||||
broker = _load("scm_broker")
|
||||
body = b"z" * (broker.SPOOL_MEMORY_LIMIT + 1)
|
||||
seen = []
|
||||
|
||||
def opener(request, **_kwargs):
|
||||
seen.append(request)
|
||||
assert request.get_header("Content-length") == "7"
|
||||
assert request.data.read() == b"request"
|
||||
return Response(body, content_type="application/x-git-upload-pack-result")
|
||||
|
||||
streamed = broker._upstream_git_request(
|
||||
"/atlas/cassandra.git/git-upload-pack",
|
||||
method="POST",
|
||||
body=io.BytesIO(b"request"),
|
||||
body_length=7,
|
||||
content_type="application/x-git-upload-pack-request",
|
||||
expected_type="application/x-git-upload-pack-result",
|
||||
token="runtime-sentinel",
|
||||
stream_result=True,
|
||||
opener=opener,
|
||||
)
|
||||
spool, length = streamed
|
||||
try:
|
||||
assert length == len(body)
|
||||
assert spool._rolled is True
|
||||
assert spool.read(3) == b"zzz"
|
||||
assert len(seen) == 1
|
||||
finally:
|
||||
spool.close()
|
||||
|
||||
|
||||
def test_body_deadline_fails_closed_before_reading():
|
||||
broker = _load("scm_broker")
|
||||
with pytest.raises(broker.PolicyError, match="deadline"):
|
||||
broker._spool_bounded(
|
||||
io.BytesIO(b"body"),
|
||||
broker.MAX_GIT_REQUEST,
|
||||
4,
|
||||
token="runtime-sentinel",
|
||||
context="Git request",
|
||||
deadline=broker.time.monotonic() - 1,
|
||||
)
|
||||
|
||||
|
||||
def test_upstream_stream_applies_absolute_socket_deadline():
|
||||
broker = _load("scm_broker")
|
||||
|
||||
class DeadlineSocket:
|
||||
def __init__(self):
|
||||
self.values = []
|
||||
|
||||
def settimeout(self, value):
|
||||
self.values.append(value)
|
||||
|
||||
response = Response(b"safe", content_type="application/x-git-upload-pack-result")
|
||||
sock = DeadlineSocket()
|
||||
response.fp = type("FP", (), {"raw": type("Raw", (), {"_sock": sock})()})()
|
||||
spool, length = broker._spool_response(response, broker.MAX_GIT_RESPONSE, "sentinel")
|
||||
try:
|
||||
assert length == 4
|
||||
assert sock.values
|
||||
assert all(0 < value <= broker.INBOUND_BODY_TIMEOUT for value in sock.values)
|
||||
finally:
|
||||
spool.close()
|
||||
|
||||
|
||||
def test_broker_rejects_work_when_concurrency_slots_are_exhausted():
|
||||
broker = _load("scm_broker")
|
||||
|
||||
class FakeSocket:
|
||||
def __init__(self):
|
||||
self.value = b""
|
||||
|
||||
def sendall(self, value):
|
||||
self.value += value
|
||||
|
||||
def shutdown(self, _how):
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
server = broker.BoundedThreadingHTTPServer(("127.0.0.1", 0), broker.BrokerHandler)
|
||||
try:
|
||||
assert server._slots.acquire(blocking=False)
|
||||
assert server._slots.acquire(blocking=False)
|
||||
request = FakeSocket()
|
||||
server.process_request(request, ("127.0.0.1", 1))
|
||||
assert b"503 Service Unavailable" in request.value
|
||||
finally:
|
||||
server._slots.release()
|
||||
server._slots.release()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def test_broker_bounds_header_count_size_and_duplicate_lengths():
|
||||
broker = _load("scm_broker")
|
||||
|
||||
def validate(headers):
|
||||
handler = object.__new__(broker.BrokerHandler)
|
||||
handler.headers = headers
|
||||
handler._validate_headers()
|
||||
|
||||
too_many = Message()
|
||||
for index in range(broker.MAX_HEADERS + 1):
|
||||
too_many[f"X-Test-{index}"] = "safe"
|
||||
with pytest.raises(broker.PolicyError, match="too many headers"):
|
||||
validate(too_many)
|
||||
|
||||
too_large = Message()
|
||||
too_large["X-Test"] = "x" * broker.MAX_HEADER_BYTES
|
||||
with pytest.raises(broker.PolicyError, match="safe limit"):
|
||||
validate(too_large)
|
||||
|
||||
duplicate = Message()
|
||||
duplicate["Content-Length"] = "1"
|
||||
duplicate["Content-Length"] = "1"
|
||||
with pytest.raises(broker.PolicyError, match="duplicate Content-Length"):
|
||||
validate(duplicate)
|
||||
|
||||
|
||||
def _can_i(role: dict, api_group: str, resource: str, verb: str) -> bool:
|
||||
return any(
|
||||
(api_group in rule["apiGroups"] or "*" in rule["apiGroups"])
|
||||
and (resource in rule["resources"] or "*" in rule["resources"])
|
||||
and (verb in rule["verbs"] or "*" in rule["verbs"])
|
||||
for rule in role["rules"]
|
||||
)
|
||||
|
||||
|
||||
def test_agent_rbac_preserves_read_diagnostics_without_administrator_authority():
|
||||
documents = list(
|
||||
yaml.safe_load_all(
|
||||
(ROOT / "services/hermes-observer-rbac/rbac.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
)
|
||||
cluster_role, namespaced_role, binding = documents
|
||||
assert binding["roleRef"]["name"] == "hermes-agent-cluster-observer-v2"
|
||||
assert _can_i(cluster_role, "", "nodes", "get")
|
||||
assert _can_i(namespaced_role, "", "pods", "get")
|
||||
assert _can_i(namespaced_role, "", "pods/log", "get")
|
||||
assert _can_i(
|
||||
namespaced_role,
|
||||
"kustomize.toolkit.fluxcd.io",
|
||||
"kustomizations",
|
||||
"list",
|
||||
)
|
||||
|
||||
denied = (
|
||||
("", "secrets", "get"),
|
||||
("", "serviceaccounts/token", "create"),
|
||||
("", "pods", "create"),
|
||||
("", "pods/exec", "create"),
|
||||
("", "pods/attach", "create"),
|
||||
("", "pods/portforward", "create"),
|
||||
("rbac.authorization.k8s.io", "clusterrolebindings", "create"),
|
||||
("authorization.k8s.io", "selfsubjectaccessreviews", "create"),
|
||||
("apps", "deployments", "patch"),
|
||||
)
|
||||
assert all(
|
||||
not _can_i(role, *request)
|
||||
for request in denied
|
||||
for role in (cluster_role, namespaced_role)
|
||||
)
|
||||
|
||||
bindings = yaml.safe_load(
|
||||
(ROOT / "services/hermes-observer-rbac/rolebindings.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)["items"]
|
||||
namespaces = {item["metadata"]["namespace"] for item in bindings}
|
||||
assert "hermes-scm" not in namespaces
|
||||
assert {"cassandra", "flux-system", "hermes", "kube-system"} <= namespaces
|
||||
assert all(item["roleRef"]["name"].endswith("namespaced-observer-v2") for item in bindings)
|
||||
|
||||
|
||||
def test_flux_boundary_keeps_broker_secret_and_network_separate_from_agent():
|
||||
agent = yaml.safe_load(
|
||||
(ROOT / "services/hermes/agent-deployment.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
annotations = agent["spec"]["template"]["metadata"]["annotations"]
|
||||
assert not any("gitea" in key.lower() for key in annotations)
|
||||
assert "GIT_ASKPASS" not in json.dumps(agent)
|
||||
|
||||
broker = yaml.safe_load(
|
||||
(ROOT / "services/hermes-scm-broker/deployment.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
assert broker["metadata"]["namespace"] == "hermes-scm"
|
||||
assert broker["spec"]["template"]["spec"]["serviceAccountName"] == (
|
||||
"hermes-scm-broker"
|
||||
)
|
||||
assert "gitea-token" in json.dumps(broker)
|
||||
container = broker["spec"]["template"]["spec"]["containers"][0]
|
||||
assert container["resources"]["limits"]["memory"] == "768Mi"
|
||||
assert container["livenessProbe"]["failureThreshold"] == 10
|
||||
tmp = next(
|
||||
item for item in broker["spec"]["template"]["spec"]["volumes"] if item["name"] == "tmp"
|
||||
)
|
||||
assert tmp["emptyDir"]["sizeLimit"] == "1Gi"
|
||||
|
||||
policy = yaml.safe_load(
|
||||
(ROOT / "services/hermes-scm-broker/networkpolicy.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
ingress = policy["spec"]["ingress"]
|
||||
assert ingress[0]["from"][0]["namespaceSelector"]["matchLabels"] == {
|
||||
"kubernetes.io/metadata.name": "hermes"
|
||||
}
|
||||
assert ingress[0]["from"][0]["podSelector"]["matchLabels"] == {
|
||||
"app": "hermes-agent"
|
||||
}
|
||||
|
||||
vault = (
|
||||
ROOT / "services/vault/scripts/vault_k8s_auth_configure.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
assert 'write_policy_and_role "hermes-scm-broker" "hermes-scm"' in vault
|
||||
assert '"hermes/developer-gitea" ""' in vault
|
||||
agent_start = vault.index('write_policy_and_role "hermes-agent"')
|
||||
agent_end = vault.index("write_policy_and_role", agent_start + 1)
|
||||
assert "developer-gitea" not in vault[agent_start:agent_end]
|
||||
|
||||
|
||||
def test_flux_bootstrap_has_no_agent_namespace_dependency_cycle():
|
||||
hermes_resources = yaml.safe_load(
|
||||
(ROOT / "services/hermes/kustomization.yaml").read_text(encoding="utf-8")
|
||||
)["resources"]
|
||||
assert "namespace.yaml" in hermes_resources
|
||||
assert "scm-common" in hermes_resources
|
||||
|
||||
applications = (
|
||||
ROOT / "clusters/atlas/flux-system/applications/kustomization.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "hermes-scm-agent-code" not in applications
|
||||
|
||||
broker_code = yaml.safe_load(
|
||||
(
|
||||
ROOT
|
||||
/ "clusters/atlas/flux-system/applications/hermes-scm-broker-code/kustomization.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
assert broker_code["spec"]["path"] == "./services/hermes/scm-common"
|
||||
assert broker_code["spec"]["targetNamespace"] == "hermes-scm"
|
||||
assert broker_code["spec"]["dependsOn"] == [{"name": "hermes-scm-namespace"}]
|
||||
|
||||
|
||||
def test_gitea_bootstrap_enforces_human_review_without_overwriting_drift():
|
||||
script = (
|
||||
ROOT / "services/gitea/scripts/gitea_atlas_identity_ensure.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert 'required_approvals\\\":1' in script
|
||||
assert 'enable_push_whitelist\\\":true' in script
|
||||
assert 'push_whitelist_usernames\\\":[\\\"${protected_reviewer}\\\"]' in script
|
||||
assert 'enable_merge_whitelist\\\":true' in script
|
||||
assert 'merge_whitelist_usernames\\\":[\\\"${protected_reviewer}\\\"]' in script
|
||||
assert "protection differs from the human-review policy" in script
|
||||
assert "api_request PATCH" not in script
|
||||
job = yaml.safe_load(
|
||||
(ROOT / "services/gitea/atlas-identity-bootstrap-job.yaml").read_text()
|
||||
)
|
||||
init = job["spec"]["template"]["spec"]["initContainers"][0]
|
||||
assert init["image"] == (
|
||||
"python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df"
|
||||
)
|
||||
assert init["securityContext"]["runAsNonRoot"] is True
|
||||
assert init["securityContext"]["capabilities"]["drop"] == ["ALL"]
|
||||
assert "/opt/python/bin/python3" in script
|
||||
|
||||
|
||||
def _protection(rule_name: str, priority: int, helper, **overrides):
|
||||
value = {"rule_name": rule_name, "priority": priority, **helper._required("bstein")}
|
||||
value.update(overrides)
|
||||
return value
|
||||
|
||||
|
||||
def test_branch_protection_uses_first_effective_glob_by_priority():
|
||||
helper = _load_path(
|
||||
"branch_protection_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
earlier_drift = _protection("m*", 1, helper, required_approvals=0)
|
||||
later_exact = _protection("main", 2, helper)
|
||||
|
||||
with pytest.raises(helper.PolicyError, match="effective main protection differs"):
|
||||
helper.evaluate(json.dumps([later_exact, earlier_drift]).encode(), "main", "bstein")
|
||||
|
||||
earlier_drift["required_approvals"] = 1
|
||||
assert (
|
||||
helper.evaluate(json.dumps([later_exact, earlier_drift]).encode(), "main", "bstein")
|
||||
== "PRESENT"
|
||||
)
|
||||
brace_drift = _protection("m{ain,aster}", 1, helper, required_approvals=0)
|
||||
with pytest.raises(helper.PolicyError, match="effective master protection differs"):
|
||||
helper.evaluate(json.dumps([brace_drift]).encode(), "master", "bstein")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rules",
|
||||
[
|
||||
[{"rule_name": "main", "priority": 1}, {"rule_name": "m*", "priority": 1}],
|
||||
[{"rule_name": "[main", "priority": 1}],
|
||||
[{"rule_name": "main", "priority": 0}],
|
||||
[{"rule_name": "main"}],
|
||||
],
|
||||
)
|
||||
def test_branch_protection_rejects_ambiguous_rules_before_creation(rules):
|
||||
helper = _load_path(
|
||||
"branch_protection_adversarial_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
with pytest.raises(helper.PolicyError):
|
||||
helper.evaluate(json.dumps(rules).encode(), "main", "bstein")
|
||||
|
||||
|
||||
def test_branch_protection_reports_absent_only_when_no_rule_matches():
|
||||
helper = _load_path(
|
||||
"branch_protection_absent_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
rules = [_protection("release/*", 1, helper)]
|
||||
assert helper.evaluate(json.dumps(rules).encode(), "master", "bstein") == "ABSENT"
|
||||
|
||||
@ -1,321 +0,0 @@
|
||||
"""RBAC, Flux, and human-review contracts for the Hermes SCM broker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from testing.tests.test_hermes_scm_broker_support import ROOT, _load_path
|
||||
|
||||
def _can_i(role: dict, api_group: str, resource: str, verb: str) -> bool:
|
||||
return any(
|
||||
(api_group in rule["apiGroups"] or "*" in rule["apiGroups"])
|
||||
and (resource in rule["resources"] or "*" in rule["resources"])
|
||||
and (verb in rule["verbs"] or "*" in rule["verbs"])
|
||||
for rule in role["rules"]
|
||||
)
|
||||
|
||||
|
||||
def test_agent_rbac_preserves_read_diagnostics_without_administrator_authority():
|
||||
documents = list(
|
||||
yaml.safe_load_all(
|
||||
(ROOT / "services/hermes-observer-rbac/rbac.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
)
|
||||
cluster_role, namespaced_role, binding = documents
|
||||
assert binding["roleRef"]["name"] == "hermes-agent-cluster-observer-v2"
|
||||
assert _can_i(cluster_role, "", "nodes", "get")
|
||||
assert _can_i(namespaced_role, "", "pods", "get")
|
||||
assert _can_i(namespaced_role, "", "pods/log", "get")
|
||||
assert _can_i(
|
||||
namespaced_role,
|
||||
"kustomize.toolkit.fluxcd.io",
|
||||
"kustomizations",
|
||||
"list",
|
||||
)
|
||||
|
||||
denied = (
|
||||
("", "secrets", "get"),
|
||||
("", "serviceaccounts/token", "create"),
|
||||
("", "pods", "create"),
|
||||
("", "pods/exec", "create"),
|
||||
("", "pods/attach", "create"),
|
||||
("", "pods/portforward", "create"),
|
||||
("rbac.authorization.k8s.io", "clusterrolebindings", "create"),
|
||||
("authorization.k8s.io", "selfsubjectaccessreviews", "create"),
|
||||
("apps", "deployments", "patch"),
|
||||
)
|
||||
assert all(
|
||||
not _can_i(role, *request)
|
||||
for request in denied
|
||||
for role in (cluster_role, namespaced_role)
|
||||
)
|
||||
|
||||
bindings = yaml.safe_load(
|
||||
(ROOT / "services/hermes-observer-bindings/rolebindings.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)["items"]
|
||||
namespaces = {item["metadata"]["namespace"] for item in bindings}
|
||||
assert "hermes-scm" not in namespaces
|
||||
assert {"cassandra", "flux-system", "hermes", "kube-system"} <= namespaces
|
||||
assert all(item["roleRef"]["name"].endswith("namespaced-observer-v2") for item in bindings)
|
||||
|
||||
|
||||
def test_flux_boundary_keeps_broker_secret_and_network_separate_from_agent():
|
||||
agent = yaml.safe_load(
|
||||
(ROOT / "services/hermes/agent-deployment.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
annotations = agent["spec"]["template"]["metadata"]["annotations"]
|
||||
assert not any("gitea" in key.lower() for key in annotations)
|
||||
assert "GIT_ASKPASS" not in json.dumps(agent)
|
||||
|
||||
broker = yaml.safe_load(
|
||||
(ROOT / "services/hermes-scm-broker/deployment.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
assert broker["metadata"]["namespace"] == "hermes-scm"
|
||||
assert broker["spec"]["template"]["spec"]["serviceAccountName"] == (
|
||||
"hermes-scm-broker"
|
||||
)
|
||||
assert "gitea-token" in json.dumps(broker)
|
||||
container = broker["spec"]["template"]["spec"]["containers"][0]
|
||||
assert container["resources"]["limits"]["memory"] == "768Mi"
|
||||
assert container["livenessProbe"]["failureThreshold"] == 10
|
||||
tmp = next(
|
||||
item for item in broker["spec"]["template"]["spec"]["volumes"] if item["name"] == "tmp"
|
||||
)
|
||||
assert tmp["emptyDir"]["sizeLimit"] == "1Gi"
|
||||
|
||||
policy = yaml.safe_load(
|
||||
(ROOT / "services/hermes-scm-broker/networkpolicy.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
ingress = policy["spec"]["ingress"]
|
||||
assert ingress[0]["from"][0]["namespaceSelector"]["matchLabels"] == {
|
||||
"kubernetes.io/metadata.name": "hermes"
|
||||
}
|
||||
assert ingress[0]["from"][0]["podSelector"]["matchLabels"] == {
|
||||
"app": "hermes-agent"
|
||||
}
|
||||
|
||||
vault = (
|
||||
ROOT / "services/vault/scripts/vault_k8s_auth_configure.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
assert 'write_policy_and_role "hermes-scm-broker" "hermes-scm"' in vault
|
||||
assert '"hermes/developer-gitea" ""' in vault
|
||||
agent_start = vault.index('write_policy_and_role "hermes-agent"')
|
||||
agent_end = vault.index("write_policy_and_role", agent_start + 1)
|
||||
assert "developer-gitea" not in vault[agent_start:agent_end]
|
||||
|
||||
|
||||
def test_flux_bootstrap_has_no_agent_namespace_dependency_cycle():
|
||||
hermes_resources = yaml.safe_load(
|
||||
(ROOT / "services/hermes/kustomization.yaml").read_text(encoding="utf-8")
|
||||
)["resources"]
|
||||
assert "namespace.yaml" in hermes_resources
|
||||
assert "scm-common" in hermes_resources
|
||||
|
||||
observer = yaml.safe_load(
|
||||
(
|
||||
ROOT
|
||||
/ "clusters/atlas/flux-system/applications/hermes-observer-rbac/kustomization.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
assert "dependsOn" not in observer["spec"]
|
||||
observer_objects = [
|
||||
item
|
||||
for item in yaml.safe_load_all(
|
||||
(ROOT / "services/hermes-observer-rbac/rbac.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
if item
|
||||
]
|
||||
assert {item["kind"] for item in observer_objects} <= {
|
||||
"ClusterRole",
|
||||
"ClusterRoleBinding",
|
||||
}
|
||||
|
||||
hermes_flux = yaml.safe_load(
|
||||
(
|
||||
ROOT / "clusters/atlas/flux-system/applications/hermes/kustomization.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
assert "hermes-observer-rbac" in {
|
||||
item["name"] for item in hermes_flux["spec"]["dependsOn"]
|
||||
}
|
||||
bindings_flux = yaml.safe_load(
|
||||
(
|
||||
ROOT
|
||||
/ "clusters/atlas/flux-system/applications/hermes-observer-bindings/kustomization.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
assert {item["name"] for item in bindings_flux["spec"]["dependsOn"]} == {
|
||||
"hermes",
|
||||
"hermes-observer-rbac",
|
||||
}
|
||||
assert bindings_flux["metadata"]["name"] not in {
|
||||
item["name"] for item in hermes_flux["spec"]["dependsOn"]
|
||||
}
|
||||
|
||||
applications = (
|
||||
ROOT / "clusters/atlas/flux-system/applications/kustomization.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "hermes-scm-agent-code" not in applications
|
||||
|
||||
broker_code = yaml.safe_load(
|
||||
(
|
||||
ROOT
|
||||
/ "clusters/atlas/flux-system/applications/hermes-scm-broker-code/kustomization.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
assert broker_code["spec"]["path"] == "./services/hermes/scm-common"
|
||||
assert broker_code["spec"]["targetNamespace"] == "hermes-scm"
|
||||
assert broker_code["spec"]["dependsOn"] == [{"name": "hermes-scm-namespace"}]
|
||||
|
||||
|
||||
def test_gitea_bootstrap_enforces_human_review_without_overwriting_drift():
|
||||
script = (
|
||||
ROOT / "services/gitea/scripts/gitea_atlas_identity_ensure.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert 'required_approvals\\\":1' in script
|
||||
assert 'enable_push_whitelist\\\":true' in script
|
||||
assert 'push_whitelist_usernames\\\":[\\\"${protected_reviewer}\\\"]' in script
|
||||
assert 'enable_merge_whitelist\\\":true' in script
|
||||
assert 'merge_whitelist_usernames\\\":[\\\"${protected_reviewer}\\\"]' in script
|
||||
assert "protection differs from the human-review policy" in script
|
||||
assert "api_request PATCH" not in script
|
||||
job = yaml.safe_load(
|
||||
(ROOT / "services/gitea/atlas-identity-bootstrap-job.yaml").read_text()
|
||||
)
|
||||
init = job["spec"]["template"]["spec"]["initContainers"][0]
|
||||
assert init["image"] == (
|
||||
"python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df"
|
||||
)
|
||||
assert init["securityContext"]["runAsNonRoot"] is True
|
||||
assert init["securityContext"]["capabilities"]["drop"] == ["ALL"]
|
||||
assert "/opt/python/bin/python3" in script
|
||||
|
||||
|
||||
def _protection(rule_name: str, priority: int, helper, **overrides):
|
||||
value = {
|
||||
"rule_name": rule_name,
|
||||
"priority": priority,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
**helper._required("bstein"),
|
||||
}
|
||||
value.update(overrides)
|
||||
return value
|
||||
|
||||
|
||||
def test_branch_protection_uses_first_effective_glob_by_priority():
|
||||
helper = _load_path(
|
||||
"branch_protection_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
earlier_drift = _protection("m*", 1, helper, required_approvals=0)
|
||||
later_exact = _protection("main", 2, helper)
|
||||
|
||||
with pytest.raises(helper.PolicyError, match="effective main protection differs"):
|
||||
helper.evaluate(json.dumps([later_exact, earlier_drift]).encode(), "main", "bstein")
|
||||
|
||||
earlier_drift["required_approvals"] = 1
|
||||
assert (
|
||||
helper.evaluate(json.dumps([later_exact, earlier_drift]).encode(), "main", "bstein")
|
||||
== "PRESENT"
|
||||
)
|
||||
brace_drift = _protection("m{ain,aster}", 1, helper, required_approvals=0)
|
||||
with pytest.raises(helper.PolicyError, match="effective master protection differs"):
|
||||
helper.evaluate(json.dumps([brace_drift]).encode(), "master", "bstein")
|
||||
|
||||
upper_glob = _protection("M*", 1, helper, required_approvals=0)
|
||||
assert (
|
||||
helper.evaluate(json.dumps([upper_glob, later_exact]).encode(), "main", "bstein")
|
||||
== "PRESENT"
|
||||
)
|
||||
|
||||
|
||||
def test_branch_protection_matches_gitea_v1238_tie_order():
|
||||
helper = _load_path(
|
||||
"branch_protection_order_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
older_glob = _protection(
|
||||
"m*", 1, helper, created_at="2025-01-01T00:00:00Z", required_approvals=0
|
||||
)
|
||||
newer_plain = _protection(
|
||||
"MAIN", 1, helper, created_at="2026-01-01T00:00:00Z"
|
||||
)
|
||||
assert (
|
||||
helper.evaluate(json.dumps([older_glob, newer_plain]).encode(), "main", "bstein")
|
||||
== "PRESENT"
|
||||
)
|
||||
|
||||
older_glob["created_at"] = "2026-01-02T00:00:00Z"
|
||||
earlier_glob = _protection(
|
||||
"m{ain,aster}",
|
||||
1,
|
||||
helper,
|
||||
created_at="2026-01-01T00:00:00Z",
|
||||
required_approvals=0,
|
||||
)
|
||||
with pytest.raises(helper.PolicyError, match="effective main protection differs"):
|
||||
helper.evaluate(json.dumps([older_glob, earlier_glob]).encode(), "main", "bstein")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rule_name", "branch", "expected"),
|
||||
[
|
||||
("release/*", "release/v1.17", True),
|
||||
("release/**/v1.17", "release/test/1/v1.17", True),
|
||||
("release/*/v1.17", "release/test/1/v1.17", False),
|
||||
("*", "release/v1.16", False),
|
||||
("**", "release/v1.16", True),
|
||||
("MAIN", "main", True),
|
||||
("M*", "main", False),
|
||||
("m{ain,aster}", "master", True),
|
||||
(r"m\ain", "main", True),
|
||||
],
|
||||
)
|
||||
def test_branch_glob_matches_gitea_v1238_source_corpus(
|
||||
rule_name: str, branch: str, expected: bool
|
||||
):
|
||||
helper = _load_path(
|
||||
"branch_protection_glob_parity_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
assert helper._matches(rule_name, branch) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rules",
|
||||
[
|
||||
[{"rule_name": "main", "priority": 0, "created_at": "2026-01-01T00:00:00Z"}],
|
||||
[{"rule_name": "main", "priority": 1, "created_at": "not-a-time"}],
|
||||
[{"rule_name": "main"}],
|
||||
],
|
||||
)
|
||||
def test_branch_protection_rejects_ambiguous_rules_before_creation(rules):
|
||||
helper = _load_path(
|
||||
"branch_protection_adversarial_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
with pytest.raises(helper.PolicyError):
|
||||
helper.evaluate(json.dumps(rules).encode(), "main", "bstein")
|
||||
|
||||
|
||||
def test_branch_protection_reports_absent_only_when_no_rule_matches():
|
||||
helper = _load_path(
|
||||
"branch_protection_absent_test",
|
||||
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
||||
)
|
||||
rules = [_protection("release/*", 1, helper)]
|
||||
assert helper.evaluate(json.dumps(rules).encode(), "master", "bstein") == "ABSENT"
|
||||
@ -1,237 +0,0 @@
|
||||
"""Bounded I/O and concurrency contracts for the Hermes SCM broker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
from email.message import Message
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.tests.test_hermes_scm_broker_support import Response, _load
|
||||
|
||||
|
||||
def test_agent_broker_client_sends_no_credential_or_authorization_header():
|
||||
client = _load("scm_broker_client")
|
||||
seen = []
|
||||
|
||||
def opener(request, timeout):
|
||||
seen.append((request, timeout))
|
||||
return Response(b"{}", content_type="application/json")
|
||||
|
||||
assert client.read("/api/v1/repos/atlas/cassandra", opener=opener) == b"{}"
|
||||
request = seen[0][0]
|
||||
assert request.full_url == client.BROKER_ORIGIN + "/v1/metadata"
|
||||
assert request.get_header("Authorization") is None
|
||||
assert json.loads(request.data) == {"path": "/api/v1/repos/atlas/cassandra"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", ["", "01", "9" * 4000, "134217729", "12"])
|
||||
def test_broker_content_length_is_canonical_and_bounded(raw: str):
|
||||
broker = _load("scm_broker")
|
||||
headers = Message()
|
||||
headers["Content-Length"] = raw
|
||||
|
||||
with pytest.raises(broker.PolicyError, match="request length"):
|
||||
broker._content_length(headers, broker.MAX_GIT_REQUEST)
|
||||
|
||||
|
||||
def test_large_git_exchange_rolls_to_disk_and_scans_chunk_boundaries():
|
||||
broker = _load("scm_broker")
|
||||
token = "runtime-sentinel"
|
||||
safe = b"x" * (broker.SPOOL_MEMORY_LIMIT + 1)
|
||||
inbound_timeouts = []
|
||||
spool, length = broker._spool_bounded(
|
||||
io.BytesIO(safe),
|
||||
broker.MAX_GIT_REQUEST,
|
||||
len(safe),
|
||||
token=token,
|
||||
context="Git request",
|
||||
deadline=broker.time.monotonic() + 30,
|
||||
set_timeout=inbound_timeouts.append,
|
||||
)
|
||||
try:
|
||||
assert length == len(safe)
|
||||
assert spool._rolled is True
|
||||
assert spool.read(4) == b"xxxx"
|
||||
assert inbound_timeouts
|
||||
assert all(0 < value <= 30 for value in inbound_timeouts)
|
||||
finally:
|
||||
spool.close()
|
||||
|
||||
crossing = b"x" * (broker.STREAM_CHUNK - 5) + token.encode() + b"tail"
|
||||
with pytest.raises(broker.PolicyError, match="credential material"):
|
||||
broker._spool_bounded(
|
||||
io.BytesIO(crossing),
|
||||
broker.MAX_GIT_REQUEST,
|
||||
len(crossing),
|
||||
token=token,
|
||||
context="Git request",
|
||||
deadline=broker.time.monotonic() + 30,
|
||||
)
|
||||
|
||||
|
||||
def test_large_upstream_response_is_streamed_through_bounded_spool():
|
||||
broker = _load("scm_broker")
|
||||
body = b"z" * (broker.SPOOL_MEMORY_LIMIT + 1)
|
||||
seen = []
|
||||
|
||||
def opener(request, **_kwargs):
|
||||
seen.append(request)
|
||||
assert request.get_header("Content-length") == "7"
|
||||
assert request.data.read() == b"request"
|
||||
return Response(body, content_type="application/x-git-upload-pack-result")
|
||||
|
||||
streamed = broker._upstream_git_request(
|
||||
"/atlas/cassandra.git/git-upload-pack",
|
||||
method="POST",
|
||||
body=io.BytesIO(b"request"),
|
||||
body_length=7,
|
||||
content_type="application/x-git-upload-pack-request",
|
||||
expected_type="application/x-git-upload-pack-result",
|
||||
token="runtime-sentinel",
|
||||
stream_result=True,
|
||||
opener=opener,
|
||||
)
|
||||
spool, length = streamed
|
||||
try:
|
||||
assert length == len(body)
|
||||
assert spool._rolled is True
|
||||
assert spool.read(3) == b"zzz"
|
||||
assert len(seen) == 1
|
||||
finally:
|
||||
spool.close()
|
||||
|
||||
|
||||
def test_body_deadline_fails_closed_before_reading():
|
||||
broker = _load("scm_broker")
|
||||
with pytest.raises(broker.PolicyError, match="deadline"):
|
||||
broker._spool_bounded(
|
||||
io.BytesIO(b"body"),
|
||||
broker.MAX_GIT_REQUEST,
|
||||
4,
|
||||
token="runtime-sentinel",
|
||||
context="Git request",
|
||||
deadline=broker.time.monotonic() - 1,
|
||||
)
|
||||
|
||||
|
||||
def test_upstream_stream_applies_absolute_socket_deadline():
|
||||
broker = _load("scm_broker")
|
||||
|
||||
class DeadlineSocket:
|
||||
def __init__(self):
|
||||
self.values = []
|
||||
|
||||
def settimeout(self, value):
|
||||
self.values.append(value)
|
||||
|
||||
response = Response(b"safe", content_type="application/x-git-upload-pack-result")
|
||||
sock = DeadlineSocket()
|
||||
response.fp = type("FP", (), {"raw": type("Raw", (), {"_sock": sock})()})()
|
||||
spool, length = broker._spool_response(
|
||||
response, broker.MAX_GIT_RESPONSE, "sentinel"
|
||||
)
|
||||
try:
|
||||
assert length == 4
|
||||
assert sock.values
|
||||
assert all(0 < value <= broker.INBOUND_BODY_TIMEOUT for value in sock.values)
|
||||
finally:
|
||||
spool.close()
|
||||
|
||||
|
||||
def test_broker_rejects_work_when_concurrency_slots_are_exhausted():
|
||||
broker = _load("scm_broker")
|
||||
|
||||
class FakeSocket:
|
||||
def __init__(self):
|
||||
self.value = b""
|
||||
|
||||
def sendall(self, value):
|
||||
self.value += value
|
||||
|
||||
def shutdown(self, _how):
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
server = broker.BoundedThreadingHTTPServer(("127.0.0.1", 0), broker.BrokerHandler)
|
||||
try:
|
||||
assert broker.BoundedThreadingHTTPServer.__module__ == "scm_broker_server"
|
||||
assert server._slots.acquire(blocking=False)
|
||||
assert server._slots.acquire(blocking=False)
|
||||
assert server._slots.acquire(blocking=False)
|
||||
request = FakeSocket()
|
||||
server.process_request(request, ("127.0.0.1", 1))
|
||||
assert b"503 Service Unavailable" in request.value
|
||||
finally:
|
||||
server._slots.release()
|
||||
server._slots.release()
|
||||
server._slots.release()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def test_three_simultaneous_broker_calls_have_bounded_capacity():
|
||||
server_module = _load("scm_broker_server")
|
||||
semaphore = server_module.threading.BoundedSemaphore(
|
||||
server_module.MAX_CONCURRENT_REQUESTS
|
||||
)
|
||||
assert server_module.MAX_CONCURRENT_REQUESTS >= 3
|
||||
assert [semaphore.acquire(blocking=False) for _ in range(3)] == [True, True, True]
|
||||
assert semaphore.acquire(blocking=False) is False
|
||||
|
||||
|
||||
def test_header_deadline_is_absolute_against_slow_trickle(monkeypatch):
|
||||
server_module = _load("scm_broker_server")
|
||||
|
||||
class Stream:
|
||||
def __init__(self):
|
||||
self.value = io.BytesIO(b"GET /healthz HTTP/1.1\r\n")
|
||||
|
||||
def read(self, length):
|
||||
return self.value.read(length)
|
||||
|
||||
class Connection:
|
||||
def __init__(self):
|
||||
self.timeouts = []
|
||||
|
||||
def settimeout(self, value):
|
||||
self.timeouts.append(value)
|
||||
|
||||
ticks = iter((0.0, 0.02, 0.04, 0.06, 0.08, 0.11))
|
||||
monkeypatch.setattr(server_module.time, "monotonic", lambda: next(ticks))
|
||||
connection = Connection()
|
||||
reader = server_module._AbsoluteDeadlineReader(Stream(), connection)
|
||||
reader.begin(0.1)
|
||||
|
||||
with pytest.raises(TimeoutError, match="absolute SCM header deadline"):
|
||||
reader.readline(65537)
|
||||
|
||||
assert connection.timeouts == pytest.approx([0.08, 0.06, 0.04, 0.02])
|
||||
|
||||
|
||||
def test_broker_bounds_header_count_size_and_duplicate_lengths():
|
||||
broker = _load("scm_broker")
|
||||
|
||||
def validate(headers):
|
||||
handler = object.__new__(broker.BrokerHandler)
|
||||
handler.headers = headers
|
||||
handler._validate_headers()
|
||||
|
||||
too_many = Message()
|
||||
for index in range(broker.MAX_HEADERS + 1):
|
||||
too_many[f"X-Test-{index}"] = "safe"
|
||||
with pytest.raises(broker.PolicyError, match="too many headers"):
|
||||
validate(too_many)
|
||||
|
||||
too_large = Message()
|
||||
too_large["X-Test"] = "x" * broker.MAX_HEADER_BYTES
|
||||
with pytest.raises(broker.PolicyError, match="safe limit"):
|
||||
validate(too_large)
|
||||
|
||||
duplicate = Message()
|
||||
duplicate["Content-Length"] = "1"
|
||||
duplicate["Content-Length"] = "1"
|
||||
with pytest.raises(broker.PolicyError, match="duplicate Content-Length"):
|
||||
validate(duplicate)
|
||||
@ -1,55 +0,0 @@
|
||||
"""Shared fixtures for credential-isolated Hermes SCM broker tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import sys
|
||||
from email.message import Message
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
SCRIPTS = ROOT / "services/hermes/scm-common/scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
|
||||
def _load(name: str):
|
||||
path = SCRIPTS / f"{name}.py"
|
||||
spec = importlib.util.spec_from_file_location(f"test_{name}", path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _load_path(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class Response:
|
||||
def __init__(self, body: bytes, *, status: int = 200, content_type: str):
|
||||
self.body = body
|
||||
self.stream = io.BytesIO(body)
|
||||
self.status = status
|
||||
self.headers = Message()
|
||||
self.headers["Content-Type"] = content_type
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def read(self, limit=-1):
|
||||
return self.stream.read(limit)
|
||||
|
||||
|
||||
def _receive_command(old: bytes, new: bytes, ref: bytes) -> bytes:
|
||||
command = old + b" " + new + b" " + ref + b"\x00report-status\n"
|
||||
return f"{len(command) + 4:04x}".encode() + command + b"0000PACK"
|
||||
Loading…
x
Reference in New Issue
Block a user