fix: sync Python Auggie matcher validation

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 09:04:41 -04:00
1 parent e334e84333
commit 51921e9101
5 files changed
+127 -8

No files matched your search

+74
View File
@@ -3,6 +3,8 @@
from __future__ import annotations
import os
import shutil
import subprocess # nosec B404
from typing import List
from ._file_io import (
@@ -17,6 +19,7 @@ from .augment_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
AugmentDocument,
AugmentHooks,
JsonObject,
RuntimeContract,
build_handler,
@@ -32,6 +35,76 @@ from .base import AgentPlugin, InstallConfig, PluginStatus
from .hook_template import generate_hook_script
_REGEX_VALIDATION_TIMEOUT_SECONDS = 10
_REGEX_VALIDATOR = """import fs from "node:fs";
const pattern = fs.readFileSync(0, "utf8");
try {
new RegExp(pattern);
} catch (error) {
process.stderr.write(error instanceof Error ? error.message : String(error));
process.exit(1);
}
"""
def _resolve_node_runtime() -> str:
node_path = shutil.which("node")
if node_path is None:
raise FileNotFoundError(
"Node.js runtime is required to validate Auggie matcher expressions"
)
return node_path
def _validate_matcher(node_path: str, label: str, matcher: str) -> None:
try:
result = subprocess.run( # nosec B603
[
node_path,
"--input-type=module",
"--eval",
_REGEX_VALIDATOR,
],
input=matcher,
text=True,
capture_output=True,
check=False,
timeout=_REGEX_VALIDATION_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired as error:
raise TimeoutError(
f"{label} matcher validation timed out after "
f"{_REGEX_VALIDATION_TIMEOUT_SECONDS} seconds"
) from error
except OSError as error:
raise OSError(f"Run Node.js matcher validator: {error}") from error
if result.returncode == 0:
return
message = (
result.stderr.strip()
or result.stdout.strip()
or f"Node.js exited with code {result.returncode}"
)
raise ValueError(
f"{label} matcher must be a valid JavaScript regular expression: {message}"
)
def _validate_matchers(hooks: AugmentHooks) -> None:
matchers = [
(event, group_index, group["matcher"])
for event in sorted(hooks)
for group_index, group in enumerate(hooks[event])
if "matcher" in group
]
if not matchers:
return
node_path = _resolve_node_runtime()
for event, group_index, matcher in matchers:
label = f"Auggie settings group hooks.{event}[{group_index}]"
_validate_matcher(node_path, label, matcher)
def _read_config() -> AugmentDocument:
config_path = resolve_config_path()
root = _read_json(config_path, "Auggie settings")
@@ -102,6 +175,7 @@ class AugmentPlugin(AgentPlugin):
if not agent_id:
raise ValueError("agent_id is required")
source = _read_config()
_validate_matchers(source.hooks)
guard_path = config.get("guard_script_path", "")
_require_runtime(guard_path, "Elydora guard runtime")
@@ -5,7 +5,6 @@ from __future__ import annotations
from dataclasses import dataclass
import math
import os
import re
import sys
from typing import Any, Dict, List, Optional, Set, Tuple
@@ -241,12 +240,6 @@ def _validate_group(value: Any, event: str, group_index: int) -> JsonObject:
if "matcher" in group:
if not isinstance(matcher, str):
raise ValueError(f"{label} matcher must be a string")
try:
re.compile(matcher)
except re.error as error:
raise ValueError(
f"{label} matcher must be a valid regular expression: {error}"
) from error
if "metadata" in group:
_validate_metadata(group["metadata"], label)
handlers = group.get("hooks")
@@ -0,0 +1,51 @@
from __future__ import annotations
import subprocess
from typing import NoReturn
import pytest
from elydora.plugins import augment
MATCHER_HOOKS = {
"PreToolUse": [
{
"matcher": "(?<tool>launch-process)",
"hooks": [],
}
]
}
def test_matcher_validation_requires_node(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(augment.shutil, "which", lambda _: None)
with pytest.raises(FileNotFoundError, match="Node.js runtime is required"):
augment._validate_matchers(MATCHER_HOOKS)
def test_matcher_validation_skips_node_without_matchers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fail_lookup(_: str) -> NoReturn:
raise AssertionError("Node.js lookup must be skipped")
monkeypatch.setattr(augment.shutil, "which", fail_lookup)
augment._validate_matchers({"SessionStart": [{"hooks": []}]})
def test_matcher_validation_surfaces_timeout(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def time_out(*args: object, **kwargs: object) -> NoReturn:
raise subprocess.TimeoutExpired(cmd="node", timeout=10)
monkeypatch.setattr(augment.shutil, "which", lambda _: "node")
monkeypatch.setattr(augment.subprocess, "run", time_out)
with pytest.raises(TimeoutError, match="timed out after 10 seconds"):
augment._validate_matchers(MATCHER_HOOKS)
+1 -1
View File
@@ -159,7 +159,7 @@ def test_install_preserves_user_settings_and_is_idempotent(
],
"PreToolUse": [
{
"matcher": "launch-process",
"matcher": "(?<tool>launch-process)",
"hooks": [
{
"type": "command",