diff --git a/.github/workflows/guard-check.yml b/.github/workflows/guard-check.yml index 802804f..31449c0 100644 --- a/.github/workflows/guard-check.yml +++ b/.github/workflows/guard-check.yml @@ -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" \ No newline at end of file + echo "No stale artifacts found" diff --git a/.hermes.md b/.hermes.md index 4e763d9..a3a9e80 100644 --- a/.hermes.md +++ b/.hermes.md @@ -126,10 +126,10 @@ which # 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 85fe02e..f23171e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index be823f3..14bfd55 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@

- 31 playbooks · 8 references · 1 optional guard plugin · 0 brokers · Hermes-native + 31 playbooks · 8 references · required execution guard · 0 brokers · Hermes-native

-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 diff --git a/distribution.yaml b/distribution.yaml index 78279cf..0b40807 100644 --- a/distribution.yaml +++ b/distribution.yaml @@ -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" diff --git a/plugins/violin_guard/core/__init__.py b/plugins/violin_guard/core/__init__.py index ab70ae7..15e7722 100644 --- a/plugins/violin_guard/core/__init__.py +++ b/plugins/violin_guard/core/__init__.py @@ -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 diff --git a/plugins/violin_guard/core/command.py b/plugins/violin_guard/core/command.py index b0d1752..5f29376 100644 --- a/plugins/violin_guard/core/command.py +++ b/plugins/violin_guard/core/command.py @@ -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. """ diff --git a/plugins/violin_guard/core/release.py b/plugins/violin_guard/core/release.py index b3b88f0..7fdc594 100644 --- a/plugins/violin_guard/core/release.py +++ b/plugins/violin_guard/core/release.py @@ -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", diff --git a/plugins/violin_guard/core/service.py b/plugins/violin_guard/core/service.py index 19faccc..c9074fc 100644 --- a/plugins/violin_guard/core/service.py +++ b/plugins/violin_guard/core/service.py @@ -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)) diff --git a/plugins/violin_guard/plugin.yaml b/plugins/violin_guard/plugin.yaml index f9c7918..03da281 100644 --- a/plugins/violin_guard/plugin.yaml +++ b/plugins/violin_guard/plugin.yaml @@ -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 \ No newline at end of file +app_style: sh diff --git a/plugins/violin_guard/schemas.py b/plugins/violin_guard/schemas.py index 83ece27..f4d5902 100644 --- a/plugins/violin_guard/schemas.py +++ b/plugins/violin_guard/schemas.py @@ -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": { diff --git a/pyproject.toml b/pyproject.toml index 2fbd549..fb82aed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [] diff --git a/scripts/smoke-test.ps1 b/scripts/smoke-test.ps1 index 17dde15..18f954c 100644 --- a/scripts/smoke-test.ps1 +++ b/scripts/smoke-test.ps1 @@ -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 diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 1b74883..7af5121 100644 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -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=$? diff --git a/uv.lock b/uv.lock index c84abac..ed73daf 100644 --- a/uv.lock +++ b/uv.lock @@ -145,7 +145,7 @@ wheels = [ [[package]] name = "violin" -version = "1.2.0" +version = "1.3.1" source = { virtual = "." } [package.dev-dependencies]