fix: align Kimi hooks with official contracts

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 04:25:38 -04:00
1 parent cb6173e6ee
commit 978e15e251
13 files changed
+2105 -745

No files matched your search

+67 -442
View File
@@ -2,472 +2,97 @@
from __future__ import annotations
import copy
from dataclasses import dataclass
import os
import shlex
import shutil
# subprocess provides argument quoting only; this module starts no process.
import subprocess # nosec B404
import sys
from typing import Any, Dict, FrozenSet, List, Optional
from tomlkit import aot, document, dumps, inline_table, parse, table
from tomlkit.items import AoT, Array
from typing import List
from .base import AgentPlugin, InstallConfig, PluginStatus
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 .kimi_contract import (
AGENT_KEY,
KimiDocument,
TomlObject,
build_kimi_hook,
kimi_runtime_contracts,
remove_managed_kimi_hooks,
render_kimi_document,
)
from .hook_template import generate_hook_script
from .kimi_installation import (
commit_kimi_installation,
commit_kimi_uninstall,
preflight_kimi_installation,
prepare_kimi_installation,
prepare_kimi_uninstall,
)
from .kimi_io import kimi_runtime_files_exist, read_kimi_documents
AGENT_KEY = "kimi"
GUARD_SCRIPT = "guard.py"
AUDIT_SCRIPT = "hook.py"
HOOK_TIMEOUT_SECONDS = 10
ELYDORA_DIR = os.path.join(os.path.expanduser("~"), ".elydora")
SHARED_EVENTS: FrozenSet[str] = frozenset({
"PreToolUse",
"PostToolUse",
"PostToolUseFailure",
"UserPromptSubmit",
"Stop",
"StopFailure",
"SessionStart",
"SessionEnd",
"SubagentStart",
"SubagentStop",
"PreCompact",
"PostCompact",
"Notification",
})
MODERN_EVENTS = SHARED_EVENTS | {
"PermissionRequest",
"PermissionResult",
"Interrupt",
}
LEGACY_EVENTS = SHARED_EVENTS
SUPPORTED_FIELDS = frozenset({"event", "matcher", "command", "timeout"})
TomlObject = Dict[str, Any]
@dataclass(frozen=True)
class KimiContract:
runtime_name: str
label: str
config_path: str
events: FrozenSet[str]
@dataclass(frozen=True)
class KimiDocument:
contract: KimiContract
exists: bool
raw: str
value: Any
hooks: List[TomlObject]
@dataclass(frozen=True)
class RuntimeContract:
guard: str
audit: str
config_path: str
@dataclass(frozen=True)
class ConfigMutation:
kind: str
source: KimiDocument
raw: str = ""
def _home_dir() -> str:
return os.path.expanduser("~")
def _path_exists(path: str, label: str) -> bool:
try:
os.stat(path)
return True
except (FileNotFoundError, NotADirectoryError):
return False
except OSError as error:
raise OSError(f"Inspect {label} at {path}: {error}") from error
def _legacy_cli_on_path() -> bool:
try:
return shutil.which("kimi-cli") is not None
except OSError as error:
raise OSError(f"Inspect kimi-cli executable on PATH: {error}") from error
def _resolve_contracts() -> List[KimiContract]:
home = _home_dir()
explicit_home = os.environ.get("KIMI_CODE_HOME") or None
kimi_home = explicit_home or os.path.join(home, ".kimi-code")
modern = KimiContract(
runtime_name="Kimi Code",
label="Kimi Code hooks config",
config_path=os.path.join(kimi_home, "config.toml"),
events=MODERN_EVENTS,
)
legacy = KimiContract(
runtime_name="kimi-cli",
label="kimi-cli legacy hooks config",
config_path=os.path.join(home, ".kimi", "config.toml"),
events=LEGACY_EVENTS,
)
modern_detected = explicit_home is not None or _path_exists(
kimi_home, "Kimi Code home"
)
legacy_detected = _path_exists(
legacy.config_path, legacy.label
) or _legacy_cli_on_path()
if legacy_detected and not modern_detected:
return [legacy]
return [modern, legacy] if legacy_detected else [modern]
def _unwrap(value: Any) -> Any:
unwrap = getattr(value, "unwrap", None)
return unwrap() if callable(unwrap) else value
def _validate_hook(value: Any, contract: KimiContract, index: int) -> TomlObject:
raw = _unwrap(value)
if not isinstance(raw, dict):
raise ValueError(f"{contract.label} hook {index + 1} must be a table")
hook = dict(raw)
unsupported = next((key for key in hook if key not in SUPPORTED_FIELDS), None)
if unsupported is not None:
raise ValueError(
f'{contract.label} hook {index + 1} has unsupported field "{unsupported}"'
)
event = hook.get("event")
if not isinstance(event, str) or event not in contract.events:
raise ValueError(
f'{contract.label} hook {index + 1} has unsupported event "{event}"'
)
command = hook.get("command")
if not isinstance(command, str) or not command:
raise ValueError(
f"{contract.label} hook {index + 1} requires a non-empty command"
)
matcher = hook.get("matcher")
if matcher is not None and not isinstance(matcher, str):
raise ValueError(f"{contract.label} hook {index + 1} matcher must be a string")
timeout = hook.get("timeout")
if timeout is not None and (
isinstance(timeout, bool)
or not isinstance(timeout, int)
or timeout < 1
or timeout > 600
):
raise ValueError(
f"{contract.label} hook {index + 1} timeout must be an integer from 1 to 600"
)
return hook
def _read_hooks(value: Any, contract: KimiContract) -> List[TomlObject]:
if "hooks" not in value:
return []
container = value["hooks"]
raw = _unwrap(container)
if not isinstance(raw, list):
raise ValueError(f'{contract.label} field "hooks" must be an array')
return [_validate_hook(item, contract, index) for index, item in enumerate(raw)]
def _read_config(contract: KimiContract) -> KimiDocument:
try:
with open(
contract.config_path,
"r",
encoding="utf-8",
newline="",
) as file:
raw = file.read()
except FileNotFoundError:
value = document()
return KimiDocument(contract, False, "", value, [])
except OSError as error:
raise OSError(
f"Read {contract.label} at {contract.config_path}: {error}"
) from error
try:
value = parse(raw)
except Exception as error:
raise ValueError(
f"Failed to parse {contract.label} at {contract.config_path}: {error}"
) from error
return KimiDocument(contract, True, raw, value, _read_hooks(value, contract))
def _read_all_configs() -> List[KimiDocument]:
return [_read_config(contract) for contract in _resolve_contracts()]
def _build_command(script_path: str) -> str:
arguments = [sys.executable, script_path]
return (
subprocess.list2cmdline(arguments)
if os.name == "nt"
else shlex.join(arguments)
)
def _build_hook(event: str, script_path: str) -> TomlObject:
return {
"event": event,
"command": _build_command(script_path),
"timeout": HOOK_TIMEOUT_SECONDS,
}
def _render_hooks(
def _installed_hooks(
source: KimiDocument,
keep_indices: List[int],
additions: List[TomlObject],
) -> str:
candidate = copy.deepcopy(source.value)
if "hooks" in candidate:
container: Any = candidate["hooks"]
if not isinstance(container, (AoT, Array)):
raise ValueError(f'{source.contract.label} field "hooks" must be an array')
keep = set(keep_indices)
for index in range(len(source.hooks) - 1, -1, -1):
if index not in keep:
del container[index]
else:
container = aot()
candidate.add("hooks", container)
for hook in additions:
item = table() if isinstance(container, AoT) else inline_table()
for key, value in hook.items():
item[key] = value
container.append(item)
if len(container) == 0:
del candidate["hooks"]
return dumps(candidate)
def _normalize(value: str) -> str:
return os.path.normcase(value)
def _command_references(command: str, path: str) -> bool:
return _normalize(path) in _normalize(command)
def _is_managed_hook(
hook: TomlObject,
event: str,
script_name: str,
agent_id: str = "",
) -> bool:
if hook.get("event") != event or not isinstance(hook.get("command"), str):
return False
command = str(hook["command"])
if agent_id:
script_path = os.path.join(ELYDORA_DIR, agent_id, script_name)
return _command_references(command, script_path)
normalized = _normalize(command)
return (
_normalize(f"{os.sep}.elydora{os.sep}") in normalized
and _normalize(f"{os.sep}{script_name}") in normalized
)
def _kept_hook_indices(source: KimiDocument, agent_id: str = "") -> List[int]:
guard_path: str,
audit_path: str,
) -> List[TomlObject]:
return [
index
for index, hook in enumerate(source.hooks)
if not _is_managed_hook(hook, "PreToolUse", GUARD_SCRIPT, agent_id)
and not _is_managed_hook(hook, "PostToolUse", AUDIT_SCRIPT, agent_id)
*remove_managed_kimi_hooks(source.hooks),
build_kimi_hook("PreToolUse", guard_path),
build_kimi_hook("PostToolUse", audit_path),
build_kimi_hook("PostToolUseFailure", audit_path),
]
def _runtime_contract(source: KimiDocument) -> Optional[RuntimeContract]:
guard = next(
(
hook
for hook in source.hooks
if _is_managed_hook(hook, "PreToolUse", GUARD_SCRIPT)
),
None,
)
audit = next(
(
hook
for hook in source.hooks
if _is_managed_hook(hook, "PostToolUse", AUDIT_SCRIPT)
),
None,
)
if guard is None or audit is None:
return None
return RuntimeContract(
guard=str(guard["command"]),
audit=str(audit["command"]),
config_path=source.contract.config_path,
)
def _runtime_scripts_exist(contracts: List[RuntimeContract]) -> bool:
try:
with os.scandir(ELYDORA_DIR) 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 {ELYDORA_DIR}: {error}") from error
for entry in entries:
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
guard_path = os.path.join(entry.path, GUARD_SCRIPT)
audit_path = os.path.join(entry.path, AUDIT_SCRIPT)
if not any(
_command_references(contract.guard, guard_path)
and _command_references(contract.audit, audit_path)
for contract in contracts
):
continue
config_path = os.path.join(entry.path, "config.json")
runtime_config = _read_json(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 {config_path} field "agent_name" '
"must be a string"
)
if agent_name != AGENT_KEY:
continue
return _regular_file_exists(
guard_path, "Elydora guard runtime"
) and _regular_file_exists(audit_path, "Elydora audit runtime")
return False
def _apply_mutation(mutation: ConfigMutation) -> None:
path = mutation.source.contract.config_path
if mutation.kind == "write":
_write_text_atomic(path, mutation.raw, 0o600, mutation.source.contract.label)
elif mutation.kind == "remove":
_remove_file(path, mutation.source.contract.label)
class KimiPlugin(AgentPlugin):
"""Install Elydora into detected Kimi user hook contracts."""
manages_guard_runtime = True
def preflight_install(self, config: InstallConfig) -> None:
documents = read_kimi_documents()
preflight_kimi_installation(config, documents)
def install(self, config: InstallConfig) -> None:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
sources = _read_all_configs()
guard_path = config.get("guard_script_path", "")
_require_runtime(guard_path, "Elydora guard runtime")
agent_dir = os.path.join(ELYDORA_DIR, agent_id)
audit_path = os.path.join(agent_dir, AUDIT_SCRIPT)
mutations = [
ConfigMutation(
"write",
documents = read_kimi_documents()
paths = preflight_kimi_installation(config, documents)
rendered = [
render_kimi_document(
source,
_render_hooks(
source,
_kept_hook_indices(source),
[
_build_hook("PreToolUse", guard_path),
_build_hook("PostToolUse", audit_path),
],
),
_installed_hooks(source, paths.guard_path, paths.audit_path),
)
for source in sources
for source in documents
]
runtime_config: TomlObject = {
"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,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
changes = prepare_kimi_installation(config, paths, rendered)
commit_kimi_installation(changes)
runtimes = " and ".join(
source.contract.runtime_name for source in documents
)
_write_json_atomic(
os.path.join(agent_dir, "config.json"),
runtime_config,
0o600,
"Elydora runtime config",
print(
f"{runtimes}: global PreToolUse, PostToolUse, "
"and PostToolUseFailure hooks installed."
)
_write_text_atomic(
os.path.join(agent_dir, "private.key"),
config.get("private_key", ""),
0o600,
"Elydora private key",
)
_write_text_atomic(audit_path, audit_script, 0o700, "Elydora audit runtime")
for mutation in mutations:
_apply_mutation(mutation)
runtimes = " and ".join(source.contract.runtime_name for source in sources)
print(f"{runtimes}: global PreToolUse and PostToolUse hooks installed.")
def uninstall(self, agent_id: str = "") -> None:
sources = _read_all_configs()
mutations: List[ConfigMutation] = []
for source in sources:
keep_indices = _kept_hook_indices(source, agent_id)
if len(keep_indices) == len(source.hooks):
mutations.append(ConfigMutation("none", source))
continue
raw = _render_hooks(source, keep_indices, [])
mutations.append(
ConfigMutation("write" if raw.strip() else "remove", source, raw)
documents = read_kimi_documents()
rendered = [
render_kimi_document(
source,
remove_managed_kimi_hooks(source.hooks, agent_id),
)
for mutation in mutations:
_apply_mutation(mutation)
for source in documents
]
commit_kimi_uninstall(prepare_kimi_uninstall(rendered))
def status(self) -> PluginStatus:
sources = _read_all_configs()
contracts = [
contract
for source in sources
for contract in [_runtime_contract(source)]
if contract is not None
]
if not contracts:
return PluginStatus(
installed=False,
agent=AGENT_KEY,
details="Not installed",
)
installed = _runtime_scripts_exist(contracts)
paths = ", ".join(contract.config_path for contract in contracts)
documents = read_kimi_documents()
contracts = kimi_runtime_contracts(documents)
installed = bool(contracts) and kimi_runtime_files_exist(contracts)
config_path = (
contracts[-1].config_path
if contracts
else documents[0].contract.config_path
)
details = (
f"Config: {paths}"
f"Config: {config_path}"
if installed
else f"Configured at {paths}; runtime scripts missing"
else f"Configured at {config_path}; managed contract incomplete"
)
return PluginStatus(
installed=installed,
agent=AGENT_KEY,
details=details if contracts else "Not installed",
)
return PluginStatus(installed=installed, agent=AGENT_KEY, details=details)
+232
View File
@@ -0,0 +1,232 @@
"""Kimi hook command rendering and exact runtime ownership."""
from __future__ import annotations
import base64
from dataclasses import dataclass
import ntpath
import os
import re
import shlex
import subprocess # nosec B404 - used only for legacy command rendering
import sys
from typing import Optional, Tuple
@dataclass(frozen=True)
class KimiRuntimeReference:
agent_id: str
script_path: str
@dataclass(frozen=True)
class _ParsedArgument:
value: str
next_index: int
def runtime_root() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_kimi_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_kimi_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _windows_powershell_path() -> str:
configured = os.environ.get("SystemRoot") if os.name == "nt" else None
system_root = (
configured
if configured
and ntpath.isabs(configured)
and re.search(r'["%\r\n]', configured) is None
else r"C:\Windows"
)
return ntpath.join(
system_root,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
)
def _windows_command(script_path: str) -> str:
source = (
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
)
encoded = base64.b64encode(source.encode("utf-16le")).decode("ascii")
return (
f'"{_windows_powershell_path()}" -NoLogo -NoProfile '
f"-NonInteractive -EncodedCommand {encoded}"
)
def build_kimi_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Kimi hook commands require absolute executable and script paths"
)
if os.name == "nt":
return _windows_command(script_path)
return f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
def _read_posix_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return _ParsedArgument(value, index + 1)
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if executable is None or command[executable.next_index : executable.next_index + 1] != " ":
return None
script = _read_posix_argument(command, executable.next_index + 1)
if script is None or script.next_index != len(command):
return None
return executable.value, script.value
def _read_powershell_argument(
command: str, start: int
) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return _ParsedArgument(value, index + 1)
return None
def _parse_powershell_source(source: str) -> Optional[Tuple[str, str]]:
if not source.startswith("& "):
return None
executable = _read_powershell_argument(source, 2)
if executable is None or source[executable.next_index : executable.next_index + 1] != " ":
return None
script = _read_powershell_argument(source, executable.next_index + 1)
if script is None or source[script.next_index :] != "; exit $LASTEXITCODE":
return None
return executable.value, script.value
def _parse_windows_command(command: str) -> Optional[Tuple[str, str]]:
match = re.fullmatch(
r'"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive '
r"-EncodedCommand ([A-Za-z0-9+/]+={0,2})",
command,
)
if (
match is None
or not ntpath.isabs(match.group(1))
or ntpath.basename(match.group(1)).lower() != "powershell.exe"
):
return None
try:
raw = base64.b64decode(match.group(2), validate=True)
if base64.b64encode(raw).decode("ascii") != match.group(2):
return None
source = raw.decode("utf-16le")
except (UnicodeDecodeError, ValueError):
return None
return _parse_powershell_source(source)
def _parse_legacy_posix_command(command: str) -> Optional[Tuple[str, str]]:
try:
arguments = shlex.split(command, posix=True)
except ValueError:
return None
if len(arguments) != 2 or shlex.join(arguments) != command:
return None
return arguments[0], arguments[1]
def _parse_legacy_windows_command(command: str) -> Optional[Tuple[str, str]]:
match = re.fullmatch(
r'(?:(?:"([^"\r\n]+)")|([^\s"\r\n]+)) '
r'(?:(?:"([^"\r\n]+)")|([^\s"\r\n]+))',
command,
)
if match is None:
return None
arguments = [match.group(1) or match.group(2), match.group(3) or match.group(4)]
if subprocess.list2cmdline(arguments) != command:
return None
return arguments[0], arguments[1]
def _is_python_executable(file_path: str) -> bool:
return (
re.fullmatch(
r"python(?:[0-9]+(?:\.[0-9]+)*)?(?:\.exe)?",
os.path.basename(file_path),
re.IGNORECASE,
)
is not None
)
def kimi_runtime_reference(
command: str, script_name: str
) -> Optional[KimiRuntimeReference]:
parsed = (
_parse_posix_command(command)
or _parse_windows_command(command)
or _parse_legacy_posix_command(command)
or _parse_legacy_windows_command(command)
)
if parsed is None:
return None
executable, script_path = parsed
if (
not os.path.isabs(executable)
or not os.path.isabs(script_path)
or not _is_python_executable(executable)
or os.path.basename(script_path) != script_name
):
return None
agent_directory = os.path.dirname(script_path)
if not same_kimi_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in {"", ".", ".."}:
return None
return KimiRuntimeReference(agent_id, script_path)
+331
View File
@@ -0,0 +1,331 @@
"""Kimi hook schemas, exact ownership, and TOML rendering."""
from __future__ import annotations
import copy
from dataclasses import dataclass
import os
from typing import Any, Dict, FrozenSet, List, Optional, Tuple
from tomlkit import aot, document, dumps, inline_table, parse, table
from tomlkit.items import AoT, Array
from .kimi_command import (
KimiRuntimeReference,
build_kimi_command,
kimi_runtime_reference,
same_kimi_agent_id,
same_kimi_path,
)
AGENT_KEY = "kimi"
GUARD_SCRIPT = "guard.py"
AUDIT_SCRIPT = "hook.py"
HOOK_TIMEOUT_SECONDS = 10
SHARED_EVENTS: FrozenSet[str] = frozenset(
{
"PreToolUse",
"PostToolUse",
"PostToolUseFailure",
"UserPromptSubmit",
"Stop",
"StopFailure",
"SessionStart",
"SessionEnd",
"SubagentStart",
"SubagentStop",
"PreCompact",
"PostCompact",
"Notification",
}
)
STABLE_EVENTS = SHARED_EVENTS | {
"PermissionRequest",
"PermissionResult",
"Interrupt",
}
LEGACY_EVENTS = SHARED_EVENTS
SUPPORTED_FIELDS = frozenset({"event", "matcher", "command", "timeout"})
TomlObject = Dict[str, Any]
ManagedEvent = str
@dataclass(frozen=True)
class KimiContract:
generation: str
runtime_name: str
label: str
directory_label: str
config_path: str
events: FrozenSet[str]
@dataclass(frozen=True)
class KimiDocument:
contract: KimiContract
exists: bool
value: Any
hooks: List[TomlObject]
uses_hook_tables: bool
raw: Optional[str] = None
@dataclass(frozen=True)
class RenderedKimiDocument:
document: KimiDocument
changed: bool
next_source: Optional[str] = None
@dataclass(frozen=True)
class KimiRuntimeContract:
agent_id: str
guard_path: str
audit_path: str
config_path: str
def _unwrap(value: Any) -> Any:
unwrap = getattr(value, "unwrap", None)
return unwrap() if callable(unwrap) else value
def validate_kimi_hook(
value: Any, contract: KimiContract, index: int
) -> TomlObject:
raw = _unwrap(value)
if not isinstance(raw, dict):
raise ValueError(f"{contract.label} hook {index + 1} must be a table")
hook = dict(raw)
unsupported = next(
(key for key in hook if key not in SUPPORTED_FIELDS),
None,
)
if unsupported is not None:
raise ValueError(
f'{contract.label} hook {index + 1} has unsupported field "{unsupported}"'
)
event = hook.get("event")
if not isinstance(event, str) or event not in contract.events:
raise ValueError(
f'{contract.label} hook {index + 1} has unsupported event "{event}"'
)
command = hook.get("command")
if not isinstance(command, str) or not command:
raise ValueError(
f"{contract.label} hook {index + 1} requires a non-empty command"
)
matcher = hook.get("matcher")
if matcher is not None and not isinstance(matcher, str):
raise ValueError(
f"{contract.label} hook {index + 1} matcher must be a string"
)
timeout = hook.get("timeout")
if timeout is not None and (
type(timeout) is not int or timeout < 1 or timeout > 600
):
raise ValueError(
f"{contract.label} hook {index + 1} timeout must be an integer from 1 to 600"
)
return hook
def _read_hooks(value: Any, contract: KimiContract) -> List[TomlObject]:
if "hooks" not in value:
return []
container = value["hooks"]
raw = _unwrap(container)
if not isinstance(raw, list):
raise ValueError(f'{contract.label} field "hooks" must be an array')
return [
validate_kimi_hook(item, contract, index)
for index, item in enumerate(raw)
]
def parse_kimi_document(contract: KimiContract, raw: str) -> KimiDocument:
try:
value = parse(raw)
except Exception as error:
raise ValueError(
f"Failed to parse {contract.label} at {contract.config_path}: {error}"
) from error
hooks = _read_hooks(value, contract)
return KimiDocument(
contract=contract,
exists=True,
value=value,
hooks=hooks,
uses_hook_tables=isinstance(value.get("hooks"), AoT),
raw=raw,
)
def create_kimi_document(contract: KimiContract) -> KimiDocument:
return KimiDocument(contract, False, document(), [], False)
def build_kimi_hook(event: ManagedEvent, script_path: str) -> TomlObject:
if event not in {"PreToolUse", "PostToolUse", "PostToolUseFailure"}:
raise ValueError(f"Unsupported managed Kimi event: {event}")
return {
"event": event,
"command": build_kimi_command(script_path),
"timeout": HOOK_TIMEOUT_SECONDS,
}
def _managed_reference(
hook: TomlObject,
event: ManagedEvent,
script_name: str,
) -> Optional[KimiRuntimeReference]:
if (
set(hook) != {"event", "command", "timeout"}
or hook.get("event") != event
or type(hook.get("timeout")) is not int
or hook.get("timeout") != HOOK_TIMEOUT_SECONDS
or not isinstance(hook.get("command"), str)
):
return None
return kimi_runtime_reference(str(hook["command"]), script_name)
def _managed_contract(event: Any) -> Optional[Tuple[ManagedEvent, str]]:
if event == "PreToolUse":
return "PreToolUse", GUARD_SCRIPT
if event == "PostToolUse":
return "PostToolUse", AUDIT_SCRIPT
if event == "PostToolUseFailure":
return "PostToolUseFailure", AUDIT_SCRIPT
return None
def remove_managed_kimi_hooks(
hooks: List[TomlObject], agent_id: str = ""
) -> List[TomlObject]:
result = []
for hook in hooks:
expected = _managed_contract(hook.get("event"))
reference = (
_managed_reference(hook, expected[0], expected[1])
if expected is not None
else None
)
remove = reference is not None and (
not agent_id or same_kimi_agent_id(reference.agent_id, agent_id)
)
if not remove:
result.append(hook)
return result
def _matched_indices(
original: List[TomlObject], desired: List[TomlObject]
) -> Tuple[set[int], set[int]]:
original_matches: set[int] = set()
desired_matches: set[int] = set()
start = 0
for desired_index, desired_hook in enumerate(desired):
for original_index in range(start, len(original)):
if original[original_index] == desired_hook:
original_matches.add(original_index)
desired_matches.add(desired_index)
start = original_index + 1
break
return original_matches, desired_matches
def _append_hook(container: Any, hook: TomlObject) -> None:
item = table() if isinstance(container, AoT) else inline_table()
for key, value in hook.items():
item[key] = value
container.append(item)
def _render_hook_source(
source: KimiDocument, hooks: List[TomlObject]
) -> Optional[str]:
candidate = copy.deepcopy(source.value)
original_matches, desired_matches = _matched_indices(source.hooks, hooks)
if "hooks" in candidate:
container: Any = candidate["hooks"]
if not isinstance(container, (AoT, Array)):
raise ValueError(f'{source.contract.label} field "hooks" must be an array')
for index in range(len(source.hooks) - 1, -1, -1):
if index not in original_matches:
del container[index]
else:
container = aot()
candidate.add("hooks", container)
for index, hook in enumerate(hooks):
if index not in desired_matches:
_append_hook(container, hook)
if len(container) == 0 and source.uses_hook_tables:
del candidate["hooks"]
rendered = dumps(candidate)
if not rendered.strip():
return None
parse_kimi_document(source.contract, rendered)
return rendered
def render_kimi_document(
source: KimiDocument, hooks: List[TomlObject]
) -> RenderedKimiDocument:
if not source.exists and not hooks:
return RenderedKimiDocument(source, False)
next_source = _render_hook_source(source, hooks)
return RenderedKimiDocument(
document=source,
changed=next_source != source.raw,
next_source=next_source,
)
def _references_for_event(
hooks: List[TomlObject], event: ManagedEvent, script_name: str
) -> Dict[str, List[KimiRuntimeReference]]:
result: Dict[str, List[KimiRuntimeReference]] = {}
for hook in hooks:
reference = _managed_reference(hook, event, script_name)
if reference is None:
continue
key = os.path.normcase(reference.agent_id)
result.setdefault(key, []).append(reference)
return result
def kimi_runtime_contracts(
documents: List[KimiDocument],
) -> List[KimiRuntimeContract]:
contracts: List[KimiRuntimeContract] = []
for source in documents:
guards = _references_for_event(source.hooks, "PreToolUse", GUARD_SCRIPT)
successes = _references_for_event(
source.hooks, "PostToolUse", AUDIT_SCRIPT
)
failures = _references_for_event(
source.hooks, "PostToolUseFailure", AUDIT_SCRIPT
)
for key, guard in guards.items():
success = successes.get(key, [])
failure = failures.get(key, [])
if len(guard) != 1 or len(success) != 1 or len(failure) != 1:
continue
if not same_kimi_path(success[0].script_path, failure[0].script_path):
continue
contracts.append(
KimiRuntimeContract(
agent_id=guard[0].agent_id,
guard_path=guard[0].script_path,
audit_path=success[0].script_path,
config_path=source.contract.config_path,
)
)
return contracts
+195
View File
@@ -0,0 +1,195 @@
"""Transactional Kimi hook and runtime installation."""
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import FileChange, file_change, source_change, write_changes
from .base import InstallConfig
from .hook_template import generate_guard_script, generate_hook_script
from .kimi_command import runtime_root, same_kimi_path
from .kimi_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
KimiDocument,
RenderedKimiDocument,
)
from .kimi_io import (
validate_api_origin,
validate_private_key,
validate_runtime_tree,
)
@dataclass(frozen=True)
class KimiRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> KimiRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
configured_guard = config.get("guard_script_path", "")
if not same_kimi_path(configured_guard, guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return KimiRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(f"Kimi installation requires agent_name {AGENT_KEY}")
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
def preflight_kimi_installation(
config: InstallConfig, documents: List[KimiDocument]
) -> KimiRuntimePaths:
if not documents:
raise ValueError("Kimi installation requires at least one hook contract")
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(
file_path,
label,
next_source,
mode,
maximum_bytes,
)
def rendered_change(
rendered: RenderedKimiDocument,
) -> Optional[FileChange]:
if not rendered.changed:
return None
source = rendered.document
return source_change(
source.contract.config_path,
source.contract.label,
source.raw,
rendered.next_source,
0o600,
MAX_SOURCE_BYTES,
)
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_kimi_installation(
config: InstallConfig,
paths: KimiRuntimePaths,
rendered: List[RenderedKimiDocument],
) -> List[FileChange]:
config_changes = _present([rendered_change(item) for item in rendered])
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id)
audit_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
native_payload=True,
agent_name=AGENT_KEY,
)
runtime_changes = _present(
[
_runtime_change(
paths.guard_path,
"Elydora guard runtime",
guard_script,
0o700,
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path,
"Elydora audit runtime",
audit_script,
0o700,
),
]
)
return [*runtime_changes, *config_changes]
def commit_kimi_installation(changes: List[FileChange]) -> None:
write_changes(changes, "Install Kimi hooks")
def prepare_kimi_uninstall(
rendered: List[RenderedKimiDocument],
) -> List[FileChange]:
return _present([rendered_change(item) for item in rendered])
def commit_kimi_uninstall(changes: List[FileChange]) -> None:
write_changes(changes, "Uninstall Kimi hooks")
+276
View File
@@ -0,0 +1,276 @@
"""Kimi configuration discovery and managed runtime I/O."""
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from .kimi_command import (
runtime_root,
same_kimi_agent_id,
same_kimi_path,
)
from .kimi_contract import (
AGENT_KEY,
LEGACY_EVENTS,
STABLE_EVENTS,
KimiContract,
KimiDocument,
KimiRuntimeContract,
create_kimi_document,
parse_kimi_document,
)
def _entry_exists(file_path: str, label: str) -> bool:
try:
os.lstat(file_path)
return True
except (FileNotFoundError, NotADirectoryError):
return False
except OSError as error:
raise OSError(f"Inspect {label} at {file_path}: {error}") from error
def _stable_contract(config_path: str) -> KimiContract:
return KimiContract(
generation="stable",
runtime_name="Kimi Code",
label="Kimi Code hooks config",
directory_label="Kimi Code home directory",
config_path=config_path,
events=frozenset(STABLE_EVENTS),
)
def _legacy_contract(config_path: str) -> KimiContract:
return KimiContract(
generation="legacy",
runtime_name="kimi-cli",
label="kimi-cli legacy hooks config",
directory_label="kimi-cli legacy home directory",
config_path=config_path,
events=frozenset(LEGACY_EVENTS),
)
def resolve_kimi_contracts() -> List[KimiContract]:
home = os.path.expanduser("~")
configured_home = os.environ.get("KIMI_CODE_HOME")
explicit_home = (
None
if configured_home is None or configured_home == ""
else os.path.abspath(configured_home)
)
stable_home = explicit_home or os.path.join(home, ".kimi-code")
legacy_home = os.path.join(home, ".kimi")
stable = _stable_contract(os.path.join(stable_home, "config.toml"))
legacy = _legacy_contract(os.path.join(legacy_home, "config.toml"))
if same_kimi_path(stable.config_path, legacy.config_path):
return [stable]
stable_detected = explicit_home is not None or _entry_exists(
stable_home, "Kimi Code home"
)
legacy_detected = _entry_exists(legacy_home, "kimi-cli legacy home")
if legacy_detected and not stable_detected:
return [legacy]
return [stable, legacy] if legacy_detected else [stable]
def _read_kimi_document(contract: KimiContract) -> KimiDocument:
directory = os.path.dirname(contract.config_path)
physical_directory_exists(directory, contract.directory_label)
snapshot = read_physical_file(contract.config_path, contract.label)
if snapshot is None:
return create_kimi_document(contract)
return parse_kimi_document(contract, snapshot.contents)
def read_kimi_documents() -> List[KimiDocument]:
return [_read_kimi_document(contract) for contract in resolve_kimi_contracts()]
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"),
validate=True,
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(
value: Any, field: str, config_path: str
) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject,
expected_agent_id: str,
config_path: str,
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_kimi_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime identity does not match Kimi hooks: {config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path,
"Elydora runtime config",
MAX_CONFIG_BYTES,
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents,
f"Elydora runtime config at {file_path}",
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = runtime_root()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = [
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
]
artifact_states = [
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
]
artifact_exists = any(artifact_states)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_kimi_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime config identity does not match Kimi agent {agent_id}: "
f"{config_path}"
)
def _runtime_contract_exists(contract: KimiRuntimeContract) -> bool:
root = runtime_root()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_kimi_path(os.path.dirname(agent_directory), root)
or not same_kimi_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
return bool(guard.contents) and bool(audit.contents)
def kimi_runtime_files_exist(contracts: List[KimiRuntimeContract]) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
+1 -1
View File
@@ -63,7 +63,7 @@ SUPPORTED_AGENTS: Dict[str, Dict[str, str]] = {
},
"kimi": {
"name": "Kimi Code",
"hook_event": "PreToolUse/PostToolUse",
"hook_event": "PreToolUse/PostToolUse/PostToolUseFailure",
"config_path": "~/.kimi-code/config.toml",
},
"letta": {