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. - Use each provider's documented blocking mechanism and failure mode.
- Report installation as healthy only when a complete contract references all required runtime files. - 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. - 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. - 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 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. - 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`. 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. 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 ( import (
"encoding/base64" "encoding/base64"
"encoding/binary"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -29,8 +30,7 @@ func encodeCodexPowerShell(source string) string {
runes := utf16.Encode([]rune(source)) runes := utf16.Encode([]rune(source))
raw := make([]byte, len(runes)*2) raw := make([]byte, len(runes)*2)
for index, value := range runes { for index, value := range runes {
raw[index*2] = byte(value) binary.LittleEndian.PutUint16(raw[index*2:], value)
raw[index*2+1] = byte(value >> 8)
} }
return base64.StdEncoding.EncodeToString(raw) return base64.StdEncoding.EncodeToString(raw)
} }
@@ -1,15 +1,10 @@
package plugins package plugins
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings"
"unicode"
) )
type codexRuntimePaths struct { type codexRuntimePaths struct {
@@ -21,51 +16,6 @@ type codexRuntimePaths struct {
auditPath string 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 { func validateCodexInstallConfig(config InstallConfig) error {
for _, field := range []struct{ name, value string }{ for _, field := range []struct{ name, value string }{
{"agent name", config.AgentName}, {"agent name", config.AgentName},
@@ -83,10 +33,10 @@ func validateCodexInstallConfig(config InstallConfig) error {
if config.AgentName != codexAgentKey { if config.AgentName != codexAgentKey {
return fmt.Errorf("codex installation requires agent name %s", 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 err
} }
return validateCodexBaseURL(config.BaseURL) return validateManagedBaseURL(config.BaseURL)
} }
func codexAgentPaths(config InstallConfig) (*codexRuntimePaths, error) { func codexAgentPaths(config InstallConfig) (*codexRuntimePaths, error) {
+88 -87
View File
@@ -2,74 +2,87 @@ package plugins
import ( import (
"fmt" "fmt"
"strings"
) )
type kimiMutation struct { // KimiPlugin manages Kimi Code and legacy kimi-cli global lifecycle hooks.
document kimiDocument type KimiPlugin struct {
raw []byte rename renameFunc
remove bool
} }
// KimiPlugin manages Kimi Code and legacy kimi-cli global lifecycle hooks. // ManagesGuardRuntime reports that Kimi commits its provider guard with the
type KimiPlugin struct{} // audit runtime and every detected user hook document.
func (p *KimiPlugin) ManagesGuardRuntime() bool {
return true
}
func (p *KimiPlugin) Install(config InstallConfig) error { // PreflightInstall validates every detected hook source and runtime identity.
if config.AgentID == "" { func (p *KimiPlugin) PreflightInstall(config InstallConfig) error {
return fmt.Errorf("agent ID is required")
}
documents, err := readAllKimiConfigs() documents, err := readAllKimiConfigs()
if err != nil { if err != nil {
return err return err
} }
if config.GuardScriptPath == "" { _, _, err = preflightKimiInstallation(config, documents)
return fmt.Errorf("guard script path is required")
}
guardExists, err := regularFileExists(config.GuardScriptPath, "Elydora guard runtime")
if err != nil {
return err 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 func (p *KimiPlugin) Install(config InstallConfig) error {
runtimeConfig.AgentName = kimiAgentKey documents, err := readAllKimiConfigs()
if err := GenerateHookScript(hookPath, runtimeConfig); err != nil { if err != nil {
return fmt.Errorf("generate hook script: %w", err)
}
for _, mutation := range mutations {
if err := writeKimiConfig(mutation.document, mutation.raw); err != nil {
return err 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 return nil
} }
@@ -78,63 +91,51 @@ func (p *KimiPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
mutations := make([]kimiMutation, 0, len(documents)) rendered := make([]kimiRenderedDocument, 0, len(documents))
for _, document := range documents { for _, document := range documents {
keep := keptKimiHookIndices(document.hooks, agentID) keep, err := keptKimiHookIndices(document.hooks, agentID)
if len(keep) == len(document.hooks) {
continue
}
raw, err := renderKimiHooks(document, keep, nil)
if err != nil { if err != nil {
return err return err
} }
if _, err := parseKimiDocument(document.contract, raw, true); err != nil { change, err := renderKimiChange(document, keep, nil)
return fmt.Errorf("validate rendered %s: %w", document.contract.label, err) if err != nil {
}
mutations = append(mutations, kimiMutation{
document: document,
raw: raw,
remove: strings.TrimSpace(string(raw)) == "",
})
}
for _, mutation := range mutations {
if mutation.remove {
if err := removeKimiConfig(mutation.document); err != nil {
return err 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 err
} }
} return writeKimiChanges(
return nil changes,
"Uninstall Kimi hooks",
p.rename,
"",
"",
)
} }
func (p *KimiPlugin) Status() (PluginStatus, error) { func (p *KimiPlugin) Status() (PluginStatus, error) {
documents, err := readAllKimiConfigs() documents, err := readAllKimiConfigs()
entry := SupportedAgents[kimiAgentKey]
status := PluginStatus{ status := PluginStatus{
AgentName: kimiAgentKey, AgentName: kimiAgentKey, DisplayName: entry.Name,
DisplayName: "Kimi Code",
} }
if err != nil { if err != nil {
return status, err return status, err
} }
status.ConfigPath = documents[0].contract.configPath status.ConfigPath = documents[0].contract.configPath
contracts := make([]kimiRuntimeContract, 0, len(documents)) contracts, err := kimiRuntimeContracts(documents)
for _, document := range documents { if err != nil {
contract := kimiRuntimeForDocument(document) return status, err
if contract == nil {
continue
}
contracts = append(contracts, *contract)
status.ConfigPath = contract.configPath
} }
if len(contracts) == 0 { if len(contracts) == 0 {
return status, nil return status, nil
} }
status.ConfigPath = contracts[len(contracts)-1].configPath
status.HookConfigured = true status.HookConfigured = true
status.HookScriptExists, err = kimiRuntimeScriptsExist(contracts) status.HookScriptExists, err = kimiRuntimeFilesExist(contracts)
if err != nil { if err != nil {
return status, err 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 ( import (
"fmt" "fmt"
"path/filepath"
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
@@ -38,8 +37,10 @@ var kimiModernEvents = mergeStringSets(kimiSharedEvents, stringSet(
)) ))
type kimiContract struct { type kimiContract struct {
generation string
runtimeName string runtimeName string
label string label string
directoryLabel string
configPath string configPath string
events map[string]struct{} events map[string]struct{}
} }
@@ -49,11 +50,13 @@ type kimiHook struct {
matcher *string matcher *string
command string command string
timeout *int64 timeout *int64
fieldCount int
} }
type kimiRuntimeContract struct { type kimiRuntimeContract struct {
guard string agentID string
audit string guardPath string
auditPath string
configPath 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) 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 { if value, exists := object["matcher"]; exists {
matcher, ok := value.(string) matcher, ok := value.(string)
if !ok { if !ok {
@@ -154,59 +157,124 @@ func kimiHooks(root map[string]any, contract kimiContract) ([]kimiHook, error) {
return hooks, nil return hooks, nil
} }
func normalizeKimiPath(value string) string { func buildKimiHook(event, command string) (kimiHook, error) {
normalized := filepath.ToSlash(value) switch event {
if runtime.GOOS == "windows" { case "PreToolUse", "PostToolUse", "PostToolUseFailure":
return strings.ToLower(normalized) 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 { func managedKimiReference(
normalized := strings.TrimSpace(normalizeKimiPath(command)) hook kimiHook,
expected := normalizeKimiPath(filePath) event string,
return strings.HasSuffix(normalized, expected) || scriptName string,
strings.HasSuffix(normalized, expected+`'`) || ) (*kimiRuntimeReference, error) {
strings.HasSuffix(normalized, expected+`"`) 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 { func managedKimiEvent(event string) (string, string, bool) {
if hook.event != event || hook.matcher != nil || hook.timeout == nil || *hook.timeout != kimiHookTimeoutSeconds { switch event {
return false 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)) indices := make([]int, 0, len(hooks))
for index, hook := range hooks { for index, hook := range hooks {
managed := isManagedKimiHook(hook, "PreToolUse", kimiGuardScript, agentID) || event, scriptName, managedEvent := managedKimiEvent(hook.event)
isManagedKimiHook(hook, "PostToolUse", kimiAuditScript, agentID) var reference *kimiRuntimeReference
if !managed { 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) indices = append(indices, index)
} }
} }
return indices return indices, nil
} }
func kimiRuntimeForDocument(document kimiDocument) *kimiRuntimeContract { func kimiReferenceKey(agentID string) string {
var guard, audit string if runtime.GOOS == "windows" {
for _, hook := range document.hooks { return strings.ToLower(agentID)
if isManagedKimiHook(hook, "PreToolUse", kimiGuardScript, "") {
guard = hook.command
} }
if isManagedKimiHook(hook, "PostToolUse", kimiAuditScript, "") { return agentID
audit = hook.command }
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 == "" { key := kimiReferenceKey(reference.agentID)
return nil 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" "errors"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"syscall" "syscall"
) )
@@ -23,9 +21,8 @@ func isMissingKimiPath(err error) bool {
return os.IsNotExist(err) || errors.Is(err, syscall.ENOTDIR) return os.IsNotExist(err) || errors.Is(err, syscall.ENOTDIR)
} }
func kimiPathExists(path, label string) (bool, error) { func kimiEntryExists(path, label string) (bool, error) {
// #nosec G703 -- KIMI_CODE_HOME is an explicit local-user configuration path. _, err := os.Lstat(path)
_, err := os.Stat(path)
if err == nil { if err == nil {
return true, 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) return false, fmt.Errorf("inspect %s at %s: %w", label, path, err)
} }
func legacyKimiCLIOnPath() (bool, error) { func stableKimiContract(configPath string) kimiContract {
_, err := exec.LookPath("kimi-cli") return kimiContract{
if err == nil { generation: "stable", runtimeName: "Kimi Code",
return true, nil 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) { func resolveKimiContracts() ([]kimiContract, error) {
@@ -51,58 +54,59 @@ func resolveKimiContracts() ([]kimiContract, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
explicitHome := os.Getenv("KIMI_CODE_HOME") configuredHome := os.Getenv("KIMI_CODE_HOME")
kimiHome := explicitHome stableHome := filepath.Join(home, ".kimi-code")
if kimiHome == "" { explicitHome := configuredHome != ""
kimiHome = filepath.Join(home, ".kimi-code") 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{ legacyHome := filepath.Join(home, ".kimi")
runtimeName: "kimi-cli", stable := stableKimiContract(filepath.Join(stableHome, "config.toml"))
label: "kimi-cli legacy hooks config", legacy := legacyKimiContract(filepath.Join(legacyHome, "config.toml"))
configPath: filepath.Join(home, ".kimi", "config.toml"), if sameKimiPath(stable.configPath, legacy.configPath) {
events: kimiSharedEvents, return []kimiContract{stable}, nil
} }
modernDetected := explicitHome != ""
if !modernDetected { stableDetected := explicitHome
modernDetected, err = kimiPathExists(kimiHome, "Kimi Code home") if !stableDetected {
stableDetected, err = kimiEntryExists(stableHome, "Kimi Code home")
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
legacyDetected, err := kimiPathExists(legacy.configPath, legacy.label) legacyDetected, err := kimiEntryExists(legacyHome, "kimi-cli legacy home")
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !legacyDetected { if legacyDetected && !stableDetected {
legacyDetected, err = legacyKimiCLIOnPath()
if err != nil {
return nil, err
}
}
if legacyDetected && !modernDetected {
return []kimiContract{legacy}, nil return []kimiContract{legacy}, nil
} }
if legacyDetected { 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) { 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 err != nil {
if os.IsNotExist(err) { return kimiDocument{}, err
}
if snapshot == nil {
return parseKimiDocument(contract, nil, false) return parseKimiDocument(contract, nil, false)
} }
return kimiDocument{}, fmt.Errorf("read %s at %s: %w", contract.label, contract.configPath, err) return parseKimiDocument(contract, snapshot.contents, true)
}
return parseKimiDocument(contract, raw, true)
} }
func readAllKimiConfigs() ([]kimiDocument, error) { func readAllKimiConfigs() ([]kimiDocument, error) {
@@ -121,32 +125,6 @@ func readAllKimiConfigs() ([]kimiDocument, error) {
return documents, nil 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 { func kimiRuntimeNames(documents []kimiDocument) string {
names := make([]string, 0, len(documents)) names := make([]string, 0, len(documents))
for _, document := range 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 ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"sort"
"strings"
) )
func kimiRuntimeScriptsExist(contracts []kimiRuntimeContract) (bool, error) { func readKimiRuntimeConfig(path string) (map[string]any, bool, error) {
home, err := kimiHomeDirectory() 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 { if err != nil {
return false, err return err
} }
root := filepath.Join(home, ".elydora") if !sameKimiAgentID(agentID, expectedAgentID) || config["agent_name"] != kimiAgentKey {
entries, err := os.ReadDir(root) 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 err != nil {
if os.IsNotExist(err) { return err
return false, nil
} }
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 { return nil
if !entry.IsDir() { }
continue
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()) directoryExists, err := managedPhysicalDirectoryExists(
guardPath := filepath.Join(agentDir, kimiGuardScript) agentDirectory, "Elydora agent runtime directory",
auditPath := filepath.Join(agentDir, kimiAuditScript) )
if !kimiContractsReferenceRuntime(contracts, guardPath, auditPath) { if err != nil || !directoryExists {
continue return err
} }
configPath := filepath.Join(agentDir, "config.json") configPath := filepath.Join(agentDirectory, "config.json")
config, exists, err := readHookJSONObject(configPath, "Elydora runtime config") config, configExists, err := readKimiRuntimeConfig(configPath)
if err != nil { if err != nil {
return false, err return err
} }
if !exists { artifactExists := false
continue 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) artifactExists = artifactExists || exists
if !ok { }
return false, fmt.Errorf( if !configExists {
`Elydora runtime config at %s field "agent_name" must be a string`, 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, configPath,
) )
} }
if agentName != kimiAgentKey { return nil
continue }
}
guardExists, err := regularFileExists(guardPath, "Elydora guard runtime") func kimiRuntimeContractExists(contract kimiRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
return false, err 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 { if err != nil {
return false, err 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 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 ( import (
"encoding/json" "encoding/json"
"errors"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
) )
func TestKimiRegistryUsesModernGlobalContract(t *testing.T) { func TestKimiRegistryUsesStableGlobalContract(t *testing.T) {
entry := SupportedAgents[kimiAgentKey] 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) 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)) t.Fatalf("Kimi registry plugin = %T", NewPlugin(kimiAgentKey))
} }
} }
func TestKimiInstallPreservesBothConfigsAndIsIdempotent(t *testing.T) { func TestKimiInstallPreservesBothConfigsAndIsIdempotent(t *testing.T) {
modern := "# modern user config\ndefault_model = \"kimi-code/k3\"\n\n" + modern := "# stable user config\ndefault_model = \"kimi-code/k3\"\n\n" +
"[[hooks]]\nevent = \"SessionStart\"\ncommand = \"existing-modern\"\n" + "[[hooks]]\nevent = \"SessionStart\"\ncommand = \"existing-stable\"\n" +
"timeout = 30 # keep modern hook\n" "timeout = 30 # keep stable hook\n"
legacy := "# legacy user config\ntelemetry = false\n\n" + legacy := "# legacy user config\ntelemetry = false\n\n" +
"[[hooks]]\nevent = \"SessionEnd\"\ncommand = \"existing-legacy\"\n" "[[hooks]]\nevent = \"SessionEnd\"\ncommand = \"existing-legacy\"\n"
fixture := prepareKimiFixture(t, kimiFixtureOptions{ fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(modern), modernConfig: kimiString(modern), legacyConfig: kimiString(legacy),
legacyConfig: kimiString(legacy),
}) })
if err := fixture.plugin.Install(fixture.config); err != nil { if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err) 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 { if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("repeat Kimi install: %v", err) t.Fatalf("repeat Kimi install: %v", err)
} }
for _, contract := range []struct{ path, comment, command string }{ 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"}, {fixture.legacyPath, "# legacy user config", "existing-legacy"},
} { } {
raw, err := os.ReadFile(contract.path) raw, err := os.ReadFile(contract.path)
if err != nil { if err != nil {
t.Fatalf("read Kimi config: %v", err) 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) t.Fatalf("user config changed: %s", raw)
} }
hooks := readKimiTestHooks(t, contract.path) hooks := readKimiTestHooks(t, contract.path)
if len(hooks) != 3 { if len(hooks) != 4 {
t.Fatalf("hooks = %#v, want three entries", hooks) t.Fatalf("hooks = %#v, want four entries", hooks)
} }
requireStrictKimiHook(t, findKimiTestHook(t, hooks, "PreToolUse", kimiGuardScript)) requireKimiManagedTriple(t, hooks)
requireStrictKimiHook(t, findKimiTestHook(t, hooks, "PostToolUse", kimiAuditScript)) }
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) { func TestKimiInstallPreservesInlineHookArrayStyle(t *testing.T) {
modern := "# inline user hook\nhooks = [\n # keep leading\n { event = \"SessionStart\", " + modern := "# inline user hook\nhooks = [\n # keep leading\n" +
"command = \"existing-inline\" }, # keep gap\n] # keep array\n" " { event = \"SessionStart\", command = \"existing-inline\" }, # keep gap\n" +
"] # keep array\n"
fixture := prepareKimiFixture(t, kimiFixtureOptions{ fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(modern), modernConfig: kimiString(modern), withoutLegacyEvidence: true,
withoutLegacyCLI: true,
}) })
if err := fixture.plugin.Install(fixture.config); err != nil { if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install inline Kimi hooks: %v", err) 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) t.Fatalf("read inline Kimi config: %v", err)
} }
for _, marker := range []string{ for _, marker := range []string{
"# inline user hook", "# inline user hook", "# keep leading", "# keep gap", "# keep array", "hooks = [",
"# keep leading",
"# keep gap",
"# keep array",
"hooks = [",
} { } {
if !strings.Contains(string(raw), marker) { if !strings.Contains(string(raw), marker) {
t.Fatalf("inline marker %q missing from %s", marker, raw) t.Fatalf("inline marker %q missing from %s", marker, raw)
} }
} }
if len(readKimiTestHooks(t, fixture.modernPath)) != 3 { hooks := readKimiTestHooks(t, fixture.modernPath)
t.Fatalf("inline Kimi hooks are not idempotent: %s", raw) 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{ fixture := prepareKimiFixture(t, kimiFixtureOptions{
useDefaultHome: true, modernConfig: kimiString(source), withoutLegacyEvidence: true,
withoutLegacyCLI: true,
}) })
if err := fixture.plugin.Install(fixture.config); err != nil { 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 { if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
t.Fatalf("modern Kimi config has unexpected hooks") t.Fatalf("uninstall Kimi hooks: %v", err)
} }
if _, err := os.Stat(fixture.legacyPath); !os.IsNotExist(err) { raw, err := os.ReadFile(fixture.modernPath)
t.Fatalf("false legacy migration target exists: %v", err) if err != nil {
t.Fatalf("read empty inline config: %v", err)
} }
status, err := fixture.plugin.Status() if !strings.Contains(string(raw), "# user container") ||
if err != nil || !status.Installed { !strings.Contains(string(raw), "# keep empty array") ||
t.Fatalf("empty KIMI_CODE_HOME status = %#v, %v", status, err) len(readKimiTestHooks(t, fixture.modernPath)) != 0 {
t.Fatalf("empty inline array changed: %s", raw)
} }
} }
func TestKimiLegacyInstallAvoidsPrematureModernTarget(t *testing.T) { func TestKimiUsesDefaultStableAndLegacyEvidence(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{useDefaultHome: true}) 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 { if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install legacy Kimi hooks: %v", err) t.Fatalf("install legacy Kimi hooks: %v", err)
} }
if len(readKimiTestHooks(t, fixture.legacyPath)) != 2 { requireKimiManagedTriple(t, readKimiTestHooks(t, fixture.legacyPath))
t.Fatalf("legacy Kimi config has unexpected hooks") if _, err := os.Stat(fixture.modernPath); !errors.Is(err, os.ErrNotExist) {
} t.Fatalf("stable config exists: %v", err)
if _, err := os.Stat(fixture.modernPath); !os.IsNotExist(err) {
t.Fatalf("premature modern migration target exists: %v", err)
}
if err := os.Remove(fixture.legacyCLIPath); err != nil {
t.Fatalf("remove legacy CLI marker: %v", err)
} }
status, err := fixture.plugin.Status() status, err := fixture.plugin.Status()
if err != nil || !status.Installed { 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) { func TestKimiMigratesExactLegacyCommandsAndPreservesLookalikes(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{}) 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 { if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err) t.Fatalf("migrate legacy Kimi commands: %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)
} }
hooks := readKimiTestHooks(t, fixture.modernPath) hooks := readKimiTestHooks(t, fixture.modernPath)
payload := map[string]any{ if len(hooks) != 4 {
"hook_event_name": "PreToolUse", t.Fatalf("migrated hooks = %#v", hooks)
"session_id": "session-1",
"cwd": fixture.homeDir,
"tool_name": "Bash",
"tool_input": map[string]any{"command": "echo test"},
"tool_call_id": "call-1",
} }
guard := findKimiTestHook(t, hooks, "PreToolUse", kimiGuardScript) requireKimiManagedTriple(t, hooks)
exitCode, stderr := runKimiCommand(t, guard["command"].(string), fixture.homeDir, payload) if !containsKimiCommand(hooks, lookalike) {
if exitCode != 2 || !strings.Contains(stderr, "Agent is frozen by Elydora") { t.Fatalf("lookalike command removed: %#v", hooks)
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
} }
payload["hook_event_name"] = "PostToolUse" if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil {
payload["tool_output"] = map[string]any{"output": "test"} t.Fatalf("uninstall migrated Kimi hooks: %v", err)
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)
} }
capturedRaw, err := os.ReadFile(capturePath) hooks = readKimiTestHooks(t, fixture.modernPath)
if err != nil { if len(hooks) != 1 || hooks[0]["command"] != lookalike {
t.Fatalf("read captured event: %v", err) t.Fatalf("remaining hooks = %#v", hooks)
}
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)
} }
} }
func TestKimiStatusAcceptsEitherContractAndRequiresBothScripts(t *testing.T) { func containsKimiCommand(hooks []map[string]any, command string) bool {
fixture := prepareKimiFixture(t, kimiFixtureOptions{}) for _, hook := range hooks {
if err := fixture.plugin.Install(fixture.config); err != nil { if hook["command"] == command {
t.Fatalf("install Kimi hooks: %v", err) 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) { func TestKimiUninstallPreservesUserConfigsAndRemovesManagedConfigs(t *testing.T) {
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) {
userHook := "# user hook\n[[hooks]]\nevent = \"SessionStart\"\n" + userHook := "# user hook\n[[hooks]]\nevent = \"SessionStart\"\n" +
"command = \"existing-command\"\ntimeout = 30 # keep timeout\n" "command = \"existing-command\"\ntimeout = 30 # keep timeout\n"
fixture := prepareKimiFixture(t, kimiFixtureOptions{ fixture := prepareKimiFixture(t, kimiFixtureOptions{
modernConfig: kimiString(userHook), modernConfig: kimiString(userHook), legacyConfig: kimiString(userHook),
legacyConfig: kimiString(userHook),
}) })
if err := fixture.plugin.Install(fixture.config); err != nil { if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err) t.Fatalf("install Kimi hooks: %v", err)
@@ -232,159 +282,129 @@ func TestKimiUninstallPreservesUserHooks(t *testing.T) {
} }
for _, path := range []string{fixture.modernPath, fixture.legacyPath} { for _, path := range []string{fixture.modernPath, fixture.legacyPath} {
raw, err := os.ReadFile(path) raw, err := os.ReadFile(path)
if err != nil { if err != nil || !strings.Contains(string(raw), "# keep timeout") {
t.Fatalf("read user config after uninstall: %v", err) t.Fatalf("user config changed: %s, %v", raw, err)
}
if !strings.Contains(string(raw), "# user hook") || !strings.Contains(string(raw), "# keep timeout") {
t.Fatalf("user comments changed: %s", raw)
} }
hooks := readKimiTestHooks(t, path) hooks := readKimiTestHooks(t, path)
if len(hooks) != 1 || hooks[0]["command"] != "existing-command" { if len(hooks) != 1 || hooks[0]["command"] != "existing-command" {
t.Fatalf("user hook changed: %#v", hooks) 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) { func TestKimiStatusRequiresCompleteTripleAndEveryRuntimeFile(t *testing.T) {
fixture := prepareKimiFixture(t, kimiFixtureOptions{}) fixture := prepareKimiFixture(t, kimiFixtureOptions{withoutLegacyEvidence: true})
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)
}
if err := fixture.plugin.Install(fixture.config); err != nil { if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Kimi hooks: %v", err) t.Fatalf("install Kimi hooks: %v", err)
} }
if err := fixture.plugin.Uninstall(kimiTestAgentID); err != nil { status, err := fixture.plugin.Status()
t.Fatalf("uninstall Kimi hooks: %v", err) 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) { raw, err := os.ReadFile(fixture.modernPath)
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)
if err != nil { if err != nil {
t.Fatalf("read config directory: %v", err) t.Fatalf("read Kimi config: %v", err)
} }
for _, entry := range entries { document, err := parseKimiDocument(
if strings.HasSuffix(entry.Name(), ".tmp") { stableKimiContract(fixture.modernPath), raw, true,
t.Fatalf("temporary Kimi config remains at %s", filepath.Join(directory, entry.Name())) )
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" "github.com/pelletier/go-toml/v2"
) )
const kimiTestAgentID = "agent-1" const (
kimiTestAgentID = "agent-1"
kimiPrivateKey = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc"
)
type kimiFixtureOptions struct { type kimiFixtureOptions struct {
modernConfig *string modernConfig *string
legacyConfig *string legacyConfig *string
useDefaultHome bool useDefaultHome bool
withoutLegacyCLI bool withoutModernEvidence bool
withoutGuard bool withoutLegacyEvidence bool
} }
type kimiFixture struct { type kimiFixture struct {
plugin *KimiPlugin plugin *KimiPlugin
config InstallConfig config InstallConfig
homeDir string homeDir string
binDir string projectDir string
kimiHome string kimiHome string
modernPath string modernPath string
legacyHome string
legacyPath string legacyPath string
agentDir string agentDir string
guardPath string guardPath string
hookPath string hookPath string
runtimeConfig string runtimeConfig string
privateKey string privateKey string
legacyCLIPath string
} }
func kimiString(value string) *string { func kimiString(value string) *string {
@@ -47,45 +50,38 @@ func kimiString(value string) *string {
func prepareKimiFixture(t *testing.T, options kimiFixtureOptions) *kimiFixture { func prepareKimiFixture(t *testing.T, options kimiFixtureOptions) *kimiFixture {
t.Helper() t.Helper()
homeDir := filepath.Join(t.TempDir(), "home with spaces") root := t.TempDir()
binDir := filepath.Join(homeDir, "bin") homeDir := filepath.Join(root, "home with spaces and 'quote %ELYDORA_HOOK_PATH%")
kimiHome := filepath.Join(homeDir, "custom kimi code") projectDir := filepath.Join(root, "project with spaces")
kimiHome := filepath.Join(homeDir, "custom kimi-code")
if options.useDefaultHome { if options.useDefaultHome {
kimiHome = filepath.Join(homeDir, ".kimi-code") kimiHome = filepath.Join(homeDir, ".kimi-code")
} }
modernPath := filepath.Join(kimiHome, "config.toml") 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) agentDir := filepath.Join(homeDir, ".elydora", kimiTestAgentID)
guardPath := filepath.Join(agentDir, kimiGuardScript) guardPath := filepath.Join(agentDir, kimiGuardScript)
hookPath := filepath.Join(agentDir, kimiAuditScript) hookPath := filepath.Join(agentDir, kimiAuditScript)
if err := os.MkdirAll(agentDir, 0755); err != nil { if err := os.MkdirAll(projectDir, 0700); err != nil {
t.Fatalf("create agent directory: %v", err) t.Fatalf("create project directory: %v", err)
} }
if err := os.MkdirAll(binDir, 0755); err != nil { if options.useDefaultHome && !options.withoutModernEvidence {
t.Fatalf("create binary directory: %v", err) 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 !options.withoutLegacyEvidence {
if err := os.WriteFile(guardPath, []byte(source), 0700); err != nil { if err := os.MkdirAll(legacyHome, 0700); err != nil {
t.Fatalf("write guard runtime: %v", err) t.Fatalf("create legacy Kimi home: %v", err)
} }
} }
writeOptionalKimiConfig(t, modernPath, options.modernConfig) writeOptionalKimiConfig(t, modernPath, options.modernConfig)
writeOptionalKimiConfig(t, legacyPath, options.legacyConfig) 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("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir) t.Setenv("USERPROFILE", homeDir)
t.Setenv("ELYDORA_HOOK_PATH", filepath.Join(root, "expanded-command-fragment"))
if options.useDefaultHome { if options.useDefaultHome {
t.Setenv("KIMI_CODE_HOME", "") t.Setenv("KIMI_CODE_HOME", "")
} else { } else {
@@ -93,27 +89,17 @@ func prepareKimiFixture(t *testing.T, options kimiFixtureOptions) *kimiFixture {
} }
return &kimiFixture{ return &kimiFixture{
plugin: &KimiPlugin{}, plugin: &KimiPlugin{}, homeDir: homeDir, projectDir: projectDir,
homeDir: homeDir, kimiHome: kimiHome, modernPath: modernPath,
binDir: binDir, legacyHome: legacyHome, legacyPath: legacyPath,
kimiHome: kimiHome, agentDir: agentDir, guardPath: guardPath, hookPath: hookPath,
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,
runtimeConfig: filepath.Join(agentDir, "config.json"), runtimeConfig: filepath.Join(agentDir, "config.json"),
privateKey: filepath.Join(agentDir, "private.key"), 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 { if raw == nil {
return 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) t.Fatalf("create Kimi config directory: %v", err)
} }
if err := os.WriteFile(path, []byte(*raw), 0600); err != nil { 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 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() t.Helper()
for _, hook := range hooks { for index := len(hooks) - 1; index >= 0; index-- {
command, _ := hook["command"].(string) hook := hooks[index]
if hook["event"] == event && strings.Contains(command, script) { if hook["event"] == event && hook["timeout"] == int64(10) {
return hook return hook
} }
} }
t.Fatalf("%s hook for %s not found", event, script) t.Fatalf("managed %s hook not found", event)
return nil return nil
} }
func requireStrictKimiHook(t *testing.T, hook map[string]any) { func requireStrictKimiHook(t *testing.T, hook map[string]any, event string) {
t.Helper() t.Helper()
want := map[string]any{ want := map[string]any{
"event": hook["event"], "event": event, "command": hook["command"], "timeout": int64(10),
"command": hook["command"],
"timeout": int64(10),
} }
if !reflect.DeepEqual(hook, want) { if !reflect.DeepEqual(hook, want) {
t.Fatalf("hook = %#v, want strict contract %#v", 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() t.Helper()
encoded, err := json.Marshal(payload) encoded, err := json.Marshal(payload)
if err != nil { if err != nil {
t.Fatalf("marshal Kimi payload: %v", err) 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 var process *exec.Cmd
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
commandFile := filepath.Join(t.TempDir(), "run-kimi-hook.cmd") 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 { } else {
process = exec.Command("sh", "-c", command) 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) process.Stdin = bytes.NewReader(encoded)
var stderr bytes.Buffer var stderr bytes.Buffer
process.Stderr = &stderr process.Stderr = &stderr
err = process.Run() err := process.Run()
if err == nil { if err == nil {
return 0, stderr.String() 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) t.Fatalf("run Kimi hook command: %v", err)
return -1, stderr.String() 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 { type kimiDocument struct {
contract kimiContract contract kimiContract
exists bool
raw []byte raw []byte
hooks []kimiHook hooks []kimiHook
layout kimiTomlLayout layout kimiTomlLayout
@@ -31,7 +32,9 @@ type kimiDocument struct {
func parseKimiDocument(contract kimiContract, raw []byte, exists bool) (kimiDocument, error) { func parseKimiDocument(contract kimiContract, raw []byte, exists bool) (kimiDocument, error) {
if !exists { 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{} root := map[string]any{}
if err := toml.Unmarshal(raw, &root); err != nil { if err := toml.Unmarshal(raw, &root); err != nil {
@@ -76,6 +79,7 @@ func parseKimiDocument(contract kimiContract, raw []byte, exists bool) (kimiDocu
} }
return kimiDocument{ return kimiDocument{
contract: contract, contract: contract,
exists: true,
raw: append([]byte(nil), raw...), raw: append([]byte(nil), raw...),
hooks: hooks, hooks: hooks,
layout: layout, 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
}