fix(go): align Kimi hooks with official contracts

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 05:00:50 -04:00
1 parent fd62dca155
commit 3ddce86c37
17 files changed
+1971 -616

No files matched your search

+1 -1
View File
@@ -23,7 +23,7 @@ This repository publishes the open-source server, console, integration catalog,
- Use each provider's documented blocking mechanism and failure mode.
- Report installation as healthy only when a complete contract references all required runtime files.
- Keep stable, legacy, and early-access contracts explicit, including their activation commands.
- Resolve stable Kimi hooks through `$KIMI_CODE_HOME/config.toml` with `~/.kimi-code/config.toml` as the default, and treat an empty override as the default. Activate `~/.kimi/config.toml` only when the legacy home exists. Register exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` rules with ten-second commands; stable Kimi supports its current sixteen-event schema and legacy kimi-cli supports its thirteen-event schema. Preserve the native snake_case payload, propagate exit code `2`, and keep audit delivery fail-open with owner-only error logs. Use encoded PowerShell commands on Windows and exact two-argument shell commands on Unix. Commit every detected config, generated runtime, runtime config, and private key through one rollback-capable transaction.
- Resolve stable Kimi hooks through `$KIMI_CODE_HOME/config.toml` with `~/.kimi-code/config.toml` as the default, and treat an empty override as the default. Activate `~/.kimi/config.toml` only when the legacy home exists, ignore executable lookup as activation evidence, and collapse equal stable and legacy paths to the stable contract. Register exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` rules with ten-second commands; stable Kimi supports its current sixteen-event schema and legacy kimi-cli supports its thirteen-event schema. Preserve the native snake_case payload, propagate exit code `2`, and keep audit delivery fail-open with owner-only error logs. Use encoded PowerShell commands on Windows and exact two-argument shell commands on Unix. Commit every detected config, generated runtime, runtime config, and private key through one rollback-capable transaction. Status requires strict runtime metadata, a canonical private key, exact identity, and non-empty physical scripts.
- Preserve Kimi TOML comments, array style, and unrelated formatting through syntax-preserving edits, then parse every selected document before writing any file.
- Write Grok Build integrations to a native user hook file under non-empty `GROK_HOME` or `~/.grok`; keep Claude Code and Cursor compatibility sources read-only.
- 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.
+1 -1
View File
@@ -86,7 +86,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)
}
@@ -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) {
+88 -87
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")
if err != nil {
_, _, err = preflightKimiInstallation(config, documents)
return err
}
if !guardExists {
return fmt.Errorf("Elydora guard runtime is missing: %s", config.GuardScriptPath)
}
hookPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
if config.HookScript != "" {
hookPath = config.HookScript
}
nodePath, err := resolveNodeRuntime()
if err != nil {
return err
}
additions := []kimiHook{
buildKimiHook("PreToolUse", buildKimiCommand(nodePath, config.GuardScriptPath)),
buildKimiHook("PostToolUse", buildKimiCommand(nodePath, hookPath)),
}
mutations := make([]kimiMutation, 0, len(documents))
for _, document := range documents {
raw, err := renderKimiHooks(document, keptKimiHookIndices(document.hooks, ""), additions)
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})
}
}
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 {
func (p *KimiPlugin) Install(config InstallConfig) error {
documents, err := readAllKimiConfigs()
if err != nil {
return err
}
paths, nodePath, err := preflightKimiInstallation(config, documents)
if err != nil {
return err
}
fmt.Printf("%s: global PreToolUse and PostToolUse hooks installed.\n", kimiRuntimeNames(documents))
guardCommand, err := buildKimiCommand(nodePath, paths.guardPath)
if err != nil {
return err
}
auditCommand, err := buildKimiCommand(nodePath, paths.auditPath)
if err != nil {
return err
}
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
}
additions = append(additions, hook)
}
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)
}
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 {
change, err := renderKimiChange(document, keep, nil)
if err != nil {
return err
}
continue
rendered = append(rendered, change)
}
if err := writeKimiConfig(mutation.document, mutation.raw); err != nil {
changes, err := prepareKimiUninstallChanges(rendered)
if err != nil {
return err
}
}
return nil
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
}
+108 -40
View File
@@ -2,7 +2,6 @@ package plugins
import (
"fmt"
"path/filepath"
"runtime"
"sort"
"strings"
@@ -38,8 +37,10 @@ var kimiModernEvents = mergeStringSets(kimiSharedEvents, stringSet(
))
type kimiContract struct {
generation string
runtimeName string
label string
directoryLabel string
configPath string
events map[string]struct{}
}
@@ -49,11 +50,13 @@ type kimiHook struct {
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
func kimiReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
if isManagedKimiHook(hook, "PostToolUse", kimiAuditScript, "") {
audit = hook.command
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
}
if guard == "" || audit == "" {
return nil
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
}
@@ -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)
}
+48 -70
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,
}
legacy := kimiContract{
runtimeName: "kimi-cli",
label: "kimi-cli legacy hooks config",
configPath: filepath.Join(home, ".kimi", "config.toml"),
events: kimiSharedEvents,
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
}
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)
directory := filepath.Dir(contract.configPath)
if _, err := managedPhysicalDirectoryExists(directory, contract.directoryLabel); err != nil {
return kimiDocument{}, err
}
snapshot, err := readManagedFile(
contract.configPath,
contract.label,
maxManagedSourceBytes,
)
if err != nil {
if os.IsNotExist(err) {
return kimiDocument{}, err
}
if snapshot == nil {
return parseKimiDocument(contract, nil, false)
}
return kimiDocument{}, fmt.Errorf("read %s at %s: %w", contract.label, contract.configPath, err)
}
return parseKimiDocument(contract, raw, true)
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 {
@@ -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)
}
}
@@ -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)
}
}
+166 -42
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 false, err
return err
}
root := filepath.Join(home, ".elydora")
entries, err := os.ReadDir(root)
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 {
if os.IsNotExist(err) {
return false, nil
return err
}
return false, fmt.Errorf("read Elydora runtime directory at %s: %w", root, err)
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf("elydora runtime config base URL is invalid at %s: %w", configPath, err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue
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
}
agentDir := filepath.Join(root, entry.Name())
guardPath := filepath.Join(agentDir, kimiGuardScript)
auditPath := filepath.Join(agentDir, kimiAuditScript)
if !kimiContractsReferenceRuntime(contracts, guardPath, auditPath) {
continue
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory, "Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDir, "config.json")
config, exists, err := readHookJSONObject(configPath, "Elydora runtime config")
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readKimiRuntimeConfig(configPath)
if err != nil {
return false, err
return err
}
if !exists {
continue
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
}
agentName, ok := config["agent_name"].(string)
if !ok {
return false, fmt.Errorf(
`Elydora runtime config at %s field "agent_name" must be a string`,
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,
)
}
if agentName != kimiAgentKey {
continue
}
guardExists, err := regularFileExists(guardPath, "Elydora guard runtime")
return nil
}
func kimiRuntimeContractExists(contract kimiRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
auditExists, err := regularFileExists(auditPath, "Elydora audit runtime")
agentDirectory := filepath.Dir(contract.guardPath)
if !sameKimiPath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameKimiPath(contract.auditPath, filepath.Join(agentDirectory, kimiAuditScript)) {
return false, nil
}
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
}
return guardExists && auditExists, nil
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 {
return true, 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
}
+280 -260
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)
}
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)
}
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)
}
}
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)
t.Fatalf("install Kimi hooks: %v", err)
}
if len(readKimiTestHooks(t, fixture.modernPath)) != 2 {
t.Fatalf("modern Kimi config has unexpected hooks")
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("uninstall Kimi hooks: %v", err)
}
if _, err := os.Stat(fixture.legacyPath); !os.IsNotExist(err) {
t.Fatalf("false legacy migration target exists: %v", err)
raw, err := os.ReadFile(fixture.modernPath)
if err != nil {
t.Fatalf("read empty inline config: %v", err)
}
status, err := fixture.plugin.Status()
if err != nil || !status.Installed {
t.Fatalf("empty KIMI_CODE_HOME status = %#v, %v", status, 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 TestKimiLegacyInstallAvoidsPrematureModernTarget(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{useDefaultHome: true})
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)
}
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)
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 status after PATH change = %#v, %v", status, err)
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 TestKimiCommandsBlockAndForwardOfficialPayload(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{})
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("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)
}
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)
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)
func containsKimiCommand(hooks []map[string]any, command string) bool {
for _, hook := range hooks {
if hook["command"] == command {
return true
}
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)
}
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)
}
}
managed := prepareKimiFixture(t, kimiFixtureOptions{})
if err := managed.plugin.Install(managed.config); err != nil {
t.Fatalf("install managed Kimi configs: %v", err)
}
if err := managed.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("remove managed Kimi configs: %v", 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 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)
}
if err := os.WriteFile(fixture.modernPath, []byte(userHooks), 0600); err != nil {
t.Fatalf("write ownership fixture: %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)
}
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("uninstall Kimi hooks: %v", err)
status, err := fixture.plugin.Status()
if err != nil || !status.Installed || !status.HookConfigured {
t.Fatalf("Kimi status = %#v, %v", status, 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)
}
}
}
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)
}
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)
}
}
requireNoKimiRuntimeWrites(t, fixture)
}
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`},
} {
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)
}
raw, readErr := os.ReadFile(fixture.legacyPath)
if readErr != nil || string(raw) != testCase.raw {
t.Fatalf("invalid config changed: %q, %v", raw, readErr)
}
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) {
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)
raw, err := os.ReadFile(fixture.modernPath)
if err != nil {
t.Fatalf("read config directory: %v", err)
t.Fatalf("read Kimi config: %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()))
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)
}
}
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)
}
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(filepath.Base(path), func(t *testing.T) {
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read runtime file: %v", err)
}
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)
}
})
}
}
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)
}
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)
}
}
@@ -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
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
}