7 Commits
Author SHA1 Message Date
雷电芽衣 fcdc2af2b9 chore(go): release 2.1.0
Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 02:44:20 -04:00
雷电芽衣 301210ff25 docs(go): audit README and AGENTS
Align the README API reference with the exported surface, add the
upgrade note, document the shared modules and Linux test setup in
AGENTS.md, and scope the Grok expandable-path assertion to Windows.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 02:43:55 -04:00
雷电芽衣 96489cd051 chore(go): audit providers kirocli kiroide opencode
Move Kiro IDE onto managed_runtime.go and shell_command.go, share
cloneJSONObject across providers, make OpenCode status verify the managed
runtime contract instead of any hook.js, propagate script removal errors,
and build the legacy runtime config through buildManagedRuntimeConfig.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 02:32:20 -04:00
雷电芽衣 34ff4c1ea8 chore(go): audit providers kimi grok qwen letta
Move Kimi, Grok, and Qwen onto managed_runtime.go and shell_command.go;
share the encoded and shell command builders and the quoted Windows
legacy parser with Droid; split managed_changes.go; dedupe Letta's hook
entry filtering and stop swallowing settings read errors.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 02:16:58 -04:00
雷电芽衣 1ede03d989 chore(go): audit providers copilot droid augment cline
Move the four providers onto managed_runtime.go and shell_command.go;
keep the Auggie wrappers, Cline metadata wrappers, and the Copilot and
Droid precondition handling provider-local.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 01:56:07 -04:00
雷电芽衣 a7fc75bf6b chore(go): audit providers claudecode codex cursor gemini
Add managed_runtime.go (paths, config validation, identity, runtime file
changes, status checks) and shell_command.go (quoting, PowerShell
encoding, command parsing) and move the four providers onto them.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 01:42:27 -04:00
雷电芽衣 c1caed3d42 chore(go): audit transaction engine
Share change filtering between the durable and test transaction paths,
collapse repeated precondition handling, unify journal sequence parsing,
and split the subprocess worker test file.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 01:20:38 -04:00
107 changed files with 2016 additions and 6841 deletions

No files matched your search

+12 -1
View File
@@ -47,6 +47,15 @@ This repository owns the public Go SDK, the `cmd/elydora` CLI, local signing beh
- Preserve JSONC comments, line endings, and unknown future hook fields. Reject trailing commas and duplicate keys, validate all 21 current events plus command, HTTP, and prompt handlers, reject settings-defined function handlers, and apply Node.js `RegExp` semantics only to the 15 matcher-enabled events.
- Own one exact named matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` triple with 10,000 millisecond timeouts. Generate guard, strict runtime config, canonical private key, and native-payload audit runtime with user settings in one rollback-capable transaction. Propagate frozen and revoked agents through exit code `2`; require effective hooks to be enabled, one exact triple, physical runtime files, strict identity, and exact generated sources for healthy status; direct users to `/hooks` for review.
## Shared Modules
- `cmd/elydora/plugins/managed_runtime.go` owns install-config validation, managed runtime paths, runtime config validation, runtime identity checks, and the strict and presence-only status contracts. `managed_changes.go` owns runtime config encoding, the four runtime file changes, and the shared directory-plus-commit writer.
- `cmd/elydora/plugins/shell_command.go` owns Node.js resolution, POSIX and PowerShell quoting, encoded Windows commands, the shell and encoded command builders and parsers, and the pre-2.1 quoted Windows parser.
- `cmd/elydora/plugins/jsonc.go` owns strict JSON object decoding, JSONC editing, and `cloneJSONObject`; `managed_files.go` owns physical file and directory inspection; `transaction*.go` owns the rollback-capable commit.
- Provider files keep only their hook document schema, ownership rules, rendering, and provider-specific preconditions. Add a provider-local helper only when the shared module cannot express the provider contract.
- `jsonutil.go` and `legacy_hook_entries.go` serve the Kiro CLI, Letta, and OpenCode adapters, which write settings without the managed transaction; moving them onto the managed model is a product decision.
- Recognizing pre-2.1 Elydora commands stays in place so `elydora install` replaces older entries during upgrades.
## Code Quality
- Preserve the minimum Go version declared in `go.mod`.
@@ -65,7 +74,7 @@ This repository owns the public Go SDK, the `cmd/elydora` CLI, local signing beh
Run the focused adapter test during development, then execute all release gates before commit:
```powershell
```sh
go test ./cmd/elydora/plugins -run <Provider> -count=1
go test ./...
go test -race ./...
@@ -75,6 +84,8 @@ govulncheck ./...
git diff --check
```
The Linux test environment leaks provider config into `$HOME`; run the suite with `HOME` and `XDG_CONFIG_HOME` pointed at a scratch directory while `GOPATH`, `GOMODCACHE`, and `GOCACHE` keep their real values.
Provider adapter tests must cover installation, idempotency, official event forwarding, blocking behavior, status, missing runtime files, uninstall ownership, and malformed configuration preservation.
Commit and push one root issue before starting the next one.
+50 -25
View File
@@ -10,6 +10,8 @@ go get github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2
Requires Go 1.21+. The CLI uses focused TOML and JSONC parsers to preserve user-owned agent configuration.
Upgrading from 2.0.x: run `elydora install` again for every agent. The generated runtimes changed, and `elydora status` does not detect stale runtimes for every adapter.
## Quick Start
```go
@@ -23,18 +25,12 @@ import (
)
func main() {
// Authenticate
auth, err := elydora.Login("https://api.elydora.com", "user@example.com", "password")
if err != nil {
log.Fatal(err)
}
// Create client
// Create client with an API token issued in the Console
client, err := elydora.NewClient(&elydora.Config{
OrgID: auth.User.OrgID,
OrgID: "org-123",
AgentID: "my-agent-id",
PrivateKey: "<base64url-encoded-ed25519-seed>",
Token: auth.Token,
Token: "<api-token>",
})
if err != nil {
log.Fatal(err)
@@ -113,8 +109,9 @@ Qwen Code `0.20.0` installation writes native user hooks to `$QWEN_HOME/settings
|---------|-------------|
| `elydora install` | Install Elydora audit hook for a coding agent |
| `elydora uninstall` | Remove Elydora audit hook for a coding agent |
| `elydora status` | Show installation status for all agents |
| `elydora status` | Show installation status for one agent or all agents |
| `elydora agents` | List supported coding agents |
| `elydora version` | Print the CLI version |
### Supported Agents
@@ -162,24 +159,23 @@ client.SetToken("new-api-token")
### Authentication
```go
// Register a new user and organization
reg, err := elydora.Register(baseURL, email, password,
elydora.WithDisplayName("Alice"),
elydora.WithOrgName("Acme Corp"),
)
// Login and receive a session token
auth, err := elydora.Login(baseURL, email, password)
// Get current authenticated user profile
// Get the authenticated user and current organization
me, err := client.GetMe()
// me.User.Email, me.User.Role, me.CurrentOrganization.OrgID
// Issue a new API token (with optional TTL)
// Issue an API token; this call needs a client whose Token is a Console session token
tokenResp, err := client.IssueApiToken(&elydora.IssueApiTokenRequest{
TTLSeconds: &ttlSeconds,
})
client.SetToken(tokenResp.Token)
// Rotate the installed API token; the response carries the replacement
rotated, err := client.RotateApiToken()
client.SetToken(rotated.Token)
```
`elydora.Register` and `elydora.Login` remain for password accounts and are deprecated; use the Console sign-in flow.
### Operations
```go
@@ -219,8 +215,11 @@ agent, err := client.RegisterAgent(&elydora.RegisterAgentRequest{
// Get agent details
details, err := client.GetAgent(agentID)
// Change the integration type
updated, err := client.UpdateAgent(agentID, elydora.IntegrationTypeClaudecode)
// Freeze an agent
err := client.FreezeAgent(agentID, "security review")
frozen, err := client.FreezeAgent(agentID, "security review")
// Revoke a key
err := client.RevokeKey(agentID, kid, "key rotation")
@@ -229,7 +228,7 @@ err := client.RevokeKey(agentID, kid, "key rotation")
agents, err := client.ListAgents()
// Unfreeze a previously frozen agent
err := client.UnfreezeAgent(agentID, "review complete")
unfrozen, err := client.UnfreezeAgent(agentID, "review complete")
// Delete an agent permanently
deleted, err := client.DeleteAgent(agentID)
@@ -270,6 +269,25 @@ detail, err := client.GetExport(exportID)
data, err := client.DownloadExport(exportID)
```
### Webhooks
```go
webhook, err := client.RegisterWebhook(&elydora.RegisterWebhookRequest{
EndpointURL: "https://example.com/elydora",
Events: []string{"operation.accepted"},
Secret: "<signing-secret>",
})
webhooks, err := client.ListWebhooks()
err = client.DeleteWebhook(webhook.Webhook.WebhookID)
```
### Organization
```go
members, err := client.ListMembers()
events, err := client.ListAdminEvents(50)
```
### JWKS
```go
@@ -281,7 +299,11 @@ jwks, err := client.GetJWKS()
```go
// Check API health (no authentication required, does not need a client)
health, err := elydora.Health("https://api.elydora.com")
// health.Status, health.Version, health.ProtocolVersion, health.Timestamp
// health.Status, health.Version, health.ProtocolVersion, health.Capabilities, health.Timestamp
// Check dependency health (D1, auth storage, R2, KV)
deep, err := elydora.DeepHealth("https://api.elydora.com")
// deep.Dependencies.D1.Status, deep.Dependencies.KV.LatencyMs
```
### Constants
@@ -299,8 +321,11 @@ elydora.GenesisChainHash // "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
| `KeyStatus` | `KeyStatusActive`, `KeyStatusRetired`, `KeyStatusRevoked` |
| `ExportStatus` | `ExportStatusQueued`, `ExportStatusRunning`, `ExportStatusDone`, `ExportStatusFailed` |
| `ExportFormat` | `ExportFormatJSON`, `ExportFormatPDF` |
| `WebhookStatus` | `WebhookStatusActive`, `WebhookStatusDisabled` |
| `IntegrationType` | One constant per supported agent key plus `IntegrationTypeEnterprise`, `IntegrationTypeGUI`, `IntegrationTypeSDK`, `IntegrationTypeOther` |
| `AdminAction` | One constant per admin event action, for example `AdminActionAgentFreeze` |
| `RbacRole` | `RbacRoleOrgOwner`, `RbacRoleSecurityAdmin`, `RbacRoleComplianceAuditor`, `RbacRoleReadonlyInvestigator`, `RbacRoleIntegrationEngineer` |
| `ErrorCode` | `ErrorCodeInvalidSignature`, `ErrorCodeUnknownAgent`, `ErrorCodeKeyRevoked`, `ErrorCodeAgentFrozen`, `ErrorCodeTTLExpired`, `ErrorCodeReplayDetected`, `ErrorCodePrevHashMismatch`, `ErrorCodePayloadTooLarge`, `ErrorCodeRateLimited`, `ErrorCodeInternalError`, `ErrorCodeUnauthorized`, `ErrorCodeForbidden`, `ErrorCodeNotFound`, `ErrorCodeValidationError` |
| `ErrorCode` | `ErrorCodeInvalidSignature`, `ErrorCodeUnknownAgent`, `ErrorCodeKeyRevoked`, `ErrorCodeKeyRetired`, `ErrorCodeAgentFrozen`, `ErrorCodeAgentRevoked`, `ErrorCodeTTLExpired`, `ErrorCodeReplayDetected`, `ErrorCodePrevHashMismatch`, `ErrorCodePayloadTooLarge`, `ErrorCodeRateLimited`, `ErrorCodeInternalError`, `ErrorCodeUnauthorized`, `ErrorCodeForbidden`, `ErrorCodeNotFound`, `ErrorCodeValidationError` |
## Error Handling
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2/cmd/elydora/plugins"
)
const version = "2.0.1"
const version = "2.1.0"
func main() {
if len(os.Args) < 2 {
+5 -4
View File
@@ -10,8 +10,7 @@ type AugmentPlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Auggie commits its provider guard together
// with both wrappers, the audit runtime, and user settings.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *AugmentPlugin) ManagesGuardRuntime() bool {
return true
}
@@ -57,13 +56,14 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
if err := writeAugmentChanges(
if err := writeManagedChanges(
changes,
"Install Augment Code CLI hooks",
p.rename,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.configPath),
"Auggie configuration directory",
); err != nil {
return err
}
@@ -99,13 +99,14 @@ func (p *AugmentPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
return writeAugmentChanges(
return writeManagedChanges(
[]*fileChange{change},
"Uninstall Augment Code CLI hooks",
p.rename,
"",
"",
filepath.Dir(document.configPath),
"Auggie configuration directory",
)
}
+13 -67
View File
@@ -12,11 +12,10 @@ import (
)
const (
augmentAgentKey = "augment"
augmentGuardScript = "guard.js"
augmentAuditScript = "hook.js"
augmentHookTimeout = float64(10_000)
augmentPOSIXApostrophe = `'"'"'`
augmentAgentKey = "augment"
augmentGuardScript = "guard.js"
augmentAuditScript = "hook.js"
augmentHookTimeout = float64(10_000)
)
var (
@@ -68,14 +67,6 @@ type augmentRuntimeContract struct {
auditWrapper string
}
func cloneAugmentObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
func validateAugmentHandler(
value any,
event string,
@@ -114,7 +105,7 @@ func validateAugmentHandler(
return nil, fmt.Errorf("%s timeout must be a positive finite number", label)
}
}
return cloneAugmentObject(handler), nil
return cloneJSONObject(handler), nil
}
func validateAugmentMetadata(value any, label string) error {
@@ -169,7 +160,7 @@ func validateAugmentGroup(value any, event string, groupIndex int) (augmentGroup
}
handlers = append(handlers, handler)
}
return augmentGroup{object: cloneAugmentObject(object), handlers: handlers}, nil
return augmentGroup{object: cloneJSONObject(object), handlers: handlers}, nil
}
func readAugmentHooks(root map[string]any) (augmentHooks, error) {
@@ -248,7 +239,7 @@ func renderAugmentDocument(
if !document.exists && len(hooks) == 0 {
return &augmentRenderedDocument{document: document}, nil
}
root := cloneAugmentObject(document.root)
root := cloneJSONObject(document.root)
if len(hooks) == 0 {
delete(root, "hooks")
} else {
@@ -281,7 +272,7 @@ func renderAugmentHooks(hooks augmentHooks) map[string]any {
for event, groups := range hooks {
values := make([]any, 0, len(groups))
for _, group := range groups {
object := cloneAugmentObject(group.object)
object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers {
handlers = append(handlers, handler)
@@ -314,23 +305,8 @@ func readAugmentWindowsArgument(command string) (string, bool) {
}
func readAugmentPOSIXArgument(command string) (string, bool) {
if len(command) < 2 || command[0] != '\'' {
return "", false
}
var value strings.Builder
for index := 1; index < len(command); {
if strings.HasPrefix(command[index:], augmentPOSIXApostrophe) {
value.WriteByte('\'')
index += len(augmentPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index == len(command)-1 && value.Len() > 0
}
value.WriteByte(command[index])
index++
}
return "", false
value, end, ok := readPOSIXArgument(command, 0)
return value, ok && end == len(command) && value != ""
}
func parseAugmentCommand(command string) (string, bool) {
@@ -340,36 +316,6 @@ func parseAugmentCommand(command string) (string, bool) {
return readAugmentPOSIXArgument(command)
}
func normalizeAugmentPath(value string) string {
absolute, err := filepath.Abs(value)
if err == nil {
value = absolute
}
value = filepath.ToSlash(filepath.Clean(value))
if runtime.GOOS == "windows" {
return strings.ToLower(value)
}
return value
}
func sameAugmentPath(left, right string) bool {
return normalizeAugmentPath(left) == normalizeAugmentPath(right)
}
func sameAugmentAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameAugmentFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func managedAugmentAgentID(
handler map[string]any,
wrapperName, runtimeRoot string,
@@ -385,11 +331,11 @@ func managedAugmentAgentID(
return "", false
}
wrapperPath, ok := parseAugmentCommand(command)
if !ok || !sameAugmentFileName(filepath.Base(wrapperPath), wrapperName) {
if !ok || !sameManagedName(filepath.Base(wrapperPath), wrapperName) {
return "", false
}
agentDirectory := filepath.Dir(wrapperPath)
if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) {
if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false
}
agentID := filepath.Base(agentDirectory)
@@ -407,7 +353,7 @@ func removeManagedAugmentGroups(
groupChanged := false
for _, handler := range group.handlers {
managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
remove := managed && (agentID == "" || sameAugmentAgentID(managedID, agentID))
remove := managed && (agentID == "" || sameManagedAgentID(managedID, agentID))
if remove {
changed = true
groupChanged = true
+38 -158
View File
@@ -1,96 +1,42 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
import "fmt"
type augmentRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
managedRuntimePaths
guardWrapperPath string
auditWrapperPath string
}
func validateAugmentInstallConfig(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)
}
func augmentWrapperArtifacts(agentDirectory string) []managedArtifact {
wrappers := resolveAugmentWrapperPaths(agentDirectory)
return []managedArtifact{
{wrappers.guard, "Auggie guard wrapper", maxManagedSourceBytes},
{wrappers.audit, "Auggie audit wrapper", maxManagedSourceBytes},
}
if config.AgentName != augmentAgentKey {
return fmt.Errorf(
"Augment Code CLI installation requires agent name %s",
augmentAgentKey,
)
}
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 augmentAgentPaths(config InstallConfig) (*augmentRuntimePaths, error) {
if err := validateAugmentInstallConfig(config); err != nil {
if err := validateManagedInstallConfig(config, augmentAgentKey, "Augment Code CLI"); err != nil {
return nil, err
}
runtimeRoot, err := AgentRuntimeRoot()
paths, err := resolveManagedRuntimePaths(config, augmentGuardScript, augmentAuditScript)
if err != nil {
return nil, err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return nil, err
}
wrappers := resolveAugmentWrapperPaths(agentDirectory)
paths := &augmentRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, augmentGuardScript),
auditPath: filepath.Join(agentDirectory, augmentAuditScript),
guardWrapperPath: wrappers.guard,
auditWrapperPath: wrappers.audit,
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameAugmentPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameAugmentPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
paths.auditPath,
)
}
return paths, nil
wrappers := resolveAugmentWrapperPaths(paths.agentDirectory)
return &augmentRuntimePaths{
managedRuntimePaths: *paths,
guardWrapperPath: wrappers.guard,
auditWrapperPath: wrappers.audit,
}, nil
}
func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error {
return validateManagedRuntimeIdentity(
agentDirectory, agentID, augmentAgentKey, "Auggie",
augmentWrapperArtifacts(agentDirectory)...,
)
}
func preflightAugmentInstallation(
@@ -98,55 +44,25 @@ func preflightAugmentInstallation(
document *augmentDocument,
) (*augmentRuntimePaths, string, error) {
if document == nil || document.configPath == "" {
return nil, "", fmt.Errorf(
"Augment Code CLI installation requires a user settings document",
)
return nil, "", fmt.Errorf("Augment Code CLI installation requires a user settings document")
}
paths, err := augmentAgentPaths(config)
if err != nil {
return nil, "", err
}
if err := validateAugmentRuntimeIdentity(
paths.agentDirectory,
config.AgentID,
); err != nil {
if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
nodePath, err := resolveAbsoluteNodeRuntime("Auggie")
if err != nil {
return nil, "", err
}
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
return nil, "", err
}
if err := validateAugmentMatchers(document.hooks, nodePath); err != nil {
return nil, "", err
}
return paths, nodePath, nil
}
func buildAugmentRuntimeConfig(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": augmentAgentKey,
}
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 prepareAugmentInstallationChanges(
config InstallConfig,
paths *augmentRuntimePaths,
@@ -156,63 +72,27 @@ func prepareAugmentInstallationChanges(
if paths == nil || rendered == nil {
return nil, fmt.Errorf("prepared Auggie installation is required")
}
if err := validateAugmentRuntimeIdentity(
paths.agentDirectory,
config.AgentID,
); err != nil {
if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
return nil, err
}
runtimeConfig, err := buildAugmentRuntimeConfig(config)
changes, err := managedRuntimeFileChanges(
config,
&paths.managedRuntimePaths,
augmentAgentKey,
generateGuardScript(augmentAgentKey, config.AgentID, "", false, ""),
buildHookScriptWithOutput(augmentAgentKey, config.AgentID, "", false, true),
)
if err != nil {
return nil, err
}
items := []struct {
for _, item := range []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath,
"Elydora guard runtime",
[]byte(generateGuardScript(
augmentAgentKey,
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(
augmentAgentKey,
config.AgentID,
"",
false,
true,
)),
0700,
},
{
paths.guardWrapperPath,
"Auggie guard wrapper",
buildAugmentWrapper(nodePath, paths.guardPath),
0700,
},
{
paths.auditWrapperPath,
"Auggie audit wrapper",
buildAugmentWrapper(nodePath, paths.auditPath),
0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
for _, item := range items {
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
{paths.guardWrapperPath, "Auggie guard wrapper", buildAugmentWrapper(nodePath, paths.guardPath)},
{paths.auditWrapperPath, "Auggie audit wrapper", buildAugmentWrapper(nodePath, paths.auditPath)},
} {
change, err := prepareFileChange(item.path, item.label, item.content, 0700)
if err != nil {
return nil, err
}
-44
View File
@@ -83,43 +83,6 @@ func prepareRenderedAugmentChange(
)
}
func writeAugmentChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot string,
agentDirectory string,
settingsDirectory 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
}
}
if settingsDirectory != "" {
if err := ensureManagedDirectory(
settingsDirectory,
"Auggie configuration directory",
); err != nil {
return err
}
}
return writeChanges(changes, label, rename)
}
func validateAugmentMatchers(hooks augmentHooks, nodePath string) error {
events := make([]string, 0, len(hooks))
for event := range hooks {
@@ -209,10 +172,3 @@ func resolveAugmentWrapperPaths(agentDirectory string) augmentWrapperPaths {
audit: filepath.Join(agentDirectory, augmentAuditWrapperName()),
}
}
func requireAugmentAbsoluteNode(nodePath string) error {
if !filepath.IsAbs(nodePath) || !isClaudeNodeExecutable(nodePath) {
return fmt.Errorf("Auggie hooks require an absolute Node.js executable path")
}
return nil
}
+29 -284
View File
@@ -2,11 +2,8 @@ package plugins
import (
"bytes"
"fmt"
"path/filepath"
"runtime"
"sort"
"strings"
)
func managedAugmentIDs(
@@ -17,19 +14,10 @@ func managedAugmentIDs(
result := map[string]string{}
for _, group := range groups {
for _, handler := range group.handlers {
agentID, managed := managedAugmentAgentID(
handler,
wrapperName,
runtimeRoot,
)
if !managed {
continue
agentID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
if managed {
result[managedReferenceKey(agentID)] = agentID
}
key := agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(key)
}
result[key] = agentID
}
}
return result
@@ -39,16 +27,8 @@ func augmentRuntimeContracts(
hooks augmentHooks,
runtimeRoot string,
) []augmentRuntimeContract {
guards := managedAugmentIDs(
hooks["PreToolUse"],
augmentGuardWrapperName(),
runtimeRoot,
)
audits := managedAugmentIDs(
hooks["PostToolUse"],
augmentAuditWrapperName(),
runtimeRoot,
)
guards := managedAugmentIDs(hooks["PreToolUse"], augmentGuardWrapperName(), runtimeRoot)
audits := managedAugmentIDs(hooks["PostToolUse"], augmentAuditWrapperName(), runtimeRoot)
keys := make([]string, 0, len(guards))
for key := range guards {
if _, exists := audits[key]; exists {
@@ -72,282 +52,47 @@ func augmentRuntimeContracts(
return contracts
}
func readAugmentRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(
path,
"Elydora runtime config",
maxRuntimeConfigBytes,
)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
func augmentWrappersMatch(contract augmentRuntimeContract, nodePath string) (bool, error) {
wrappers := resolveAugmentWrapperPaths(filepath.Dir(contract.guardPath))
if !sameManagedPath(contract.guardWrapper, wrappers.guard) ||
!sameManagedPath(contract.auditWrapper, wrappers.audit) {
return false, nil
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireAugmentRuntimeString(
config map[string]any,
field string,
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 validateAugmentRuntimeConfig(
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 := requireAugmentRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireAugmentRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireAugmentRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameAugmentAgentID(agentID, expectedAgentID) ||
config["agent_name"] != augmentAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Auggie hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireAugmentRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireAugmentRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readAugmentRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
path, label string
expected []byte
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, augmentGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, augmentAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, augmentGuardWrapperName()), "Auggie guard wrapper", maxManagedSourceBytes},
{filepath.Join(agentDirectory, augmentAuditWrapperName()), "Auggie audit wrapper", 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},
{contract.guardWrapper, "Auggie guard wrapper", buildAugmentWrapper(nodePath, contract.guardPath)},
{contract.auditWrapper, "Auggie audit wrapper", buildAugmentWrapper(nodePath, contract.auditPath)},
} {
exists, inspectErr := managedPhysicalFileExists(
item.path,
item.label,
item.limit,
)
if inspectErr != nil {
return inspectErr
snapshot, err := readManagedFile(item.path, item.label, maxManagedSourceBytes)
if err != nil || snapshot == nil || !bytes.Equal(snapshot.contents, item.expected) {
return false, err
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != augmentAgentKey ||
!sameAugmentAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Auggie agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func augmentRuntimeContractExists(
contract augmentRuntimeContract,
runtimeRoot string,
nodePath string,
) (bool, error) {
agentDirectory := filepath.Dir(contract.guardPath)
wrappers := resolveAugmentWrapperPaths(agentDirectory)
if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameAugmentPath(
contract.auditPath,
filepath.Join(agentDirectory, augmentAuditScript),
) ||
!sameAugmentPath(contract.guardWrapper, wrappers.guard) ||
!sameAugmentPath(contract.auditWrapper, wrappers.audit) {
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 := readAugmentRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
guardWrapper, err := readManagedFile(
contract.guardWrapper,
"Auggie guard wrapper",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
auditWrapper, err := readManagedFile(
contract.auditWrapper,
"Auggie audit wrapper",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil ||
guardWrapper == nil || auditWrapper == nil {
return false, nil
}
if err := validateAugmentRuntimeConfig(
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 &&
bytes.Equal(
guardWrapper.contents,
buildAugmentWrapper(nodePath, contract.guardPath),
) &&
bytes.Equal(
auditWrapper.contents,
buildAugmentWrapper(nodePath, contract.auditPath),
), nil
return true, nil
}
func augmentRuntimeFilesExist(
contracts []augmentRuntimeContract,
runtimeRoot string,
) (bool, error) {
nodePath, err := resolveNodeRuntime()
nodePath, err := resolveAbsoluteNodeRuntime("Auggie")
if err != nil {
return false, err
}
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
return false, err
}
for _, contract := range contracts {
exists, err := augmentRuntimeContractExists(contract, runtimeRoot, nodePath)
if err != nil {
return false, err
runtime := managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath}
exists, err := managedRuntimeContractExists(runtime, augmentAgentKey, "Auggie", nil)
if err != nil || !exists {
if err != nil {
return false, err
}
continue
}
if exists {
return true, nil
matches, err := augmentWrappersMatch(contract, nodePath)
if err != nil || matches {
return matches, err
}
}
return false, nil
@@ -54,7 +54,7 @@ func TestAugmentInstallRollsBackAllSevenFiles(t *testing.T) {
)
failed := false
fixture.plugin.rename = func(source, destination string) error {
if sameAugmentPath(destination, fixture.configPath) &&
if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") &&
!failed {
failed = true
@@ -87,13 +87,14 @@ func TestPreparedAugmentInstallRejectsConcurrentSettingsChange(t *testing.T) {
if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent settings: %v", err)
}
err := writeAugmentChanges(
err := writeManagedChanges(
changes,
"Install Augment Code CLI hooks",
nil,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.configPath),
"Auggie configuration directory",
)
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err)
@@ -375,7 +376,7 @@ func TestAugmentUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed settings: %v", err)
}
fixture.plugin.rename = func(source, destination string) error {
if sameAugmentPath(destination, fixture.configPath) &&
if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") {
return errors.New("injected Auggie uninstall failure")
}
+5 -4
View File
@@ -10,8 +10,7 @@ type ClaudeCodePlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Claude Code commits its provider guard with
// the audit runtime and user settings document.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *ClaudeCodePlugin) ManagesGuardRuntime() bool {
return true
}
@@ -73,13 +72,14 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
if err := writeClaudeChanges(
if err := writeManagedChanges(
changes,
"Install Claude Code hooks",
p.rename,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.filePath),
"Claude Code configuration directory",
); err != nil {
return err
}
@@ -105,13 +105,14 @@ func (p *ClaudeCodePlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
return writeClaudeChanges(
return writeManagedChanges(
[]*fileChange{change},
"Uninstall Claude Code hooks",
p.rename,
"",
"",
filepath.Dir(document.filePath),
"Claude Code configuration directory",
)
}
+5 -17
View File
@@ -77,19 +77,7 @@ type claudeRenderedDocument struct {
remove bool
}
type claudeRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
func cloneClaudeObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
type claudeRuntimeContract = managedRuntimeContract
func cloneClaudeHooks(source claudeHooks) claudeHooks {
clone := make(claudeHooks, len(source))
@@ -299,7 +287,7 @@ func validateClaudeHandler(
}
}
}
return cloneClaudeObject(handler), nil
return cloneJSONObject(handler), nil
}
func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup, error) {
@@ -332,7 +320,7 @@ func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup,
}
handlers = append(handlers, handler)
}
return claudeGroup{object: cloneClaudeObject(object), handlers: handlers}, nil
return claudeGroup{object: cloneJSONObject(object), handlers: handlers}, nil
}
func readClaudeHooks(root map[string]any) (claudeHooks, error) {
@@ -411,7 +399,7 @@ func renderClaudeHooks(hooks claudeHooks) map[string]any {
for event, groups := range hooks {
values := make([]any, 0, len(groups))
for _, group := range groups {
object := cloneClaudeObject(group.object)
object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers {
handlers = append(handlers, handler)
@@ -439,7 +427,7 @@ func renderClaudeDocument(
document: document, changed: true, remove: true,
}, nil
}
root := cloneClaudeObject(document.root)
root := cloneJSONObject(document.root)
if len(hooks) == 0 {
delete(root, "hooks")
} else {
+19 -153
View File
@@ -1,182 +1,48 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type claudeRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
func validateClaudeInstallConfig(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 != claudeAgentKey {
return fmt.Errorf(
"Claude Code installation requires agent name %s",
claudeAgentKey,
)
}
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 claudeAgentPaths(config InstallConfig) (*claudeRuntimePaths, error) {
if err := validateClaudeInstallConfig(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 := &claudeRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, claudeGuardScript),
auditPath: filepath.Join(agentDirectory, claudeAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameClaudePath(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) ||
!sameClaudePath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"audit runtime must use the managed Elydora agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
import "fmt"
func preflightClaudeInstallation(
config InstallConfig,
document *claudeDocument,
) (*claudeRuntimePaths, string, error) {
) (*managedRuntimePaths, string, error) {
if document == nil || document.filePath == "" {
return nil, "", fmt.Errorf(
"Claude Code installation requires a user settings document",
)
return nil, "", fmt.Errorf("Claude Code installation requires a user settings document")
}
paths, err := claudeAgentPaths(config)
if err := validateManagedInstallConfig(config, claudeAgentKey, "Claude Code"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, claudeGuardScript, claudeAuditScript)
if err != nil {
return nil, "", err
}
if err := validateClaudeRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, claudeAgentKey, "Claude Code",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
nodePath, err := resolveAbsoluteNodeRuntime("Claude Code")
if err != nil {
return nil, "", err
}
if err := requireClaudeAbsoluteNode(nodePath); err != nil {
return nil, "", err
}
return paths, nodePath, nil
}
func buildClaudeRuntimeConfig(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": claudeAgentKey,
}
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 prepareClaudeInstallationChanges(
config InstallConfig,
paths *claudeRuntimePaths,
paths *managedRuntimePaths,
rendered *claudeRenderedDocument,
) ([]*fileChange, error) {
runtimeConfig, err := buildClaudeRuntimeConfig(config)
changes, err := managedRuntimeFileChanges(
config,
paths,
claudeAgentKey,
generateGuardScript(claudeAgentKey, config.AgentID, "", false, ""),
buildHookScriptWithOutput(claudeAgentKey, config.AgentID, "", false, true),
)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(claudeAgentKey, 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(
claudeAgentKey,
config.AgentID,
"",
false,
true,
)),
0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
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)
}
documentChange, err := prepareRenderedClaudeChange(rendered)
if err != nil {
return nil, err
-35
View File
@@ -71,38 +71,3 @@ func prepareRenderedClaudeChange(
rendered.remove,
)
}
func writeClaudeChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot string,
agentDirectory string,
settingsDirectory 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
}
}
if err := ensureManagedDirectory(
settingsDirectory,
"Claude Code configuration directory",
); err != nil {
return err
}
return writeChanges(changes, label, rename)
}
+7 -76
View File
@@ -1,73 +1,18 @@
package plugins
import (
"fmt"
"path/filepath"
"runtime"
"sort"
"strings"
)
type claudeRuntimeReference struct {
agentID string
scriptPath string
}
func sameClaudePath(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 sameClaudeAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func isClaudeNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func sameClaudeFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
type claudeRuntimeReference = managedScriptReference
func claudeRuntimeReferenceForScript(
scriptPath string,
scriptName string,
) (*claudeRuntimeReference, error) {
if !filepath.IsAbs(scriptPath) ||
!sameClaudeFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameClaudePath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &claudeRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
return resolveManagedScript(scriptPath, scriptName)
}
func claudeLegacyReference(
@@ -95,7 +40,7 @@ func managedClaudeReference(
handler["statusMessage"] == statusMessage {
nodePath, nodeOK := handler["command"].(string)
args, argsOK := handler["args"].([]any)
if nodeOK && filepath.IsAbs(nodePath) && isClaudeNodeExecutable(nodePath) &&
if nodeOK && filepath.IsAbs(nodePath) && isNodeExecutable(nodePath) &&
argsOK && len(args) == 1 {
if scriptPath, ok := args[0].(string); ok {
return claudeRuntimeReferenceForScript(scriptPath, scriptName)
@@ -146,7 +91,7 @@ func removeManagedClaudeGroups(
return nil, false, err
}
owned := reference != nil &&
(agentID == "" || sameClaudeAgentID(reference.agentID, agentID))
(agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if owned {
removed = true
continue
@@ -154,7 +99,7 @@ func removeManagedClaudeGroups(
kept = append(kept, handler)
}
if len(kept) > 0 || !exactManagedClaudeGroup(group) {
object := cloneClaudeObject(group.object)
object := cloneJSONObject(group.object)
object["hooks"] = group.object["hooks"]
result = append(result, claudeGroup{object: object, handlers: kept})
}
@@ -235,13 +180,6 @@ func entirelyManagedClaudeDocument(document *claudeDocument) bool {
return handlerCount > 0
}
func claudeReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func claudeReferencesForEvent(
groups []claudeGroup,
scriptName string,
@@ -263,7 +201,7 @@ func claudeReferencesForEvent(
return nil, err
}
if reference != nil {
key := claudeReferenceKey(reference.agentID)
key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference)
}
}
@@ -307,7 +245,7 @@ func claudeRuntimeContracts(hooks claudeHooks) ([]claudeRuntimeContract, error)
success := successes[key]
failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 ||
!sameClaudePath(success[0].scriptPath, failure[0].scriptPath) {
!sameManagedPath(success[0].scriptPath, failure[0].scriptPath) {
continue
}
contracts = append(contracts, claudeRuntimeContract{
@@ -317,10 +255,3 @@ func claudeRuntimeContracts(hooks claudeHooks) ([]claudeRuntimeContract, error)
}
return contracts, nil
}
func requireClaudeAbsoluteNode(nodePath string) error {
if !filepath.IsAbs(nodePath) || !isClaudeNodeExecutable(nodePath) {
return fmt.Errorf("Claude Code hooks require an absolute Node.js executable path")
}
return nil
}
@@ -150,10 +150,10 @@ func TestClaudeValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) {
}, "query parameters"},
{"guard path", func(f *claudeFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"},
}, "managed agent directory"},
{"audit path", func(f *claudeFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js")
}, "managed Elydora agent directory"},
}, "managed agent directory"},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
+2 -228
View File
@@ -1,231 +1,5 @@
package plugins
import (
"fmt"
"path/filepath"
"sort"
"strings"
)
func readClaudeRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireClaudeRuntimeString(
config map[string]any,
field string,
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 validateClaudeRuntimeConfig(
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 := requireClaudeRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireClaudeRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireClaudeRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameClaudeAgentID(agentID, expectedAgentID) ||
config["agent_name"] != claudeAgentKey {
return fmt.Errorf(
"elydora runtime identity does not match Claude Code hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireClaudeRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireClaudeRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateClaudeRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readClaudeRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, claudeGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, claudeAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != claudeAgentKey ||
!sameClaudeAgentID(configuredID, agentID) {
return fmt.Errorf(
"elydora runtime config identity does not match Claude Code agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func claudeRuntimeContractExists(contract claudeRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
if !sameClaudePath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameClaudePath(
contract.auditPath,
filepath.Join(agentDirectory, claudeAuditScript),
) {
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 := readClaudeRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateClaudeRuntimeConfig(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 claudeRuntimeFilesExist(contracts []claudeRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := claudeRuntimeContractExists(contract)
if err != nil {
return false, err
}
if exists {
return true, nil
}
}
return false, nil
func claudeRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesExist(contracts, claudeAgentKey, "Claude Code", nil)
}
@@ -60,7 +60,7 @@ func TestClaudeInstallRollsBackAllRuntimesAfterSettingsFailure(t *testing.T) {
}
before := snapshotClaudeFiles(t, paths...)
fixture.plugin.rename = func(source, destination string) error {
if sameClaudePath(destination, fixture.configPath) &&
if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") {
return errors.New("injected Claude settings failure")
}
@@ -114,13 +114,14 @@ func TestPreparedClaudeInstallRejectsConcurrentSettingsChange(t *testing.T) {
if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent settings: %v", err)
}
err = writeClaudeChanges(
err = writeManagedChanges(
changes,
"Install Claude Code hooks",
nil,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.filePath),
"Claude Code configuration directory",
)
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err)
@@ -176,7 +177,7 @@ func TestClaudeUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed settings: %v", err)
}
fixture.plugin.rename = func(source, destination string) error {
if sameClaudePath(destination, fixture.configPath) &&
if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") {
return errors.New("injected Claude uninstall failure")
}
+1 -2
View File
@@ -7,8 +7,7 @@ type ClinePlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Cline commits both generated runtimes and
// both native hook files in one transaction.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *ClinePlugin) ManagesGuardRuntime() bool {
return true
}
+3 -42
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
@@ -80,37 +79,6 @@ func resolveClineHookFiles() (clineHookPaths, error) {
}, nil
}
func sameClineAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func normalizeClinePath(value string) (string, error) {
absolute, err := filepath.Abs(value)
if err != nil {
return "", err
}
normalized := filepath.Clean(absolute)
if runtime.GOOS == "windows" {
normalized = strings.ToLower(normalized)
}
return normalized, nil
}
func sameClinePath(left, right string) (bool, error) {
normalizedLeft, err := normalizeClinePath(left)
if err != nil {
return false, err
}
normalizedRight, err := normalizeClinePath(right)
if err != nil {
return false, err
}
return normalizedLeft == normalizedRight, nil
}
func buildClineMetadata(kind, agentID, runtimePath string) (clineHookMetadata, error) {
metadata := clineHookMetadata{
Version: clineMetadataVersion,
@@ -314,7 +282,7 @@ func clineContractForFiles(
if guard.Kind != "guard" || audit.Kind != "audit" {
return nil, fmt.Errorf("Elydora Cline hook files contain mismatched event metadata")
}
if !sameClineAgentID(guard.AgentID, audit.AgentID) {
if !sameManagedAgentID(guard.AgentID, audit.AgentID) {
return nil, fmt.Errorf("Elydora Cline hook files reference different agents")
}
agentDirectory, err := ResolveAgentRuntimeDirectory(guard.AgentID)
@@ -323,15 +291,8 @@ func clineContractForFiles(
}
expectedGuard := filepath.Join(agentDirectory, clineGuardScript)
expectedAudit := filepath.Join(agentDirectory, clineAuditScript)
guardMatches, err := sameClinePath(guard.RuntimePath, expectedGuard)
if err != nil {
return nil, fmt.Errorf("compare Cline guard runtime path: %w", err)
}
auditMatches, err := sameClinePath(audit.RuntimePath, expectedAudit)
if err != nil {
return nil, fmt.Errorf("compare Cline audit runtime path: %w", err)
}
if !guardMatches || !auditMatches {
if !sameManagedPath(guard.RuntimePath, expectedGuard) ||
!sameManagedPath(audit.RuntimePath, expectedAudit) {
return nil, fmt.Errorf("Elydora Cline hook metadata references an unexpected runtime path")
}
return &clineRuntimeContract{
+23 -131
View File
@@ -1,97 +1,21 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
import "fmt"
type clineRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
hooks clineHookPaths
managedRuntimePaths
hooks clineHookPaths
}
func validateClineInstallConfig(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 != clineAgentKey {
return fmt.Errorf("Cline installation requires agent name %s", clineAgentKey)
}
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 clineAgentPaths(
config InstallConfig,
hooks clineHookPaths,
) (*clineRuntimePaths, error) {
if err := validateClineInstallConfig(config); err != nil {
func clineAgentPaths(config InstallConfig, hooks clineHookPaths) (*clineRuntimePaths, error) {
if err := validateManagedInstallConfig(config, clineAgentKey, "Cline"); err != nil {
return nil, err
}
runtimeRoot, err := AgentRuntimeRoot()
paths, err := resolveManagedRuntimePaths(config, clineGuardScript, clineAuditScript)
if err != nil {
return nil, err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return nil, err
}
paths := &clineRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory, hooks: hooks,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, clineGuardScript),
auditPath: filepath.Join(agentDirectory, clineAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameClinePathValue(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameClinePathValue(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
func sameClinePathValue(left, right string) bool {
matches, err := sameClinePath(left, right)
return err == nil && matches
return &clineRuntimePaths{managedRuntimePaths: *paths, hooks: hooks}, nil
}
func preflightClineInstallation(
@@ -102,39 +26,20 @@ func preflightClineInstallation(
if err != nil {
return nil, err
}
if err := validateClineRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, clineAgentKey, "Cline",
); err != nil {
return nil, err
}
nodePath, err := resolveNodeRuntime()
if err != nil {
if _, err := resolveAbsoluteNodeRuntime("Cline"); err != nil {
return nil, err
}
if !filepath.IsAbs(nodePath) || !isClaudeNodeExecutable(nodePath) {
return nil, fmt.Errorf("Cline hooks require an absolute Node.js executable path")
}
return paths, nil
}
func buildClineRuntimeConfig(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": clineAgentKey,
}
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 clineExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(clineAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(clineAgentKey, agentID, "", false, true))
}
func prepareClineInstallationChanges(
@@ -146,11 +51,9 @@ func prepareClineInstallationChanges(
if paths == nil {
return nil, fmt.Errorf("prepared Cline installation is required")
}
if err := validateClineRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
return nil, err
}
runtimeConfig, err := buildClineRuntimeConfig(config)
if err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, clineAgentKey, "Cline",
); err != nil {
return nil, err
}
guardMetadata, err := buildClineMetadata("guard", config.AgentID, paths.guardPath)
@@ -175,23 +78,12 @@ func prepareClineInstallationChanges(
); err != nil {
return nil, err
}
runtimeItems := []struct {
path, label string
content []byte
mode os.FileMode
}{
{paths.guardPath, "Elydora guard runtime", []byte(generateGuardScript(clineAgentKey, 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(clineAgentKey, config.AgentID, "", false, true)), 0700},
}
changes := make([]*fileChange, 0, len(runtimeItems)+2)
for _, item := range runtimeItems {
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
if err != nil {
return nil, err
}
changes = append(changes, change)
guardScript, auditScript := clineExpectedScripts(config.AgentID, nil)
changes, err := managedRuntimeFileChanges(
config, &paths.managedRuntimePaths, clineAgentKey, string(guardScript), string(auditScript),
)
if err != nil {
return nil, err
}
for _, item := range []struct {
file clineHookFile
+3 -12
View File
@@ -74,7 +74,7 @@ func writeClineHookChanges(
rename renameFunc,
paths clineHookPaths,
) error {
if !hasClineChanges(changes) {
if !hasFileChanges(changes) {
return nil
}
if err := ensureManagedDirectory(
@@ -95,7 +95,7 @@ func writeClineChanges(
rename renameFunc,
paths *clineRuntimePaths,
) error {
if !hasClineChanges(changes) {
if !hasFileChanges(changes) {
return nil
}
if err := EnsurePrivateDirectory(paths.runtimeRoot); err != nil {
@@ -107,15 +107,6 @@ func writeClineChanges(
return writeClineHookChanges(changes, label, rename, paths.hooks)
}
func hasClineChanges(changes []*fileChange) bool {
for _, change := range changes {
if change != nil {
return true
}
}
return false
}
func prepareClineUninstallChanges(
files []clineHookFile,
agentID string,
@@ -123,7 +114,7 @@ func prepareClineUninstallChanges(
changes := make([]*fileChange, 0, len(files))
for _, file := range files {
if file.metadata == nil ||
(agentID != "" && !sameClineAgentID(file.metadata.AgentID, agentID)) {
(agentID != "" && !sameManagedAgentID(file.metadata.AgentID, agentID)) {
continue
}
if err := assertClineWrapperIntegrity(file); err != nil {
+7 -198
View File
@@ -1,206 +1,15 @@
package plugins
import (
"bytes"
"fmt"
"path/filepath"
"sort"
"strings"
)
func readClineRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireClineRuntimeString(
config map[string]any,
field string,
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 validateClineRuntimeConfig(
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 := requireClineRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireClineRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireClineRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameClineAgentID(agentID, expectedAgentID) || config["agent_name"] != clineAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Cline hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireClineRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireClineRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateClineRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readClineRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, clineGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, clineAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != clineAgentKey ||
!sameClineAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Cline agent %s: %s",
agentID,
configPath,
)
}
return nil
}
import "path/filepath"
func clineRuntimeFilesExist(contract *clineRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
if !sameClinePathValue(filepath.Dir(contract.agentDirectory), runtimeRoot) ||
!sameClinePathValue(contract.guardPath, filepath.Join(contract.agentDirectory, clineGuardScript)) ||
!sameClinePathValue(contract.auditPath, filepath.Join(contract.agentDirectory, clineAuditScript)) {
if !sameManagedPath(contract.guardPath, filepath.Join(contract.agentDirectory, clineGuardScript)) {
return false, nil
}
rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory")
if err != nil || !rootExists {
return false, err
}
directoryExists, err := managedPhysicalDirectoryExists(
contract.agentDirectory,
"Elydora agent runtime directory",
return managedRuntimeContractExists(
managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath},
clineAgentKey,
"Cline",
clineExpectedScripts,
)
if err != nil || !directoryExists {
return false, err
}
configPath := filepath.Join(contract.agentDirectory, "config.json")
keyPath := filepath.Join(contract.agentDirectory, "private.key")
config, configExists, err := readClineRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
audit, err := readManagedFile(contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateClineRuntimeConfig(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)
}
expectedGuard := []byte(generateGuardScript(clineAgentKey, contract.agentID, "", false, ""))
expectedAudit := []byte(buildHookScriptWithOutput(clineAgentKey, contract.agentID, "", false, true))
return bytes.Equal(guard.contents, expectedGuard) && bytes.Equal(audit.contents, expectedAudit), nil
}
+6 -6
View File
@@ -10,14 +10,12 @@ type CodexPlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Codex commits its provider guard with the
// audit runtime and user hook document.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *CodexPlugin) ManagesGuardRuntime() bool {
return true
}
// PreflightInstall validates every existing source before the CLI creates
// runtime state.
// PreflightInstall validates every source before the CLI creates runtime state.
func (p *CodexPlugin) PreflightInstall(config InstallConfig) error {
document, err := readCodexDocument()
if err != nil {
@@ -61,13 +59,14 @@ func (p *CodexPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
if err := writeCodexChanges(
if err := writeManagedChanges(
changes,
"Install Codex hooks",
p.rename,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.filePath),
"Codex hooks directory",
); err != nil {
return err
}
@@ -90,13 +89,14 @@ func (p *CodexPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
return writeCodexChanges(
return writeManagedChanges(
[]*fileChange{change},
"Uninstall Codex hooks",
p.rename,
"",
"",
filepath.Dir(document.filePath),
"Codex hooks directory",
)
}
+16 -176
View File
@@ -1,53 +1,12 @@
package plugins
import (
"encoding/base64"
"encoding/binary"
"os"
"path/filepath"
"runtime"
"strings"
"unicode/utf16"
)
func quoteCodexPowerShell(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func codexPowerShellPath() string {
root := "C:\\Windows"
if runtime.GOOS == "windows" {
configured := os.Getenv("SystemRoot")
if filepath.IsAbs(configured) && !strings.ContainsAny(configured, "\"%\r\n") {
root = configured
}
}
return strings.TrimRight(root, `\/`) +
`\System32\WindowsPowerShell\v1.0\powershell.exe`
}
func encodeCodexPowerShell(source string) string {
runes := utf16.Encode([]rune(source))
raw := make([]byte, len(runes)*2)
for index, value := range runes {
binary.LittleEndian.PutUint16(raw[index*2:], value)
}
return base64.StdEncoding.EncodeToString(raw)
}
func codexWindowsCommand(runtimePath, scriptPath string) string {
source := "& " + quoteCodexPowerShell(runtimePath) + " " +
quoteCodexPowerShell(scriptPath) + "; exit $LASTEXITCODE"
return `"` + codexPowerShellPath() +
`" -NoLogo -NoProfile -NonInteractive -EncodedCommand ` +
encodeCodexPowerShell(source)
}
import "path/filepath"
func codexHandler(runtimePath, scriptPath, statusMessage string) map[string]any {
return map[string]any{
"type": "command",
"command": quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath),
"commandWindows": codexWindowsCommand(runtimePath, scriptPath),
"command": posixSource(runtimePath, scriptPath),
"commandWindows": encodedWindowsCommand(runtimePath, scriptPath),
"timeout": codexHookTimeout,
"statusMessage": statusMessage,
}
@@ -57,124 +16,6 @@ func codexMatcherGroup(handler map[string]any) map[string]any {
return map[string]any{"matcher": "*", "hooks": []any{handler}}
}
func readCodexPOSIXArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if strings.HasPrefix(command[index:], codexPOSIXApostrophe) {
value.WriteByte('\'')
index += len(codexPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func parseCodexPOSIXCommand(value any) (string, string, bool) {
command, ok := value.(string)
if !ok {
return "", "", false
}
runtimePath, next, ok := readCodexPOSIXArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
scriptPath, end, ok := readCodexPOSIXArgument(command, next+1)
return runtimePath, scriptPath, ok && end == len(command)
}
func readCodexPowerShellArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); index++ {
if command[index] != '\'' {
value.WriteByte(command[index])
continue
}
if index+1 < len(command) && command[index+1] == '\'' {
value.WriteByte('\'')
index++
continue
}
return value.String(), index + 1, true
}
return "", start, false
}
func parseCodexPowerShellSource(source string) (string, string, bool) {
if !strings.HasPrefix(source, "& ") {
return "", "", false
}
runtimePath, next, ok := readCodexPowerShellArgument(source, 2)
if !ok || next >= len(source) || source[next] != ' ' {
return "", "", false
}
scriptPath, end, ok := readCodexPowerShellArgument(source, next+1)
return runtimePath, scriptPath, ok && source[end:] == "; exit $LASTEXITCODE"
}
func windowsPathBase(value string) string {
index := strings.LastIndexAny(value, `\/`)
return value[index+1:]
}
func isAbsoluteWindowsPath(value string) bool {
return len(value) >= 3 && ((value[0] >= 'A' && value[0] <= 'Z') ||
(value[0] >= 'a' && value[0] <= 'z')) && value[1] == ':' &&
(value[2] == '\\' || value[2] == '/') || strings.HasPrefix(value, `\\`)
}
func decodeCodexPowerShell(encoded string) (string, bool) {
raw, err := base64.StdEncoding.DecodeString(encoded)
if err != nil || base64.StdEncoding.EncodeToString(raw) != encoded || len(raw)%2 != 0 {
return "", false
}
units := make([]uint16, len(raw)/2)
for index := range units {
units[index] = uint16(raw[index*2]) | uint16(raw[index*2+1])<<8
}
return string(utf16.Decode(units)), true
}
func parseCodexWindowsCommand(value any) (string, string, bool) {
command, ok := value.(string)
if !ok || !strings.HasPrefix(command, `"`) {
return "", "", false
}
closing := strings.IndexByte(command[1:], '"')
if closing < 0 {
return "", "", false
}
closing++
powerShell := command[1:closing]
prefix := `" -NoLogo -NoProfile -NonInteractive -EncodedCommand `
if !isAbsoluteWindowsPath(powerShell) ||
!strings.EqualFold(windowsPathBase(powerShell), "powershell.exe") ||
!strings.HasPrefix(command[closing:], prefix) {
return "", "", false
}
encoded := command[closing+len(prefix):]
source, ok := decodeCodexPowerShell(encoded)
if !ok {
return "", "", false
}
return parseCodexPowerShellSource(source)
}
func isCodexNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func exactCodexHandlerKeys(handler map[string]any) bool {
if len(handler) != 5 {
return false
@@ -192,20 +33,21 @@ func codexManagedScriptPath(handler map[string]any, status string) (string, bool
handler["timeout"] != codexHookTimeout || handler["statusMessage"] != status {
return "", false
}
posixRuntime, posixScript, posixOK := parseCodexPOSIXCommand(handler["command"])
windowsRuntime, windowsScript, windowsOK := parseCodexWindowsCommand(handler["commandWindows"])
command, _ := handler["command"].(string)
commandWindows, _ := handler["commandWindows"].(string)
posixRuntime, posixScript, posixOK := parsePOSIXCommand(command)
windowsRuntime, windowsScript, windowsOK := parseEncodedWindowsCommand(commandWindows)
if !windowsOK && posixOK {
legacy, ok := handler["commandWindows"].(string)
expected := quoteWindowsArgument(posixRuntime) + " " + quoteWindowsArgument(posixScript)
if ok && legacy == expected {
if commandWindows == expected {
windowsRuntime, windowsScript, windowsOK = posixRuntime, posixScript, true
}
}
if !posixOK || !windowsOK || !filepath.IsAbs(posixRuntime) ||
!filepath.IsAbs(posixScript) || !isCodexNodeExecutable(posixRuntime) ||
!isCodexNodeExecutable(windowsRuntime) ||
!sameCodexPath(posixRuntime, windowsRuntime) ||
!sameCodexPath(posixScript, windowsScript) {
!filepath.IsAbs(posixScript) || !isNodeExecutable(posixRuntime) ||
!isNodeExecutable(windowsRuntime) ||
!sameManagedPath(posixRuntime, windowsRuntime) ||
!sameManagedPath(posixScript, windowsScript) {
return "", false
}
return posixScript, true
@@ -217,14 +59,12 @@ func codexManagedAgentID(
status string,
) (string, bool) {
scriptPath, managed := codexManagedScriptPath(handler, status)
if !managed || filepath.Base(scriptPath) != scriptName {
if !managed {
return "", false
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil || !sameCodexPath(filepath.Dir(agentDirectory), runtimeRoot) {
reference, err := resolveManagedScript(scriptPath, scriptName)
if err != nil || reference == nil {
return "", false
}
agentID := filepath.Base(agentDirectory)
return agentID, agentID != "" && agentID != "." && agentID != ".."
return reference.agentID, true
}
+5 -56
View File
@@ -6,9 +6,7 @@ import (
"fmt"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
)
const (
@@ -20,7 +18,6 @@ const (
codexOwnedDescription = "Elydora audit and freeze enforcement"
codexGuardStatusMessage = "Checking Elydora agent state"
codexAuditStatusMessage = "Recording Elydora tool use"
codexPOSIXApostrophe = `'"'"'`
)
type codexHooks map[string][]map[string]any
@@ -40,19 +37,7 @@ type codexRenderedDocument struct {
remove bool
}
type codexRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
func cloneCodexObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
type codexRuntimeContract = managedRuntimeContract
func cloneCodexHooks(source codexHooks) codexHooks {
clone := make(codexHooks, len(source))
@@ -62,28 +47,6 @@ func cloneCodexHooks(source codexHooks) codexHooks {
return clone
}
func sameCodexPath(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 sameCodexAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func readCodexHooks(value any, label string) (codexHooks, error) {
object, ok := value.(map[string]any)
if !ok {
@@ -126,16 +89,6 @@ func readCodexHooks(value any, label string) (codexHooks, error) {
return hooks, nil
}
func decodeStrictJSONObject(source []byte, label string) (map[string]any, error) {
if !json.Valid(source) {
var value any
if err := json.Unmarshal(source, &value); err != nil {
return nil, fmt.Errorf("parse %s: %w", label, err)
}
}
return decodeJSONCObject(source, label, false)
}
func parseCodexDocument(filePath string, raw []byte) (*codexDocument, error) {
label := fmt.Sprintf("Codex user hooks at %s", filePath)
root, err := decodeStrictJSONObject(raw, label)
@@ -180,13 +133,13 @@ func removeCodexGroups(
for _, value := range handlers {
handler := value.(map[string]any)
managedID, managed := codexManagedAgentID(handler, scriptName, status)
if managed && (agentID == "" || sameCodexAgentID(managedID, agentID)) {
if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) {
continue
}
kept = append(kept, handler)
}
if len(kept) > 0 || !exactCodexMatcherGroup(group) {
next := cloneCodexObject(group)
next := cloneJSONObject(group)
next["hooks"] = kept
result = append(result, next)
}
@@ -262,7 +215,7 @@ func renderCodexDocument(
if len(hooks) == 0 && entirelyManagedCodexDocument(document) {
return &codexRenderedDocument{document: document, changed: true, remove: true}, nil
}
root := cloneCodexObject(document.root)
root := cloneJSONObject(document.root)
if len(hooks) == 0 {
delete(root, "hooks")
} else {
@@ -295,11 +248,7 @@ func managedCodexIDs(
if !managed {
continue
}
key := agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(agentID)
}
result[key] = agentID
result[managedReferenceKey(agentID)] = agentID
}
}
return result
+18 -129
View File
@@ -1,94 +1,26 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
type codexRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
func validateCodexInstallConfig(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 != codexAgentKey {
return fmt.Errorf("codex installation requires agent name %s", codexAgentKey)
}
if err := validateManagedPrivateKey(config.PrivateKey); err != nil {
return err
}
return validateManagedBaseURL(config.BaseURL)
}
func codexAgentPaths(config InstallConfig) (*codexRuntimePaths, error) {
if err := validateCodexInstallConfig(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 := &codexRuntimePaths{
runtimeRoot: runtimeRoot,
agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, codexGuardScript),
auditPath: filepath.Join(agentDirectory, codexAuditScript),
}
if !sameCodexPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"guard runtime must use the managed Elydora agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && !sameCodexPath(config.HookScript, paths.auditPath) {
return nil, fmt.Errorf(
"audit runtime must use the managed Elydora agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
import "path/filepath"
func preflightCodexInstallation(
config InstallConfig,
hooksPath string,
) (*codexRuntimePaths, string, error) {
paths, err := codexAgentPaths(config)
) (*managedRuntimePaths, string, error) {
if err := validateManagedInstallConfig(config, codexAgentKey, "Codex"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, codexGuardScript, codexAuditScript)
if err != nil {
return nil, "", err
}
if _, err := managedPhysicalDirectoryExists(
filepath.Dir(hooksPath),
"Codex hooks directory",
filepath.Dir(hooksPath), "Codex hooks directory",
); err != nil {
return nil, "", err
}
if err := validateCodexRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, codexAgentKey, "Codex",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
@@ -98,64 +30,21 @@ func preflightCodexInstallation(
return paths, nodePath, nil
}
func buildCodexRuntimeConfig(config InstallConfig) ([]byte, error) {
encoded, err := json.MarshalIndent(agentRuntimeConfig{
OrgID: config.OrgID, AgentID: config.AgentID, KID: config.KID,
BaseURL: config.BaseURL, Token: config.Token, AgentName: codexAgentKey,
}, "", " ")
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 prepareCodexInstallationChanges(
config InstallConfig,
paths *codexRuntimePaths,
paths *managedRuntimePaths,
rendered *codexRenderedDocument,
) ([]*fileChange, error) {
runtimeConfig, err := buildCodexRuntimeConfig(config)
changes, err := managedRuntimeFileChanges(
config,
paths,
codexAgentKey,
generateGuardScript(codexAgentKey, config.AgentID, "", false, ""),
buildHookScriptWithOutput(codexAgentKey, config.AgentID, "", false, true),
)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(codexAgentKey, 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(
codexAgentKey, config.AgentID, "", false, true,
)), 0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
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)
}
documentChange, err := prepareRenderedCodexChange(rendered)
if err != nil {
return nil, err
+2 -148
View File
@@ -82,152 +82,6 @@ func prepareRenderedCodexChange(rendered *codexRenderedDocument) (*fileChange, e
)
}
func writeCodexChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot string,
agentDirectory string,
hooksDirectory 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
}
}
if err := ensureManagedDirectory(hooksDirectory, "Codex hooks directory"); err != nil {
return err
}
return writeChanges(changes, label, rename)
}
func readCodexRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func validateCodexRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readCodexRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, codexGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, codexAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != codexAgentKey ||
!sameCodexAgentID(configuredID, agentID) {
return fmt.Errorf(
"elydora runtime config identity does not match Codex agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func codexRuntimeFilesExist(contracts []codexRuntimeContract) (bool, error) {
for _, contract := range contracts {
agentDirectory := filepath.Dir(contract.guardPath)
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil {
return false, err
}
if !directoryExists {
continue
}
config, exists, err := readCodexRuntimeConfig(
filepath.Join(agentDirectory, "config.json"),
)
if err != nil {
return false, err
}
configuredID, idOK := config["agent_id"].(string)
if !exists || !idOK || config["agent_name"] != codexAgentKey ||
!sameCodexAgentID(configuredID, contract.agentID) {
continue
}
complete := true
for _, item := range []struct {
path string
label string
limit int64
}{
{contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes},
{contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return false, inspectErr
}
complete = complete && exists
}
if complete {
return true, nil
}
}
return false, nil
func codexRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesPresent(contracts, codexAgentKey)
}
+1 -1
View File
@@ -249,7 +249,7 @@ func TestCodexInstallRollsBackAllFilesOnHooksCommitFailure(t *testing.T) {
source := []byte(`{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"keep"}]}]}}`)
fixture := prepareCodexFixture(t, string(source))
fixture.plugin.rename = func(source, destination string) error {
if sameCodexPath(destination, fixture.configPath) {
if sameManagedPath(destination, fixture.configPath) {
return errors.New("injected Codex hooks commit failure")
}
return os.Rename(source, destination)
+3 -3
View File
@@ -355,10 +355,10 @@ func TestCodexUninstallPreservesUnrelatedHooksAndExactAgent(t *testing.T) {
if len(requireArray(t, hooks["PreToolUse"])) != 2 || len(requireArray(t, hooks["PostToolUse"])) != 1 {
t.Fatalf("unexpected remaining hooks: %#v", hooks)
}
_, remainingScript, parsed := parseCodexPOSIXCommand(
requireCodexHandler(t, remaining, "PostToolUse", codexAuditStatus)["command"],
_, remainingScript, parsed := parsePOSIXCommand(
requireCodexHandler(t, remaining, "PostToolUse", codexAuditStatus)["command"].(string),
)
if !parsed || !sameCodexPath(remainingScript, filepath.Join(otherDir, codexAuditScript)) {
if !parsed || !sameManagedPath(remainingScript, filepath.Join(otherDir, codexAuditScript)) {
t.Fatalf("other agent handler was removed: %#v", hooks)
}
}
+3 -5
View File
@@ -10,14 +10,12 @@ type CopilotPlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Copilot commits its provider guard with the
// audit runtime and hook documents.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *CopilotPlugin) ManagesGuardRuntime() bool {
return true
}
// PreflightInstall validates hook sources, settings, matchers, credentials,
// and runtime identity before any write.
// PreflightInstall validates every source before the CLI creates runtime state.
func (p *CopilotPlugin) PreflightInstall(config InstallConfig) error {
sources, _, err := readCopilotSources()
if err != nil {
@@ -165,7 +163,7 @@ func mergedCopilotContracts(
}
unique := map[string]copilotRuntimeContract{}
for _, contract := range contracts {
unique[copilotEntryKey(contract.agentID)] = contract
unique[managedReferenceKey(contract.agentID)] = contract
}
result := make([]copilotRuntimeContract, 0, len(unique))
for _, contract := range unique {
+26 -162
View File
@@ -5,19 +5,17 @@ import (
"encoding/json"
"fmt"
"path/filepath"
"runtime"
"sort"
"strings"
)
const (
copilotAgentKey = "copilot"
copilotGuardScript = "guard.js"
copilotAuditScript = "hook.js"
copilotConfigFile = "elydora-audit.json"
copilotHookTimeout = float64(10)
copilotLegacyTimeout = float64(5)
copilotPOSIXApostrophe = `'"'"'`
copilotAgentKey = "copilot"
copilotGuardScript = "guard.js"
copilotAuditScript = "hook.js"
copilotConfigFile = "elydora-audit.json"
copilotHookTimeout = float64(10)
copilotLegacyTimeout = float64(5)
)
var copilotManagedEvents = []struct {
@@ -61,23 +59,19 @@ type copilotRenderedDocument struct {
remove bool
}
type copilotRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type copilotRuntimeContract = managedRuntimeContract
type copilotManagedEntry struct {
agentID string
scriptPath string
}
type copilotManagedEntry = managedScriptReference
func cloneCopilotObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
func copilotManagedEntryForHandler(
handler map[string]any,
scriptName string,
) (*copilotManagedEntry, error) {
scriptPath, managed := copilotManagedScriptPath(handler)
if !managed {
return nil, nil
}
return clone
return resolveManagedScript(scriptPath, scriptName)
}
func cloneCopilotHooks(source copilotHooks) copilotHooks {
@@ -90,109 +84,15 @@ func cloneCopilotHooks(source copilotHooks) copilotHooks {
return clone
}
func sameCopilotPath(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 sameCopilotAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameCopilotFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func quoteCopilotPowerShell(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func buildCopilotHandler(nodePath, scriptPath string) map[string]any {
return map[string]any{
"type": "command",
"bash": quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath),
"powershell": "& " + quoteCopilotPowerShell(nodePath) + " " + quoteCopilotPowerShell(scriptPath) + "; exit $LASTEXITCODE",
"bash": posixSource(nodePath, scriptPath),
"powershell": powerShellSource(nodePath, scriptPath),
"timeoutSec": copilotHookTimeout,
}
}
func readCopilotPOSIXArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if strings.HasPrefix(command[index:], copilotPOSIXApostrophe) {
value.WriteByte('\'')
index += len(copilotPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func readCopilotPowerShellArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); index++ {
if command[index] != '\'' {
value.WriteByte(command[index])
continue
}
if index+1 < len(command) && command[index+1] == '\'' {
value.WriteByte('\'')
index++
continue
}
return value.String(), index + 1, true
}
return "", start, false
}
func parseCopilotBash(command string) (string, string, bool) {
executable, next, ok := readCopilotPOSIXArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readCopilotPOSIXArgument(command, next+1)
return executable, script, ok && end == len(command)
}
func parseCopilotPowerShell(command string) (string, string, bool) {
if !strings.HasPrefix(command, "& ") {
return "", "", false
}
executable, next, ok := readCopilotPowerShellArgument(command, 2)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readCopilotPowerShellArgument(command, next+1)
return executable, script, ok && command[end:] == "; exit $LASTEXITCODE"
}
func parseCopilotLegacyCommand(command string) (string, bool) {
command = strings.TrimSpace(command)
separator := strings.IndexAny(command, " \t")
@@ -211,11 +111,6 @@ func parseCopilotLegacyCommand(command string) (string, bool) {
return script, script != ""
}
func isCopilotNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func copilotManagedScriptPath(handler map[string]any) (string, bool) {
if len(handler) != 4 || handler["type"] != "command" {
return "", false
@@ -226,11 +121,11 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) {
return "", false
}
if handler["timeoutSec"] == copilotHookTimeout {
bashNode, bashScript, bashParsed := parseCopilotBash(bash)
psNode, psScript, psParsed := parseCopilotPowerShell(powershell)
bashNode, bashScript, bashParsed := parsePOSIXCommand(bash)
psNode, psScript, psParsed := parsePowerShellSource(powershell)
if bashParsed && psParsed && filepath.IsAbs(bashNode) && filepath.IsAbs(bashScript) &&
isCopilotNodeExecutable(bashNode) && sameCopilotPath(bashNode, psNode) &&
sameCopilotPath(bashScript, psScript) {
isNodeExecutable(bashNode) && sameManagedPath(bashNode, psNode) &&
sameManagedPath(bashScript, psScript) {
return bashScript, true
}
}
@@ -240,30 +135,6 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) {
return parseCopilotLegacyCommand(bash)
}
func copilotManagedEntryForHandler(
handler map[string]any,
scriptName string,
) (*copilotManagedEntry, error) {
scriptPath, managed := copilotManagedScriptPath(handler)
if !managed || !filepath.IsAbs(scriptPath) ||
!sameCopilotFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
agentDirectory := filepath.Dir(scriptPath)
if !sameCopilotPath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &copilotManagedEntry{agentID: agentID, scriptPath: scriptPath}, nil
}
func removeManagedCopilotHooks(
hooks copilotHooks,
agentID string,
@@ -277,7 +148,7 @@ func removeManagedCopilotHooks(
return nil, err
}
remove := entry != nil &&
(agentID == "" || sameCopilotAgentID(entry.agentID, agentID))
(agentID == "" || sameManagedAgentID(entry.agentID, agentID))
if !remove {
kept = append(kept, handler)
}
@@ -361,7 +232,7 @@ func renderCopilotDocument(
document: document, changed: true, remove: true,
}, nil
}
root := cloneCopilotObject(document.root)
root := cloneJSONObject(document.root)
root["version"] = float64(1)
if len(hooks) == 0 {
delete(root, "hooks")
@@ -387,13 +258,6 @@ func renderCopilotDocument(
}, nil
}
func copilotEntryKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func copilotManagedEntries(
handlers []map[string]any,
scriptName string,
@@ -405,7 +269,7 @@ func copilotManagedEntries(
return nil, err
}
if entry != nil {
key := copilotEntryKey(entry.agentID)
key := managedReferenceKey(entry.agentID)
result[key] = append(result[key], *entry)
}
}
@@ -438,7 +302,7 @@ func copilotRuntimeContracts(hooks copilotHooks) ([]copilotRuntimeContract, erro
success := successes[key]
failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 ||
!sameCopilotPath(success[0].scriptPath, failure[0].scriptPath) {
!sameManagedPath(success[0].scriptPath, failure[0].scriptPath) {
continue
}
contracts = append(contracts, copilotRuntimeContract{
+20 -142
View File
@@ -1,103 +1,19 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type copilotRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type preparedCopilotInstallation struct {
changes []*fileChange
preconditions []filePrecondition
paths *copilotRuntimePaths
}
func validateCopilotInstallConfig(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 != copilotAgentKey {
return fmt.Errorf(
"GitHub Copilot CLI installation requires agent name %s",
copilotAgentKey,
)
}
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 copilotAgentPaths(config InstallConfig) (*copilotRuntimePaths, error) {
if err := validateCopilotInstallConfig(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 := &copilotRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, copilotGuardScript),
auditPath: filepath.Join(agentDirectory, copilotAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameCopilotPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameCopilotPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
paths.auditPath,
)
}
return paths, nil
paths *managedRuntimePaths
}
func preflightCopilotInstallation(
config InstallConfig,
sources *copilotSources,
) (*copilotRuntimePaths, string, error) {
) (*managedRuntimePaths, string, error) {
if sources == nil || sources.user == nil || sources.user.filePath == "" {
return nil, "", fmt.Errorf(
"GitHub Copilot CLI installation requires a user hook path",
@@ -106,22 +22,22 @@ func preflightCopilotInstallation(
if err := requireCopilotHooksEnabled(sources); err != nil {
return nil, "", err
}
paths, err := copilotAgentPaths(config)
if err := validateManagedInstallConfig(config, copilotAgentKey, "GitHub Copilot CLI"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, copilotGuardScript, copilotAuditScript)
if err != nil {
return nil, "", err
}
if err := validateCopilotRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, copilotAgentKey, "Copilot",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
nodePath, err := resolveAbsoluteNodeRuntime("GitHub Copilot CLI")
if err != nil {
return nil, "", err
}
if !filepath.IsAbs(nodePath) || !isCopilotNodeExecutable(nodePath) {
return nil, "", fmt.Errorf(
"GitHub Copilot CLI hooks require an absolute Node.js executable path",
)
}
hookSources := []copilotHooks{sources.user.hooks}
if sources.legacy != nil {
hookSources = append(hookSources, sources.legacy.hooks)
@@ -132,28 +48,6 @@ func preflightCopilotInstallation(
return paths, nodePath, nil
}
func buildCopilotRuntimeConfig(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": copilotAgentKey,
}
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 validateCopilotRenderedSet(
sources *copilotSources,
rendered []*copilotRenderedDocument,
@@ -169,7 +63,7 @@ func validateCopilotRenderedSet(
matches := 0
for _, item := range rendered {
if item != nil && item.document != nil &&
sameCopilotPath(item.document.filePath, document.filePath) {
sameManagedPath(item.document.filePath, document.filePath) {
matches++
}
}
@@ -218,34 +112,13 @@ func prepareCopilotInstallation(
if err := validateCopilotRenderedSet(sources, rendered); err != nil {
return nil, err
}
runtimeConfig, err := buildCopilotRuntimeConfig(config)
guardScript, auditScript := copilotExpectedScripts(config.AgentID, nil)
changes, err := managedRuntimeFileChanges(
config, paths, copilotAgentKey, string(guardScript), string(auditScript),
)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(copilotAgentKey, 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(copilotAgentKey, config.AgentID, "", false, true)), 0700,
},
}
changes := make([]*fileChange, 0, len(items)+len(rendered))
for _, item := range items {
change, changeErr := prepareFileChange(item.path, item.label, item.content, item.mode)
if changeErr != nil {
return nil, changeErr
}
changes = append(changes, change)
}
for _, document := range rendered {
change, changeErr := prepareRenderedCopilotChange(document)
if changeErr != nil {
@@ -275,3 +148,8 @@ func commitCopilotInstallation(
prepared.preconditions,
)
}
func copilotExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(copilotAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(copilotAgentKey, agentID, "", false, true))
}
+1 -8
View File
@@ -240,14 +240,7 @@ func writeCopilotChanges(
agentDirectory string,
preconditions []filePrecondition,
) error {
hasChanges := false
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
if !hasFileChanges(changes) {
return writeChanges(changes, label, rename, preconditions...)
}
if agentDirectory != "" {
+1 -1
View File
@@ -274,7 +274,7 @@ func validateCopilotHooks(value any, label string) (copilotHooks, error) {
if err := validateCopilotHandler(handler, event, itemLabel); err != nil {
return nil, err
}
handlers = append(handlers, cloneCopilotObject(handler))
handlers = append(handlers, cloneJSONObject(handler))
}
hooks[event] = handlers
}
+2 -258
View File
@@ -1,261 +1,5 @@
package plugins
import (
"bytes"
"fmt"
"path/filepath"
"sort"
"strings"
)
func readCopilotRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireCopilotRuntimeString(
config map[string]any,
field string,
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 validateCopilotRuntimeConfig(
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 := requireCopilotRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireCopilotRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireCopilotRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameCopilotAgentID(agentID, expectedAgentID) ||
config["agent_name"] != copilotAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Copilot hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireCopilotRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireCopilotRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateCopilotRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readCopilotRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, copilotGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, copilotAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != copilotAgentKey ||
!sameCopilotAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Copilot agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func validCopilotContractPaths(contract copilotRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
return sameCopilotPath(filepath.Dir(agentDirectory), runtimeRoot) &&
sameCopilotPath(
contract.guardPath,
filepath.Join(agentDirectory, copilotGuardScript),
) &&
sameCopilotPath(
contract.auditPath,
filepath.Join(agentDirectory, copilotAuditScript),
), nil
}
func copilotRuntimeContractExists(contract copilotRuntimeContract) (bool, error) {
validPaths, err := validCopilotContractPaths(contract)
if err != nil || !validPaths {
return false, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
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 := readCopilotRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateCopilotRuntimeConfig(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)
}
expectedGuard := []byte(generateGuardScript(
copilotAgentKey,
contract.agentID,
"",
false,
"",
))
expectedAudit := []byte(buildHookScriptWithOutput(
copilotAgentKey,
contract.agentID,
"",
false,
true,
))
return bytes.Equal(guard.contents, expectedGuard) &&
bytes.Equal(audit.contents, expectedAudit), nil
}
func copilotRuntimeFilesExist(contracts []copilotRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := copilotRuntimeContractExists(contract)
if err != nil {
return false, err
}
if exists {
return true, nil
}
}
return false, nil
func copilotRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesExist(contracts, copilotAgentKey, "Copilot", copilotExpectedScripts)
}
+26 -54
View File
@@ -10,55 +10,27 @@ type CursorPlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Cursor commits its provider-specific guard
// in the same transaction as the rest of the installation.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *CursorPlugin) ManagesGuardRuntime() bool {
return true
}
func cursorAgentPaths(config InstallConfig) (
runtimeRoot, agentDirectory, guardPath, auditPath string,
err error,
) {
if config.AgentID == "" {
return "", "", "", "", fmt.Errorf("agent ID is required")
}
runtimeRoot, err = AgentRuntimeRoot()
if err != nil {
return "", "", "", "", err
}
agentDirectory, err = ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return "", "", "", "", err
}
guardPath = filepath.Join(agentDirectory, cursorGuardScript)
if !sameCursorPath(config.GuardScriptPath, guardPath) {
return "", "", "", "", fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
guardPath,
)
}
auditPath = filepath.Join(agentDirectory, cursorAuditScript)
if config.HookScript != "" && !sameCursorPath(config.HookScript, auditPath) {
return "", "", "", "", fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
auditPath,
)
}
return runtimeRoot, agentDirectory, guardPath, auditPath, nil
func cursorAgentPaths(config InstallConfig) (*managedRuntimePaths, error) {
return resolveManagedRuntimePaths(config, cursorGuardScript, cursorAuditScript)
}
// PreflightInstall validates every existing source before the CLI creates a
// runtime directory or guard file.
// PreflightInstall validates every source before the CLI creates runtime state.
func (p *CursorPlugin) PreflightInstall(config InstallConfig) error {
if _, err := readCursorDocument(); err != nil {
return err
}
_, agentDirectory, _, _, err := cursorAgentPaths(config)
paths, err := cursorAgentPaths(config)
if err != nil {
return err
}
if err := preflightCursorRuntime(agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, cursorAgentKey, "Cursor",
); err != nil {
return err
}
_, err = resolveNodeRuntime()
@@ -70,44 +42,42 @@ func (p *CursorPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
runtimeRoot, agentDirectory, guardPath, auditPath, err := cursorAgentPaths(config)
paths, err := cursorAgentPaths(config)
if err != nil {
return err
}
if err := preflightCursorRuntime(agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, cursorAgentKey, "Cursor",
); err != nil {
return err
}
nodePath, err := resolveNodeRuntime()
if err != nil {
return err
}
hooks := removeManagedCursorHooks(document.hooks, runtimeRoot, "")
hooks["preToolUse"] = append(
hooks["preToolUse"],
buildCursorHandler(nodePath, guardPath),
)
hooks["postToolUse"] = append(
hooks["postToolUse"],
buildCursorHandler(nodePath, auditPath),
)
hooks := removeManagedCursorHooks(document.hooks, paths.runtimeRoot, "")
hooks["preToolUse"] = append(hooks["preToolUse"], buildCursorHandler(nodePath, paths.guardPath))
hooks["postToolUse"] = append(hooks["postToolUse"], buildCursorHandler(nodePath, paths.auditPath))
hooks["postToolUseFailure"] = append(
hooks["postToolUseFailure"],
buildCursorHandler(nodePath, auditPath),
buildCursorHandler(nodePath, paths.auditPath),
)
rendered, err := renderCursorDocument(document, hooks, runtimeRoot)
rendered, err := renderCursorDocument(document, hooks, paths.runtimeRoot)
if err != nil {
return fmt.Errorf("render Cursor user hooks: %w", err)
}
changes, err := prepareCursorInstallationChanges(config, agentDirectory, rendered)
changes, err := prepareCursorInstallationChanges(config, paths, rendered)
if err != nil {
return err
}
if err := writeCursorChanges(
if err := writeManagedChanges(
changes,
"Install Cursor hooks",
p.rename,
runtimeRoot,
agentDirectory,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.filePath),
"Cursor hooks directory",
); err != nil {
return err
}
@@ -133,12 +103,14 @@ func (p *CursorPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
return writeCursorChanges(
return writeManagedChanges(
[]*fileChange{change},
"Uninstall Cursor hooks",
p.rename,
"",
"",
filepath.Dir(document.filePath),
"Cursor hooks directory",
)
}
+14 -116
View File
@@ -11,12 +11,11 @@ import (
)
const (
cursorAgentKey = "cursor"
cursorConfigFile = "hooks.json"
cursorGuardScript = "guard.js"
cursorAuditScript = "hook.js"
cursorHookTimeout = float64(10)
cursorPOSIXApostrophe = `'"'"'`
cursorAgentKey = "cursor"
cursorConfigFile = "hooks.json"
cursorGuardScript = "guard.js"
cursorAuditScript = "hook.js"
cursorHookTimeout = float64(10)
)
type cursorHooks map[string][]map[string]any
@@ -36,11 +35,7 @@ type cursorRenderedDocument struct {
remove bool
}
type cursorRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type cursorRuntimeContract = managedRuntimeContract
func cloneCursorHooks(source cursorHooks) cursorHooks {
clone := make(cursorHooks, len(source))
@@ -50,104 +45,16 @@ func cloneCursorHooks(source cursorHooks) cursorHooks {
return clone
}
func sameCursorPath(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 sameCursorAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func quoteCursorPowerShell(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func buildCursorHandler(nodePath, scriptPath string) map[string]any {
command := quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
command := posixSource(nodePath, scriptPath)
if runtime.GOOS == "windows" {
command = "& " + quoteCursorPowerShell(nodePath) + " " +
quoteCursorPowerShell(scriptPath) + "; exit $LASTEXITCODE"
command = powerShellSource(nodePath, scriptPath)
}
return map[string]any{
"command": command, "timeout": cursorHookTimeout, "failClosed": true,
}
}
func readCursorPOSIXArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if strings.HasPrefix(command[index:], cursorPOSIXApostrophe) {
value.WriteByte('\'')
index += len(cursorPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func readCursorPowerShellArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); index++ {
if command[index] != '\'' {
value.WriteByte(command[index])
continue
}
if index+1 < len(command) && command[index+1] == '\'' {
value.WriteByte('\'')
index++
continue
}
return value.String(), index + 1, true
}
return "", start, false
}
func parseCursorPOSIXCommand(command string) (string, string, bool) {
executable, next, ok := readCursorPOSIXArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readCursorPOSIXArgument(command, next+1)
return executable, script, ok && end == len(command)
}
func parseCursorPowerShellCommand(command string) (string, string, bool) {
if !strings.HasPrefix(command, "& ") {
return "", "", false
}
executable, next, ok := readCursorPowerShellArgument(command, 2)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readCursorPowerShellArgument(command, next+1)
return executable, script, ok && command[end:] == "; exit $LASTEXITCODE"
}
func parseCursorLegacyCommand(command string) (string, bool) {
separator := strings.IndexByte(command, ' ')
if separator < 0 {
@@ -164,11 +71,6 @@ func parseCursorLegacyCommand(command string) (string, bool) {
return script, script != "" && !strings.ContainsAny(script, "\r\n")
}
func isCursorNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func cursorManagedScriptPath(handler map[string]any) (string, bool) {
if len(handler) == 3 && handler["timeout"] == cursorHookTimeout && handler["failClosed"] == true {
command, ok := handler["command"].(string)
@@ -178,12 +80,12 @@ func cursorManagedScriptPath(handler map[string]any) (string, bool) {
var nodePath, scriptPath string
var parsed bool
if runtime.GOOS == "windows" {
nodePath, scriptPath, parsed = parseCursorPowerShellCommand(command)
nodePath, scriptPath, parsed = parsePowerShellSource(command)
} else {
nodePath, scriptPath, parsed = parseCursorPOSIXCommand(command)
nodePath, scriptPath, parsed = parsePOSIXCommand(command)
}
if parsed && filepath.IsAbs(nodePath) && filepath.IsAbs(scriptPath) &&
isCursorNodeExecutable(nodePath) {
isNodeExecutable(nodePath) {
return scriptPath, true
}
}
@@ -207,7 +109,7 @@ func cursorManagedAgentID(
return "", false
}
agentDirectory := filepath.Dir(scriptPath)
if !sameCursorPath(filepath.Dir(agentDirectory), runtimeRoot) {
if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false
}
agentID := filepath.Base(agentDirectory)
@@ -297,7 +199,7 @@ func removeManagedCursorHooks(hooks cursorHooks, runtimeRoot, agentID string) cu
kept := make([]map[string]any, 0, len(result[contract.event]))
for _, handler := range result[contract.event] {
managedID, managed := cursorManagedAgentID(handler, contract.script, runtimeRoot)
if managed && (agentID == "" || sameCursorAgentID(managedID, agentID)) {
if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) {
continue
}
kept = append(kept, handler)
@@ -384,11 +286,7 @@ func managedCursorIDs(
if !managed {
continue
}
key := agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(agentID)
}
result[key] = agentID
result[managedReferenceKey(agentID)] = agentID
}
return result
}
+21 -266
View File
@@ -1,13 +1,9 @@
package plugins
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
func cursorConfigPath() (string, error) {
@@ -27,21 +23,14 @@ func readCursorDocument() (*cursorDocument, error) {
if err != nil {
return nil, err
}
info, err := os.Lstat(filePath)
if errors.Is(err, os.ErrNotExist) {
snapshot, err := readManagedFile(filePath, "Cursor user hooks", maxManagedSourceBytes)
if err != nil {
return nil, err
}
if snapshot == nil {
return createCursorDocument(filePath), nil
}
if err != nil {
return nil, fmt.Errorf("inspect Cursor user hooks at %s: %w", filePath, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return nil, fmt.Errorf("Cursor user hooks path is not a physical file: %s", filePath)
}
raw, err := os.ReadFile(filePath) // #nosec G304 -- filePath is the fixed Cursor user configuration path.
if err != nil {
return nil, fmt.Errorf("read Cursor user hooks at %s: %w", filePath, err)
}
return parseCursorDocument(filePath, raw, runtimeRoot)
return parseCursorDocument(filePath, snapshot.contents, runtimeRoot)
}
func prepareRenderedCursorChange(rendered *cursorRenderedDocument) (*fileChange, error) {
@@ -59,167 +48,25 @@ func prepareRenderedCursorChange(rendered *cursorRenderedDocument) (*fileChange,
)
}
func cursorPhysicalFileExists(path, label string) (bool, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("inspect %s at %s: %w", label, path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return false, fmt.Errorf("%s path is not a physical file: %s", label, path)
}
return true, nil
}
func cursorPhysicalDirectoryExists(path string) (bool, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("inspect Elydora runtime directory at %s: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return false, fmt.Errorf("Elydora runtime path is not a physical directory: %s", path)
}
return true, nil
}
func readCursorRuntimeConfig(path string) (map[string]any, bool, error) {
exists, err := cursorPhysicalFileExists(path, "Elydora runtime config")
if err != nil || !exists {
return nil, exists, err
}
raw, err := os.ReadFile(path) // #nosec G304 -- path is confined to one validated Elydora agent directory.
if err != nil {
return nil, true, fmt.Errorf("read Elydora runtime config at %s: %w", path, err)
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
if !json.Valid(raw) {
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return nil, true, fmt.Errorf("parse %s: %w", label, err)
}
}
config, err := decodeJSONCObject(raw, label, false)
if err != nil {
return nil, true, err
}
return config, true, nil
}
func validateCursorRuntimeIdentity(path, agentID string) (bool, error) {
config, exists, err := readCursorRuntimeConfig(path)
if err != nil || !exists {
return exists, err
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != cursorAgentKey ||
!sameCursorAgentID(configuredID, agentID) {
return true, fmt.Errorf(
"Elydora runtime config identity does not match Cursor agent %s: %s",
agentID,
path,
)
}
return true, nil
}
func preflightCursorRuntime(agentDirectory, agentID string) error {
exists, err := cursorPhysicalDirectoryExists(agentDirectory)
if err != nil || !exists {
return err
}
identityExists, err := validateCursorRuntimeIdentity(
filepath.Join(agentDirectory, "config.json"), agentID,
)
if err != nil {
return err
}
runtimeExists := false
for _, item := range []struct{ path, label string }{
{filepath.Join(agentDirectory, cursorGuardScript), "Elydora guard runtime"},
{filepath.Join(agentDirectory, cursorAuditScript), "Elydora audit runtime"},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key"},
} {
exists, err := cursorPhysicalFileExists(item.path, item.label)
if err != nil {
return err
}
runtimeExists = runtimeExists || exists
}
if runtimeExists && !identityExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
func prepareCursorInstallationChanges(
config InstallConfig,
agentDirectory string,
paths *managedRuntimePaths,
rendered *cursorRenderedDocument,
) ([]*fileChange, error) {
baseURL := config.BaseURL
if baseURL == "" {
baseURL = "https://api.elydora.com"
if config.BaseURL == "" {
config.BaseURL = "https://api.elydora.com"
}
runtimeConfig, err := json.MarshalIndent(agentRuntimeConfig{
OrgID: config.OrgID, AgentID: config.AgentID, KID: config.KID,
BaseURL: baseURL, Token: config.Token, AgentName: cursorAgentKey,
}, "", " ")
changes, err := managedRuntimeFileChanges(
config,
paths,
cursorAgentKey,
generateGuardScript(
cursorAgentKey, config.AgentID, `{"permission":"allow"}`+"\n", true, "cursor",
),
buildHookScriptWithOutput(cursorAgentKey, config.AgentID, "{}\n", true, true),
)
if err != nil {
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
}
runtimeConfig = append(runtimeConfig, '\n')
if len(runtimeConfig) > maxRuntimeConfigBytes {
return nil, fmt.Errorf(
"Elydora runtime config exceeds %d bytes after JSON encoding",
maxRuntimeConfigBytes,
)
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
filepath.Join(agentDirectory, cursorGuardScript), "Elydora guard runtime",
[]byte(generateGuardScript(
cursorAgentKey,
config.AgentID,
`{"permission":"allow"}`+"\n",
true,
"cursor",
)), 0700,
},
{filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600},
{
filepath.Join(agentDirectory, cursorAuditScript), "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
cursorAgentKey,
config.AgentID,
"{}\n",
true,
true,
)), 0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
for _, item := range items {
if err := validateRuntimeFileTarget(item.path, item.label); err != nil {
return nil, err
}
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
if err != nil {
return nil, err
}
changes = append(changes, change)
return nil, err
}
documentChange, err := prepareRenderedCursorChange(rendered)
if err != nil {
@@ -228,98 +75,6 @@ func prepareCursorInstallationChanges(
return append(changes, documentChange), nil
}
func ensureCursorDirectory(path, label string) error {
if err := os.MkdirAll(path, 0700); err != nil {
return fmt.Errorf("create %s directory at %s: %w", label, path, err)
}
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("inspect %s directory at %s: %w", label, path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return fmt.Errorf("%s directory is not a physical directory: %s", label, path)
}
return nil
}
func writeCursorChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot, 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
}
}
directories := map[string]string{}
for _, change := range changes {
if change == nil {
continue
}
directory := filepath.Dir(change.filePath)
key := filepath.Clean(directory)
if runtime.GOOS == "windows" {
key = strings.ToLower(key)
}
directories[key] = directory
}
for _, directory := range directories {
if err := ensureCursorDirectory(directory, label); err != nil {
return err
}
}
return writeChanges(changes, label, rename)
}
func cursorRuntimeFilesExist(contracts []cursorRuntimeContract) (bool, error) {
for _, contract := range contracts {
agentDirectory := filepath.Dir(contract.guardPath)
directoryExists, err := cursorPhysicalDirectoryExists(agentDirectory)
if err != nil {
return false, err
}
if !directoryExists {
continue
}
config, exists, err := readCursorRuntimeConfig(filepath.Join(agentDirectory, "config.json"))
if err != nil {
return false, err
}
configuredID, idOK := config["agent_id"].(string)
if !exists || !idOK || config["agent_name"] != cursorAgentKey ||
!sameCursorAgentID(configuredID, contract.agentID) {
continue
}
complete := true
for _, item := range []struct{ path, label string }{
{contract.guardPath, "Elydora guard runtime"},
{contract.auditPath, "Elydora audit runtime"},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key"},
} {
exists, err := cursorPhysicalFileExists(item.path, item.label)
if err != nil {
return false, err
}
complete = complete && exists
}
if complete {
return true, nil
}
}
return false, nil
func cursorRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesPresent(contracts, cursorAgentKey)
}
+2 -2
View File
@@ -183,7 +183,7 @@ func TestCursorInstallCreatesRuntimeDirectoryTransactionally(t *testing.T) {
fixture.guardPath,
fixture.hookPath,
} {
if exists, err := cursorPhysicalFileExists(path, "Cursor runtime"); err != nil || !exists {
if exists, err := managedPhysicalFileExists(path, "Cursor runtime", maxManagedSourceBytes); err != nil || !exists {
t.Fatalf("runtime file %s exists = %v, %v", path, exists, err)
}
}
@@ -200,7 +200,7 @@ func TestCursorInstallRollsBackEveryFileOnConfigCommitFailure(t *testing.T) {
}
failed := false
fixture.plugin.rename = func(source, destination string) error {
if !failed && sameCursorPath(destination, fixture.configPath) {
if !failed && sameManagedPath(destination, fixture.configPath) {
failed = true
return errors.New("injected Cursor config commit failure")
}
+4 -6
View File
@@ -7,14 +7,12 @@ type DroidPlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Droid commits its guard, audit runtime,
// credentials, and hook documents in one transaction.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *DroidPlugin) ManagesGuardRuntime() bool {
return true
}
// PreflightInstall validates every hook source, policy layer, credential,
// matcher, and runtime identity before any write.
// PreflightInstall validates every source before the CLI creates runtime state.
func (p *DroidPlugin) PreflightInstall(config InstallConfig) error {
sources, err := readDroidSources()
if err != nil {
@@ -107,7 +105,7 @@ func (p *DroidPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
runtimeRoot, err := droidRuntimeRoot()
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return err
}
@@ -133,7 +131,7 @@ func (p *DroidPlugin) Status() (PluginStatus, error) {
if droidHookBlocked(sources) != nil {
return status, nil
}
runtimeRoot, err := droidRuntimeRoot()
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return status, err
}
+1 -1
View File
@@ -239,7 +239,7 @@ func droidAdditionsFor(
document, target *droidDocument,
groups map[string]map[string]any,
) map[string]map[string]any {
if sameDroidPath(document.filePath, target.filePath) {
if sameManagedPath(document.filePath, target.filePath) {
return groups
}
return map[string]map[string]any{}
+14 -156
View File
@@ -15,13 +15,11 @@ import (
)
const (
droidAgentKey = "droid"
droidGuardScript = "guard.js"
droidAuditScript = "hook.js"
droidHookTimeout = float64(10)
droidOwnedFileMarker = "// Managed by Elydora"
droidPOSIXApostrophe = `'"'"'`
droidWindowsExitSuffix = "; exit $LASTEXITCODE"
droidAgentKey = "droid"
droidGuardScript = "guard.js"
droidAuditScript = "hook.js"
droidHookTimeout = float64(10)
droidOwnedFileMarker = "// Managed by Elydora"
)
var droidToolEvents = [...]string{"PreToolUse", "PostToolUse"}
@@ -35,11 +33,7 @@ type droidManagedRemoval struct {
removeGroup bool
}
type droidRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type droidRuntimeContract = managedRuntimeContract
type droidRegexEntry struct {
Label string `json:"label"`
@@ -178,145 +172,21 @@ for (const entry of entries) {
return nil
}
func buildDroidCommand(nodePath, scriptPath string) string {
if runtime.GOOS == "windows" {
return "& " + quoteDroidPowerShellArgument(nodePath) + " " +
quoteDroidPowerShellArgument(scriptPath) + droidWindowsExitSuffix
}
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
}
func quoteDroidPowerShellArgument(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func buildDroidGroup(nodePath, scriptPath string) map[string]any {
return map[string]any{
"matcher": "*",
"hooks": []any{map[string]any{
"type": "command", "command": buildDroidCommand(nodePath, scriptPath), "timeout": droidHookTimeout,
"type": "command", "command": buildShellCommand(nodePath, scriptPath), "timeout": droidHookTimeout,
}},
}
}
func parseDroidCommand(command string, includeLegacy ...bool) (string, string, bool) {
if runtime.GOOS != "windows" {
return parseDroidPOSIXCommand(command)
}
executable, script, ok := parseDroidPowerShellCommand(command)
if ok || len(includeLegacy) == 0 || !includeLegacy[0] {
executable, script, ok := parseShellCommand(command)
if ok || runtime.GOOS != "windows" || len(includeLegacy) == 0 || !includeLegacy[0] {
return executable, script, ok
}
return parseDroidLegacyWindowsCommand(command)
}
func parseDroidPOSIXCommand(command string) (string, string, bool) {
executable, next, ok := readDroidPOSIXArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readDroidPOSIXArgument(command, next+1)
return executable, script, ok && end == len(command) && executable != "" && script != ""
}
func parseDroidPowerShellCommand(command string) (string, string, bool) {
if !strings.HasPrefix(command, "& ") {
return "", "", false
}
executable, next, ok := readDroidPowerShellArgument(command, 2)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readDroidPowerShellArgument(command, next+1)
return executable, script, ok && command[end:] == droidWindowsExitSuffix && executable != "" && script != ""
}
func parseDroidLegacyWindowsCommand(command string) (string, string, bool) {
executable, next, ok := readDroidLegacyWindowsArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readDroidLegacyWindowsArgument(command, next+1)
return executable, script, ok && end == len(command) && executable != "" && script != ""
}
func readDroidPowerShellArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); index++ {
if command[index] != '\'' {
value.WriteByte(command[index])
continue
}
if index+1 < len(command) && command[index+1] == '\'' {
value.WriteByte('\'')
index++
continue
}
return value.String(), index + 1, true
}
return "", start, false
}
func readDroidLegacyWindowsArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '"' {
return "", start, false
}
end := strings.IndexByte(command[start+1:], '"')
if end < 0 {
return "", start, false
}
end += start + 1
value := command[start+1 : end]
return value, end + 1, value != "" && !strings.ContainsAny(value, "\r\n")
}
func readDroidPOSIXArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if strings.HasPrefix(command[index:], droidPOSIXApostrophe) {
value.WriteByte('\'')
index += len(droidPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func sameDroidPath(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 sameDroidAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func droidRuntimeRoot() (string, error) {
return AgentRuntimeRoot()
return parseQuotedWindowsCommand(command)
}
func managedDroidAgentID(
@@ -333,29 +203,17 @@ func managedDroidAgentID(
}
executable, scriptPath, ok := parseDroidCommand(command, includeLegacy...)
if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) ||
!isDroidNodeExecutable(executable) || !sameDroidFileName(filepath.Base(scriptPath), scriptName) {
!isNodeExecutable(executable) || !sameManagedName(filepath.Base(scriptPath), scriptName) {
return "", false
}
agentDirectory := filepath.Dir(scriptPath)
if !sameDroidPath(filepath.Dir(agentDirectory), runtimeRoot) {
if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false
}
agentID := filepath.Base(agentDirectory)
return agentID, agentID != "" && agentID != "." && agentID != ".."
}
func isDroidNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func sameDroidFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot string) []droidManagedRemoval {
removals := make([]droidManagedRemoval, 0)
for _, contract := range []struct{ event, script string }{
@@ -370,7 +228,7 @@ func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot strin
managedID, managed := managedDroidAgentID(
handlerValue.(map[string]any), contract.script, runtimeRoot, true,
)
if managed && (agentID == "" || sameDroidAgentID(managedID, agentID)) {
if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) {
indexes = append(indexes, handlerIndex)
}
}
@@ -400,7 +258,7 @@ func droidRuntimeContracts(settings droidHookSettings, runtimeRoot string) []dro
contracts := make([]droidRuntimeContract, 0, len(guardIDs))
for _, guardID := range guardIDs {
for auditID := range audits {
if !sameDroidAgentID(guardID, auditID) {
if !sameManagedAgentID(guardID, auditID) {
continue
}
root := filepath.Join(runtimeRoot, guardID)
+23 -156
View File
@@ -1,24 +1,13 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type droidRuntimePaths struct {
runtimeRoot string
agentDirectory string
guardPath string
auditPath string
}
type preparedDroidInstallation struct {
changes []*fileChange
preconditions []filePrecondition
paths *droidRuntimePaths
paths *managedRuntimePaths
}
type preparedDroidUninstall struct {
@@ -39,80 +28,10 @@ func droidSourceLabel(document *droidDocument) string {
}
}
func validateDroidInstallConfig(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 != droidAgentKey {
return fmt.Errorf(
"Factory Droid installation requires agent name %s",
droidAgentKey,
)
}
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 droidAgentPaths(config InstallConfig) (*droidRuntimePaths, error) {
if err := validateDroidInstallConfig(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 := &droidRuntimePaths{
runtimeRoot: runtimeRoot,
agentDirectory: agentDirectory,
guardPath: filepath.Join(agentDirectory, droidGuardScript),
auditPath: filepath.Join(agentDirectory, droidAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameDroidPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameDroidPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
func preflightDroidInstallation(
config InstallConfig,
sources *droidSources,
) (*droidRuntimePaths, string, error) {
) (*managedRuntimePaths, string, error) {
if sources == nil || sources.root == nil || sources.policy == nil {
return nil, "", fmt.Errorf("Factory Droid installation sources are required")
}
@@ -126,50 +45,25 @@ func preflightDroidInstallation(
if err := validateDroidRegexes(hooks...); err != nil {
return nil, "", err
}
paths, err := droidAgentPaths(config)
if err := validateManagedInstallConfig(config, droidAgentKey, "Factory Droid"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, droidGuardScript, droidAuditScript)
if err != nil {
return nil, "", err
}
if err := validateDroidRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, droidAgentKey, "Factory Droid",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
nodePath, err := resolveAbsoluteNodeRuntime("Factory Droid")
if err != nil {
return nil, "", err
}
if !filepath.IsAbs(nodePath) || !isDroidNodeExecutable(nodePath) {
return nil, "", fmt.Errorf(
"Factory Droid hooks require an absolute Node.js executable path",
)
}
return paths, nodePath, nil
}
func buildDroidRuntimeConfig(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": droidAgentKey,
}
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 prepareRenderedDroidChange(
rendered *droidRenderedDocument,
) (*fileChange, error) {
@@ -198,7 +92,7 @@ func validateDroidRenderedSet(
matches := 0
for _, item := range rendered {
if item != nil && item.document != nil &&
sameDroidPath(item.document.filePath, document.filePath) {
sameManagedPath(item.document.filePath, document.filePath) {
matches++
}
}
@@ -217,7 +111,7 @@ func droidSourcePreconditions(
for _, document := range droidSourceDocuments(sources) {
changed := false
for _, path := range changedPaths {
if sameDroidPath(document.filePath, path) {
if sameManagedPath(document.filePath, path) {
changed = true
break
}
@@ -246,38 +140,13 @@ func prepareDroidInstallation(
if err := validateDroidRenderedSet(sources, rendered); err != nil {
return nil, err
}
runtimeConfig, err := buildDroidRuntimeConfig(config)
guardScript, auditScript := droidExpectedScripts(config.AgentID, nil)
changes, err := managedRuntimeFileChanges(
config, paths, droidAgentKey, string(guardScript), string(auditScript),
)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath,
"Elydora guard runtime",
[]byte(generateGuardScript(droidAgentKey, config.AgentID, "", false, "")),
0700,
},
{filepath.Join(paths.agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600},
{filepath.Join(paths.agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600},
{
paths.auditPath,
"Elydora audit runtime",
[]byte(buildHookScriptWithOutput(droidAgentKey, config.AgentID, "", false, true)),
0700,
},
}
changes := make([]*fileChange, 0, len(items)+len(rendered))
for _, item := range items {
change, changeErr := prepareFileChange(item.path, item.label, item.content, item.mode)
if changeErr != nil {
return nil, changeErr
}
changes = append(changes, change)
}
changedPaths := make([]string, 0, len(rendered))
for _, document := range rendered {
change, changeErr := prepareRenderedDroidChange(document)
@@ -305,14 +174,7 @@ func writeDroidChanges(
runtimeRoot, agentDirectory string,
preconditions []filePrecondition,
) error {
hasChanges := false
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
if !hasFileChanges(changes) {
return writeChanges(changes, label, rename, preconditions...)
}
if err := assertFilePreconditions(preconditions, label); err != nil {
@@ -365,7 +227,7 @@ func prepareDroidUninstall(
for _, item := range rendered {
changed := false
for _, path := range changedPaths {
if sameDroidPath(item.document.filePath, path) {
if sameManagedPath(item.document.filePath, path) {
changed = true
break
}
@@ -398,3 +260,8 @@ func commitDroidUninstall(
prepared.preconditions,
)
}
func droidExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(droidAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(droidAgentKey, agentID, "", false, true))
}
+2 -248
View File
@@ -1,251 +1,5 @@
package plugins
import (
"bytes"
"fmt"
"path/filepath"
"sort"
"strings"
)
func readDroidRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
config, err := decodeStrictJSONObject(
snapshot.contents,
fmt.Sprintf("Elydora runtime config at %s", path),
)
return config, true, err
}
func requireDroidRuntimeString(
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 validateDroidRuntimeConfig(
config map[string]any,
expectedAgentID, 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 := requireDroidRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireDroidRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireDroidRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameDroidAgentID(agentID, expectedAgentID) || config["agent_name"] != droidAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Factory Droid hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireDroidRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireDroidRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateDroidRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readDroidRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, droidGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, droidAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != droidAgentKey ||
!sameDroidAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Factory Droid agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func validDroidContractPaths(contract droidRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
return sameDroidPath(filepath.Dir(agentDirectory), runtimeRoot) &&
sameDroidPath(
contract.guardPath,
filepath.Join(agentDirectory, droidGuardScript),
) &&
sameDroidPath(
contract.auditPath,
filepath.Join(agentDirectory, droidAuditScript),
), nil
}
func droidRuntimeContractExists(contract droidRuntimeContract) (bool, error) {
validPaths, err := validDroidContractPaths(contract)
if err != nil || !validPaths {
return false, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
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 := readDroidRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
audit, err := readManagedFile(contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateDroidRuntimeConfig(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)
}
expectedGuard := []byte(generateGuardScript(droidAgentKey, contract.agentID, "", false, ""))
expectedAudit := []byte(buildHookScriptWithOutput(
droidAgentKey,
contract.agentID,
"",
false,
true,
))
return bytes.Equal(guard.contents, expectedGuard) &&
bytes.Equal(audit.contents, expectedAudit), nil
}
func droidRuntimeFilesExist(contracts []droidRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := droidRuntimeContractExists(contract)
if err != nil {
return false, err
}
if exists {
return true, nil
}
}
return false, nil
func droidRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesExist(contracts, droidAgentKey, "Factory Droid", droidExpectedScripts)
}
+1 -1
View File
@@ -57,7 +57,7 @@ func TestDroidInstallWritesCurrentContainerAndCompleteRuntime(t *testing.T) {
requireDroidNativeGroup(t, auditGroup)
guardCommand := droidManagedHandler(t, hooks["PreToolUse"], fixture.guardPath)["command"].(string)
if runtime.GOOS == "windows" &&
(!strings.HasPrefix(guardCommand, "& '") || !strings.HasSuffix(guardCommand, droidWindowsExitSuffix)) {
(!strings.HasPrefix(guardCommand, "& '") || !strings.HasSuffix(guardCommand, powerShellExitSuffix)) {
t.Fatalf("Windows Factory command = %q", guardCommand)
}
runtimeConfig := readDroidTestObject(t, fixture.runtimeConfig)
@@ -237,7 +237,7 @@ func droidManagedGroup(t *testing.T, groupsValue any, scriptPath string) map[str
handler := requireDroidObject(t, handlerValue)
command, _ := handler["command"].(string)
_, configuredPath, managed := parseDroidCommand(command, true)
if managed && sameDroidPath(configuredPath, scriptPath) {
if managed && sameManagedPath(configuredPath, scriptPath) {
return group
}
}
@@ -253,7 +253,7 @@ func droidManagedHandler(t *testing.T, groupsValue any, scriptPath string) map[s
handler := requireDroidObject(t, handlerValue)
command, _ := handler["command"].(string)
_, configuredPath, managed := parseDroidCommand(command, true)
if managed && sameDroidPath(configuredPath, scriptPath) {
if managed && sameManagedPath(configuredPath, scriptPath) {
return handler
}
}
@@ -53,7 +53,7 @@ func TestDroidInstallRollsBackRuntimeAfterLateHookCommitFailure(t *testing.T) {
prepared := prepareDroidTestInstallation(t, fixture, fixture.config)
failed := false
rename := func(source, destination string) error {
if !failed && sameDroidPath(destination, fixture.configPath) {
if !failed && sameManagedPath(destination, fixture.configPath) {
failed = true
return errors.New("injected Droid hook commit failure")
}
@@ -85,7 +85,7 @@ func TestDroidConcurrentActiveHookReplacementPreventsRuntimeCommit(t *testing.T)
t.Fatalf("concurrent source error = %v", err)
}
for path, source := range before {
if sameDroidPath(path, fixture.configPath) {
if sameManagedPath(path, fixture.configPath) {
continue
}
if readDroidTestFile(t, path) != source {
@@ -205,7 +205,7 @@ func TestDroidSameContentReplacementIsRejectedByPhysicalIdentity(t *testing.T) {
t.Fatal("same-content replacement changed")
}
for path, source := range before {
if !sameDroidPath(path, fixture.configPath) && readDroidTestFile(t, path) != source {
if !sameManagedPath(path, fixture.configPath) && readDroidTestFile(t, path) != source {
t.Fatalf("runtime changed after inode replacement: %s", path)
}
}
@@ -226,7 +226,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) {
if err != nil {
t.Fatalf("read uninstall sources: %v", err)
}
runtimeRoot, err := droidRuntimeRoot()
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
t.Fatalf("resolve runtime root: %v", err)
}
@@ -240,7 +240,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) {
}
failed := false
rename := func(source, destination string) error {
if !failed && sameDroidPath(destination, fixture.settingsPath) {
if !failed && sameManagedPath(destination, fixture.settingsPath) {
failed = true
return errors.New("injected Droid uninstall failure")
}
+5 -4
View File
@@ -10,8 +10,7 @@ type GeminiPlugin struct {
rename renameFunc
}
// ManagesGuardRuntime reports that Gemini commits both generated runtimes and
// the user settings document through one transaction.
// ManagesGuardRuntime reports that the guard is committed with the audit runtime.
func (p *GeminiPlugin) ManagesGuardRuntime() bool {
return true
}
@@ -92,13 +91,14 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
if err := writeGeminiChanges(
if err := writeManagedChanges(
changes,
"Install Gemini CLI hooks",
p.rename,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.filePath),
"Gemini CLI configuration directory",
); err != nil {
return err
}
@@ -120,13 +120,14 @@ func (p *GeminiPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
return writeGeminiChanges(
return writeManagedChanges(
[]*fileChange{change},
"Uninstall Gemini CLI hooks",
p.rename,
"",
"",
filepath.Dir(document.filePath),
"Gemini CLI configuration directory",
)
}
+15 -74
View File
@@ -7,83 +7,37 @@ import (
"strings"
)
type geminiRuntimeReference struct {
agentID string
scriptPath string
}
func sameGeminiPath(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 sameGeminiAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameGeminiFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func isGeminiNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
type geminiRuntimeReference = managedScriptReference
func buildGeminiCommand(runtimePath, scriptPath string) (string, error) {
if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) {
return "", fmt.Errorf(
"Gemini CLI hook commands require absolute runtime and script paths",
)
return "", fmt.Errorf("Gemini CLI hook commands require absolute runtime and script paths")
}
if runtime.GOOS != "windows" {
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), nil
return posixSource(runtimePath, scriptPath), nil
}
source := "& " + quoteCodexPowerShell(runtimePath) + " " +
quoteCodexPowerShell(scriptPath) + "; exit $LASTEXITCODE"
return "& " + quoteCodexPowerShell(codexPowerShellPath()) +
" -NoLogo -NoProfile -NonInteractive -EncodedCommand " +
encodeCodexPowerShell(source), nil
return "& " + quotePowerShellArgument(windowsPowerShellPath()) + powerShellEncodedFlags +
encodePowerShellSource(powerShellSource(runtimePath, scriptPath)), nil
}
func parseGeminiWindowsCommand(command string) (string, string, bool) {
if !strings.HasPrefix(command, "& ") {
return "", "", false
}
powerShell, next, ok := readCodexPowerShellArgument(command, 2)
if !ok || !isAbsoluteWindowsPath(powerShell) ||
!strings.EqualFold(windowsPathBase(powerShell), "powershell.exe") {
powerShell, next, ok := readPowerShellArgument(command, 2)
if !ok || !isPowerShellExecutable(powerShell) ||
!strings.HasPrefix(command[next:], powerShellEncodedFlags) {
return "", "", false
}
prefix := " -NoLogo -NoProfile -NonInteractive -EncodedCommand "
if !strings.HasPrefix(command[next:], prefix) {
return "", "", false
}
source, ok := decodeCodexPowerShell(command[next+len(prefix):])
source, ok := decodePowerShellSource(command[next+len(powerShellEncodedFlags):])
if !ok {
return "", "", false
}
return parseCodexPowerShellSource(source)
return parsePowerShellSource(source)
}
func parseGeminiCommand(command string) (string, string, bool) {
if runtimePath, scriptPath, ok := parseCodexPOSIXCommand(command); ok {
if runtimePath, scriptPath, ok := parsePOSIXCommand(command); ok {
return runtimePath, scriptPath, true
}
return parseGeminiWindowsCommand(command)
@@ -110,23 +64,10 @@ func geminiRuntimeReferenceForCommand(
if !current && includeLegacy {
runtimePath, scriptPath, legacy = parseLegacyGeminiCommand(command)
}
validRuntime := (current && filepath.IsAbs(runtimePath) &&
isGeminiNodeExecutable(runtimePath)) || (legacy && runtimePath == "node")
if !validRuntime || !filepath.IsAbs(scriptPath) ||
!sameGeminiFileName(filepath.Base(scriptPath), scriptName) {
validRuntime := (current && filepath.IsAbs(runtimePath) && isNodeExecutable(runtimePath)) ||
(legacy && runtimePath == "node")
if !validRuntime {
return nil, nil
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameGeminiPath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &geminiRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
return resolveManagedScript(scriptPath, scriptName)
}
+3 -25
View File
@@ -1,10 +1,7 @@
package plugins
import (
"path/filepath"
"runtime"
"sort"
"strings"
)
const (
@@ -27,11 +24,7 @@ type geminiManagedRemoval struct {
removeGroup bool
}
type geminiRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type geminiRuntimeContract = managedRuntimeContract
func buildGeminiGroup(runtimePath, scriptPath, name string) (map[string]any, error) {
command, err := buildGeminiCommand(runtimePath, scriptPath)
@@ -151,7 +144,7 @@ func managedGeminiRemovals(
return nil, err
}
if reference != nil &&
(agentID == "" || sameGeminiAgentID(reference.agentID, agentID)) {
(agentID == "" || sameManagedAgentID(reference.agentID, agentID)) {
indexes = append(indexes, handlerIndex)
}
}
@@ -168,13 +161,6 @@ func managedGeminiRemovals(
return removals, nil
}
func geminiReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func geminiReferencesForEvent(
groups []any,
scriptName string,
@@ -196,7 +182,7 @@ func geminiReferencesForEvent(
return nil, err
}
if reference != nil {
key := geminiReferenceKey(reference.agentID)
key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference)
}
}
@@ -243,11 +229,3 @@ func geminiRuntimeContracts(
}
return contracts, nil
}
func geminiRuntimeRoot() (string, error) {
root, err := AgentRuntimeRoot()
if err != nil {
return "", err
}
return filepath.Clean(root), nil
}
+19 -161
View File
@@ -1,190 +1,48 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type geminiRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
func validateGeminiInstallConfig(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 != geminiAgentKey {
return fmt.Errorf(
"Gemini CLI installation requires agent name %s",
geminiAgentKey,
)
}
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 geminiAgentPaths(config InstallConfig) (*geminiRuntimePaths, error) {
if err := validateGeminiInstallConfig(config); err != nil {
return nil, err
}
runtimeRoot, err := geminiRuntimeRoot()
if err != nil {
return nil, err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return nil, err
}
paths := &geminiRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, geminiGuardScript),
auditPath: filepath.Join(agentDirectory, geminiAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameGeminiPath(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) ||
!sameGeminiPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"audit runtime must use the managed Elydora agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
import "fmt"
func preflightGeminiInstallation(
config InstallConfig,
document *geminiDocument,
) (*geminiRuntimePaths, string, error) {
) (*managedRuntimePaths, string, error) {
if document == nil || document.filePath == "" {
return nil, "", fmt.Errorf(
"Gemini CLI installation requires a user settings document",
)
return nil, "", fmt.Errorf("Gemini CLI installation requires a user settings document")
}
paths, err := geminiAgentPaths(config)
if err := validateManagedInstallConfig(config, geminiAgentKey, "Gemini CLI"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, geminiGuardScript, geminiAuditScript)
if err != nil {
return nil, "", err
}
if err := validateGeminiRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, geminiAgentKey, "Gemini CLI",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
nodePath, err := resolveAbsoluteNodeRuntime("Gemini CLI")
if err != nil {
return nil, "", err
}
if !filepath.IsAbs(nodePath) || !isGeminiNodeExecutable(nodePath) {
return nil, "", fmt.Errorf(
"Gemini CLI hooks require an absolute Node.js executable path",
)
}
return paths, nodePath, nil
}
func buildGeminiRuntimeConfig(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": geminiAgentKey,
}
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 prepareGeminiInstallationChanges(
config InstallConfig,
paths *geminiRuntimePaths,
paths *managedRuntimePaths,
rendered *geminiRenderedDocument,
) ([]*fileChange, error) {
runtimeConfig, err := buildGeminiRuntimeConfig(config)
changes, err := managedRuntimeFileChanges(
config,
paths,
geminiAgentKey,
generateGuardScript(geminiAgentKey, config.AgentID, "{}\n", false, ""),
buildHookScriptWithOutput(geminiAgentKey, config.AgentID, "{}\n", false, true),
)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(
geminiAgentKey,
config.AgentID,
"{}\n",
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(
geminiAgentKey,
config.AgentID,
"{}\n",
false,
true,
)),
0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
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)
}
documentChange, err := prepareRenderedGeminiChange(rendered)
if err != nil {
return nil, err
-35
View File
@@ -67,38 +67,3 @@ func prepareRenderedGeminiChange(
rendered.remove,
)
}
func writeGeminiChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot string,
agentDirectory string,
settingsDirectory 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
}
}
if err := ensureManagedDirectory(
settingsDirectory,
"Gemini CLI configuration directory",
); err != nil {
return err
}
return writeChanges(changes, label, rename)
}
+2 -2
View File
@@ -164,10 +164,10 @@ func TestGeminiValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) {
}, "query parameters"},
{"guard path", func(f *geminiFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"},
}, "managed agent directory"},
{"audit path", func(f *geminiFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js")
}, "managed Elydora agent directory"},
}, "managed agent directory"},
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareGeminiFixture(t, geminiFixtureOptions{})
+2 -228
View File
@@ -1,231 +1,5 @@
package plugins
import (
"fmt"
"path/filepath"
"sort"
"strings"
)
func readGeminiRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireGeminiRuntimeString(
config map[string]any,
field string,
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 validateGeminiRuntimeConfig(
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 := requireGeminiRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireGeminiRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireGeminiRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameGeminiAgentID(agentID, expectedAgentID) ||
config["agent_name"] != geminiAgentKey {
return fmt.Errorf(
"elydora runtime identity does not match Gemini CLI hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireGeminiRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireGeminiRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateGeminiRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readGeminiRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, geminiGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, geminiAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != geminiAgentKey ||
!sameGeminiAgentID(configuredID, agentID) {
return fmt.Errorf(
"elydora runtime config identity does not match Gemini CLI agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func geminiRuntimeContractExists(contract geminiRuntimeContract) (bool, error) {
runtimeRoot, err := geminiRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
if !sameGeminiPath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameGeminiPath(
contract.auditPath,
filepath.Join(agentDirectory, geminiAuditScript),
) {
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 := readGeminiRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateGeminiRuntimeConfig(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 geminiRuntimeFilesExist(contracts []geminiRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := geminiRuntimeContractExists(contract)
if err != nil {
return false, err
}
if exists {
return true, nil
}
}
return false, nil
func geminiRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesExist(contracts, geminiAgentKey, "Gemini CLI", nil)
}
+4 -4
View File
@@ -33,8 +33,8 @@ func TestGeminiCommandRoundTripAndRejectsInjection(t *testing.T) {
t.Fatalf("build Gemini command: %v", err)
}
executable, script, ok := parseGeminiCommand(command)
if !ok || !sameGeminiPath(executable, nodePath) ||
!sameGeminiPath(script, fixture.guardPath) {
if !ok || !sameManagedPath(executable, nodePath) ||
!sameManagedPath(script, fixture.guardPath) {
t.Fatalf("parsed Gemini command = %q, %q, %v", executable, script, ok)
}
if runtime.GOOS == "windows" && (strings.Contains(command, "%GEMINI_CWD%") ||
@@ -425,12 +425,12 @@ func TestGeminiRuntimeConfigOmitsEmptyOptionalToken(t *testing.T) {
func TestGeminiAgentIDMatchingIsPlatformCorrect(t *testing.T) {
if runtime.GOOS == "windows" {
if !sameGeminiAgentID("AGENT-1", "agent-1") {
if !sameManagedAgentID("AGENT-1", "agent-1") {
t.Fatal("Windows Gemini agent IDs should compare case-insensitively")
}
return
}
if sameGeminiAgentID("AGENT-1", "agent-1") {
if sameManagedAgentID("AGENT-1", "agent-1") {
t.Fatal("POSIX Gemini agent IDs should preserve case")
}
}
@@ -251,7 +251,7 @@ func runGeminiRawCommand(
var process *exec.Cmd
if runtime.GOOS == "windows" {
process = exec.Command(
codexPowerShellPath(),
windowsPowerShellPath(),
"-NoLogo",
"-NoProfile",
"-NonInteractive",
@@ -35,7 +35,7 @@ func prepareGeminiTestChanges(
t *testing.T,
fixture *geminiFixture,
document *geminiDocument,
) ([]*fileChange, *geminiRuntimePaths) {
) ([]*fileChange, *managedRuntimePaths) {
t.Helper()
paths, nodePath, err := preflightGeminiInstallation(fixture.config, document)
if err != nil {
@@ -110,7 +110,7 @@ func TestGeminiInstallRollsBackAllFiveFilesAfterSettingsFailure(t *testing.T) {
fixture.config.OrgID = "org-updated"
fixture.config.Token = "token-updated"
fixture.plugin.rename = func(source, destination string) error {
if sameGeminiPath(destination, fixture.settingsPath) &&
if sameManagedPath(destination, fixture.settingsPath) &&
strings.HasSuffix(source, ".tmp") {
return errors.New("injected Gemini settings failure")
}
@@ -139,13 +139,14 @@ func TestPreparedGeminiInstallRejectsConcurrentSettingsChange(t *testing.T) {
if err := os.WriteFile(fixture.settingsPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent settings: %v", err)
}
err = writeGeminiChanges(
err = writeManagedChanges(
changes,
"Install Gemini CLI hooks",
nil,
paths.runtimeRoot,
paths.agentDirectory,
filepath.Dir(document.filePath),
"Gemini CLI configuration directory",
)
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err)
@@ -204,7 +205,7 @@ func TestGeminiUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed Gemini settings: %v", err)
}
fixture.plugin.rename = func(source, destination string) error {
if sameGeminiPath(destination, fixture.settingsPath) &&
if sameManagedPath(destination, fixture.settingsPath) &&
strings.HasSuffix(source, ".tmp") {
return errors.New("injected Gemini uninstall failure")
}
+2 -2
View File
@@ -36,11 +36,11 @@ func (p *GrokPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
guardCommand, err := buildGrokCommand(nodePath, paths.guardPath)
guardCommand, err := buildEncodedCommand("Grok", nodePath, paths.guardPath)
if err != nil {
return err
}
auditCommand, err := buildGrokCommand(nodePath, paths.auditPath)
auditCommand, err := buildEncodedCommand("Grok", nodePath, paths.auditPath)
if err != nil {
return err
}
+6 -117
View File
@@ -1,132 +1,21 @@
package plugins
import (
"fmt"
"path/filepath"
"runtime"
"strings"
)
type grokRuntimeReference struct {
agentID string
scriptPath string
}
func sameGrokPath(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 sameGrokAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func buildGrokCommand(runtimePath, scriptPath string) (string, error) {
if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) {
return "", fmt.Errorf(
"Grok hook commands require absolute runtime and script paths",
)
}
if runtime.GOOS == "windows" {
return codexWindowsCommand(runtimePath, scriptPath), nil
}
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), nil
}
func quoteGrokLegacyWindowsArgument(value string) string {
return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"`
}
func readGrokLegacyWindowsArgument(
command string,
start int,
) (string, int, bool) {
if start >= len(command) || command[start] != '"' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); index++ {
if command[index] == '"' {
return value.String(), index + 1, true
}
if command[index] == '\r' || command[index] == '\n' {
return "", start, false
}
value.WriteByte(command[index])
}
return "", start, false
}
func parseGrokLegacyWindowsCommand(command string) (string, string, bool) {
runtimePath, next, ok := readGrokLegacyWindowsArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
scriptPath, end, ok := readGrokLegacyWindowsArgument(command, next+1)
if !ok || end != len(command) {
return "", "", false
}
expected := quoteGrokLegacyWindowsArgument(runtimePath) + " " +
quoteGrokLegacyWindowsArgument(scriptPath)
return runtimePath, scriptPath, command == expected
}
import "path/filepath"
func parseGrokCommand(command string) (string, string, bool) {
if runtimePath, scriptPath, ok := parseCodexPOSIXCommand(command); ok {
if runtimePath, scriptPath, ok := parseEncodedCommand(command); ok {
return runtimePath, scriptPath, true
}
if runtimePath, scriptPath, ok := parseCodexWindowsCommand(command); ok {
return runtimePath, scriptPath, true
}
return parseGrokLegacyWindowsCommand(command)
}
func isGrokNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
return parseQuotedWindowsCommand(command)
}
func grokRuntimeReferenceForCommand(
command string,
scriptName string,
) (*grokRuntimeReference, error) {
) (*managedScriptReference, error) {
runtimePath, scriptPath, ok := parseGrokCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) ||
!isGrokNodeExecutable(runtimePath) ||
!sameGrokFileName(filepath.Base(scriptPath), scriptName) {
if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
return nil, nil
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameGrokPath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &grokRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
}
func sameGrokFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
return resolveManagedScript(scriptPath, scriptName)
}
+13 -36
View File
@@ -6,9 +6,7 @@ import (
"fmt"
"math"
"reflect"
"runtime"
"sort"
"strings"
)
const (
@@ -48,20 +46,6 @@ type grokRenderedDocument struct {
remove bool
}
type grokRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
func cloneGrokObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
func cloneGrokHooks(source grokHooks) grokHooks {
clone := make(grokHooks, len(source))
for event, groups := range source {
@@ -128,7 +112,7 @@ func validateGrokHandler(
}
}
}
return cloneGrokObject(handler), nil
return cloneJSONObject(handler), nil
}
func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, error) {
@@ -166,7 +150,7 @@ func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, erro
}
handlers = append(handlers, handler)
}
return grokGroup{object: cloneGrokObject(object), handlers: handlers}, nil
return grokGroup{object: cloneJSONObject(object), handlers: handlers}, nil
}
func readGrokHooks(root map[string]any) (grokHooks, error) {
@@ -229,7 +213,7 @@ func renderGrokHooks(hooks grokHooks) map[string]any {
for event, groups := range hooks {
values := make([]any, 0, len(groups))
for _, group := range groups {
object := cloneGrokObject(group.object)
object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers {
handlers = append(handlers, handler)
@@ -262,7 +246,7 @@ func exactManagedGrokGroup(group grokGroup) bool {
func managedGrokReference(
handler map[string]any,
scriptName string,
) (*grokRuntimeReference, error) {
) (*managedScriptReference, error) {
if len(handler) != 3 || handler["type"] != "command" ||
handler["timeout"] != grokHookTimeout {
return nil, nil
@@ -303,7 +287,7 @@ func removeManagedGrokGroups(
return nil, err
}
remove := reference != nil &&
(agentID == "" || sameGrokAgentID(reference.agentID, agentID))
(agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if !remove {
kept = append(kept, handler)
}
@@ -386,7 +370,7 @@ func renderGrokDocument(
document: document, changed: true, remove: true,
}, nil
}
root := cloneGrokObject(document.root)
root := cloneJSONObject(document.root)
if len(hooks) == 0 {
delete(root, "hooks")
} else {
@@ -407,18 +391,11 @@ func renderGrokDocument(
}, nil
}
func grokReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func grokReferencesForEvent(
groups []grokGroup,
scriptName string,
) (map[string][]grokRuntimeReference, error) {
result := map[string][]grokRuntimeReference{}
) (map[string][]managedScriptReference, error) {
result := map[string][]managedScriptReference{}
for _, group := range groups {
if !exactManagedGrokGroup(group) {
continue
@@ -431,14 +408,14 @@ func grokReferencesForEvent(
if reference == nil {
continue
}
key := grokReferenceKey(reference.agentID)
key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference)
}
}
return result, nil
}
func grokRuntimeContracts(hooks grokHooks) ([]grokRuntimeContract, error) {
func grokRuntimeContracts(hooks grokHooks) ([]managedRuntimeContract, error) {
guards, err := grokReferencesForEvent(hooks["PreToolUse"], grokGuardScript)
if err != nil {
return nil, err
@@ -459,16 +436,16 @@ func grokRuntimeContracts(hooks grokHooks) ([]grokRuntimeContract, error) {
keys = append(keys, key)
}
sort.Strings(keys)
contracts := make([]grokRuntimeContract, 0, len(keys))
contracts := make([]managedRuntimeContract, 0, len(keys))
for _, key := range keys {
guard := guards[key]
success := successes[key]
failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 ||
!sameGrokPath(success[0].scriptPath, failure[0].scriptPath) {
!sameManagedPath(success[0].scriptPath, failure[0].scriptPath) {
continue
}
contracts = append(contracts, grokRuntimeContract{
contracts = append(contracts, managedRuntimeContract{
agentID: guard[0].agentID, guardPath: guard[0].scriptPath,
auditPath: success[0].scriptPath,
})
+17 -135
View File
@@ -1,102 +1,24 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type grokRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
func validateGrokInstallConfig(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 != grokAgentKey {
return fmt.Errorf("grok installation requires agent name %s", grokAgentKey)
}
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 grokAgentPaths(config InstallConfig) (*grokRuntimePaths, error) {
if err := validateGrokInstallConfig(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 := &grokRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, grokGuardScript),
auditPath: filepath.Join(agentDirectory, grokAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameGrokPath(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) ||
!sameGrokPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"audit runtime must use the managed Elydora agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
import "fmt"
func preflightGrokInstallation(
config InstallConfig,
document *grokDocument,
) (*grokRuntimePaths, string, error) {
) (*managedRuntimePaths, string, error) {
if document == nil {
return nil, "", fmt.Errorf("grok installation requires a hook document")
}
paths, err := grokAgentPaths(config)
if err := validateManagedInstallConfig(config, grokAgentKey, "grok"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, grokGuardScript, grokAuditScript)
if err != nil {
return nil, "", err
}
if err := validateGrokRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, grokAgentKey, "Grok",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
@@ -106,63 +28,23 @@ func preflightGrokInstallation(
return paths, nodePath, nil
}
func buildGrokRuntimeConfig(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": grokAgentKey,
}
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 grokExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(grokAgentKey, agentID, "", false, "grok")),
[]byte(buildHookScriptWithOutput(grokAgentKey, agentID, "", false, true))
}
func prepareGrokInstallationChanges(
config InstallConfig,
paths *grokRuntimePaths,
paths *managedRuntimePaths,
rendered *grokRenderedDocument,
) ([]*fileChange, error) {
runtimeConfig, err := buildGrokRuntimeConfig(config)
guardScript, auditScript := grokExpectedScripts(config.AgentID, nil)
changes, err := managedRuntimeFileChanges(
config, paths, grokAgentKey, string(guardScript), string(auditScript),
)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(grokAgentKey, config.AgentID, "", false, "grok")), 0700,
},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600},
{
paths.auditPath, "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
grokAgentKey, config.AgentID, "", false, true,
)), 0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
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)
}
documentChange, err := prepareRenderedGrokChange(rendered)
if err != nil {
return nil, err
+5 -20
View File
@@ -85,32 +85,17 @@ func writeGrokChanges(
agentDirectory string,
hooksDirectory string,
) error {
hasChanges := false
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
if !hasFileChanges(changes) {
return nil
}
if agentDirectory != "" {
if err := EnsurePrivateDirectory(runtimeRoot); err != nil {
return err
}
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
return err
}
}
if err := ensureManagedDirectory(
filepath.Dir(hooksDirectory),
"Grok home directory",
); err != nil {
return err
}
if err := ensureManagedDirectory(hooksDirectory, "Grok hooks directory"); err != nil {
return err
}
return writeChanges(changes, label, rename)
return writeManagedChanges(
changes, label, rename, runtimeRoot, agentDirectory,
hooksDirectory, "Grok hooks directory",
)
}
+2 -2
View File
@@ -164,10 +164,10 @@ func TestGrokValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) {
}, "query parameters"},
{"guard path", func(f *grokFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"},
}, "managed agent directory"},
{"audit path", func(f *grokFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js")
}, "managed Elydora agent directory"},
}, "managed agent directory"},
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareGrokFixture(t, grokFixtureOptions{})
+2 -229
View File
@@ -1,232 +1,5 @@
package plugins
import (
"fmt"
"path/filepath"
"sort"
"strings"
)
func readGrokRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireGrokRuntimeString(
config map[string]any,
field string,
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 validateGrokRuntimeConfig(
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 := requireGrokRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireGrokRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireGrokRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameGrokAgentID(agentID, expectedAgentID) ||
config["agent_name"] != grokAgentKey {
return fmt.Errorf(
"elydora runtime identity does not match Grok hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireGrokRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireGrokRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateGrokRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readGrokRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, grokGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, grokAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != grokAgentKey ||
!sameGrokAgentID(configuredID, agentID) {
return fmt.Errorf(
"elydora runtime config identity does not match Grok agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func grokRuntimeContractExists(contract grokRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
if !sameGrokPath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameGrokPath(
contract.auditPath,
filepath.Join(agentDirectory, grokAuditScript),
) {
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 := readGrokRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateGrokRuntimeConfig(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 grokRuntimeFilesExist(contracts []grokRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := grokRuntimeContractExists(contract)
if err != nil {
return false, err
}
if exists {
return true, nil
}
}
return false, nil
func grokRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesExist(contracts, grokAgentKey, "Grok", nil)
}
+8 -8
View File
@@ -27,22 +27,22 @@ func TestGrokCommandRoundTripAndRejectsInjection(t *testing.T) {
if err != nil {
t.Fatalf("resolve Node.js runtime: %v", err)
}
command, err := buildGrokCommand(nodePath, fixture.guardPath)
command, err := buildEncodedCommand("Grok", nodePath, fixture.guardPath)
if err != nil {
t.Fatalf("build Grok command: %v", err)
}
executable, script, ok := parseGrokCommand(command)
if !ok || !sameGrokPath(executable, nodePath) ||
!sameGrokPath(script, fixture.guardPath) {
if !ok || !sameManagedPath(executable, nodePath) ||
!sameManagedPath(script, fixture.guardPath) {
t.Fatalf("parsed Grok command = %q, %q, %v", executable, script, ok)
}
if strings.Contains(command, "%ELYDORA_HOOK_PATH%") {
if runtime.GOOS == "windows" && strings.Contains(command, "%ELYDORA_HOOK_PATH%") {
t.Fatalf("Grok command exposes an expandable path: %q", command)
}
if _, err := buildGrokCommand("node", fixture.guardPath); err == nil {
if _, err := buildEncodedCommand("Grok", "node", fixture.guardPath); err == nil {
t.Fatal("accepted a relative Grok runtime path")
}
if _, err := buildGrokCommand(nodePath, "guard.js"); err == nil {
if _, err := buildEncodedCommand("Grok", nodePath, "guard.js"); err == nil {
t.Fatal("accepted a relative Grok script path")
}
for _, invalid := range []string{
@@ -337,12 +337,12 @@ func TestGrokRuntimeConfigOmitsEmptyOptionalToken(t *testing.T) {
func TestGrokWindowsAgentIDMatchingIsPlatformCorrect(t *testing.T) {
if runtime.GOOS == "windows" {
if !sameGrokAgentID("AGENT-1", "agent-1") {
if !sameManagedAgentID("AGENT-1", "agent-1") {
t.Fatal("Windows Grok agent IDs should compare case-insensitively")
}
return
}
if sameGrokAgentID("AGENT-1", "agent-1") {
if sameManagedAgentID("AGENT-1", "agent-1") {
t.Fatal("POSIX Grok agent IDs should preserve case")
}
}
@@ -275,8 +275,7 @@ func legacyGrokCommand(t *testing.T, scriptPath string) string {
t.Fatalf("resolve Node.js runtime: %v", err)
}
if runtime.GOOS == "windows" {
return quoteGrokLegacyWindowsArgument(nodePath) + " " +
quoteGrokLegacyWindowsArgument(scriptPath)
return `"` + nodePath + `" "` + scriptPath + `"`
}
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
}
+4 -4
View File
@@ -59,7 +59,7 @@ func TestGrokInstallRollsBackFourRuntimesAfterConfigCommitFailure(t *testing.T)
}
before := snapshotGrokFiles(t, paths...)
fixture.plugin.rename = func(source, destination string) error {
if sameGrokPath(destination, fixture.configPath) &&
if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") {
return errors.New("injected Grok config failure")
}
@@ -85,11 +85,11 @@ func TestPreparedGrokInstallRejectsConcurrentConfigChange(t *testing.T) {
if err != nil {
t.Fatalf("preflight Grok installation: %v", err)
}
guardCommand, err := buildGrokCommand(nodePath, paths.guardPath)
guardCommand, err := buildEncodedCommand("Grok", nodePath, paths.guardPath)
if err != nil {
t.Fatalf("build guard command: %v", err)
}
auditCommand, err := buildGrokCommand(nodePath, paths.auditPath)
auditCommand, err := buildEncodedCommand("Grok", nodePath, paths.auditPath)
if err != nil {
t.Fatalf("build audit command: %v", err)
}
@@ -179,7 +179,7 @@ func TestGrokUninstallPreservesConfigAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed Grok config: %v", err)
}
fixture.plugin.rename = func(source, destination string) error {
if sameGrokPath(destination, fixture.configPath) &&
if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") {
return errors.New("injected Grok uninstall failure")
}
+18
View File
@@ -88,6 +88,16 @@ func rejectDuplicateJSONCKeys(value *hujson.Value, label, location string) error
return nil
}
func decodeStrictJSONObject(source []byte, label string) (map[string]any, error) {
if !json.Valid(source) {
var value any
if err := json.Unmarshal(source, &value); err != nil {
return nil, fmt.Errorf("parse %s: %w", label, err)
}
}
return decodeJSONCObject(source, label, false)
}
func decodeJSONCObject(source []byte, label string, allowTrailingCommas bool) (map[string]any, error) {
value, err := parseJSONC(source, label, allowTrailingCommas)
if err != nil {
@@ -283,3 +293,11 @@ func jsonPointer(path []any) string {
}
return pointer.String()
}
func cloneJSONObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
+15
View File
@@ -2,6 +2,7 @@ package plugins
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -49,6 +50,20 @@ func hookScriptPath(agentId string) (string, error) {
return filepath.Join(agentDirectory, "hook.js"), nil
}
// removeAgentScripts deletes hook.js and guard.js; missing files are fine.
func removeAgentScripts(agentID string) error {
for _, resolve := range []func(string) (string, error){hookScriptPath, guardScriptPath} {
path, err := resolve(agentID)
if err != nil {
return err
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove %s: %w", path, err)
}
}
return nil
}
// guardScriptPath returns ~/.elydora/<agentId>/guard.js.
func guardScriptPath(agentId string) (string, error) {
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
+6 -15
View File
@@ -34,11 +34,11 @@ func (p *KimiPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
guardCommand, err := buildKimiCommand(nodePath, paths.guardPath)
guardCommand, err := buildEncodedCommand("kimi", nodePath, paths.guardPath)
if err != nil {
return err
}
auditCommand, err := buildKimiCommand(nodePath, paths.auditPath)
auditCommand, err := buildEncodedCommand("kimi", nodePath, paths.auditPath)
if err != nil {
return err
}
@@ -70,12 +70,9 @@ func (p *KimiPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
if err := writeKimiChanges(
changes,
"Install Kimi hooks",
p.rename,
paths.runtimeRoot,
paths.agentDirectory,
if err := writeManagedChanges(
changes, "Install Kimi hooks", p.rename,
paths.runtimeRoot, paths.agentDirectory, "", "",
); err != nil {
return err
}
@@ -107,13 +104,7 @@ func (p *KimiPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
return writeKimiChanges(
changes,
"Uninstall Kimi hooks",
p.rename,
"",
"",
)
return writeManagedChanges(changes, "Uninstall Kimi hooks", p.rename, "", "", "", "")
}
func (p *KimiPlugin) Status() (PluginStatus, error) {
+5 -64
View File
@@ -1,49 +1,10 @@
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
@@ -66,6 +27,7 @@ func readKimiLegacyWindowsArgument(command string, start int) (string, int, bool
return command[start:end], end, end > start
}
// parseKimiLegacyWindowsCommand reads the pre-2.1 quoteWindowsArgument form.
func parseKimiLegacyWindowsCommand(command string) (string, string, bool) {
runtimePath, next, ok := readKimiLegacyWindowsArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
@@ -80,40 +42,19 @@ func parseKimiLegacyWindowsCommand(command string) (string, string, bool) {
}
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 {
if runtimePath, scriptPath, ok := parseEncodedCommand(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) {
) (*managedScriptReference, error) {
runtimePath, scriptPath, ok := parseKimiCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) ||
!isKimiNodeExecutable(runtimePath) || filepath.Base(scriptPath) != scriptName {
if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
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
return resolveManagedScript(scriptPath, scriptName)
}
+13 -29
View File
@@ -2,9 +2,7 @@ package plugins
import (
"fmt"
"runtime"
"sort"
"strings"
)
const (
@@ -54,20 +52,10 @@ type kimiHook struct {
}
type kimiRuntimeContract struct {
agentID string
guardPath string
auditPath string
managedRuntimeContract
configPath string
}
func stringSet(values ...string) map[string]struct{} {
result := make(map[string]struct{}, len(values))
for _, value := range values {
result[value] = struct{}{}
}
return result
}
func mergeStringSets(sets ...map[string]struct{}) map[string]struct{} {
result := map[string]struct{}{}
for _, set := range sets {
@@ -173,7 +161,7 @@ func managedKimiReference(
hook kimiHook,
event string,
scriptName string,
) (*kimiRuntimeReference, error) {
) (*managedScriptReference, error) {
if hook.fieldCount != 3 || hook.event != event || hook.matcher != nil ||
hook.timeout == nil || *hook.timeout != kimiHookTimeoutSeconds {
return nil, nil
@@ -196,7 +184,7 @@ func keptKimiHookIndices(hooks []kimiHook, agentID string) ([]int, error) {
indices := make([]int, 0, len(hooks))
for index, hook := range hooks {
event, scriptName, managedEvent := managedKimiEvent(hook.event)
var reference *kimiRuntimeReference
var reference *managedScriptReference
var err error
if managedEvent {
reference, err = managedKimiReference(hook, event, scriptName)
@@ -205,7 +193,7 @@ func keptKimiHookIndices(hooks []kimiHook, agentID string) ([]int, error) {
}
}
remove := reference != nil &&
(agentID == "" || sameKimiAgentID(reference.agentID, agentID))
(agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if !remove {
indices = append(indices, index)
}
@@ -213,19 +201,12 @@ func keptKimiHookIndices(hooks []kimiHook, agentID string) ([]int, error) {
return indices, nil
}
func kimiReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func kimiReferencesForEvent(
hooks []kimiHook,
event string,
scriptName string,
) (map[string][]kimiRuntimeReference, error) {
result := map[string][]kimiRuntimeReference{}
) (map[string][]managedScriptReference, error) {
result := map[string][]managedScriptReference{}
for _, hook := range hooks {
reference, err := managedKimiReference(hook, event, scriptName)
if err != nil {
@@ -234,7 +215,7 @@ func kimiReferencesForEvent(
if reference == nil {
continue
}
key := kimiReferenceKey(reference.agentID)
key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference)
}
return result, nil
@@ -267,12 +248,15 @@ func kimiRuntimeContracts(documents []kimiDocument) ([]kimiRuntimeContract, erro
success := successes[key]
failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 ||
!sameKimiPath(success[0].scriptPath, failure[0].scriptPath) {
!sameManagedPath(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,
managedRuntimeContract: managedRuntimeContract{
agentID: guard[0].agentID, guardPath: guard[0].scriptPath,
auditPath: success[0].scriptPath,
},
configPath: document.contract.configPath,
})
}
}
+20 -166
View File
@@ -2,107 +2,32 @@ 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) {
) (*managedRuntimePaths, string, error) {
if len(documents) == 0 {
return nil, "", fmt.Errorf("kimi installation requires at least one hook contract")
}
paths, err := kimiAgentPaths(config)
if err := validateManagedInstallConfig(config, kimiAgentKey, "kimi"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, kimiGuardScript, kimiAuditScript)
if err != nil {
return nil, "", err
}
if err := validateKimiRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, kimiAgentKey, "Kimi",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
@@ -112,26 +37,9 @@ func preflightKimiInstallation(
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 kimiExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(kimiAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(kimiAgentKey, agentID, "", false, true))
}
func renderKimiChange(
@@ -172,47 +80,21 @@ func prepareRenderedKimiChange(rendered kimiRenderedDocument) (*fileChange, erro
func prepareKimiInstallationChanges(
config InstallConfig,
paths *kimiRuntimePaths,
paths *managedRuntimePaths,
rendered []kimiRenderedDocument,
) ([]*fileChange, error) {
runtimeConfig, err := buildKimiRuntimeConfig(config)
guardScript, auditScript := kimiExpectedScripts(config.AgentID, nil)
changes, err := managedRuntimeFileChanges(
config, paths, kimiAgentKey, string(guardScript), string(auditScript),
)
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,
},
documentChanges, err := prepareKimiUninstallChanges(rendered)
if err != nil {
return nil, err
}
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
return append(changes, documentChanges...), nil
}
func prepareKimiUninstallChanges(
@@ -228,31 +110,3 @@ func prepareKimiUninstallChanges(
}
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)
}
+1 -1
View File
@@ -66,7 +66,7 @@ func resolveKimiContracts() ([]kimiContract, error) {
legacyHome := filepath.Join(home, ".kimi")
stable := stableKimiContract(filepath.Join(stableHome, "config.toml"))
legacy := legacyKimiContract(filepath.Join(legacyHome, "config.toml"))
if sameKimiPath(stable.configPath, legacy.configPath) {
if sameManagedPath(stable.configPath, legacy.configPath) {
return []kimiContract{stable}, nil
}
+5 -187
View File
@@ -1,194 +1,12 @@
package plugins
import (
"fmt"
"path/filepath"
"sort"
"strings"
)
func readKimiRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireKimiRuntimeString(config map[string]any, field, configPath string) (string, error) {
value, ok := config[field].(string)
if !ok || strings.TrimSpace(value) == "" {
return "", fmt.Errorf(
"elydora runtime config %s is invalid: %s", field, configPath,
)
}
return value, nil
}
func validateKimiRuntimeConfig(
config map[string]any,
expectedAgentID string,
configPath string,
) error {
supported := stringSet("org_id", "agent_id", "kid", "base_url", "token", "agent_name")
fields := make([]string, 0, len(config))
for field := range config {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
if _, ok := supported[field]; !ok {
return fmt.Errorf(
`elydora runtime config has unsupported field %q: %s`, field, configPath,
)
}
}
if _, err := requireKimiRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireKimiRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireKimiRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameKimiAgentID(agentID, expectedAgentID) || config["agent_name"] != kimiAgentKey {
return fmt.Errorf("elydora runtime identity does not match Kimi hooks: %s", configPath)
}
if _, exists := config["token"]; exists {
if _, err := requireKimiRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireKimiRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf("elydora runtime config base URL is invalid at %s: %w", configPath, err)
}
return nil
}
func validateKimiRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory")
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory, "Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readKimiRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, kimiGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, kimiAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != kimiAgentKey ||
!sameKimiAgentID(configuredID, agentID) {
return fmt.Errorf(
"elydora runtime config identity does not match Kimi agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func kimiRuntimeContractExists(contract kimiRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
if !sameKimiPath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameKimiPath(contract.auditPath, filepath.Join(agentDirectory, kimiAuditScript)) {
return false, nil
}
rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory")
if err != nil || !rootExists {
return false, err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory, "Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return false, err
}
configPath := filepath.Join(agentDirectory, "config.json")
keyPath := filepath.Join(agentDirectory, "private.key")
config, configExists, err := readKimiRuntimeConfig(configPath)
if err != nil {
return false, err
}
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
exists, err := managedRuntimeContractExists(
contract.managedRuntimeContract, kimiAgentKey, "Kimi", nil,
)
if err != nil || exists {
return exists, err
}
}
return false, nil
+6 -9
View File
@@ -38,7 +38,7 @@ func TestKimiInstallRollsBackRuntimeAndBothConfigs(t *testing.T) {
modernConfig: kimiString(string(stable)), legacyConfig: kimiString(string(legacy)),
})
fixture.plugin.rename = func(source, destination string) error {
if sameKimiPath(destination, fixture.legacyPath) {
if sameManagedPath(destination, fixture.legacyPath) {
return errors.New("injected legacy config failure")
}
return os.Rename(source, destination)
@@ -160,11 +160,11 @@ func TestPreparedKimiInstallDetectsSourceChangeBeforeCommit(t *testing.T) {
if err != nil {
t.Fatalf("preflight Kimi installation: %v", err)
}
guardCommand, err := buildKimiCommand(nodePath, paths.guardPath)
guardCommand, err := buildEncodedCommand("kimi", nodePath, paths.guardPath)
if err != nil {
t.Fatalf("build guard command: %v", err)
}
auditCommand, err := buildKimiCommand(nodePath, paths.auditPath)
auditCommand, err := buildEncodedCommand("kimi", nodePath, paths.auditPath)
if err != nil {
t.Fatalf("build audit command: %v", err)
}
@@ -204,12 +204,9 @@ func TestPreparedKimiInstallDetectsSourceChangeBeforeCommit(t *testing.T) {
t.Fatalf("write concurrent source: %v", err)
}
err = writeKimiChanges(
changes,
"Install Kimi hooks",
nil,
paths.runtimeRoot,
paths.agentDirectory,
err = writeManagedChanges(
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)
+1 -1
View File
@@ -79,7 +79,7 @@ func (p *KiroCliPlugin) Install(config InstallConfig) error {
copyKiroObject(nextV2, v2Settings)
nextV2["hooks"] = v2Hooks
nextV3 := cloneKiroObject(v3Settings)
nextV3 := cloneJSONObject(v3Settings)
nextV3["version"] = "v1"
v3Hooks := make([]any, 0, len(currentV3Hooks)+2)
for _, hook := range currentV3Hooks {
+3 -9
View File
@@ -28,12 +28,6 @@ type kiroConfigMutation struct {
remove bool
}
func cloneKiroObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
copyKiroObject(clone, value)
return clone
}
func copyKiroObject(destination, source map[string]any) {
for key, value := range source {
destination[key] = value
@@ -49,7 +43,7 @@ func kiroHooksObject(settings map[string]any, label string) (map[string]any, err
if !ok || hooks == nil {
return nil, fmt.Errorf(`%s field "hooks" must be an object`, label)
}
return cloneKiroObject(hooks), nil
return cloneJSONObject(hooks), nil
}
func kiroHookEntries(hooks map[string]any, event, label string) ([]map[string]any, error) {
@@ -193,7 +187,7 @@ func prepareKiroV2Uninstall(
}
hooks["preToolUse"] = filteredPre
hooks["postToolUse"] = filteredPost
next := cloneKiroObject(settings)
next := cloneJSONObject(settings)
next["hooks"] = hooks
mutation.value = next
mutation.changed = true
@@ -250,7 +244,7 @@ func prepareKiroV3Uninstall(
if len(filtered) == len(hooks) {
return mutation, nil
}
next := cloneKiroObject(settings)
next := cloneJSONObject(settings)
next["hooks"] = filtered
owned := true
for key := range settings {
-17
View File
@@ -3,7 +3,6 @@ package plugins
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
@@ -18,18 +17,6 @@ func kiroConfigPaths() (string, string, error) {
filepath.Join(home, ".kiro", "hooks", "elydora-audit.json"), nil
}
func resolveNodeRuntime() (string, error) {
nodePath, err := exec.LookPath("node")
if err != nil {
return "", fmt.Errorf("resolve Node.js runtime: %w", err)
}
absolute, err := filepath.Abs(nodePath)
if err != nil {
return "", fmt.Errorf("resolve Node.js runtime path: %w", err)
}
return absolute, nil
}
func buildKiroCommand(runtimePath, scriptPath string) string {
if runtime.GOOS == "windows" {
return quoteWindowsArgument(runtimePath) + " " + quoteWindowsArgument(scriptPath)
@@ -37,10 +24,6 @@ func buildKiroCommand(runtimePath, scriptPath string) string {
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath)
}
func quotePOSIXArgument(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}
func quoteWindowsArgument(value string) string {
if value != "" && !strings.ContainsAny(value, " \t\n\v\"") {
return value
+9 -94
View File
@@ -1,101 +1,32 @@
package plugins
import (
"fmt"
"path/filepath"
"runtime"
"strings"
)
type kiroIdeRuntimeReference struct {
agentID string
scriptPath string
}
func sameKiroIdePath(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 sameKiroIdeAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameKiroIdeFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func buildKiroIdeCommand(runtimePath, scriptPath string) (string, error) {
if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) {
return "", fmt.Errorf(
"Kiro IDE hook commands require absolute runtime and script paths",
)
}
if runtime.GOOS == "windows" {
return codexWindowsCommand(runtimePath, scriptPath), nil
}
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), nil
}
func parseKiroIdeCommand(command string) (string, string, bool) {
if runtime.GOOS == "windows" {
return parseCodexWindowsCommand(command)
}
return parseCodexPOSIXCommand(command)
}
func kiroIdeRuntimeReferenceForCommand(
command string,
scriptName string,
) (*kiroIdeRuntimeReference, error) {
runtimePath, scriptPath, ok := parseKiroIdeCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) ||
!isCodexNodeExecutable(runtimePath) ||
!sameKiroIdeFileName(filepath.Base(scriptPath), scriptName) {
) (*managedScriptReference, error) {
runtimePath, scriptPath, ok := parseEncodedCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
return nil, nil
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &kiroIdeRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
return resolveManagedScript(scriptPath, scriptName)
}
func currentKiroIdeReference(
hook map[string]any,
nodePath string,
) (*kiroIdeRuntimeReference, error) {
) (*managedScriptReference, error) {
reference, err := managedKiroIdeReference(hook)
if err != nil || reference == nil {
return reference, err
}
action := hook["action"].(map[string]any)
command := action["command"].(string)
expected, err := buildKiroIdeCommand(nodePath, reference.scriptPath)
expected, err := buildEncodedCommand("Kiro IDE", nodePath, reference.scriptPath)
if err != nil {
return nil, err
}
@@ -105,30 +36,14 @@ func currentKiroIdeReference(
return reference, nil
}
// legacyKiroIdeGoReference reads the pre-2.1 `node <script>` form.
func legacyKiroIdeGoReference(
command any,
scriptName string,
) (*kiroIdeRuntimeReference, error) {
) (*managedScriptReference, error) {
text, ok := command.(string)
if !ok || !strings.HasPrefix(text, "node ") {
return nil, nil
}
scriptPath := strings.TrimPrefix(text, "node ")
if !filepath.IsAbs(scriptPath) ||
!sameKiroIdeFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &kiroIdeRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
return resolveManagedScript(strings.TrimPrefix(text, "node "), scriptName)
}
+17 -31
View File
@@ -45,7 +45,7 @@ type kiroIdeDocument struct {
type kiroIdeLegacyDocument struct {
filePath string
snapshot *managedFileSnapshot
contract *kiroIdeRuntimeContract
contract *managedRuntimeContract
}
type kiroIdeRenderedDocument struct {
@@ -55,12 +55,6 @@ type kiroIdeRenderedDocument struct {
remove bool
}
type kiroIdeRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type kiroIdeHookSpecification struct {
description string
trigger string
@@ -82,14 +76,6 @@ func kiroIdeSpecification(name any) (kiroIdeHookSpecification, bool) {
}
}
func cloneKiroIdeObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
func validateKiroIdeAction(value any, label string) error {
action, ok := value.(map[string]any)
if !ok || action == nil {
@@ -212,7 +198,7 @@ func exactKiroIdeKeys(value map[string]any, keys ...string) bool {
func ownedKiroIdeReference(
hook map[string]any,
) (*kiroIdeRuntimeReference, error) {
) (*managedScriptReference, error) {
specification, managed := kiroIdeSpecification(hook["name"])
if !managed {
return nil, nil
@@ -230,7 +216,7 @@ func ownedKiroIdeReference(
func managedKiroIdeReference(
hook map[string]any,
) (*kiroIdeRuntimeReference, error) {
) (*managedScriptReference, error) {
specification, managed := kiroIdeSpecification(hook["name"])
if !managed ||
!exactKiroIdeKeys(
@@ -278,7 +264,7 @@ func buildKiroIdeHook(
if !managed {
return nil, fmt.Errorf("unsupported Elydora Kiro IDE hook name %q", name)
}
command, err := buildKiroIdeCommand(runtimePath, scriptPath)
command, err := buildEncodedCommand("Kiro IDE", runtimePath, scriptPath)
if err != nil {
return nil, err
}
@@ -298,7 +284,7 @@ func withoutManagedKiroIdeHooks(
for _, hook := range hooks {
reference, err := ownedKiroIdeReference(hook)
remove := err == nil && reference != nil &&
(agentID == "" || sameKiroIdeAgentID(reference.agentID, agentID))
(agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if !remove {
filtered = append(filtered, hook)
}
@@ -339,7 +325,7 @@ func renderKiroIdeDocument(
document: document, changed: true, remove: true,
}, nil
}
root := cloneKiroIdeObject(document.root)
root := cloneJSONObject(document.root)
root["version"] = "v1"
values := make([]any, 0, len(hooks))
for _, hook := range hooks {
@@ -364,7 +350,7 @@ func renderKiroIdeDocument(
func kiroIdeRuntimeContracts(
hooks []map[string]any,
) ([]kiroIdeRuntimeContract, error) {
) ([]managedRuntimeContract, error) {
managedHooks := make([]map[string]any, 0, 2)
for _, hook := range hooks {
_, managed := kiroIdeSpecification(hook["name"])
@@ -373,38 +359,38 @@ func kiroIdeRuntimeContracts(
}
}
if len(managedHooks) != 2 {
return []kiroIdeRuntimeContract{}, nil
return []managedRuntimeContract{}, nil
}
nodePath, err := resolveNodeRuntime()
if err != nil {
return nil, err
}
var guard, audit *kiroIdeRuntimeReference
var guard, audit *managedScriptReference
for _, hook := range managedHooks {
reference, err := currentKiroIdeReference(hook, nodePath)
if err != nil {
return nil, err
}
if reference == nil {
return []kiroIdeRuntimeContract{}, nil
return []managedRuntimeContract{}, nil
}
if hook["name"] == kiroIdeGuardName {
if guard != nil {
return []kiroIdeRuntimeContract{}, nil
return []managedRuntimeContract{}, nil
}
guard = reference
} else {
if audit != nil {
return []kiroIdeRuntimeContract{}, nil
return []managedRuntimeContract{}, nil
}
audit = reference
}
}
if guard == nil || audit == nil ||
!sameKiroIdeAgentID(guard.agentID, audit.agentID) {
return []kiroIdeRuntimeContract{}, nil
!sameManagedAgentID(guard.agentID, audit.agentID) {
return []managedRuntimeContract{}, nil
}
return []kiroIdeRuntimeContract{{
return []managedRuntimeContract{{
agentID: guard.agentID, guardPath: guard.scriptPath,
auditPath: audit.scriptPath,
}}, nil
@@ -449,10 +435,10 @@ func parseKiroIdeLegacyDocument(
return nil, err
}
if guardReference == nil || auditReference == nil ||
!sameKiroIdeAgentID(guardReference.agentID, auditReference.agentID) {
!sameManagedAgentID(guardReference.agentID, auditReference.agentID) {
return document, nil
}
document.contract = &kiroIdeRuntimeContract{
document.contract = &managedRuntimeContract{
agentID: guardReference.agentID, guardPath: guardReference.scriptPath,
auditPath: auditReference.scriptPath,
}
+15 -122
View File
@@ -1,20 +1,13 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type kiroIdeRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
managedRuntimePaths
initialDirectories []kiroIdeDirectoryIdentity
}
@@ -28,84 +21,17 @@ type kiroIdePreparedTransaction struct {
initialDirectories []kiroIdeDirectoryIdentity
}
func validateKiroIdeInstallConfig(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 != kiroIdeAgentKey {
return fmt.Errorf(
"Kiro IDE installation requires agent name %s",
kiroIdeAgentKey,
)
}
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 kiroIdeAgentPaths(config InstallConfig) (*kiroIdeRuntimePaths, error) {
if err := validateKiroIdeInstallConfig(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 := &kiroIdeRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, kiroIdeGuardScript),
auditPath: filepath.Join(agentDirectory, kiroIdeAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameKiroIdePath(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) ||
!sameKiroIdePath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"audit runtime must use the managed Elydora agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
func preflightKiroIdeInstallation(
config InstallConfig,
) (*kiroIdeRuntimePaths, string, error) {
paths, err := kiroIdeAgentPaths(config)
if err := validateManagedInstallConfig(config, kiroIdeAgentKey, "Kiro IDE"); err != nil {
return nil, "", err
}
managed, err := resolveManagedRuntimePaths(config, kiroIdeGuardScript, kiroIdeAuditScript)
if err != nil {
return nil, "", err
}
paths := &kiroIdeRuntimePaths{managedRuntimePaths: *managed}
initialDirectories := make([]kiroIdeDirectoryIdentity, 0, 3)
for _, item := range []struct{ path, label string }{
{filepath.Dir(paths.runtimeRoot), "home directory"},
@@ -124,9 +50,8 @@ func preflightKiroIdeInstallation(
initialDirectories[0].path,
)
}
if err := validateKiroIdeRuntimeIdentity(
paths.agentDirectory,
config.AgentID,
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, kiroIdeAgentKey, "Kiro IDE",
); err != nil {
return nil, "", err
}
@@ -144,29 +69,6 @@ func preflightKiroIdeInstallation(
return paths, nodePath, nil
}
func buildKiroIdeRuntimeConfig(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": kiroIdeAgentKey,
}
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 prepareKiroIdeFile(
filePath string,
label string,
@@ -221,10 +123,11 @@ func prepareKiroIdeInstallation(
if sources == nil || paths == nil || rendered == nil {
return nil, fmt.Errorf("Kiro IDE installation requires prepared sources")
}
runtimeConfig, err := buildKiroIdeRuntimeConfig(config)
runtimeConfig, err := buildManagedRuntimeConfig(config, kiroIdeAgentKey)
if err != nil {
return nil, err
}
guardScript, auditScript := kiroIdeExpectedScripts(config.AgentID, nil)
prepared := &kiroIdePreparedTransaction{
operation: "installation", paths: sources.paths, runtimePaths: paths,
initialDirectories: append(
@@ -238,20 +141,10 @@ func prepareKiroIdeInstallation(
mode os.FileMode
maximum int64
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(kiroIdeAgentKey, config.AgentID, "", false, "")),
0700, maxManagedSourceBytes,
},
{paths.guardPath, "Elydora guard runtime", guardScript, 0700, maxManagedSourceBytes},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600, maxRuntimeConfigBytes},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600, maxProtectedSecretBytes},
{
paths.auditPath, "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
kiroIdeAgentKey, config.AgentID, "", false, true,
)),
0700, maxManagedSourceBytes,
},
{paths.auditPath, "Elydora audit runtime", auditScript, 0700, maxManagedSourceBytes},
} {
change, condition, err := prepareKiroIdeFile(
item.path, item.label, item.contents, item.mode, item.maximum,
@@ -274,7 +167,7 @@ func prepareKiroIdeInstallation(
prepared.changes = append(prepared.changes, documentChange)
}
removeLegacy := sources.legacy.contract != nil &&
sameKiroIdeAgentID(sources.legacy.contract.agentID, config.AgentID)
sameManagedAgentID(sources.legacy.contract.agentID, config.AgentID)
if removeLegacy {
legacyChange, err := prepareSnapshotSourceChange(
sources.legacy.filePath,
@@ -335,7 +228,7 @@ func prepareKiroIdeUninstall(
prepared.changes = append(prepared.changes, documentChange)
}
removeLegacy := sources.legacy.contract != nil &&
(agentID == "" || sameKiroIdeAgentID(sources.legacy.contract.agentID, agentID))
(agentID == "" || sameManagedAgentID(sources.legacy.contract.agentID, agentID))
if removeLegacy {
change, err := prepareSnapshotSourceChange(
sources.legacy.filePath,
@@ -383,7 +276,7 @@ func commitKiroIdeTransaction(
}
workspaceChange := false
for _, change := range prepared.changes {
if change != nil && sameKiroIdePath(change.filePath, prepared.paths.configPath) {
if change != nil && sameManagedPath(change.filePath, prepared.paths.configPath) {
workspaceChange = true
break
}
+33 -249
View File
@@ -1,157 +1,13 @@
package plugins
import (
"bytes"
"fmt"
"path/filepath"
"runtime"
"sort"
"strings"
)
func readKiroIdeRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireKiroIdeRuntimeString(
config map[string]any,
field string,
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 validateKiroIdeRuntimeConfig(
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 := requireKiroIdeRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireKiroIdeRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireKiroIdeRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameKiroIdeAgentID(agentID, expectedAgentID) ||
config["agent_name"] != kiroIdeAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Kiro IDE hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireKiroIdeRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireKiroIdeRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateKiroIdeRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readKiroIdeRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, kiroIdeGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, kiroIdeAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != kiroIdeAgentKey ||
!sameKiroIdeAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Kiro IDE agent %s: %s",
agentID,
configPath,
)
}
return nil
func kiroIdeExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(kiroIdeAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(kiroIdeAgentKey, agentID, "", false, true))
}
func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool {
@@ -161,116 +17,44 @@ func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool
return uint32(snapshot.mode.Perm()) == expected
}
func kiroIdeRuntimeContractExists(contract kiroIdeRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
// kiroIdeRuntimeModesMatch requires the private modes the installer writes.
func kiroIdeRuntimeModesMatch(contract managedRuntimeContract) (bool, error) {
agentDirectory := filepath.Dir(contract.guardPath)
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameKiroIdeAgentID(filepath.Base(agentDirectory), contract.agentID) ||
!sameKiroIdePath(
contract.guardPath,
filepath.Join(agentDirectory, kiroIdeGuardScript),
) ||
!sameKiroIdePath(
contract.auditPath,
filepath.Join(agentDirectory, kiroIdeAuditScript),
) {
return false, nil
for _, item := range []struct {
path, label string
limit int64
mode uint32
}{
{filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", maxRuntimeConfigBytes, 0600},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes, 0600},
{contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes, 0700},
{contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes, 0700},
} {
snapshot, err := readManagedFile(item.path, item.label, item.limit)
if err != nil || snapshot == nil {
return false, err
}
if !kiroIdeRuntimeFileMode(snapshot, item.mode) {
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")
configSnapshot, err := readManagedFile(
configPath,
"Elydora runtime config",
maxRuntimeConfigBytes,
)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if configSnapshot == nil || key == nil || guard == nil || audit == nil {
return false, nil
}
config, err := decodeStrictJSONObject(
configSnapshot.contents,
fmt.Sprintf("Elydora runtime config at %s", configPath),
)
if err != nil {
return false, err
}
if err := validateKiroIdeRuntimeConfig(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)
}
if !kiroIdeRuntimeFileMode(configSnapshot, 0600) ||
!kiroIdeRuntimeFileMode(key, 0600) ||
!kiroIdeRuntimeFileMode(guard, 0700) ||
!kiroIdeRuntimeFileMode(audit, 0700) {
return false, nil
}
expectedGuard := []byte(generateGuardScript(
kiroIdeAgentKey,
contract.agentID,
"",
false,
"",
))
expectedAudit := []byte(buildHookScriptWithOutput(
kiroIdeAgentKey,
contract.agentID,
"",
false,
true,
))
return bytes.Equal(guard.contents, expectedGuard) &&
bytes.Equal(audit.contents, expectedAudit), nil
return true, nil
}
func kiroIdeRuntimeFilesExist(contracts []kiroIdeRuntimeContract) (bool, error) {
func kiroIdeRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := kiroIdeRuntimeContractExists(contract)
exists, err := managedRuntimeContractExists(
contract, kiroIdeAgentKey, "Kiro IDE", kiroIdeExpectedScripts,
)
if err != nil {
return false, err
}
if exists {
return true, nil
if !exists {
continue
}
modes, err := kiroIdeRuntimeModesMatch(contract)
if err != nil || modes {
return modes, err
}
}
return false, nil
+3 -3
View File
@@ -164,7 +164,7 @@ func TestKiroIdeStatusRequiresUniqueExactPairAndRuntimeIntegrity(t *testing.T) {
document = readKiroIdeObject(t, fixture.configPath)
document["hooks"] = append(
requireArray(t, document["hooks"]),
cloneKiroIdeObject(findKiroIdeHook(t, document, kiroIdeGuardName)),
cloneJSONObject(findKiroIdeHook(t, document, kiroIdeGuardName)),
)
writeKiroIdeJSON(t, fixture.configPath, document)
assertStatus(false, false)
@@ -283,7 +283,7 @@ func TestKiroIdeStatusRejectsExtraOrphanAndStaleRuntimeCommands(t *testing.T) {
name = "node.exe"
}
staleNode := filepath.Join(fixture.homeDir, "old-node", name)
command, err := buildKiroIdeCommand(staleNode, fixture.guardPath)
command, err := buildEncodedCommand("Kiro IDE", staleNode, fixture.guardPath)
if err != nil {
t.Fatalf("build stale Kiro IDE command: %v", err)
}
@@ -331,7 +331,7 @@ func TestKiroIdeStatusRejectsStaleWindowsPowerShellLauncher(t *testing.T) {
command := action["command"].(string)
action["command"] = strings.Replace(
command,
codexPowerShellPath(),
windowsPowerShellPath(),
`C:\StaleWindows\System32\WindowsPowerShell\v1.0\powershell.exe`,
1,
)
@@ -20,7 +20,7 @@ func TestKiroIdeRollbackRestoresRuntimeWorkspaceAndLegacySources(t *testing.T) {
legacyKiroIdeGoDocument(fixture, kiroIdeTestAgentID),
)
fixture.plugin.rename = func(source, destination string) error {
if sameKiroIdePath(source, fixture.legacyPath) &&
if sameManagedPath(source, fixture.legacyPath) &&
strings.HasSuffix(destination, ".rollback") {
return errors.New("injected Kiro IDE legacy cleanup failure")
}
@@ -320,7 +320,7 @@ func TestKiroIdeInstallSurvivesAdversarialWorkspaceLinkSwap(t *testing.T) {
}
swapped := false
fixture.plugin.rename = func(source, destination string) error {
if !swapped && sameKiroIdePath(destination, fixture.configPath) &&
if !swapped && sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") {
swapped = true
backup := kiroDirectory + ".original"
@@ -42,6 +42,18 @@ func extractPathFromNodeCommand(command string) string {
return ""
}
func withoutElydoraEntries(hookArray any) []any {
entries, _ := hookArray.([]any)
kept := make([]any, 0, len(entries))
for _, entry := range entries {
if object, ok := entry.(map[string]any); ok && isElydoraHookEntry(object) {
continue
}
kept = append(kept, entry)
}
return kept
}
func hasElydoraEntry(hookArray any) bool {
entries, _ := hookArray.([]any)
for _, entry := range entries {
+37 -123
View File
@@ -6,11 +6,10 @@ import (
"path/filepath"
)
// LettaPlugin manages the Elydora audit hook for Letta Code.
// It merges PreToolUse/PostToolUse hooks into ~/.letta/settings.json.
// LettaPlugin merges Elydora hooks into ~/.letta/settings.json.
type LettaPlugin struct{}
func (p *LettaPlugin) configPath() (string, error) {
func lettaConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
@@ -18,6 +17,16 @@ func (p *LettaPlugin) configPath() (string, error) {
return filepath.Join(home, ".letta", "settings.json"), nil
}
func lettaHookEntry(scriptPath string) map[string]any {
return map[string]any{
"matcher": "*",
"hooks": []any{map[string]any{
"type": "command",
"command": "node " + scriptPath,
}},
}
}
func (p *LettaPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
@@ -26,11 +35,9 @@ func (p *LettaPlugin) Install(config InstallConfig) error {
if config.HookScript != "" {
scriptPath = config.HookScript
}
if err := GenerateHookScript(scriptPath, config); err != nil {
return fmt.Errorf("generate hook script: %w", err)
}
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath(config.AgentID)
@@ -38,70 +45,21 @@ func (p *LettaPlugin) Install(config InstallConfig) error {
return err
}
}
configPath, err := p.configPath()
configPath, err := lettaConfigPath()
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
if err != nil {
return err
}
hooks, _ := settings["hooks"].(map[string]interface{})
hooks, _ := settings["hooks"].(map[string]any)
if hooks == nil {
hooks = make(map[string]interface{})
hooks = map[string]any{}
}
// --- PreToolUse (guard — freeze enforcement) ---
preToolUse, _ := hooks["PreToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
preFiltered = append(preFiltered, entry)
}
guardEntry := map[string]interface{}{
"matcher": "*",
"hooks": []interface{}{
map[string]interface{}{
"type": "command",
"command": "node " + guardPath,
},
},
}
preFiltered = append(preFiltered, guardEntry)
hooks["PreToolUse"] = preFiltered
// --- PostToolUse (audit logging) ---
postToolUse, _ := hooks["PostToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
postFiltered = append(postFiltered, entry)
}
hookEntry := map[string]interface{}{
"matcher": "*",
"hooks": []interface{}{
map[string]interface{}{
"type": "command",
"command": "node " + scriptPath,
},
},
}
postFiltered = append(postFiltered, hookEntry)
hooks["PostToolUse"] = postFiltered
hooks["PreToolUse"] = append(withoutElydoraEntries(hooks["PreToolUse"]), lettaHookEntry(guardPath))
hooks["PostToolUse"] = append(withoutElydoraEntries(hooks["PostToolUse"]), lettaHookEntry(scriptPath))
settings["hooks"] = hooks
if err := writeJSONFile(configPath, settings); err != nil {
return err
}
@@ -110,74 +68,38 @@ func (p *LettaPlugin) Install(config InstallConfig) error {
}
func (p *LettaPlugin) Uninstall(agentID string) error {
configPath, err := p.configPath()
configPath, err := lettaConfigPath()
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
if err != nil {
return err
}
hooks, _ := settings["hooks"].(map[string]interface{})
hooks, _ := settings["hooks"].(map[string]any)
if hooks == nil {
fmt.Println("No Letta Code hooks found.")
return nil
}
// Remove PreToolUse Elydora entries
preToolUse, _ := hooks["PreToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
for _, event := range []string{"PreToolUse", "PostToolUse"} {
kept := withoutElydoraEntries(hooks[event])
if len(kept) == 0 {
delete(hooks, event)
} else {
hooks[event] = kept
}
preFiltered = append(preFiltered, entry)
}
if len(preFiltered) == 0 {
delete(hooks, "PreToolUse")
} else {
hooks["PreToolUse"] = preFiltered
}
// Remove PostToolUse Elydora entries
postToolUse, _ := hooks["PostToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
postFiltered = append(postFiltered, entry)
}
if len(postFiltered) == 0 {
delete(hooks, "PostToolUse")
} else {
hooks["PostToolUse"] = postFiltered
}
if len(hooks) == 0 {
delete(settings, "hooks")
} else {
settings["hooks"] = hooks
}
if err := writeJSONFile(configPath, settings); err != nil {
return err
}
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
if err := removeAgentScripts(agentID); err != nil {
return err
}
}
fmt.Println("Uninstalled Elydora hook for Letta Code.")
@@ -185,38 +107,30 @@ func (p *LettaPlugin) Uninstall(agentID string) error {
}
func (p *LettaPlugin) Status() (PluginStatus, error) {
configPath, err := p.configPath()
configPath, err := lettaConfigPath()
if err != nil {
return PluginStatus{}, err
}
status := PluginStatus{
AgentName: "letta",
DisplayName: "Letta Code",
ConfigPath: configPath,
}
settings, err := readJSONFile(configPath)
if err != nil {
return status, err
}
hooks, _ := settings["hooks"].(map[string]any)
if hooks == nil {
return status, nil
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks != nil {
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractElydoraScriptPath(hooks["PostToolUse"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
status.HookConfigured = hasElydoraEntry(hooks["PreToolUse"]) &&
hasElydoraEntry(hooks["PostToolUse"])
if scriptPath := extractElydoraScriptPath(hooks["PostToolUse"]); scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil
}
+95
View File
@@ -0,0 +1,95 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
)
func buildManagedRuntimeConfig(config InstallConfig, agentKey string) ([]byte, error) {
value := map[string]any{
"org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID,
"base_url": config.BaseURL, "agent_name": agentKey,
}
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
}
// managedRuntimeFileChanges prepares the guard, config, key, and audit runtime changes.
func managedRuntimeFileChanges(
config InstallConfig,
paths *managedRuntimePaths,
agentKey, guardScript, auditScript string,
) ([]*fileChange, error) {
runtimeConfig, err := buildManagedRuntimeConfig(config, agentKey)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{paths.guardPath, "Elydora guard runtime", []byte(guardScript), 0700},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600},
{paths.auditPath, "Elydora audit runtime", []byte(auditScript), 0700},
}
changes := make([]*fileChange, 0, len(items)+1)
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)
}
return changes, nil
}
func hasFileChanges(changes []*fileChange) bool {
for _, change := range changes {
if change != nil {
return true
}
}
return false
}
// writeManagedChanges creates the runtime and settings directories, then commits.
func writeManagedChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot, agentDirectory, settingsDirectory, settingsLabel string,
) error {
if !hasFileChanges(changes) {
return nil
}
if agentDirectory != "" {
if err := EnsurePrivateDirectory(runtimeRoot); err != nil {
return err
}
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
return err
}
}
if settingsDirectory != "" {
if err := ensureManagedDirectory(settingsDirectory, settingsLabel); err != nil {
return err
}
}
return writeChanges(changes, label, rename)
}
+413
View File
@@ -0,0 +1,413 @@
package plugins
import (
"fmt"
"path/filepath"
"runtime"
"sort"
"strings"
)
type managedRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type managedRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type managedScriptReference struct {
agentID string
scriptPath string
}
func stringSet(values ...string) map[string]struct{} {
result := make(map[string]struct{}, len(values))
for _, value := range values {
result[value] = struct{}{}
}
return result
}
var managedRuntimeConfigFields = stringSet(
"org_id", "agent_id", "kid", "base_url", "token", "agent_name",
)
func sameManagedPath(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
}
return sameManagedName(left, right)
}
// sameManagedName compares identifiers case-insensitively on Windows.
func sameManagedName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameManagedAgentID(left, right string) bool {
return sameManagedName(left, right)
}
func managedReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
// managedScriptWithin accepts absolute <runtimeRoot>/<agent>/<scriptName> paths.
func managedScriptWithin(scriptPath, scriptName, runtimeRoot string) *managedScriptReference {
if !filepath.IsAbs(scriptPath) || !sameManagedName(filepath.Base(scriptPath), scriptName) {
return nil
}
agentDirectory := filepath.Dir(scriptPath)
if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil
}
return &managedScriptReference{agentID: agentID, scriptPath: scriptPath}
}
func resolveManagedScript(scriptPath, scriptName string) (*managedScriptReference, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
return managedScriptWithin(scriptPath, scriptName, runtimeRoot), nil
}
func validateManagedInstallConfig(config InstallConfig, agentKey, product string) 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 strings.TrimSpace(field.value) == "" {
return fmt.Errorf("%s is required", field.name)
}
}
if config.AgentName != agentKey {
return fmt.Errorf("%s installation requires agent name %s", product, agentKey)
}
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 resolveManagedRuntimePaths(
config InstallConfig,
guardScript, auditScript string,
) (*managedRuntimePaths, error) {
if config.AgentID == "" {
return nil, fmt.Errorf("agent ID is required")
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return nil, err
}
paths := &managedRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, guardScript),
auditPath: filepath.Join(agentDirectory, auditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameManagedPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameManagedPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
func readManagedRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireManagedRuntimeString(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 validateManagedRuntimeConfig(
config map[string]any,
expectedAgentID, configPath, agentKey, product string,
) error {
fields := make([]string, 0, len(config))
for field := range config {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
if _, ok := managedRuntimeConfigFields[field]; !ok {
return fmt.Errorf(
`elydora runtime config has unsupported field %q: %s`,
field,
configPath,
)
}
}
for _, field := range []string{"org_id", "kid"} {
if _, err := requireManagedRuntimeString(config, field, configPath); err != nil {
return err
}
}
agentID, err := requireManagedRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameManagedAgentID(agentID, expectedAgentID) || config["agent_name"] != agentKey {
return fmt.Errorf(
"elydora runtime identity does not match %s hooks: %s",
product,
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireManagedRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireManagedRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf("elydora runtime config base URL is invalid at %s: %w", configPath, err)
}
return nil
}
type managedArtifact struct {
path string
label string
limit int64
}
func managedRuntimeArtifacts(agentDirectory, guardScript, auditScript string) []managedArtifact {
return []managedArtifact{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, guardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, auditScript), "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},
}
}
// validateManagedRuntimeIdentity rejects an agent directory owned by another identity.
func validateManagedRuntimeIdentity(
agentDirectory, agentID, agentKey, product string,
extraArtifacts ...managedArtifact,
) error {
rootExists, err := managedPhysicalDirectoryExists(
filepath.Dir(agentDirectory), "Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory, "Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readManagedRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
artifacts := append(managedRuntimeArtifacts(agentDirectory, "guard.js", "hook.js"), extraArtifacts...)
for _, item := range artifacts {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != agentKey || !sameManagedAgentID(configuredID, agentID) {
return fmt.Errorf(
"elydora runtime config identity does not match %s agent %s: %s",
product,
agentID,
configPath,
)
}
return nil
}
type expectedRuntimeScripts func(agentID string, config map[string]any) (guard, audit []byte)
// managedRuntimeContractExists is the strict status check; nil expected accepts non-empty scripts.
func managedRuntimeContractExists(
contract managedRuntimeContract,
agentKey, product string,
expected expectedRuntimeScripts,
) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath)
if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) ||
!sameManagedPath(contract.auditPath, filepath.Join(agentDirectory, "hook.js")) {
return false, nil
}
for _, directory := range []struct{ path, label string }{
{runtimeRoot, "Elydora runtime directory"},
{agentDirectory, "Elydora agent runtime directory"},
} {
exists, err := managedPhysicalDirectoryExists(directory.path, directory.label)
if err != nil || !exists {
return false, err
}
}
configPath := filepath.Join(agentDirectory, "config.json")
keyPath := filepath.Join(agentDirectory, "private.key")
config, configExists, err := readManagedRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
audit, err := readManagedFile(contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateManagedRuntimeConfig(config, contract.agentID, configPath, agentKey, product); 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)
}
if expected == nil {
return len(guard.contents) > 0 && len(audit.contents) > 0, nil
}
expectedGuard, expectedAudit := expected(contract.agentID, config)
return string(guard.contents) == string(expectedGuard) &&
string(audit.contents) == string(expectedAudit), nil
}
func managedRuntimeFilesExist(
contracts []managedRuntimeContract,
agentKey, product string,
expected expectedRuntimeScripts,
) (bool, error) {
for _, contract := range contracts {
exists, err := managedRuntimeContractExists(contract, agentKey, product, expected)
if err != nil || exists {
return exists, err
}
}
return false, nil
}
// managedRuntimePresent is the presence-only status check; identity mismatch reports false.
func managedRuntimePresent(contract managedRuntimeContract, agentKey string) (bool, error) {
agentDirectory := filepath.Dir(contract.guardPath)
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory, "Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return false, err
}
config, exists, err := readManagedRuntimeConfig(filepath.Join(agentDirectory, "config.json"))
if err != nil {
return false, err
}
configuredID, idOK := config["agent_id"].(string)
if !exists || !idOK || config["agent_name"] != agentKey ||
!sameManagedAgentID(configuredID, contract.agentID) {
return false, nil
}
for _, item := range []managedArtifact{
{contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes},
{contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil || !exists {
return false, inspectErr
}
}
return true, nil
}
func managedRuntimeFilesPresent(contracts []managedRuntimeContract, agentKey string) (bool, error) {
for _, contract := range contracts {
present, err := managedRuntimePresent(contract, agentKey)
if err != nil || present {
return present, err
}
}
return false, nil
}
+57 -45
View File
@@ -1,14 +1,17 @@
package plugins
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"regexp"
"strconv"
)
// OpenCodePlugin manages the Elydora audit hook for OpenCode.
// It writes .config/opencode/plugins/elydora-audit.mjs in the home directory.
var openCodeScriptPathPattern = regexp.MustCompile(`(?m)^const (GUARD_SCRIPT_PATH|HOOK_SCRIPT_PATH) = (".*");$`)
// OpenCodePlugin writes ~/.config/opencode/plugins/elydora-audit.mjs.
type OpenCodePlugin struct{}
func (p *OpenCodePlugin) configPath() (string, error) {
@@ -59,8 +62,7 @@ func (p *OpenCodePlugin) Install(config InstallConfig) error {
}
func buildOpenCodePlugin(scriptPath, guardPath string) string {
return fmt.Sprintf(`// Elydora Audit Plugin for OpenCode
// Generated by @elydora/sdk — DO NOT EDIT
return fmt.Sprintf(`// Elydora audit plugin for OpenCode. Generated by the Elydora CLI; do not edit.
import { spawn, spawnSync } from 'node:child_process';
@@ -112,62 +114,72 @@ func (p *OpenCodePlugin) Uninstall(agentID string) error {
}
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
if err := removeAgentScripts(agentID); err != nil {
return err
}
}
fmt.Println("Uninstalled Elydora hook for OpenCode.")
return nil
}
func openCodeScriptPaths(data []byte, pluginFile string) (string, string, error) {
paths := map[string]string{}
for _, match := range openCodeScriptPathPattern.FindAllSubmatch(data, -1) {
value, err := strconv.Unquote(string(match[2]))
if err != nil {
return "", "", fmt.Errorf(
"OpenCode plugin at %s has an invalid %s: %w", pluginFile, match[1], err,
)
}
paths[string(match[1])] = value
}
guardPath, hasGuard := paths["GUARD_SCRIPT_PATH"]
hookPath, hasHook := paths["HOOK_SCRIPT_PATH"]
if !hasGuard || !hasHook {
return "", "", fmt.Errorf("OpenCode plugin at %s has no Elydora script paths", pluginFile)
}
return guardPath, hookPath, nil
}
func (p *OpenCodePlugin) Status() (PluginStatus, error) {
pluginFile, err := p.configPath()
if err != nil {
return PluginStatus{}, err
}
status := PluginStatus{
AgentName: "opencode",
DisplayName: "OpenCode",
ConfigPath: pluginFile,
}
if _, err := os.Stat(pluginFile); err == nil {
status.HookConfigured = true
// Read the plugin file to extract the hook script path
data, readErr := os.ReadFile(pluginFile)
if readErr == nil {
content := string(data)
// Look for the elydora hook.js path in the plugin file content
if idx := strings.Index(content, ".elydora"); idx >= 0 {
// Extract path from the spawn('node', ['/path/to/hook.js']) pattern
// Pragmatic: scan ~/.elydora/*/hook.js for existence
home, homeErr := os.UserHomeDir()
if homeErr == nil {
entries, dirErr := os.ReadDir(filepath.Join(home, ".elydora"))
if dirErr == nil {
for _, entry := range entries {
if entry.IsDir() {
hookPath := filepath.Join(home, ".elydora", entry.Name(), "hook.js")
if _, statErr := os.Stat(hookPath); statErr == nil {
status.HookScriptExists = true
break
}
}
}
}
}
}
}
data, err := os.ReadFile(pluginFile)
if errors.Is(err, os.ErrNotExist) {
return status, nil
}
status.Installed = status.HookConfigured && status.HookScriptExists
if err != nil {
return status, fmt.Errorf("read %s: %w", pluginFile, err)
}
status.HookConfigured = true
guardPath, hookPath, err := openCodeScriptPaths(data, pluginFile)
if err != nil {
return status, err
}
guard, err := resolveManagedScript(guardPath, "guard.js")
if err != nil {
return status, err
}
audit, err := resolveManagedScript(hookPath, "hook.js")
if err != nil {
return status, err
}
if guard == nil || audit == nil || !sameManagedAgentID(guard.agentID, audit.agentID) {
return status, nil
}
status.HookScriptExists, err = managedRuntimePresent(managedRuntimeContract{
agentID: guard.agentID, guardPath: guard.scriptPath, auditPath: audit.scriptPath,
}, "opencode")
if err != nil {
return status, err
}
status.Installed = status.HookScriptExists
return status, nil
}
+2 -2
View File
@@ -73,7 +73,7 @@ func (p *QwenPlugin) Uninstall(agentID string) error {
if err != nil {
return err
}
runtimeRoot, err := qwenRuntimeRoot()
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return err
}
@@ -104,7 +104,7 @@ func (p *QwenPlugin) Status() (PluginStatus, error) {
return status, err
}
status.ConfigPath = sources.user.filePath
runtimeRoot, err := qwenRuntimeRoot()
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return status, err
}
-116
View File
@@ -1,116 +0,0 @@
package plugins
import (
"path/filepath"
"runtime"
"strings"
)
const qwenPOSIXApostrophe = `'"'"'`
type qwenRuntimeReference struct {
agentID string
scriptPath string
}
func buildQwenCommand(nodePath, scriptPath string) string {
if runtime.GOOS == "windows" {
return "& " + quoteQwenPowerShell(nodePath) + " " +
quoteQwenPowerShell(scriptPath) + "; exit $LASTEXITCODE"
}
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
}
func quoteQwenPowerShell(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func parseQwenCommand(command string) (string, string, bool) {
start := 0
suffix := ""
if runtime.GOOS == "windows" {
if !strings.HasPrefix(command, "& ") {
return "", "", false
}
start = 2
suffix = "; exit $LASTEXITCODE"
}
executable, next, ok := readQwenQuotedArgument(command, start)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readQwenQuotedArgument(command, next+1)
return executable, script, ok && command[end:] == suffix &&
executable != "" && script != ""
}
func readQwenQuotedArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if runtime.GOOS == "windows" && strings.HasPrefix(command[index:], "''") {
value.WriteByte('\'')
index += 2
continue
}
if runtime.GOOS != "windows" && strings.HasPrefix(command[index:], qwenPOSIXApostrophe) {
value.WriteByte('\'')
index += len(qwenPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func sameQwenPath(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 sameQwenAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func isQwenNodeExecutable(filePath string) bool {
name := strings.ToLower(filepath.Base(filePath))
return name == "node" || name == "node.exe"
}
func qwenRuntimeReferenceForCommand(
command, scriptName, runtimeRoot string,
) (qwenRuntimeReference, bool) {
executable, scriptPath, ok := parseQwenCommand(command)
if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) ||
!isQwenNodeExecutable(executable) || filepath.Base(scriptPath) != scriptName {
return qwenRuntimeReference{}, false
}
agentDirectory := filepath.Dir(scriptPath)
if !sameQwenPath(filepath.Dir(agentDirectory), runtimeRoot) {
return qwenRuntimeReference{}, false
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return qwenRuntimeReference{}, false
}
return qwenRuntimeReference{agentID: agentID, scriptPath: scriptPath}, true
}
+38 -48
View File
@@ -4,7 +4,6 @@ import (
"path/filepath"
"runtime"
"sort"
"strings"
)
const (
@@ -40,12 +39,6 @@ type qwenManagedRemoval struct {
removeGroup bool
}
type qwenRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
func qwenExpectedShell() string {
if runtime.GOOS == "windows" {
return "powershell"
@@ -58,41 +51,52 @@ func buildQwenGroup(nodePath, scriptPath, name string) map[string]any {
"hooks": []any{map[string]any{
"type": "command",
"name": name,
"command": buildQwenCommand(nodePath, scriptPath),
"command": buildShellCommand(nodePath, scriptPath),
"shell": qwenExpectedShell(),
"timeout": qwenHookTimeout,
}},
}
}
func qwenRuntimeReferenceForCommand(
command, scriptName, runtimeRoot string,
) *managedScriptReference {
executable, scriptPath, ok := parseShellCommand(command)
if !ok || !filepath.IsAbs(executable) || !isNodeExecutable(executable) {
return nil
}
return managedScriptWithin(scriptPath, scriptName, runtimeRoot)
}
func currentQwenReference(
handler map[string]any,
scriptName, hookName, runtimeRoot string,
) (qwenRuntimeReference, bool) {
) *managedScriptReference {
if len(handler) != 5 || handler["type"] != "command" ||
handler["name"] != hookName || handler["shell"] != qwenExpectedShell() ||
handler["timeout"] != qwenHookTimeout {
return qwenRuntimeReference{}, false
return nil
}
command, ok := handler["command"].(string)
if !ok {
return qwenRuntimeReference{}, false
return nil
}
return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot)
}
// legacyQwenReference reads the pre-2.1 unnamed handler.
func legacyQwenReference(
handler map[string]any,
scriptName, runtimeRoot string,
) (qwenRuntimeReference, bool) {
) *managedScriptReference {
if len(handler) != 4 || handler["type"] != "command" ||
handler["shell"] != qwenExpectedShell() ||
handler["timeout"] != qwenHookTimeout {
return qwenRuntimeReference{}, false
return nil
}
command, ok := handler["command"].(string)
if !ok {
return qwenRuntimeReference{}, false
return nil
}
return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot)
}
@@ -101,19 +105,19 @@ func managedQwenReference(
handler map[string]any,
scriptName, hookName, runtimeRoot string,
includeLegacy bool,
) (qwenRuntimeReference, bool) {
if reference, ok := currentQwenReference(
) *managedScriptReference {
if reference := currentQwenReference(
handler,
scriptName,
hookName,
runtimeRoot,
); ok {
return reference, true
); reference != nil {
return reference
}
if includeLegacy {
return legacyQwenReference(handler, scriptName, runtimeRoot)
}
return qwenRuntimeReference{}, false
return nil
}
func exactCurrentQwenGroup(group map[string]any) bool {
@@ -138,14 +142,15 @@ func managedQwenRemovals(
handlers := group["hooks"].([]any)
indexes := make([]int, 0)
for handlerIndex, handlerValue := range handlers {
reference, managed := managedQwenReference(
reference := managedQwenReference(
handlerValue.(map[string]any),
contract.script,
contract.name,
runtimeRoot,
true,
)
if managed && (agentID == "" || sameQwenAgentID(reference.agentID, agentID)) {
if reference != nil &&
(agentID == "" || sameManagedAgentID(reference.agentID, agentID)) {
indexes = append(indexes, handlerIndex)
}
}
@@ -163,18 +168,11 @@ func managedQwenRemovals(
return removals
}
func normalizedQwenAgentID(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func qwenReferencesForEvent(
settings qwenHookSettings,
event, scriptName, hookName, runtimeRoot string,
) map[string][]qwenRuntimeReference {
references := map[string][]qwenRuntimeReference{}
) map[string][]managedScriptReference {
references := map[string][]managedScriptReference{}
groups, _ := settings[event].([]any)
for _, groupValue := range groups {
group := groupValue.(map[string]any)
@@ -182,17 +180,17 @@ func qwenReferencesForEvent(
continue
}
for _, handlerValue := range group["hooks"].([]any) {
reference, ok := currentQwenReference(
reference := currentQwenReference(
handlerValue.(map[string]any),
scriptName,
hookName,
runtimeRoot,
)
if !ok {
if reference == nil {
continue
}
key := normalizedQwenAgentID(reference.agentID)
references[key] = append(references[key], reference)
key := managedReferenceKey(reference.agentID)
references[key] = append(references[key], *reference)
}
}
return references
@@ -201,7 +199,7 @@ func qwenReferencesForEvent(
func qwenRuntimeContracts(
settings qwenHookSettings,
runtimeRoot string,
) []qwenRuntimeContract {
) []managedRuntimeContract {
guards := qwenReferencesForEvent(
settings,
"PreToolUse",
@@ -223,31 +221,23 @@ func qwenRuntimeContracts(
qwenAuditHookName,
runtimeRoot,
)
contracts := make([]qwenRuntimeContract, 0)
contracts := make([]managedRuntimeContract, 0)
for key, guard := range guards {
post := posts[key]
failure := failures[key]
if len(guard) != 1 || len(post) != 1 || len(failure) != 1 ||
!sameQwenPath(post[0].scriptPath, failure[0].scriptPath) {
!sameManagedPath(post[0].scriptPath, failure[0].scriptPath) {
continue
}
contracts = append(contracts, qwenRuntimeContract{
contracts = append(contracts, managedRuntimeContract{
agentID: guard[0].agentID,
guardPath: guard[0].scriptPath,
auditPath: post[0].scriptPath,
})
}
sort.Slice(contracts, func(left, right int) bool {
return normalizedQwenAgentID(contracts[left].agentID) <
normalizedQwenAgentID(contracts[right].agentID)
return managedReferenceKey(contracts[left].agentID) <
managedReferenceKey(contracts[right].agentID)
})
return contracts
}
func qwenRuntimeRoot() (string, error) {
return AgentRuntimeRoot()
}
func qwenContractDirectory(contract qwenRuntimeContract) string {
return filepath.Dir(contract.guardPath)
}
+20 -164
View File
@@ -1,158 +1,45 @@
package plugins
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type qwenRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
import "fmt"
type preparedQwenInstallation struct {
changes []*fileChange
preconditions []filePrecondition
paths *qwenRuntimePaths
}
type qwenRuntimeConfig struct {
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
KID string `json:"kid"`
BaseURL string `json:"base_url"`
Token string `json:"token,omitempty"`
AgentName string `json:"agent_name"`
}
func validateQwenInstallConfig(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 != qwenAgentKey {
return fmt.Errorf(
"Qwen Code installation requires agent name %s",
qwenAgentKey,
)
}
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 qwenAgentPaths(config InstallConfig) (*qwenRuntimePaths, error) {
if err := validateQwenInstallConfig(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 := &qwenRuntimePaths{
runtimeRoot: runtimeRoot,
agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, qwenGuardScript),
auditPath: filepath.Join(agentDirectory, qwenAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameQwenPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameQwenPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
paths.auditPath,
)
}
return paths, nil
paths *managedRuntimePaths
}
func preflightQwenInstallation(
config InstallConfig,
sources *qwenSources,
) (*qwenRuntimePaths, string, error) {
) (*managedRuntimePaths, string, error) {
if sources == nil || sources.user == nil || sources.user.filePath == "" {
return nil, "", fmt.Errorf("Qwen Code installation requires user settings")
}
if err := requireQwenHooksEnabled(sources); err != nil {
return nil, "", err
}
paths, err := qwenAgentPaths(config)
if err := validateManagedInstallConfig(config, qwenAgentKey, "Qwen Code"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, qwenGuardScript, qwenAuditScript)
if err != nil {
return nil, "", err
}
if err := validateQwenRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, qwenAgentKey, "Qwen Code",
); err != nil {
return nil, "", err
}
nodePath, err := resolveNodeRuntime()
nodePath, err := resolveAbsoluteNodeRuntime("Qwen Code")
if err != nil {
return nil, "", err
}
if !filepath.IsAbs(nodePath) || !isQwenNodeExecutable(nodePath) {
return nil, "", fmt.Errorf(
"Qwen Code hooks require an absolute Node.js executable path",
)
}
return paths, nodePath, nil
}
func buildQwenRuntimeConfig(config InstallConfig) ([]byte, error) {
encoded, err := json.MarshalIndent(qwenRuntimeConfig{
OrgID: config.OrgID,
AgentID: config.AgentID,
KID: config.KID,
BaseURL: config.BaseURL,
Token: config.Token,
AgentName: qwenAgentKey,
}, "", " ")
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 qwenExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(qwenAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(qwenAgentKey, agentID, "", false, true))
}
func prepareRenderedQwenChange(
@@ -180,7 +67,7 @@ func qwenReadOnlyPreconditions(
}
result := make([]filePrecondition, 0, len(sources.preconditions))
for _, condition := range sources.preconditions {
if changedPath != "" && sameQwenPath(condition.filePath, changedPath) {
if changedPath != "" && sameManagedPath(condition.filePath, changedPath) {
continue
}
result = append(result, condition)
@@ -198,47 +85,16 @@ func prepareQwenInstallation(
return nil, err
}
if rendered == nil || rendered.document == nil ||
!sameQwenPath(rendered.document.filePath, sources.user.filePath) {
!sameManagedPath(rendered.document.filePath, sources.user.filePath) {
return nil, fmt.Errorf("Qwen Code rendered user settings are required")
}
runtimeConfig, err := buildQwenRuntimeConfig(config)
guardScript, auditScript := qwenExpectedScripts(config.AgentID, nil)
changes, err := managedRuntimeFileChanges(
config, paths, qwenAgentKey, string(guardScript), string(auditScript),
)
if err != nil {
return nil, err
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath,
"Elydora guard runtime",
[]byte(generateGuardScript(qwenAgentKey, 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(
qwenAgentKey,
config.AgentID,
"",
false,
true,
)),
0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
for _, item := range items {
change, changeErr := prepareFileChange(item.path, item.label, item.content, item.mode)
if changeErr != nil {
return nil, changeErr
}
changes = append(changes, change)
}
settingsChange, err := prepareRenderedQwenChange(rendered)
if err != nil {
return nil, err
-266
View File
@@ -1,266 +0,0 @@
package plugins
import (
"bytes"
"fmt"
"path/filepath"
"regexp"
"sort"
"strings"
)
var qwenPathSeparatorPattern = regexp.MustCompile(`[/\\]+`)
func readQwenRuntimeConfig(path string) (map[string]any, bool, error) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
if err != nil || snapshot == nil {
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireQwenRuntimeString(
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 validateQwenRuntimeConfig(
config map[string]any,
expectedAgentID, 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 := requireQwenRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireQwenRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireQwenRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameQwenAgentID(agentID, expectedAgentID) || config["agent_name"] != qwenAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Qwen Code hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireQwenRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireQwenRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateQwenRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readQwenRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, qwenGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, qwenAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != qwenAgentKey ||
!sameQwenAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Qwen Code agent %s: %s",
agentID,
configPath,
)
}
return nil
}
func validQwenContractPaths(contract qwenRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := qwenContractDirectory(contract)
return sameQwenPath(filepath.Dir(agentDirectory), runtimeRoot) &&
sameQwenPath(
contract.guardPath,
filepath.Join(agentDirectory, qwenGuardScript),
) &&
sameQwenPath(
contract.auditPath,
filepath.Join(agentDirectory, qwenAuditScript),
), nil
}
func qwenRuntimeContractExists(contract qwenRuntimeContract) (bool, error) {
validPaths, err := validQwenContractPaths(contract)
if err != nil || !validPaths {
return false, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return false, err
}
agentDirectory := qwenContractDirectory(contract)
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 := readQwenRuntimeConfig(configPath)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if !configExists || key == nil || guard == nil || audit == nil {
return false, nil
}
if err := validateQwenRuntimeConfig(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)
}
expectedGuard := []byte(generateGuardScript(
qwenAgentKey,
contract.agentID,
"",
false,
"",
))
expectedAudit := []byte(buildHookScriptWithOutput(
qwenAgentKey,
contract.agentID,
"",
false,
true,
))
return bytes.Equal(guard.contents, expectedGuard) &&
bytes.Equal(audit.contents, expectedAudit), nil
}
func qwenRuntimeFilesExist(contracts []qwenRuntimeContract) (bool, error) {
for _, contract := range contracts {
exists, err := qwenRuntimeContractExists(contract)
if err != nil {
return false, err
}
if exists {
return true, nil
}
}
return false, nil
}
+1 -1
View File
@@ -244,7 +244,7 @@ func TestQwenTransactionRollsBackAllFilesAndCleansStaging(t *testing.T) {
original := readQwenTestFile(t, fixture.configPath)
failed := false
fixture.plugin.rename = func(source, destination string) error {
if !failed && sameQwenPath(destination, fixture.configPath) {
if !failed && sameManagedPath(destination, fixture.configPath) {
failed = true
return errors.New("simulated settings commit failure")
}
+4 -1
View File
@@ -4,10 +4,13 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
)
var qwenPathSeparatorPattern = regexp.MustCompile(`[/\\]+`)
var qwenHomeEnvironmentKeys = [...]string{"QWEN_HOME", "QWEN_RUNTIME_DIR"}
type qwenDisableControl struct {
@@ -154,7 +157,7 @@ func resolveQwenRouting() (*qwenRoutingResult, error) {
if resolveErr != nil {
return nil, resolveErr
}
if !sameQwenPath(discoveredDirectory, initialDirectory) {
if !sameManagedPath(discoveredDirectory, initialDirectory) {
if err := readCandidate(filepath.Join(discoveredDirectory, ".env")); err != nil {
return nil, err
}
+3 -3
View File
@@ -101,12 +101,12 @@ func TestQwenRoutingReadsDiscoveredHomeEnvironment(t *testing.T) {
if err != nil {
t.Fatalf("resolve Qwen routing: %v", err)
}
if !sameQwenPath(routing.qwenHome, discovered) {
if !sameManagedPath(routing.qwenHome, discovered) {
t.Fatalf("Qwen home = %s", routing.qwenHome)
}
found := false
for _, condition := range routing.preconditions {
found = found || sameQwenPath(condition.filePath, discoveredEnv)
found = found || sameManagedPath(condition.filePath, discoveredEnv)
}
if !found {
t.Fatalf("discovered environment is absent from preconditions: %#v", routing.preconditions)
@@ -127,7 +127,7 @@ func TestQwenRoutingSkipsDotenvWhenBothEnvironmentValuesExist(t *testing.T) {
if err != nil {
t.Fatalf("resolve Qwen routing: %v", err)
}
if !sameQwenPath(routing.qwenHome, explicit) || len(routing.preconditions) != 0 {
if !sameManagedPath(routing.qwenHome, explicit) || len(routing.preconditions) != 0 {
t.Fatalf("routing = %#v", routing)
}
}
+5
View File
@@ -0,0 +1,5 @@
package plugins
func qwenRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
return managedRuntimeFilesExist(contracts, qwenAgentKey, "Qwen Code", qwenExpectedScripts)
}
+1 -1
View File
@@ -153,7 +153,7 @@ func legacyQwenGroupForTest(
"matcher": "*",
"hooks": []any{map[string]any{
"type": "command",
"command": buildQwenCommand(nodePath, scriptPath),
"command": buildShellCommand(nodePath, scriptPath),
"shell": qwenExpectedShell(),
"timeout": qwenHookTimeout,
}},
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more