fix: align Kimi hooks with official contracts

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 04:56:41 -04:00
1 parent f0e4694da2
commit c60b46b994
17 files changed
+1991 -635

No files matched your search

+3 -2
View File
@@ -24,8 +24,9 @@ This repository owns the public Go SDK, the `cmd/elydora` CLI, local signing beh
- Resolve Codex user hooks through `$CODEX_HOME/hooks.json` with `~/.codex/hooks.json` as the default and canonicalize configured existing directories. Preserve additive TOML, project, plugin, and managed sources. Register exact `PreToolUse` and `PostToolUse` match-all command groups, retain the native payload, propagate freeze and revocation through exit code `2`, keep guard lookup and audit delivery fail-open with observable errors, and commit user hooks plus all four runtime artifacts through one rollback-capable transaction. Require `/hooks` approval for both definition hashes.
- Write Cursor hooks only to `~/.cursor/hooks.json`; keep project and enterprise sources read-only. Preserve user hooks, migrate the prior versionless Elydora contract, audit successful and failed tool calls, emit valid native JSON responses, retain PowerShell exit codes, and commit guard, runtime metadata, private key, audit runtime, and user hooks in one rollback-capable transaction.
- Model stable, legacy, and early-access hook generations as explicit contracts. Keep their activation requirements visible in CLI output and README guidance.
- Select Kimi Code and legacy `kimi-cli` contracts from runtime evidence. An empty `KIMI_CODE_HOME` uses `~/.kimi-code`; create no cross-runtime migration marker.
- Preserve Kimi TOML comments and unrelated formatting through range-based edits, then parse the complete rendered document before writing it.
- Select Kimi Code from an explicit `KIMI_CODE_HOME` or an existing `~/.kimi-code` home, and select legacy `kimi-cli` from an existing `~/.kimi` home. An empty `KIMI_CODE_HOME` uses `~/.kimi-code`; executable lookup is outside the activation contract, and equal stable and legacy config paths collapse to the stable contract.
- Preserve Kimi TOML comments and unrelated formatting through range-based edits, then parse the complete rendered document before writing it. Own only exact three-field hooks for `PreToolUse`, `PostToolUse`, and `PostToolUseFailure`; require one complete triple per runtime identity, use encoded PowerShell commands on Windows, and retain the native snake_case payload.
- Generate Kimi guard, config, private key, and audit runtimes together with every selected TOML document in one rollback-capable transaction. Preflight physical paths and existing runtime identity before creating state; status validates the complete runtime metadata, canonical private key, exact agent identity, and non-empty physical scripts.
- Keep Grok Build writes inside its native global `$GROK_HOME/hooks/*.json` contract. Treat Claude Code and Cursor compatibility files plus project `.grok/hooks` as read-only integration sources.
- Write Auggie hooks only to `~/.augment/settings.json`; keep system and workspace settings read-only. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix because Auggie dispatches supported script paths, and express hook timeouts in milliseconds.
- Write Cline hooks only to `$CLINE_DIR/hooks` with `~/.cline/hooks` as the default; keep Documents and workspace hook roots read-only. Preserve official input byte-for-byte and translate guard exit code `2` into Cline's JSON stdout cancellation control.
+1 -1
View File
@@ -89,7 +89,7 @@ Cline installation writes `PreToolUse.mjs` and `PostToolUse.mjs` to `$CLINE_DIR/
Kiro CLI installation covers both runtime contracts. Kiro CLI v2 uses the generated custom agent through `kiro-cli --agent elydora-audit`. Kiro CLI v3 loads the global standalone hooks when started with `kiro-cli --v3`.
Kimi installation follows detected runtime evidence. Kimi Code uses `$KIMI_CODE_HOME/config.toml` with `~/.kimi-code/config.toml` as the default. Legacy `kimi-cli` uses `~/.kimi/config.toml`; Elydora updates both contracts only when both runtimes are present.
Kimi installation follows home-directory evidence. Kimi Code uses `$KIMI_CODE_HOME/config.toml` with `~/.kimi-code/config.toml` as the default, and legacy `kimi-cli` uses `~/.kimi/config.toml`. Elydora installs exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` hooks, preserves native snake_case payloads, and commits all selected TOML documents with the four runtime artifacts in one rollback-capable transaction. Windows commands use encoded PowerShell so paths containing spaces, apostrophes, percent signs, and environment-like text remain literal.
Grok Build installation writes native global hooks to `$GROK_HOME/hooks/elydora-audit.json` (default `~/.grok/hooks/elydora-audit.json`). Project hooks still follow Grok's `/hooks-trust` workflow; Elydora leaves project, Claude Code, and Cursor compatibility files unchanged.
+2 -2
View File
@@ -2,6 +2,7 @@ package plugins
import (
"encoding/base64"
"encoding/binary"
"os"
"path/filepath"
"runtime"
@@ -29,8 +30,7 @@ func encodeCodexPowerShell(source string) string {
runes := utf16.Encode([]rune(source))
raw := make([]byte, len(runes)*2)
for index, value := range runes {
raw[index*2] = byte(value)
raw[index*2+1] = byte(value >> 8)
binary.LittleEndian.PutUint16(raw[index*2:], value)
}
return base64.StdEncoding.EncodeToString(raw)
}
+2 -52
View File
@@ -1,15 +1,10 @@
package plugins
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"unicode"
)
type codexRuntimePaths struct {
@@ -21,51 +16,6 @@ type codexRuntimePaths struct {
auditPath string
}
func validateCodexPrivateKey(value string) error {
seed, err := base64.RawURLEncoding.DecodeString(value)
if err != nil || len(seed) != 32 || base64.RawURLEncoding.EncodeToString(seed) != value {
return fmt.Errorf("private key must be a canonical 32-byte base64url value")
}
return nil
}
func validateCodexBaseURL(value string) error {
if strings.ContainsRune(value, '\\') {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
for _, character := range value {
if character < 32 || unicode.IsSpace(character) {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
}
parsed, err := url.Parse(value)
if err != nil {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
validScheme := strings.EqualFold(parsed.Scheme, "http") ||
strings.EqualFold(parsed.Scheme, "https")
if !validScheme ||
parsed.Host == "" || parsed.Hostname() == "" || parsed.Opaque != "" {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
if parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
return fmt.Errorf(
"base URL must exclude credentials, query parameters, and fragments",
)
}
port := parsed.Port()
if strings.HasSuffix(parsed.Host, ":") {
return fmt.Errorf("base URL must contain a valid port")
}
if port != "" {
value, convertErr := strconv.Atoi(port)
if convertErr != nil || value < 1 || value > 65535 {
return fmt.Errorf("base URL must contain a valid port")
}
}
return nil
}
func validateCodexInstallConfig(config InstallConfig) error {
for _, field := range []struct{ name, value string }{
{"agent name", config.AgentName},
@@ -83,10 +33,10 @@ func validateCodexInstallConfig(config InstallConfig) error {
if config.AgentName != codexAgentKey {
return fmt.Errorf("codex installation requires agent name %s", codexAgentKey)
}
if err := validateCodexPrivateKey(config.PrivateKey); err != nil {
if err := validateManagedPrivateKey(config.PrivateKey); err != nil {
return err
}
return validateCodexBaseURL(config.BaseURL)
return validateManagedBaseURL(config.BaseURL)
}
func codexAgentPaths(config InstallConfig) (*codexRuntimePaths, error) {
+78 -77
View File
@@ -2,74 +2,87 @@ package plugins
import (
"fmt"
"strings"
)
type kimiMutation struct {
document kimiDocument
raw []byte
remove bool
// KimiPlugin manages Kimi Code and legacy kimi-cli global lifecycle hooks.
type KimiPlugin struct {
rename renameFunc
}
// KimiPlugin manages Kimi Code and legacy kimi-cli global lifecycle hooks.
type KimiPlugin struct{}
// ManagesGuardRuntime reports that Kimi commits its provider guard with the
// audit runtime and every detected user hook document.
func (p *KimiPlugin) ManagesGuardRuntime() bool {
return true
}
func (p *KimiPlugin) Install(config InstallConfig) error {
if config.AgentID == "" {
return fmt.Errorf("agent ID is required")
}
// PreflightInstall validates every detected hook source and runtime identity.
func (p *KimiPlugin) PreflightInstall(config InstallConfig) error {
documents, err := readAllKimiConfigs()
if err != nil {
return err
}
if config.GuardScriptPath == "" {
return fmt.Errorf("guard script path is required")
}
guardExists, err := regularFileExists(config.GuardScriptPath, "Elydora guard runtime")
_, _, err = preflightKimiInstallation(config, documents)
return err
}
func (p *KimiPlugin) Install(config InstallConfig) error {
documents, err := readAllKimiConfigs()
if err != nil {
return err
}
if !guardExists {
return fmt.Errorf("Elydora guard runtime is missing: %s", config.GuardScriptPath)
}
hookPath, err := hookScriptPath(config.AgentID)
paths, nodePath, err := preflightKimiInstallation(config, documents)
if err != nil {
return err
}
if config.HookScript != "" {
hookPath = config.HookScript
}
nodePath, err := resolveNodeRuntime()
guardCommand, err := buildKimiCommand(nodePath, paths.guardPath)
if err != nil {
return err
}
additions := []kimiHook{
buildKimiHook("PreToolUse", buildKimiCommand(nodePath, config.GuardScriptPath)),
buildKimiHook("PostToolUse", buildKimiCommand(nodePath, hookPath)),
auditCommand, err := buildKimiCommand(nodePath, paths.auditPath)
if err != nil {
return err
}
mutations := make([]kimiMutation, 0, len(documents))
for _, document := range documents {
raw, err := renderKimiHooks(document, keptKimiHookIndices(document.hooks, ""), additions)
additions := make([]kimiHook, 0, 3)
for _, item := range []struct{ event, command string }{
{"PreToolUse", guardCommand},
{"PostToolUse", auditCommand},
{"PostToolUseFailure", auditCommand},
} {
hook, err := buildKimiHook(item.event, item.command)
if err != nil {
return err
}
if _, err := parseKimiDocument(document.contract, raw, true); err != nil {
return fmt.Errorf("validate rendered %s: %w", document.contract.label, err)
}
mutations = append(mutations, kimiMutation{document: document, raw: raw})
additions = append(additions, hook)
}
runtimeConfig := config
runtimeConfig.AgentName = kimiAgentKey
if err := GenerateHookScript(hookPath, runtimeConfig); err != nil {
return fmt.Errorf("generate hook script: %w", err)
}
for _, mutation := range mutations {
if err := writeKimiConfig(mutation.document, mutation.raw); err != nil {
rendered := make([]kimiRenderedDocument, 0, len(documents))
for _, document := range documents {
keep, err := keptKimiHookIndices(document.hooks, "")
if err != nil {
return err
}
change, err := renderKimiChange(document, keep, additions)
if err != nil {
return err
}
rendered = append(rendered, change)
}
fmt.Printf("%s: global PreToolUse and PostToolUse hooks installed.\n", kimiRuntimeNames(documents))
changes, err := prepareKimiInstallationChanges(config, paths, rendered)
if err != nil {
return err
}
if err := writeKimiChanges(
changes,
"Install Kimi hooks",
p.rename,
paths.runtimeRoot,
paths.agentDirectory,
); err != nil {
return err
}
fmt.Printf(
"%s: global PreToolUse, PostToolUse, and PostToolUseFailure hooks installed.\n",
kimiRuntimeNames(documents),
)
return nil
}
@@ -78,63 +91,51 @@ func (p *KimiPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
mutations := make([]kimiMutation, 0, len(documents))
rendered := make([]kimiRenderedDocument, 0, len(documents))
for _, document := range documents {
keep := keptKimiHookIndices(document.hooks, agentID)
if len(keep) == len(document.hooks) {
continue
}
raw, err := renderKimiHooks(document, keep, nil)
keep, err := keptKimiHookIndices(document.hooks, agentID)
if err != nil {
return err
}
if _, err := parseKimiDocument(document.contract, raw, true); err != nil {
return fmt.Errorf("validate rendered %s: %w", document.contract.label, err)
}
mutations = append(mutations, kimiMutation{
document: document,
raw: raw,
remove: strings.TrimSpace(string(raw)) == "",
})
}
for _, mutation := range mutations {
if mutation.remove {
if err := removeKimiConfig(mutation.document); err != nil {
return err
}
continue
}
if err := writeKimiConfig(mutation.document, mutation.raw); err != nil {
change, err := renderKimiChange(document, keep, nil)
if err != nil {
return err
}
rendered = append(rendered, change)
}
return nil
changes, err := prepareKimiUninstallChanges(rendered)
if err != nil {
return err
}
return writeKimiChanges(
changes,
"Uninstall Kimi hooks",
p.rename,
"",
"",
)
}
func (p *KimiPlugin) Status() (PluginStatus, error) {
documents, err := readAllKimiConfigs()
entry := SupportedAgents[kimiAgentKey]
status := PluginStatus{
AgentName: kimiAgentKey,
DisplayName: "Kimi Code",
AgentName: kimiAgentKey, DisplayName: entry.Name,
}
if err != nil {
return status, err
}
status.ConfigPath = documents[0].contract.configPath
contracts := make([]kimiRuntimeContract, 0, len(documents))
for _, document := range documents {
contract := kimiRuntimeForDocument(document)
if contract == nil {
continue
}
contracts = append(contracts, *contract)
status.ConfigPath = contract.configPath
contracts, err := kimiRuntimeContracts(documents)
if err != nil {
return status, err
}
if len(contracts) == 0 {
return status, nil
}
status.ConfigPath = contracts[len(contracts)-1].configPath
status.HookConfigured = true
status.HookScriptExists, err = kimiRuntimeScriptsExist(contracts)
status.HookScriptExists, err = kimiRuntimeFilesExist(contracts)
if err != nil {
return status, err
}
+119
View File
@@ -0,0 +1,119 @@
package plugins
import (
"fmt"
"path/filepath"
"runtime"
"strings"
)
type kimiRuntimeReference struct {
agentID string
scriptPath string
}
func sameKimiPath(left, right string) bool {
left = filepath.Clean(left)
right = filepath.Clean(right)
if absolute, err := filepath.Abs(left); err == nil {
left = absolute
}
if absolute, err := filepath.Abs(right); err == nil {
right = absolute
}
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameKimiAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func buildKimiCommand(runtimePath, scriptPath string) (string, error) {
if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) {
return "", fmt.Errorf("kimi hook commands require absolute runtime and script paths")
}
if runtime.GOOS == "windows" {
return codexWindowsCommand(runtimePath, scriptPath), nil
}
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), nil
}
func readKimiLegacyWindowsArgument(command string, start int) (string, int, bool) {
if start >= len(command) {
return "", start, false
}
if command[start] == '"' {
closing := strings.IndexByte(command[start+1:], '"')
if closing < 0 {
return "", start, false
}
closing += start + 1
return command[start+1 : closing], closing + 1, true
}
end := start
for end < len(command) && command[end] != ' ' {
if command[end] == '"' || command[end] == '\t' || command[end] == '\r' || command[end] == '\n' {
return "", start, false
}
end++
}
return command[start:end], end, end > start
}
func parseKimiLegacyWindowsCommand(command string) (string, string, bool) {
runtimePath, next, ok := readKimiLegacyWindowsArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
scriptPath, end, ok := readKimiLegacyWindowsArgument(command, next+1)
if !ok || end != len(command) {
return "", "", false
}
expected := quoteWindowsArgument(runtimePath) + " " + quoteWindowsArgument(scriptPath)
return runtimePath, scriptPath, command == expected
}
func parseKimiCommand(command string) (string, string, bool) {
if runtimePath, scriptPath, ok := parseCodexPOSIXCommand(command); ok {
return runtimePath, scriptPath, true
}
if runtimePath, scriptPath, ok := parseCodexWindowsCommand(command); ok {
return runtimePath, scriptPath, true
}
return parseKimiLegacyWindowsCommand(command)
}
func isKimiNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func kimiRuntimeReferenceForCommand(
command string,
scriptName string,
) (*kimiRuntimeReference, error) {
runtimePath, scriptPath, ok := parseKimiCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) ||
!isKimiNodeExecutable(runtimePath) || filepath.Base(scriptPath) != scriptName {
return nil, nil
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameKimiPath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &kimiRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
}
+118 -50
View File
@@ -2,7 +2,6 @@ package plugins
import (
"fmt"
"path/filepath"
"runtime"
"sort"
"strings"
@@ -38,22 +37,26 @@ var kimiModernEvents = mergeStringSets(kimiSharedEvents, stringSet(
))
type kimiContract struct {
runtimeName string
label string
configPath string
events map[string]struct{}
generation string
runtimeName string
label string
directoryLabel string
configPath string
events map[string]struct{}
}
type kimiHook struct {
event string
matcher *string
command string
timeout *int64
event string
matcher *string
command string
timeout *int64
fieldCount int
}
type kimiRuntimeContract struct {
guard string
audit string
agentID string
guardPath string
auditPath string
configPath string
}
@@ -112,7 +115,7 @@ func validateKimiHook(value any, contract kimiContract, index int) (kimiHook, er
return kimiHook{}, fmt.Errorf("%s hook %d requires a non-empty command", contract.label, index+1)
}
hook := kimiHook{event: event, command: command}
hook := kimiHook{event: event, command: command, fieldCount: len(object)}
if value, exists := object["matcher"]; exists {
matcher, ok := value.(string)
if !ok {
@@ -154,59 +157,124 @@ func kimiHooks(root map[string]any, contract kimiContract) ([]kimiHook, error) {
return hooks, nil
}
func normalizeKimiPath(value string) string {
normalized := filepath.ToSlash(value)
if runtime.GOOS == "windows" {
return strings.ToLower(normalized)
func buildKimiHook(event, command string) (kimiHook, error) {
switch event {
case "PreToolUse", "PostToolUse", "PostToolUseFailure":
default:
return kimiHook{}, fmt.Errorf("unsupported managed Kimi event: %s", event)
}
return normalized
timeout := kimiHookTimeoutSeconds
return kimiHook{
event: event, command: command, timeout: &timeout, fieldCount: 3,
}, nil
}
func kimiCommandEndsWithPath(command, filePath string) bool {
normalized := strings.TrimSpace(normalizeKimiPath(command))
expected := normalizeKimiPath(filePath)
return strings.HasSuffix(normalized, expected) ||
strings.HasSuffix(normalized, expected+`'`) ||
strings.HasSuffix(normalized, expected+`"`)
func managedKimiReference(
hook kimiHook,
event string,
scriptName string,
) (*kimiRuntimeReference, error) {
if hook.fieldCount != 3 || hook.event != event || hook.matcher != nil ||
hook.timeout == nil || *hook.timeout != kimiHookTimeoutSeconds {
return nil, nil
}
return kimiRuntimeReferenceForCommand(hook.command, scriptName)
}
func isManagedKimiHook(hook kimiHook, event, scriptName, agentID string) bool {
if hook.event != event || hook.matcher != nil || hook.timeout == nil || *hook.timeout != kimiHookTimeoutSeconds {
return false
func managedKimiEvent(event string) (string, string, bool) {
switch event {
case "PreToolUse":
return event, kimiGuardScript, true
case "PostToolUse", "PostToolUseFailure":
return event, kimiAuditScript, true
default:
return "", "", false
}
if agentID != "" {
expected := normalizeKimiPath(filepath.Join(".elydora", agentID, scriptName))
return kimiCommandEndsWithPath(hook.command, "/"+expected)
}
normalized := normalizeKimiPath(hook.command)
return strings.Contains(normalized, "/.elydora/") &&
kimiCommandEndsWithPath(hook.command, "/"+normalizeKimiPath(scriptName))
}
func keptKimiHookIndices(hooks []kimiHook, agentID string) []int {
func keptKimiHookIndices(hooks []kimiHook, agentID string) ([]int, error) {
indices := make([]int, 0, len(hooks))
for index, hook := range hooks {
managed := isManagedKimiHook(hook, "PreToolUse", kimiGuardScript, agentID) ||
isManagedKimiHook(hook, "PostToolUse", kimiAuditScript, agentID)
if !managed {
event, scriptName, managedEvent := managedKimiEvent(hook.event)
var reference *kimiRuntimeReference
var err error
if managedEvent {
reference, err = managedKimiReference(hook, event, scriptName)
if err != nil {
return nil, err
}
}
remove := reference != nil &&
(agentID == "" || sameKimiAgentID(reference.agentID, agentID))
if !remove {
indices = append(indices, index)
}
}
return indices
return indices, nil
}
func kimiRuntimeForDocument(document kimiDocument) *kimiRuntimeContract {
var guard, audit string
for _, hook := range document.hooks {
if isManagedKimiHook(hook, "PreToolUse", kimiGuardScript, "") {
guard = hook.command
}
if isManagedKimiHook(hook, "PostToolUse", kimiAuditScript, "") {
audit = hook.command
}
func kimiReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
if guard == "" || audit == "" {
return nil
return agentID
}
func kimiReferencesForEvent(
hooks []kimiHook,
event string,
scriptName string,
) (map[string][]kimiRuntimeReference, error) {
result := map[string][]kimiRuntimeReference{}
for _, hook := range hooks {
reference, err := managedKimiReference(hook, event, scriptName)
if err != nil {
return nil, err
}
if reference == nil {
continue
}
key := kimiReferenceKey(reference.agentID)
result[key] = append(result[key], *reference)
}
return &kimiRuntimeContract{guard: guard, audit: audit, configPath: document.contract.configPath}
return result, nil
}
func kimiRuntimeContracts(documents []kimiDocument) ([]kimiRuntimeContract, error) {
contracts := make([]kimiRuntimeContract, 0)
for _, document := range documents {
guards, err := kimiReferencesForEvent(document.hooks, "PreToolUse", kimiGuardScript)
if err != nil {
return nil, err
}
successes, err := kimiReferencesForEvent(document.hooks, "PostToolUse", kimiAuditScript)
if err != nil {
return nil, err
}
failures, err := kimiReferencesForEvent(
document.hooks, "PostToolUseFailure", kimiAuditScript,
)
if err != nil {
return nil, err
}
keys := make([]string, 0, len(guards))
for key := range guards {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
guard := guards[key]
success := successes[key]
failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 ||
!sameKimiPath(success[0].scriptPath, failure[0].scriptPath) {
continue
}
contracts = append(contracts, kimiRuntimeContract{
agentID: guard[0].agentID, guardPath: guard[0].scriptPath,
auditPath: success[0].scriptPath, configPath: document.contract.configPath,
})
}
}
return contracts, nil
}
+258
View File
@@ -0,0 +1,258 @@
package plugins
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type kimiRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type kimiRenderedDocument struct {
document kimiDocument
next []byte
}
func validateKimiInstallConfig(config InstallConfig) error {
for _, field := range []struct{ name, value string }{
{"agent name", config.AgentName},
{"organization ID", config.OrgID},
{"agent ID", config.AgentID},
{"key ID", config.KID},
{"private key", config.PrivateKey},
{"base URL", config.BaseURL},
{"guard script path", config.GuardScriptPath},
} {
if field.value == "" {
return fmt.Errorf("%s is required", field.name)
}
}
if config.AgentName != kimiAgentKey {
return fmt.Errorf("kimi installation requires agent name %s", kimiAgentKey)
}
if strings.TrimSpace(config.OrgID) == "" {
return fmt.Errorf("organization ID is required")
}
if strings.TrimSpace(config.KID) == "" {
return fmt.Errorf("key ID is required")
}
if config.Token != "" && strings.TrimSpace(config.Token) == "" {
return fmt.Errorf("token must contain a non-whitespace value when provided")
}
if err := validateManagedPrivateKey(config.PrivateKey); err != nil {
return err
}
return validateManagedBaseURL(config.BaseURL)
}
func kimiAgentPaths(config InstallConfig) (*kimiRuntimePaths, error) {
if err := validateKimiInstallConfig(config); err != nil {
return nil, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return nil, err
}
paths := &kimiRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, kimiGuardScript),
auditPath: filepath.Join(agentDirectory, kimiAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameKimiPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"guard runtime must use the managed Elydora agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameKimiPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"audit runtime must use the managed Elydora agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
func preflightKimiInstallation(
config InstallConfig,
documents []kimiDocument,
) (*kimiRuntimePaths, string, error) {
if len(documents) == 0 {
return nil, "", fmt.Errorf("kimi installation requires at least one hook contract")
}
paths, err := kimiAgentPaths(config)
if err != nil {
return nil, "", err
}
if err := validateKimiRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
if err != nil {
return nil, "", err
}
return paths, nodePath, nil
}
func buildKimiRuntimeConfig(config InstallConfig) ([]byte, error) {
value := map[string]any{
"org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID,
"base_url": config.BaseURL, "agent_name": kimiAgentKey,
}
if config.Token != "" {
value["token"] = config.Token
}
encoded, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
}
encoded = append(encoded, '\n')
if len(encoded) > maxRuntimeConfigBytes {
return nil, fmt.Errorf(
"elydora runtime config exceeds %d bytes after JSON encoding",
maxRuntimeConfigBytes,
)
}
return encoded, nil
}
func renderKimiChange(
document kimiDocument,
keep []int,
additions []kimiHook,
) (kimiRenderedDocument, error) {
next, err := renderKimiHooks(document, keep, additions)
if err != nil {
return kimiRenderedDocument{}, err
}
if strings.TrimSpace(string(next)) != "" {
if _, err := parseKimiDocument(document.contract, next, true); err != nil {
return kimiRenderedDocument{}, fmt.Errorf(
"validate rendered %s: %w", document.contract.label, err,
)
}
}
return kimiRenderedDocument{document: document, next: next}, nil
}
func prepareRenderedKimiChange(rendered kimiRenderedDocument) (*fileChange, error) {
document := rendered.document
if document.exists && bytes.Equal(document.raw, rendered.next) {
return nil, nil
}
remove := document.exists && strings.TrimSpace(string(rendered.next)) == ""
return prepareSourceChange(
document.contract.configPath,
document.contract.label,
document.raw,
document.exists,
rendered.next,
0600,
remove,
)
}
func prepareKimiInstallationChanges(
config InstallConfig,
paths *kimiRuntimePaths,
rendered []kimiRenderedDocument,
) ([]*fileChange, error) {
runtimeConfig, err := buildKimiRuntimeConfig(config)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(kimiAgentKey, config.AgentID, "", false)), 0700,
},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600},
{
paths.auditPath, "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
kimiAgentKey, config.AgentID, "", false, true,
)), 0700,
},
}
changes := make([]*fileChange, 0, len(items)+len(rendered))
for _, item := range items {
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
if err != nil {
return nil, err
}
changes = append(changes, change)
}
for _, document := range rendered {
change, err := prepareRenderedKimiChange(document)
if err != nil {
return nil, err
}
changes = append(changes, change)
}
return changes, nil
}
func prepareKimiUninstallChanges(
rendered []kimiRenderedDocument,
) ([]*fileChange, error) {
changes := make([]*fileChange, 0, len(rendered))
for _, document := range rendered {
change, err := prepareRenderedKimiChange(document)
if err != nil {
return nil, err
}
changes = append(changes, change)
}
return changes, nil
}
func writeKimiChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot string,
agentDirectory string,
) error {
hasChanges := false
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
return nil
}
if agentDirectory != "" {
if err := EnsurePrivateDirectory(runtimeRoot); err != nil {
return err
}
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
return err
}
}
return writeChanges(changes, label, rename)
}
+51 -73
View File
@@ -4,9 +4,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
)
@@ -23,9 +21,8 @@ func isMissingKimiPath(err error) bool {
return os.IsNotExist(err) || errors.Is(err, syscall.ENOTDIR)
}
func kimiPathExists(path, label string) (bool, error) {
// #nosec G703 -- KIMI_CODE_HOME is an explicit local-user configuration path.
_, err := os.Stat(path)
func kimiEntryExists(path, label string) (bool, error) {
_, err := os.Lstat(path)
if err == nil {
return true, nil
}
@@ -35,15 +32,21 @@ func kimiPathExists(path, label string) (bool, error) {
return false, fmt.Errorf("inspect %s at %s: %w", label, path, err)
}
func legacyKimiCLIOnPath() (bool, error) {
_, err := exec.LookPath("kimi-cli")
if err == nil {
return true, nil
func stableKimiContract(configPath string) kimiContract {
return kimiContract{
generation: "stable", runtimeName: "Kimi Code",
label: "Kimi Code hooks config", directoryLabel: "Kimi Code home directory",
configPath: configPath, events: kimiModernEvents,
}
if errors.Is(err, exec.ErrNotFound) {
return false, nil
}
func legacyKimiContract(configPath string) kimiContract {
return kimiContract{
generation: "legacy", runtimeName: "kimi-cli",
label: "kimi-cli legacy hooks config",
directoryLabel: "kimi-cli legacy home directory",
configPath: configPath, events: kimiSharedEvents,
}
return false, fmt.Errorf("inspect kimi-cli executable on PATH: %w", err)
}
func resolveKimiContracts() ([]kimiContract, error) {
@@ -51,58 +54,59 @@ func resolveKimiContracts() ([]kimiContract, error) {
if err != nil {
return nil, err
}
explicitHome := os.Getenv("KIMI_CODE_HOME")
kimiHome := explicitHome
if kimiHome == "" {
kimiHome = filepath.Join(home, ".kimi-code")
configuredHome := os.Getenv("KIMI_CODE_HOME")
stableHome := filepath.Join(home, ".kimi-code")
explicitHome := configuredHome != ""
if explicitHome {
stableHome, err = filepath.Abs(configuredHome)
if err != nil {
return nil, fmt.Errorf("resolve KIMI_CODE_HOME at %s: %w", configuredHome, err)
}
}
modern := kimiContract{
runtimeName: "Kimi Code",
label: "Kimi Code hooks config",
configPath: filepath.Join(kimiHome, "config.toml"),
events: kimiModernEvents,
legacyHome := filepath.Join(home, ".kimi")
stable := stableKimiContract(filepath.Join(stableHome, "config.toml"))
legacy := legacyKimiContract(filepath.Join(legacyHome, "config.toml"))
if sameKimiPath(stable.configPath, legacy.configPath) {
return []kimiContract{stable}, nil
}
legacy := kimiContract{
runtimeName: "kimi-cli",
label: "kimi-cli legacy hooks config",
configPath: filepath.Join(home, ".kimi", "config.toml"),
events: kimiSharedEvents,
}
modernDetected := explicitHome != ""
if !modernDetected {
modernDetected, err = kimiPathExists(kimiHome, "Kimi Code home")
stableDetected := explicitHome
if !stableDetected {
stableDetected, err = kimiEntryExists(stableHome, "Kimi Code home")
if err != nil {
return nil, err
}
}
legacyDetected, err := kimiPathExists(legacy.configPath, legacy.label)
legacyDetected, err := kimiEntryExists(legacyHome, "kimi-cli legacy home")
if err != nil {
return nil, err
}
if !legacyDetected {
legacyDetected, err = legacyKimiCLIOnPath()
if err != nil {
return nil, err
}
}
if legacyDetected && !modernDetected {
if legacyDetected && !stableDetected {
return []kimiContract{legacy}, nil
}
if legacyDetected {
return []kimiContract{modern, legacy}, nil
return []kimiContract{stable, legacy}, nil
}
return []kimiContract{modern}, nil
return []kimiContract{stable}, nil
}
func readKimiConfig(contract kimiContract) (kimiDocument, error) {
raw, err := os.ReadFile(contract.configPath)
if err != nil {
if os.IsNotExist(err) {
return parseKimiDocument(contract, nil, false)
}
return kimiDocument{}, fmt.Errorf("read %s at %s: %w", contract.label, contract.configPath, err)
directory := filepath.Dir(contract.configPath)
if _, err := managedPhysicalDirectoryExists(directory, contract.directoryLabel); err != nil {
return kimiDocument{}, err
}
return parseKimiDocument(contract, raw, true)
snapshot, err := readManagedFile(
contract.configPath,
contract.label,
maxManagedSourceBytes,
)
if err != nil {
return kimiDocument{}, err
}
if snapshot == nil {
return parseKimiDocument(contract, nil, false)
}
return parseKimiDocument(contract, snapshot.contents, true)
}
func readAllKimiConfigs() ([]kimiDocument, error) {
@@ -121,32 +125,6 @@ func readAllKimiConfigs() ([]kimiDocument, error) {
return documents, nil
}
func writeKimiConfig(document kimiDocument, raw []byte) error {
if err := writeHookFileAtomic(document.contract.configPath, raw, 0600); err != nil {
return fmt.Errorf("write %s at %s: %w", document.contract.label, document.contract.configPath, err)
}
return nil
}
func removeKimiConfig(document kimiDocument) error {
if err := removeHookFile(document.contract.configPath, document.contract.label); err != nil {
return err
}
return nil
}
func buildKimiCommand(runtimePath, scriptPath string) string {
if runtime.GOOS == "windows" {
return quoteWindowsArgument(runtimePath) + " " + quoteWindowsArgument(scriptPath)
}
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath)
}
func buildKimiHook(event, command string) kimiHook {
timeout := kimiHookTimeoutSeconds
return kimiHook{event: event, command: command, timeout: &timeout}
}
func kimiRuntimeNames(documents []kimiDocument) string {
names := make([]string, 0, len(documents))
for _, document := range documents {
+181
View File
@@ -0,0 +1,181 @@
package plugins
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func installKimiRuntimeFixture(
t *testing.T,
status string,
) (*kimiFixture, *codexTestAPI, *httptest.Server) {
t.Helper()
api := &codexTestAPI{status: status}
server := httptest.NewServer(api)
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
fixture.config.BaseURL = server.URL
fixture.config.Token = "ely_test_token"
if err := fixture.plugin.Install(fixture.config); err != nil {
server.Close()
t.Fatalf("install Kimi hooks: %v", err)
}
return fixture, api, server
}
func TestKimiGeneratedRuntimePreservesSuccessAndFailurePayloads(t *testing.T) {
fixture, api, server := installKimiRuntimeFixture(t, "active")
defer server.Close()
hooks := readKimiTestHooks(t, fixture.modernPath)
guard := managedKimiTestHook(t, hooks, "PreToolUse")["command"].(string)
success := managedKimiTestHook(t, hooks, "PostToolUse")["command"].(string)
failure := managedKimiTestHook(t, hooks, "PostToolUseFailure")["command"].(string)
if exitCode, stderr := runKimiCommand(
t, guard, fixture, kimiOfficialPayload("PreToolUse"),
); exitCode != 0 || stderr != "" {
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
}
successPayload := kimiOfficialPayload("PostToolUse")
failurePayload := kimiOfficialPayload("PostToolUseFailure")
for _, item := range []struct {
command string
payload map[string]any
}{{success, successPayload}, {failure, failurePayload}} {
if exitCode, stderr := runKimiCommand(
t, item.command, fixture, item.payload,
); exitCode != 0 || stderr != "" {
t.Fatalf("audit exit = %d, stderr = %q", exitCode, stderr)
}
}
getAuth, postAuth, operations := api.snapshot()
if getAuth != "Bearer ely_test_token" || postAuth != "Bearer ely_test_token" {
t.Fatalf("authorization headers = %q, %q", getAuth, postAuth)
}
if len(operations) != 2 ||
!reflect.DeepEqual(operations[0]["payload"], successPayload) ||
!reflect.DeepEqual(operations[1]["payload"], failurePayload) {
t.Fatalf("Kimi native operations = %#v", operations)
}
if operations[0]["prev_chain_hash"] != "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ||
operations[1]["prev_chain_hash"] != operations[0]["chain_hash"] {
t.Fatalf("Kimi operation chain = %#v", operations)
}
}
func TestKimiGuardPropagatesOfficialBlockExitCode(t *testing.T) {
for _, status := range []string{"frozen", "revoked"} {
t.Run(status, func(t *testing.T) {
fixture, _, server := installKimiRuntimeFixture(t, status)
defer server.Close()
command := managedKimiTestHook(
t, readKimiTestHooks(t, fixture.modernPath), "PreToolUse",
)["command"].(string)
exitCode, stderr := runKimiCommand(
t, command, fixture, kimiOfficialPayload("PreToolUse"),
)
if exitCode != 2 || !strings.Contains(stderr, "Tool execution blocked") {
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
}
})
}
}
func TestKimiFailOpenGuardReportsInputConfigStatusAndAPIFailures(t *testing.T) {
fixture, api, server := installKimiRuntimeFixture(t, "active")
command := managedKimiTestHook(
t, readKimiTestHooks(t, fixture.modernPath), "PreToolUse",
)["command"].(string)
runtimeConfig, err := os.ReadFile(fixture.runtimeConfig)
if err != nil {
t.Fatalf("read runtime config: %v", err)
}
malformedExit, malformedError := runKimiRawCommand(
t, command, fixture, []byte("{ malformed"),
)
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
t.Fatalf("corrupt runtime config: %v", err)
}
configExit, configError := runKimiCommand(
t, command, fixture, kimiOfficialPayload("PreToolUse"),
)
if err := os.WriteFile(fixture.runtimeConfig, runtimeConfig, 0600); err != nil {
t.Fatalf("restore runtime config: %v", err)
}
_ = os.Remove(filepath.Join(fixture.agentDir, "status-cache.json"))
api.mu.Lock()
api.status = "unknown"
api.mu.Unlock()
statusExit, statusError := runKimiCommand(
t, command, fixture, kimiOfficialPayload("PreToolUse"),
)
_ = os.Remove(filepath.Join(fixture.agentDir, "status-cache.json"))
server.Close()
apiExit, apiError := runKimiCommand(
t, command, fixture, kimiOfficialPayload("PreToolUse"),
)
for _, result := range []struct {
exit int
err string
want string
}{
{malformedExit, malformedError, "invalid JSON"},
{configExit, configError, "Failed to read agent config"},
{statusExit, statusError, "invalid agent status"},
{apiExit, apiError, "Failed to resolve agent status"},
} {
if result.exit != 0 || !strings.Contains(result.err, result.want) {
t.Fatalf("fail-open result = %d, %q; want %q", result.exit, result.err, result.want)
}
}
}
func TestKimiAuditRecordsMalformedInputAndAPIFailures(t *testing.T) {
fixture, api, server := installKimiRuntimeFixture(t, "active")
defer server.Close()
command := managedKimiTestHook(
t, readKimiTestHooks(t, fixture.modernPath), "PostToolUseFailure",
)["command"].(string)
malformedExit, malformedError := runKimiRawCommand(
t, command, fixture, []byte("{ malformed"),
)
api.mu.Lock()
api.postStatus = http.StatusServiceUnavailable
api.mu.Unlock()
upstreamExit, upstreamError := runKimiCommand(
t, command, fixture, kimiOfficialPayload("PostToolUseFailure"),
)
log, err := os.ReadFile(filepath.Join(fixture.agentDir, "error.log"))
if malformedExit != 0 || malformedError != "" || upstreamExit != 0 || upstreamError != "" ||
err != nil || !strings.Contains(string(log), "invalid JSON") ||
!strings.Contains(string(log), "HTTP 503") {
t.Fatalf(
"audit failures = (%d, %q), (%d, %q), log=%q, err=%v",
malformedExit, malformedError, upstreamExit, upstreamError, log, err,
)
}
}
func TestKimiFailurePayloadIncludesOfficialErrorShape(t *testing.T) {
payload := kimiOfficialPayload("PostToolUseFailure")
encoded, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(encoded, &decoded); err != nil {
t.Fatalf("decode payload: %v", err)
}
if decoded["hook_event_name"] != "PostToolUseFailure" || decoded["error"] == nil {
t.Fatalf("failure payload = %#v", decoded)
}
}
+282
View File
@@ -0,0 +1,282 @@
package plugins
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)
func TestKimiRejectsLinkedConfigBeforeCreatingRuntime(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
target := filepath.Join(t.TempDir(), "config-target.toml")
source := []byte("# protected target\ntelemetry = false\n")
if err := os.WriteFile(target, source, 0600); err != nil {
t.Fatalf("write config target: %v", err)
}
if err := os.MkdirAll(filepath.Dir(fixture.modernPath), 0700); err != nil {
t.Fatalf("create Kimi config directory: %v", err)
}
kimiSymlinkOrSkip(t, target, fixture.modernPath)
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "physical file") {
t.Fatalf("linked config error = %v", err)
}
actual, readErr := os.ReadFile(target)
if readErr != nil || string(actual) != string(source) {
t.Fatalf("linked config target changed: %q, %v", actual, readErr)
}
assertNoKimiRuntimeWrites(t, fixture)
}
func TestKimiRejectsLinkedConfigAndRuntimeDirectories(t *testing.T) {
for _, kind := range []string{"config", "runtime"} {
t.Run(kind, func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
target := filepath.Join(t.TempDir(), kind+"-target")
if err := os.MkdirAll(target, 0700); err != nil {
t.Fatalf("create link target: %v", err)
}
link := fixture.kimiHome
if kind == "runtime" {
link = filepath.Dir(fixture.agentDir)
}
if err := os.MkdirAll(filepath.Dir(link), 0700); err != nil {
t.Fatalf("create link parent: %v", err)
}
kimiSymlinkOrSkip(t, target, link)
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "physical directory") {
t.Fatalf("linked %s directory error = %v", kind, err)
}
entries, readErr := os.ReadDir(target)
if readErr != nil || len(entries) != 0 {
t.Fatalf("linked directory target changed: %#v, %v", entries, readErr)
}
})
}
}
func TestKimiRejectsLinkedRuntimeFilesBeforeConfigWrites(t *testing.T) {
for _, name := range []string{"config", "key", "guard", "audit"} {
t.Run(name, func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
path := map[string]string{
"config": fixture.runtimeConfig, "key": fixture.privateKey,
"guard": fixture.guardPath, "audit": fixture.hookPath,
}[name]
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
t.Fatalf("create runtime directory: %v", err)
}
target := filepath.Join(t.TempDir(), name+"-target")
source := []byte("external\n")
if err := os.WriteFile(target, source, 0600); err != nil {
t.Fatalf("write runtime target: %v", err)
}
kimiSymlinkOrSkip(t, target, path)
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "physical file") {
t.Fatalf("linked %s error = %v", name, err)
}
actual, readErr := os.ReadFile(target)
if readErr != nil || string(actual) != string(source) {
t.Fatalf("linked runtime target changed: %q, %v", actual, readErr)
}
if _, statErr := os.Stat(fixture.modernPath); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("Kimi config exists after rejection: %v", statErr)
}
})
}
}
func TestKimiRejectsOrphanAndMismatchedRuntimeIdentity(t *testing.T) {
for _, testCase := range []struct{ name, config string }{
{"orphan", ""},
{"mismatch", `{"agent_name":"kimi","agent_id":"another-agent"}`},
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
if err := os.MkdirAll(fixture.agentDir, 0700); err != nil {
t.Fatalf("create agent directory: %v", err)
}
if testCase.config == "" {
if err := os.WriteFile(fixture.guardPath, []byte("orphan\n"), 0700); err != nil {
t.Fatalf("write orphan guard: %v", err)
}
} else if err := os.WriteFile(
fixture.runtimeConfig, []byte(testCase.config), 0600,
); err != nil {
t.Fatalf("write mismatched config: %v", err)
}
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "runtime") {
t.Fatalf("runtime identity error = %v", err)
}
if _, err := os.Stat(fixture.modernPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Kimi config exists after rejection: %v", err)
}
})
}
}
func TestKimiValidatesIdentityCredentialsAndOriginBeforeWrites(t *testing.T) {
for _, testCase := range []struct {
name string
mutate func(*InstallConfig)
}{
{"agent name", func(config *InstallConfig) { config.AgentName = "codex" }},
{"organization", func(config *InstallConfig) { config.OrgID = " " }},
{"agent ID", func(config *InstallConfig) { config.AgentID = "../escape" }},
{"key ID", func(config *InstallConfig) { config.KID = "\t" }},
{"private key", func(config *InstallConfig) { config.PrivateKey = "invalid" }},
{"token", func(config *InstallConfig) { config.Token = " " }},
{"scheme", func(config *InstallConfig) { config.BaseURL = "file:///tmp/elydora" }},
{"credentials", func(config *InstallConfig) { config.BaseURL = "https://user:secret@api.elydora.com" }},
{"query", func(config *InstallConfig) { config.BaseURL = "https://api.elydora.com?tenant=one" }},
{"backslash", func(config *InstallConfig) { config.BaseURL = `https://api.elydora.com\evil` }},
{"space", func(config *InstallConfig) { config.BaseURL = "https://api.elydora.com/invalid path" }},
{"port", func(config *InstallConfig) { config.BaseURL = "https://api.elydora.com:invalid" }},
{"guard path", func(config *InstallConfig) { config.GuardScriptPath = "unmanaged.js" }},
{"audit path", func(config *InstallConfig) { config.HookScript = "unmanaged.js" }},
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
testCase.mutate(&fixture.config)
err := fixture.plugin.Install(fixture.config)
if err == nil {
t.Fatal("install accepted invalid configuration")
}
if _, err := os.Stat(fixture.modernPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Kimi config exists after validation failure: %v", err)
}
assertNoKimiRuntimeWrites(t, fixture)
})
}
}
func TestKimiStatusRejectsLinkedRuntimeFiles(t *testing.T) {
for _, name := range []string{"config", "key", "guard", "audit"} {
t.Run(name, func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
path := map[string]string{
"config": fixture.runtimeConfig, "key": fixture.privateKey,
"guard": fixture.guardPath, "audit": fixture.hookPath,
}[name]
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read runtime file: %v", err)
}
target := filepath.Join(t.TempDir(), name+"-target")
if err := os.WriteFile(target, content, 0600); err != nil {
t.Fatalf("write linked target: %v", err)
}
if err := os.Remove(path); err != nil {
t.Fatalf("remove runtime file: %v", err)
}
kimiSymlinkOrSkip(t, target, path)
status, err := fixture.plugin.Status()
if err == nil || status.Installed {
t.Fatalf("linked runtime status = %#v, %v", status, err)
}
})
}
}
func TestKimiStatusValidatesRuntimeMetadataAndKey(t *testing.T) {
for _, testCase := range []struct {
name string
mutate func(map[string]any)
want string
}{
{"extra", func(value map[string]any) { value["extra"] = true }, "unsupported field"},
{"organization", func(value map[string]any) { value["org_id"] = "" }, "org_id is invalid"},
{"identity", func(value map[string]any) { value["agent_id"] = "other" }, "identity"},
{"base URL", func(value map[string]any) { value["base_url"] = "file:///tmp" }, "base URL"},
{"token", func(value map[string]any) { value["token"] = "" }, "token is invalid"},
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
raw, err := os.ReadFile(fixture.runtimeConfig)
if err != nil {
t.Fatalf("read runtime config: %v", err)
}
var value map[string]any
if err := json.Unmarshal(raw, &value); err != nil {
t.Fatalf("decode runtime config: %v", err)
}
testCase.mutate(value)
encoded, err := json.Marshal(value)
if err != nil {
t.Fatalf("encode runtime config: %v", err)
}
if err := os.WriteFile(fixture.runtimeConfig, encoded, 0600); err != nil {
t.Fatalf("write runtime config: %v", err)
}
status, err := fixture.plugin.Status()
if err == nil || status.Installed || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("invalid runtime status = %#v, %v", status, err)
}
})
}
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
if err := os.WriteFile(fixture.privateKey, []byte("invalid"), 0600); err != nil {
t.Fatalf("write invalid private key: %v", err)
}
if status, err := fixture.plugin.Status(); err == nil || status.Installed ||
!strings.Contains(err.Error(), "canonical 32-byte") {
t.Fatalf("invalid key status = %#v, %v", status, err)
}
}
func TestKimiOwnershipRequiresExactSupportedFields(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
legacy := legacyKimiCommand(t, fixture.guardPath)
source := "[[hooks]]\nevent = \"PreToolUse\"\ncommand = " + strconv.Quote(legacy) +
"\ntimeout = 10\nmatcher = \"Bash\"\n"
writeOptionalKimiConfig(t, fixture.modernPath, kimiString(source))
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
hooks := readKimiTestHooks(t, fixture.modernPath)
if len(hooks) != 4 || hooks[0]["matcher"] != "Bash" {
t.Fatalf("user-owned matcher hook changed: %#v", hooks)
}
requireKimiManagedTriple(t, hooks)
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("uninstall Kimi hooks: %v", err)
}
hooks = readKimiTestHooks(t, fixture.modernPath)
if len(hooks) != 1 || hooks[0]["matcher"] != "Bash" {
t.Fatalf("user-owned matcher hook removed: %#v", hooks)
}
}
func TestKimiStatusSurfacesMalformedRuntimeMetadata(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
t.Fatalf("corrupt runtime config: %v", err)
}
if _, err := fixture.plugin.Status(); err == nil ||
!strings.Contains(err.Error(), "parse Elydora runtime config") {
t.Fatalf("status error = %v", err)
}
}
+177 -53
View File
@@ -2,70 +2,194 @@ package plugins
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
func kimiRuntimeScriptsExist(contracts []kimiRuntimeContract) (bool, error) {
home, err := kimiHomeDirectory()
func readKimiRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireKimiRuntimeString(config map[string]any, field, configPath string) (string, error) {
value, ok := config[field].(string)
if !ok || strings.TrimSpace(value) == "" {
return "", fmt.Errorf(
"elydora runtime config %s is invalid: %s", field, configPath,
)
}
return value, nil
}
func validateKimiRuntimeConfig(
config map[string]any,
expectedAgentID string,
configPath string,
) error {
supported := stringSet("org_id", "agent_id", "kid", "base_url", "token", "agent_name")
fields := make([]string, 0, len(config))
for field := range config {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
if _, ok := supported[field]; !ok {
return fmt.Errorf(
`elydora runtime config has unsupported field %q: %s`, field, configPath,
)
}
}
if _, err := requireKimiRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireKimiRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireKimiRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameKimiAgentID(agentID, expectedAgentID) || config["agent_name"] != kimiAgentKey {
return fmt.Errorf("elydora runtime identity does not match Kimi hooks: %s", configPath)
}
if _, exists := config["token"]; exists {
if _, err := requireKimiRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireKimiRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf("elydora runtime config base URL is invalid at %s: %w", configPath, err)
}
return nil
}
func validateKimiRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory")
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory, "Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readKimiRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, kimiGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, kimiAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != kimiAgentKey ||
!sameKimiAgentID(configuredID, agentID) {
return fmt.Errorf(
"elydora runtime config identity does not match Kimi agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func kimiRuntimeContractExists(contract kimiRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
root := filepath.Join(home, ".elydora")
entries, err := os.ReadDir(root)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("read Elydora runtime directory at %s: %w", root, err)
agentDirectory := filepath.Dir(contract.guardPath)
if !sameKimiPath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameKimiPath(contract.auditPath, filepath.Join(agentDirectory, kimiAuditScript)) {
return false, nil
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
agentDir := filepath.Join(root, entry.Name())
guardPath := filepath.Join(agentDir, kimiGuardScript)
auditPath := filepath.Join(agentDir, kimiAuditScript)
if !kimiContractsReferenceRuntime(contracts, guardPath, auditPath) {
continue
}
configPath := filepath.Join(agentDir, "config.json")
config, exists, err := readHookJSONObject(configPath, "Elydora runtime config")
rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory")
if err != nil || !rootExists {
return false, err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory, "Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return false, err
}
configPath := filepath.Join(agentDirectory, "config.json")
keyPath := filepath.Join(agentDirectory, "private.key")
config, configExists, err := readKimiRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
audit, err := readManagedFile(contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateKimiRuntimeConfig(config, contract.agentID, configPath); err != nil {
return false, err
}
if err := validateManagedPrivateKey(string(key.contents)); err != nil {
return false, fmt.Errorf("elydora private key at %s: %w", keyPath, err)
}
return len(guard.contents) > 0 && len(audit.contents) > 0, nil
}
func kimiRuntimeFilesExist(contracts []kimiRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := kimiRuntimeContractExists(contract)
if err != nil {
return false, err
}
if !exists {
continue
if exists {
return true, nil
}
agentName, ok := config["agent_name"].(string)
if !ok {
return false, fmt.Errorf(
`Elydora runtime config at %s field "agent_name" must be a string`,
configPath,
)
}
if agentName != kimiAgentKey {
continue
}
guardExists, err := regularFileExists(guardPath, "Elydora guard runtime")
if err != nil {
return false, err
}
auditExists, err := regularFileExists(auditPath, "Elydora audit runtime")
if err != nil {
return false, err
}
return guardExists && auditExists, nil
}
return false, nil
}
func kimiContractsReferenceRuntime(contracts []kimiRuntimeContract, guardPath, auditPath string) bool {
for _, contract := range contracts {
if kimiCommandEndsWithPath(contract.guard, guardPath) &&
kimiCommandEndsWithPath(contract.audit, auditPath) {
return true
}
}
return false
}
+281 -261
View File
@@ -2,71 +2,90 @@ package plugins
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
)
func TestKimiRegistryUsesModernGlobalContract(t *testing.T) {
func TestKimiRegistryUsesStableGlobalContract(t *testing.T) {
entry := SupportedAgents[kimiAgentKey]
if entry.Name != "Kimi Code" || entry.ConfigDir != "~/.kimi-code" || entry.ConfigFile != "config.toml" {
if entry.Name != "Kimi Code" || entry.ConfigDir != "~/.kimi-code" ||
entry.ConfigFile != "config.toml" {
t.Fatalf("Kimi registry entry = %#v", entry)
}
if _, ok := NewPlugin(kimiAgentKey).(*KimiPlugin); !ok {
plugin, ok := NewPlugin(kimiAgentKey).(*KimiPlugin)
if !ok || !plugin.ManagesGuardRuntime() {
t.Fatalf("Kimi registry plugin = %T", NewPlugin(kimiAgentKey))
}
}
func TestKimiInstallPreservesBothConfigsAndIsIdempotent(t *testing.T) {
modern := "# modern user config\ndefault_model = \"kimi-code/k3\"\n\n" +
"[[hooks]]\nevent = \"SessionStart\"\ncommand = \"existing-modern\"\n" +
"timeout = 30 # keep modern hook\n"
modern := "# stable user config\ndefault_model = \"kimi-code/k3\"\n\n" +
"[[hooks]]\nevent = \"SessionStart\"\ncommand = \"existing-stable\"\n" +
"timeout = 30 # keep stable hook\n"
legacy := "# legacy user config\ntelemetry = false\n\n" +
"[[hooks]]\nevent = \"SessionEnd\"\ncommand = \"existing-legacy\"\n"
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(modern),
legacyConfig: kimiString(legacy),
modernConfig: kimiString(modern), legacyConfig: kimiString(legacy),
})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
first := map[string][]byte{}
for _, path := range []string{fixture.modernPath, fixture.legacyPath} {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read first Kimi source: %v", err)
}
first[path] = raw
}
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("repeat Kimi install: %v", err)
}
for _, contract := range []struct{ path, comment, command string }{
{fixture.modernPath, "# modern user config", "existing-modern"},
{fixture.modernPath, "# stable user config", "existing-stable"},
{fixture.legacyPath, "# legacy user config", "existing-legacy"},
} {
raw, err := os.ReadFile(contract.path)
if err != nil {
t.Fatalf("read Kimi config: %v", err)
}
if !strings.Contains(string(raw), contract.comment) || !strings.Contains(string(raw), contract.command) {
if !reflect.DeepEqual(raw, first[contract.path]) ||
!strings.Contains(string(raw), contract.comment) ||
!strings.Contains(string(raw), contract.command) {
t.Fatalf("user config changed: %s", raw)
}
hooks := readKimiTestHooks(t, contract.path)
if len(hooks) != 3 {
t.Fatalf("hooks = %#v, want three entries", hooks)
if len(hooks) != 4 {
t.Fatalf("hooks = %#v, want four entries", hooks)
}
requireStrictKimiHook(t, findKimiTestHook(t, hooks, "PreToolUse", kimiGuardScript))
requireStrictKimiHook(t, findKimiTestHook(t, hooks, "PostToolUse", kimiAuditScript))
requireKimiManagedTriple(t, hooks)
}
defaultModern := filepath.Join(fixture.homeDir, ".kimi-code", "config.toml")
if _, err := os.Stat(defaultModern); !os.IsNotExist(err) {
t.Fatalf("false default migration target exists: %v", err)
defaultStable := filepath.Join(fixture.homeDir, ".kimi-code", "config.toml")
if _, err := os.Stat(defaultStable); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("unselected default config exists: %v", err)
}
for _, path := range []string{
fixture.runtimeConfig, fixture.privateKey, fixture.guardPath, fixture.hookPath,
} {
if info, err := os.Stat(path); err != nil || !info.Mode().IsRegular() {
t.Fatalf("managed runtime missing at %s: %v", path, err)
}
}
}
func TestKimiInstallPreservesInlineHookArrayStyle(t *testing.T) {
modern := "# inline user hook\nhooks = [\n # keep leading\n { event = \"SessionStart\", " +
"command = \"existing-inline\" }, # keep gap\n] # keep array\n"
modern := "# inline user hook\nhooks = [\n # keep leading\n" +
" { event = \"SessionStart\", command = \"existing-inline\" }, # keep gap\n" +
"] # keep array\n"
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(modern),
withoutLegacyCLI: true,
modernConfig: kimiString(modern), withoutLegacyEvidence: true,
})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install inline Kimi hooks: %v", err)
@@ -79,150 +98,181 @@ func TestKimiInstallPreservesInlineHookArrayStyle(t *testing.T) {
t.Fatalf("read inline Kimi config: %v", err)
}
for _, marker := range []string{
"# inline user hook",
"# keep leading",
"# keep gap",
"# keep array",
"hooks = [",
"# inline user hook", "# keep leading", "# keep gap", "# keep array", "hooks = [",
} {
if !strings.Contains(string(raw), marker) {
t.Fatalf("inline marker %q missing from %s", marker, raw)
}
}
if len(readKimiTestHooks(t, fixture.modernPath)) != 3 {
t.Fatalf("inline Kimi hooks are not idempotent: %s", raw)
hooks := readKimiTestHooks(t, fixture.modernPath)
if len(hooks) != 4 {
t.Fatalf("inline hooks = %#v", hooks)
}
requireKimiManagedTriple(t, hooks)
}
func TestKimiModernDefaultAvoidsLegacyMigrationMarker(t *testing.T) {
func TestKimiUninstallPreservesUserOwnedEmptyInlineArray(t *testing.T) {
source := "# user container\nhooks = [] # keep empty array\n"
fixture := prepareKimiFixture(t, kimiFixtureOptions{
useDefaultHome: true,
withoutLegacyCLI: true,
modernConfig: kimiString(source), withoutLegacyEvidence: true,
})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install modern Kimi hooks: %v", err)
}
if len(readKimiTestHooks(t, fixture.modernPath)) != 2 {
t.Fatalf("modern Kimi config has unexpected hooks")
}
if _, err := os.Stat(fixture.legacyPath); !os.IsNotExist(err) {
t.Fatalf("false legacy migration target exists: %v", err)
}
status, err := fixture.plugin.Status()
if err != nil || !status.Installed {
t.Fatalf("empty KIMI_CODE_HOME status = %#v, %v", status, err)
}
}
func TestKimiLegacyInstallAvoidsPrematureModernTarget(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{useDefaultHome: true})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install legacy Kimi hooks: %v", err)
}
if len(readKimiTestHooks(t, fixture.legacyPath)) != 2 {
t.Fatalf("legacy Kimi config has unexpected hooks")
}
if _, err := os.Stat(fixture.modernPath); !os.IsNotExist(err) {
t.Fatalf("premature modern migration target exists: %v", err)
}
if err := os.Remove(fixture.legacyCLIPath); err != nil {
t.Fatalf("remove legacy CLI marker: %v", err)
}
status, err := fixture.plugin.Status()
if err != nil || !status.Installed {
t.Fatalf("legacy status after PATH change = %#v, %v", status, err)
}
}
func TestKimiCommandsBlockAndForwardOfficialPayload(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
capturePath := filepath.Join(t.TempDir(), "captured-event.json")
encodedPath, err := json.Marshal(capturePath)
if err != nil {
t.Fatalf("marshal capture path: %v", err)
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("uninstall Kimi hooks: %v", err)
}
source := "const fs = require('node:fs'); const chunks = []; " +
"process.stdin.on('data', chunk => chunks.push(chunk)); " +
"process.stdin.on('end', () => fs.writeFileSync(" + string(encodedPath) + ", Buffer.concat(chunks)));\n"
if err := os.WriteFile(fixture.hookPath, []byte(source), 0700); err != nil {
t.Fatalf("write capture runtime: %v", err)
raw, err := os.ReadFile(fixture.modernPath)
if err != nil {
t.Fatalf("read empty inline config: %v", err)
}
if !strings.Contains(string(raw), "# user container") ||
!strings.Contains(string(raw), "# keep empty array") ||
len(readKimiTestHooks(t, fixture.modernPath)) != 0 {
t.Fatalf("empty inline array changed: %s", raw)
}
}
func TestKimiUsesDefaultStableAndLegacyEvidence(t *testing.T) {
t.Run("stable", func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{
useDefaultHome: true, withoutLegacyEvidence: true,
})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install stable Kimi hooks: %v", err)
}
requireKimiManagedTriple(t, readKimiTestHooks(t, fixture.modernPath))
if _, err := os.Stat(fixture.legacyPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("legacy config exists: %v", err)
}
})
t.Run("legacy", func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{
useDefaultHome: true, withoutModernEvidence: true,
})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install legacy Kimi hooks: %v", err)
}
requireKimiManagedTriple(t, readKimiTestHooks(t, fixture.legacyPath))
if _, err := os.Stat(fixture.modernPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("stable config exists: %v", err)
}
status, err := fixture.plugin.Status()
if err != nil || !status.Installed {
t.Fatalf("legacy Kimi status = %#v, %v", status, err)
}
})
}
func TestKimiParsesEverySelectedConfigBeforeRuntimeWrites(t *testing.T) {
modern := "# untouched stable\ndefault_model = \"kimi-code/k3\"\n"
legacy := "[malformed"
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(modern), legacyConfig: kimiString(legacy),
})
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "parse kimi-cli legacy hooks config") {
t.Fatalf("install error = %v", err)
}
for path, want := range map[string]string{
fixture.modernPath: modern, fixture.legacyPath: legacy,
} {
raw, readErr := os.ReadFile(path)
if readErr != nil || string(raw) != want {
t.Fatalf("config %s changed: %q, %v", path, raw, readErr)
}
}
assertNoKimiRuntimeWrites(t, fixture)
}
func TestKimiRejectsFieldsAndEventsOutsideEachContract(t *testing.T) {
for _, testCase := range []struct {
name, modern, legacy, want string
}{
{
"unsupported-field",
"[[hooks]]\nevent=\"PreToolUse\"\ncommand=\"x\"\ncwd=\"/tmp\"\n",
"", `unsupported field "cwd"`,
},
{
"legacy-stable-event", "",
"[[hooks]]\nevent=\"Interrupt\"\ncommand=\"x\"\n",
"unsupported event",
},
{
"bad-timeout",
"[[hooks]]\nevent=\"PreToolUse\"\ncommand=\"x\"\ntimeout=601\n",
"", "integer from 1 to 600",
},
{
"hooks-object", "hooks = { event=\"PreToolUse\", command=\"x\" }\n",
"", `field "hooks" must be an array`,
},
} {
t.Run(testCase.name, func(t *testing.T) {
options := kimiFixtureOptions{}
if testCase.modern != "" {
options.modernConfig = kimiString(testCase.modern)
}
if testCase.legacy != "" {
options.legacyConfig = kimiString(testCase.legacy)
}
fixture := prepareKimiFixture(t, options)
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("install error = %v, want %q", err, testCase.want)
}
assertNoKimiRuntimeWrites(t, fixture)
})
}
}
func TestKimiMigratesExactLegacyCommandsAndPreservesLookalikes(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
guard := legacyKimiCommand(t, fixture.guardPath)
audit := legacyKimiCommand(t, fixture.hookPath)
lookalike := guard + " --inspect"
source := "[[hooks]]\nevent = \"PreToolUse\"\ncommand = " + strconv.Quote(guard) +
"\ntimeout = 10\n\n[[hooks]]\nevent = \"PostToolUse\"\ncommand = " +
strconv.Quote(audit) + "\ntimeout = 10\n\n[[hooks]]\nevent = \"PreToolUse\"\ncommand = " +
strconv.Quote(lookalike) + "\ntimeout = 10\n"
writeOptionalKimiConfig(t, fixture.modernPath, kimiString(source))
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("migrate legacy Kimi commands: %v", err)
}
hooks := readKimiTestHooks(t, fixture.modernPath)
payload := map[string]any{
"hook_event_name": "PreToolUse",
"session_id": "session-1",
"cwd": fixture.homeDir,
"tool_name": "Bash",
"tool_input": map[string]any{"command": "echo test"},
"tool_call_id": "call-1",
if len(hooks) != 4 {
t.Fatalf("migrated hooks = %#v", hooks)
}
guard := findKimiTestHook(t, hooks, "PreToolUse", kimiGuardScript)
exitCode, stderr := runKimiCommand(t, guard["command"].(string), fixture.homeDir, payload)
if exitCode != 2 || !strings.Contains(stderr, "Agent is frozen by Elydora") {
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
requireKimiManagedTriple(t, hooks)
if !containsKimiCommand(hooks, lookalike) {
t.Fatalf("lookalike command removed: %#v", hooks)
}
payload["hook_event_name"] = "PostToolUse"
payload["tool_output"] = map[string]any{"output": "test"}
audit := findKimiTestHook(t, hooks, "PostToolUse", kimiAuditScript)
exitCode, stderr = runKimiCommand(t, audit["command"].(string), fixture.homeDir, payload)
if exitCode != 0 {
t.Fatalf("audit exit = %d, stderr = %q", exitCode, stderr)
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("uninstall migrated Kimi hooks: %v", err)
}
capturedRaw, err := os.ReadFile(capturePath)
if err != nil {
t.Fatalf("read captured event: %v", err)
}
var captured map[string]any
if err := json.Unmarshal(capturedRaw, &captured); err != nil {
t.Fatalf("decode captured event: %v", err)
}
if !reflect.DeepEqual(captured, payload) {
t.Fatalf("captured event = %#v, want %#v", captured, payload)
hooks = readKimiTestHooks(t, fixture.modernPath)
if len(hooks) != 1 || hooks[0]["command"] != lookalike {
t.Fatalf("remaining hooks = %#v", hooks)
}
}
func TestKimiStatusAcceptsEitherContractAndRequiresBothScripts(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
if err := os.Remove(fixture.modernPath); err != nil {
t.Fatalf("remove modern config: %v", err)
}
requireKimiInstalledStatus(t, fixture.plugin, fixture.legacyPath)
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("reinstall Kimi hooks: %v", err)
}
if err := os.Remove(fixture.legacyPath); err != nil {
t.Fatalf("remove legacy config: %v", err)
}
requireKimiInstalledStatus(t, fixture.plugin, fixture.modernPath)
if err := os.Remove(fixture.guardPath); err != nil {
t.Fatalf("remove Kimi guard: %v", err)
}
status, err := fixture.plugin.Status()
if err != nil || status.Installed || !status.HookConfigured || status.HookScriptExists {
t.Fatalf("degraded Kimi status = %#v, %v", status, err)
func containsKimiCommand(hooks []map[string]any, command string) bool {
for _, hook := range hooks {
if hook["command"] == command {
return true
}
}
return false
}
func requireKimiInstalledStatus(t *testing.T, plugin *KimiPlugin, configPath string) {
t.Helper()
status, err := plugin.Status()
if err != nil || !status.Installed || status.ConfigPath != configPath {
t.Fatalf("Kimi status = %#v, %v; want installed at %s", status, err, configPath)
}
}
func TestKimiUninstallPreservesUserHooks(t *testing.T) {
func TestKimiUninstallPreservesUserConfigsAndRemovesManagedConfigs(t *testing.T) {
userHook := "# user hook\n[[hooks]]\nevent = \"SessionStart\"\n" +
"command = \"existing-command\"\ntimeout = 30 # keep timeout\n"
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(userHook),
legacyConfig: kimiString(userHook),
modernConfig: kimiString(userHook), legacyConfig: kimiString(userHook),
})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
@@ -232,159 +282,129 @@ func TestKimiUninstallPreservesUserHooks(t *testing.T) {
}
for _, path := range []string{fixture.modernPath, fixture.legacyPath} {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read user config after uninstall: %v", err)
}
if !strings.Contains(string(raw), "# user hook") || !strings.Contains(string(raw), "# keep timeout") {
t.Fatalf("user comments changed: %s", raw)
if err != nil || !strings.Contains(string(raw), "# keep timeout") {
t.Fatalf("user config changed: %s, %v", raw, err)
}
hooks := readKimiTestHooks(t, path)
if len(hooks) != 1 || hooks[0]["command"] != "existing-command" {
t.Fatalf("user hook changed: %#v", hooks)
}
}
}
func TestKimiOwnershipRequiresExactAgentAndScript(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
backupPath := filepath.Join(fixture.homeDir, ".elydora", kimiTestAgentID, "hook.js.backup")
otherAgentPath := filepath.Join(fixture.homeDir, ".elydora", "agent-10", kimiAuditScript)
userHooks := "[[hooks]]\nevent = \"PostToolUse\"\ncommand = " +
strconv.Quote("node "+backupPath) + "\n\n[[hooks]]\nevent = \"PostToolUse\"\ncommand = " +
strconv.Quote("node "+otherAgentPath) + "\n"
if err := os.MkdirAll(filepath.Dir(fixture.modernPath), 0755); err != nil {
t.Fatalf("create modern Kimi directory: %v", err)
managed := prepareKimiFixture(t, kimiFixtureOptions{})
if err := managed.plugin.Install(managed.config); err != nil {
t.Fatalf("install managed Kimi configs: %v", err)
}
if err := os.WriteFile(fixture.modernPath, []byte(userHooks), 0600); err != nil {
t.Fatalf("write ownership fixture: %v", err)
if err := managed.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("remove managed Kimi configs: %v", err)
}
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("uninstall Kimi hooks: %v", err)
}
hooks := readKimiTestHooks(t, fixture.modernPath)
if len(hooks) != 2 {
t.Fatalf("ownership filter removed user hooks: %#v", hooks)
}
commands := []any{hooks[0]["command"], hooks[1]["command"]}
if !reflect.DeepEqual(commands, []any{"node " + backupPath, "node " + otherAgentPath}) {
t.Fatalf("remaining commands = %#v", commands)
}
}
func TestKimiUninstallRemovesOwnedConfigs(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("uninstall Kimi hooks: %v", err)
}
for _, path := range []string{fixture.modernPath, fixture.legacyPath} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("owned Kimi config remains at %s: %v", path, err)
for _, path := range []string{managed.modernPath, managed.legacyPath} {
if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("managed config remains at %s: %v", path, err)
}
}
}
func TestKimiParsesEveryConfigBeforeRuntimeWrites(t *testing.T) {
modern := "# untouched modern\ndefault_model = \"kimi-code/k3\"\n"
legacy := "[malformed"
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(modern),
legacyConfig: kimiString(legacy),
})
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "parse kimi-cli legacy hooks config") {
t.Fatalf("install error = %v", err)
func TestKimiStatusRequiresCompleteTripleAndEveryRuntimeFile(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
for path, want := range map[string]string{fixture.modernPath: modern, fixture.legacyPath: legacy} {
raw, readErr := os.ReadFile(path)
if readErr != nil || string(raw) != want {
t.Fatalf("config %s changed: %q, %v", path, raw, readErr)
status, err := fixture.plugin.Status()
if err != nil || !status.Installed || !status.HookConfigured {
t.Fatalf("Kimi status = %#v, %v", status, err)
}
raw, err := os.ReadFile(fixture.modernPath)
if err != nil {
t.Fatalf("read Kimi config: %v", err)
}
document, err := parseKimiDocument(
stableKimiContract(fixture.modernPath), raw, true,
)
if err != nil {
t.Fatalf("parse Kimi config: %v", err)
}
keep := make([]int, 0, len(document.hooks)-1)
for index, hook := range document.hooks {
if hook.event != "PostToolUseFailure" {
keep = append(keep, index)
}
}
requireNoKimiRuntimeWrites(t, fixture)
}
withoutFailure, err := renderKimiHooks(document, keep, nil)
if err != nil {
t.Fatalf("render incomplete Kimi hooks: %v", err)
}
if err := os.WriteFile(fixture.modernPath, withoutFailure, 0600); err != nil {
t.Fatalf("write incomplete Kimi hooks: %v", err)
}
status, err = fixture.plugin.Status()
if err != nil || status.Installed || status.HookConfigured {
t.Fatalf("incomplete Kimi status = %#v, %v", status, err)
}
func TestKimiRejectsInvalidHookContractsBeforeWrites(t *testing.T) {
for _, testCase := range []struct{ name, raw, want string }{
{"unsupported-field", "[[hooks]]\nevent=\"PreToolUse\"\ncommand=\"x\"\ncwd=\"/tmp\"\n", `unsupported field "cwd"`},
{"unsupported-event", "[[hooks]]\nevent=\"Interrupt\"\ncommand=\"x\"\n", "unsupported event"},
{"bad-matcher", "[[hooks]]\nevent=\"PreToolUse\"\ncommand=\"x\"\nmatcher=1\n", "matcher must be a string"},
{"bad-timeout", "[[hooks]]\nevent=\"PreToolUse\"\ncommand=\"x\"\ntimeout=601\n", "integer from 1 to 600"},
{"hooks-object", "hooks = { event=\"PreToolUse\", command=\"x\" }\n", `field "hooks" must be an array`},
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("repair Kimi hooks: %v", err)
}
for _, path := range []string{
fixture.guardPath, fixture.hookPath, fixture.runtimeConfig, fixture.privateKey,
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{
useDefaultHome: true,
legacyConfig: kimiString(testCase.raw),
})
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("install error = %v, want %q", err, testCase.want)
t.Run(filepath.Base(path), func(t *testing.T) {
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read runtime file: %v", err)
}
raw, readErr := os.ReadFile(fixture.legacyPath)
if readErr != nil || string(raw) != testCase.raw {
t.Fatalf("invalid config changed: %q, %v", raw, readErr)
if err := os.Remove(path); err != nil {
t.Fatalf("remove runtime file: %v", err)
}
status, err := fixture.plugin.Status()
if err != nil || status.Installed || !status.HookConfigured {
t.Fatalf("degraded Kimi status = %#v, %v", status, err)
}
if err := os.WriteFile(path, content, 0600); err != nil {
t.Fatalf("restore runtime file: %v", err)
}
requireNoKimiRuntimeWrites(t, fixture)
})
}
}
func TestKimiRejectsMissingGuardBeforeWrites(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutGuard: true})
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "guard runtime is missing") {
t.Fatalf("install error = %v", err)
}
for _, path := range []string{fixture.modernPath, fixture.legacyPath} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("config write occurred at %s: %v", path, err)
}
}
requireNoKimiRuntimeWrites(t, fixture)
}
func requireNoKimiRuntimeWrites(t *testing.T, fixture *kimiFixture) {
t.Helper()
for _, path := range []string{fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("runtime write occurred at %s: %v", path, err)
}
}
}
func TestKimiStatusSurfacesMalformedRuntimeMetadata(t *testing.T) {
func TestKimiInstallationLeavesNoTransactionArtifacts(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
t.Fatalf("corrupt runtime config: %v", err)
}
if _, err := fixture.plugin.Status(); err == nil || !strings.Contains(err.Error(), "parse Elydora runtime config") {
t.Fatalf("status error = %v", err)
}
}
func TestKimiAtomicWritesLeaveNoTemporaryFiles(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
for _, directory := range []string{filepath.Dir(fixture.modernPath), filepath.Dir(fixture.legacyPath)} {
entries, err := os.ReadDir(directory)
if err != nil {
t.Fatalf("read config directory: %v", err)
}
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".tmp") {
t.Fatalf("temporary Kimi config remains at %s", filepath.Join(directory, entry.Name()))
assertNoKimiTransactionArtifacts(t, fixture.homeDir)
if runtime.GOOS != "windows" {
for _, path := range []string{
fixture.modernPath, fixture.legacyPath,
fixture.runtimeConfig, fixture.privateKey,
} {
info, err := os.Stat(path)
if err != nil {
t.Fatalf("inspect mode for %s: %v", path, err)
}
if info.Mode().Perm() != 0600 {
t.Fatalf("mode for %s = %v", path, info.Mode().Perm())
}
}
}
}
func TestKimiRuntimeConfigOmitsEmptyOptionalToken(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
fixture.config.Token = ""
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks without token: %v", err)
}
raw, err := os.ReadFile(fixture.runtimeConfig)
if err != nil {
t.Fatalf("read runtime config: %v", err)
}
var config map[string]any
if err := json.Unmarshal(raw, &config); err != nil {
t.Fatalf("decode runtime config: %v", err)
}
if _, exists := config["token"]; exists {
t.Fatalf("empty optional token persisted: %#v", config)
}
}
+156 -63
View File
@@ -15,30 +15,33 @@ import (
"github.com/pelletier/go-toml/v2"
)
const kimiTestAgentID = "agent-1"
const (
kimiTestAgentID = "agent-1"
kimiPrivateKey = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc"
)
type kimiFixtureOptions struct {
modernConfig *string
legacyConfig *string
useDefaultHome bool
withoutLegacyCLI bool
withoutGuard bool
modernConfig *string
legacyConfig *string
useDefaultHome bool
withoutModernEvidence bool
withoutLegacyEvidence bool
}
type kimiFixture struct {
plugin *KimiPlugin
config InstallConfig
homeDir string
binDir string
projectDir string
kimiHome string
modernPath string
legacyHome string
legacyPath string
agentDir string
guardPath string
hookPath string
runtimeConfig string
privateKey string
legacyCLIPath string
}
func kimiString(value string) *string {
@@ -47,45 +50,38 @@ func kimiString(value string) *string {
func prepareKimiFixture(t *testing.T, options kimiFixtureOptions) *kimiFixture {
t.Helper()
homeDir := filepath.Join(t.TempDir(), "home with spaces")
binDir := filepath.Join(homeDir, "bin")
kimiHome := filepath.Join(homeDir, "custom kimi code")
root := t.TempDir()
homeDir := filepath.Join(root, "home with spaces and 'quote %ELYDORA_HOOK_PATH%")
projectDir := filepath.Join(root, "project with spaces")
kimiHome := filepath.Join(homeDir, "custom kimi-code")
if options.useDefaultHome {
kimiHome = filepath.Join(homeDir, ".kimi-code")
}
modernPath := filepath.Join(kimiHome, "config.toml")
legacyPath := filepath.Join(homeDir, ".kimi", "config.toml")
legacyHome := filepath.Join(homeDir, ".kimi")
legacyPath := filepath.Join(legacyHome, "config.toml")
agentDir := filepath.Join(homeDir, ".elydora", kimiTestAgentID)
guardPath := filepath.Join(agentDir, kimiGuardScript)
hookPath := filepath.Join(agentDir, kimiAuditScript)
if err := os.MkdirAll(agentDir, 0755); err != nil {
t.Fatalf("create agent directory: %v", err)
if err := os.MkdirAll(projectDir, 0700); err != nil {
t.Fatalf("create project directory: %v", err)
}
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatalf("create binary directory: %v", err)
if options.useDefaultHome && !options.withoutModernEvidence {
if err := os.MkdirAll(kimiHome, 0700); err != nil {
t.Fatalf("create Kimi Code home: %v", err)
}
}
if !options.withoutGuard {
source := "process.stdin.resume(); process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n"
if err := os.WriteFile(guardPath, []byte(source), 0700); err != nil {
t.Fatalf("write guard runtime: %v", err)
if !options.withoutLegacyEvidence {
if err := os.MkdirAll(legacyHome, 0700); err != nil {
t.Fatalf("create legacy Kimi home: %v", err)
}
}
writeOptionalKimiConfig(t, modernPath, options.modernConfig)
writeOptionalKimiConfig(t, legacyPath, options.legacyConfig)
legacyCLIPath := filepath.Join(binDir, "kimi-cli")
if runtime.GOOS == "windows" {
legacyCLIPath += ".cmd"
}
if !options.withoutLegacyCLI {
if err := os.WriteFile(legacyCLIPath, []byte(""), 0700); err != nil {
t.Fatalf("write legacy CLI marker: %v", err)
}
}
originalPath := os.Getenv("PATH")
t.Setenv("PATH", binDir+string(os.PathListSeparator)+originalPath)
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
t.Setenv("ELYDORA_HOOK_PATH", filepath.Join(root, "expanded-command-fragment"))
if options.useDefaultHome {
t.Setenv("KIMI_CODE_HOME", "")
} else {
@@ -93,27 +89,17 @@ func prepareKimiFixture(t *testing.T, options kimiFixtureOptions) *kimiFixture {
}
return &kimiFixture{
plugin: &KimiPlugin{},
homeDir: homeDir,
binDir: binDir,
kimiHome: kimiHome,
config: InstallConfig{
AgentName: kimiAgentKey,
OrgID: "org-1",
AgentID: kimiTestAgentID,
PrivateKey: "test-key",
KID: "kid-1",
BaseURL: "https://api.elydora.test",
GuardScriptPath: guardPath,
},
modernPath: modernPath,
legacyPath: legacyPath,
agentDir: agentDir,
guardPath: guardPath,
hookPath: hookPath,
plugin: &KimiPlugin{}, homeDir: homeDir, projectDir: projectDir,
kimiHome: kimiHome, modernPath: modernPath,
legacyHome: legacyHome, legacyPath: legacyPath,
agentDir: agentDir, guardPath: guardPath, hookPath: hookPath,
runtimeConfig: filepath.Join(agentDir, "config.json"),
privateKey: filepath.Join(agentDir, "private.key"),
legacyCLIPath: legacyCLIPath,
config: InstallConfig{
AgentName: kimiAgentKey, OrgID: "org-1", AgentID: kimiTestAgentID,
PrivateKey: kimiPrivateKey, KID: "kid-1", Token: "token-1",
BaseURL: "https://api.elydora.test", GuardScriptPath: guardPath,
},
}
}
@@ -122,7 +108,7 @@ func writeOptionalKimiConfig(t *testing.T, path string, raw *string) {
if raw == nil {
return
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
t.Fatalf("create Kimi config directory: %v", err)
}
if err := os.WriteFile(path, []byte(*raw), 0600); err != nil {
@@ -155,36 +141,87 @@ func readKimiTestHooks(t *testing.T, path string) []map[string]any {
return hooks
}
func findKimiTestHook(t *testing.T, hooks []map[string]any, event, script string) map[string]any {
func managedKimiTestHook(t *testing.T, hooks []map[string]any, event string) map[string]any {
t.Helper()
for _, hook := range hooks {
command, _ := hook["command"].(string)
if hook["event"] == event && strings.Contains(command, script) {
for index := len(hooks) - 1; index >= 0; index-- {
hook := hooks[index]
if hook["event"] == event && hook["timeout"] == int64(10) {
return hook
}
}
t.Fatalf("%s hook for %s not found", event, script)
t.Fatalf("managed %s hook not found", event)
return nil
}
func requireStrictKimiHook(t *testing.T, hook map[string]any) {
func requireStrictKimiHook(t *testing.T, hook map[string]any, event string) {
t.Helper()
want := map[string]any{
"event": hook["event"],
"command": hook["command"],
"timeout": int64(10),
"event": event, "command": hook["command"], "timeout": int64(10),
}
if !reflect.DeepEqual(hook, want) {
t.Fatalf("hook = %#v, want strict contract %#v", hook, want)
}
command, ok := hook["command"].(string)
if !ok || command == "" {
t.Fatalf("Kimi command = %#v", hook["command"])
}
if runtime.GOOS == "windows" && !strings.Contains(command, " -EncodedCommand ") {
t.Fatalf("Windows Kimi command is not encoded PowerShell: %q", command)
}
}
func runKimiCommand(t *testing.T, command, homeDir string, payload map[string]any) (int, string) {
func requireKimiManagedTriple(t *testing.T, hooks []map[string]any) {
t.Helper()
for _, event := range []string{"PreToolUse", "PostToolUse", "PostToolUseFailure"} {
requireStrictKimiHook(t, managedKimiTestHook(t, hooks, event), event)
}
if managedKimiTestHook(t, hooks, "PostToolUse")["command"] !=
managedKimiTestHook(t, hooks, "PostToolUseFailure")["command"] {
t.Fatal("Kimi success and failure audit commands differ")
}
}
func kimiOfficialPayload(event string) map[string]any {
payload := map[string]any{
"hook_event_name": event,
"session_id": "session-1",
"cwd": "C:/project",
"tool_name": "Bash",
"tool_input": map[string]any{"command": "echo test"},
"tool_call_id": "call-1",
}
switch event {
case "PostToolUse":
payload["tool_output"] = map[string]any{"output": "test", "success": true}
case "PostToolUseFailure":
payload["error"] = map[string]any{
"name": "ToolError", "message": "command failed", "code": "tool.failed",
}
}
return payload
}
func runKimiCommand(
t *testing.T,
command string,
fixture *kimiFixture,
payload map[string]any,
) (int, string) {
t.Helper()
encoded, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal Kimi payload: %v", err)
}
return runKimiRawCommand(t, command, fixture, encoded)
}
func runKimiRawCommand(
t *testing.T,
command string,
fixture *kimiFixture,
encoded []byte,
) (int, string) {
t.Helper()
var process *exec.Cmd
if runtime.GOOS == "windows" {
commandFile := filepath.Join(t.TempDir(), "run-kimi-hook.cmd")
@@ -195,11 +232,17 @@ func runKimiCommand(t *testing.T, command, homeDir string, payload map[string]an
} else {
process = exec.Command("sh", "-c", command)
}
process.Env = append(os.Environ(), "HOME="+homeDir, "USERPROFILE="+homeDir)
process.Dir = fixture.projectDir
process.Env = append(
os.Environ(),
"HOME="+fixture.homeDir,
"USERPROFILE="+fixture.homeDir,
"ELYDORA_HOOK_PATH=injected-command-fragment",
)
process.Stdin = bytes.NewReader(encoded)
var stderr bytes.Buffer
process.Stderr = &stderr
err = process.Run()
err := process.Run()
if err == nil {
return 0, stderr.String()
}
@@ -210,3 +253,53 @@ func runKimiCommand(t *testing.T, command, homeDir string, payload map[string]an
t.Fatalf("run Kimi hook command: %v", err)
return -1, stderr.String()
}
func legacyKimiCommand(t *testing.T, scriptPath string) string {
t.Helper()
nodePath, err := resolveNodeRuntime()
if err != nil {
t.Fatalf("resolve Node.js runtime: %v", err)
}
if runtime.GOOS == "windows" {
return quoteWindowsArgument(nodePath) + " " + quoteWindowsArgument(scriptPath)
}
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
}
func assertNoKimiRuntimeWrites(t *testing.T, fixture *kimiFixture) {
t.Helper()
for _, path := range []string{
fixture.runtimeConfig, fixture.privateKey, fixture.guardPath, fixture.hookPath,
} {
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("runtime file exists at %s: %v", path, err)
}
}
}
func assertNoKimiTransactionArtifacts(t *testing.T, root string) {
t.Helper()
if _, err := os.Lstat(root); errors.Is(err, os.ErrNotExist) {
return
}
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(entry.Name(), ".tmp") ||
strings.HasSuffix(entry.Name(), ".rollback") {
t.Errorf("transaction artifact remains at %s", path)
}
return nil
})
if err != nil {
t.Fatalf("walk Kimi fixture: %v", err)
}
}
func kimiSymlinkOrSkip(t *testing.T, target, link string) {
t.Helper()
if err := os.Symlink(target, link); err != nil {
t.Skipf("symbolic links unavailable: %v", err)
}
}
+5 -1
View File
@@ -24,6 +24,7 @@ type kimiTomlLayout struct {
type kimiDocument struct {
contract kimiContract
exists bool
raw []byte
hooks []kimiHook
layout kimiTomlLayout
@@ -31,7 +32,9 @@ type kimiDocument struct {
func parseKimiDocument(contract kimiContract, raw []byte, exists bool) (kimiDocument, error) {
if !exists {
return kimiDocument{contract: contract, raw: []byte{}, hooks: []kimiHook{}}, nil
return kimiDocument{
contract: contract, exists: false, raw: []byte{}, hooks: []kimiHook{},
}, nil
}
root := map[string]any{}
if err := toml.Unmarshal(raw, &root); err != nil {
@@ -76,6 +79,7 @@ func parseKimiDocument(contract kimiContract, raw []byte, exists bool) (kimiDocu
}
return kimiDocument{
contract: contract,
exists: true,
raw: append([]byte(nil), raw...),
hooks: hooks,
layout: layout,
@@ -0,0 +1,223 @@
package plugins
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func snapshotKimiFiles(t *testing.T, paths ...string) map[string][]byte {
t.Helper()
result := make(map[string][]byte, len(paths))
for _, path := range paths {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read snapshot %s: %v", path, err)
}
result[path] = raw
}
return result
}
func requireKimiSnapshot(t *testing.T, snapshot map[string][]byte) {
t.Helper()
for path, want := range snapshot {
actual, err := os.ReadFile(path)
if err != nil || string(actual) != string(want) {
t.Fatalf("snapshot changed at %s: %q, %v", path, actual, err)
}
}
}
func TestKimiInstallRollsBackRuntimeAndBothConfigs(t *testing.T) {
stable := []byte("# stable owner config\ndefault_model = \"kimi-code/k3\"\n")
legacy := []byte("# legacy owner config\ntelemetry = false\n")
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(string(stable)), legacyConfig: kimiString(string(legacy)),
})
fixture.plugin.rename = func(source, destination string) error {
if sameKimiPath(destination, fixture.legacyPath) {
return errors.New("injected legacy config failure")
}
return os.Rename(source, destination)
}
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "injected legacy config failure") {
t.Fatalf("install error = %v", err)
}
for path, want := range map[string][]byte{
fixture.modernPath: stable, fixture.legacyPath: legacy,
} {
actual, readErr := os.ReadFile(path)
if readErr != nil || string(actual) != string(want) {
t.Fatalf("config changed at %s: %q, %v", path, actual, readErr)
}
}
assertNoKimiRuntimeWrites(t, fixture)
assertNoKimiTransactionArtifacts(t, fixture.homeDir)
}
func TestKimiInstallPreservesConcurrentConfigMutation(t *testing.T) {
original := "# original owner config\ndefault_model = \"kimi-code/k3\"\n"
concurrent := []byte("# concurrent owner change\ntelemetry = false\n")
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(original), withoutLegacyEvidence: true,
})
mutated := false
fixture.plugin.rename = func(source, destination string) error {
if !mutated && strings.HasSuffix(source, ".tmp") {
mutated = true
if err := os.WriteFile(fixture.modernPath, concurrent, 0600); err != nil {
return err
}
}
return os.Rename(source, destination)
}
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("concurrent install error = %v", err)
}
actual, readErr := os.ReadFile(fixture.modernPath)
if readErr != nil || string(actual) != string(concurrent) {
t.Fatalf("concurrent config changed: %q, %v", actual, readErr)
}
assertNoKimiRuntimeWrites(t, fixture)
assertNoKimiTransactionArtifacts(t, fixture.homeDir)
}
func TestKimiInstallDetectsConcurrentConfigIdentityReplacement(t *testing.T) {
original := []byte("# original owner config\n")
fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(string(original)), withoutLegacyEvidence: true,
})
mutated := false
fixture.plugin.rename = func(source, destination string) error {
if !mutated && strings.HasSuffix(source, ".tmp") {
mutated = true
external := fixture.modernPath + ".external"
if err := os.Rename(fixture.modernPath, external); err != nil {
return err
}
if err := os.WriteFile(fixture.modernPath, original, 0600); err != nil {
return err
}
}
return os.Rename(source, destination)
}
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("concurrent identity error = %v", err)
}
actual, readErr := os.ReadFile(fixture.modernPath)
if readErr != nil || string(actual) != string(original) {
t.Fatalf("replacement config changed: %q, %v", actual, readErr)
}
assertNoKimiRuntimeWrites(t, fixture)
assertNoKimiTransactionArtifacts(t, fixture.homeDir)
}
func TestKimiUninstallRollsBackBothConfigRemovals(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err)
}
paths := []string{
fixture.guardPath, fixture.runtimeConfig, fixture.privateKey, fixture.hookPath,
fixture.modernPath, fixture.legacyPath,
}
before := snapshotKimiFiles(t, paths...)
removals := 0
fixture.plugin.rename = func(source, destination string) error {
if strings.HasSuffix(destination, ".rollback") {
removals++
if removals == 2 {
return errors.New("injected uninstall failure")
}
}
return os.Rename(source, destination)
}
err := fixture.plugin.Uninstall(kimiTestAgentID)
if err == nil || !strings.Contains(err.Error(), "injected uninstall failure") {
t.Fatalf("uninstall error = %v", err)
}
requireKimiSnapshot(t, before)
assertNoKimiTransactionArtifacts(t, fixture.homeDir)
}
func TestPreparedKimiInstallDetectsSourceChangeBeforeCommit(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
documents, err := readAllKimiConfigs()
if err != nil {
t.Fatalf("read Kimi configs: %v", err)
}
paths, nodePath, err := preflightKimiInstallation(fixture.config, documents)
if err != nil {
t.Fatalf("preflight Kimi installation: %v", err)
}
guardCommand, err := buildKimiCommand(nodePath, paths.guardPath)
if err != nil {
t.Fatalf("build guard command: %v", err)
}
auditCommand, err := buildKimiCommand(nodePath, paths.auditPath)
if err != nil {
t.Fatalf("build audit command: %v", err)
}
additions := make([]kimiHook, 0, 3)
for _, item := range []struct{ event, command string }{
{"PreToolUse", guardCommand},
{"PostToolUse", auditCommand},
{"PostToolUseFailure", auditCommand},
} {
hook, hookErr := buildKimiHook(item.event, item.command)
if hookErr != nil {
t.Fatalf("build Kimi hook: %v", hookErr)
}
additions = append(additions, hook)
}
rendered := make([]kimiRenderedDocument, 0, len(documents))
for _, document := range documents {
keep, keepErr := keptKimiHookIndices(document.hooks, "")
if keepErr != nil {
t.Fatalf("select Kimi hooks: %v", keepErr)
}
change, renderErr := renderKimiChange(document, keep, additions)
if renderErr != nil {
t.Fatalf("render Kimi hooks: %v", renderErr)
}
rendered = append(rendered, change)
}
changes, err := prepareKimiInstallationChanges(fixture.config, paths, rendered)
if err != nil {
t.Fatalf("prepare Kimi installation: %v", err)
}
if err := os.MkdirAll(filepath.Dir(fixture.modernPath), 0700); err != nil {
t.Fatalf("create Kimi home: %v", err)
}
concurrent := []byte("# changed after preparation\n")
if err := os.WriteFile(fixture.modernPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent source: %v", err)
}
err = writeKimiChanges(
changes,
"Install Kimi hooks",
nil,
paths.runtimeRoot,
paths.agentDirectory,
)
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err)
}
actual, readErr := os.ReadFile(fixture.modernPath)
if readErr != nil || string(actual) != string(concurrent) {
t.Fatalf("prepared source changed: %q, %v", actual, readErr)
}
assertNoKimiRuntimeWrites(t, fixture)
assertNoKimiTransactionArtifacts(t, fixture.homeDir)
}
@@ -0,0 +1,54 @@
package plugins
import (
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
"unicode"
)
func validateManagedPrivateKey(value string) error {
seed, err := base64.RawURLEncoding.DecodeString(value)
if err != nil || len(seed) != 32 || base64.RawURLEncoding.EncodeToString(seed) != value {
return fmt.Errorf("private key must be a canonical 32-byte base64url value")
}
return nil
}
func validateManagedBaseURL(value string) error {
if strings.ContainsRune(value, '\\') {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
for _, character := range value {
if character < 32 || unicode.IsSpace(character) {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
}
parsed, err := url.Parse(value)
if err != nil {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
validScheme := strings.EqualFold(parsed.Scheme, "http") ||
strings.EqualFold(parsed.Scheme, "https")
if !validScheme || parsed.Host == "" || parsed.Hostname() == "" || parsed.Opaque != "" {
return fmt.Errorf("base URL must be an absolute HTTP or HTTPS URL")
}
if parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
return fmt.Errorf(
"base URL must exclude credentials, query parameters, and fragments",
)
}
port := parsed.Port()
if strings.HasSuffix(parsed.Host, ":") {
return fmt.Errorf("base URL must contain a valid port")
}
if port != "" {
value, convertErr := strconv.Atoi(port)
if convertErr != nil || value < 1 || value > 65535 {
return fmt.Errorf("base URL must contain a valid port")
}
}
return nil
}