Merge P0 release and CI hardening

This commit is contained in:
Violin
2026-07-13 08:53:57 +01:00
15 changed files with 62 additions and 34 deletions
+8 -7
View File
@@ -1,9 +1,9 @@
name: guard-check
on:
push:
branches: [master]
branches: [master, dev]
pull_request:
branches: [master]
branches: [master, dev]
jobs:
guard-check:
runs-on: ubuntu-latest
@@ -12,13 +12,14 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install pyyaml
- uses: astral-sh/setup-uv@v6
- name: Install development dependencies
run: uv sync --dev
- name: Run guard release check
run: python scripts/violin_guard.py check-release
run: uv run python scripts/violin_guard.py check-release
- name: Validate YAML
run: |
python -c "
uv run python -c "
import yaml
for f in ['distribution.yaml', 'config.yaml', 'skills/pentest/templates/scope-template.yaml']:
yaml.safe_load(open(f))
@@ -34,4 +35,4 @@ jobs:
for stale in PLAN.md skills/pentest/scripts/recon-scan.sh; do
if [ -f "$stale" ]; then echo "Stale: $stale"; exit 1; fi
done
echo "No stale artifacts found"
echo "No stale artifacts found"
+2 -2
View File
@@ -126,10 +126,10 @@ which <tool> # native
## Tool Philosophy
Violin runs on **Hermes built-in toolsets** (see [`skills/pentest/SKILL.md §1`](./skills/pentest/SKILL.md#1-operating-model) for the capability inventory and [`README.md §Toolsets`](./README.md#enabled-toolsets) for the full matrix). The canonical command-gate logic lives in `scripts/guard/` and is callable two ways:
Violin runs on **Hermes built-in toolsets** (see [`skills/pentest/SKILL.md §1`](./skills/pentest/SKILL.md#1-operating-model) for the capability inventory and [`README.md §Toolsets`](./README.md#enabled-toolsets) for the full matrix). The canonical command-gate logic lives in `plugins/violin_guard/core/` and is callable two ways:
- **CLI:** `python scripts/violin_guard.py check-command ...` (the path documented in SKILL.md §2).
- **Hermes plugin:** `plugins/violin_guard/` registers typed guard tools (`violin_check_command`, `violin_record_*`, `violin_exec`, `violin_sync_done`, …) that delegate to the same `scripts/guard/` state machine. Loading the plugin is optional but recommended — it surfaces the guards as first-class tools.
- **Hermes plugin:** `plugins/violin_guard/` registers typed guard tools (`violin_check_command`, `violin_record_*`, `violin_exec`, `violin_sync_done`, …) that call the core service directly. The plugin is required for target execution.
Both entry points enforce the identical skill-load, active-PTT, history-freshness, hypothesis, and doc-sync gates. The executor itself writes exact history, but never updates PTT progress; that remains an explicit reviewed checkpoint after each bounded batch. Do **not** develop a third implementation.
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 1.3.1
- Enforced scope authorisation, exclusions, phase-aligned PTT tasks, and relevant hypotheses at the execution boundary.
- Made synchronization credits apply to all target-touching commands and bound reviewed batches to their captured PTT task.
- Serialized guard state transitions, fixed isolated plugin imports, and made release and PowerShell smoke checks fail reliably.
## 1.3.0
- Consolidated Violin Guard into a Hermes-native plugin.
+2 -2
View File
@@ -13,10 +13,10 @@
</p>
<p align="center">
<b>31 playbooks · 8 references · 1 optional guard plugin · 0 brokers · Hermes-native</b>
<b>31 playbooks · 8 references · required execution guard · 0 brokers · Hermes-native</b>
</p>
Violin is a **Hermes-native agentic pentest profile** for supervised, authorised penetration tests — from reconnaissance through safe exploit validation to reporting. It uses Hermes' built-in toolsets, skill-based playbooks, an optional `violin-guard` plugin, and lightweight guard scripts. No extra API keys, no per-profile credentials, no lock-in.
Violin is a **Hermes-native agentic pentest profile** for supervised, authorised penetration tests — from reconnaissance through safe exploit validation to reporting. It uses Hermes' built-in toolsets, skill-based playbooks, and the required `violin-guard` plugin at the target-execution boundary. The standalone CLI is retained only for diagnostics and recovery. No extra API keys, no per-profile credentials, no lock-in.
```
hermes profile install https://github.com/Strategic-Automation/violin
+1 -1
View File
@@ -1,6 +1,6 @@
# violin - supervised agentic Hermes pentest profile
name: violin
version: 1.3.0
version: 1.3.1
description: "A supervised agentic Hermes penetration testing profile for authorised Kali/Parrot-based security assessment, reconnaissance, exploit validation, and reporting workflows."
hermes_requires: ">=0.18.0"
author: "Violin contributors"
+2 -3
View File
@@ -1,8 +1,7 @@
"""Violin guard — core subpackage with the shared state machine, execution, and adapters.
All modules in this package define their public API in ``__all__``. Functions that
depend on the ``scripts/guard/`` package use lazy imports inside their bodies so
the module graph is resolvable without ``scripts/`` on ``sys.path`` at import time.
All modules in this package define their public API in ``__all__`` and are
importable directly from the installed plugin directory.
"""
from __future__ import annotations
+1 -1
View File
@@ -1,6 +1,6 @@
"""Check-command sub-guards — pure validation functions.
All logic ported from scripts/guard/{command,record,freshness,closeout}.py
This is the canonical command, freshness, and closeout policy implementation.
No subprocess calls pure functions returning dataclasses.
"""
+26 -5
View File
@@ -133,17 +133,30 @@ def check_release() -> ReleaseCheckResult:
result.add_warning("CHANGELOG.md not found")
# 3. Isolated plugin import (catches broken module-level code / imports).
module_name = "violin_guard_release_check"
old_module = sys.modules.get(module_name)
try:
sys.path.insert(0, str(root.parents[1]))
sys.path.insert(0, str(root.parent))
spec = importlib.util.spec_from_file_location(
"violin_guard_release_check", root / "__init__.py"
module_name,
root / "__init__.py",
submodule_search_locations=[str(root)],
)
if spec is None or spec.loader is None:
raise ImportError("could not build plugin import specification")
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod)
result.add_info("isolated plugin import OK")
except Exception as exc: # noqa: BLE001
result.add_error(f"plugin import failed: {type(exc).__name__}: {exc}")
mod = None
finally:
sys.path.pop(0)
if old_module is None:
sys.modules.pop(module_name, None)
else:
sys.modules[module_name] = old_module
# 3b. Manifest vs registered tools.
if mod is not None:
@@ -178,7 +191,16 @@ def check_release() -> ReleaseCheckResult:
result.add_warning("ruff not installed; skipped")
try:
pytest = subprocess.run(
[sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"],
[
sys.executable,
"-m",
"pytest",
"-q",
"-p",
"no:cacheprovider",
"--basetemp",
str(Path(repo_root) / "engagements" / ".pytest-release"),
],
cwd=repo_root,
capture_output=True,
text=True,
@@ -204,12 +226,11 @@ def check_release() -> ReleaseCheckResult:
# 6. Skill documentation staleness scan (corrected forbidden set).
profile_root = root.parents[1]
skills_root = profile_root / "skills"
# 'violin_record_history' is RE-REGISTERED in __init__.py, so it is no
# longer a stale reference; 'violin_message_tick' is genuinely absent.
forbidden = {
"scripts/guard/": "removed legacy guard package",
"hypothesis_guard.py": "removed hypothesis wrapper",
"session_search": "unavailable session-search tool",
"violin_record_history": "removed executor-owned history tool",
"violin_message_tick": "removed model-visible message tool",
"violin_guard.py close": "nonexistent close subcommand",
"check-closeout": "nonexistent closeout subcommand",
+2 -6
View File
@@ -8,6 +8,7 @@ import re
from pathlib import Path
from . import command, execution, hypotheses, ptt, state
from .adapters import search_exploit
def _json(status_name, **payload):
@@ -335,9 +336,4 @@ def handle_status(a, **kwargs):
def handle_search_exploit(a, **kwargs):
return _json(
"ok",
**__import__(
"plugins.violin_guard.core.adapters", fromlist=["search_exploit"]
).search_exploit(a),
)
return _json("ok", **search_exploit(a))
+2 -2
View File
@@ -1,5 +1,5 @@
name: violin-guard
version: "1.3.0"
version: "1.3.1"
description: Typed scope guards and an execute-and-record boundary with bounded synchronization windows.
kind: standalone
provides_tools:
@@ -26,4 +26,4 @@ hooks:
toolsets:
violin_guard:
description: Violin engagement guard tools
app_style: sh
app_style: sh
+1 -1
View File
@@ -38,7 +38,7 @@ RECORD_PTT_SCHEMA = {
}
RECORD_HYPOTHESIS_SCHEMA = {
"description": "Record/update a hypothesis row (delegates to hypothesis_guard.py record-hypothesis).",
"description": "Record or update a hypothesis row in the engagement state.",
"parameters": {
"type": "object",
"properties": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "violin"
version = "1.2.0"
version = "1.3.1"
description = "Supervised agentic Hermes penetration-testing profile"
requires-python = ">=3.11"
dependencies = []
+5
View File
@@ -10,6 +10,7 @@ Write-Host "Violin smoke test (PowerShell)"
Write-Host "Repo: $RepoRoot"
python scripts/violin_guard.py check-release
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if ($NoHermes) {
Write-Host "Skipping Hermes install smoke because -NoHermes was supplied."
@@ -25,9 +26,13 @@ if (-not $Hermes) {
$ProfileName = "violin-smoke-$([int][double]::Parse((Get-Date -UFormat %s)))"
try {
hermes profile install . --name $ProfileName -y
if ($LASTEXITCODE -ne 0) { throw "Hermes profile install failed." }
hermes profile show $ProfileName
if ($LASTEXITCODE -ne 0) { throw "Hermes profile show failed." }
hermes -p $ProfileName tools --summary
if ($LASTEXITCODE -ne 0) { throw "Hermes tool listing failed." }
hermes -p $ProfileName chat -q "Smoke test: reply with Violin profile loaded" -Q
if ($LASTEXITCODE -ne 0) { throw "Hermes chat smoke test failed." }
}
finally {
hermes profile delete $ProfileName -y 2>$null
+2 -2
View File
@@ -657,11 +657,11 @@ s4 = st(tools.handle_exec, **base2)
assert s4 == "approved", f"step4 expected approved, got {s4}"
print(" ok: violin_exec after sync -> approved")
# Step 5: hypothesis recording via plugin routes to hypothesis_guard.py
# Step 5: hypothesis recording through the plugin core service.
s5 = st(tools.handle_record_hypothesis, eng_dir=eng_dir, service="SMB",
port="445", title="anon access", status="researching")
assert s5 == "ok", f"step5 expected ok, got {s5}"
print(" ok: violin_record_hypothesis -> ok (routed to hypothesis_guard.py)")
print(" ok: violin_record_hypothesis -> ok")
print("GATES_OK")
PY
gates_exit=$?
Generated
+1 -1
View File
@@ -145,7 +145,7 @@ wheels = [
[[package]]
name = "violin"
version = "1.2.0"
version = "1.3.1"
source = { virtual = "." }
[package.dev-dependencies]