10 Commits
Author SHA1 Message Date
雷电芽衣 6c39ce7781 chore(go): release 2.1.1
Carries the non-idempotent retry guard, previous_token_grace_until, and
agent-id path escaping.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 21:47:57 -04:00
雷电芽衣 b84c79816c docs: drop the em-dash from the genesis hash comment
Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 21:07:25 -04:00
雷电芽衣 ce0c924c58 fix: stop replaying non-idempotent requests and expose the rotation grace
A retried POST /v1/auth/rotate loses the replacement token because the
presented token is already in grace, and a retried webhook registration
creates duplicates. Only idempotent methods now retry. Add
previous_token_grace_until to RotateApiTokenResponse and percent-encode
the agent id in request paths.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 20:12:48 -04:00
雷电芽衣 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
110 changed files with 1993 additions and 6801 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. - 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. - 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 ## Code Quality
- Preserve the minimum Go version declared in `go.mod`. - 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: 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 ./cmd/elydora/plugins -run <Provider> -count=1
go test ./... go test ./...
go test -race ./... go test -race ./...
@@ -75,6 +84,8 @@ govulncheck ./...
git diff --check 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. 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. Commit and push one root issue before starting the next one.
+51 -26
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. 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 ## Quick Start
```go ```go
@@ -23,18 +25,12 @@ import (
) )
func main() { func main() {
// Authenticate // Create client with an API token issued in the Console
auth, err := elydora.Login("https://api.elydora.com", "user@example.com", "password")
if err != nil {
log.Fatal(err)
}
// Create client
client, err := elydora.NewClient(&elydora.Config{ client, err := elydora.NewClient(&elydora.Config{
OrgID: auth.User.OrgID, OrgID: "org-123",
AgentID: "my-agent-id", AgentID: "my-agent-id",
PrivateKey: "<base64url-encoded-ed25519-seed>", PrivateKey: "<base64url-encoded-ed25519-seed>",
Token: auth.Token, Token: "<api-token>",
}) })
if err != nil { if err != nil {
log.Fatal(err) 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 install` | Install Elydora audit hook for a coding agent |
| `elydora uninstall` | Remove 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 agents` | List supported coding agents |
| `elydora version` | Print the CLI version |
### Supported Agents ### Supported Agents
@@ -162,24 +159,23 @@ client.SetToken("new-api-token")
### Authentication ### Authentication
```go ```go
// Register a new user and organization // Get the authenticated user and current 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
me, err := client.GetMe() 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{ tokenResp, err := client.IssueApiToken(&elydora.IssueApiTokenRequest{
TTLSeconds: &ttlSeconds, 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 ### Operations
```go ```go
@@ -219,8 +215,11 @@ agent, err := client.RegisterAgent(&elydora.RegisterAgentRequest{
// Get agent details // Get agent details
details, err := client.GetAgent(agentID) details, err := client.GetAgent(agentID)
// Change the integration type
updated, err := client.UpdateAgent(agentID, elydora.IntegrationTypeClaudecode)
// Freeze an agent // Freeze an agent
err := client.FreezeAgent(agentID, "security review") frozen, err := client.FreezeAgent(agentID, "security review")
// Revoke a key // Revoke a key
err := client.RevokeKey(agentID, kid, "key rotation") err := client.RevokeKey(agentID, kid, "key rotation")
@@ -229,7 +228,7 @@ err := client.RevokeKey(agentID, kid, "key rotation")
agents, err := client.ListAgents() agents, err := client.ListAgents()
// Unfreeze a previously frozen agent // Unfreeze a previously frozen agent
err := client.UnfreezeAgent(agentID, "review complete") unfrozen, err := client.UnfreezeAgent(agentID, "review complete")
// Delete an agent permanently // Delete an agent permanently
deleted, err := client.DeleteAgent(agentID) deleted, err := client.DeleteAgent(agentID)
@@ -270,6 +269,25 @@ detail, err := client.GetExport(exportID)
data, err := client.DownloadExport(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 ### JWKS
```go ```go
@@ -281,13 +299,17 @@ jwks, err := client.GetJWKS()
```go ```go
// Check API health (no authentication required, does not need a client) // Check API health (no authentication required, does not need a client)
health, err := elydora.Health("https://api.elydora.com") 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 ### Constants
```go ```go
// Genesis chain hash — initial prev_chain_hash for an agent's first operation // Genesis chain hash: initial prev_chain_hash for an agent's first operation
elydora.GenesisChainHash // "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" elydora.GenesisChainHash // "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
``` ```
@@ -299,8 +321,11 @@ elydora.GenesisChainHash // "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
| `KeyStatus` | `KeyStatusActive`, `KeyStatusRetired`, `KeyStatusRevoked` | | `KeyStatus` | `KeyStatusActive`, `KeyStatusRetired`, `KeyStatusRevoked` |
| `ExportStatus` | `ExportStatusQueued`, `ExportStatusRunning`, `ExportStatusDone`, `ExportStatusFailed` | | `ExportStatus` | `ExportStatusQueued`, `ExportStatusRunning`, `ExportStatusDone`, `ExportStatusFailed` |
| `ExportFormat` | `ExportFormatJSON`, `ExportFormatPDF` | | `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` | | `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 ## Error Handling
+7 -6
View File
@@ -3,6 +3,7 @@ package elydora
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
) )
// RegisterAgent registers a new AI agent within the organization. // RegisterAgent registers a new AI agent within the organization.
@@ -17,7 +18,7 @@ func (c *Client) RegisterAgent(req *RegisterAgentRequest) (*RegisterAgentRespons
// GetAgent retrieves an agent by ID. // GetAgent retrieves an agent by ID.
func (c *Client) GetAgent(agentID string) (*GetAgentResponse, error) { func (c *Client) GetAgent(agentID string) (*GetAgentResponse, error) {
var result GetAgentResponse var result GetAgentResponse
if err := c.doGet(fmt.Sprintf("/v1/agents/%s", agentID), &result); err != nil { if err := c.doGet(fmt.Sprintf("/v1/agents/%s", url.PathEscape(agentID)), &result); err != nil {
return nil, err return nil, err
} }
return &result, nil return &result, nil
@@ -26,7 +27,7 @@ func (c *Client) GetAgent(agentID string) (*GetAgentResponse, error) {
// FreezeAgent freezes an agent, preventing it from submitting operations. // FreezeAgent freezes an agent, preventing it from submitting operations.
func (c *Client) FreezeAgent(agentID, reason string) (*FreezeAgentResponse, error) { func (c *Client) FreezeAgent(agentID, reason string) (*FreezeAgentResponse, error) {
var result FreezeAgentResponse var result FreezeAgentResponse
if err := c.doPost(fmt.Sprintf("/v1/agents/%s/freeze", agentID), &FreezeAgentRequest{ if err := c.doPost(fmt.Sprintf("/v1/agents/%s/freeze", url.PathEscape(agentID)), &FreezeAgentRequest{
Reason: reason, Reason: reason,
}, &result); err != nil { }, &result); err != nil {
return nil, err return nil, err
@@ -36,7 +37,7 @@ func (c *Client) FreezeAgent(agentID, reason string) (*FreezeAgentResponse, erro
// RevokeKey revokes a specific key for an agent. // RevokeKey revokes a specific key for an agent.
func (c *Client) RevokeKey(agentID, kid, reason string) error { func (c *Client) RevokeKey(agentID, kid, reason string) error {
return c.doPost(fmt.Sprintf("/v1/agents/%s/revoke", agentID), &RevokeAgentRequest{ return c.doPost(fmt.Sprintf("/v1/agents/%s/revoke", url.PathEscape(agentID)), &RevokeAgentRequest{
KID: kid, KID: kid,
Reason: reason, Reason: reason,
}, nil) }, nil)
@@ -54,7 +55,7 @@ func (c *Client) ListAgents() (*ListAgentsResponse, error) {
// UnfreezeAgent unfreezes a previously frozen agent. // UnfreezeAgent unfreezes a previously frozen agent.
func (c *Client) UnfreezeAgent(agentID, reason string) (*UnfreezeAgentResponse, error) { func (c *Client) UnfreezeAgent(agentID, reason string) (*UnfreezeAgentResponse, error) {
var result UnfreezeAgentResponse var result UnfreezeAgentResponse
if err := c.doPost(fmt.Sprintf("/v1/agents/%s/unfreeze", agentID), &UnfreezeAgentRequest{ if err := c.doPost(fmt.Sprintf("/v1/agents/%s/unfreeze", url.PathEscape(agentID)), &UnfreezeAgentRequest{
Reason: reason, Reason: reason,
}, &result); err != nil { }, &result); err != nil {
return nil, err return nil, err
@@ -65,7 +66,7 @@ func (c *Client) UnfreezeAgent(agentID, reason string) (*UnfreezeAgentResponse,
// UpdateAgent updates an agent's integration type. // UpdateAgent updates an agent's integration type.
func (c *Client) UpdateAgent(agentID string, integrationType IntegrationType) (*UpdateAgentResponse, error) { func (c *Client) UpdateAgent(agentID string, integrationType IntegrationType) (*UpdateAgentResponse, error) {
var result UpdateAgentResponse var result UpdateAgentResponse
if err := c.doRequest(http.MethodPatch, fmt.Sprintf("/v1/agents/%s", agentID), &UpdateAgentRequest{ if err := c.doRequest(http.MethodPatch, fmt.Sprintf("/v1/agents/%s", url.PathEscape(agentID)), &UpdateAgentRequest{
IntegrationType: integrationType, IntegrationType: integrationType,
}, &result); err != nil { }, &result); err != nil {
return nil, err return nil, err
@@ -76,7 +77,7 @@ func (c *Client) UpdateAgent(agentID string, integrationType IntegrationType) (*
// DeleteAgent permanently deletes an agent and all associated data. // DeleteAgent permanently deletes an agent and all associated data.
func (c *Client) DeleteAgent(agentID string) (*DeleteAgentResponse, error) { func (c *Client) DeleteAgent(agentID string) (*DeleteAgentResponse, error) {
var result DeleteAgentResponse var result DeleteAgentResponse
if err := c.doRequest(http.MethodDelete, fmt.Sprintf("/v1/agents/%s", agentID), nil, &result); err != nil { if err := c.doRequest(http.MethodDelete, fmt.Sprintf("/v1/agents/%s", url.PathEscape(agentID)), nil, &result); err != nil {
return nil, err return nil, err
} }
return &result, nil return &result, nil
+17 -2
View File
@@ -151,7 +151,16 @@ func retryable(status int) bool {
return status >= 500 || status == http.StatusTooManyRequests return status >= 500 || status == http.StatusTooManyRequests
} }
// doRequest executes an authenticated request, retrying transport and 5xx failures. // A lost response to a non-idempotent request must not be replayed.
func idempotent(method string) bool {
switch method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodPut, http.MethodDelete:
return true
}
return false
}
// doRequest executes an authenticated request; only idempotent methods retry.
func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error { func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
data, err := marshalBody(body) data, err := marshalBody(body)
if err != nil { if err != nil {
@@ -169,18 +178,24 @@ func (c *Client) doRequest(method, path string, body interface{}, result interfa
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
lastErr = fmt.Errorf("elydora: http request: %w", err) lastErr = fmt.Errorf("elydora: http request: %w", err)
if !idempotent(method) {
return lastErr
}
continue continue
} }
respBody, err := readBody(resp, maxResponseBytes) respBody, err := readBody(resp, maxResponseBytes)
if err != nil { if err != nil {
lastErr = err lastErr = err
if !idempotent(method) {
return lastErr
}
continue continue
} }
if resp.StatusCode >= 200 && resp.StatusCode < 300 { if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return decodeSuccess(respBody, result) return decodeSuccess(respBody, result)
} }
lastErr = apiError(resp.StatusCode, respBody) lastErr = apiError(resp.StatusCode, respBody)
if !retryable(resp.StatusCode) { if !retryable(resp.StatusCode) || !idempotent(method) {
return lastErr return lastErr
} }
} }
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2/cmd/elydora/plugins" "github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2/cmd/elydora/plugins"
) )
const version = "2.0.1" const version = "2.1.1"
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
+5 -4
View File
@@ -10,8 +10,7 @@ type AugmentPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Auggie commits its provider guard together // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// with both wrappers, the audit runtime, and user settings.
func (p *AugmentPlugin) ManagesGuardRuntime() bool { func (p *AugmentPlugin) ManagesGuardRuntime() bool {
return true return true
} }
@@ -57,13 +56,14 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
if err := writeAugmentChanges( if err := writeManagedChanges(
changes, changes,
"Install Augment Code CLI hooks", "Install Augment Code CLI hooks",
p.rename, p.rename,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.configPath), filepath.Dir(document.configPath),
"Auggie configuration directory",
); err != nil { ); err != nil {
return err return err
} }
@@ -99,13 +99,14 @@ func (p *AugmentPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
return writeAugmentChanges( return writeManagedChanges(
[]*fileChange{change}, []*fileChange{change},
"Uninstall Augment Code CLI hooks", "Uninstall Augment Code CLI hooks",
p.rename, p.rename,
"", "",
"", "",
filepath.Dir(document.configPath), filepath.Dir(document.configPath),
"Auggie configuration directory",
) )
} }
+9 -63
View File
@@ -16,7 +16,6 @@ const (
augmentGuardScript = "guard.js" augmentGuardScript = "guard.js"
augmentAuditScript = "hook.js" augmentAuditScript = "hook.js"
augmentHookTimeout = float64(10_000) augmentHookTimeout = float64(10_000)
augmentPOSIXApostrophe = `'"'"'`
) )
var ( var (
@@ -68,14 +67,6 @@ type augmentRuntimeContract struct {
auditWrapper string 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( func validateAugmentHandler(
value any, value any,
event string, event string,
@@ -114,7 +105,7 @@ func validateAugmentHandler(
return nil, fmt.Errorf("%s timeout must be a positive finite number", label) 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 { func validateAugmentMetadata(value any, label string) error {
@@ -169,7 +160,7 @@ func validateAugmentGroup(value any, event string, groupIndex int) (augmentGroup
} }
handlers = append(handlers, handler) 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) { func readAugmentHooks(root map[string]any) (augmentHooks, error) {
@@ -248,7 +239,7 @@ func renderAugmentDocument(
if !document.exists && len(hooks) == 0 { if !document.exists && len(hooks) == 0 {
return &augmentRenderedDocument{document: document}, nil return &augmentRenderedDocument{document: document}, nil
} }
root := cloneAugmentObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
@@ -281,7 +272,7 @@ func renderAugmentHooks(hooks augmentHooks) map[string]any {
for event, groups := range hooks { for event, groups := range hooks {
values := make([]any, 0, len(groups)) values := make([]any, 0, len(groups))
for _, group := range groups { for _, group := range groups {
object := cloneAugmentObject(group.object) object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers)) handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers { for _, handler := range group.handlers {
handlers = append(handlers, handler) handlers = append(handlers, handler)
@@ -314,23 +305,8 @@ func readAugmentWindowsArgument(command string) (string, bool) {
} }
func readAugmentPOSIXArgument(command string) (string, bool) { func readAugmentPOSIXArgument(command string) (string, bool) {
if len(command) < 2 || command[0] != '\'' { value, end, ok := readPOSIXArgument(command, 0)
return "", false return value, ok && end == len(command) && value != ""
}
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
} }
func parseAugmentCommand(command string) (string, bool) { func parseAugmentCommand(command string) (string, bool) {
@@ -340,36 +316,6 @@ func parseAugmentCommand(command string) (string, bool) {
return readAugmentPOSIXArgument(command) 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( func managedAugmentAgentID(
handler map[string]any, handler map[string]any,
wrapperName, runtimeRoot string, wrapperName, runtimeRoot string,
@@ -385,11 +331,11 @@ func managedAugmentAgentID(
return "", false return "", false
} }
wrapperPath, ok := parseAugmentCommand(command) wrapperPath, ok := parseAugmentCommand(command)
if !ok || !sameAugmentFileName(filepath.Base(wrapperPath), wrapperName) { if !ok || !sameManagedName(filepath.Base(wrapperPath), wrapperName) {
return "", false return "", false
} }
agentDirectory := filepath.Dir(wrapperPath) agentDirectory := filepath.Dir(wrapperPath)
if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) { if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false return "", false
} }
agentID := filepath.Base(agentDirectory) agentID := filepath.Base(agentDirectory)
@@ -407,7 +353,7 @@ func removeManagedAugmentGroups(
groupChanged := false groupChanged := false
for _, handler := range group.handlers { for _, handler := range group.handlers {
managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot) managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
remove := managed && (agentID == "" || sameAugmentAgentID(managedID, agentID)) remove := managed && (agentID == "" || sameManagedAgentID(managedID, agentID))
if remove { if remove {
changed = true changed = true
groupChanged = true groupChanged = true
+35 -155
View File
@@ -1,96 +1,42 @@
package plugins package plugins
import ( import "fmt"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type augmentRuntimePaths struct { type augmentRuntimePaths struct {
runtimeRoot string managedRuntimePaths
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
guardWrapperPath string guardWrapperPath string
auditWrapperPath string auditWrapperPath string
} }
func validateAugmentInstallConfig(config InstallConfig) error { func augmentWrapperArtifacts(agentDirectory string) []managedArtifact {
for _, field := range []struct{ name, value string }{ wrappers := resolveAugmentWrapperPaths(agentDirectory)
{"agent name", config.AgentName}, return []managedArtifact{
{"organization ID", config.OrgID}, {wrappers.guard, "Auggie guard wrapper", maxManagedSourceBytes},
{"agent ID", config.AgentID}, {wrappers.audit, "Auggie audit wrapper", maxManagedSourceBytes},
{"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 != 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) { 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 return nil, err
} }
runtimeRoot, err := AgentRuntimeRoot() paths, err := resolveManagedRuntimePaths(config, augmentGuardScript, augmentAuditScript)
if err != nil { if err != nil {
return nil, err return nil, err
} }
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) wrappers := resolveAugmentWrapperPaths(paths.agentDirectory)
if err != nil { return &augmentRuntimePaths{
return nil, err managedRuntimePaths: *paths,
}
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, guardWrapperPath: wrappers.guard,
auditWrapperPath: wrappers.audit, auditWrapperPath: wrappers.audit,
} }, nil
if !filepath.IsAbs(config.GuardScriptPath) || }
!sameAugmentPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf( func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error {
"Elydora guard runtime must use the managed agent directory: %s", return validateManagedRuntimeIdentity(
paths.guardPath, agentDirectory, agentID, augmentAgentKey, "Auggie",
augmentWrapperArtifacts(agentDirectory)...,
) )
}
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
} }
func preflightAugmentInstallation( func preflightAugmentInstallation(
@@ -98,55 +44,25 @@ func preflightAugmentInstallation(
document *augmentDocument, document *augmentDocument,
) (*augmentRuntimePaths, string, error) { ) (*augmentRuntimePaths, string, error) {
if document == nil || document.configPath == "" { if document == nil || document.configPath == "" {
return nil, "", fmt.Errorf( return nil, "", fmt.Errorf("Augment Code CLI installation requires a user settings document")
"Augment Code CLI installation requires a user settings document",
)
} }
paths, err := augmentAgentPaths(config) paths, err := augmentAgentPaths(config)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if err := validateAugmentRuntimeIdentity( if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
paths.agentDirectory,
config.AgentID,
); err != nil {
return nil, "", err return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Auggie")
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
return nil, "", err
}
if err := validateAugmentMatchers(document.hooks, nodePath); err != nil { if err := validateAugmentMatchers(document.hooks, nodePath); err != nil {
return nil, "", err return nil, "", err
} }
return paths, nodePath, nil 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( func prepareAugmentInstallationChanges(
config InstallConfig, config InstallConfig,
paths *augmentRuntimePaths, paths *augmentRuntimePaths,
@@ -156,63 +72,27 @@ func prepareAugmentInstallationChanges(
if paths == nil || rendered == nil { if paths == nil || rendered == nil {
return nil, fmt.Errorf("prepared Auggie installation is required") return nil, fmt.Errorf("prepared Auggie installation is required")
} }
if err := validateAugmentRuntimeIdentity( if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
paths.agentDirectory,
config.AgentID,
); err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
items := []struct { for _, item := range []struct {
path, label string path, label string
content []byte content []byte
mode os.FileMode
}{ }{
{ {paths.guardWrapperPath, "Auggie guard wrapper", buildAugmentWrapper(nodePath, paths.guardPath)},
paths.guardPath, {paths.auditWrapperPath, "Auggie audit wrapper", buildAugmentWrapper(nodePath, paths.auditPath)},
"Elydora guard runtime", } {
[]byte(generateGuardScript( change, err := prepareFileChange(item.path, item.label, item.content, 0700)
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)
if err != nil { if err != nil {
return nil, err 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 { func validateAugmentMatchers(hooks augmentHooks, nodePath string) error {
events := make([]string, 0, len(hooks)) events := make([]string, 0, len(hooks))
for event := range hooks { for event := range hooks {
@@ -209,10 +172,3 @@ func resolveAugmentWrapperPaths(agentDirectory string) augmentWrapperPaths {
audit: filepath.Join(agentDirectory, augmentAuditWrapperName()), 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
}
+26 -281
View File
@@ -2,11 +2,8 @@ package plugins
import ( import (
"bytes" "bytes"
"fmt"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strings"
) )
func managedAugmentIDs( func managedAugmentIDs(
@@ -17,19 +14,10 @@ func managedAugmentIDs(
result := map[string]string{} result := map[string]string{}
for _, group := range groups { for _, group := range groups {
for _, handler := range group.handlers { for _, handler := range group.handlers {
agentID, managed := managedAugmentAgentID( agentID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
handler, if managed {
wrapperName, result[managedReferenceKey(agentID)] = agentID
runtimeRoot,
)
if !managed {
continue
} }
key := agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(key)
}
result[key] = agentID
} }
} }
return result return result
@@ -39,16 +27,8 @@ func augmentRuntimeContracts(
hooks augmentHooks, hooks augmentHooks,
runtimeRoot string, runtimeRoot string,
) []augmentRuntimeContract { ) []augmentRuntimeContract {
guards := managedAugmentIDs( guards := managedAugmentIDs(hooks["PreToolUse"], augmentGuardWrapperName(), runtimeRoot)
hooks["PreToolUse"], audits := managedAugmentIDs(hooks["PostToolUse"], augmentAuditWrapperName(), runtimeRoot)
augmentGuardWrapperName(),
runtimeRoot,
)
audits := managedAugmentIDs(
hooks["PostToolUse"],
augmentAuditWrapperName(),
runtimeRoot,
)
keys := make([]string, 0, len(guards)) keys := make([]string, 0, len(guards))
for key := range guards { for key := range guards {
if _, exists := audits[key]; exists { if _, exists := audits[key]; exists {
@@ -72,282 +52,47 @@ func augmentRuntimeContracts(
return contracts return contracts
} }
func readAugmentRuntimeConfig(path string) (map[string]any, bool, error) { func augmentWrappersMatch(contract augmentRuntimeContract, nodePath string) (bool, error) {
snapshot, err := readManagedFile( wrappers := resolveAugmentWrapperPaths(filepath.Dir(contract.guardPath))
path, if !sameManagedPath(contract.guardWrapper, wrappers.guard) ||
"Elydora runtime config", !sameManagedPath(contract.auditWrapper, wrappers.audit) {
maxRuntimeConfigBytes, return false, nil
)
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 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 { for _, item := range []struct {
path string path, label string
label string expected []byte
limit int64
}{ }{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes}, {contract.guardWrapper, "Auggie guard wrapper", buildAugmentWrapper(nodePath, contract.guardPath)},
{filepath.Join(agentDirectory, augmentGuardScript), "Elydora guard runtime", maxManagedSourceBytes}, {contract.auditWrapper, "Auggie audit wrapper", buildAugmentWrapper(nodePath, contract.auditPath)},
{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},
} { } {
exists, inspectErr := managedPhysicalFileExists( snapshot, err := readManagedFile(item.path, item.label, maxManagedSourceBytes)
item.path, if err != nil || snapshot == nil || !bytes.Equal(snapshot.contents, item.expected) {
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"] != 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 return false, err
} }
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return false, err
} }
configPath := filepath.Join(agentDirectory, "config.json") return true, nil
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
} }
func augmentRuntimeFilesExist( func augmentRuntimeFilesExist(
contracts []augmentRuntimeContract, contracts []augmentRuntimeContract,
runtimeRoot string, runtimeRoot string,
) (bool, error) { ) (bool, error) {
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Auggie")
if err != nil { if err != nil {
return false, err return false, err
} }
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
return false, err
}
for _, contract := range contracts { for _, contract := range contracts {
exists, err := augmentRuntimeContractExists(contract, runtimeRoot, nodePath) runtime := managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath}
exists, err := managedRuntimeContractExists(runtime, augmentAgentKey, "Auggie", nil)
if err != nil || !exists {
if err != nil { if err != nil {
return false, err return false, err
} }
if exists { continue
return true, nil }
matches, err := augmentWrappersMatch(contract, nodePath)
if err != nil || matches {
return matches, err
} }
} }
return false, nil return false, nil
@@ -54,7 +54,7 @@ func TestAugmentInstallRollsBackAllSevenFiles(t *testing.T) {
) )
failed := false failed := false
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameAugmentPath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") && strings.HasSuffix(source, ".tmp") &&
!failed { !failed {
failed = true failed = true
@@ -87,13 +87,14 @@ func TestPreparedAugmentInstallRejectsConcurrentSettingsChange(t *testing.T) {
if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil { if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent settings: %v", err) t.Fatalf("write concurrent settings: %v", err)
} }
err := writeAugmentChanges( err := writeManagedChanges(
changes, changes,
"Install Augment Code CLI hooks", "Install Augment Code CLI hooks",
nil, nil,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.configPath), filepath.Dir(document.configPath),
"Auggie configuration directory",
) )
if err == nil || !strings.Contains(err.Error(), "changed during installation") { if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err) t.Fatalf("prepared install error = %v", err)
@@ -375,7 +376,7 @@ func TestAugmentUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed settings: %v", err) t.Fatalf("read installed settings: %v", err)
} }
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameAugmentPath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Auggie uninstall failure") return errors.New("injected Auggie uninstall failure")
} }
+5 -4
View File
@@ -10,8 +10,7 @@ type ClaudeCodePlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Claude Code commits its provider guard with // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// the audit runtime and user settings document.
func (p *ClaudeCodePlugin) ManagesGuardRuntime() bool { func (p *ClaudeCodePlugin) ManagesGuardRuntime() bool {
return true return true
} }
@@ -73,13 +72,14 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
if err := writeClaudeChanges( if err := writeManagedChanges(
changes, changes,
"Install Claude Code hooks", "Install Claude Code hooks",
p.rename, p.rename,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Claude Code configuration directory",
); err != nil { ); err != nil {
return err return err
} }
@@ -105,13 +105,14 @@ func (p *ClaudeCodePlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
return writeClaudeChanges( return writeManagedChanges(
[]*fileChange{change}, []*fileChange{change},
"Uninstall Claude Code hooks", "Uninstall Claude Code hooks",
p.rename, p.rename,
"", "",
"", "",
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Claude Code configuration directory",
) )
} }
+5 -17
View File
@@ -77,19 +77,7 @@ type claudeRenderedDocument struct {
remove bool remove bool
} }
type claudeRuntimeContract struct { type claudeRuntimeContract = managedRuntimeContract
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
}
func cloneClaudeHooks(source claudeHooks) claudeHooks { func cloneClaudeHooks(source claudeHooks) claudeHooks {
clone := make(claudeHooks, len(source)) 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) { 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) 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) { func readClaudeHooks(root map[string]any) (claudeHooks, error) {
@@ -411,7 +399,7 @@ func renderClaudeHooks(hooks claudeHooks) map[string]any {
for event, groups := range hooks { for event, groups := range hooks {
values := make([]any, 0, len(groups)) values := make([]any, 0, len(groups))
for _, group := range groups { for _, group := range groups {
object := cloneClaudeObject(group.object) object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers)) handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers { for _, handler := range group.handlers {
handlers = append(handlers, handler) handlers = append(handlers, handler)
@@ -439,7 +427,7 @@ func renderClaudeDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneClaudeObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
+18 -152
View File
@@ -1,182 +1,48 @@
package plugins package plugins
import ( import "fmt"
"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
}
func preflightClaudeInstallation( func preflightClaudeInstallation(
config InstallConfig, config InstallConfig,
document *claudeDocument, document *claudeDocument,
) (*claudeRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if document == nil || document.filePath == "" { if document == nil || document.filePath == "" {
return nil, "", fmt.Errorf( return nil, "", fmt.Errorf("Claude Code installation requires a user settings document")
"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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Claude Code")
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if err := requireClaudeAbsoluteNode(nodePath); err != nil {
return nil, "", err
}
return paths, nodePath, nil 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( func prepareClaudeInstallationChanges(
config InstallConfig, config InstallConfig,
paths *claudeRuntimePaths, paths *managedRuntimePaths,
rendered *claudeRenderedDocument, rendered *claudeRenderedDocument,
) ([]*fileChange, error) { ) ([]*fileChange, error) {
runtimeConfig, err := buildClaudeRuntimeConfig(config) changes, err := managedRuntimeFileChanges(
if err != nil { config,
return nil, err paths,
}
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, claudeAgentKey,
config.AgentID, generateGuardScript(claudeAgentKey, config.AgentID, "", false, ""),
"", buildHookScriptWithOutput(claudeAgentKey, config.AgentID, "", false, true),
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 { if err != nil {
return nil, err return nil, err
} }
changes = append(changes, change)
}
documentChange, err := prepareRenderedClaudeChange(rendered) documentChange, err := prepareRenderedClaudeChange(rendered)
if err != nil { if err != nil {
return nil, err return nil, err
-35
View File
@@ -71,38 +71,3 @@ func prepareRenderedClaudeChange(
rendered.remove, 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 package plugins
import ( import (
"fmt"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strings" "strings"
) )
type claudeRuntimeReference struct { type claudeRuntimeReference = managedScriptReference
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
}
func claudeRuntimeReferenceForScript( func claudeRuntimeReferenceForScript(
scriptPath string, scriptPath string,
scriptName string, scriptName string,
) (*claudeRuntimeReference, error) { ) (*claudeRuntimeReference, error) {
if !filepath.IsAbs(scriptPath) || return resolveManagedScript(scriptPath, scriptName)
!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
} }
func claudeLegacyReference( func claudeLegacyReference(
@@ -95,7 +40,7 @@ func managedClaudeReference(
handler["statusMessage"] == statusMessage { handler["statusMessage"] == statusMessage {
nodePath, nodeOK := handler["command"].(string) nodePath, nodeOK := handler["command"].(string)
args, argsOK := handler["args"].([]any) args, argsOK := handler["args"].([]any)
if nodeOK && filepath.IsAbs(nodePath) && isClaudeNodeExecutable(nodePath) && if nodeOK && filepath.IsAbs(nodePath) && isNodeExecutable(nodePath) &&
argsOK && len(args) == 1 { argsOK && len(args) == 1 {
if scriptPath, ok := args[0].(string); ok { if scriptPath, ok := args[0].(string); ok {
return claudeRuntimeReferenceForScript(scriptPath, scriptName) return claudeRuntimeReferenceForScript(scriptPath, scriptName)
@@ -146,7 +91,7 @@ func removeManagedClaudeGroups(
return nil, false, err return nil, false, err
} }
owned := reference != nil && owned := reference != nil &&
(agentID == "" || sameClaudeAgentID(reference.agentID, agentID)) (agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if owned { if owned {
removed = true removed = true
continue continue
@@ -154,7 +99,7 @@ func removeManagedClaudeGroups(
kept = append(kept, handler) kept = append(kept, handler)
} }
if len(kept) > 0 || !exactManagedClaudeGroup(group) { if len(kept) > 0 || !exactManagedClaudeGroup(group) {
object := cloneClaudeObject(group.object) object := cloneJSONObject(group.object)
object["hooks"] = group.object["hooks"] object["hooks"] = group.object["hooks"]
result = append(result, claudeGroup{object: object, handlers: kept}) result = append(result, claudeGroup{object: object, handlers: kept})
} }
@@ -235,13 +180,6 @@ func entirelyManagedClaudeDocument(document *claudeDocument) bool {
return handlerCount > 0 return handlerCount > 0
} }
func claudeReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func claudeReferencesForEvent( func claudeReferencesForEvent(
groups []claudeGroup, groups []claudeGroup,
scriptName string, scriptName string,
@@ -263,7 +201,7 @@ func claudeReferencesForEvent(
return nil, err return nil, err
} }
if reference != nil { if reference != nil {
key := claudeReferenceKey(reference.agentID) key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference) result[key] = append(result[key], *reference)
} }
} }
@@ -307,7 +245,7 @@ func claudeRuntimeContracts(hooks claudeHooks) ([]claudeRuntimeContract, error)
success := successes[key] success := successes[key]
failure := failures[key] failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 || 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 continue
} }
contracts = append(contracts, claudeRuntimeContract{ contracts = append(contracts, claudeRuntimeContract{
@@ -317,10 +255,3 @@ func claudeRuntimeContracts(hooks claudeHooks) ([]claudeRuntimeContract, error)
} }
return contracts, nil 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"}, }, "query parameters"},
{"guard path", func(f *claudeFixture) { {"guard path", func(f *claudeFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js") f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"}, }, "managed agent directory"},
{"audit path", func(f *claudeFixture) { {"audit path", func(f *claudeFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js") f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js")
}, "managed Elydora agent directory"}, }, "managed agent directory"},
} }
for _, testCase := range tests { for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
+2 -228
View File
@@ -1,231 +1,5 @@
package plugins package plugins
import ( func claudeRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
"fmt" return managedRuntimeFilesExist(contracts, claudeAgentKey, "Claude Code", nil)
"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
} }
@@ -60,7 +60,7 @@ func TestClaudeInstallRollsBackAllRuntimesAfterSettingsFailure(t *testing.T) {
} }
before := snapshotClaudeFiles(t, paths...) before := snapshotClaudeFiles(t, paths...)
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameClaudePath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Claude settings failure") 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 { if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent settings: %v", err) t.Fatalf("write concurrent settings: %v", err)
} }
err = writeClaudeChanges( err = writeManagedChanges(
changes, changes,
"Install Claude Code hooks", "Install Claude Code hooks",
nil, nil,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Claude Code configuration directory",
) )
if err == nil || !strings.Contains(err.Error(), "changed during installation") { if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err) t.Fatalf("prepared install error = %v", err)
@@ -176,7 +177,7 @@ func TestClaudeUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed settings: %v", err) t.Fatalf("read installed settings: %v", err)
} }
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameClaudePath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Claude uninstall failure") return errors.New("injected Claude uninstall failure")
} }
+1 -2
View File
@@ -7,8 +7,7 @@ type ClinePlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Cline commits both generated runtimes and // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// both native hook files in one transaction.
func (p *ClinePlugin) ManagesGuardRuntime() bool { func (p *ClinePlugin) ManagesGuardRuntime() bool {
return true return true
} }
+3 -42
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
) )
@@ -80,37 +79,6 @@ func resolveClineHookFiles() (clineHookPaths, error) {
}, nil }, 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) { func buildClineMetadata(kind, agentID, runtimePath string) (clineHookMetadata, error) {
metadata := clineHookMetadata{ metadata := clineHookMetadata{
Version: clineMetadataVersion, Version: clineMetadataVersion,
@@ -314,7 +282,7 @@ func clineContractForFiles(
if guard.Kind != "guard" || audit.Kind != "audit" { if guard.Kind != "guard" || audit.Kind != "audit" {
return nil, fmt.Errorf("Elydora Cline hook files contain mismatched event metadata") 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") return nil, fmt.Errorf("Elydora Cline hook files reference different agents")
} }
agentDirectory, err := ResolveAgentRuntimeDirectory(guard.AgentID) agentDirectory, err := ResolveAgentRuntimeDirectory(guard.AgentID)
@@ -323,15 +291,8 @@ func clineContractForFiles(
} }
expectedGuard := filepath.Join(agentDirectory, clineGuardScript) expectedGuard := filepath.Join(agentDirectory, clineGuardScript)
expectedAudit := filepath.Join(agentDirectory, clineAuditScript) expectedAudit := filepath.Join(agentDirectory, clineAuditScript)
guardMatches, err := sameClinePath(guard.RuntimePath, expectedGuard) if !sameManagedPath(guard.RuntimePath, expectedGuard) ||
if err != nil { !sameManagedPath(audit.RuntimePath, expectedAudit) {
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 {
return nil, fmt.Errorf("Elydora Cline hook metadata references an unexpected runtime path") return nil, fmt.Errorf("Elydora Cline hook metadata references an unexpected runtime path")
} }
return &clineRuntimeContract{ return &clineRuntimeContract{
+20 -128
View File
@@ -1,97 +1,21 @@
package plugins package plugins
import ( import "fmt"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type clineRuntimePaths struct { type clineRuntimePaths struct {
runtimeRoot string managedRuntimePaths
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
hooks clineHookPaths hooks clineHookPaths
} }
func validateClineInstallConfig(config InstallConfig) error { func clineAgentPaths(config InstallConfig, hooks clineHookPaths) (*clineRuntimePaths, error) {
for _, field := range []struct{ name, value string }{ if err := validateManagedInstallConfig(config, clineAgentKey, "Cline"); err != nil {
{"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 {
return nil, err return nil, err
} }
runtimeRoot, err := AgentRuntimeRoot() paths, err := resolveManagedRuntimePaths(config, clineGuardScript, clineAuditScript)
if err != nil { if err != nil {
return nil, err return nil, err
} }
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) return &clineRuntimePaths{managedRuntimePaths: *paths, hooks: hooks}, nil
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
} }
func preflightClineInstallation( func preflightClineInstallation(
@@ -102,39 +26,20 @@ func preflightClineInstallation(
if err != nil { if err != nil {
return nil, err 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 return nil, err
} }
nodePath, err := resolveNodeRuntime() if _, err := resolveAbsoluteNodeRuntime("Cline"); err != nil {
if err != nil {
return nil, err 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 return paths, nil
} }
func buildClineRuntimeConfig(config InstallConfig) ([]byte, error) { func clineExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
value := map[string]any{ return []byte(generateGuardScript(clineAgentKey, agentID, "", false, "")),
"org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID, []byte(buildHookScriptWithOutput(clineAgentKey, agentID, "", false, true))
"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 prepareClineInstallationChanges( func prepareClineInstallationChanges(
@@ -146,11 +51,9 @@ func prepareClineInstallationChanges(
if paths == nil { if paths == nil {
return nil, fmt.Errorf("prepared Cline installation is required") return nil, fmt.Errorf("prepared Cline installation is required")
} }
if err := validateClineRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { if err := validateManagedRuntimeIdentity(
return nil, err paths.agentDirectory, config.AgentID, clineAgentKey, "Cline",
} ); err != nil {
runtimeConfig, err := buildClineRuntimeConfig(config)
if err != nil {
return nil, err return nil, err
} }
guardMetadata, err := buildClineMetadata("guard", config.AgentID, paths.guardPath) guardMetadata, err := buildClineMetadata("guard", config.AgentID, paths.guardPath)
@@ -175,24 +78,13 @@ func prepareClineInstallationChanges(
); err != nil { ); err != nil {
return nil, err return nil, err
} }
runtimeItems := []struct { guardScript, auditScript := clineExpectedScripts(config.AgentID, nil)
path, label string changes, err := managedRuntimeFileChanges(
content []byte config, &paths.managedRuntimePaths, clineAgentKey, string(guardScript), string(auditScript),
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 { if err != nil {
return nil, err return nil, err
} }
changes = append(changes, change)
}
for _, item := range []struct { for _, item := range []struct {
file clineHookFile file clineHookFile
label string label string
+3 -12
View File
@@ -74,7 +74,7 @@ func writeClineHookChanges(
rename renameFunc, rename renameFunc,
paths clineHookPaths, paths clineHookPaths,
) error { ) error {
if !hasClineChanges(changes) { if !hasFileChanges(changes) {
return nil return nil
} }
if err := ensureManagedDirectory( if err := ensureManagedDirectory(
@@ -95,7 +95,7 @@ func writeClineChanges(
rename renameFunc, rename renameFunc,
paths *clineRuntimePaths, paths *clineRuntimePaths,
) error { ) error {
if !hasClineChanges(changes) { if !hasFileChanges(changes) {
return nil return nil
} }
if err := EnsurePrivateDirectory(paths.runtimeRoot); err != nil { if err := EnsurePrivateDirectory(paths.runtimeRoot); err != nil {
@@ -107,15 +107,6 @@ func writeClineChanges(
return writeClineHookChanges(changes, label, rename, paths.hooks) 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( func prepareClineUninstallChanges(
files []clineHookFile, files []clineHookFile,
agentID string, agentID string,
@@ -123,7 +114,7 @@ func prepareClineUninstallChanges(
changes := make([]*fileChange, 0, len(files)) changes := make([]*fileChange, 0, len(files))
for _, file := range files { for _, file := range files {
if file.metadata == nil || if file.metadata == nil ||
(agentID != "" && !sameClineAgentID(file.metadata.AgentID, agentID)) { (agentID != "" && !sameManagedAgentID(file.metadata.AgentID, agentID)) {
continue continue
} }
if err := assertClineWrapperIntegrity(file); err != nil { if err := assertClineWrapperIntegrity(file); err != nil {
+7 -198
View File
@@ -1,206 +1,15 @@
package plugins package plugins
import ( import "path/filepath"
"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
}
func clineRuntimeFilesExist(contract *clineRuntimeContract) (bool, error) { func clineRuntimeFilesExist(contract *clineRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot() if !sameManagedPath(contract.guardPath, filepath.Join(contract.agentDirectory, clineGuardScript)) {
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)) {
return false, nil return false, nil
} }
rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory") return managedRuntimeContractExists(
if err != nil || !rootExists { managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath},
return false, err clineAgentKey,
} "Cline",
directoryExists, err := managedPhysicalDirectoryExists( clineExpectedScripts,
contract.agentDirectory,
"Elydora agent runtime directory",
) )
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 rename renameFunc
} }
// ManagesGuardRuntime reports that Codex commits its provider guard with the // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// audit runtime and user hook document.
func (p *CodexPlugin) ManagesGuardRuntime() bool { func (p *CodexPlugin) ManagesGuardRuntime() bool {
return true return true
} }
// PreflightInstall validates every existing source before the CLI creates // PreflightInstall validates every source before the CLI creates runtime state.
// runtime state.
func (p *CodexPlugin) PreflightInstall(config InstallConfig) error { func (p *CodexPlugin) PreflightInstall(config InstallConfig) error {
document, err := readCodexDocument() document, err := readCodexDocument()
if err != nil { if err != nil {
@@ -61,13 +59,14 @@ func (p *CodexPlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
if err := writeCodexChanges( if err := writeManagedChanges(
changes, changes,
"Install Codex hooks", "Install Codex hooks",
p.rename, p.rename,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Codex hooks directory",
); err != nil { ); err != nil {
return err return err
} }
@@ -90,13 +89,14 @@ func (p *CodexPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
return writeCodexChanges( return writeManagedChanges(
[]*fileChange{change}, []*fileChange{change},
"Uninstall Codex hooks", "Uninstall Codex hooks",
p.rename, p.rename,
"", "",
"", "",
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Codex hooks directory",
) )
} }
+16 -176
View File
@@ -1,53 +1,12 @@
package plugins package plugins
import ( import "path/filepath"
"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)
}
func codexHandler(runtimePath, scriptPath, statusMessage string) map[string]any { func codexHandler(runtimePath, scriptPath, statusMessage string) map[string]any {
return map[string]any{ return map[string]any{
"type": "command", "type": "command",
"command": quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), "command": posixSource(runtimePath, scriptPath),
"commandWindows": codexWindowsCommand(runtimePath, scriptPath), "commandWindows": encodedWindowsCommand(runtimePath, scriptPath),
"timeout": codexHookTimeout, "timeout": codexHookTimeout,
"statusMessage": statusMessage, "statusMessage": statusMessage,
} }
@@ -57,124 +16,6 @@ func codexMatcherGroup(handler map[string]any) map[string]any {
return map[string]any{"matcher": "*", "hooks": []any{handler}} 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 { func exactCodexHandlerKeys(handler map[string]any) bool {
if len(handler) != 5 { if len(handler) != 5 {
return false return false
@@ -192,20 +33,21 @@ func codexManagedScriptPath(handler map[string]any, status string) (string, bool
handler["timeout"] != codexHookTimeout || handler["statusMessage"] != status { handler["timeout"] != codexHookTimeout || handler["statusMessage"] != status {
return "", false return "", false
} }
posixRuntime, posixScript, posixOK := parseCodexPOSIXCommand(handler["command"]) command, _ := handler["command"].(string)
windowsRuntime, windowsScript, windowsOK := parseCodexWindowsCommand(handler["commandWindows"]) commandWindows, _ := handler["commandWindows"].(string)
posixRuntime, posixScript, posixOK := parsePOSIXCommand(command)
windowsRuntime, windowsScript, windowsOK := parseEncodedWindowsCommand(commandWindows)
if !windowsOK && posixOK { if !windowsOK && posixOK {
legacy, ok := handler["commandWindows"].(string)
expected := quoteWindowsArgument(posixRuntime) + " " + quoteWindowsArgument(posixScript) expected := quoteWindowsArgument(posixRuntime) + " " + quoteWindowsArgument(posixScript)
if ok && legacy == expected { if commandWindows == expected {
windowsRuntime, windowsScript, windowsOK = posixRuntime, posixScript, true windowsRuntime, windowsScript, windowsOK = posixRuntime, posixScript, true
} }
} }
if !posixOK || !windowsOK || !filepath.IsAbs(posixRuntime) || if !posixOK || !windowsOK || !filepath.IsAbs(posixRuntime) ||
!filepath.IsAbs(posixScript) || !isCodexNodeExecutable(posixRuntime) || !filepath.IsAbs(posixScript) || !isNodeExecutable(posixRuntime) ||
!isCodexNodeExecutable(windowsRuntime) || !isNodeExecutable(windowsRuntime) ||
!sameCodexPath(posixRuntime, windowsRuntime) || !sameManagedPath(posixRuntime, windowsRuntime) ||
!sameCodexPath(posixScript, windowsScript) { !sameManagedPath(posixScript, windowsScript) {
return "", false return "", false
} }
return posixScript, true return posixScript, true
@@ -217,14 +59,12 @@ func codexManagedAgentID(
status string, status string,
) (string, bool) { ) (string, bool) {
scriptPath, managed := codexManagedScriptPath(handler, status) scriptPath, managed := codexManagedScriptPath(handler, status)
if !managed || filepath.Base(scriptPath) != scriptName { if !managed {
return "", false return "", false
} }
agentDirectory := filepath.Dir(scriptPath) reference, err := resolveManagedScript(scriptPath, scriptName)
runtimeRoot, err := AgentRuntimeRoot() if err != nil || reference == nil {
if err != nil || !sameCodexPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false return "", false
} }
agentID := filepath.Base(agentDirectory) return reference.agentID, true
return agentID, agentID != "" && agentID != "." && agentID != ".."
} }
+5 -56
View File
@@ -6,9 +6,7 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"sort" "sort"
"strings"
) )
const ( const (
@@ -20,7 +18,6 @@ const (
codexOwnedDescription = "Elydora audit and freeze enforcement" codexOwnedDescription = "Elydora audit and freeze enforcement"
codexGuardStatusMessage = "Checking Elydora agent state" codexGuardStatusMessage = "Checking Elydora agent state"
codexAuditStatusMessage = "Recording Elydora tool use" codexAuditStatusMessage = "Recording Elydora tool use"
codexPOSIXApostrophe = `'"'"'`
) )
type codexHooks map[string][]map[string]any type codexHooks map[string][]map[string]any
@@ -40,19 +37,7 @@ type codexRenderedDocument struct {
remove bool remove bool
} }
type codexRuntimeContract struct { type codexRuntimeContract = managedRuntimeContract
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
}
func cloneCodexHooks(source codexHooks) codexHooks { func cloneCodexHooks(source codexHooks) codexHooks {
clone := make(codexHooks, len(source)) clone := make(codexHooks, len(source))
@@ -62,28 +47,6 @@ func cloneCodexHooks(source codexHooks) codexHooks {
return clone 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) { func readCodexHooks(value any, label string) (codexHooks, error) {
object, ok := value.(map[string]any) object, ok := value.(map[string]any)
if !ok { if !ok {
@@ -126,16 +89,6 @@ func readCodexHooks(value any, label string) (codexHooks, error) {
return hooks, nil 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) { func parseCodexDocument(filePath string, raw []byte) (*codexDocument, error) {
label := fmt.Sprintf("Codex user hooks at %s", filePath) label := fmt.Sprintf("Codex user hooks at %s", filePath)
root, err := decodeStrictJSONObject(raw, label) root, err := decodeStrictJSONObject(raw, label)
@@ -180,13 +133,13 @@ func removeCodexGroups(
for _, value := range handlers { for _, value := range handlers {
handler := value.(map[string]any) handler := value.(map[string]any)
managedID, managed := codexManagedAgentID(handler, scriptName, status) managedID, managed := codexManagedAgentID(handler, scriptName, status)
if managed && (agentID == "" || sameCodexAgentID(managedID, agentID)) { if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) {
continue continue
} }
kept = append(kept, handler) kept = append(kept, handler)
} }
if len(kept) > 0 || !exactCodexMatcherGroup(group) { if len(kept) > 0 || !exactCodexMatcherGroup(group) {
next := cloneCodexObject(group) next := cloneJSONObject(group)
next["hooks"] = kept next["hooks"] = kept
result = append(result, next) result = append(result, next)
} }
@@ -262,7 +215,7 @@ func renderCodexDocument(
if len(hooks) == 0 && entirelyManagedCodexDocument(document) { if len(hooks) == 0 && entirelyManagedCodexDocument(document) {
return &codexRenderedDocument{document: document, changed: true, remove: true}, nil return &codexRenderedDocument{document: document, changed: true, remove: true}, nil
} }
root := cloneCodexObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
@@ -295,11 +248,7 @@ func managedCodexIDs(
if !managed { if !managed {
continue continue
} }
key := agentID result[managedReferenceKey(agentID)] = agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(agentID)
}
result[key] = agentID
} }
} }
return result return result
+17 -128
View File
@@ -1,94 +1,26 @@
package plugins package plugins
import ( import "path/filepath"
"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
}
func preflightCodexInstallation( func preflightCodexInstallation(
config InstallConfig, config InstallConfig,
hooksPath string, hooksPath string,
) (*codexRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
paths, err := codexAgentPaths(config) if err := validateManagedInstallConfig(config, codexAgentKey, "Codex"); err != nil {
return nil, "", err
}
paths, err := resolveManagedRuntimePaths(config, codexGuardScript, codexAuditScript)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if _, err := managedPhysicalDirectoryExists( if _, err := managedPhysicalDirectoryExists(
filepath.Dir(hooksPath), filepath.Dir(hooksPath), "Codex hooks directory",
"Codex hooks directory",
); err != nil { ); err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveNodeRuntime()
@@ -98,64 +30,21 @@ func preflightCodexInstallation(
return paths, nodePath, nil 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( func prepareCodexInstallationChanges(
config InstallConfig, config InstallConfig,
paths *codexRuntimePaths, paths *managedRuntimePaths,
rendered *codexRenderedDocument, rendered *codexRenderedDocument,
) ([]*fileChange, error) { ) ([]*fileChange, error) {
runtimeConfig, err := buildCodexRuntimeConfig(config) changes, err := managedRuntimeFileChanges(
if err != nil { config,
return nil, err paths,
} codexAgentKey,
items := []struct { generateGuardScript(codexAgentKey, config.AgentID, "", false, ""),
path, label string buildHookScriptWithOutput(codexAgentKey, config.AgentID, "", false, true),
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 { if err != nil {
return nil, err return nil, err
} }
changes = append(changes, change)
}
documentChange, err := prepareRenderedCodexChange(rendered) documentChange, err := prepareRenderedCodexChange(rendered)
if err != nil { if err != nil {
return nil, err return nil, err
+2 -148
View File
@@ -82,152 +82,6 @@ func prepareRenderedCodexChange(rendered *codexRenderedDocument) (*fileChange, e
) )
} }
func writeCodexChanges( func codexRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
changes []*fileChange, return managedRuntimeFilesPresent(contracts, codexAgentKey)
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
} }
+1 -1
View File
@@ -249,7 +249,7 @@ func TestCodexInstallRollsBackAllFilesOnHooksCommitFailure(t *testing.T) {
source := []byte(`{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"keep"}]}]}}`) source := []byte(`{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"keep"}]}]}}`)
fixture := prepareCodexFixture(t, string(source)) fixture := prepareCodexFixture(t, string(source))
fixture.plugin.rename = func(source, destination string) error { 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 errors.New("injected Codex hooks commit failure")
} }
return os.Rename(source, destination) 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 { if len(requireArray(t, hooks["PreToolUse"])) != 2 || len(requireArray(t, hooks["PostToolUse"])) != 1 {
t.Fatalf("unexpected remaining hooks: %#v", hooks) t.Fatalf("unexpected remaining hooks: %#v", hooks)
} }
_, remainingScript, parsed := parseCodexPOSIXCommand( _, remainingScript, parsed := parsePOSIXCommand(
requireCodexHandler(t, remaining, "PostToolUse", codexAuditStatus)["command"], 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) t.Fatalf("other agent handler was removed: %#v", hooks)
} }
} }
+3 -5
View File
@@ -10,14 +10,12 @@ type CopilotPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Copilot commits its provider guard with the // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// audit runtime and hook documents.
func (p *CopilotPlugin) ManagesGuardRuntime() bool { func (p *CopilotPlugin) ManagesGuardRuntime() bool {
return true return true
} }
// PreflightInstall validates hook sources, settings, matchers, credentials, // PreflightInstall validates every source before the CLI creates runtime state.
// and runtime identity before any write.
func (p *CopilotPlugin) PreflightInstall(config InstallConfig) error { func (p *CopilotPlugin) PreflightInstall(config InstallConfig) error {
sources, _, err := readCopilotSources() sources, _, err := readCopilotSources()
if err != nil { if err != nil {
@@ -165,7 +163,7 @@ func mergedCopilotContracts(
} }
unique := map[string]copilotRuntimeContract{} unique := map[string]copilotRuntimeContract{}
for _, contract := range contracts { for _, contract := range contracts {
unique[copilotEntryKey(contract.agentID)] = contract unique[managedReferenceKey(contract.agentID)] = contract
} }
result := make([]copilotRuntimeContract, 0, len(unique)) result := make([]copilotRuntimeContract, 0, len(unique))
for _, contract := range unique { for _, contract := range unique {
+20 -156
View File
@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strings" "strings"
) )
@@ -17,7 +16,6 @@ const (
copilotConfigFile = "elydora-audit.json" copilotConfigFile = "elydora-audit.json"
copilotHookTimeout = float64(10) copilotHookTimeout = float64(10)
copilotLegacyTimeout = float64(5) copilotLegacyTimeout = float64(5)
copilotPOSIXApostrophe = `'"'"'`
) )
var copilotManagedEvents = []struct { var copilotManagedEvents = []struct {
@@ -61,23 +59,19 @@ type copilotRenderedDocument struct {
remove bool remove bool
} }
type copilotRuntimeContract struct { type copilotRuntimeContract = managedRuntimeContract
agentID string
guardPath string
auditPath string
}
type copilotManagedEntry struct { type copilotManagedEntry = managedScriptReference
agentID string
scriptPath string
}
func cloneCopilotObject(value map[string]any) map[string]any { func copilotManagedEntryForHandler(
clone := make(map[string]any, len(value)) handler map[string]any,
for key, item := range value { scriptName string,
clone[key] = item ) (*copilotManagedEntry, error) {
scriptPath, managed := copilotManagedScriptPath(handler)
if !managed {
return nil, nil
} }
return clone return resolveManagedScript(scriptPath, scriptName)
} }
func cloneCopilotHooks(source copilotHooks) copilotHooks { func cloneCopilotHooks(source copilotHooks) copilotHooks {
@@ -90,109 +84,15 @@ func cloneCopilotHooks(source copilotHooks) copilotHooks {
return clone 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 { func buildCopilotHandler(nodePath, scriptPath string) map[string]any {
return map[string]any{ return map[string]any{
"type": "command", "type": "command",
"bash": quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath), "bash": posixSource(nodePath, scriptPath),
"powershell": "& " + quoteCopilotPowerShell(nodePath) + " " + quoteCopilotPowerShell(scriptPath) + "; exit $LASTEXITCODE", "powershell": powerShellSource(nodePath, scriptPath),
"timeoutSec": copilotHookTimeout, "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) { func parseCopilotLegacyCommand(command string) (string, bool) {
command = strings.TrimSpace(command) command = strings.TrimSpace(command)
separator := strings.IndexAny(command, " \t") separator := strings.IndexAny(command, " \t")
@@ -211,11 +111,6 @@ func parseCopilotLegacyCommand(command string) (string, bool) {
return script, script != "" 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) { func copilotManagedScriptPath(handler map[string]any) (string, bool) {
if len(handler) != 4 || handler["type"] != "command" { if len(handler) != 4 || handler["type"] != "command" {
return "", false return "", false
@@ -226,11 +121,11 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) {
return "", false return "", false
} }
if handler["timeoutSec"] == copilotHookTimeout { if handler["timeoutSec"] == copilotHookTimeout {
bashNode, bashScript, bashParsed := parseCopilotBash(bash) bashNode, bashScript, bashParsed := parsePOSIXCommand(bash)
psNode, psScript, psParsed := parseCopilotPowerShell(powershell) psNode, psScript, psParsed := parsePowerShellSource(powershell)
if bashParsed && psParsed && filepath.IsAbs(bashNode) && filepath.IsAbs(bashScript) && if bashParsed && psParsed && filepath.IsAbs(bashNode) && filepath.IsAbs(bashScript) &&
isCopilotNodeExecutable(bashNode) && sameCopilotPath(bashNode, psNode) && isNodeExecutable(bashNode) && sameManagedPath(bashNode, psNode) &&
sameCopilotPath(bashScript, psScript) { sameManagedPath(bashScript, psScript) {
return bashScript, true return bashScript, true
} }
} }
@@ -240,30 +135,6 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) {
return parseCopilotLegacyCommand(bash) 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( func removeManagedCopilotHooks(
hooks copilotHooks, hooks copilotHooks,
agentID string, agentID string,
@@ -277,7 +148,7 @@ func removeManagedCopilotHooks(
return nil, err return nil, err
} }
remove := entry != nil && remove := entry != nil &&
(agentID == "" || sameCopilotAgentID(entry.agentID, agentID)) (agentID == "" || sameManagedAgentID(entry.agentID, agentID))
if !remove { if !remove {
kept = append(kept, handler) kept = append(kept, handler)
} }
@@ -361,7 +232,7 @@ func renderCopilotDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneCopilotObject(document.root) root := cloneJSONObject(document.root)
root["version"] = float64(1) root["version"] = float64(1)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
@@ -387,13 +258,6 @@ func renderCopilotDocument(
}, nil }, nil
} }
func copilotEntryKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func copilotManagedEntries( func copilotManagedEntries(
handlers []map[string]any, handlers []map[string]any,
scriptName string, scriptName string,
@@ -405,7 +269,7 @@ func copilotManagedEntries(
return nil, err return nil, err
} }
if entry != nil { if entry != nil {
key := copilotEntryKey(entry.agentID) key := managedReferenceKey(entry.agentID)
result[key] = append(result[key], *entry) result[key] = append(result[key], *entry)
} }
} }
@@ -438,7 +302,7 @@ func copilotRuntimeContracts(hooks copilotHooks) ([]copilotRuntimeContract, erro
success := successes[key] success := successes[key]
failure := failures[key] failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 || 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 continue
} }
contracts = append(contracts, copilotRuntimeContract{ contracts = append(contracts, copilotRuntimeContract{
+20 -142
View File
@@ -1,103 +1,19 @@
package plugins package plugins
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings"
) )
type copilotRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type preparedCopilotInstallation struct { type preparedCopilotInstallation struct {
changes []*fileChange changes []*fileChange
preconditions []filePrecondition preconditions []filePrecondition
paths *copilotRuntimePaths paths *managedRuntimePaths
}
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
} }
func preflightCopilotInstallation( func preflightCopilotInstallation(
config InstallConfig, config InstallConfig,
sources *copilotSources, sources *copilotSources,
) (*copilotRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if sources == nil || sources.user == nil || sources.user.filePath == "" { if sources == nil || sources.user == nil || sources.user.filePath == "" {
return nil, "", fmt.Errorf( return nil, "", fmt.Errorf(
"GitHub Copilot CLI installation requires a user hook path", "GitHub Copilot CLI installation requires a user hook path",
@@ -106,22 +22,22 @@ func preflightCopilotInstallation(
if err := requireCopilotHooksEnabled(sources); err != nil { if err := requireCopilotHooksEnabled(sources); err != nil {
return nil, "", err 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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("GitHub Copilot CLI")
if err != nil { if err != nil {
return nil, "", err 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} hookSources := []copilotHooks{sources.user.hooks}
if sources.legacy != nil { if sources.legacy != nil {
hookSources = append(hookSources, sources.legacy.hooks) hookSources = append(hookSources, sources.legacy.hooks)
@@ -132,28 +48,6 @@ func preflightCopilotInstallation(
return paths, nodePath, nil 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( func validateCopilotRenderedSet(
sources *copilotSources, sources *copilotSources,
rendered []*copilotRenderedDocument, rendered []*copilotRenderedDocument,
@@ -169,7 +63,7 @@ func validateCopilotRenderedSet(
matches := 0 matches := 0
for _, item := range rendered { for _, item := range rendered {
if item != nil && item.document != nil && if item != nil && item.document != nil &&
sameCopilotPath(item.document.filePath, document.filePath) { sameManagedPath(item.document.filePath, document.filePath) {
matches++ matches++
} }
} }
@@ -218,34 +112,13 @@ func prepareCopilotInstallation(
if err := validateCopilotRenderedSet(sources, rendered); err != nil { if err := validateCopilotRenderedSet(sources, rendered); err != nil {
return nil, err 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 { if err != nil {
return nil, err 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 { for _, document := range rendered {
change, changeErr := prepareRenderedCopilotChange(document) change, changeErr := prepareRenderedCopilotChange(document)
if changeErr != nil { if changeErr != nil {
@@ -275,3 +148,8 @@ func commitCopilotInstallation(
prepared.preconditions, 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, agentDirectory string,
preconditions []filePrecondition, preconditions []filePrecondition,
) error { ) error {
hasChanges := false if !hasFileChanges(changes) {
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
return writeChanges(changes, label, rename, preconditions...) return writeChanges(changes, label, rename, preconditions...)
} }
if agentDirectory != "" { 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 { if err := validateCopilotHandler(handler, event, itemLabel); err != nil {
return nil, err return nil, err
} }
handlers = append(handlers, cloneCopilotObject(handler)) handlers = append(handlers, cloneJSONObject(handler))
} }
hooks[event] = handlers hooks[event] = handlers
} }
+2 -258
View File
@@ -1,261 +1,5 @@
package plugins package plugins
import ( func copilotRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
"bytes" return managedRuntimeFilesExist(contracts, copilotAgentKey, "Copilot", copilotExpectedScripts)
"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
} }
+26 -54
View File
@@ -10,55 +10,27 @@ type CursorPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Cursor commits its provider-specific guard // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// in the same transaction as the rest of the installation.
func (p *CursorPlugin) ManagesGuardRuntime() bool { func (p *CursorPlugin) ManagesGuardRuntime() bool {
return true return true
} }
func cursorAgentPaths(config InstallConfig) ( func cursorAgentPaths(config InstallConfig) (*managedRuntimePaths, error) {
runtimeRoot, agentDirectory, guardPath, auditPath string, return resolveManagedRuntimePaths(config, cursorGuardScript, cursorAuditScript)
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
} }
// PreflightInstall validates every existing source before the CLI creates a // PreflightInstall validates every source before the CLI creates runtime state.
// runtime directory or guard file.
func (p *CursorPlugin) PreflightInstall(config InstallConfig) error { func (p *CursorPlugin) PreflightInstall(config InstallConfig) error {
if _, err := readCursorDocument(); err != nil { if _, err := readCursorDocument(); err != nil {
return err return err
} }
_, agentDirectory, _, _, err := cursorAgentPaths(config) paths, err := cursorAgentPaths(config)
if err != nil { if err != nil {
return err return err
} }
if err := preflightCursorRuntime(agentDirectory, config.AgentID); err != nil { if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, cursorAgentKey, "Cursor",
); err != nil {
return err return err
} }
_, err = resolveNodeRuntime() _, err = resolveNodeRuntime()
@@ -70,44 +42,42 @@ func (p *CursorPlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
runtimeRoot, agentDirectory, guardPath, auditPath, err := cursorAgentPaths(config) paths, err := cursorAgentPaths(config)
if err != nil { if err != nil {
return err return err
} }
if err := preflightCursorRuntime(agentDirectory, config.AgentID); err != nil { if err := validateManagedRuntimeIdentity(
paths.agentDirectory, config.AgentID, cursorAgentKey, "Cursor",
); err != nil {
return err return err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveNodeRuntime()
if err != nil { if err != nil {
return err return err
} }
hooks := removeManagedCursorHooks(document.hooks, runtimeRoot, "") hooks := removeManagedCursorHooks(document.hooks, paths.runtimeRoot, "")
hooks["preToolUse"] = append( hooks["preToolUse"] = append(hooks["preToolUse"], buildCursorHandler(nodePath, paths.guardPath))
hooks["preToolUse"], hooks["postToolUse"] = append(hooks["postToolUse"], buildCursorHandler(nodePath, paths.auditPath))
buildCursorHandler(nodePath, guardPath),
)
hooks["postToolUse"] = append(
hooks["postToolUse"],
buildCursorHandler(nodePath, auditPath),
)
hooks["postToolUseFailure"] = append( hooks["postToolUseFailure"] = append(
hooks["postToolUseFailure"], 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 { if err != nil {
return fmt.Errorf("render Cursor user hooks: %w", err) return fmt.Errorf("render Cursor user hooks: %w", err)
} }
changes, err := prepareCursorInstallationChanges(config, agentDirectory, rendered) changes, err := prepareCursorInstallationChanges(config, paths, rendered)
if err != nil { if err != nil {
return err return err
} }
if err := writeCursorChanges( if err := writeManagedChanges(
changes, changes,
"Install Cursor hooks", "Install Cursor hooks",
p.rename, p.rename,
runtimeRoot, paths.runtimeRoot,
agentDirectory, paths.agentDirectory,
filepath.Dir(document.filePath),
"Cursor hooks directory",
); err != nil { ); err != nil {
return err return err
} }
@@ -133,12 +103,14 @@ func (p *CursorPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
return writeCursorChanges( return writeManagedChanges(
[]*fileChange{change}, []*fileChange{change},
"Uninstall Cursor hooks", "Uninstall Cursor hooks",
p.rename, p.rename,
"", "",
"", "",
filepath.Dir(document.filePath),
"Cursor hooks directory",
) )
} }
+9 -111
View File
@@ -16,7 +16,6 @@ const (
cursorGuardScript = "guard.js" cursorGuardScript = "guard.js"
cursorAuditScript = "hook.js" cursorAuditScript = "hook.js"
cursorHookTimeout = float64(10) cursorHookTimeout = float64(10)
cursorPOSIXApostrophe = `'"'"'`
) )
type cursorHooks map[string][]map[string]any type cursorHooks map[string][]map[string]any
@@ -36,11 +35,7 @@ type cursorRenderedDocument struct {
remove bool remove bool
} }
type cursorRuntimeContract struct { type cursorRuntimeContract = managedRuntimeContract
agentID string
guardPath string
auditPath string
}
func cloneCursorHooks(source cursorHooks) cursorHooks { func cloneCursorHooks(source cursorHooks) cursorHooks {
clone := make(cursorHooks, len(source)) clone := make(cursorHooks, len(source))
@@ -50,104 +45,16 @@ func cloneCursorHooks(source cursorHooks) cursorHooks {
return clone 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 { func buildCursorHandler(nodePath, scriptPath string) map[string]any {
command := quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath) command := posixSource(nodePath, scriptPath)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
command = "& " + quoteCursorPowerShell(nodePath) + " " + command = powerShellSource(nodePath, scriptPath)
quoteCursorPowerShell(scriptPath) + "; exit $LASTEXITCODE"
} }
return map[string]any{ return map[string]any{
"command": command, "timeout": cursorHookTimeout, "failClosed": true, "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) { func parseCursorLegacyCommand(command string) (string, bool) {
separator := strings.IndexByte(command, ' ') separator := strings.IndexByte(command, ' ')
if separator < 0 { if separator < 0 {
@@ -164,11 +71,6 @@ func parseCursorLegacyCommand(command string) (string, bool) {
return script, script != "" && !strings.ContainsAny(script, "\r\n") 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) { func cursorManagedScriptPath(handler map[string]any) (string, bool) {
if len(handler) == 3 && handler["timeout"] == cursorHookTimeout && handler["failClosed"] == true { if len(handler) == 3 && handler["timeout"] == cursorHookTimeout && handler["failClosed"] == true {
command, ok := handler["command"].(string) command, ok := handler["command"].(string)
@@ -178,12 +80,12 @@ func cursorManagedScriptPath(handler map[string]any) (string, bool) {
var nodePath, scriptPath string var nodePath, scriptPath string
var parsed bool var parsed bool
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
nodePath, scriptPath, parsed = parseCursorPowerShellCommand(command) nodePath, scriptPath, parsed = parsePowerShellSource(command)
} else { } else {
nodePath, scriptPath, parsed = parseCursorPOSIXCommand(command) nodePath, scriptPath, parsed = parsePOSIXCommand(command)
} }
if parsed && filepath.IsAbs(nodePath) && filepath.IsAbs(scriptPath) && if parsed && filepath.IsAbs(nodePath) && filepath.IsAbs(scriptPath) &&
isCursorNodeExecutable(nodePath) { isNodeExecutable(nodePath) {
return scriptPath, true return scriptPath, true
} }
} }
@@ -207,7 +109,7 @@ func cursorManagedAgentID(
return "", false return "", false
} }
agentDirectory := filepath.Dir(scriptPath) agentDirectory := filepath.Dir(scriptPath)
if !sameCursorPath(filepath.Dir(agentDirectory), runtimeRoot) { if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false return "", false
} }
agentID := filepath.Base(agentDirectory) 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])) kept := make([]map[string]any, 0, len(result[contract.event]))
for _, handler := range result[contract.event] { for _, handler := range result[contract.event] {
managedID, managed := cursorManagedAgentID(handler, contract.script, runtimeRoot) managedID, managed := cursorManagedAgentID(handler, contract.script, runtimeRoot)
if managed && (agentID == "" || sameCursorAgentID(managedID, agentID)) { if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) {
continue continue
} }
kept = append(kept, handler) kept = append(kept, handler)
@@ -384,11 +286,7 @@ func managedCursorIDs(
if !managed { if !managed {
continue continue
} }
key := agentID result[managedReferenceKey(agentID)] = agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(agentID)
}
result[key] = agentID
} }
return result return result
} }
+19 -264
View File
@@ -1,13 +1,9 @@
package plugins package plugins
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings"
) )
func cursorConfigPath() (string, error) { func cursorConfigPath() (string, error) {
@@ -27,21 +23,14 @@ func readCursorDocument() (*cursorDocument, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
info, err := os.Lstat(filePath) snapshot, err := readManagedFile(filePath, "Cursor user hooks", maxManagedSourceBytes)
if errors.Is(err, os.ErrNotExist) { if err != nil {
return nil, err
}
if snapshot == nil {
return createCursorDocument(filePath), nil return createCursorDocument(filePath), nil
} }
if err != nil { return parseCursorDocument(filePath, snapshot.contents, runtimeRoot)
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)
} }
func prepareRenderedCursorChange(rendered *cursorRenderedDocument) (*fileChange, error) { func prepareRenderedCursorChange(rendered *cursorRenderedDocument) (*fileChange, error) {
@@ -59,168 +48,26 @@ 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( func prepareCursorInstallationChanges(
config InstallConfig, config InstallConfig,
agentDirectory string, paths *managedRuntimePaths,
rendered *cursorRenderedDocument, rendered *cursorRenderedDocument,
) ([]*fileChange, error) { ) ([]*fileChange, error) {
baseURL := config.BaseURL if config.BaseURL == "" {
if baseURL == "" { config.BaseURL = "https://api.elydora.com"
baseURL = "https://api.elydora.com"
} }
runtimeConfig, err := json.MarshalIndent(agentRuntimeConfig{ changes, err := managedRuntimeFileChanges(
OrgID: config.OrgID, AgentID: config.AgentID, KID: config.KID, config,
BaseURL: baseURL, Token: config.Token, AgentName: cursorAgentKey, paths,
}, "", " ") cursorAgentKey,
if err != nil { generateGuardScript(
return nil, fmt.Errorf("encode Elydora runtime config: %w", err) cursorAgentKey, config.AgentID, `{"permission":"allow"}`+"\n", true, "cursor",
} ),
runtimeConfig = append(runtimeConfig, '\n') buildHookScriptWithOutput(cursorAgentKey, config.AgentID, "{}\n", true, true),
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 { if err != nil {
return nil, err return nil, err
} }
changes = append(changes, change)
}
documentChange, err := prepareRenderedCursorChange(rendered) documentChange, err := prepareRenderedCursorChange(rendered)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -228,98 +75,6 @@ func prepareCursorInstallationChanges(
return append(changes, documentChange), nil return append(changes, documentChange), nil
} }
func ensureCursorDirectory(path, label string) error { func cursorRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
if err := os.MkdirAll(path, 0700); err != nil { return managedRuntimeFilesPresent(contracts, cursorAgentKey)
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
} }
+2 -2
View File
@@ -183,7 +183,7 @@ func TestCursorInstallCreatesRuntimeDirectoryTransactionally(t *testing.T) {
fixture.guardPath, fixture.guardPath,
fixture.hookPath, 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) t.Fatalf("runtime file %s exists = %v, %v", path, exists, err)
} }
} }
@@ -200,7 +200,7 @@ func TestCursorInstallRollsBackEveryFileOnConfigCommitFailure(t *testing.T) {
} }
failed := false failed := false
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if !failed && sameCursorPath(destination, fixture.configPath) { if !failed && sameManagedPath(destination, fixture.configPath) {
failed = true failed = true
return errors.New("injected Cursor config commit failure") return errors.New("injected Cursor config commit failure")
} }
+4 -6
View File
@@ -7,14 +7,12 @@ type DroidPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Droid commits its guard, audit runtime, // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// credentials, and hook documents in one transaction.
func (p *DroidPlugin) ManagesGuardRuntime() bool { func (p *DroidPlugin) ManagesGuardRuntime() bool {
return true return true
} }
// PreflightInstall validates every hook source, policy layer, credential, // PreflightInstall validates every source before the CLI creates runtime state.
// matcher, and runtime identity before any write.
func (p *DroidPlugin) PreflightInstall(config InstallConfig) error { func (p *DroidPlugin) PreflightInstall(config InstallConfig) error {
sources, err := readDroidSources() sources, err := readDroidSources()
if err != nil { if err != nil {
@@ -107,7 +105,7 @@ func (p *DroidPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
runtimeRoot, err := droidRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
return err return err
} }
@@ -133,7 +131,7 @@ func (p *DroidPlugin) Status() (PluginStatus, error) {
if droidHookBlocked(sources) != nil { if droidHookBlocked(sources) != nil {
return status, nil return status, nil
} }
runtimeRoot, err := droidRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
return status, err return status, err
} }
+1 -1
View File
@@ -239,7 +239,7 @@ func droidAdditionsFor(
document, target *droidDocument, document, target *droidDocument,
groups map[string]map[string]any, groups map[string]map[string]any,
) 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 groups
} }
return map[string]map[string]any{} return map[string]map[string]any{}
+9 -151
View File
@@ -20,8 +20,6 @@ const (
droidAuditScript = "hook.js" droidAuditScript = "hook.js"
droidHookTimeout = float64(10) droidHookTimeout = float64(10)
droidOwnedFileMarker = "// Managed by Elydora" droidOwnedFileMarker = "// Managed by Elydora"
droidPOSIXApostrophe = `'"'"'`
droidWindowsExitSuffix = "; exit $LASTEXITCODE"
) )
var droidToolEvents = [...]string{"PreToolUse", "PostToolUse"} var droidToolEvents = [...]string{"PreToolUse", "PostToolUse"}
@@ -35,11 +33,7 @@ type droidManagedRemoval struct {
removeGroup bool removeGroup bool
} }
type droidRuntimeContract struct { type droidRuntimeContract = managedRuntimeContract
agentID string
guardPath string
auditPath string
}
type droidRegexEntry struct { type droidRegexEntry struct {
Label string `json:"label"` Label string `json:"label"`
@@ -178,145 +172,21 @@ for (const entry of entries) {
return nil 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 { func buildDroidGroup(nodePath, scriptPath string) map[string]any {
return map[string]any{ return map[string]any{
"matcher": "*", "matcher": "*",
"hooks": []any{map[string]any{ "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) { func parseDroidCommand(command string, includeLegacy ...bool) (string, string, bool) {
if runtime.GOOS != "windows" { executable, script, ok := parseShellCommand(command)
return parseDroidPOSIXCommand(command) if ok || runtime.GOOS != "windows" || len(includeLegacy) == 0 || !includeLegacy[0] {
}
executable, script, ok := parseDroidPowerShellCommand(command)
if ok || len(includeLegacy) == 0 || !includeLegacy[0] {
return executable, script, ok return executable, script, ok
} }
return parseDroidLegacyWindowsCommand(command) return parseQuotedWindowsCommand(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()
} }
func managedDroidAgentID( func managedDroidAgentID(
@@ -333,29 +203,17 @@ func managedDroidAgentID(
} }
executable, scriptPath, ok := parseDroidCommand(command, includeLegacy...) executable, scriptPath, ok := parseDroidCommand(command, includeLegacy...)
if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) || if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) ||
!isDroidNodeExecutable(executable) || !sameDroidFileName(filepath.Base(scriptPath), scriptName) { !isNodeExecutable(executable) || !sameManagedName(filepath.Base(scriptPath), scriptName) {
return "", false return "", false
} }
agentDirectory := filepath.Dir(scriptPath) agentDirectory := filepath.Dir(scriptPath)
if !sameDroidPath(filepath.Dir(agentDirectory), runtimeRoot) { if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false return "", false
} }
agentID := filepath.Base(agentDirectory) agentID := filepath.Base(agentDirectory)
return agentID, agentID != "" && agentID != "." && agentID != ".." 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 { func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot string) []droidManagedRemoval {
removals := make([]droidManagedRemoval, 0) removals := make([]droidManagedRemoval, 0)
for _, contract := range []struct{ event, script string }{ for _, contract := range []struct{ event, script string }{
@@ -370,7 +228,7 @@ func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot strin
managedID, managed := managedDroidAgentID( managedID, managed := managedDroidAgentID(
handlerValue.(map[string]any), contract.script, runtimeRoot, true, handlerValue.(map[string]any), contract.script, runtimeRoot, true,
) )
if managed && (agentID == "" || sameDroidAgentID(managedID, agentID)) { if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) {
indexes = append(indexes, handlerIndex) indexes = append(indexes, handlerIndex)
} }
} }
@@ -400,7 +258,7 @@ func droidRuntimeContracts(settings droidHookSettings, runtimeRoot string) []dro
contracts := make([]droidRuntimeContract, 0, len(guardIDs)) contracts := make([]droidRuntimeContract, 0, len(guardIDs))
for _, guardID := range guardIDs { for _, guardID := range guardIDs {
for auditID := range audits { for auditID := range audits {
if !sameDroidAgentID(guardID, auditID) { if !sameManagedAgentID(guardID, auditID) {
continue continue
} }
root := filepath.Join(runtimeRoot, guardID) root := filepath.Join(runtimeRoot, guardID)
+23 -156
View File
@@ -1,24 +1,13 @@
package plugins package plugins
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings"
) )
type droidRuntimePaths struct {
runtimeRoot string
agentDirectory string
guardPath string
auditPath string
}
type preparedDroidInstallation struct { type preparedDroidInstallation struct {
changes []*fileChange changes []*fileChange
preconditions []filePrecondition preconditions []filePrecondition
paths *droidRuntimePaths paths *managedRuntimePaths
} }
type preparedDroidUninstall struct { 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( func preflightDroidInstallation(
config InstallConfig, config InstallConfig,
sources *droidSources, sources *droidSources,
) (*droidRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if sources == nil || sources.root == nil || sources.policy == nil { if sources == nil || sources.root == nil || sources.policy == nil {
return nil, "", fmt.Errorf("Factory Droid installation sources are required") return nil, "", fmt.Errorf("Factory Droid installation sources are required")
} }
@@ -126,50 +45,25 @@ func preflightDroidInstallation(
if err := validateDroidRegexes(hooks...); err != nil { if err := validateDroidRegexes(hooks...); err != nil {
return nil, "", err 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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Factory Droid")
if err != nil { if err != nil {
return nil, "", err 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 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( func prepareRenderedDroidChange(
rendered *droidRenderedDocument, rendered *droidRenderedDocument,
) (*fileChange, error) { ) (*fileChange, error) {
@@ -198,7 +92,7 @@ func validateDroidRenderedSet(
matches := 0 matches := 0
for _, item := range rendered { for _, item := range rendered {
if item != nil && item.document != nil && if item != nil && item.document != nil &&
sameDroidPath(item.document.filePath, document.filePath) { sameManagedPath(item.document.filePath, document.filePath) {
matches++ matches++
} }
} }
@@ -217,7 +111,7 @@ func droidSourcePreconditions(
for _, document := range droidSourceDocuments(sources) { for _, document := range droidSourceDocuments(sources) {
changed := false changed := false
for _, path := range changedPaths { for _, path := range changedPaths {
if sameDroidPath(document.filePath, path) { if sameManagedPath(document.filePath, path) {
changed = true changed = true
break break
} }
@@ -246,38 +140,13 @@ func prepareDroidInstallation(
if err := validateDroidRenderedSet(sources, rendered); err != nil { if err := validateDroidRenderedSet(sources, rendered); err != nil {
return nil, err 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 { if err != nil {
return nil, err 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)) changedPaths := make([]string, 0, len(rendered))
for _, document := range rendered { for _, document := range rendered {
change, changeErr := prepareRenderedDroidChange(document) change, changeErr := prepareRenderedDroidChange(document)
@@ -305,14 +174,7 @@ func writeDroidChanges(
runtimeRoot, agentDirectory string, runtimeRoot, agentDirectory string,
preconditions []filePrecondition, preconditions []filePrecondition,
) error { ) error {
hasChanges := false if !hasFileChanges(changes) {
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
return writeChanges(changes, label, rename, preconditions...) return writeChanges(changes, label, rename, preconditions...)
} }
if err := assertFilePreconditions(preconditions, label); err != nil { if err := assertFilePreconditions(preconditions, label); err != nil {
@@ -365,7 +227,7 @@ func prepareDroidUninstall(
for _, item := range rendered { for _, item := range rendered {
changed := false changed := false
for _, path := range changedPaths { for _, path := range changedPaths {
if sameDroidPath(item.document.filePath, path) { if sameManagedPath(item.document.filePath, path) {
changed = true changed = true
break break
} }
@@ -398,3 +260,8 @@ func commitDroidUninstall(
prepared.preconditions, 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 package plugins
import ( func droidRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
"bytes" return managedRuntimeFilesExist(contracts, droidAgentKey, "Factory Droid", droidExpectedScripts)
"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
} }
+1 -1
View File
@@ -57,7 +57,7 @@ func TestDroidInstallWritesCurrentContainerAndCompleteRuntime(t *testing.T) {
requireDroidNativeGroup(t, auditGroup) requireDroidNativeGroup(t, auditGroup)
guardCommand := droidManagedHandler(t, hooks["PreToolUse"], fixture.guardPath)["command"].(string) guardCommand := droidManagedHandler(t, hooks["PreToolUse"], fixture.guardPath)["command"].(string)
if runtime.GOOS == "windows" && 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) t.Fatalf("Windows Factory command = %q", guardCommand)
} }
runtimeConfig := readDroidTestObject(t, fixture.runtimeConfig) runtimeConfig := readDroidTestObject(t, fixture.runtimeConfig)
@@ -237,7 +237,7 @@ func droidManagedGroup(t *testing.T, groupsValue any, scriptPath string) map[str
handler := requireDroidObject(t, handlerValue) handler := requireDroidObject(t, handlerValue)
command, _ := handler["command"].(string) command, _ := handler["command"].(string)
_, configuredPath, managed := parseDroidCommand(command, true) _, configuredPath, managed := parseDroidCommand(command, true)
if managed && sameDroidPath(configuredPath, scriptPath) { if managed && sameManagedPath(configuredPath, scriptPath) {
return group return group
} }
} }
@@ -253,7 +253,7 @@ func droidManagedHandler(t *testing.T, groupsValue any, scriptPath string) map[s
handler := requireDroidObject(t, handlerValue) handler := requireDroidObject(t, handlerValue)
command, _ := handler["command"].(string) command, _ := handler["command"].(string)
_, configuredPath, managed := parseDroidCommand(command, true) _, configuredPath, managed := parseDroidCommand(command, true)
if managed && sameDroidPath(configuredPath, scriptPath) { if managed && sameManagedPath(configuredPath, scriptPath) {
return handler return handler
} }
} }
@@ -53,7 +53,7 @@ func TestDroidInstallRollsBackRuntimeAfterLateHookCommitFailure(t *testing.T) {
prepared := prepareDroidTestInstallation(t, fixture, fixture.config) prepared := prepareDroidTestInstallation(t, fixture, fixture.config)
failed := false failed := false
rename := func(source, destination string) error { rename := func(source, destination string) error {
if !failed && sameDroidPath(destination, fixture.configPath) { if !failed && sameManagedPath(destination, fixture.configPath) {
failed = true failed = true
return errors.New("injected Droid hook commit failure") return errors.New("injected Droid hook commit failure")
} }
@@ -85,7 +85,7 @@ func TestDroidConcurrentActiveHookReplacementPreventsRuntimeCommit(t *testing.T)
t.Fatalf("concurrent source error = %v", err) t.Fatalf("concurrent source error = %v", err)
} }
for path, source := range before { for path, source := range before {
if sameDroidPath(path, fixture.configPath) { if sameManagedPath(path, fixture.configPath) {
continue continue
} }
if readDroidTestFile(t, path) != source { if readDroidTestFile(t, path) != source {
@@ -205,7 +205,7 @@ func TestDroidSameContentReplacementIsRejectedByPhysicalIdentity(t *testing.T) {
t.Fatal("same-content replacement changed") t.Fatal("same-content replacement changed")
} }
for path, source := range before { 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) t.Fatalf("runtime changed after inode replacement: %s", path)
} }
} }
@@ -226,7 +226,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read uninstall sources: %v", err) t.Fatalf("read uninstall sources: %v", err)
} }
runtimeRoot, err := droidRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
t.Fatalf("resolve runtime root: %v", err) t.Fatalf("resolve runtime root: %v", err)
} }
@@ -240,7 +240,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) {
} }
failed := false failed := false
rename := func(source, destination string) error { rename := func(source, destination string) error {
if !failed && sameDroidPath(destination, fixture.settingsPath) { if !failed && sameManagedPath(destination, fixture.settingsPath) {
failed = true failed = true
return errors.New("injected Droid uninstall failure") return errors.New("injected Droid uninstall failure")
} }
+5 -4
View File
@@ -10,8 +10,7 @@ type GeminiPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Gemini commits both generated runtimes and // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// the user settings document through one transaction.
func (p *GeminiPlugin) ManagesGuardRuntime() bool { func (p *GeminiPlugin) ManagesGuardRuntime() bool {
return true return true
} }
@@ -92,13 +91,14 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
if err := writeGeminiChanges( if err := writeManagedChanges(
changes, changes,
"Install Gemini CLI hooks", "Install Gemini CLI hooks",
p.rename, p.rename,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Gemini CLI configuration directory",
); err != nil { ); err != nil {
return err return err
} }
@@ -120,13 +120,14 @@ func (p *GeminiPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
return writeGeminiChanges( return writeManagedChanges(
[]*fileChange{change}, []*fileChange{change},
"Uninstall Gemini CLI hooks", "Uninstall Gemini CLI hooks",
p.rename, p.rename,
"", "",
"", "",
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Gemini CLI configuration directory",
) )
} }
+15 -74
View File
@@ -7,83 +7,37 @@ import (
"strings" "strings"
) )
type geminiRuntimeReference struct { type geminiRuntimeReference = managedScriptReference
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")
}
func buildGeminiCommand(runtimePath, scriptPath string) (string, error) { func buildGeminiCommand(runtimePath, scriptPath string) (string, error) {
if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) { if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) {
return "", fmt.Errorf( return "", fmt.Errorf("Gemini CLI hook commands require absolute runtime and script paths")
"Gemini CLI hook commands require absolute runtime and script paths",
)
} }
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), nil return posixSource(runtimePath, scriptPath), nil
} }
source := "& " + quoteCodexPowerShell(runtimePath) + " " + return "& " + quotePowerShellArgument(windowsPowerShellPath()) + powerShellEncodedFlags +
quoteCodexPowerShell(scriptPath) + "; exit $LASTEXITCODE" encodePowerShellSource(powerShellSource(runtimePath, scriptPath)), nil
return "& " + quoteCodexPowerShell(codexPowerShellPath()) +
" -NoLogo -NoProfile -NonInteractive -EncodedCommand " +
encodeCodexPowerShell(source), nil
} }
func parseGeminiWindowsCommand(command string) (string, string, bool) { func parseGeminiWindowsCommand(command string) (string, string, bool) {
if !strings.HasPrefix(command, "& ") { if !strings.HasPrefix(command, "& ") {
return "", "", false return "", "", false
} }
powerShell, next, ok := readCodexPowerShellArgument(command, 2) powerShell, next, ok := readPowerShellArgument(command, 2)
if !ok || !isAbsoluteWindowsPath(powerShell) || if !ok || !isPowerShellExecutable(powerShell) ||
!strings.EqualFold(windowsPathBase(powerShell), "powershell.exe") { !strings.HasPrefix(command[next:], powerShellEncodedFlags) {
return "", "", false return "", "", false
} }
prefix := " -NoLogo -NoProfile -NonInteractive -EncodedCommand " source, ok := decodePowerShellSource(command[next+len(powerShellEncodedFlags):])
if !strings.HasPrefix(command[next:], prefix) {
return "", "", false
}
source, ok := decodeCodexPowerShell(command[next+len(prefix):])
if !ok { if !ok {
return "", "", false return "", "", false
} }
return parseCodexPowerShellSource(source) return parsePowerShellSource(source)
} }
func parseGeminiCommand(command string) (string, string, bool) { 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 runtimePath, scriptPath, true
} }
return parseGeminiWindowsCommand(command) return parseGeminiWindowsCommand(command)
@@ -110,23 +64,10 @@ func geminiRuntimeReferenceForCommand(
if !current && includeLegacy { if !current && includeLegacy {
runtimePath, scriptPath, legacy = parseLegacyGeminiCommand(command) runtimePath, scriptPath, legacy = parseLegacyGeminiCommand(command)
} }
validRuntime := (current && filepath.IsAbs(runtimePath) && validRuntime := (current && filepath.IsAbs(runtimePath) && isNodeExecutable(runtimePath)) ||
isGeminiNodeExecutable(runtimePath)) || (legacy && runtimePath == "node") (legacy && runtimePath == "node")
if !validRuntime || !filepath.IsAbs(scriptPath) || if !validRuntime {
!sameGeminiFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil return nil, nil
} }
agentDirectory := filepath.Dir(scriptPath) return resolveManagedScript(scriptPath, scriptName)
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
} }
+3 -25
View File
@@ -1,10 +1,7 @@
package plugins package plugins
import ( import (
"path/filepath"
"runtime"
"sort" "sort"
"strings"
) )
const ( const (
@@ -27,11 +24,7 @@ type geminiManagedRemoval struct {
removeGroup bool removeGroup bool
} }
type geminiRuntimeContract struct { type geminiRuntimeContract = managedRuntimeContract
agentID string
guardPath string
auditPath string
}
func buildGeminiGroup(runtimePath, scriptPath, name string) (map[string]any, error) { func buildGeminiGroup(runtimePath, scriptPath, name string) (map[string]any, error) {
command, err := buildGeminiCommand(runtimePath, scriptPath) command, err := buildGeminiCommand(runtimePath, scriptPath)
@@ -151,7 +144,7 @@ func managedGeminiRemovals(
return nil, err return nil, err
} }
if reference != nil && if reference != nil &&
(agentID == "" || sameGeminiAgentID(reference.agentID, agentID)) { (agentID == "" || sameManagedAgentID(reference.agentID, agentID)) {
indexes = append(indexes, handlerIndex) indexes = append(indexes, handlerIndex)
} }
} }
@@ -168,13 +161,6 @@ func managedGeminiRemovals(
return removals, nil return removals, nil
} }
func geminiReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func geminiReferencesForEvent( func geminiReferencesForEvent(
groups []any, groups []any,
scriptName string, scriptName string,
@@ -196,7 +182,7 @@ func geminiReferencesForEvent(
return nil, err return nil, err
} }
if reference != nil { if reference != nil {
key := geminiReferenceKey(reference.agentID) key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference) result[key] = append(result[key], *reference)
} }
} }
@@ -243,11 +229,3 @@ func geminiRuntimeContracts(
} }
return contracts, nil 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 package plugins
import ( import "fmt"
"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
}
func preflightGeminiInstallation( func preflightGeminiInstallation(
config InstallConfig, config InstallConfig,
document *geminiDocument, document *geminiDocument,
) (*geminiRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if document == nil || document.filePath == "" { if document == nil || document.filePath == "" {
return nil, "", fmt.Errorf( return nil, "", fmt.Errorf("Gemini CLI installation requires a user settings document")
"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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Gemini CLI")
if err != nil { if err != nil {
return nil, "", err 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 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( func prepareGeminiInstallationChanges(
config InstallConfig, config InstallConfig,
paths *geminiRuntimePaths, paths *managedRuntimePaths,
rendered *geminiRenderedDocument, rendered *geminiRenderedDocument,
) ([]*fileChange, error) { ) ([]*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 { if err != nil {
return nil, err 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) documentChange, err := prepareRenderedGeminiChange(rendered)
if err != nil { if err != nil {
return nil, err return nil, err
-35
View File
@@ -67,38 +67,3 @@ func prepareRenderedGeminiChange(
rendered.remove, 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"}, }, "query parameters"},
{"guard path", func(f *geminiFixture) { {"guard path", func(f *geminiFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js") f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"}, }, "managed agent directory"},
{"audit path", func(f *geminiFixture) { {"audit path", func(f *geminiFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js") 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) { t.Run(testCase.name, func(t *testing.T) {
fixture := prepareGeminiFixture(t, geminiFixtureOptions{}) fixture := prepareGeminiFixture(t, geminiFixtureOptions{})
+2 -228
View File
@@ -1,231 +1,5 @@
package plugins package plugins
import ( func geminiRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
"fmt" return managedRuntimeFilesExist(contracts, geminiAgentKey, "Gemini CLI", nil)
"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
} }
+4 -4
View File
@@ -33,8 +33,8 @@ func TestGeminiCommandRoundTripAndRejectsInjection(t *testing.T) {
t.Fatalf("build Gemini command: %v", err) t.Fatalf("build Gemini command: %v", err)
} }
executable, script, ok := parseGeminiCommand(command) executable, script, ok := parseGeminiCommand(command)
if !ok || !sameGeminiPath(executable, nodePath) || if !ok || !sameManagedPath(executable, nodePath) ||
!sameGeminiPath(script, fixture.guardPath) { !sameManagedPath(script, fixture.guardPath) {
t.Fatalf("parsed Gemini command = %q, %q, %v", executable, script, ok) t.Fatalf("parsed Gemini command = %q, %q, %v", executable, script, ok)
} }
if runtime.GOOS == "windows" && (strings.Contains(command, "%GEMINI_CWD%") || if runtime.GOOS == "windows" && (strings.Contains(command, "%GEMINI_CWD%") ||
@@ -425,12 +425,12 @@ func TestGeminiRuntimeConfigOmitsEmptyOptionalToken(t *testing.T) {
func TestGeminiAgentIDMatchingIsPlatformCorrect(t *testing.T) { func TestGeminiAgentIDMatchingIsPlatformCorrect(t *testing.T) {
if runtime.GOOS == "windows" { 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") t.Fatal("Windows Gemini agent IDs should compare case-insensitively")
} }
return return
} }
if sameGeminiAgentID("AGENT-1", "agent-1") { if sameManagedAgentID("AGENT-1", "agent-1") {
t.Fatal("POSIX Gemini agent IDs should preserve case") t.Fatal("POSIX Gemini agent IDs should preserve case")
} }
} }
@@ -251,7 +251,7 @@ func runGeminiRawCommand(
var process *exec.Cmd var process *exec.Cmd
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
process = exec.Command( process = exec.Command(
codexPowerShellPath(), windowsPowerShellPath(),
"-NoLogo", "-NoLogo",
"-NoProfile", "-NoProfile",
"-NonInteractive", "-NonInteractive",
@@ -35,7 +35,7 @@ func prepareGeminiTestChanges(
t *testing.T, t *testing.T,
fixture *geminiFixture, fixture *geminiFixture,
document *geminiDocument, document *geminiDocument,
) ([]*fileChange, *geminiRuntimePaths) { ) ([]*fileChange, *managedRuntimePaths) {
t.Helper() t.Helper()
paths, nodePath, err := preflightGeminiInstallation(fixture.config, document) paths, nodePath, err := preflightGeminiInstallation(fixture.config, document)
if err != nil { if err != nil {
@@ -110,7 +110,7 @@ func TestGeminiInstallRollsBackAllFiveFilesAfterSettingsFailure(t *testing.T) {
fixture.config.OrgID = "org-updated" fixture.config.OrgID = "org-updated"
fixture.config.Token = "token-updated" fixture.config.Token = "token-updated"
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameGeminiPath(destination, fixture.settingsPath) && if sameManagedPath(destination, fixture.settingsPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Gemini settings failure") 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 { if err := os.WriteFile(fixture.settingsPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent settings: %v", err) t.Fatalf("write concurrent settings: %v", err)
} }
err = writeGeminiChanges( err = writeManagedChanges(
changes, changes,
"Install Gemini CLI hooks", "Install Gemini CLI hooks",
nil, nil,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.filePath), filepath.Dir(document.filePath),
"Gemini CLI configuration directory",
) )
if err == nil || !strings.Contains(err.Error(), "changed during installation") { if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err) t.Fatalf("prepared install error = %v", err)
@@ -204,7 +205,7 @@ func TestGeminiUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed Gemini settings: %v", err) t.Fatalf("read installed Gemini settings: %v", err)
} }
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameGeminiPath(destination, fixture.settingsPath) && if sameManagedPath(destination, fixture.settingsPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Gemini uninstall failure") 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 { if err != nil {
return err return err
} }
guardCommand, err := buildGrokCommand(nodePath, paths.guardPath) guardCommand, err := buildEncodedCommand("Grok", nodePath, paths.guardPath)
if err != nil { if err != nil {
return err return err
} }
auditCommand, err := buildGrokCommand(nodePath, paths.auditPath) auditCommand, err := buildEncodedCommand("Grok", nodePath, paths.auditPath)
if err != nil { if err != nil {
return err return err
} }
+6 -117
View File
@@ -1,132 +1,21 @@
package plugins package plugins
import ( import "path/filepath"
"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
}
func parseGrokCommand(command string) (string, string, bool) { 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 return runtimePath, scriptPath, true
} }
if runtimePath, scriptPath, ok := parseCodexWindowsCommand(command); ok { return parseQuotedWindowsCommand(command)
return runtimePath, scriptPath, true
}
return parseGrokLegacyWindowsCommand(command)
}
func isGrokNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
} }
func grokRuntimeReferenceForCommand( func grokRuntimeReferenceForCommand(
command string, command string,
scriptName string, scriptName string,
) (*grokRuntimeReference, error) { ) (*managedScriptReference, error) {
runtimePath, scriptPath, ok := parseGrokCommand(command) runtimePath, scriptPath, ok := parseGrokCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) || if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
!isGrokNodeExecutable(runtimePath) ||
!sameGrokFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil return nil, nil
} }
agentDirectory := filepath.Dir(scriptPath) return resolveManagedScript(scriptPath, scriptName)
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
} }
+13 -36
View File
@@ -6,9 +6,7 @@ import (
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
"runtime"
"sort" "sort"
"strings"
) )
const ( const (
@@ -48,20 +46,6 @@ type grokRenderedDocument struct {
remove bool 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 { func cloneGrokHooks(source grokHooks) grokHooks {
clone := make(grokHooks, len(source)) clone := make(grokHooks, len(source))
for event, groups := range 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) { 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) 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) { func readGrokHooks(root map[string]any) (grokHooks, error) {
@@ -229,7 +213,7 @@ func renderGrokHooks(hooks grokHooks) map[string]any {
for event, groups := range hooks { for event, groups := range hooks {
values := make([]any, 0, len(groups)) values := make([]any, 0, len(groups))
for _, group := range groups { for _, group := range groups {
object := cloneGrokObject(group.object) object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers)) handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers { for _, handler := range group.handlers {
handlers = append(handlers, handler) handlers = append(handlers, handler)
@@ -262,7 +246,7 @@ func exactManagedGrokGroup(group grokGroup) bool {
func managedGrokReference( func managedGrokReference(
handler map[string]any, handler map[string]any,
scriptName string, scriptName string,
) (*grokRuntimeReference, error) { ) (*managedScriptReference, error) {
if len(handler) != 3 || handler["type"] != "command" || if len(handler) != 3 || handler["type"] != "command" ||
handler["timeout"] != grokHookTimeout { handler["timeout"] != grokHookTimeout {
return nil, nil return nil, nil
@@ -303,7 +287,7 @@ func removeManagedGrokGroups(
return nil, err return nil, err
} }
remove := reference != nil && remove := reference != nil &&
(agentID == "" || sameGrokAgentID(reference.agentID, agentID)) (agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if !remove { if !remove {
kept = append(kept, handler) kept = append(kept, handler)
} }
@@ -386,7 +370,7 @@ func renderGrokDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneGrokObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
@@ -407,18 +391,11 @@ func renderGrokDocument(
}, nil }, nil
} }
func grokReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func grokReferencesForEvent( func grokReferencesForEvent(
groups []grokGroup, groups []grokGroup,
scriptName string, scriptName string,
) (map[string][]grokRuntimeReference, error) { ) (map[string][]managedScriptReference, error) {
result := map[string][]grokRuntimeReference{} result := map[string][]managedScriptReference{}
for _, group := range groups { for _, group := range groups {
if !exactManagedGrokGroup(group) { if !exactManagedGrokGroup(group) {
continue continue
@@ -431,14 +408,14 @@ func grokReferencesForEvent(
if reference == nil { if reference == nil {
continue continue
} }
key := grokReferenceKey(reference.agentID) key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference) result[key] = append(result[key], *reference)
} }
} }
return result, nil return result, nil
} }
func grokRuntimeContracts(hooks grokHooks) ([]grokRuntimeContract, error) { func grokRuntimeContracts(hooks grokHooks) ([]managedRuntimeContract, error) {
guards, err := grokReferencesForEvent(hooks["PreToolUse"], grokGuardScript) guards, err := grokReferencesForEvent(hooks["PreToolUse"], grokGuardScript)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -459,16 +436,16 @@ func grokRuntimeContracts(hooks grokHooks) ([]grokRuntimeContract, error) {
keys = append(keys, key) keys = append(keys, key)
} }
sort.Strings(keys) sort.Strings(keys)
contracts := make([]grokRuntimeContract, 0, len(keys)) contracts := make([]managedRuntimeContract, 0, len(keys))
for _, key := range keys { for _, key := range keys {
guard := guards[key] guard := guards[key]
success := successes[key] success := successes[key]
failure := failures[key] failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 || 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 continue
} }
contracts = append(contracts, grokRuntimeContract{ contracts = append(contracts, managedRuntimeContract{
agentID: guard[0].agentID, guardPath: guard[0].scriptPath, agentID: guard[0].agentID, guardPath: guard[0].scriptPath,
auditPath: success[0].scriptPath, auditPath: success[0].scriptPath,
}) })
+17 -135
View File
@@ -1,102 +1,24 @@
package plugins package plugins
import ( import "fmt"
"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
}
func preflightGrokInstallation( func preflightGrokInstallation(
config InstallConfig, config InstallConfig,
document *grokDocument, document *grokDocument,
) (*grokRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if document == nil { if document == nil {
return nil, "", fmt.Errorf("grok installation requires a hook document") 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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveNodeRuntime()
@@ -106,63 +28,23 @@ func preflightGrokInstallation(
return paths, nodePath, nil return paths, nodePath, nil
} }
func buildGrokRuntimeConfig(config InstallConfig) ([]byte, error) { func grokExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
value := map[string]any{ return []byte(generateGuardScript(grokAgentKey, agentID, "", false, "grok")),
"org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID, []byte(buildHookScriptWithOutput(grokAgentKey, agentID, "", false, true))
"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 prepareGrokInstallationChanges( func prepareGrokInstallationChanges(
config InstallConfig, config InstallConfig,
paths *grokRuntimePaths, paths *managedRuntimePaths,
rendered *grokRenderedDocument, rendered *grokRenderedDocument,
) ([]*fileChange, error) { ) ([]*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 { if err != nil {
return nil, err 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) documentChange, err := prepareRenderedGrokChange(rendered)
if err != nil { if err != nil {
return nil, err return nil, err
+5 -20
View File
@@ -85,32 +85,17 @@ func writeGrokChanges(
agentDirectory string, agentDirectory string,
hooksDirectory string, hooksDirectory string,
) error { ) error {
hasChanges := false if !hasFileChanges(changes) {
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
return nil return nil
} }
if agentDirectory != "" {
if err := EnsurePrivateDirectory(runtimeRoot); err != nil {
return err
}
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
return err
}
}
if err := ensureManagedDirectory( if err := ensureManagedDirectory(
filepath.Dir(hooksDirectory), filepath.Dir(hooksDirectory),
"Grok home directory", "Grok home directory",
); err != nil { ); err != nil {
return err return err
} }
if err := ensureManagedDirectory(hooksDirectory, "Grok hooks directory"); err != nil { return writeManagedChanges(
return err changes, label, rename, runtimeRoot, agentDirectory,
} hooksDirectory, "Grok hooks directory",
return writeChanges(changes, label, rename) )
} }
+2 -2
View File
@@ -164,10 +164,10 @@ func TestGrokValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) {
}, "query parameters"}, }, "query parameters"},
{"guard path", func(f *grokFixture) { {"guard path", func(f *grokFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js") f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"}, }, "managed agent directory"},
{"audit path", func(f *grokFixture) { {"audit path", func(f *grokFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js") 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) { t.Run(testCase.name, func(t *testing.T) {
fixture := prepareGrokFixture(t, grokFixtureOptions{}) fixture := prepareGrokFixture(t, grokFixtureOptions{})
+2 -229
View File
@@ -1,232 +1,5 @@
package plugins package plugins
import ( func grokRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
"fmt" return managedRuntimeFilesExist(contracts, grokAgentKey, "Grok", nil)
"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
} }
+8 -8
View File
@@ -27,22 +27,22 @@ func TestGrokCommandRoundTripAndRejectsInjection(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("resolve Node.js runtime: %v", err) t.Fatalf("resolve Node.js runtime: %v", err)
} }
command, err := buildGrokCommand(nodePath, fixture.guardPath) command, err := buildEncodedCommand("Grok", nodePath, fixture.guardPath)
if err != nil { if err != nil {
t.Fatalf("build Grok command: %v", err) t.Fatalf("build Grok command: %v", err)
} }
executable, script, ok := parseGrokCommand(command) executable, script, ok := parseGrokCommand(command)
if !ok || !sameGrokPath(executable, nodePath) || if !ok || !sameManagedPath(executable, nodePath) ||
!sameGrokPath(script, fixture.guardPath) { !sameManagedPath(script, fixture.guardPath) {
t.Fatalf("parsed Grok command = %q, %q, %v", executable, script, ok) 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) 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") 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") t.Fatal("accepted a relative Grok script path")
} }
for _, invalid := range []string{ for _, invalid := range []string{
@@ -337,12 +337,12 @@ func TestGrokRuntimeConfigOmitsEmptyOptionalToken(t *testing.T) {
func TestGrokWindowsAgentIDMatchingIsPlatformCorrect(t *testing.T) { func TestGrokWindowsAgentIDMatchingIsPlatformCorrect(t *testing.T) {
if runtime.GOOS == "windows" { 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") t.Fatal("Windows Grok agent IDs should compare case-insensitively")
} }
return return
} }
if sameGrokAgentID("AGENT-1", "agent-1") { if sameManagedAgentID("AGENT-1", "agent-1") {
t.Fatal("POSIX Grok agent IDs should preserve case") 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) t.Fatalf("resolve Node.js runtime: %v", err)
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return quoteGrokLegacyWindowsArgument(nodePath) + " " + return `"` + nodePath + `" "` + scriptPath + `"`
quoteGrokLegacyWindowsArgument(scriptPath)
} }
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath) return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
} }
+4 -4
View File
@@ -59,7 +59,7 @@ func TestGrokInstallRollsBackFourRuntimesAfterConfigCommitFailure(t *testing.T)
} }
before := snapshotGrokFiles(t, paths...) before := snapshotGrokFiles(t, paths...)
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameGrokPath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Grok config failure") return errors.New("injected Grok config failure")
} }
@@ -85,11 +85,11 @@ func TestPreparedGrokInstallRejectsConcurrentConfigChange(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("preflight Grok installation: %v", err) t.Fatalf("preflight Grok installation: %v", err)
} }
guardCommand, err := buildGrokCommand(nodePath, paths.guardPath) guardCommand, err := buildEncodedCommand("Grok", nodePath, paths.guardPath)
if err != nil { if err != nil {
t.Fatalf("build guard command: %v", err) t.Fatalf("build guard command: %v", err)
} }
auditCommand, err := buildGrokCommand(nodePath, paths.auditPath) auditCommand, err := buildEncodedCommand("Grok", nodePath, paths.auditPath)
if err != nil { if err != nil {
t.Fatalf("build audit command: %v", err) t.Fatalf("build audit command: %v", err)
} }
@@ -179,7 +179,7 @@ func TestGrokUninstallPreservesConfigAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed Grok config: %v", err) t.Fatalf("read installed Grok config: %v", err)
} }
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameGrokPath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Grok uninstall failure") 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 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) { func decodeJSONCObject(source []byte, label string, allowTrailingCommas bool) (map[string]any, error) {
value, err := parseJSONC(source, label, allowTrailingCommas) value, err := parseJSONC(source, label, allowTrailingCommas)
if err != nil { if err != nil {
@@ -283,3 +293,11 @@ func jsonPointer(path []any) string {
} }
return pointer.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 ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -49,6 +50,20 @@ func hookScriptPath(agentId string) (string, error) {
return filepath.Join(agentDirectory, "hook.js"), nil 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. // guardScriptPath returns ~/.elydora/<agentId>/guard.js.
func guardScriptPath(agentId string) (string, error) { func guardScriptPath(agentId string) (string, error) {
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId) agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
+6 -15
View File
@@ -34,11 +34,11 @@ func (p *KimiPlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
guardCommand, err := buildKimiCommand(nodePath, paths.guardPath) guardCommand, err := buildEncodedCommand("kimi", nodePath, paths.guardPath)
if err != nil { if err != nil {
return err return err
} }
auditCommand, err := buildKimiCommand(nodePath, paths.auditPath) auditCommand, err := buildEncodedCommand("kimi", nodePath, paths.auditPath)
if err != nil { if err != nil {
return err return err
} }
@@ -70,12 +70,9 @@ func (p *KimiPlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
if err := writeKimiChanges( if err := writeManagedChanges(
changes, changes, "Install Kimi hooks", p.rename,
"Install Kimi hooks", paths.runtimeRoot, paths.agentDirectory, "", "",
p.rename,
paths.runtimeRoot,
paths.agentDirectory,
); err != nil { ); err != nil {
return err return err
} }
@@ -107,13 +104,7 @@ func (p *KimiPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
return writeKimiChanges( return writeManagedChanges(changes, "Uninstall Kimi hooks", p.rename, "", "", "", "")
changes,
"Uninstall Kimi hooks",
p.rename,
"",
"",
)
} }
func (p *KimiPlugin) Status() (PluginStatus, error) { func (p *KimiPlugin) Status() (PluginStatus, error) {
+5 -64
View File
@@ -1,49 +1,10 @@
package plugins package plugins
import ( import (
"fmt"
"path/filepath" "path/filepath"
"runtime"
"strings" "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) { func readKimiLegacyWindowsArgument(command string, start int) (string, int, bool) {
if start >= len(command) { if start >= len(command) {
return "", start, false return "", start, false
@@ -66,6 +27,7 @@ func readKimiLegacyWindowsArgument(command string, start int) (string, int, bool
return command[start:end], end, end > start return command[start:end], end, end > start
} }
// parseKimiLegacyWindowsCommand reads the pre-2.1 quoteWindowsArgument form.
func parseKimiLegacyWindowsCommand(command string) (string, string, bool) { func parseKimiLegacyWindowsCommand(command string) (string, string, bool) {
runtimePath, next, ok := readKimiLegacyWindowsArgument(command, 0) runtimePath, next, ok := readKimiLegacyWindowsArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' { 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) { func parseKimiCommand(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 runtimePath, scriptPath, true
} }
return parseKimiLegacyWindowsCommand(command) return parseKimiLegacyWindowsCommand(command)
} }
func isKimiNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func kimiRuntimeReferenceForCommand( func kimiRuntimeReferenceForCommand(
command string, command string,
scriptName string, scriptName string,
) (*kimiRuntimeReference, error) { ) (*managedScriptReference, error) {
runtimePath, scriptPath, ok := parseKimiCommand(command) runtimePath, scriptPath, ok := parseKimiCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) || if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
!isKimiNodeExecutable(runtimePath) || filepath.Base(scriptPath) != scriptName {
return nil, nil return nil, nil
} }
agentDirectory := filepath.Dir(scriptPath) return resolveManagedScript(scriptPath, scriptName)
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
} }
+12 -28
View File
@@ -2,9 +2,7 @@ package plugins
import ( import (
"fmt" "fmt"
"runtime"
"sort" "sort"
"strings"
) )
const ( const (
@@ -54,20 +52,10 @@ type kimiHook struct {
} }
type kimiRuntimeContract struct { type kimiRuntimeContract struct {
agentID string managedRuntimeContract
guardPath string
auditPath string
configPath string 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{} { func mergeStringSets(sets ...map[string]struct{}) map[string]struct{} {
result := map[string]struct{}{} result := map[string]struct{}{}
for _, set := range sets { for _, set := range sets {
@@ -173,7 +161,7 @@ func managedKimiReference(
hook kimiHook, hook kimiHook,
event string, event string,
scriptName string, scriptName string,
) (*kimiRuntimeReference, error) { ) (*managedScriptReference, error) {
if hook.fieldCount != 3 || hook.event != event || hook.matcher != nil || if hook.fieldCount != 3 || hook.event != event || hook.matcher != nil ||
hook.timeout == nil || *hook.timeout != kimiHookTimeoutSeconds { hook.timeout == nil || *hook.timeout != kimiHookTimeoutSeconds {
return nil, nil return nil, nil
@@ -196,7 +184,7 @@ func keptKimiHookIndices(hooks []kimiHook, agentID string) ([]int, error) {
indices := make([]int, 0, len(hooks)) indices := make([]int, 0, len(hooks))
for index, hook := range hooks { for index, hook := range hooks {
event, scriptName, managedEvent := managedKimiEvent(hook.event) event, scriptName, managedEvent := managedKimiEvent(hook.event)
var reference *kimiRuntimeReference var reference *managedScriptReference
var err error var err error
if managedEvent { if managedEvent {
reference, err = managedKimiReference(hook, event, scriptName) reference, err = managedKimiReference(hook, event, scriptName)
@@ -205,7 +193,7 @@ func keptKimiHookIndices(hooks []kimiHook, agentID string) ([]int, error) {
} }
} }
remove := reference != nil && remove := reference != nil &&
(agentID == "" || sameKimiAgentID(reference.agentID, agentID)) (agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if !remove { if !remove {
indices = append(indices, index) indices = append(indices, index)
} }
@@ -213,19 +201,12 @@ func keptKimiHookIndices(hooks []kimiHook, agentID string) ([]int, error) {
return indices, nil return indices, nil
} }
func kimiReferenceKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func kimiReferencesForEvent( func kimiReferencesForEvent(
hooks []kimiHook, hooks []kimiHook,
event string, event string,
scriptName string, scriptName string,
) (map[string][]kimiRuntimeReference, error) { ) (map[string][]managedScriptReference, error) {
result := map[string][]kimiRuntimeReference{} result := map[string][]managedScriptReference{}
for _, hook := range hooks { for _, hook := range hooks {
reference, err := managedKimiReference(hook, event, scriptName) reference, err := managedKimiReference(hook, event, scriptName)
if err != nil { if err != nil {
@@ -234,7 +215,7 @@ func kimiReferencesForEvent(
if reference == nil { if reference == nil {
continue continue
} }
key := kimiReferenceKey(reference.agentID) key := managedReferenceKey(reference.agentID)
result[key] = append(result[key], *reference) result[key] = append(result[key], *reference)
} }
return result, nil return result, nil
@@ -267,12 +248,15 @@ func kimiRuntimeContracts(documents []kimiDocument) ([]kimiRuntimeContract, erro
success := successes[key] success := successes[key]
failure := failures[key] failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 || 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 continue
} }
contracts = append(contracts, kimiRuntimeContract{ contracts = append(contracts, kimiRuntimeContract{
managedRuntimeContract: managedRuntimeContract{
agentID: guard[0].agentID, guardPath: guard[0].scriptPath, agentID: guard[0].agentID, guardPath: guard[0].scriptPath,
auditPath: success[0].scriptPath, configPath: document.contract.configPath, auditPath: success[0].scriptPath,
},
configPath: document.contract.configPath,
}) })
} }
} }
+18 -164
View File
@@ -2,107 +2,32 @@ package plugins
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
) )
type kimiRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type kimiRenderedDocument struct { type kimiRenderedDocument struct {
document kimiDocument document kimiDocument
next []byte 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( func preflightKimiInstallation(
config InstallConfig, config InstallConfig,
documents []kimiDocument, documents []kimiDocument,
) (*kimiRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if len(documents) == 0 { if len(documents) == 0 {
return nil, "", fmt.Errorf("kimi installation requires at least one hook contract") 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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveNodeRuntime()
@@ -112,26 +37,9 @@ func preflightKimiInstallation(
return paths, nodePath, nil return paths, nodePath, nil
} }
func buildKimiRuntimeConfig(config InstallConfig) ([]byte, error) { func kimiExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
value := map[string]any{ return []byte(generateGuardScript(kimiAgentKey, agentID, "", false, "")),
"org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID, []byte(buildHookScriptWithOutput(kimiAgentKey, agentID, "", false, true))
"base_url": config.BaseURL, "agent_name": kimiAgentKey,
}
if config.Token != "" {
value["token"] = config.Token
}
encoded, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
}
encoded = append(encoded, '\n')
if len(encoded) > maxRuntimeConfigBytes {
return nil, fmt.Errorf(
"elydora runtime config exceeds %d bytes after JSON encoding",
maxRuntimeConfigBytes,
)
}
return encoded, nil
} }
func renderKimiChange( func renderKimiChange(
@@ -172,47 +80,21 @@ func prepareRenderedKimiChange(rendered kimiRenderedDocument) (*fileChange, erro
func prepareKimiInstallationChanges( func prepareKimiInstallationChanges(
config InstallConfig, config InstallConfig,
paths *kimiRuntimePaths, paths *managedRuntimePaths,
rendered []kimiRenderedDocument, rendered []kimiRenderedDocument,
) ([]*fileChange, error) { ) ([]*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 { if err != nil {
return nil, err return nil, err
} }
items := []struct { documentChanges, err := prepareKimiUninstallChanges(rendered)
path, label string
content []byte
mode os.FileMode
}{
{
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(kimiAgentKey, config.AgentID, "", false, "")), 0700,
},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600},
{
paths.auditPath, "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
kimiAgentKey, config.AgentID, "", false, true,
)), 0700,
},
}
changes := make([]*fileChange, 0, len(items)+len(rendered))
for _, item := range items {
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
if err != nil { if err != nil {
return nil, err return nil, err
} }
changes = append(changes, change) return append(changes, documentChanges...), nil
}
for _, document := range rendered {
change, err := prepareRenderedKimiChange(document)
if err != nil {
return nil, err
}
changes = append(changes, change)
}
return changes, nil
} }
func prepareKimiUninstallChanges( func prepareKimiUninstallChanges(
@@ -228,31 +110,3 @@ func prepareKimiUninstallChanges(
} }
return changes, nil 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") legacyHome := filepath.Join(home, ".kimi")
stable := stableKimiContract(filepath.Join(stableHome, "config.toml")) stable := stableKimiContract(filepath.Join(stableHome, "config.toml"))
legacy := legacyKimiContract(filepath.Join(legacyHome, "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 return []kimiContract{stable}, nil
} }
+5 -187
View File
@@ -1,194 +1,12 @@
package plugins 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) { func kimiRuntimeFilesExist(contracts []kimiRuntimeContract) (bool, error) {
for _, contract := range contracts { for _, contract := range contracts {
exists, err := kimiRuntimeContractExists(contract) exists, err := managedRuntimeContractExists(
if err != nil { contract.managedRuntimeContract, kimiAgentKey, "Kimi", nil,
return false, err )
} if err != nil || exists {
if exists { return exists, err
return true, nil
} }
} }
return false, nil return false, nil
+6 -9
View File
@@ -38,7 +38,7 @@ func TestKimiInstallRollsBackRuntimeAndBothConfigs(t *testing.T) {
modernConfig: kimiString(string(stable)), legacyConfig: kimiString(string(legacy)), modernConfig: kimiString(string(stable)), legacyConfig: kimiString(string(legacy)),
}) })
fixture.plugin.rename = func(source, destination string) error { 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 errors.New("injected legacy config failure")
} }
return os.Rename(source, destination) return os.Rename(source, destination)
@@ -160,11 +160,11 @@ func TestPreparedKimiInstallDetectsSourceChangeBeforeCommit(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("preflight Kimi installation: %v", err) t.Fatalf("preflight Kimi installation: %v", err)
} }
guardCommand, err := buildKimiCommand(nodePath, paths.guardPath) guardCommand, err := buildEncodedCommand("kimi", nodePath, paths.guardPath)
if err != nil { if err != nil {
t.Fatalf("build guard command: %v", err) t.Fatalf("build guard command: %v", err)
} }
auditCommand, err := buildKimiCommand(nodePath, paths.auditPath) auditCommand, err := buildEncodedCommand("kimi", nodePath, paths.auditPath)
if err != nil { if err != nil {
t.Fatalf("build audit command: %v", err) t.Fatalf("build audit command: %v", err)
} }
@@ -204,12 +204,9 @@ func TestPreparedKimiInstallDetectsSourceChangeBeforeCommit(t *testing.T) {
t.Fatalf("write concurrent source: %v", err) t.Fatalf("write concurrent source: %v", err)
} }
err = writeKimiChanges( err = writeManagedChanges(
changes, changes, "Install Kimi hooks", nil,
"Install Kimi hooks", paths.runtimeRoot, paths.agentDirectory, "", "",
nil,
paths.runtimeRoot,
paths.agentDirectory,
) )
if err == nil || !strings.Contains(err.Error(), "changed during installation") { if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err) 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) copyKiroObject(nextV2, v2Settings)
nextV2["hooks"] = v2Hooks nextV2["hooks"] = v2Hooks
nextV3 := cloneKiroObject(v3Settings) nextV3 := cloneJSONObject(v3Settings)
nextV3["version"] = "v1" nextV3["version"] = "v1"
v3Hooks := make([]any, 0, len(currentV3Hooks)+2) v3Hooks := make([]any, 0, len(currentV3Hooks)+2)
for _, hook := range currentV3Hooks { for _, hook := range currentV3Hooks {
+3 -9
View File
@@ -28,12 +28,6 @@ type kiroConfigMutation struct {
remove bool 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) { func copyKiroObject(destination, source map[string]any) {
for key, value := range source { for key, value := range source {
destination[key] = value destination[key] = value
@@ -49,7 +43,7 @@ func kiroHooksObject(settings map[string]any, label string) (map[string]any, err
if !ok || hooks == nil { if !ok || hooks == nil {
return nil, fmt.Errorf(`%s field "hooks" must be an object`, label) 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) { func kiroHookEntries(hooks map[string]any, event, label string) ([]map[string]any, error) {
@@ -193,7 +187,7 @@ func prepareKiroV2Uninstall(
} }
hooks["preToolUse"] = filteredPre hooks["preToolUse"] = filteredPre
hooks["postToolUse"] = filteredPost hooks["postToolUse"] = filteredPost
next := cloneKiroObject(settings) next := cloneJSONObject(settings)
next["hooks"] = hooks next["hooks"] = hooks
mutation.value = next mutation.value = next
mutation.changed = true mutation.changed = true
@@ -250,7 +244,7 @@ func prepareKiroV3Uninstall(
if len(filtered) == len(hooks) { if len(filtered) == len(hooks) {
return mutation, nil return mutation, nil
} }
next := cloneKiroObject(settings) next := cloneJSONObject(settings)
next["hooks"] = filtered next["hooks"] = filtered
owned := true owned := true
for key := range settings { for key := range settings {
-17
View File
@@ -3,7 +3,6 @@ package plugins
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@@ -18,18 +17,6 @@ func kiroConfigPaths() (string, string, error) {
filepath.Join(home, ".kiro", "hooks", "elydora-audit.json"), nil 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 { func buildKiroCommand(runtimePath, scriptPath string) string {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return quoteWindowsArgument(runtimePath) + " " + quoteWindowsArgument(scriptPath) return quoteWindowsArgument(runtimePath) + " " + quoteWindowsArgument(scriptPath)
@@ -37,10 +24,6 @@ func buildKiroCommand(runtimePath, scriptPath string) string {
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath) return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath)
} }
func quotePOSIXArgument(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}
func quoteWindowsArgument(value string) string { func quoteWindowsArgument(value string) string {
if value != "" && !strings.ContainsAny(value, " \t\n\v\"") { if value != "" && !strings.ContainsAny(value, " \t\n\v\"") {
return value return value
+9 -94
View File
@@ -1,101 +1,32 @@
package plugins package plugins
import ( import (
"fmt"
"path/filepath" "path/filepath"
"runtime"
"strings" "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( func kiroIdeRuntimeReferenceForCommand(
command string, command string,
scriptName string, scriptName string,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
runtimePath, scriptPath, ok := parseKiroIdeCommand(command) runtimePath, scriptPath, ok := parseEncodedCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) || if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
!isCodexNodeExecutable(runtimePath) ||
!sameKiroIdeFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil return nil, nil
} }
agentDirectory := filepath.Dir(scriptPath) return resolveManagedScript(scriptPath, scriptName)
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
} }
func currentKiroIdeReference( func currentKiroIdeReference(
hook map[string]any, hook map[string]any,
nodePath string, nodePath string,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
reference, err := managedKiroIdeReference(hook) reference, err := managedKiroIdeReference(hook)
if err != nil || reference == nil { if err != nil || reference == nil {
return reference, err return reference, err
} }
action := hook["action"].(map[string]any) action := hook["action"].(map[string]any)
command := action["command"].(string) command := action["command"].(string)
expected, err := buildKiroIdeCommand(nodePath, reference.scriptPath) expected, err := buildEncodedCommand("Kiro IDE", nodePath, reference.scriptPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -105,30 +36,14 @@ func currentKiroIdeReference(
return reference, nil return reference, nil
} }
// legacyKiroIdeGoReference reads the pre-2.1 `node <script>` form.
func legacyKiroIdeGoReference( func legacyKiroIdeGoReference(
command any, command any,
scriptName string, scriptName string,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
text, ok := command.(string) text, ok := command.(string)
if !ok || !strings.HasPrefix(text, "node ") { if !ok || !strings.HasPrefix(text, "node ") {
return nil, nil return nil, nil
} }
scriptPath := strings.TrimPrefix(text, "node ") return resolveManagedScript(strings.TrimPrefix(text, "node "), scriptName)
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
} }
+17 -31
View File
@@ -45,7 +45,7 @@ type kiroIdeDocument struct {
type kiroIdeLegacyDocument struct { type kiroIdeLegacyDocument struct {
filePath string filePath string
snapshot *managedFileSnapshot snapshot *managedFileSnapshot
contract *kiroIdeRuntimeContract contract *managedRuntimeContract
} }
type kiroIdeRenderedDocument struct { type kiroIdeRenderedDocument struct {
@@ -55,12 +55,6 @@ type kiroIdeRenderedDocument struct {
remove bool remove bool
} }
type kiroIdeRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type kiroIdeHookSpecification struct { type kiroIdeHookSpecification struct {
description string description string
trigger 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 { func validateKiroIdeAction(value any, label string) error {
action, ok := value.(map[string]any) action, ok := value.(map[string]any)
if !ok || action == nil { if !ok || action == nil {
@@ -212,7 +198,7 @@ func exactKiroIdeKeys(value map[string]any, keys ...string) bool {
func ownedKiroIdeReference( func ownedKiroIdeReference(
hook map[string]any, hook map[string]any,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
specification, managed := kiroIdeSpecification(hook["name"]) specification, managed := kiroIdeSpecification(hook["name"])
if !managed { if !managed {
return nil, nil return nil, nil
@@ -230,7 +216,7 @@ func ownedKiroIdeReference(
func managedKiroIdeReference( func managedKiroIdeReference(
hook map[string]any, hook map[string]any,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
specification, managed := kiroIdeSpecification(hook["name"]) specification, managed := kiroIdeSpecification(hook["name"])
if !managed || if !managed ||
!exactKiroIdeKeys( !exactKiroIdeKeys(
@@ -278,7 +264,7 @@ func buildKiroIdeHook(
if !managed { if !managed {
return nil, fmt.Errorf("unsupported Elydora Kiro IDE hook name %q", name) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -298,7 +284,7 @@ func withoutManagedKiroIdeHooks(
for _, hook := range hooks { for _, hook := range hooks {
reference, err := ownedKiroIdeReference(hook) reference, err := ownedKiroIdeReference(hook)
remove := err == nil && reference != nil && remove := err == nil && reference != nil &&
(agentID == "" || sameKiroIdeAgentID(reference.agentID, agentID)) (agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if !remove { if !remove {
filtered = append(filtered, hook) filtered = append(filtered, hook)
} }
@@ -339,7 +325,7 @@ func renderKiroIdeDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneKiroIdeObject(document.root) root := cloneJSONObject(document.root)
root["version"] = "v1" root["version"] = "v1"
values := make([]any, 0, len(hooks)) values := make([]any, 0, len(hooks))
for _, hook := range hooks { for _, hook := range hooks {
@@ -364,7 +350,7 @@ func renderKiroIdeDocument(
func kiroIdeRuntimeContracts( func kiroIdeRuntimeContracts(
hooks []map[string]any, hooks []map[string]any,
) ([]kiroIdeRuntimeContract, error) { ) ([]managedRuntimeContract, error) {
managedHooks := make([]map[string]any, 0, 2) managedHooks := make([]map[string]any, 0, 2)
for _, hook := range hooks { for _, hook := range hooks {
_, managed := kiroIdeSpecification(hook["name"]) _, managed := kiroIdeSpecification(hook["name"])
@@ -373,38 +359,38 @@ func kiroIdeRuntimeContracts(
} }
} }
if len(managedHooks) != 2 { if len(managedHooks) != 2 {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveNodeRuntime()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var guard, audit *kiroIdeRuntimeReference var guard, audit *managedScriptReference
for _, hook := range managedHooks { for _, hook := range managedHooks {
reference, err := currentKiroIdeReference(hook, nodePath) reference, err := currentKiroIdeReference(hook, nodePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if reference == nil { if reference == nil {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
if hook["name"] == kiroIdeGuardName { if hook["name"] == kiroIdeGuardName {
if guard != nil { if guard != nil {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
guard = reference guard = reference
} else { } else {
if audit != nil { if audit != nil {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
audit = reference audit = reference
} }
} }
if guard == nil || audit == nil || if guard == nil || audit == nil ||
!sameKiroIdeAgentID(guard.agentID, audit.agentID) { !sameManagedAgentID(guard.agentID, audit.agentID) {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
return []kiroIdeRuntimeContract{{ return []managedRuntimeContract{{
agentID: guard.agentID, guardPath: guard.scriptPath, agentID: guard.agentID, guardPath: guard.scriptPath,
auditPath: audit.scriptPath, auditPath: audit.scriptPath,
}}, nil }}, nil
@@ -449,10 +435,10 @@ func parseKiroIdeLegacyDocument(
return nil, err return nil, err
} }
if guardReference == nil || auditReference == nil || if guardReference == nil || auditReference == nil ||
!sameKiroIdeAgentID(guardReference.agentID, auditReference.agentID) { !sameManagedAgentID(guardReference.agentID, auditReference.agentID) {
return document, nil return document, nil
} }
document.contract = &kiroIdeRuntimeContract{ document.contract = &managedRuntimeContract{
agentID: guardReference.agentID, guardPath: guardReference.scriptPath, agentID: guardReference.agentID, guardPath: guardReference.scriptPath,
auditPath: auditReference.scriptPath, auditPath: auditReference.scriptPath,
} }
+15 -122
View File
@@ -1,20 +1,13 @@
package plugins package plugins
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
type kiroIdeRuntimePaths struct { type kiroIdeRuntimePaths struct {
runtimeRoot string managedRuntimePaths
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
initialDirectories []kiroIdeDirectoryIdentity initialDirectories []kiroIdeDirectoryIdentity
} }
@@ -28,84 +21,17 @@ type kiroIdePreparedTransaction struct {
initialDirectories []kiroIdeDirectoryIdentity 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( func preflightKiroIdeInstallation(
config InstallConfig, config InstallConfig,
) (*kiroIdeRuntimePaths, string, error) { ) (*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 { if err != nil {
return nil, "", err return nil, "", err
} }
paths := &kiroIdeRuntimePaths{managedRuntimePaths: *managed}
initialDirectories := make([]kiroIdeDirectoryIdentity, 0, 3) initialDirectories := make([]kiroIdeDirectoryIdentity, 0, 3)
for _, item := range []struct{ path, label string }{ for _, item := range []struct{ path, label string }{
{filepath.Dir(paths.runtimeRoot), "home directory"}, {filepath.Dir(paths.runtimeRoot), "home directory"},
@@ -124,9 +50,8 @@ func preflightKiroIdeInstallation(
initialDirectories[0].path, initialDirectories[0].path,
) )
} }
if err := validateKiroIdeRuntimeIdentity( if err := validateManagedRuntimeIdentity(
paths.agentDirectory, paths.agentDirectory, config.AgentID, kiroIdeAgentKey, "Kiro IDE",
config.AgentID,
); err != nil { ); err != nil {
return nil, "", err return nil, "", err
} }
@@ -144,29 +69,6 @@ func preflightKiroIdeInstallation(
return paths, nodePath, nil 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( func prepareKiroIdeFile(
filePath string, filePath string,
label string, label string,
@@ -221,10 +123,11 @@ func prepareKiroIdeInstallation(
if sources == nil || paths == nil || rendered == nil { if sources == nil || paths == nil || rendered == nil {
return nil, fmt.Errorf("Kiro IDE installation requires prepared sources") return nil, fmt.Errorf("Kiro IDE installation requires prepared sources")
} }
runtimeConfig, err := buildKiroIdeRuntimeConfig(config) runtimeConfig, err := buildManagedRuntimeConfig(config, kiroIdeAgentKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
guardScript, auditScript := kiroIdeExpectedScripts(config.AgentID, nil)
prepared := &kiroIdePreparedTransaction{ prepared := &kiroIdePreparedTransaction{
operation: "installation", paths: sources.paths, runtimePaths: paths, operation: "installation", paths: sources.paths, runtimePaths: paths,
initialDirectories: append( initialDirectories: append(
@@ -238,20 +141,10 @@ func prepareKiroIdeInstallation(
mode os.FileMode mode os.FileMode
maximum int64 maximum int64
}{ }{
{ {paths.guardPath, "Elydora guard runtime", guardScript, 0700, maxManagedSourceBytes},
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(kiroIdeAgentKey, config.AgentID, "", false, "")),
0700, maxManagedSourceBytes,
},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600, maxRuntimeConfigBytes}, {paths.configPath, "Elydora runtime config", runtimeConfig, 0600, maxRuntimeConfigBytes},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600, maxProtectedSecretBytes}, {paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600, maxProtectedSecretBytes},
{ {paths.auditPath, "Elydora audit runtime", auditScript, 0700, maxManagedSourceBytes},
paths.auditPath, "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
kiroIdeAgentKey, config.AgentID, "", false, true,
)),
0700, maxManagedSourceBytes,
},
} { } {
change, condition, err := prepareKiroIdeFile( change, condition, err := prepareKiroIdeFile(
item.path, item.label, item.contents, item.mode, item.maximum, item.path, item.label, item.contents, item.mode, item.maximum,
@@ -274,7 +167,7 @@ func prepareKiroIdeInstallation(
prepared.changes = append(prepared.changes, documentChange) prepared.changes = append(prepared.changes, documentChange)
} }
removeLegacy := sources.legacy.contract != nil && removeLegacy := sources.legacy.contract != nil &&
sameKiroIdeAgentID(sources.legacy.contract.agentID, config.AgentID) sameManagedAgentID(sources.legacy.contract.agentID, config.AgentID)
if removeLegacy { if removeLegacy {
legacyChange, err := prepareSnapshotSourceChange( legacyChange, err := prepareSnapshotSourceChange(
sources.legacy.filePath, sources.legacy.filePath,
@@ -335,7 +228,7 @@ func prepareKiroIdeUninstall(
prepared.changes = append(prepared.changes, documentChange) prepared.changes = append(prepared.changes, documentChange)
} }
removeLegacy := sources.legacy.contract != nil && removeLegacy := sources.legacy.contract != nil &&
(agentID == "" || sameKiroIdeAgentID(sources.legacy.contract.agentID, agentID)) (agentID == "" || sameManagedAgentID(sources.legacy.contract.agentID, agentID))
if removeLegacy { if removeLegacy {
change, err := prepareSnapshotSourceChange( change, err := prepareSnapshotSourceChange(
sources.legacy.filePath, sources.legacy.filePath,
@@ -383,7 +276,7 @@ func commitKiroIdeTransaction(
} }
workspaceChange := false workspaceChange := false
for _, change := range prepared.changes { 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 workspaceChange = true
break break
} }
+31 -247
View File
@@ -1,157 +1,13 @@
package plugins package plugins
import ( import (
"bytes"
"fmt"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sort"
"strings"
) )
func readKiroIdeRuntimeConfig(path string) (map[string]any, bool, error) { func kiroIdeExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes) return []byte(generateGuardScript(kiroIdeAgentKey, agentID, "", false, "")),
if err != nil || snapshot == nil { []byte(buildHookScriptWithOutput(kiroIdeAgentKey, agentID, "", false, true))
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 kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool { func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool {
@@ -161,116 +17,44 @@ func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool
return uint32(snapshot.mode.Perm()) == expected return uint32(snapshot.mode.Perm()) == expected
} }
func kiroIdeRuntimeContractExists(contract kiroIdeRuntimeContract) (bool, error) { // kiroIdeRuntimeModesMatch requires the private modes the installer writes.
runtimeRoot, err := AgentRuntimeRoot() func kiroIdeRuntimeModesMatch(contract managedRuntimeContract) (bool, error) {
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath) agentDirectory := filepath.Dir(contract.guardPath)
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) || for _, item := range []struct {
!sameKiroIdeAgentID(filepath.Base(agentDirectory), contract.agentID) || path, label string
!sameKiroIdePath( limit int64
contract.guardPath, mode uint32
filepath.Join(agentDirectory, kiroIdeGuardScript), }{
) || {filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", maxRuntimeConfigBytes, 0600},
!sameKiroIdePath( {filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes, 0600},
contract.auditPath, {contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes, 0700},
filepath.Join(agentDirectory, kiroIdeAuditScript), {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 return false, nil
} }
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return false, err
} }
directoryExists, err := managedPhysicalDirectoryExists( return true, nil
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
} }
func kiroIdeRuntimeFilesExist(contracts []kiroIdeRuntimeContract) (bool, error) { func kiroIdeRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
for _, contract := range contracts { for _, contract := range contracts {
exists, err := kiroIdeRuntimeContractExists(contract) exists, err := managedRuntimeContractExists(
contract, kiroIdeAgentKey, "Kiro IDE", kiroIdeExpectedScripts,
)
if err != nil { if err != nil {
return false, err return false, err
} }
if exists { if !exists {
return true, nil continue
}
modes, err := kiroIdeRuntimeModesMatch(contract)
if err != nil || modes {
return modes, err
} }
} }
return false, nil return false, nil
+3 -3
View File
@@ -164,7 +164,7 @@ func TestKiroIdeStatusRequiresUniqueExactPairAndRuntimeIntegrity(t *testing.T) {
document = readKiroIdeObject(t, fixture.configPath) document = readKiroIdeObject(t, fixture.configPath)
document["hooks"] = append( document["hooks"] = append(
requireArray(t, document["hooks"]), requireArray(t, document["hooks"]),
cloneKiroIdeObject(findKiroIdeHook(t, document, kiroIdeGuardName)), cloneJSONObject(findKiroIdeHook(t, document, kiroIdeGuardName)),
) )
writeKiroIdeJSON(t, fixture.configPath, document) writeKiroIdeJSON(t, fixture.configPath, document)
assertStatus(false, false) assertStatus(false, false)
@@ -283,7 +283,7 @@ func TestKiroIdeStatusRejectsExtraOrphanAndStaleRuntimeCommands(t *testing.T) {
name = "node.exe" name = "node.exe"
} }
staleNode := filepath.Join(fixture.homeDir, "old-node", name) 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 { if err != nil {
t.Fatalf("build stale Kiro IDE command: %v", err) t.Fatalf("build stale Kiro IDE command: %v", err)
} }
@@ -331,7 +331,7 @@ func TestKiroIdeStatusRejectsStaleWindowsPowerShellLauncher(t *testing.T) {
command := action["command"].(string) command := action["command"].(string)
action["command"] = strings.Replace( action["command"] = strings.Replace(
command, command,
codexPowerShellPath(), windowsPowerShellPath(),
`C:\StaleWindows\System32\WindowsPowerShell\v1.0\powershell.exe`, `C:\StaleWindows\System32\WindowsPowerShell\v1.0\powershell.exe`,
1, 1,
) )
@@ -20,7 +20,7 @@ func TestKiroIdeRollbackRestoresRuntimeWorkspaceAndLegacySources(t *testing.T) {
legacyKiroIdeGoDocument(fixture, kiroIdeTestAgentID), legacyKiroIdeGoDocument(fixture, kiroIdeTestAgentID),
) )
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameKiroIdePath(source, fixture.legacyPath) && if sameManagedPath(source, fixture.legacyPath) &&
strings.HasSuffix(destination, ".rollback") { strings.HasSuffix(destination, ".rollback") {
return errors.New("injected Kiro IDE legacy cleanup failure") return errors.New("injected Kiro IDE legacy cleanup failure")
} }
@@ -320,7 +320,7 @@ func TestKiroIdeInstallSurvivesAdversarialWorkspaceLinkSwap(t *testing.T) {
} }
swapped := false swapped := false
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if !swapped && sameKiroIdePath(destination, fixture.configPath) && if !swapped && sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
swapped = true swapped = true
backup := kiroDirectory + ".original" backup := kiroDirectory + ".original"
@@ -42,6 +42,18 @@ func extractPathFromNodeCommand(command string) string {
return "" 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 { func hasElydoraEntry(hookArray any) bool {
entries, _ := hookArray.([]any) entries, _ := hookArray.([]any)
for _, entry := range entries { for _, entry := range entries {
+34 -120
View File
@@ -6,11 +6,10 @@ import (
"path/filepath" "path/filepath"
) )
// LettaPlugin manages the Elydora audit hook for Letta Code. // LettaPlugin merges Elydora hooks into ~/.letta/settings.json.
// It merges PreToolUse/PostToolUse hooks into ~/.letta/settings.json.
type LettaPlugin struct{} type LettaPlugin struct{}
func (p *LettaPlugin) configPath() (string, error) { func lettaConfigPath() (string, error) {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err) 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 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 { func (p *LettaPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath(config.AgentID) scriptPath, err := hookScriptPath(config.AgentID)
if err != nil { if err != nil {
@@ -26,11 +35,9 @@ func (p *LettaPlugin) Install(config InstallConfig) error {
if config.HookScript != "" { if config.HookScript != "" {
scriptPath = config.HookScript scriptPath = config.HookScript
} }
if err := GenerateHookScript(scriptPath, config); err != nil { if err := GenerateHookScript(scriptPath, config); err != nil {
return fmt.Errorf("generate hook script: %w", err) return fmt.Errorf("generate hook script: %w", err)
} }
guardPath := config.GuardScriptPath guardPath := config.GuardScriptPath
if guardPath == "" { if guardPath == "" {
guardPath, err = guardScriptPath(config.AgentID) guardPath, err = guardScriptPath(config.AgentID)
@@ -38,70 +45,21 @@ func (p *LettaPlugin) Install(config InstallConfig) error {
return err return err
} }
} }
configPath, err := lettaConfigPath()
configPath, err := p.configPath()
if err != nil { if err != nil {
return err return err
} }
settings, err := readJSONFile(configPath) settings, err := readJSONFile(configPath)
if err != nil { if err != nil {
return err return err
} }
hooks, _ := settings["hooks"].(map[string]any)
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil { if hooks == nil {
hooks = make(map[string]interface{}) hooks = map[string]any{}
} }
hooks["PreToolUse"] = append(withoutElydoraEntries(hooks["PreToolUse"]), lettaHookEntry(guardPath))
// --- PreToolUse (guard — freeze enforcement) --- hooks["PostToolUse"] = append(withoutElydoraEntries(hooks["PostToolUse"]), lettaHookEntry(scriptPath))
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
settings["hooks"] = hooks settings["hooks"] = hooks
if err := writeJSONFile(configPath, settings); err != nil { if err := writeJSONFile(configPath, settings); err != nil {
return err return err
} }
@@ -110,74 +68,38 @@ func (p *LettaPlugin) Install(config InstallConfig) error {
} }
func (p *LettaPlugin) Uninstall(agentID string) error { func (p *LettaPlugin) Uninstall(agentID string) error {
configPath, err := p.configPath() configPath, err := lettaConfigPath()
if err != nil { if err != nil {
return err return err
} }
settings, err := readJSONFile(configPath) settings, err := readJSONFile(configPath)
if err != nil { if err != nil {
return err return err
} }
hooks, _ := settings["hooks"].(map[string]any)
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil { if hooks == nil {
fmt.Println("No Letta Code hooks found.") fmt.Println("No Letta Code hooks found.")
return nil return nil
} }
for _, event := range []string{"PreToolUse", "PostToolUse"} {
// Remove PreToolUse Elydora entries kept := withoutElydoraEntries(hooks[event])
preToolUse, _ := hooks["PreToolUse"].([]interface{}) if len(kept) == 0 {
var preFiltered []interface{} delete(hooks, event)
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
preFiltered = append(preFiltered, entry)
}
if len(preFiltered) == 0 {
delete(hooks, "PreToolUse")
} else { } else {
hooks["PreToolUse"] = preFiltered hooks[event] = kept
}
// 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 { if len(hooks) == 0 {
delete(settings, "hooks") delete(settings, "hooks")
} else { } else {
settings["hooks"] = hooks settings["hooks"] = hooks
} }
if err := writeJSONFile(configPath, settings); err != nil { if err := writeJSONFile(configPath, settings); err != nil {
return err return err
} }
if agentID != "" { if agentID != "" {
scriptPath, _ := hookScriptPath(agentID) if err := removeAgentScripts(agentID); err != nil {
if scriptPath != "" { return err
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
} }
} }
fmt.Println("Uninstalled Elydora hook for Letta Code.") 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) { func (p *LettaPlugin) Status() (PluginStatus, error) {
configPath, err := p.configPath() configPath, err := lettaConfigPath()
if err != nil { if err != nil {
return PluginStatus{}, err return PluginStatus{}, err
} }
status := PluginStatus{ status := PluginStatus{
AgentName: "letta", AgentName: "letta",
DisplayName: "Letta Code", DisplayName: "Letta Code",
ConfigPath: configPath, ConfigPath: configPath,
} }
settings, err := readJSONFile(configPath) settings, err := readJSONFile(configPath)
if err != nil { if err != nil {
return status, err
}
hooks, _ := settings["hooks"].(map[string]any)
if hooks == nil {
return status, nil return status, nil
} }
status.HookConfigured = hasElydoraEntry(hooks["PreToolUse"]) &&
hooks, _ := settings["hooks"].(map[string]interface{}) hasElydoraEntry(hooks["PostToolUse"])
if hooks != nil { if scriptPath := extractElydoraScriptPath(hooks["PostToolUse"]); scriptPath != "" {
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 { if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true status.HookScriptExists = true
} }
} }
}
status.Installed = status.HookConfigured && status.HookScriptExists status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil 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
}
+52 -40
View File
@@ -1,14 +1,17 @@
package plugins package plugins
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "regexp"
"strconv"
) )
// OpenCodePlugin manages the Elydora audit hook for OpenCode. var openCodeScriptPathPattern = regexp.MustCompile(`(?m)^const (GUARD_SCRIPT_PATH|HOOK_SCRIPT_PATH) = (".*");$`)
// It writes .config/opencode/plugins/elydora-audit.mjs in the home directory.
// OpenCodePlugin writes ~/.config/opencode/plugins/elydora-audit.mjs.
type OpenCodePlugin struct{} type OpenCodePlugin struct{}
func (p *OpenCodePlugin) configPath() (string, error) { func (p *OpenCodePlugin) configPath() (string, error) {
@@ -59,8 +62,7 @@ func (p *OpenCodePlugin) Install(config InstallConfig) error {
} }
func buildOpenCodePlugin(scriptPath, guardPath string) string { func buildOpenCodePlugin(scriptPath, guardPath string) string {
return fmt.Sprintf(`// Elydora Audit Plugin for OpenCode return fmt.Sprintf(`// Elydora audit plugin for OpenCode. Generated by the Elydora CLI; do not edit.
// Generated by @elydora/sdk — DO NOT EDIT
import { spawn, spawnSync } from 'node:child_process'; import { spawn, spawnSync } from 'node:child_process';
@@ -112,62 +114,72 @@ func (p *OpenCodePlugin) Uninstall(agentID string) error {
} }
if agentID != "" { if agentID != "" {
scriptPath, _ := hookScriptPath(agentID) if err := removeAgentScripts(agentID); err != nil {
if scriptPath != "" { return err
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
} }
} }
fmt.Println("Uninstalled Elydora hook for OpenCode.") fmt.Println("Uninstalled Elydora hook for OpenCode.")
return nil 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) { func (p *OpenCodePlugin) Status() (PluginStatus, error) {
pluginFile, err := p.configPath() pluginFile, err := p.configPath()
if err != nil { if err != nil {
return PluginStatus{}, err return PluginStatus{}, err
} }
status := PluginStatus{ status := PluginStatus{
AgentName: "opencode", AgentName: "opencode",
DisplayName: "OpenCode", DisplayName: "OpenCode",
ConfigPath: pluginFile, ConfigPath: pluginFile,
} }
data, err := os.ReadFile(pluginFile)
if _, err := os.Stat(pluginFile); err == nil { if errors.Is(err, os.ErrNotExist) {
return status, nil
}
if err != nil {
return status, fmt.Errorf("read %s: %w", pluginFile, err)
}
status.HookConfigured = true status.HookConfigured = true
guardPath, hookPath, err := openCodeScriptPaths(data, pluginFile)
// Read the plugin file to extract the hook script path if err != nil {
data, readErr := os.ReadFile(pluginFile) return status, err
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
} }
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
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil return status, nil
} }
+2 -2
View File
@@ -73,7 +73,7 @@ func (p *QwenPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
runtimeRoot, err := qwenRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
return err return err
} }
@@ -104,7 +104,7 @@ func (p *QwenPlugin) Status() (PluginStatus, error) {
return status, err return status, err
} }
status.ConfigPath = sources.user.filePath status.ConfigPath = sources.user.filePath
runtimeRoot, err := qwenRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
return status, err 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" "path/filepath"
"runtime" "runtime"
"sort" "sort"
"strings"
) )
const ( const (
@@ -40,12 +39,6 @@ type qwenManagedRemoval struct {
removeGroup bool removeGroup bool
} }
type qwenRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
func qwenExpectedShell() string { func qwenExpectedShell() string {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return "powershell" return "powershell"
@@ -58,41 +51,52 @@ func buildQwenGroup(nodePath, scriptPath, name string) map[string]any {
"hooks": []any{map[string]any{ "hooks": []any{map[string]any{
"type": "command", "type": "command",
"name": name, "name": name,
"command": buildQwenCommand(nodePath, scriptPath), "command": buildShellCommand(nodePath, scriptPath),
"shell": qwenExpectedShell(), "shell": qwenExpectedShell(),
"timeout": qwenHookTimeout, "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( func currentQwenReference(
handler map[string]any, handler map[string]any,
scriptName, hookName, runtimeRoot string, scriptName, hookName, runtimeRoot string,
) (qwenRuntimeReference, bool) { ) *managedScriptReference {
if len(handler) != 5 || handler["type"] != "command" || if len(handler) != 5 || handler["type"] != "command" ||
handler["name"] != hookName || handler["shell"] != qwenExpectedShell() || handler["name"] != hookName || handler["shell"] != qwenExpectedShell() ||
handler["timeout"] != qwenHookTimeout { handler["timeout"] != qwenHookTimeout {
return qwenRuntimeReference{}, false return nil
} }
command, ok := handler["command"].(string) command, ok := handler["command"].(string)
if !ok { if !ok {
return qwenRuntimeReference{}, false return nil
} }
return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot) return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot)
} }
// legacyQwenReference reads the pre-2.1 unnamed handler.
func legacyQwenReference( func legacyQwenReference(
handler map[string]any, handler map[string]any,
scriptName, runtimeRoot string, scriptName, runtimeRoot string,
) (qwenRuntimeReference, bool) { ) *managedScriptReference {
if len(handler) != 4 || handler["type"] != "command" || if len(handler) != 4 || handler["type"] != "command" ||
handler["shell"] != qwenExpectedShell() || handler["shell"] != qwenExpectedShell() ||
handler["timeout"] != qwenHookTimeout { handler["timeout"] != qwenHookTimeout {
return qwenRuntimeReference{}, false return nil
} }
command, ok := handler["command"].(string) command, ok := handler["command"].(string)
if !ok { if !ok {
return qwenRuntimeReference{}, false return nil
} }
return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot) return qwenRuntimeReferenceForCommand(command, scriptName, runtimeRoot)
} }
@@ -101,19 +105,19 @@ func managedQwenReference(
handler map[string]any, handler map[string]any,
scriptName, hookName, runtimeRoot string, scriptName, hookName, runtimeRoot string,
includeLegacy bool, includeLegacy bool,
) (qwenRuntimeReference, bool) { ) *managedScriptReference {
if reference, ok := currentQwenReference( if reference := currentQwenReference(
handler, handler,
scriptName, scriptName,
hookName, hookName,
runtimeRoot, runtimeRoot,
); ok { ); reference != nil {
return reference, true return reference
} }
if includeLegacy { if includeLegacy {
return legacyQwenReference(handler, scriptName, runtimeRoot) return legacyQwenReference(handler, scriptName, runtimeRoot)
} }
return qwenRuntimeReference{}, false return nil
} }
func exactCurrentQwenGroup(group map[string]any) bool { func exactCurrentQwenGroup(group map[string]any) bool {
@@ -138,14 +142,15 @@ func managedQwenRemovals(
handlers := group["hooks"].([]any) handlers := group["hooks"].([]any)
indexes := make([]int, 0) indexes := make([]int, 0)
for handlerIndex, handlerValue := range handlers { for handlerIndex, handlerValue := range handlers {
reference, managed := managedQwenReference( reference := managedQwenReference(
handlerValue.(map[string]any), handlerValue.(map[string]any),
contract.script, contract.script,
contract.name, contract.name,
runtimeRoot, runtimeRoot,
true, true,
) )
if managed && (agentID == "" || sameQwenAgentID(reference.agentID, agentID)) { if reference != nil &&
(agentID == "" || sameManagedAgentID(reference.agentID, agentID)) {
indexes = append(indexes, handlerIndex) indexes = append(indexes, handlerIndex)
} }
} }
@@ -163,18 +168,11 @@ func managedQwenRemovals(
return removals return removals
} }
func normalizedQwenAgentID(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func qwenReferencesForEvent( func qwenReferencesForEvent(
settings qwenHookSettings, settings qwenHookSettings,
event, scriptName, hookName, runtimeRoot string, event, scriptName, hookName, runtimeRoot string,
) map[string][]qwenRuntimeReference { ) map[string][]managedScriptReference {
references := map[string][]qwenRuntimeReference{} references := map[string][]managedScriptReference{}
groups, _ := settings[event].([]any) groups, _ := settings[event].([]any)
for _, groupValue := range groups { for _, groupValue := range groups {
group := groupValue.(map[string]any) group := groupValue.(map[string]any)
@@ -182,17 +180,17 @@ func qwenReferencesForEvent(
continue continue
} }
for _, handlerValue := range group["hooks"].([]any) { for _, handlerValue := range group["hooks"].([]any) {
reference, ok := currentQwenReference( reference := currentQwenReference(
handlerValue.(map[string]any), handlerValue.(map[string]any),
scriptName, scriptName,
hookName, hookName,
runtimeRoot, runtimeRoot,
) )
if !ok { if reference == nil {
continue continue
} }
key := normalizedQwenAgentID(reference.agentID) key := managedReferenceKey(reference.agentID)
references[key] = append(references[key], reference) references[key] = append(references[key], *reference)
} }
} }
return references return references
@@ -201,7 +199,7 @@ func qwenReferencesForEvent(
func qwenRuntimeContracts( func qwenRuntimeContracts(
settings qwenHookSettings, settings qwenHookSettings,
runtimeRoot string, runtimeRoot string,
) []qwenRuntimeContract { ) []managedRuntimeContract {
guards := qwenReferencesForEvent( guards := qwenReferencesForEvent(
settings, settings,
"PreToolUse", "PreToolUse",
@@ -223,31 +221,23 @@ func qwenRuntimeContracts(
qwenAuditHookName, qwenAuditHookName,
runtimeRoot, runtimeRoot,
) )
contracts := make([]qwenRuntimeContract, 0) contracts := make([]managedRuntimeContract, 0)
for key, guard := range guards { for key, guard := range guards {
post := posts[key] post := posts[key]
failure := failures[key] failure := failures[key]
if len(guard) != 1 || len(post) != 1 || len(failure) != 1 || 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 continue
} }
contracts = append(contracts, qwenRuntimeContract{ contracts = append(contracts, managedRuntimeContract{
agentID: guard[0].agentID, agentID: guard[0].agentID,
guardPath: guard[0].scriptPath, guardPath: guard[0].scriptPath,
auditPath: post[0].scriptPath, auditPath: post[0].scriptPath,
}) })
} }
sort.Slice(contracts, func(left, right int) bool { sort.Slice(contracts, func(left, right int) bool {
return normalizedQwenAgentID(contracts[left].agentID) < return managedReferenceKey(contracts[left].agentID) <
normalizedQwenAgentID(contracts[right].agentID) managedReferenceKey(contracts[right].agentID)
}) })
return contracts 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 package plugins
import ( import "fmt"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type qwenRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type preparedQwenInstallation struct { type preparedQwenInstallation struct {
changes []*fileChange changes []*fileChange
preconditions []filePrecondition preconditions []filePrecondition
paths *qwenRuntimePaths paths *managedRuntimePaths
}
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
} }
func preflightQwenInstallation( func preflightQwenInstallation(
config InstallConfig, config InstallConfig,
sources *qwenSources, sources *qwenSources,
) (*qwenRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if sources == nil || sources.user == nil || sources.user.filePath == "" { if sources == nil || sources.user == nil || sources.user.filePath == "" {
return nil, "", fmt.Errorf("Qwen Code installation requires user settings") return nil, "", fmt.Errorf("Qwen Code installation requires user settings")
} }
if err := requireQwenHooksEnabled(sources); err != nil { if err := requireQwenHooksEnabled(sources); err != nil {
return nil, "", err 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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Qwen Code")
if err != nil { if err != nil {
return nil, "", err 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 return paths, nodePath, nil
} }
func buildQwenRuntimeConfig(config InstallConfig) ([]byte, error) { func qwenExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
encoded, err := json.MarshalIndent(qwenRuntimeConfig{ return []byte(generateGuardScript(qwenAgentKey, agentID, "", false, "")),
OrgID: config.OrgID, []byte(buildHookScriptWithOutput(qwenAgentKey, agentID, "", false, true))
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 prepareRenderedQwenChange( func prepareRenderedQwenChange(
@@ -180,7 +67,7 @@ func qwenReadOnlyPreconditions(
} }
result := make([]filePrecondition, 0, len(sources.preconditions)) result := make([]filePrecondition, 0, len(sources.preconditions))
for _, condition := range sources.preconditions { for _, condition := range sources.preconditions {
if changedPath != "" && sameQwenPath(condition.filePath, changedPath) { if changedPath != "" && sameManagedPath(condition.filePath, changedPath) {
continue continue
} }
result = append(result, condition) result = append(result, condition)
@@ -198,47 +85,16 @@ func prepareQwenInstallation(
return nil, err return nil, err
} }
if rendered == nil || rendered.document == nil || 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") 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 { if err != nil {
return nil, err 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) settingsChange, err := prepareRenderedQwenChange(rendered)
if err != nil { if err != nil {
return nil, err 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) original := readQwenTestFile(t, fixture.configPath)
failed := false failed := false
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if !failed && sameQwenPath(destination, fixture.configPath) { if !failed && sameManagedPath(destination, fixture.configPath) {
failed = true failed = true
return errors.New("simulated settings commit failure") return errors.New("simulated settings commit failure")
} }
+4 -1
View File
@@ -4,10 +4,13 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"strings" "strings"
) )
var qwenPathSeparatorPattern = regexp.MustCompile(`[/\\]+`)
var qwenHomeEnvironmentKeys = [...]string{"QWEN_HOME", "QWEN_RUNTIME_DIR"} var qwenHomeEnvironmentKeys = [...]string{"QWEN_HOME", "QWEN_RUNTIME_DIR"}
type qwenDisableControl struct { type qwenDisableControl struct {
@@ -154,7 +157,7 @@ func resolveQwenRouting() (*qwenRoutingResult, error) {
if resolveErr != nil { if resolveErr != nil {
return nil, resolveErr return nil, resolveErr
} }
if !sameQwenPath(discoveredDirectory, initialDirectory) { if !sameManagedPath(discoveredDirectory, initialDirectory) {
if err := readCandidate(filepath.Join(discoveredDirectory, ".env")); err != nil { if err := readCandidate(filepath.Join(discoveredDirectory, ".env")); err != nil {
return nil, err return nil, err
} }
+3 -3
View File
@@ -101,12 +101,12 @@ func TestQwenRoutingReadsDiscoveredHomeEnvironment(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("resolve Qwen routing: %v", err) t.Fatalf("resolve Qwen routing: %v", err)
} }
if !sameQwenPath(routing.qwenHome, discovered) { if !sameManagedPath(routing.qwenHome, discovered) {
t.Fatalf("Qwen home = %s", routing.qwenHome) t.Fatalf("Qwen home = %s", routing.qwenHome)
} }
found := false found := false
for _, condition := range routing.preconditions { for _, condition := range routing.preconditions {
found = found || sameQwenPath(condition.filePath, discoveredEnv) found = found || sameManagedPath(condition.filePath, discoveredEnv)
} }
if !found { if !found {
t.Fatalf("discovered environment is absent from preconditions: %#v", routing.preconditions) t.Fatalf("discovered environment is absent from preconditions: %#v", routing.preconditions)
@@ -127,7 +127,7 @@ func TestQwenRoutingSkipsDotenvWhenBothEnvironmentValuesExist(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("resolve Qwen routing: %v", err) 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) t.Fatalf("routing = %#v", routing)
} }
} }
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff. Show more