Files
Elydora-Open-Source/sdks/python/elydora/plugins/droid_contract.py
T
雷电芽衣 bc6983a62e chore(oss): audit the server package and resync the SDK mirrors
Split the 657-line operation service into eor, submit, and verify
modules, add the AGENT_REVOKED and KEY_RETIRED codes the Backend already
emits, and condense every multi-line comment. Mirror Node, Python, and
Go at 2.1.0 with the deep-health 503, non-idempotent retry, rotation
grace, and path-encoding fixes. Document which routes and response
fields the self-hosted server does not provide.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 20:48:20 -04:00

308 lines
9.6 KiB
Python

"""Factory Droid hook schema, command construction, and ownership rules."""
from __future__ import annotations
from dataclasses import dataclass
import json
import math
import os
import re
import shutil
import subprocess # nosec B404
import sys
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
from elydora._runtime_paths import runtime_root
from ._runtime import managed_script_reference, same_agent_id, same_path
from ._shell_command import (
parse_posix_command,
parse_powershell_source,
posix_source,
powershell_source,
)
AGENT_KEY = "droid"
GUARD_SCRIPT = "guard.py"
AUDIT_SCRIPT = "hook.py"
HOOK_TIMEOUT_SECONDS = 10
TOOL_EVENTS = ("PreToolUse", "PostToolUse")
_HANDLER_KEYS = {"command", "timeout", "type"}
_GROUP_KEYS = {"hooks", "matcher"}
_REGEX_TIMEOUT_SECONDS = 10
_REGEX_VALIDATOR = """import fs from 'node:fs';
const entries = JSON.parse(fs.readFileSync(0, 'utf8'));
for (const entry of entries) {
try {
new RegExp(entry.pattern);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(entry.label + ': ' + message);
process.exit(1);
}
}
"""
JsonObject = Dict[str, Any]
DroidHookMap = Dict[str, Any]
@dataclass(frozen=True)
class RuntimeContract:
agent_id: str
guard_path: str
audit_path: str
@dataclass(frozen=True)
class ManagedRemoval:
event: str
group_index: int
handler_indexes: Tuple[int, ...]
remove_group: bool
def build_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Factory Droid hook commands require absolute executable and script paths"
)
if os.name == "nt":
return powershell_source(script_path)
return posix_source(script_path)
def build_group(script_path: str) -> JsonObject:
return {
"matcher": "*",
"hooks": [{
"type": "command",
"command": build_command(script_path),
"timeout": HOOK_TIMEOUT_SECONDS,
}],
}
def _validate_handler(value: Any, label: str) -> JsonObject:
if not isinstance(value, dict):
raise ValueError(f"{label} must be an object")
handler = dict(value)
if handler.get("type") != "command":
raise ValueError(f'{label} type must be "command"')
command = handler.get("command")
if not isinstance(command, str) or not command.strip():
raise ValueError(f"{label} command must be a non-empty string")
timeout = handler.get("timeout")
if timeout is not None and (
isinstance(timeout, bool)
or not isinstance(timeout, (int, float))
or not math.isfinite(timeout)
or timeout <= 0
):
raise ValueError(f"{label} timeout must be a positive finite number")
return handler
def _validate_group(value: Any, label: str) -> JsonObject:
if not isinstance(value, dict):
raise ValueError(f"{label} must be an object")
group = dict(value)
matcher = group.get("matcher")
if "matcher" in group and not isinstance(matcher, str):
raise ValueError(f"{label} matcher must be a string")
command_regex = group.get("commandRegex")
if "commandRegex" in group and not isinstance(command_regex, str):
raise ValueError(f"{label} commandRegex must be a string")
handlers = group.get("hooks")
if not isinstance(handlers, list):
raise ValueError(f"{label} must contain a hooks array")
group["hooks"] = [
_validate_handler(handler, f"{label}.hooks[{index}]")
for index, handler in enumerate(handlers)
]
return group
def read_hook_map(value: Any, label: str) -> DroidHookMap:
if not isinstance(value, dict):
raise ValueError(f"{label} must contain a JSON object")
hooks: DroidHookMap = {}
for event, groups in value.items():
if not isinstance(groups, list):
raise ValueError(f'{label} field "{event}" must be an array')
hooks[event] = [
_validate_group(group, f'{label} field "{event}"[{index}]')
for index, group in enumerate(groups)
]
return hooks
def _regex_entries(hooks: DroidHookMap) -> List[JsonObject]:
entries: List[JsonObject] = []
for event, groups in hooks.items():
for index, group in enumerate(groups):
matcher = group.get("matcher")
if isinstance(matcher, str) and matcher not in {"", "*"}:
entries.append({
"label": f'Factory Droid hooks field "{event}"[{index}] matcher',
"pattern": matcher,
})
command_regex = group.get("commandRegex")
if isinstance(command_regex, str):
entries.append({
"label": (
f'Factory Droid hooks field "{event}"[{index}] commandRegex'
),
"pattern": command_regex,
})
return entries
def validate_javascript_regexes(sources: Sequence[DroidHookMap]) -> None:
entries = [entry for source in sources for entry in _regex_entries(source)]
if not entries:
return
node_path = shutil.which("node")
if node_path is None:
raise FileNotFoundError(
"Node.js runtime is required to validate Factory Droid hook expressions"
)
try:
result = subprocess.run( # nosec B603
[node_path, "--input-type=module", "--eval", _REGEX_VALIDATOR],
input=json.dumps(entries),
text=True,
capture_output=True,
check=False,
timeout=_REGEX_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired as error:
raise TimeoutError(
"Factory Droid hook expression validation timed out after "
f"{_REGEX_TIMEOUT_SECONDS} seconds"
) from error
except OSError as error:
raise OSError(f"Run Node.js Factory Droid 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(
"Factory Droid matcher must be a valid JavaScript regular expression: "
f"{message}"
)
def _parse_legacy_windows_command(command: str) -> Optional[Tuple[str, str]]:
match = re.fullmatch(r'"([^"\r\n]+)" "([^"\r\n]+)"', command)
return (match.group(1), match.group(2)) if match else None
def _parse_generated_command(
command: str,
include_legacy: bool,
) -> Optional[Tuple[str, str]]:
if os.name != "nt":
return parse_posix_command(command)
current = parse_powershell_source(command)
return current or (
_parse_legacy_windows_command(command) if include_legacy else None
)
def managed_agent_id(
handler: JsonObject,
script_name: str,
include_legacy: bool = False,
) -> Optional[str]:
if set(handler) != _HANDLER_KEYS:
return None
if (
handler.get("type") != "command"
or handler.get("timeout") != HOOK_TIMEOUT_SECONDS
):
return None
command = handler.get("command")
if not isinstance(command, str):
return None
parsed = _parse_generated_command(command, include_legacy)
if parsed is None or not same_path(parsed[0], sys.executable):
return None
reference = managed_script_reference(parsed[1], script_name)
return None if reference is None else reference[0]
def _exact_owned_group(group: JsonObject, indexes: Tuple[int, ...]) -> bool:
hooks = group["hooks"]
return (
set(group) == _GROUP_KEYS
and group.get("matcher") == "*"
and bool(indexes)
and len(indexes) == len(hooks)
)
def managed_removals(
hooks: DroidHookMap,
agent_id: Optional[str] = None,
) -> List[ManagedRemoval]:
removals: List[ManagedRemoval] = []
for event, script_name in (
("PreToolUse", GUARD_SCRIPT),
("PostToolUse", AUDIT_SCRIPT),
):
for group_index, group in enumerate(hooks.get(event, [])):
indexes = tuple(
index
for index, handler in enumerate(group["hooks"])
if (
(
managed_id := managed_agent_id(
handler,
script_name,
include_legacy=True,
)
)
is not None
and (agent_id is None or same_agent_id(managed_id, agent_id))
)
)
if indexes:
removals.append(ManagedRemoval(
event,
group_index,
indexes,
_exact_owned_group(group, indexes),
))
return removals
def _managed_ids(groups: List[JsonObject], script_name: str) -> Set[str]:
return {
agent_id
for group in groups
for handler in group["hooks"]
if (agent_id := managed_agent_id(handler, script_name)) is not None
}
def runtime_contracts(hooks: DroidHookMap) -> List[RuntimeContract]:
guards = _managed_ids(hooks.get("PreToolUse", []), GUARD_SCRIPT)
audits = _managed_ids(hooks.get("PostToolUse", []), AUDIT_SCRIPT)
contracts = []
for agent_id in sorted(guards):
if not any(same_agent_id(agent_id, audit_id) for audit_id in audits):
continue
root = os.path.join(runtime_root(), agent_id)
contracts.append(RuntimeContract(
agent_id,
os.path.join(root, GUARD_SCRIPT),
os.path.join(root, AUDIT_SCRIPT),
))
return contracts