feat: sync Python Auggie hook integration
This commit is contained in:
1 parent
c32fa72a68
commit
e2f319914b
7 files changed
+1099
-1
No files matched your search
@@ -0,0 +1,227 @@
|
||||
"""Augment Code CLI native user-hook integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from ._file_io import (
|
||||
read_json as _read_json,
|
||||
regular_file_exists as _regular_file_exists,
|
||||
remove_file as _remove_file,
|
||||
require_runtime as _require_runtime,
|
||||
write_json_atomic as _write_json_atomic,
|
||||
write_text_atomic as _write_text_atomic,
|
||||
)
|
||||
from .augment_contract import (
|
||||
AGENT_KEY,
|
||||
AUDIT_SCRIPT,
|
||||
AugmentDocument,
|
||||
JsonObject,
|
||||
RuntimeContract,
|
||||
build_handler,
|
||||
build_wrapper,
|
||||
elydora_dir,
|
||||
read_hooks,
|
||||
remove_managed_hooks,
|
||||
resolve_config_path,
|
||||
runtime_contracts,
|
||||
wrapper_paths,
|
||||
)
|
||||
from .base import AgentPlugin, InstallConfig, PluginStatus
|
||||
from .hook_template import generate_hook_script
|
||||
|
||||
|
||||
def _read_config() -> AugmentDocument:
|
||||
config_path = resolve_config_path()
|
||||
root = _read_json(config_path, "Auggie settings")
|
||||
if root is None:
|
||||
return AugmentDocument(False, config_path, {}, {})
|
||||
return AugmentDocument(True, config_path, root, read_hooks(root))
|
||||
|
||||
|
||||
def _same_agent_id(left: str, right: str) -> bool:
|
||||
return os.path.normcase(left) == os.path.normcase(right)
|
||||
|
||||
|
||||
def _runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
|
||||
root = elydora_dir()
|
||||
try:
|
||||
with os.scandir(root) as iterator:
|
||||
entries = sorted(iterator, key=lambda entry: entry.name)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
except OSError as error:
|
||||
raise OSError(f"Read Elydora runtime directory at {root}: {error}") from error
|
||||
|
||||
for contract in contracts:
|
||||
entry = next(
|
||||
(
|
||||
candidate
|
||||
for candidate in entries
|
||||
if _same_agent_id(candidate.name, contract.agent_id)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if entry is None:
|
||||
continue
|
||||
try:
|
||||
if not entry.is_dir(follow_symlinks=False):
|
||||
continue
|
||||
except OSError as error:
|
||||
raise OSError(
|
||||
f"Read Elydora runtime entry at {entry.path}: {error}"
|
||||
) from error
|
||||
runtime_config_path = os.path.join(entry.path, "config.json")
|
||||
runtime_config = _read_json(runtime_config_path, "Elydora runtime config")
|
||||
if runtime_config is None:
|
||||
continue
|
||||
agent_name = runtime_config.get("agent_name")
|
||||
if not isinstance(agent_name, str):
|
||||
raise ValueError(
|
||||
f"Elydora runtime config at {runtime_config_path} field "
|
||||
'"agent_name" must be a string'
|
||||
)
|
||||
if agent_name != AGENT_KEY:
|
||||
continue
|
||||
files = (
|
||||
(contract.guard_path, "Elydora guard runtime"),
|
||||
(contract.audit_path, "Elydora audit runtime"),
|
||||
(contract.guard_wrapper_path, "Auggie guard wrapper"),
|
||||
(contract.audit_wrapper_path, "Auggie audit wrapper"),
|
||||
)
|
||||
return all(_regular_file_exists(file_path, label) for file_path, label in files)
|
||||
return False
|
||||
|
||||
|
||||
class AugmentPlugin(AgentPlugin):
|
||||
"""Install Elydora into Auggie's global user settings."""
|
||||
|
||||
def install(self, config: InstallConfig) -> None:
|
||||
agent_id = config.get("agent_id", "")
|
||||
if not agent_id:
|
||||
raise ValueError("agent_id is required")
|
||||
source = _read_config()
|
||||
guard_path = config.get("guard_script_path", "")
|
||||
_require_runtime(guard_path, "Elydora guard runtime")
|
||||
|
||||
agent_directory = os.path.join(elydora_dir(), agent_id)
|
||||
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
|
||||
wrappers = wrapper_paths(agent_id)
|
||||
cleaned, _ = remove_managed_hooks(source.hooks)
|
||||
hooks = {
|
||||
**cleaned,
|
||||
"PreToolUse": [
|
||||
*cleaned.get("PreToolUse", []),
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [build_handler(wrappers.guard_path)],
|
||||
},
|
||||
],
|
||||
"PostToolUse": [
|
||||
*cleaned.get("PostToolUse", []),
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [build_handler(wrappers.audit_path)],
|
||||
},
|
||||
],
|
||||
}
|
||||
runtime_config: JsonObject = {
|
||||
"org_id": config.get("org_id", ""),
|
||||
"agent_id": agent_id,
|
||||
"kid": config.get("kid", ""),
|
||||
"base_url": config.get("base_url", "https://api.elydora.com"),
|
||||
"token": config.get("token", ""),
|
||||
"agent_name": AGENT_KEY,
|
||||
}
|
||||
audit_script = generate_hook_script(
|
||||
org_id=config.get("org_id", ""),
|
||||
agent_id=agent_id,
|
||||
private_key=config.get("private_key", ""),
|
||||
kid=config.get("kid", ""),
|
||||
base_url=config.get("base_url", "https://api.elydora.com"),
|
||||
)
|
||||
|
||||
_write_json_atomic(
|
||||
os.path.join(agent_directory, "config.json"),
|
||||
runtime_config,
|
||||
0o600,
|
||||
"Elydora runtime config",
|
||||
)
|
||||
_write_text_atomic(
|
||||
os.path.join(agent_directory, "private.key"),
|
||||
config.get("private_key", ""),
|
||||
0o600,
|
||||
"Elydora private key",
|
||||
)
|
||||
_write_text_atomic(
|
||||
audit_path,
|
||||
audit_script,
|
||||
0o700,
|
||||
"Elydora audit runtime",
|
||||
)
|
||||
_write_text_atomic(
|
||||
wrappers.guard_path,
|
||||
build_wrapper(guard_path),
|
||||
0o700,
|
||||
"Auggie guard wrapper",
|
||||
)
|
||||
_write_text_atomic(
|
||||
wrappers.audit_path,
|
||||
build_wrapper(audit_path),
|
||||
0o700,
|
||||
"Auggie audit wrapper",
|
||||
)
|
||||
_write_json_atomic(
|
||||
source.config_path,
|
||||
{**source.root, "hooks": hooks},
|
||||
0o600,
|
||||
"Auggie settings",
|
||||
)
|
||||
print("Auggie: user-level PreToolUse and PostToolUse hooks installed.")
|
||||
|
||||
def uninstall(self, agent_id: str = "") -> None:
|
||||
source = _read_config()
|
||||
if not source.exists:
|
||||
return
|
||||
hooks, changed = remove_managed_hooks(source.hooks, agent_id)
|
||||
if not changed:
|
||||
return
|
||||
root = dict(source.root)
|
||||
if hooks:
|
||||
root["hooks"] = hooks
|
||||
else:
|
||||
root.pop("hooks", None)
|
||||
if root:
|
||||
_write_json_atomic(
|
||||
source.config_path,
|
||||
root,
|
||||
0o600,
|
||||
"Auggie settings",
|
||||
)
|
||||
else:
|
||||
_remove_file(source.config_path, "Auggie settings")
|
||||
|
||||
def status(self) -> PluginStatus:
|
||||
source = _read_config()
|
||||
contracts = runtime_contracts(source.hooks)
|
||||
if not contracts:
|
||||
return PluginStatus(
|
||||
installed=False,
|
||||
agent=AGENT_KEY,
|
||||
details="Not installed",
|
||||
)
|
||||
installed = _runtime_files_exist(contracts)
|
||||
details = (
|
||||
f"Config: {source.config_path}"
|
||||
if installed
|
||||
else (
|
||||
f"Configured at {source.config_path}; "
|
||||
"runtime scripts or wrappers missing"
|
||||
)
|
||||
)
|
||||
return PluginStatus(
|
||||
installed=installed,
|
||||
agent=AGENT_KEY,
|
||||
details=details,
|
||||
)
|
||||
Reference in new issue
Block a user