fix(python): align Kimi hooks with official contracts

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 04:31:42 -04:00
1 parent 2eddd5cf88
commit fd62dca155
13 files changed
+2105 -745

No files matched your search

+265
View File
@@ -0,0 +1,265 @@
from __future__ import annotations
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import shlex
import subprocess # nosec B404
import sys
from threading import Thread
from typing import Any, Mapping
import pytest
from tomlkit import parse
from elydora.plugins.kimi import KimiPlugin
from elydora.utils import base64url_encode
AGENT_ID = "agent-1"
MISSING = object()
VALID_PRIVATE_KEY = base64url_encode(bytes(range(32)))
@dataclass
class KimiFixture:
root_dir: Path
home_dir: Path
project_dir: Path
kimi_home: Path
stable_path: Path
legacy_home: Path
legacy_path: Path
agent_dir: Path
guard_path: Path
hook_path: Path
runtime_config_path: Path
private_key_path: Path
plugin: KimiPlugin
config: dict[str, str]
def install(self) -> None:
self.plugin.install(self.config)
class KimiApiHandler(BaseHTTPRequestHandler):
agent_status: Any = "active"
operation_status = 201
requests: list[dict[str, Any]] = []
@classmethod
def reset(cls) -> None:
cls.agent_status = "active"
cls.operation_status = 201
cls.requests = []
def _respond(self, status: int, value: object) -> None:
body = json.dumps(value).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
type(self).requests.append(
{
"method": "GET",
"path": self.path,
"authorization": self.headers.get("Authorization"),
}
)
self._respond(200, {"agent": {"status": type(self).agent_status}})
def do_POST(self) -> None:
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length)
type(self).requests.append(
{
"method": "POST",
"path": self.path,
"authorization": self.headers.get("Authorization"),
"raw": raw,
"json": json.loads(raw),
}
)
status = type(self).operation_status
value: object = (
{"operation": {"accepted": True}}
if 200 <= status < 300
else {"error": {"code": "UPSTREAM_FAILURE", "message": "failed"}}
)
self._respond(status, value)
def log_message(self, _format: str, *_args: object) -> None:
return
def start_api_server(
*, status: str = "active", operation_status: int = 201
) -> ThreadingHTTPServer:
KimiApiHandler.reset()
KimiApiHandler.agent_status = status
KimiApiHandler.operation_status = operation_status
server = ThreadingHTTPServer(("127.0.0.1", 0), KimiApiHandler)
Thread(target=server.serve_forever, daemon=True).start()
return server
def server_base_url(server: ThreadingHTTPServer) -> str:
return f"http://127.0.0.1:{server.server_port}"
def write_text(path: Path, source: str, mode: int = 0o600) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(source, encoding="utf-8")
os.chmod(path, mode)
def write_optional(path: Path, value: object) -> None:
if value is MISSING:
return
write_text(path, str(value))
def prepare_fixture(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
*,
stable_config: object = MISSING,
legacy_config: object = MISSING,
stable_detected: bool = True,
legacy_detected: bool = True,
explicit_kimi_home: bool = True,
base_url: str = "http://127.0.0.1:9",
) -> KimiFixture:
home_dir = tmp_path / "home with spaces and 'quote %ELYDORA_HOOK_PATH%"
project_dir = tmp_path / "project with spaces"
kimi_home = (
home_dir / "custom kimi-code"
if explicit_kimi_home
else home_dir / ".kimi-code"
)
stable_path = kimi_home / "config.toml"
legacy_home = home_dir / ".kimi"
legacy_path = legacy_home / "config.toml"
agent_dir = home_dir / ".elydora" / AGENT_ID
guard_path = agent_dir / "guard.py"
hook_path = agent_dir / "hook.py"
project_dir.mkdir(parents=True)
if stable_detected and not explicit_kimi_home:
kimi_home.mkdir(parents=True)
if legacy_detected:
legacy_home.mkdir(parents=True)
write_optional(stable_path, stable_config)
write_optional(legacy_path, legacy_config)
monkeypatch.setenv("HOME", str(home_dir))
monkeypatch.setenv("USERPROFILE", str(home_dir))
monkeypatch.setenv("ELYDORA_HOOK_PATH", str(tmp_path / "expanded"))
if explicit_kimi_home:
monkeypatch.setenv("KIMI_CODE_HOME", str(kimi_home))
else:
monkeypatch.delenv("KIMI_CODE_HOME", raising=False)
config = {
"org_id": "org-1",
"agent_id": AGENT_ID,
"agent_name": "kimi",
"private_key": VALID_PRIVATE_KEY,
"kid": "kid-1",
"token": "token-1",
"base_url": base_url,
"guard_script_path": str(guard_path),
}
return KimiFixture(
root_dir=tmp_path,
home_dir=home_dir,
project_dir=project_dir,
kimi_home=kimi_home,
stable_path=stable_path,
legacy_home=legacy_home,
legacy_path=legacy_path,
agent_dir=agent_dir,
guard_path=guard_path,
hook_path=hook_path,
runtime_config_path=agent_dir / "config.json",
private_key_path=agent_dir / "private.key",
plugin=KimiPlugin(),
config=config,
)
def parsed_hooks(path: Path) -> list[dict[str, Any]]:
value = parse(path.read_text(encoding="utf-8"))["hooks"].unwrap()
assert isinstance(value, list)
return value
def managed_hook(
hooks: list[dict[str, Any]], event: str
) -> dict[str, Any]:
return next(
hook
for hook in reversed(hooks)
if hook.get("event") == event and hook.get("timeout") == 10
)
def assert_managed_hook(hook: Mapping[str, Any], event: str) -> None:
assert set(hook) == {"event", "command", "timeout"}
assert hook["event"] == event
assert hook["timeout"] == 10
command = str(hook["command"])
if os.name == "nt":
assert "powershell.exe" in command.lower()
assert " -EncodedCommand " in command
else:
assert command.startswith("'")
def assert_managed_triple(hooks: list[dict[str, Any]]) -> None:
for event in ("PreToolUse", "PostToolUse", "PostToolUseFailure"):
assert_managed_hook(managed_hook(hooks, event), event)
assert managed_hook(hooks, "PostToolUse")["command"] == managed_hook(
hooks, "PostToolUseFailure"
)["command"]
def run_hook(
command: str,
payload: str,
fixture: KimiFixture,
environment: Mapping[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
command,
shell=True,
input=payload,
capture_output=True,
check=False,
cwd=fixture.project_dir,
env={
**os.environ,
"HOME": str(fixture.home_dir),
"USERPROFILE": str(fixture.home_dir),
**dict(environment or {}),
},
text=True,
)
def legacy_command(script_path: Path) -> str:
arguments = [sys.executable, str(script_path)]
return (
subprocess.list2cmdline(arguments)
if os.name == "nt"
else shlex.join(arguments)
)
def assert_no_transaction_files(root: Path) -> None:
assert not list(root.rglob("*.tmp"))
assert not list(root.rglob("*.rollback"))
+217 -300
View File
@@ -1,423 +1,340 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from pathlib import Path
import subprocess
from typing import Any
import pytest
from tomlkit import parse
from tomlkit import dumps, parse
from elydora import cli
from elydora.plugins import kimi
from elydora.plugins.base import InstallConfig
from elydora.plugins.registry import SUPPORTED_AGENTS
from kimi_support import (
AGENT_ID,
MISSING,
assert_managed_triple,
assert_no_transaction_files,
legacy_command,
parsed_hooks,
prepare_fixture,
)
AGENT_ID = "agent-1"
MISSING = object()
@dataclass(frozen=True)
class KimiFixture:
plugin: kimi.KimiPlugin
config: InstallConfig
home_dir: Path
agent_dir: Path
modern_path: Path
legacy_path: Path
guard_path: Path
hook_path: Path
runtime_config_path: Path
private_key_path: Path
def write_optional(path: Path, value: object) -> None:
if value is MISSING:
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(str(value), encoding="utf-8")
def prepare_fixture(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
*,
modern_config: object = MISSING,
legacy_config: object = MISSING,
legacy_installed: bool = True,
explicit_kimi_home: bool = True,
create_guard: bool = True,
) -> KimiFixture:
home_dir = tmp_path / "home with spaces"
agent_dir = home_dir / ".elydora" / AGENT_ID
kimi_home = (
home_dir / "custom kimi code"
if explicit_kimi_home
else home_dir / ".kimi-code"
)
modern_path = kimi_home / "config.toml"
legacy_path = home_dir / ".kimi" / "config.toml"
guard_path = agent_dir / "guard.py"
hook_path = agent_dir / "hook.py"
agent_dir.mkdir(parents=True)
if create_guard:
guard_path.write_text(
"import sys\nsys.stdin.read()\n"
"sys.stderr.write('Agent is frozen by Elydora.')\n"
"raise SystemExit(2)\n",
encoding="utf-8",
)
write_optional(modern_path, modern_config)
write_optional(legacy_path, legacy_config)
monkeypatch.setattr(kimi, "ELYDORA_DIR", str(agent_dir.parent))
monkeypatch.setattr(kimi, "_home_dir", lambda: str(home_dir))
monkeypatch.setattr(
kimi.shutil,
"which",
lambda command: str(home_dir / "bin" / command)
if legacy_installed and command == "kimi-cli"
else None,
)
if explicit_kimi_home:
monkeypatch.setenv("KIMI_CODE_HOME", str(kimi_home))
else:
monkeypatch.delenv("KIMI_CODE_HOME", raising=False)
config: InstallConfig = {
"agent_id": AGENT_ID,
"agent_name": "kimi",
"org_id": "org-1",
"private_key": "test-key",
"kid": "kid-1",
"base_url": "https://api.elydora.test",
"guard_script_path": str(guard_path),
}
return KimiFixture(
plugin=kimi.KimiPlugin(),
config=config,
home_dir=home_dir,
agent_dir=agent_dir,
modern_path=modern_path,
legacy_path=legacy_path,
guard_path=guard_path,
hook_path=hook_path,
runtime_config_path=agent_dir / "config.json",
private_key_path=agent_dir / "private.key",
)
def parsed_hooks(path: Path) -> list[dict[str, Any]]:
document = parse(path.read_text(encoding="utf-8"))
value = document["hooks"].unwrap()
assert isinstance(value, list)
return value
def managed_hook(
hooks: list[dict[str, Any]], event: str, script_name: str
) -> dict[str, Any]:
return next(
hook
for hook in hooks
if hook.get("event") == event and script_name in str(hook.get("command"))
)
def assert_strict_hook(hook: dict[str, Any]) -> None:
assert set(hook) == {"event", "command", "timeout"}
assert hook["timeout"] == 10
def run_command(
command: str, home_dir: Path, payload: dict[str, Any]
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
command,
shell=True,
capture_output=True,
check=False,
env={**os.environ, "HOME": str(home_dir), "USERPROFILE": str(home_dir)},
input=json.dumps(payload),
text=True,
)
STABLE_USER_CONFIG = (
'# stable user config\ndefault_model = "kimi-code/k3"\n\n'
'[[hooks]]\nevent = "SessionStart"\ncommand = "existing-stable"\n'
"timeout = 30 # keep stable hook\n"
)
LEGACY_USER_CONFIG = (
"# legacy user config\ntelemetry = false\n\n"
'[[hooks]]\nevent = "SessionEnd"\ncommand = "existing-legacy"\n'
)
def test_kimi_is_registered_in_sdk_and_cli() -> None:
assert SUPPORTED_AGENTS["kimi"] == {
"name": "Kimi Code",
"hook_event": "PreToolUse/PostToolUse",
"hook_event": "PreToolUse/PostToolUse/PostToolUseFailure",
"config_path": "~/.kimi-code/config.toml",
}
assert cli.PLUGIN_MAP["kimi"] is kimi.KimiPlugin
def test_install_preserves_both_configs_and_is_idempotent(
def test_install_writes_exact_triples_to_both_configs_and_is_idempotent(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
modern = (
'# modern user config\ndefault_model = "kimi-code/k3"\n\n'
'[[hooks]]\nevent = "SessionStart"\ncommand = "existing-modern"\n'
"timeout = 30 # keep modern hook\n"
)
legacy = (
"# legacy user config\ntelemetry = false\n\n"
'[[hooks]]\nevent = "SessionEnd"\ncommand = "existing-legacy"\n'
)
fixture = prepare_fixture(
monkeypatch,
tmp_path,
modern_config=modern,
legacy_config=legacy,
stable_config=STABLE_USER_CONFIG,
legacy_config=LEGACY_USER_CONFIG,
)
fixture.plugin.install(fixture.config)
fixture.plugin.install(fixture.config)
fixture.install()
first_sources = {
path: path.read_text(encoding="utf-8")
for path in (fixture.stable_path, fixture.legacy_path)
}
fixture.install()
assert "Kimi Code and kimi-cli" in capsys.readouterr().out
for path, comment, command in (
(fixture.modern_path, "# modern user config", "existing-modern"),
(fixture.stable_path, "# stable user config", "existing-stable"),
(fixture.legacy_path, "# legacy user config", "existing-legacy"),
):
raw = path.read_text(encoding="utf-8")
hooks = parsed_hooks(path)
assert raw == first_sources[path]
assert comment in raw
assert command in raw
assert len(hooks) == 3
assert_strict_hook(managed_hook(hooks, "PreToolUse", "guard.py"))
assert_strict_hook(managed_hook(hooks, "PostToolUse", "hook.py"))
assert (fixture.home_dir / ".kimi-code" / "config.toml").exists() is False
assert any(hook.get("command") == command for hook in hooks)
assert len(hooks) == 4
assert_managed_triple(hooks)
assert not (fixture.home_dir / ".kimi-code" / "config.toml").exists()
def test_install_preserves_inline_hook_array_style(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
modern = (
source = (
'# inline user hook\nhooks = [{ event = "SessionStart", '
'command = "existing-inline" }] # keep array\n'
)
fixture = prepare_fixture(
monkeypatch,
tmp_path,
modern_config=modern,
legacy_installed=False,
stable_config=source,
legacy_detected=False,
)
fixture.plugin.install(fixture.config)
fixture.plugin.install(fixture.config)
fixture.install()
fixture.install()
raw = fixture.modern_path.read_text(encoding="utf-8")
raw = fixture.stable_path.read_text(encoding="utf-8")
assert "# inline user hook" in raw
assert "# keep array" in raw
assert "hooks = [" in raw
assert len(parsed_hooks(fixture.modern_path)) == 3
hooks = parsed_hooks(fixture.stable_path)
assert len(hooks) == 4
assert_managed_triple(hooks)
def test_modern_install_avoids_false_legacy_migration_source(
def test_uninstall_preserves_user_owned_empty_inline_hook_array(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_installed=False)
fixture.plugin.install(fixture.config)
source = "# user container\nhooks = [] # keep empty array\n"
fixture = prepare_fixture(
monkeypatch,
tmp_path,
stable_config=source,
legacy_detected=False,
)
fixture.install()
fixture.plugin.uninstall(AGENT_ID)
assert len(parsed_hooks(fixture.modern_path)) == 2
assert fixture.legacy_path.exists() is False
raw = fixture.stable_path.read_text(encoding="utf-8")
assert "# user container" in raw
assert "# keep empty array" in raw
assert parse(raw)["hooks"].unwrap() == []
def test_empty_home_override_uses_official_default(
def test_fresh_stable_install_leaves_legacy_source_absent(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
fixture.install()
hooks = parsed_hooks(fixture.stable_path)
assert len(hooks) == 3
assert_managed_triple(hooks)
assert not fixture.legacy_path.exists()
def test_empty_home_override_uses_documented_default(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(
monkeypatch,
tmp_path,
explicit_kimi_home=False,
legacy_installed=False,
legacy_detected=False,
)
fixture.plugin.install(fixture.config)
fixture.install()
monkeypatch.setenv("KIMI_CODE_HOME", "")
assert fixture.plugin.status()["installed"] is True
assert_managed_triple(parsed_hooks(fixture.stable_path))
def test_legacy_install_avoids_premature_modern_migration_target(
def test_legacy_only_install_leaves_stable_target_absent(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, explicit_kimi_home=False)
fixture.plugin.install(fixture.config)
assert len(parsed_hooks(fixture.legacy_path)) == 2
assert fixture.modern_path.exists() is False
monkeypatch.setattr(kimi.shutil, "which", lambda command: None)
assert fixture.plugin.status()["installed"] is True
def test_commands_block_and_forward_official_payload(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
capture_path = tmp_path / "captured-event.json"
fixture.hook_path.write_text(
"from pathlib import Path\nimport sys\n"
f"Path({str(capture_path)!r}).write_text(sys.stdin.read(), encoding='utf-8')\n",
encoding="utf-8",
)
hooks = parsed_hooks(fixture.modern_path)
payload = {
"hook_event_name": "PreToolUse",
"session_id": "session-1",
"cwd": str(fixture.home_dir),
"tool_name": "Bash",
"tool_input": {"command": "echo test"},
"tool_call_id": "call-1",
}
guard = managed_hook(hooks, "PreToolUse", "guard.py")
guard_result = run_command(guard["command"], fixture.home_dir, payload)
assert guard_result.returncode == 2
assert "Agent is frozen by Elydora" in guard_result.stderr
payload["hook_event_name"] = "PostToolUse"
payload["tool_output"] = {"output": "test"}
audit = managed_hook(hooks, "PostToolUse", "hook.py")
audit_result = run_command(audit["command"], fixture.home_dir, payload)
assert audit_result.returncode == 0
assert json.loads(capture_path.read_text(encoding="utf-8")) == payload
def test_status_accepts_either_contract_and_requires_both_scripts(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
fixture.modern_path.unlink()
assert fixture.plugin.status()["installed"] is True
fixture.plugin.install(fixture.config)
fixture.legacy_path.unlink()
assert fixture.plugin.status()["installed"] is True
fixture.guard_path.unlink()
assert fixture.plugin.status()["installed"] is False
def test_uninstall_preserves_user_hooks_in_both_contracts(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
user_hook = (
"# user hook\n[[hooks]]\nevent = \"SessionStart\"\n"
'command = "existing-command"\ntimeout = 30 # keep timeout\n'
)
fixture = prepare_fixture(
monkeypatch,
tmp_path,
modern_config=user_hook,
legacy_config=user_hook,
explicit_kimi_home=False,
stable_detected=False,
)
fixture.plugin.install(fixture.config)
fixture.install()
fixture.plugin.uninstall(AGENT_ID)
for path in (fixture.modern_path, fixture.legacy_path):
raw = path.read_text(encoding="utf-8")
assert "# user hook" in raw
assert "# keep timeout" in raw
assert parsed_hooks(path) == [{
"event": "SessionStart",
"command": "existing-command",
"timeout": 30,
}]
assert_managed_triple(parsed_hooks(fixture.legacy_path))
assert not fixture.stable_path.exists()
assert fixture.plugin.status()["installed"] is True
def test_uninstall_removes_configs_created_by_elydora(
def test_install_parses_every_selected_config_before_runtime_writes(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
fixture.plugin.uninstall(AGENT_ID)
assert fixture.modern_path.exists() is False
assert fixture.legacy_path.exists() is False
def test_install_parses_every_config_before_runtime_writes(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
modern = '# untouched modern\ndefault_model = "kimi-code/k3"\n'
fixture = prepare_fixture(
monkeypatch,
tmp_path,
modern_config=modern,
stable_config=STABLE_USER_CONFIG,
legacy_config="[malformed",
)
with pytest.raises(ValueError, match="parse kimi-cli legacy hooks config"):
fixture.plugin.install(fixture.config)
fixture.install()
assert fixture.modern_path.read_text(encoding="utf-8") == modern
assert fixture.stable_path.read_text(encoding="utf-8") == STABLE_USER_CONFIG
assert fixture.legacy_path.read_text(encoding="utf-8") == "[malformed"
assert fixture.hook_path.exists() is False
assert fixture.runtime_config_path.exists() is False
assert fixture.private_key_path.exists() is False
assert not fixture.agent_dir.exists()
def test_install_rejects_invalid_hook_shape_without_writes(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
@pytest.mark.parametrize(
("stable_config", "legacy_config", "pattern"),
[
(
'[[hooks]]\nevent = "PreToolUse"\ncommand = "existing"\n'
'cwd = "/tmp"\n',
None,
'unsupported field "cwd"',
),
(
None,
'[[hooks]]\nevent = "Interrupt"\ncommand = "existing"\n',
'unsupported event "Interrupt"',
),
(
'[[hooks]]\nevent = "PreToolUse"\ncommand = "existing"\n'
"timeout = 601\n",
None,
"integer from 1 to 600",
),
],
)
def test_install_rejects_fields_and_events_outside_each_contract(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
stable_config: str | None,
legacy_config: str | None,
pattern: str,
) -> None:
modern = (
'[[hooks]]\nevent = "PreToolUse"\ncommand = "existing-command"\n'
'cwd = "/tmp"\n'
)
fixture = prepare_fixture(
monkeypatch,
tmp_path,
modern_config=modern,
legacy_installed=False,
stable_config=stable_config if stable_config is not None else MISSING,
legacy_config=legacy_config if legacy_config is not None else MISSING,
)
with pytest.raises(ValueError, match='unsupported field "cwd"'):
fixture.plugin.install(fixture.config)
with pytest.raises(ValueError, match=pattern):
fixture.install()
assert fixture.modern_path.read_text(encoding="utf-8") == modern
assert fixture.hook_path.exists() is False
assert fixture.runtime_config_path.exists() is False
assert not fixture.agent_dir.exists()
def test_install_rejects_missing_guard_before_creating_files(
def test_install_migrates_exact_legacy_commands_and_preserves_lookalikes(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, create_guard=False)
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
lookalike = f"{legacy_command(fixture.guard_path)} --inspect"
source = (
'[[hooks]]\nevent = "PreToolUse"\n'
f"command = {json.dumps(legacy_command(fixture.guard_path))}\n"
"timeout = 10\n\n"
'[[hooks]]\nevent = "PostToolUse"\n'
f"command = {json.dumps(legacy_command(fixture.hook_path))}\n"
"timeout = 10\n\n"
'[[hooks]]\nevent = "PreToolUse"\n'
f"command = {json.dumps(lookalike)}\ntimeout = 10\n"
)
fixture.stable_path.parent.mkdir(parents=True, exist_ok=True)
fixture.stable_path.write_text(source, encoding="utf-8")
with pytest.raises(FileNotFoundError, match="guard runtime is missing"):
fixture.plugin.install(fixture.config)
fixture.install()
assert fixture.modern_path.exists() is False
assert fixture.legacy_path.exists() is False
assert fixture.hook_path.exists() is False
assert fixture.runtime_config_path.exists() is False
hooks = parsed_hooks(fixture.stable_path)
assert len(hooks) == 4
assert any(hook.get("command") == lookalike for hook in hooks)
assert_managed_triple(hooks)
fixture.plugin.uninstall(AGENT_ID)
assert parsed_hooks(fixture.stable_path) == [
{"event": "PreToolUse", "command": lookalike, "timeout": 10}
]
def test_status_surfaces_malformed_runtime_metadata(
def test_uninstall_preserves_user_configs_and_removes_managed_configs(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
fixture.runtime_config_path.write_text("{ malformed", encoding="utf-8")
fixture = prepare_fixture(
monkeypatch,
tmp_path,
stable_config=STABLE_USER_CONFIG,
legacy_config=LEGACY_USER_CONFIG,
)
fixture.install()
fixture.plugin.uninstall(AGENT_ID)
with pytest.raises(ValueError, match="parse Elydora runtime config"):
assert "# keep stable hook" in fixture.stable_path.read_text(encoding="utf-8")
assert "# legacy user config" in fixture.legacy_path.read_text(encoding="utf-8")
assert [hook["command"] for hook in parsed_hooks(fixture.stable_path)] == [
"existing-stable"
]
assert [hook["command"] for hook in parsed_hooks(fixture.legacy_path)] == [
"existing-legacy"
]
managed = prepare_fixture(
monkeypatch,
tmp_path / "managed",
)
managed.install()
managed.plugin.uninstall(AGENT_ID)
assert not managed.stable_path.exists()
assert not managed.legacy_path.exists()
def test_status_requires_complete_triple_runtime_identity_and_private_key(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
fixture.install()
assert fixture.plugin.status()["installed"] is True
document = parse(fixture.stable_path.read_text(encoding="utf-8"))
hooks = document["hooks"]
for index in range(len(hooks) - 1, -1, -1):
if hooks[index]["event"] == "PostToolUseFailure":
del hooks[index]
fixture.stable_path.write_text(dumps(document), encoding="utf-8")
assert fixture.plugin.status()["installed"] is False
fixture.install()
fixture.private_key_path.write_text("invalid", encoding="utf-8")
with pytest.raises(ValueError, match="private key.*canonical 32-byte"):
fixture.plugin.status()
def test_atomic_writes_leave_no_temporary_files(
@pytest.mark.parametrize(
"runtime_name",
["guard.py", "hook.py", "config.json", "private.key"],
)
def test_status_requires_every_runtime_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
runtime_name: str,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
fixture.install()
(fixture.agent_dir / runtime_name).unlink()
assert fixture.plugin.status()["installed"] is False
def test_installation_leaves_no_transaction_files(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
fixture.install()
for directory in (fixture.modern_path.parent, fixture.legacy_path.parent):
assert all(path.suffix != ".tmp" for path in directory.iterdir())
assert_no_transaction_files(fixture.home_dir)
if os.name != "nt":
for path in (
fixture.stable_path,
fixture.legacy_path,
fixture.runtime_config_path,
fixture.private_key_path,
):
assert path.stat().st_mode & 0o777 == 0o600
+161
View File
@@ -0,0 +1,161 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from kimi_support import (
KimiApiHandler,
managed_hook,
parsed_hooks,
prepare_fixture,
run_hook,
server_base_url,
start_api_server,
)
PRE_PAYLOAD = {
"hook_event_name": "PreToolUse",
"session_id": "session-1",
"cwd": "C:/project",
"tool_name": "Bash",
"tool_input": {"command": "echo test"},
"tool_call_id": "call-1",
}
def test_runtimes_enforce_active_state_and_preserve_success_and_failure_payloads(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
server = start_api_server()
fixture = prepare_fixture(
monkeypatch,
tmp_path,
base_url=server_base_url(server),
legacy_detected=False,
)
try:
fixture.install()
hooks = parsed_hooks(fixture.stable_path)
guard = managed_hook(hooks, "PreToolUse")
success_audit = managed_hook(hooks, "PostToolUse")
failure_audit = managed_hook(hooks, "PostToolUseFailure")
guard_result = run_hook(
str(guard["command"]),
json.dumps(PRE_PAYLOAD),
fixture,
{"ELYDORA_HOOK_PATH": "injected-command-fragment"},
)
assert guard_result.returncode == 0, guard_result.stderr
success_payload = {
**PRE_PAYLOAD,
"hook_event_name": "PostToolUse",
"tool_output": "test\n",
}
success = run_hook(
str(success_audit["command"]),
json.dumps(success_payload),
fixture,
)
assert success.returncode == 0, success.stderr
failure_payload = {
**PRE_PAYLOAD,
"hook_event_name": "PostToolUseFailure",
"error": {
"name": "ToolError",
"message": "command failed",
"code": "tool.failed",
},
}
failure = run_hook(
str(failure_audit["command"]),
json.dumps(failure_payload),
fixture,
)
assert failure.returncode == 0, failure.stderr
operations = [
request["json"]
for request in KimiApiHandler.requests
if request["method"] == "POST"
]
assert len(operations) == 2
assert operations[0]["payload"] == success_payload
assert operations[1]["payload"] == failure_payload
assert operations[0]["subject"]["session_id"] == "session-1"
assert operations[1]["action"]["tool"] == "Bash"
finally:
server.shutdown()
server.server_close()
@pytest.mark.parametrize("status", ["frozen", "revoked"])
def test_guard_propagates_exit_code_two_for_blocked_agents(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
status: str,
) -> None:
server = start_api_server(status=status)
fixture = prepare_fixture(
monkeypatch,
tmp_path,
base_url=server_base_url(server),
legacy_detected=False,
)
try:
fixture.install()
command = managed_hook(
parsed_hooks(fixture.stable_path), "PreToolUse"
)["command"]
result = run_hook(str(command), json.dumps(PRE_PAYLOAD), fixture)
assert result.returncode == 2
assert status in result.stderr.lower()
finally:
server.shutdown()
server.server_close()
def test_audit_runtime_records_malformed_input_and_api_failures(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
server = start_api_server(operation_status=503)
fixture = prepare_fixture(
monkeypatch,
tmp_path,
base_url=server_base_url(server),
legacy_detected=False,
)
try:
fixture.install()
command = managed_hook(
parsed_hooks(fixture.stable_path), "PostToolUse"
)["command"]
malformed = run_hook(str(command), "{ malformed", fixture)
assert malformed.returncode == 0
assert malformed.stderr == ""
upstream = run_hook(
str(command),
json.dumps(
{
**PRE_PAYLOAD,
"hook_event_name": "PostToolUse",
"tool_output": "test",
}
),
fixture,
)
assert upstream.returncode == 0
assert upstream.stderr == ""
log = (fixture.agent_dir / "error.log").read_text(encoding="utf-8")
assert "invalid JSON" in log
assert "HTTP 503" in log
finally:
server.shutdown()
server.server_close()
+197
View File
@@ -0,0 +1,197 @@
from __future__ import annotations
import os
from pathlib import Path
import shutil
import subprocess # nosec B404
import sys
import pytest
from kimi_support import (
AGENT_ID,
VALID_PRIVATE_KEY,
prepare_fixture,
write_text,
)
def symlink_or_skip(
target: Path, link: Path, *, directory: bool = False
) -> None:
try:
link.symlink_to(target, target_is_directory=directory)
except OSError as error:
pytest.skip(f"Symbolic links unavailable: {error}")
def test_install_rejects_linked_config_before_creating_runtimes(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
target = tmp_path / "config-target.toml"
source = "# protected target\ntelemetry = false\n"
write_text(target, source)
fixture.stable_path.parent.mkdir(parents=True)
symlink_or_skip(target, fixture.stable_path)
with pytest.raises(OSError, match="not a physical file"):
fixture.install()
assert target.read_text(encoding="utf-8") == source
assert not fixture.agent_dir.exists()
@pytest.mark.parametrize("kind", ["config", "runtime"])
def test_install_rejects_linked_config_and_runtime_directories(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
kind: str,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
target = tmp_path / f"{kind}-target"
target.mkdir()
link = fixture.kimi_home if kind == "config" else fixture.agent_dir.parent
if link.exists():
shutil.rmtree(link)
link.parent.mkdir(parents=True, exist_ok=True)
symlink_or_skip(target, link, directory=True)
with pytest.raises(OSError, match="physical directory"):
fixture.install()
assert list(target.iterdir()) == []
untouched = fixture.agent_dir if kind == "config" else fixture.stable_path
assert not untouched.exists()
def test_status_rejects_linked_private_key(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
fixture.install()
target = tmp_path / "private-key-target"
source = fixture.private_key_path.read_text(encoding="utf-8")
write_text(target, source)
fixture.private_key_path.unlink()
symlink_or_skip(target, fixture.private_key_path)
with pytest.raises(OSError, match="not a physical file"):
fixture.plugin.status()
assert target.read_text(encoding="utf-8") == source
@pytest.mark.parametrize("kind", ["orphaned", "mismatched"])
def test_install_rejects_unverifiable_runtime_identity_before_config_writes(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
kind: str,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
fixture.agent_dir.mkdir(parents=True)
if kind == "orphaned":
write_text(fixture.guard_path, "orphaned guard\n", 0o700)
else:
write_text(
fixture.runtime_config_path,
'{"agent_name":"kimi","agent_id":"another-agent"}\n',
)
pattern = "identity cannot be verified" if kind == "orphaned" else "identity does not match"
with pytest.raises(ValueError, match=pattern):
fixture.install()
assert not fixture.stable_path.exists()
@pytest.mark.parametrize(
("field", "value", "pattern"),
[
("agent_name", "codex", "requires agent_name kimi"),
("agent_id", "../escape", "Invalid agent ID"),
("private_key", "invalid", "canonical 32-byte"),
("token", "", "non-empty string"),
(
"base_url",
"https://api.elydora.com/path?token=secret",
"query parameters",
),
("guard_script_path", "outside", "managed agent directory"),
],
)
def test_install_validates_runtime_inputs_before_creating_files(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
field: str,
value: str,
pattern: str,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
fixture.config[field] = (
str(fixture.home_dir / "outside" / "guard.py")
if field == "guard_script_path"
else value
)
with pytest.raises(ValueError, match=pattern):
fixture.install()
assert not fixture.stable_path.exists()
assert not fixture.agent_dir.exists()
def test_status_surfaces_malformed_runtime_metadata(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
fixture.install()
fixture.runtime_config_path.write_text("{ malformed", encoding="utf-8")
with pytest.raises(ValueError, match="parse Elydora runtime config"):
fixture.plugin.status()
def test_cli_preflight_preserves_malformed_config_before_runtime_creation(
tmp_path: Path,
) -> None:
home_dir = tmp_path / "cli-home"
config_path = home_dir / ".kimi-code" / "config.toml"
key_path = tmp_path / "private.key"
write_text(config_path, "[malformed")
write_text(key_path, VALID_PRIVATE_KEY)
result = subprocess.run(
[
sys.executable,
"-m",
"elydora.cli",
"install",
"--agent",
"kimi",
"--org_id",
"org-1",
"--agent_id",
AGENT_ID,
"--private_key_file",
str(key_path),
"--kid",
"kid-1",
"--base_url",
"https://api.elydora.com",
],
capture_output=True,
check=False,
cwd=Path(__file__).parents[1],
env={
**os.environ,
"HOME": str(home_dir),
"USERPROFILE": str(home_dir),
"KIMI_CODE_HOME": "",
},
text=True,
)
assert result.returncode == 1
assert "parse Kimi Code hooks config" in result.stderr
assert config_path.read_text(encoding="utf-8") == "[malformed"
assert not (home_dir / ".elydora").exists()
+161
View File
@@ -0,0 +1,161 @@
from __future__ import annotations
import os
from pathlib import Path
import pytest
from elydora.plugins import _transaction, kimi_installation
from elydora.plugins.kimi_contract import (
build_kimi_hook,
remove_managed_kimi_hooks,
render_kimi_document,
)
from elydora.plugins.kimi_io import read_kimi_documents
from kimi_support import (
AGENT_ID,
assert_no_transaction_files,
prepare_fixture,
write_text,
)
RUNTIME_NAMES = ("guard.py", "config.json", "private.key", "hook.py")
def snapshot_files(paths: list[Path]) -> dict[Path, str]:
return {path: path.read_text(encoding="utf-8") for path in paths}
def assert_snapshot(snapshot: dict[Path, str]) -> None:
for path, source in snapshot.items():
assert path.read_text(encoding="utf-8") == source
def test_install_restores_both_configs_when_final_commit_fails(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
stable = '# stable owner config\ndefault_model = "kimi-code/k3"\n'
legacy = "# legacy owner config\ntelemetry = false\n"
fixture = prepare_fixture(
monkeypatch,
tmp_path,
stable_config=stable,
legacy_config=legacy,
)
real_replace = _transaction.os.replace
failed = False
def fail_legacy_once(source: str, destination: str) -> None:
nonlocal failed
if os.path.abspath(destination) == str(fixture.legacy_path) and not failed:
failed = True
raise OSError("injected legacy config failure")
real_replace(source, destination)
monkeypatch.setattr(_transaction.os, "replace", fail_legacy_once)
with pytest.raises(OSError, match="injected legacy config failure"):
fixture.install()
assert fixture.stable_path.read_text(encoding="utf-8") == stable
assert fixture.legacy_path.read_text(encoding="utf-8") == legacy
for name in RUNTIME_NAMES:
assert not (fixture.agent_dir / name).exists()
assert_no_transaction_files(fixture.home_dir)
def test_install_rejects_concurrent_config_change_before_runtime_staging(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
original = '# owner config\ndefault_model = "kimi-code/k3"\n'
concurrent = "# concurrent owner change\ntelemetry = false\n"
fixture = prepare_fixture(
monkeypatch,
tmp_path,
stable_config=original,
legacy_detected=False,
)
real_rendered_change = kimi_installation.rendered_change
changed = False
def mutate_before_change(rendered: object):
nonlocal changed
if not changed:
changed = True
write_text(fixture.stable_path, concurrent)
return real_rendered_change(rendered) # type: ignore[arg-type]
monkeypatch.setattr(kimi_installation, "rendered_change", mutate_before_change)
with pytest.raises(OSError, match="changed before staging"):
fixture.install()
assert fixture.stable_path.read_text(encoding="utf-8") == concurrent
assert not fixture.agent_dir.exists()
assert_no_transaction_files(fixture.home_dir)
def test_uninstall_restores_first_config_when_second_removal_fails(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.install()
paths = [
*(fixture.agent_dir / name for name in RUNTIME_NAMES),
fixture.stable_path,
fixture.legacy_path,
]
before = snapshot_files(paths)
real_replace = _transaction.os.replace
removal_commits = 0
def fail_second_removal(source: str, destination: str) -> None:
nonlocal removal_commits
if destination.endswith(".rollback"):
removal_commits += 1
if removal_commits == 2:
raise OSError("injected uninstall failure")
real_replace(source, destination)
monkeypatch.setattr(_transaction.os, "replace", fail_second_removal)
with pytest.raises(OSError, match="injected uninstall failure"):
fixture.plugin.uninstall(AGENT_ID)
assert_snapshot(before)
assert_no_transaction_files(fixture.home_dir)
def test_prepared_install_detects_change_before_commit_and_rolls_back_staging(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, legacy_detected=False)
documents = read_kimi_documents()
paths = kimi_installation.preflight_kimi_installation(
fixture.config, documents
)
rendered = [
render_kimi_document(
document,
[
*remove_managed_kimi_hooks(document.hooks),
build_kimi_hook("PreToolUse", paths.guard_path),
build_kimi_hook("PostToolUse", paths.audit_path),
build_kimi_hook("PostToolUseFailure", paths.audit_path),
],
)
for document in documents
]
changes = kimi_installation.prepare_kimi_installation(
fixture.config, paths, rendered
)
write_text(fixture.stable_path, "# changed after preparation\n")
with pytest.raises(OSError, match="changed during installation"):
kimi_installation.commit_kimi_installation(changes)
for name in RUNTIME_NAMES:
assert not (fixture.agent_dir / name).exists()
assert fixture.stable_path.read_text(encoding="utf-8") == "# changed after preparation\n"
assert_no_transaction_files(fixture.home_dir)