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

+1
View File
@@ -26,6 +26,7 @@ This repository publishes the open-source server, console, integration catalog,
- Preserve Kimi TOML comments and unrelated formatting through range-based edits, then parse the complete rendered document before writing it. - Preserve Kimi TOML comments and unrelated formatting through range-based edits, then parse the complete rendered document before writing it.
- Write Grok Build integrations to a native user hook file under non-empty `GROK_HOME` or `~/.grok`; keep Claude Code and Cursor compatibility sources read-only. - Write Grok Build integrations to a native user hook file under non-empty `GROK_HOME` or `~/.grok`; keep Claude Code and Cursor compatibility sources read-only.
- Write Auggie hooks only to `~/.augment/settings.json`; keep system and workspace settings read-only. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix because Auggie dispatches supported script paths, and express hook timeouts in milliseconds. - Write Auggie hooks only to `~/.augment/settings.json`; keep system and workspace settings read-only. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix because Auggie dispatches supported script paths, and express hook timeouts in milliseconds.
- Validate Auggie matcher syntax during installation with Node.js `new RegExp`; keep status and uninstall independent from the JavaScript validator so recovery remains available offline.
## Mirror Workflow ## Mirror Workflow
+74
View File
@@ -3,6 +3,8 @@
from __future__ import annotations from __future__ import annotations
import os import os
import shutil
import subprocess # nosec B404
from typing import List from typing import List
from ._file_io import ( from ._file_io import (
@@ -17,6 +19,7 @@ from .augment_contract import (
AGENT_KEY, AGENT_KEY,
AUDIT_SCRIPT, AUDIT_SCRIPT,
AugmentDocument, AugmentDocument,
AugmentHooks,
JsonObject, JsonObject,
RuntimeContract, RuntimeContract,
build_handler, build_handler,
@@ -32,6 +35,76 @@ from .base import AgentPlugin, InstallConfig, PluginStatus
from .hook_template import generate_hook_script 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: def _read_config() -> AugmentDocument:
config_path = resolve_config_path() config_path = resolve_config_path()
root = _read_json(config_path, "Auggie settings") root = _read_json(config_path, "Auggie settings")
@@ -102,6 +175,7 @@ class AugmentPlugin(AgentPlugin):
if not agent_id: if not agent_id:
raise ValueError("agent_id is required") raise ValueError("agent_id is required")
source = _read_config() source = _read_config()
_validate_matchers(source.hooks)
guard_path = config.get("guard_script_path", "") guard_path = config.get("guard_script_path", "")
_require_runtime(guard_path, "Elydora guard runtime") _require_runtime(guard_path, "Elydora guard runtime")
@@ -5,7 +5,6 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import math import math
import os import os
import re
import sys import sys
from typing import Any, Dict, List, Optional, Set, Tuple 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 "matcher" in group:
if not isinstance(matcher, str): if not isinstance(matcher, str):
raise ValueError(f"{label} matcher must be a string") 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: if "metadata" in group:
_validate_metadata(group["metadata"], label) _validate_metadata(group["metadata"], label)
handlers = group.get("hooks") 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": [ "PreToolUse": [
{ {
"matcher": "launch-process", "matcher": "(?<tool>launch-process)",
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",