fix(augment): make hook installation transactional
This commit is contained in:
1 parent
f619b04289
commit
bb7186630d
13 files changed
+1794
-508
No files matched your search
@@ -33,7 +33,8 @@ This repository owns the public Go SDK, the `cmd/elydora` CLI, local signing beh
|
|||||||
- Own one exact matchless `BeforeTool`/`AfterTool` pair named `elydora-guard` and `elydora-audit` with 10,000 millisecond timeouts. Generate guard, config, private key, and native-payload audit runtimes with user settings in one rollback-capable transaction. Use encoded PowerShell commands on Windows, emit `{}` for successful hook execution, propagate frozen and revoked agents through exit code `2`, require strict runtime identity for health, and direct users to `/hooks list` for effective-source verification.
|
- Own one exact matchless `BeforeTool`/`AfterTool` pair named `elydora-guard` and `elydora-audit` with 10,000 millisecond timeouts. Generate guard, config, private key, and native-payload audit runtimes with user settings in one rollback-capable transaction. Use encoded PowerShell commands on Windows, emit `{}` for successful hook execution, propagate frozen and revoked agents through exit code `2`, require strict runtime identity for health, and direct users to `/hooks list` for effective-source verification.
|
||||||
- Keep Grok Build writes inside its native global `$GROK_HOME/hooks/*.json` contract. Treat project `.grok/hooks`, plugin hooks, Claude Code, and Cursor compatibility files as read-only integration sources. Own only exact matchless three-field command groups for `PreToolUse`, `PostToolUse`, and `PostToolUseFailure`; accept official non-negative integer timeouts and string-valued environments; reject lifecycle matchers; and preserve native camelCase payloads.
|
- Keep Grok Build writes inside its native global `$GROK_HOME/hooks/*.json` contract. Treat project `.grok/hooks`, plugin hooks, Claude Code, and Cursor compatibility files as read-only integration sources. Own only exact matchless three-field command groups for `PreToolUse`, `PostToolUse`, and `PostToolUseFailure`; accept official non-negative integer timeouts and string-valued environments; reject lifecycle matchers; and preserve native camelCase payloads.
|
||||||
- Generate Grok guard, config, private key, and audit runtimes with the user hook document in one rollback-capable transaction. Use encoded PowerShell commands on Windows, emit native deny JSON with exit code `2`, preflight physical paths and runtime identity, and require one exact event triple plus strict runtime metadata and a canonical private key for healthy status.
|
- Generate Grok guard, config, private key, and audit runtimes with the user hook document in one rollback-capable transaction. Use encoded PowerShell commands on Windows, emit native deny JSON with exit code `2`, preflight physical paths and runtime identity, and require one exact event triple plus strict runtime metadata and a canonical private key for healthy status.
|
||||||
- Write Auggie hooks only to `~/.augment/settings.json`; keep system and workspace settings read-only. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix because Auggie dispatches supported script paths, and express hook timeouts in milliseconds.
|
- Resolve Auggie user hooks through `~/.augment/settings.json`; keep system, workspace, local workspace, and alternate `--augment-cache-dir` settings read-only. Validate the shipped Auggie 0.33 hook schema: `PreToolUse`, `PostToolUse`, `Stop`, `SessionStart`, `SessionEnd`, `Notification`, and `PromptSubmit`; regex matchers belong to tool events, command handlers use string-array arguments and positive millisecond timeouts, and metadata flags are booleans. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix, preserve the complete native snake_case payload, and propagate freeze and revocation through exit code `2`.
|
||||||
|
- Validate Auggie matcher syntax during installation with Node.js `new RegExp`; keep status and uninstall independent from matcher validation so recovery remains available offline. Commit Auggie settings, both wrappers, generated runtimes, runtime config, and private key through one rollback-capable transaction. Require exact runtime identity, canonical private keys, physical files, and exact wrapper sources during status checks, then verify the effective configuration with `auggie tools list`.
|
||||||
- Write Cline hooks only to `$CLINE_DIR/hooks` with `~/.cline/hooks` as the default; keep Documents and workspace hook roots read-only. Preserve official input byte-for-byte and translate guard exit code `2` into Cline's JSON stdout cancellation control.
|
- Write Cline hooks only to `$CLINE_DIR/hooks` with `~/.cline/hooks` as the default; keep Documents and workspace hook roots read-only. Preserve official input byte-for-byte and translate guard exit code `2` into Cline's JSON stdout cancellation control.
|
||||||
- Write GitHub Copilot CLI hooks only to `$COPILOT_HOME/hooks/elydora-audit.json` with `~/.copilot/hooks/elydora-audit.json` as the default. Migrate exact Elydora-owned entries from project `.github/hooks/hooks.json`, preserve native camelCase input, require `disableAllHooks` to be false, and commit runtime plus hook documents in one rollback-capable transaction.
|
- Write GitHub Copilot CLI hooks only to `$COPILOT_HOME/hooks/elydora-audit.json` with `~/.copilot/hooks/elydora-audit.json` as the default. Migrate exact Elydora-owned entries from project `.github/hooks/hooks.json`, preserve native camelCase input, require `disableAllHooks` to be false, and commit runtime plus hook documents in one rollback-capable transaction.
|
||||||
- Factory Droid `droid@0.175.0` reads direct event maps from `~/.factory/hooks.json` or the legacy `~/.factory/hooks/hooks.json`; `~/.factory/settings.json` stores the same map under `hooks`. Select the active source per event, preserve JSONC syntax, and direct users to `/hooks` for review.
|
- Factory Droid `droid@0.175.0` reads direct event maps from `~/.factory/hooks.json` or the legacy `~/.factory/hooks/hooks.json`; `~/.factory/settings.json` stores the same map under `hooks`. Select the active source per event, preserve JSONC syntax, and direct users to `/hooks` for review.
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Kimi installation follows home-directory evidence. Kimi Code uses `$KIMI_CODE_HO
|
|||||||
|
|
||||||
Grok Build installation writes native global `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` hooks to `$GROK_HOME/hooks/elydora-audit.json` (default `~/.grok/hooks/elydora-audit.json`). The hook file, generated runtimes, runtime config, and private key commit as one rollback-capable transaction. Managed commands preserve Grok's native camelCase JSON, return `{"decision":"deny","reason":"..."}` with exit code `2` for frozen or revoked agents, and use encoded PowerShell on Windows. Project hooks follow Grok's `/hooks-trust` workflow; Elydora leaves project, plugin, `hooks-paths`, Claude Code, and Cursor compatibility sources unchanged. Run `grok inspect --json` to inspect the effective hook inventory.
|
Grok Build installation writes native global `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` hooks to `$GROK_HOME/hooks/elydora-audit.json` (default `~/.grok/hooks/elydora-audit.json`). The hook file, generated runtimes, runtime config, and private key commit as one rollback-capable transaction. Managed commands preserve Grok's native camelCase JSON, return `{"decision":"deny","reason":"..."}` with exit code `2` for frozen or revoked agents, and use encoded PowerShell on Windows. Project hooks follow Grok's `/hooks-trust` workflow; Elydora leaves project, plugin, `hooks-paths`, Claude Code, and Cursor compatibility sources unchanged. Run `grok inspect --json` to inspect the effective hook inventory.
|
||||||
|
|
||||||
Auggie installation writes user-level hooks to `~/.augment/settings.json` and creates the `.cmd` or `.sh` wrappers required by its command runner. System and workspace settings remain unchanged. Sessions started with `--augment-cache-dir` load settings from that alternate directory.
|
Auggie installation commits `~/.augment/settings.json`, both platform wrappers, generated runtimes, runtime config, and private key as one rollback-capable transaction. The managed `PreToolUse` guard propagates exit code `2`; `PostToolUse` preserves Auggie's complete native hook payload. System, workspace, local workspace, and alternate `--augment-cache-dir` settings remain unchanged. Run `auggie tools list` to validate the effective user configuration.
|
||||||
|
|
||||||
Factory Droid installation selects `PreToolUse` and `PostToolUse` independently across `~/.factory/hooks.json`, the legacy `~/.factory/hooks/hooks.json`, and `~/.factory/settings.json`. Elydora preserves JSONC comments and formatting, commits runtime and hook changes as one transaction, and reports disabled hooks as unhealthy. Run `/hooks` in Droid after installation to review the effective hooks.
|
Factory Droid installation selects `PreToolUse` and `PostToolUse` independently across `~/.factory/hooks.json`, the legacy `~/.factory/hooks/hooks.json`, and `~/.factory/settings.json`. Elydora preserves JSONC comments and formatting, commits runtime and hook changes as one transaction, and reports disabled hooks as unhealthy. Run `/hooks` in Droid after installation to review the effective hooks.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAugmentCLIPreflightPreservesMalformedSettings(t *testing.T) {
|
||||||
|
homeDir := filepath.Join(t.TempDir(), "home")
|
||||||
|
settingsPath := filepath.Join(homeDir, ".augment", "settings.json")
|
||||||
|
keyPath := filepath.Join(t.TempDir(), "private.key")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(settingsPath), 0700); err != nil {
|
||||||
|
t.Fatalf("create Auggie settings directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(settingsPath, []byte("{ malformed"), 0600); err != nil {
|
||||||
|
t.Fatalf("write malformed Auggie settings: %v", err)
|
||||||
|
}
|
||||||
|
privateKey := "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc"
|
||||||
|
if err := os.WriteFile(keyPath, []byte(privateKey+"\n"), 0600); err != nil {
|
||||||
|
t.Fatalf("write private key: %v", err)
|
||||||
|
}
|
||||||
|
command := exec.Command(
|
||||||
|
"go",
|
||||||
|
"run",
|
||||||
|
".",
|
||||||
|
"install",
|
||||||
|
"--agent",
|
||||||
|
"augment",
|
||||||
|
"--org-id",
|
||||||
|
"org-1",
|
||||||
|
"--agent-id",
|
||||||
|
"agent-1",
|
||||||
|
"--private-key-file",
|
||||||
|
keyPath,
|
||||||
|
"--kid",
|
||||||
|
"kid-1",
|
||||||
|
"--base-url",
|
||||||
|
"https://api.elydora.com",
|
||||||
|
)
|
||||||
|
command.Env = append(
|
||||||
|
os.Environ(),
|
||||||
|
"HOME="+homeDir,
|
||||||
|
"USERPROFILE="+homeDir,
|
||||||
|
)
|
||||||
|
output, err := command.CombinedOutput()
|
||||||
|
if err == nil || !strings.Contains(
|
||||||
|
string(output),
|
||||||
|
"parse Auggie user settings",
|
||||||
|
) {
|
||||||
|
t.Fatalf("CLI preflight result = %v\n%s", err, output)
|
||||||
|
}
|
||||||
|
raw, readErr := os.ReadFile(settingsPath)
|
||||||
|
if readErr != nil || string(raw) != "{ malformed" {
|
||||||
|
t.Fatalf("malformed settings changed: %q, %v", raw, readErr)
|
||||||
|
}
|
||||||
|
if _, statErr := os.Lstat(filepath.Join(homeDir, ".elydora")); !errors.Is(
|
||||||
|
statErr,
|
||||||
|
os.ErrNotExist,
|
||||||
|
) {
|
||||||
|
t.Fatalf("runtime directory created during failed preflight: %v", statErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,137 +1,139 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
// AugmentPlugin manages Auggie native global user hooks.
|
// AugmentPlugin manages Auggie native global user hooks.
|
||||||
type AugmentPlugin struct{}
|
type AugmentPlugin struct {
|
||||||
|
rename renameFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagesGuardRuntime reports that Auggie commits its provider guard together
|
||||||
|
// with both wrappers, the audit runtime, and user settings.
|
||||||
|
func (p *AugmentPlugin) ManagesGuardRuntime() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreflightInstall validates settings and runtime identity before any write.
|
||||||
|
func (p *AugmentPlugin) PreflightInstall(config InstallConfig) error {
|
||||||
|
document, err := readAugmentDocument()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _, err = preflightAugmentInstallation(config, document)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *AugmentPlugin) Install(config InstallConfig) error {
|
func (p *AugmentPlugin) Install(config InstallConfig) error {
|
||||||
if config.AgentID == "" {
|
document, err := readAugmentDocument()
|
||||||
return fmt.Errorf("agent ID is required")
|
|
||||||
}
|
|
||||||
document, err := readAugmentConfig()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if config.GuardScriptPath == "" {
|
paths, nodePath, err := preflightAugmentInstallation(config, document)
|
||||||
return fmt.Errorf("guard script path is required")
|
|
||||||
}
|
|
||||||
guardExists, err := regularFileExists(
|
|
||||||
config.GuardScriptPath, "Elydora guard runtime",
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !guardExists {
|
hooks, _ := removeManagedAugmentHooks(document.hooks, "", paths.runtimeRoot)
|
||||||
return fmt.Errorf(
|
|
||||||
"Elydora guard runtime is missing: %s", config.GuardScriptPath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
hookPath, err := hookScriptPath(config.AgentID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if config.HookScript != "" {
|
|
||||||
hookPath = config.HookScript
|
|
||||||
}
|
|
||||||
nodePath, err := resolveNodeRuntime()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := validateAugmentMatchers(document.hooks, nodePath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
runtimeRoot, err := augmentRuntimeRoot()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
wrappers := resolveAugmentWrapperPaths(runtimeRoot, config.AgentID)
|
|
||||||
hooks, _ := removeManagedAugmentHooks(document.hooks, "", runtimeRoot)
|
|
||||||
hooks["PreToolUse"] = append(
|
hooks["PreToolUse"] = append(
|
||||||
hooks["PreToolUse"],
|
hooks["PreToolUse"],
|
||||||
buildAugmentGroup(buildAugmentHandler(wrappers.guard)),
|
buildAugmentGroup(buildAugmentHandler(paths.guardWrapperPath)),
|
||||||
)
|
)
|
||||||
hooks["PostToolUse"] = append(
|
hooks["PostToolUse"] = append(
|
||||||
hooks["PostToolUse"],
|
hooks["PostToolUse"],
|
||||||
buildAugmentGroup(buildAugmentHandler(wrappers.audit)),
|
buildAugmentGroup(buildAugmentHandler(paths.auditWrapperPath)),
|
||||||
)
|
)
|
||||||
next := cloneAugmentObject(document.root)
|
rendered, err := renderAugmentDocument(document, hooks)
|
||||||
next["hooks"] = renderAugmentHooks(hooks)
|
if err != nil {
|
||||||
|
return fmt.Errorf("render Auggie user settings: %w", err)
|
||||||
runtimeConfig := config
|
|
||||||
runtimeConfig.AgentName = augmentAgentKey
|
|
||||||
if err := GenerateHookScript(hookPath, runtimeConfig); err != nil {
|
|
||||||
return fmt.Errorf("generate hook script: %w", err)
|
|
||||||
}
|
}
|
||||||
if err := writeHookFileAtomic(
|
changes, err := prepareAugmentInstallationChanges(
|
||||||
wrappers.guard,
|
config,
|
||||||
buildAugmentWrapper(nodePath, config.GuardScriptPath),
|
paths,
|
||||||
0700,
|
nodePath,
|
||||||
|
rendered,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeAugmentChanges(
|
||||||
|
changes,
|
||||||
|
"Install Augment Code CLI hooks",
|
||||||
|
p.rename,
|
||||||
|
paths.runtimeRoot,
|
||||||
|
paths.agentDirectory,
|
||||||
|
filepath.Dir(document.configPath),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("write Auggie guard wrapper: %w", err)
|
return err
|
||||||
}
|
|
||||||
if err := writeHookFileAtomic(
|
|
||||||
wrappers.audit,
|
|
||||||
buildAugmentWrapper(nodePath, hookPath),
|
|
||||||
0700,
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("write Auggie audit wrapper: %w", err)
|
|
||||||
}
|
|
||||||
if err := writeHookJSONObjectAtomic(document.configPath, next); err != nil {
|
|
||||||
return fmt.Errorf("write Auggie settings: %w", err)
|
|
||||||
}
|
}
|
||||||
fmt.Println("Auggie: user-level PreToolUse and PostToolUse hooks installed.")
|
fmt.Println("Auggie: user-level PreToolUse and PostToolUse hooks installed.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AugmentPlugin) Uninstall(agentID string) error {
|
func (p *AugmentPlugin) Uninstall(agentID string) error {
|
||||||
document, err := readAugmentConfig()
|
document, err := readAugmentDocument()
|
||||||
if err != nil || !document.exists {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
runtimeRoot, err := augmentRuntimeRoot()
|
if !document.exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
runtimeRoot, err := AgentRuntimeRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hooks, changed := removeManagedAugmentHooks(
|
hooks, changed := removeManagedAugmentHooks(
|
||||||
document.hooks, agentID, runtimeRoot,
|
document.hooks,
|
||||||
|
agentID,
|
||||||
|
runtimeRoot,
|
||||||
)
|
)
|
||||||
if !changed {
|
if !changed {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
next := cloneAugmentObject(document.root)
|
rendered, err := renderAugmentDocument(document, hooks)
|
||||||
if len(hooks) == 0 {
|
if err != nil {
|
||||||
delete(next, "hooks")
|
return fmt.Errorf("render Auggie user settings: %w", err)
|
||||||
} else {
|
|
||||||
next["hooks"] = renderAugmentHooks(hooks)
|
|
||||||
}
|
}
|
||||||
if len(next) == 0 {
|
change, err := prepareRenderedAugmentChange(rendered)
|
||||||
return removeHookFile(document.configPath, "Auggie settings")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return writeHookJSONObjectAtomic(document.configPath, next)
|
return writeAugmentChanges(
|
||||||
|
[]*fileChange{change},
|
||||||
|
"Uninstall Augment Code CLI hooks",
|
||||||
|
p.rename,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
filepath.Dir(document.configPath),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AugmentPlugin) Status() (PluginStatus, error) {
|
func (p *AugmentPlugin) Status() (PluginStatus, error) {
|
||||||
document, err := readAugmentConfig()
|
configPath, pathErr := augmentConfigPath()
|
||||||
|
entry := SupportedAgents[augmentAgentKey]
|
||||||
status := PluginStatus{
|
status := PluginStatus{
|
||||||
AgentName: augmentAgentKey,
|
AgentName: augmentAgentKey, DisplayName: entry.Name, ConfigPath: configPath,
|
||||||
DisplayName: "Augment Code CLI",
|
|
||||||
ConfigPath: document.configPath,
|
|
||||||
}
|
}
|
||||||
|
if pathErr != nil {
|
||||||
|
return status, pathErr
|
||||||
|
}
|
||||||
|
document, err := readAugmentDocument()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
runtimeRoot, err := augmentRuntimeRoot()
|
runtimeRoot, err := AgentRuntimeRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
contracts := augmentRuntimeContracts(document.hooks, runtimeRoot)
|
contracts := augmentRuntimeContracts(document.hooks, runtimeRoot)
|
||||||
if len(contracts) == 0 {
|
status.HookConfigured = len(contracts) > 0
|
||||||
|
if !status.HookConfigured {
|
||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
status.HookConfigured = true
|
|
||||||
status.HookScriptExists, err = augmentRuntimeFilesExist(
|
status.HookScriptExists, err = augmentRuntimeFilesExist(
|
||||||
contracts, runtimeRoot,
|
contracts,
|
||||||
|
runtimeRoot,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status, err
|
return status, err
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -42,6 +45,14 @@ type augmentDocument struct {
|
|||||||
configPath string
|
configPath string
|
||||||
root map[string]any
|
root map[string]any
|
||||||
hooks augmentHooks
|
hooks augmentHooks
|
||||||
|
raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type augmentRenderedDocument struct {
|
||||||
|
document *augmentDocument
|
||||||
|
changed bool
|
||||||
|
next []byte
|
||||||
|
remove bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type augmentWrapperPaths struct {
|
type augmentWrapperPaths struct {
|
||||||
@@ -200,6 +211,71 @@ func readAugmentHooks(root map[string]any) (augmentHooks, error) {
|
|||||||
return hooks, nil
|
return hooks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAugmentDocument(configPath string, raw []byte) (*augmentDocument, error) {
|
||||||
|
label := fmt.Sprintf("Auggie user settings at %s", configPath)
|
||||||
|
root, err := decodeStrictJSONObject(raw, label)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hooks, err := readAugmentHooks(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &augmentDocument{
|
||||||
|
exists: true, configPath: configPath, root: root, hooks: hooks,
|
||||||
|
raw: append([]byte(nil), raw...),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAugmentDocument(configPath string) *augmentDocument {
|
||||||
|
return &augmentDocument{
|
||||||
|
configPath: configPath,
|
||||||
|
root: map[string]any{},
|
||||||
|
hooks: augmentHooks{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderAugmentDocument(
|
||||||
|
document *augmentDocument,
|
||||||
|
hooks augmentHooks,
|
||||||
|
) (*augmentRenderedDocument, error) {
|
||||||
|
if document == nil {
|
||||||
|
return nil, fmt.Errorf("Auggie settings document is required")
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(hooks, document.hooks) {
|
||||||
|
return &augmentRenderedDocument{document: document}, nil
|
||||||
|
}
|
||||||
|
if !document.exists && len(hooks) == 0 {
|
||||||
|
return &augmentRenderedDocument{document: document}, nil
|
||||||
|
}
|
||||||
|
root := cloneAugmentObject(document.root)
|
||||||
|
if len(hooks) == 0 {
|
||||||
|
delete(root, "hooks")
|
||||||
|
} else {
|
||||||
|
root["hooks"] = renderAugmentHooks(hooks)
|
||||||
|
}
|
||||||
|
if len(root) == 0 {
|
||||||
|
return &augmentRenderedDocument{
|
||||||
|
document: document,
|
||||||
|
changed: true,
|
||||||
|
remove: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
next, err := json.MarshalIndent(root, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode Auggie user settings: %w", err)
|
||||||
|
}
|
||||||
|
next = append(next, '\n')
|
||||||
|
if _, err := parseAugmentDocument(document.configPath, next); err != nil {
|
||||||
|
return nil, fmt.Errorf("validate rendered Auggie user settings: %w", err)
|
||||||
|
}
|
||||||
|
return &augmentRenderedDocument{
|
||||||
|
document: document,
|
||||||
|
changed: !document.exists || !bytes.Equal(next, document.raw),
|
||||||
|
next: next,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func renderAugmentHooks(hooks augmentHooks) map[string]any {
|
func renderAugmentHooks(hooks augmentHooks) map[string]any {
|
||||||
result := make(map[string]any, len(hooks))
|
result := make(map[string]any, len(hooks))
|
||||||
for event, groups := range hooks {
|
for event, groups := range hooks {
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type augmentRuntimePaths struct {
|
||||||
|
runtimeRoot string
|
||||||
|
agentDirectory string
|
||||||
|
configPath string
|
||||||
|
keyPath string
|
||||||
|
guardPath string
|
||||||
|
auditPath string
|
||||||
|
guardWrapperPath string
|
||||||
|
auditWrapperPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAugmentInstallConfig(config InstallConfig) error {
|
||||||
|
for _, field := range []struct{ name, value string }{
|
||||||
|
{"agent name", config.AgentName},
|
||||||
|
{"organization ID", config.OrgID},
|
||||||
|
{"agent ID", config.AgentID},
|
||||||
|
{"key ID", config.KID},
|
||||||
|
{"private key", config.PrivateKey},
|
||||||
|
{"base URL", config.BaseURL},
|
||||||
|
{"guard script path", config.GuardScriptPath},
|
||||||
|
} {
|
||||||
|
if field.value == "" {
|
||||||
|
return fmt.Errorf("%s is required", field.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.AgentName != augmentAgentKey {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Augment Code CLI installation requires agent name %s",
|
||||||
|
augmentAgentKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config.OrgID) == "" {
|
||||||
|
return fmt.Errorf("organization ID is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config.KID) == "" {
|
||||||
|
return fmt.Errorf("key ID is required")
|
||||||
|
}
|
||||||
|
if config.Token != "" && strings.TrimSpace(config.Token) == "" {
|
||||||
|
return fmt.Errorf("token must contain a non-whitespace value when provided")
|
||||||
|
}
|
||||||
|
if err := validateManagedPrivateKey(config.PrivateKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validateManagedBaseURL(config.BaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentAgentPaths(config InstallConfig) (*augmentRuntimePaths, error) {
|
||||||
|
if err := validateAugmentInstallConfig(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
runtimeRoot, err := AgentRuntimeRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
wrappers := resolveAugmentWrapperPaths(agentDirectory)
|
||||||
|
paths := &augmentRuntimePaths{
|
||||||
|
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
|
||||||
|
configPath: filepath.Join(agentDirectory, "config.json"),
|
||||||
|
keyPath: filepath.Join(agentDirectory, "private.key"),
|
||||||
|
guardPath: filepath.Join(agentDirectory, augmentGuardScript),
|
||||||
|
auditPath: filepath.Join(agentDirectory, augmentAuditScript),
|
||||||
|
guardWrapperPath: wrappers.guard,
|
||||||
|
auditWrapperPath: wrappers.audit,
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(config.GuardScriptPath) ||
|
||||||
|
!sameAugmentPath(config.GuardScriptPath, paths.guardPath) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Elydora guard runtime must use the managed agent directory: %s",
|
||||||
|
paths.guardPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
|
||||||
|
!sameAugmentPath(config.HookScript, paths.auditPath)) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Elydora audit runtime must use the managed agent directory: %s",
|
||||||
|
paths.auditPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return paths, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func preflightAugmentInstallation(
|
||||||
|
config InstallConfig,
|
||||||
|
document *augmentDocument,
|
||||||
|
) (*augmentRuntimePaths, string, error) {
|
||||||
|
if document == nil || document.configPath == "" {
|
||||||
|
return nil, "", fmt.Errorf(
|
||||||
|
"Augment Code CLI installation requires a user settings document",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
paths, err := augmentAgentPaths(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if err := validateAugmentRuntimeIdentity(
|
||||||
|
paths.agentDirectory,
|
||||||
|
config.AgentID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
nodePath, err := resolveNodeRuntime()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if err := validateAugmentMatchers(document.hooks, nodePath); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return paths, nodePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAugmentRuntimeConfig(config InstallConfig) ([]byte, error) {
|
||||||
|
value := map[string]any{
|
||||||
|
"org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID,
|
||||||
|
"base_url": config.BaseURL, "agent_name": augmentAgentKey,
|
||||||
|
}
|
||||||
|
if config.Token != "" {
|
||||||
|
value["token"] = config.Token
|
||||||
|
}
|
||||||
|
encoded, err := json.MarshalIndent(value, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
|
||||||
|
}
|
||||||
|
encoded = append(encoded, '\n')
|
||||||
|
if len(encoded) > maxRuntimeConfigBytes {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Elydora runtime config exceeds %d bytes after JSON encoding",
|
||||||
|
maxRuntimeConfigBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareAugmentInstallationChanges(
|
||||||
|
config InstallConfig,
|
||||||
|
paths *augmentRuntimePaths,
|
||||||
|
nodePath string,
|
||||||
|
rendered *augmentRenderedDocument,
|
||||||
|
) ([]*fileChange, error) {
|
||||||
|
if paths == nil || rendered == nil {
|
||||||
|
return nil, fmt.Errorf("prepared Auggie installation is required")
|
||||||
|
}
|
||||||
|
if err := validateAugmentRuntimeIdentity(
|
||||||
|
paths.agentDirectory,
|
||||||
|
config.AgentID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
runtimeConfig, err := buildAugmentRuntimeConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := []struct {
|
||||||
|
path, label string
|
||||||
|
content []byte
|
||||||
|
mode os.FileMode
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
paths.guardPath,
|
||||||
|
"Elydora guard runtime",
|
||||||
|
[]byte(generateGuardScript(
|
||||||
|
augmentAgentKey,
|
||||||
|
config.AgentID,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
)),
|
||||||
|
0700,
|
||||||
|
},
|
||||||
|
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600},
|
||||||
|
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600},
|
||||||
|
{
|
||||||
|
paths.auditPath,
|
||||||
|
"Elydora audit runtime",
|
||||||
|
[]byte(buildHookScriptWithOutput(
|
||||||
|
augmentAgentKey,
|
||||||
|
config.AgentID,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
)),
|
||||||
|
0700,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
paths.guardWrapperPath,
|
||||||
|
"Auggie guard wrapper",
|
||||||
|
buildAugmentWrapper(nodePath, paths.guardPath),
|
||||||
|
0700,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
paths.auditWrapperPath,
|
||||||
|
"Auggie audit wrapper",
|
||||||
|
buildAugmentWrapper(nodePath, paths.auditPath),
|
||||||
|
0700,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
changes := make([]*fileChange, 0, len(items)+1)
|
||||||
|
for _, item := range items {
|
||||||
|
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
changes = append(changes, change)
|
||||||
|
}
|
||||||
|
documentChange, err := prepareRenderedAugmentChange(rendered)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(changes, documentChange), nil
|
||||||
|
}
|
||||||
@@ -33,49 +33,91 @@ func augmentAuditWrapperName() string {
|
|||||||
return "augment-hook.sh"
|
return "augment-hook.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
func augmentHomeDirectory() (string, error) {
|
func augmentConfigPath() (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)
|
||||||
}
|
}
|
||||||
return home, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func augmentRuntimeRoot() (string, error) {
|
|
||||||
home, err := augmentHomeDirectory()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Join(home, ".elydora"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func augmentConfigPath() (string, error) {
|
|
||||||
home, err := augmentHomeDirectory()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Join(home, ".augment", "settings.json"), nil
|
return filepath.Join(home, ".augment", "settings.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAugmentConfig() (augmentDocument, error) {
|
func readAugmentDocument() (*augmentDocument, error) {
|
||||||
configPath, err := augmentConfigPath()
|
configPath, err := augmentConfigPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return augmentDocument{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, exists, err := readHookJSONObject(configPath, "Auggie settings")
|
if _, err := managedPhysicalDirectoryExists(
|
||||||
|
filepath.Dir(configPath),
|
||||||
|
"Auggie configuration directory",
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snapshot, err := readManagedFile(
|
||||||
|
configPath,
|
||||||
|
"Auggie user settings",
|
||||||
|
maxManagedSourceBytes,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return augmentDocument{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
hooks, err := readAugmentHooks(root)
|
if snapshot == nil {
|
||||||
if err != nil {
|
return createAugmentDocument(configPath), nil
|
||||||
return augmentDocument{}, err
|
|
||||||
}
|
}
|
||||||
return augmentDocument{
|
return parseAugmentDocument(configPath, snapshot.contents)
|
||||||
exists: exists,
|
}
|
||||||
configPath: configPath,
|
|
||||||
root: root,
|
func prepareRenderedAugmentChange(
|
||||||
hooks: hooks,
|
rendered *augmentRenderedDocument,
|
||||||
}, nil
|
) (*fileChange, error) {
|
||||||
|
if rendered == nil || !rendered.changed {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return prepareSourceChange(
|
||||||
|
rendered.document.configPath,
|
||||||
|
"Auggie user settings",
|
||||||
|
rendered.document.raw,
|
||||||
|
rendered.document.exists,
|
||||||
|
rendered.next,
|
||||||
|
0600,
|
||||||
|
rendered.remove,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -106,7 +148,9 @@ func validateAugmentMatchers(hooks augmentHooks, nodePath string) error {
|
|||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"Auggie settings group hooks.%s[%d] matcher "+
|
"Auggie settings group hooks.%s[%d] matcher "+
|
||||||
"must be a valid JavaScript regular expression: %s",
|
"must be a valid JavaScript regular expression: %s",
|
||||||
event, groupIndex, message,
|
event,
|
||||||
|
groupIndex,
|
||||||
|
message,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,134 +203,16 @@ func buildAugmentGroup(handler map[string]any) augmentGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAugmentWrapperPaths(runtimeRoot, agentID string) augmentWrapperPaths {
|
func resolveAugmentWrapperPaths(agentDirectory string) augmentWrapperPaths {
|
||||||
agentDirectory := filepath.Join(runtimeRoot, agentID)
|
|
||||||
return augmentWrapperPaths{
|
return augmentWrapperPaths{
|
||||||
guard: filepath.Join(agentDirectory, augmentGuardWrapperName()),
|
guard: filepath.Join(agentDirectory, augmentGuardWrapperName()),
|
||||||
audit: filepath.Join(agentDirectory, augmentAuditWrapperName()),
|
audit: filepath.Join(agentDirectory, augmentAuditWrapperName()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func managedAugmentIDs(
|
func requireAugmentAbsoluteNode(nodePath string) error {
|
||||||
groups []augmentGroup,
|
if !filepath.IsAbs(nodePath) || !isClaudeNodeExecutable(nodePath) {
|
||||||
wrapperName, runtimeRoot string,
|
return fmt.Errorf("Auggie hooks require an absolute Node.js executable path")
|
||||||
) map[string]string {
|
|
||||||
result := map[string]string{}
|
|
||||||
for _, group := range groups {
|
|
||||||
for _, handler := range group.handlers {
|
|
||||||
agentID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
|
|
||||||
if managed {
|
|
||||||
key := agentID
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
key = strings.ToLower(key)
|
|
||||||
}
|
}
|
||||||
result[key] = agentID
|
return nil
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func augmentRuntimeContracts(
|
|
||||||
hooks augmentHooks,
|
|
||||||
runtimeRoot string,
|
|
||||||
) []augmentRuntimeContract {
|
|
||||||
guards := managedAugmentIDs(
|
|
||||||
hooks["PreToolUse"], augmentGuardWrapperName(), runtimeRoot,
|
|
||||||
)
|
|
||||||
audits := managedAugmentIDs(
|
|
||||||
hooks["PostToolUse"], augmentAuditWrapperName(), runtimeRoot,
|
|
||||||
)
|
|
||||||
keys := make([]string, 0, len(guards))
|
|
||||||
for key := range guards {
|
|
||||||
if _, exists := audits[key]; exists {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
contracts := make([]augmentRuntimeContract, 0, len(keys))
|
|
||||||
for _, key := range keys {
|
|
||||||
agentID := guards[key]
|
|
||||||
agentDirectory := filepath.Join(runtimeRoot, agentID)
|
|
||||||
contracts = append(contracts, augmentRuntimeContract{
|
|
||||||
agentID: agentID,
|
|
||||||
guardPath: filepath.Join(agentDirectory, augmentGuardScript),
|
|
||||||
auditPath: filepath.Join(agentDirectory, augmentAuditScript),
|
|
||||||
guardWrapper: filepath.Join(agentDirectory, augmentGuardWrapperName()),
|
|
||||||
auditWrapper: filepath.Join(agentDirectory, augmentAuditWrapperName()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return contracts
|
|
||||||
}
|
|
||||||
|
|
||||||
func augmentRuntimeFilesExist(
|
|
||||||
contracts []augmentRuntimeContract,
|
|
||||||
runtimeRoot string,
|
|
||||||
) (bool, error) {
|
|
||||||
entries, err := os.ReadDir(runtimeRoot)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf(
|
|
||||||
"read Elydora runtime directory at %s: %w", runtimeRoot, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for _, contract := range contracts {
|
|
||||||
var entryName string
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() && sameAugmentAgentID(entry.Name(), contract.agentID) {
|
|
||||||
entryName = entry.Name()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if entryName == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
agentDirectory := filepath.Join(runtimeRoot, entryName)
|
|
||||||
configPath := filepath.Join(agentDirectory, "config.json")
|
|
||||||
config, exists, err := readHookJSONObject(
|
|
||||||
configPath, "Elydora runtime config",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
agentName, ok := config["agent_name"].(string)
|
|
||||||
if !ok {
|
|
||||||
return false, fmt.Errorf(
|
|
||||||
`Elydora runtime config at %s field "agent_name" must be a string`,
|
|
||||||
configPath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if agentName != augmentAgentKey {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
files := []struct {
|
|
||||||
path string
|
|
||||||
label string
|
|
||||||
}{
|
|
||||||
{contract.guardPath, "Elydora guard runtime"},
|
|
||||||
{contract.auditPath, "Elydora audit runtime"},
|
|
||||||
{contract.guardWrapper, "Auggie guard wrapper"},
|
|
||||||
{contract.auditWrapper, "Auggie audit wrapper"},
|
|
||||||
}
|
|
||||||
complete := true
|
|
||||||
for _, file := range files {
|
|
||||||
exists, err := regularFileExists(file.path, file.label)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
complete = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if complete {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAugmentOfficialBinaryAcceptsInstalledUserHooks(t *testing.T) {
|
||||||
|
entry := os.Getenv("ELYDORA_AUGGIE_ENTRY")
|
||||||
|
if entry == "" {
|
||||||
|
t.Skip("ELYDORA_AUGGIE_ENTRY is unset")
|
||||||
|
}
|
||||||
|
nodePath, err := resolveNodeRuntime()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve Node.js runtime: %v", err)
|
||||||
|
}
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
environment := append(
|
||||||
|
os.Environ(),
|
||||||
|
"HOME="+fixture.homeDir,
|
||||||
|
"USERPROFILE="+fixture.homeDir,
|
||||||
|
)
|
||||||
|
version := exec.Command(nodePath, entry, "--version")
|
||||||
|
version.Dir = fixture.projectDir
|
||||||
|
version.Env = environment
|
||||||
|
versionOutput, err := version.CombinedOutput()
|
||||||
|
if err != nil || !strings.Contains(string(versionOutput), "0.33.0") {
|
||||||
|
t.Fatalf("official Auggie version: %v\n%s", err, versionOutput)
|
||||||
|
}
|
||||||
|
load := exec.Command(nodePath, entry, "tools", "list")
|
||||||
|
load.Dir = fixture.projectDir
|
||||||
|
load.Env = environment
|
||||||
|
loadOutput, err := load.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("official Auggie settings load: %v\n%s", err, loadOutput)
|
||||||
|
}
|
||||||
|
output := strings.ToLower(string(loadOutput))
|
||||||
|
for _, problem := range []string{
|
||||||
|
"invalid settings",
|
||||||
|
"settings validation",
|
||||||
|
"failed to parse",
|
||||||
|
"hook configuration error",
|
||||||
|
} {
|
||||||
|
if strings.Contains(output, problem) {
|
||||||
|
t.Fatalf("official Auggie loader reported %q:\n%s", problem, loadOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func installAugmentRuntimeFixture(
|
||||||
|
t *testing.T,
|
||||||
|
status string,
|
||||||
|
) (*augmentFixture, *codexTestAPI, *httptest.Server) {
|
||||||
|
t.Helper()
|
||||||
|
api := &codexTestAPI{status: status}
|
||||||
|
server := httptest.NewServer(api)
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
fixture.config.BaseURL = server.URL
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
server.Close()
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
return fixture, api, server
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentOfficialPayload(event string) map[string]any {
|
||||||
|
value := map[string]any{
|
||||||
|
"hook_event_name": event,
|
||||||
|
"conversation_id": "conversation-1",
|
||||||
|
"workspace_roots": []any{"C:/workspace"},
|
||||||
|
"tool_name": "launch-process",
|
||||||
|
"tool_input": map[string]any{
|
||||||
|
"command": "go test ./...",
|
||||||
|
"nested": map[string]any{"preserve": true},
|
||||||
|
},
|
||||||
|
"is_mcp_tool": false,
|
||||||
|
"conversation_data": []any{
|
||||||
|
map[string]any{"role": "user", "content": "preserve this"},
|
||||||
|
},
|
||||||
|
"mcp_metadata": map[string]any{
|
||||||
|
"server": "local", "transport": "stdio",
|
||||||
|
},
|
||||||
|
"user_context": map[string]any{"account": "user-1"},
|
||||||
|
"future_field": map[string]any{"survives": []any{"exactly", float64(2)}},
|
||||||
|
}
|
||||||
|
if event == "PostToolUse" {
|
||||||
|
value["tool_output"] = map[string]any{
|
||||||
|
"stdout": "passed", "stderr": "", "exit_code": float64(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalAugmentPayload(t *testing.T, payload map[string]any) []byte {
|
||||||
|
t.Helper()
|
||||||
|
encoded, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal Auggie payload: %v", err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentGuardPropagatesOfficialBlockingExitCode(t *testing.T) {
|
||||||
|
for _, status := range []string{"frozen", "revoked"} {
|
||||||
|
t.Run(status, func(t *testing.T) {
|
||||||
|
fixture, _, server := installAugmentRuntimeFixture(t, status)
|
||||||
|
defer server.Close()
|
||||||
|
handler := augmentTestManagedHandler(
|
||||||
|
t,
|
||||||
|
readAugmentTestObject(t, fixture.configPath),
|
||||||
|
"PreToolUse",
|
||||||
|
fixture.guardWrapper,
|
||||||
|
)
|
||||||
|
exit, stdout, stderr := runAugmentCommand(
|
||||||
|
t,
|
||||||
|
handler["command"].(string),
|
||||||
|
fixture,
|
||||||
|
marshalAugmentPayload(t, augmentOfficialPayload("PreToolUse")),
|
||||||
|
)
|
||||||
|
if exit != 2 || stdout != "" ||
|
||||||
|
!strings.Contains(strings.ToLower(stderr), status) ||
|
||||||
|
!strings.Contains(stderr, "Tool execution blocked") {
|
||||||
|
t.Fatalf("guard result = %d, %q, %q", exit, stdout, stderr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentAuditPreservesCompleteNativePayload(t *testing.T) {
|
||||||
|
fixture, api, server := installAugmentRuntimeFixture(t, "active")
|
||||||
|
defer server.Close()
|
||||||
|
payload := augmentOfficialPayload("PostToolUse")
|
||||||
|
handler := augmentTestManagedHandler(
|
||||||
|
t,
|
||||||
|
readAugmentTestObject(t, fixture.configPath),
|
||||||
|
"PostToolUse",
|
||||||
|
fixture.auditWrapper,
|
||||||
|
)
|
||||||
|
exit, stdout, stderr := runAugmentCommand(
|
||||||
|
t,
|
||||||
|
handler["command"].(string),
|
||||||
|
fixture,
|
||||||
|
marshalAugmentPayload(t, payload),
|
||||||
|
)
|
||||||
|
if exit != 0 || stdout != "" || stderr != "" {
|
||||||
|
t.Fatalf("audit result = %d, %q, %q", exit, stdout, stderr)
|
||||||
|
}
|
||||||
|
_, postAuth, operations := api.snapshot()
|
||||||
|
if postAuth != "Bearer token-1" ||
|
||||||
|
len(operations) != 1 ||
|
||||||
|
!reflect.DeepEqual(operations[0]["payload"], payload) {
|
||||||
|
t.Fatalf("Auggie native operation = auth %q, %#v", postAuth, operations)
|
||||||
|
}
|
||||||
|
if operations[0]["prev_chain_hash"] !=
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" {
|
||||||
|
t.Fatalf("Auggie operation chain = %#v", operations[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentRuntimeFailuresRemainObservableAndFailOpen(t *testing.T) {
|
||||||
|
fixture, _, server := installAugmentRuntimeFixture(t, "active")
|
||||||
|
settings := readAugmentTestObject(t, fixture.configPath)
|
||||||
|
guard := augmentTestManagedHandler(
|
||||||
|
t,
|
||||||
|
settings,
|
||||||
|
"PreToolUse",
|
||||||
|
fixture.guardWrapper,
|
||||||
|
)
|
||||||
|
audit := augmentTestManagedHandler(
|
||||||
|
t,
|
||||||
|
settings,
|
||||||
|
"PostToolUse",
|
||||||
|
fixture.auditWrapper,
|
||||||
|
)
|
||||||
|
server.Close()
|
||||||
|
guardExit, _, guardError := runAugmentCommand(
|
||||||
|
t,
|
||||||
|
guard["command"].(string),
|
||||||
|
fixture,
|
||||||
|
marshalAugmentPayload(t, augmentOfficialPayload("PreToolUse")),
|
||||||
|
)
|
||||||
|
auditExit, _, _ := runAugmentCommand(
|
||||||
|
t,
|
||||||
|
audit["command"].(string),
|
||||||
|
fixture,
|
||||||
|
marshalAugmentPayload(t, augmentOfficialPayload("PostToolUse")),
|
||||||
|
)
|
||||||
|
malformedExit, _, _ := runAugmentCommand(
|
||||||
|
t,
|
||||||
|
audit["command"].(string),
|
||||||
|
fixture,
|
||||||
|
[]byte("{ malformed"),
|
||||||
|
)
|
||||||
|
log, err := os.ReadFile(filepath.Join(fixture.agentDir, "error.log"))
|
||||||
|
if guardExit != 0 ||
|
||||||
|
!strings.Contains(guardError, "Failed to resolve agent status") ||
|
||||||
|
auditExit != 0 ||
|
||||||
|
malformedExit != 0 ||
|
||||||
|
err != nil ||
|
||||||
|
!strings.Contains(string(log), "invalid JSON") ||
|
||||||
|
!strings.Contains(strings.ToLower(string(log)), "fetch failed") {
|
||||||
|
t.Fatalf(
|
||||||
|
"fail-open results = (%d, %q), %d, %d, log=%q, err=%v",
|
||||||
|
guardExit,
|
||||||
|
guardError,
|
||||||
|
auditExit,
|
||||||
|
malformedExit,
|
||||||
|
log,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentStatusRequiresExactRuntimeFiles(t *testing.T) {
|
||||||
|
for _, name := range []string{
|
||||||
|
"config.json",
|
||||||
|
"private.key",
|
||||||
|
augmentGuardScript,
|
||||||
|
augmentAuditScript,
|
||||||
|
augmentGuardWrapperName(),
|
||||||
|
augmentAuditWrapperName(),
|
||||||
|
} {
|
||||||
|
t.Run("missing "+name, func(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
status, err := fixture.plugin.Status()
|
||||||
|
if err != nil || !status.Installed || !status.HookConfigured ||
|
||||||
|
!status.HookScriptExists {
|
||||||
|
t.Fatalf("installed status = %#v, %v", status, err)
|
||||||
|
}
|
||||||
|
path := filepath.Join(fixture.agentDir, name)
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
t.Fatalf("remove %s: %v", path, err)
|
||||||
|
}
|
||||||
|
status, err = fixture.plugin.Status()
|
||||||
|
if err != nil || status.Installed || !status.HookConfigured ||
|
||||||
|
status.HookScriptExists {
|
||||||
|
t.Fatalf("missing %s status = %#v, %v", path, status, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks for wrapper check: %v", err)
|
||||||
|
}
|
||||||
|
writeAugmentTestFile(
|
||||||
|
t,
|
||||||
|
fixture.auditWrapper,
|
||||||
|
[]byte("tampered wrapper\n"),
|
||||||
|
0700,
|
||||||
|
)
|
||||||
|
status, err := fixture.plugin.Status()
|
||||||
|
if err != nil || status.Installed || status.HookScriptExists {
|
||||||
|
t.Fatalf("tampered-wrapper status = %#v, %v", status, err)
|
||||||
|
}
|
||||||
|
fixture = prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks for key check: %v", err)
|
||||||
|
}
|
||||||
|
writeAugmentTestFile(t, fixture.privateKey, []byte("invalid"), 0600)
|
||||||
|
if _, err := fixture.plugin.Status(); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "canonical 32-byte") {
|
||||||
|
t.Fatalf("invalid-key status error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentStatusSurfacesInvalidRuntimeMetadata(t *testing.T) {
|
||||||
|
for _, testCase := range []struct{ name, source, want string }{
|
||||||
|
{"malformed", "{ malformed", "parse Elydora runtime config"},
|
||||||
|
{"duplicate", `{"agent_name":"augment","agent_name":"augment"}`, "duplicate key"},
|
||||||
|
{
|
||||||
|
"unsupported",
|
||||||
|
`{"org_id":"o","agent_id":"agent-1","kid":"k","base_url":"https://api.test","agent_name":"augment","extra":true}`,
|
||||||
|
"unsupported field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity",
|
||||||
|
`{"org_id":"o","agent_id":"other","kid":"k","base_url":"https://api.test","agent_name":"augment"}`,
|
||||||
|
"identity does not match",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
writeAugmentTestFile(
|
||||||
|
t,
|
||||||
|
fixture.runtimeConfig,
|
||||||
|
[]byte(testCase.source),
|
||||||
|
0600,
|
||||||
|
)
|
||||||
|
_, err := fixture.plugin.Status()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||||
|
t.Fatalf("status error = %v, want %q", err, testCase.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentStatusIgnoresIncompleteManagedHookPairs(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
settings := readAugmentTestObject(t, fixture.configPath)
|
||||||
|
delete(requireObject(t, settings["hooks"]), "PostToolUse")
|
||||||
|
writeAugmentTestObject(t, fixture.configPath, settings)
|
||||||
|
status, err := fixture.plugin.Status()
|
||||||
|
if err != nil || status.Installed || status.HookConfigured {
|
||||||
|
t.Fatalf("incomplete-pair status = %#v, %v", status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentRuntimeConfigOmitsEmptyToken(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
fixture.config.Token = ""
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
config := readAugmentTestObject(t, fixture.runtimeConfig)
|
||||||
|
if _, exists := config["token"]; exists {
|
||||||
|
t.Fatalf("empty token persisted: %#v", config)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func managedAugmentIDs(
|
||||||
|
groups []augmentGroup,
|
||||||
|
wrapperName string,
|
||||||
|
runtimeRoot string,
|
||||||
|
) map[string]string {
|
||||||
|
result := map[string]string{}
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, handler := range group.handlers {
|
||||||
|
agentID, managed := managedAugmentAgentID(
|
||||||
|
handler,
|
||||||
|
wrapperName,
|
||||||
|
runtimeRoot,
|
||||||
|
)
|
||||||
|
if !managed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := agentID
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
key = strings.ToLower(key)
|
||||||
|
}
|
||||||
|
result[key] = agentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentRuntimeContracts(
|
||||||
|
hooks augmentHooks,
|
||||||
|
runtimeRoot string,
|
||||||
|
) []augmentRuntimeContract {
|
||||||
|
guards := managedAugmentIDs(
|
||||||
|
hooks["PreToolUse"],
|
||||||
|
augmentGuardWrapperName(),
|
||||||
|
runtimeRoot,
|
||||||
|
)
|
||||||
|
audits := managedAugmentIDs(
|
||||||
|
hooks["PostToolUse"],
|
||||||
|
augmentAuditWrapperName(),
|
||||||
|
runtimeRoot,
|
||||||
|
)
|
||||||
|
keys := make([]string, 0, len(guards))
|
||||||
|
for key := range guards {
|
||||||
|
if _, exists := audits[key]; exists {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
contracts := make([]augmentRuntimeContract, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
agentID := guards[key]
|
||||||
|
agentDirectory := filepath.Join(runtimeRoot, agentID)
|
||||||
|
wrappers := resolveAugmentWrapperPaths(agentDirectory)
|
||||||
|
contracts = append(contracts, augmentRuntimeContract{
|
||||||
|
agentID: agentID,
|
||||||
|
guardPath: filepath.Join(agentDirectory, augmentGuardScript),
|
||||||
|
auditPath: filepath.Join(agentDirectory, augmentAuditScript),
|
||||||
|
guardWrapper: wrappers.guard,
|
||||||
|
auditWrapper: wrappers.audit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contracts
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAugmentRuntimeConfig(path string) (map[string]any, bool, error) {
|
||||||
|
snapshot, err := readManagedFile(
|
||||||
|
path,
|
||||||
|
"Elydora runtime config",
|
||||||
|
maxRuntimeConfigBytes,
|
||||||
|
)
|
||||||
|
if err != nil || snapshot == nil {
|
||||||
|
return nil, snapshot != nil, err
|
||||||
|
}
|
||||||
|
label := fmt.Sprintf("Elydora runtime config at %s", path)
|
||||||
|
config, err := decodeStrictJSONObject(snapshot.contents, label)
|
||||||
|
return config, true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireAugmentRuntimeString(
|
||||||
|
config map[string]any,
|
||||||
|
field string,
|
||||||
|
configPath string,
|
||||||
|
) (string, error) {
|
||||||
|
value, ok := config[field].(string)
|
||||||
|
if !ok || strings.TrimSpace(value) == "" {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"Elydora runtime config %s is invalid: %s",
|
||||||
|
field,
|
||||||
|
configPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAugmentRuntimeConfig(
|
||||||
|
config map[string]any,
|
||||||
|
expectedAgentID string,
|
||||||
|
configPath string,
|
||||||
|
) error {
|
||||||
|
supported := stringSet(
|
||||||
|
"org_id",
|
||||||
|
"agent_id",
|
||||||
|
"kid",
|
||||||
|
"base_url",
|
||||||
|
"token",
|
||||||
|
"agent_name",
|
||||||
|
)
|
||||||
|
fields := make([]string, 0, len(config))
|
||||||
|
for field := range config {
|
||||||
|
fields = append(fields, field)
|
||||||
|
}
|
||||||
|
sort.Strings(fields)
|
||||||
|
for _, field := range fields {
|
||||||
|
if _, ok := supported[field]; !ok {
|
||||||
|
return fmt.Errorf(
|
||||||
|
`Elydora runtime config has unsupported field %q: %s`,
|
||||||
|
field,
|
||||||
|
configPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := requireAugmentRuntimeString(config, "org_id", configPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := requireAugmentRuntimeString(config, "kid", configPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
agentID, err := requireAugmentRuntimeString(config, "agent_id", configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !sameAugmentAgentID(agentID, expectedAgentID) ||
|
||||||
|
config["agent_name"] != augmentAgentKey {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Elydora runtime identity does not match Auggie hooks: %s",
|
||||||
|
configPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if _, exists := config["token"]; exists {
|
||||||
|
if _, err := requireAugmentRuntimeString(config, "token", configPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseURL, err := requireAugmentRuntimeString(config, "base_url", configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateManagedBaseURL(baseURL); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Elydora runtime config base URL is invalid at %s: %w",
|
||||||
|
configPath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error {
|
||||||
|
runtimeRoot := filepath.Dir(agentDirectory)
|
||||||
|
rootExists, err := managedPhysicalDirectoryExists(
|
||||||
|
runtimeRoot,
|
||||||
|
"Elydora runtime directory",
|
||||||
|
)
|
||||||
|
if err != nil || !rootExists {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
directoryExists, err := managedPhysicalDirectoryExists(
|
||||||
|
agentDirectory,
|
||||||
|
"Elydora agent runtime directory",
|
||||||
|
)
|
||||||
|
if err != nil || !directoryExists {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configPath := filepath.Join(agentDirectory, "config.json")
|
||||||
|
config, configExists, err := readAugmentRuntimeConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
artifactExists := false
|
||||||
|
for _, item := range []struct {
|
||||||
|
path string
|
||||||
|
label string
|
||||||
|
limit int64
|
||||||
|
}{
|
||||||
|
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
|
||||||
|
{filepath.Join(agentDirectory, augmentGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
|
||||||
|
{filepath.Join(agentDirectory, augmentAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
|
||||||
|
{filepath.Join(agentDirectory, augmentGuardWrapperName()), "Auggie guard wrapper", maxManagedSourceBytes},
|
||||||
|
{filepath.Join(agentDirectory, augmentAuditWrapperName()), "Auggie audit wrapper", maxManagedSourceBytes},
|
||||||
|
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
|
||||||
|
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
|
||||||
|
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
|
||||||
|
} {
|
||||||
|
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"] != augmentAgentKey ||
|
||||||
|
!sameAugmentAgentID(configuredID, agentID) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Elydora runtime config identity does not match Auggie agent %s: %s",
|
||||||
|
agentID,
|
||||||
|
configPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentRuntimeContractExists(
|
||||||
|
contract augmentRuntimeContract,
|
||||||
|
runtimeRoot string,
|
||||||
|
nodePath string,
|
||||||
|
) (bool, error) {
|
||||||
|
agentDirectory := filepath.Dir(contract.guardPath)
|
||||||
|
wrappers := resolveAugmentWrapperPaths(agentDirectory)
|
||||||
|
if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) ||
|
||||||
|
!sameAugmentPath(
|
||||||
|
contract.auditPath,
|
||||||
|
filepath.Join(agentDirectory, augmentAuditScript),
|
||||||
|
) ||
|
||||||
|
!sameAugmentPath(contract.guardWrapper, wrappers.guard) ||
|
||||||
|
!sameAugmentPath(contract.auditWrapper, wrappers.audit) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
rootExists, err := managedPhysicalDirectoryExists(
|
||||||
|
runtimeRoot,
|
||||||
|
"Elydora runtime directory",
|
||||||
|
)
|
||||||
|
if err != nil || !rootExists {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
directoryExists, err := managedPhysicalDirectoryExists(
|
||||||
|
agentDirectory,
|
||||||
|
"Elydora agent runtime directory",
|
||||||
|
)
|
||||||
|
if err != nil || !directoryExists {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
configPath := filepath.Join(agentDirectory, "config.json")
|
||||||
|
keyPath := filepath.Join(agentDirectory, "private.key")
|
||||||
|
config, configExists, err := readAugmentRuntimeConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
guard, err := readManagedFile(
|
||||||
|
contract.guardPath,
|
||||||
|
"Elydora guard runtime",
|
||||||
|
maxManagedSourceBytes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
audit, err := readManagedFile(
|
||||||
|
contract.auditPath,
|
||||||
|
"Elydora audit runtime",
|
||||||
|
maxManagedSourceBytes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
guardWrapper, err := readManagedFile(
|
||||||
|
contract.guardWrapper,
|
||||||
|
"Auggie guard wrapper",
|
||||||
|
maxManagedSourceBytes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
auditWrapper, err := readManagedFile(
|
||||||
|
contract.auditWrapper,
|
||||||
|
"Auggie audit wrapper",
|
||||||
|
maxManagedSourceBytes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !configExists || key == nil || guard == nil || audit == nil ||
|
||||||
|
guardWrapper == nil || auditWrapper == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := validateAugmentRuntimeConfig(
|
||||||
|
config,
|
||||||
|
contract.agentID,
|
||||||
|
configPath,
|
||||||
|
); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := validateManagedPrivateKey(string(key.contents)); err != nil {
|
||||||
|
return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err)
|
||||||
|
}
|
||||||
|
return len(guard.contents) > 0 &&
|
||||||
|
len(audit.contents) > 0 &&
|
||||||
|
bytes.Equal(
|
||||||
|
guardWrapper.contents,
|
||||||
|
buildAugmentWrapper(nodePath, contract.guardPath),
|
||||||
|
) &&
|
||||||
|
bytes.Equal(
|
||||||
|
auditWrapper.contents,
|
||||||
|
buildAugmentWrapper(nodePath, contract.auditPath),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentRuntimeFilesExist(
|
||||||
|
contracts []augmentRuntimeContract,
|
||||||
|
runtimeRoot string,
|
||||||
|
) (bool, error) {
|
||||||
|
nodePath, err := resolveNodeRuntime()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, contract := range contracts {
|
||||||
|
exists, err := augmentRuntimeContractExists(contract, runtimeRoot, nodePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -10,16 +10,20 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAugmentRegistryAndFactory(t *testing.T) {
|
func TestAugmentRegistryFactoryAndGuardOwnership(t *testing.T) {
|
||||||
entry := SupportedAgents[augmentAgentKey]
|
entry := SupportedAgents[augmentAgentKey]
|
||||||
if entry.Name != "Augment Code CLI" ||
|
if entry.Name != "Augment Code CLI" ||
|
||||||
entry.ConfigDir != "~/.augment" ||
|
entry.ConfigDir != "~/.augment" ||
|
||||||
entry.ConfigFile != "settings.json" {
|
entry.ConfigFile != "settings.json" {
|
||||||
t.Fatalf("Auggie registry entry = %#v", entry)
|
t.Fatalf("Auggie registry entry = %#v", entry)
|
||||||
}
|
}
|
||||||
if _, ok := NewPlugin(augmentAgentKey).(*AugmentPlugin); !ok {
|
plugin, ok := NewPlugin(augmentAgentKey).(*AugmentPlugin)
|
||||||
|
if !ok {
|
||||||
t.Fatalf("Auggie plugin factory returned %T", NewPlugin(augmentAgentKey))
|
t.Fatalf("Auggie plugin factory returned %T", NewPlugin(augmentAgentKey))
|
||||||
}
|
}
|
||||||
|
if !plugin.ManagesGuardRuntime() {
|
||||||
|
t.Fatal("Auggie plugin must own guard runtime generation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAugmentGeneratedArgumentParsers(t *testing.T) {
|
func TestAugmentGeneratedArgumentParsers(t *testing.T) {
|
||||||
@@ -46,7 +50,8 @@ func TestAugmentInstallPreservesSettingsAndIsIdempotent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
fixture := prepareAugmentFixture(
|
fixture := prepareAugmentFixture(
|
||||||
t, augmentFixtureOptions{existingRaw: &existing},
|
t,
|
||||||
|
augmentFixtureOptions{existingRaw: &existing},
|
||||||
)
|
)
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
@@ -64,178 +69,77 @@ func TestAugmentInstallPreservesSettingsAndIsIdempotent(t *testing.T) {
|
|||||||
len(requireArray(t, hooks["PostToolUse"])) != 1 {
|
len(requireArray(t, hooks["PostToolUse"])) != 1 {
|
||||||
t.Fatalf("unexpected hooks after repeat install: %#v", hooks)
|
t.Fatalf("unexpected hooks after repeat install: %#v", hooks)
|
||||||
}
|
}
|
||||||
preGroups := requireArray(t, hooks["PreToolUse"])
|
managedGroup := requireObject(
|
||||||
managedGroup := requireObject(t, preGroups[len(preGroups)-1])
|
t,
|
||||||
|
requireArray(t, hooks["PreToolUse"])[1],
|
||||||
|
)
|
||||||
if managedGroup["matcher"] != ".*" {
|
if managedGroup["matcher"] != ".*" {
|
||||||
t.Fatalf("managed matcher = %#v", managedGroup["matcher"])
|
t.Fatalf("managed matcher = %#v", managedGroup["matcher"])
|
||||||
}
|
}
|
||||||
guard := augmentTestManagedHandler(
|
guard := augmentTestManagedHandler(
|
||||||
t, settings, "PreToolUse", fixture.guardWrapper,
|
t,
|
||||||
|
settings,
|
||||||
|
"PreToolUse",
|
||||||
|
fixture.guardWrapper,
|
||||||
)
|
)
|
||||||
audit := augmentTestManagedHandler(
|
audit := augmentTestManagedHandler(
|
||||||
t, settings, "PostToolUse", fixture.auditWrapper,
|
t,
|
||||||
|
settings,
|
||||||
|
"PostToolUse",
|
||||||
|
fixture.auditWrapper,
|
||||||
)
|
)
|
||||||
for _, handler := range []map[string]any{guard, audit} {
|
for _, handler := range []map[string]any{guard, audit} {
|
||||||
wantKeys := map[string]struct{}{"type": {}, "command": {}, "timeout": {}}
|
want := map[string]any{
|
||||||
if len(handler) != len(wantKeys) {
|
"type": "command", "command": handler["command"],
|
||||||
t.Fatalf("handler has extra fields: %#v", handler)
|
"timeout": augmentHookTimeout,
|
||||||
}
|
}
|
||||||
for key := range handler {
|
if !reflect.DeepEqual(handler, want) {
|
||||||
if _, exists := wantKeys[key]; !exists {
|
t.Fatalf("managed handler = %#v", handler)
|
||||||
t.Fatalf("handler has unexpected field %q", key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if handler["type"] != "command" || handler["timeout"] != augmentHookTimeout {
|
nodePath, err := resolveNodeRuntime()
|
||||||
t.Fatalf("unexpected handler: %#v", handler)
|
if err != nil {
|
||||||
|
t.Fatalf("resolve Node.js runtime: %v", err)
|
||||||
}
|
}
|
||||||
|
for _, item := range []struct {
|
||||||
|
path, script string
|
||||||
|
}{
|
||||||
|
{fixture.guardWrapper, fixture.guardPath},
|
||||||
|
{fixture.auditWrapper, fixture.hookPath},
|
||||||
|
} {
|
||||||
|
actual, err := os.ReadFile(item.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read wrapper %s: %v", item.path, err)
|
||||||
}
|
}
|
||||||
guardWrapper, err := os.ReadFile(fixture.guardWrapper)
|
want := buildAugmentWrapper(nodePath, item.script)
|
||||||
if err != nil || !strings.Contains(string(guardWrapper), augmentGuardScript) {
|
if string(actual) != string(want) {
|
||||||
t.Fatalf("guard wrapper = %q, %v", guardWrapper, err)
|
t.Fatalf("wrapper %s = %q, want %q", item.path, actual, want)
|
||||||
}
|
|
||||||
auditWrapper, err := os.ReadFile(fixture.auditWrapper)
|
|
||||||
if err != nil || !strings.Contains(string(auditWrapper), augmentAuditScript) {
|
|
||||||
t.Fatalf("audit wrapper = %q, %v", auditWrapper, err)
|
|
||||||
}
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
if !strings.HasPrefix(string(guardWrapper), "@echo off\r\n") ||
|
|
||||||
!strings.Contains(string(guardWrapper), "exit /b %errorlevel%") {
|
|
||||||
t.Fatalf("invalid Windows wrapper: %q", guardWrapper)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info, statErr := os.Stat(fixture.guardWrapper)
|
|
||||||
if statErr != nil || info.Mode().Perm()&0100 == 0 ||
|
|
||||||
!strings.HasPrefix(string(guardWrapper), "#!/bin/sh\nexec ") {
|
|
||||||
t.Fatalf("invalid POSIX wrapper: %q, %#v, %v", guardWrapper, info, statErr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runtimeConfig := readAugmentTestObject(t, fixture.runtimeConfig)
|
runtimeConfig := readAugmentTestObject(t, fixture.runtimeConfig)
|
||||||
if runtimeConfig["agent_name"] != augmentAgentKey {
|
if runtimeConfig["agent_name"] != augmentAgentKey ||
|
||||||
t.Fatalf("runtime agent name = %#v", runtimeConfig["agent_name"])
|
runtimeConfig["agent_id"] != augmentTestAgentID {
|
||||||
|
t.Fatalf("runtime identity = %#v", runtimeConfig)
|
||||||
|
}
|
||||||
|
auditSource, err := os.ReadFile(fixture.hookPath)
|
||||||
|
if err != nil || !strings.Contains(string(auditSource), "const NATIVE_PAYLOAD = true;") {
|
||||||
|
t.Fatalf("audit runtime native payload = %v, %v", err, strings.Contains(string(auditSource), "const NATIVE_PAYLOAD = true;"))
|
||||||
}
|
}
|
||||||
for _, path := range []string{
|
for _, path := range []string{
|
||||||
filepath.Join(fixture.workspaceDir, ".augment", "settings.json"),
|
filepath.Join(fixture.projectDir, ".augment", "settings.json"),
|
||||||
filepath.Join(fixture.workspaceDir, ".augment", "settings.local.json"),
|
filepath.Join(fixture.projectDir, ".augment", "settings.local.json"),
|
||||||
} {
|
} {
|
||||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
|
||||||
t.Fatalf("workspace settings written at %s: %v", path, err)
|
t.Fatalf("workspace settings written at %s: %v", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAugmentWrappersBlockAndForwardPayloadByteForByte(t *testing.T) {
|
|
||||||
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
|
||||||
}
|
|
||||||
capturePath := filepath.Join(t.TempDir(), "captured-event.json")
|
|
||||||
captureJSON, err := json.Marshal(capturePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal capture path: %v", err)
|
|
||||||
}
|
|
||||||
captureScript := "const fs = require('node:fs'); const chunks = []; " +
|
|
||||||
"process.stdin.on('data', chunk => chunks.push(chunk)); " +
|
|
||||||
"process.stdin.on('end', () => fs.writeFileSync(" +
|
|
||||||
string(captureJSON) + ", Buffer.concat(chunks)));\n"
|
|
||||||
if err := os.WriteFile(fixture.hookPath, []byte(captureScript), 0700); err != nil {
|
|
||||||
t.Fatalf("write capture hook: %v", err)
|
|
||||||
}
|
|
||||||
settings := readAugmentTestObject(t, fixture.configPath)
|
|
||||||
prePayload := `{"hook_event_name":"PreToolUse","conversation_id":"conversation-1","workspace_roots":["workspace"],"tool_name":"launch-process","tool_input":{"command":"echo test"},"is_mcp_tool":false}`
|
|
||||||
guard := augmentTestManagedHandler(
|
|
||||||
t, settings, "PreToolUse", fixture.guardWrapper,
|
|
||||||
)
|
|
||||||
exitCode, stderr := runAugmentCommand(
|
|
||||||
t, guard["command"].(string), fixture.homeDir, prePayload,
|
|
||||||
)
|
|
||||||
if exitCode != 2 || !strings.Contains(stderr, "Agent is frozen by Elydora") {
|
|
||||||
t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr)
|
|
||||||
}
|
|
||||||
postPayload := `{"hook_event_name":"PostToolUse","conversation_id":"conversation-1","workspace_roots":["workspace"],"tool_name":"launch-process","tool_input":{"command":"echo test"},"tool_output":"test","is_mcp_tool":false}`
|
|
||||||
audit := augmentTestManagedHandler(
|
|
||||||
t, settings, "PostToolUse", fixture.auditWrapper,
|
|
||||||
)
|
|
||||||
exitCode, stderr = runAugmentCommand(
|
|
||||||
t, audit["command"].(string), fixture.homeDir, postPayload,
|
|
||||||
)
|
|
||||||
if exitCode != 0 {
|
|
||||||
t.Fatalf("audit exit = %d, stderr = %q", exitCode, stderr)
|
|
||||||
}
|
|
||||||
captured, err := os.ReadFile(capturePath)
|
|
||||||
if err != nil || string(captured) != postPayload {
|
|
||||||
t.Fatalf("captured payload = %q, %v", captured, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAugmentStatusRequiresPairCoreRuntimesAndWrappers(t *testing.T) {
|
|
||||||
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
|
||||||
}
|
|
||||||
status, err := fixture.plugin.Status()
|
|
||||||
if err != nil || !status.Installed || !status.HookConfigured ||
|
|
||||||
!status.HookScriptExists {
|
|
||||||
t.Fatalf("installed status = %#v, %v", status, err)
|
|
||||||
}
|
|
||||||
if err := os.Remove(fixture.guardWrapper); err != nil {
|
|
||||||
t.Fatalf("remove guard wrapper: %v", err)
|
|
||||||
}
|
|
||||||
status, err = fixture.plugin.Status()
|
|
||||||
if err != nil || status.Installed || !status.HookConfigured ||
|
|
||||||
status.HookScriptExists {
|
|
||||||
t.Fatalf("missing-wrapper status = %#v, %v", status, err)
|
|
||||||
}
|
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
||||||
t.Fatalf("restore Auggie hooks: %v", err)
|
|
||||||
}
|
|
||||||
if err := os.Remove(fixture.hookPath); err != nil {
|
|
||||||
t.Fatalf("remove hook runtime: %v", err)
|
|
||||||
}
|
|
||||||
status, err = fixture.plugin.Status()
|
|
||||||
if err != nil || status.Installed || status.HookScriptExists {
|
|
||||||
t.Fatalf("missing-runtime status = %#v, %v", status, err)
|
|
||||||
}
|
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
||||||
t.Fatalf("restore Auggie hooks: %v", err)
|
|
||||||
}
|
|
||||||
settings := readAugmentTestObject(t, fixture.configPath)
|
|
||||||
delete(requireObject(t, settings["hooks"]), "PostToolUse")
|
|
||||||
writeAugmentTestObject(t, fixture.configPath, settings)
|
|
||||||
status, err = fixture.plugin.Status()
|
|
||||||
if err != nil || status.Installed || status.HookConfigured {
|
|
||||||
t.Fatalf("incomplete-pair status = %#v, %v", status, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAugmentStatusSkipsIncompleteEarlierContract(t *testing.T) {
|
|
||||||
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
|
||||||
}
|
|
||||||
settings := readAugmentTestObject(t, fixture.configPath)
|
|
||||||
hooks := requireObject(t, settings["hooks"])
|
|
||||||
for _, item := range []struct{ event, wrapper string }{
|
|
||||||
{"PreToolUse", augmentGuardWrapperName()},
|
|
||||||
{"PostToolUse", augmentAuditWrapperName()},
|
|
||||||
} {
|
|
||||||
hooks[item.event] = append(
|
|
||||||
requireArray(t, hooks[item.event]),
|
|
||||||
map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join(
|
|
||||||
filepath.Dir(fixture.agentDir), "agent-0", item.wrapper,
|
|
||||||
))}},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
writeAugmentTestObject(t, fixture.configPath, settings)
|
|
||||||
status, err := fixture.plugin.Status()
|
|
||||||
if err != nil || !status.Installed {
|
|
||||||
t.Fatalf("status with earlier incomplete contract = %#v, %v", status, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) {
|
func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) {
|
||||||
existing := `{"owner":"user","hooks":{"Notification":[]}}`
|
existing := `{"owner":"user","hooks":{"Notification":[]}}`
|
||||||
fixture := prepareAugmentFixture(
|
fixture := prepareAugmentFixture(
|
||||||
t, augmentFixtureOptions{existingRaw: &existing},
|
t,
|
||||||
|
augmentFixtureOptions{existingRaw: &existing},
|
||||||
)
|
)
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
@@ -251,12 +155,15 @@ func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) {
|
|||||||
requireArray(t, managedGroup["hooks"]),
|
requireArray(t, managedGroup["hooks"]),
|
||||||
map[string]any{"type": "command", "command": "user-command"},
|
map[string]any{"type": "command", "command": "user-command"},
|
||||||
)
|
)
|
||||||
preGroups = append(preGroups,
|
preGroups = append(
|
||||||
|
preGroups,
|
||||||
map[string]any{"hooks": []any{buildAugmentHandler(
|
map[string]any{"hooks": []any{buildAugmentHandler(
|
||||||
fixture.guardWrapper + ".backup",
|
fixture.guardWrapper + ".backup",
|
||||||
)}},
|
)}},
|
||||||
map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join(
|
map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join(
|
||||||
filepath.Dir(fixture.agentDir), "agent-10", augmentGuardWrapperName(),
|
filepath.Dir(fixture.agentDir),
|
||||||
|
"agent-10",
|
||||||
|
augmentGuardWrapperName(),
|
||||||
))}},
|
))}},
|
||||||
)
|
)
|
||||||
hooks["PreToolUse"] = preGroups
|
hooks["PreToolUse"] = preGroups
|
||||||
@@ -274,7 +181,10 @@ func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) {
|
|||||||
len(requireArray(t, remainingHooks["PreToolUse"])) != 4 {
|
len(requireArray(t, remainingHooks["PreToolUse"])) != 4 {
|
||||||
t.Fatalf("user settings changed: %#v", remaining)
|
t.Fatalf("user settings changed: %#v", remaining)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(requireArray(t, remainingHooks["Notification"]), []any{}) {
|
if !reflect.DeepEqual(
|
||||||
|
requireArray(t, remainingHooks["Notification"]),
|
||||||
|
[]any{},
|
||||||
|
) {
|
||||||
t.Fatalf("empty notification changed: %#v", remainingHooks["Notification"])
|
t.Fatalf("empty notification changed: %#v", remainingHooks["Notification"])
|
||||||
}
|
}
|
||||||
raw, _ := os.ReadFile(fixture.configPath)
|
raw, _ := os.ReadFile(fixture.configPath)
|
||||||
@@ -287,7 +197,7 @@ func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAugmentInstallReplacesStaleAndPreservesEmptyGroups(t *testing.T) {
|
func TestAugmentInstallReplacesStaleHooksAndPreservesEmptyGroups(t *testing.T) {
|
||||||
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
@@ -305,7 +215,9 @@ func TestAugmentInstallReplacesStaleAndPreservesEmptyGroups(t *testing.T) {
|
|||||||
hooks[item.event] = append(
|
hooks[item.event] = append(
|
||||||
requireArray(t, hooks[item.event]),
|
requireArray(t, hooks[item.event]),
|
||||||
map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join(
|
map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join(
|
||||||
filepath.Dir(fixture.agentDir), "agent-old", item.wrapper,
|
filepath.Dir(fixture.agentDir),
|
||||||
|
"agent-old",
|
||||||
|
item.wrapper,
|
||||||
))}},
|
))}},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -331,14 +243,17 @@ func TestAugmentUninstallRemovesOwnedSettings(t *testing.T) {
|
|||||||
if err := fixture.plugin.Uninstall(augmentTestAgentID); err != nil {
|
if err := fixture.plugin.Uninstall(augmentTestAgentID); err != nil {
|
||||||
t.Fatalf("uninstall Auggie hooks: %v", err)
|
t.Fatalf("uninstall Auggie hooks: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) {
|
if _, err := os.Stat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
|
||||||
t.Fatalf("owned Auggie settings remain: %v", err)
|
t.Fatalf("owned Auggie settings remain: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAugmentMalformedSettingsPreventRuntimeWrites(t *testing.T) {
|
func TestAugmentMalformedSettingsPreventRuntimeWrites(t *testing.T) {
|
||||||
for _, testCase := range []struct{ name, raw, want string }{
|
for _, testCase := range []struct{ name, raw, want string }{
|
||||||
{"malformed", "{ malformed", "parse Auggie settings"},
|
{"malformed", "{ malformed", "parse Auggie user settings"},
|
||||||
|
{"duplicate", `{"hooks":{},"hooks":{}}`, "duplicate key"},
|
||||||
|
{"comments", "{\"hooks\":{} // comment\n}", "invalid character"},
|
||||||
|
{"trailing-comma", `{"hooks":{},}`, "invalid character"},
|
||||||
{"null-root", "null", "must contain a JSON object"},
|
{"null-root", "null", "must contain a JSON object"},
|
||||||
{"null-hooks", `{"hooks":null}`, `field "hooks" must be an object`},
|
{"null-hooks", `{"hooks":null}`, `field "hooks" must be an object`},
|
||||||
{"unknown-event", `{"hooks":{"UnknownEvent":[]}}`, "unsupported event"},
|
{"unknown-event", `{"hooks":{"UnknownEvent":[]}}`, "unsupported event"},
|
||||||
@@ -356,7 +271,8 @@ func TestAugmentMalformedSettingsPreventRuntimeWrites(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
fixture := prepareAugmentFixture(
|
fixture := prepareAugmentFixture(
|
||||||
t, augmentFixtureOptions{existingRaw: &testCase.raw},
|
t,
|
||||||
|
augmentFixtureOptions{existingRaw: &testCase.raw},
|
||||||
)
|
)
|
||||||
err := fixture.plugin.Install(fixture.config)
|
err := fixture.plugin.Install(fixture.config)
|
||||||
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||||
@@ -366,75 +282,7 @@ func TestAugmentMalformedSettingsPreventRuntimeWrites(t *testing.T) {
|
|||||||
if readErr != nil || string(raw) != testCase.raw {
|
if readErr != nil || string(raw) != testCase.raw {
|
||||||
t.Fatalf("original settings changed: %q, %v", raw, readErr)
|
t.Fatalf("original settings changed: %q, %v", raw, readErr)
|
||||||
}
|
}
|
||||||
for _, path := range []string{
|
assertNoAugmentRuntimeWrites(t, fixture)
|
||||||
fixture.hookPath,
|
|
||||||
fixture.runtimeConfig,
|
|
||||||
fixture.privateKey,
|
|
||||||
fixture.guardWrapper,
|
|
||||||
fixture.auditWrapper,
|
|
||||||
} {
|
|
||||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("runtime write occurred at %s: %v", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAugmentMissingGuardPreventsWrites(t *testing.T) {
|
|
||||||
fixture := prepareAugmentFixture(
|
|
||||||
t, augmentFixtureOptions{withoutGuard: true},
|
|
||||||
)
|
|
||||||
err := fixture.plugin.Install(fixture.config)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "guard runtime is missing") {
|
|
||||||
t.Fatalf("install error = %v", err)
|
|
||||||
}
|
|
||||||
for _, path := range []string{
|
|
||||||
fixture.configPath,
|
|
||||||
fixture.hookPath,
|
|
||||||
fixture.runtimeConfig,
|
|
||||||
fixture.privateKey,
|
|
||||||
fixture.guardWrapper,
|
|
||||||
fixture.auditWrapper,
|
|
||||||
} {
|
|
||||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("write occurred at %s: %v", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAugmentStatusSurfacesMalformedRuntimeMetadata(t *testing.T) {
|
|
||||||
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(
|
|
||||||
fixture.runtimeConfig, []byte("{ malformed"), 0600,
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("corrupt runtime config: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := fixture.plugin.Status(); err == nil ||
|
|
||||||
!strings.Contains(err.Error(), "parse Elydora runtime config") {
|
|
||||||
t.Fatalf("status error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAugmentAtomicWritesLeaveNoTemporaryFiles(t *testing.T) {
|
|
||||||
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
|
||||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
|
||||||
t.Fatalf("install Auggie hooks: %v", err)
|
|
||||||
}
|
|
||||||
for _, directory := range []string{
|
|
||||||
fixture.agentDir, filepath.Dir(fixture.configPath),
|
|
||||||
} {
|
|
||||||
entries, err := os.ReadDir(directory)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read directory %s: %v", directory, err)
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
if strings.HasSuffix(entry.Name(), ".tmp") {
|
|
||||||
t.Fatalf("temporary file remains: %s", entry.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,18 +11,20 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
const augmentTestAgentID = "agent-1"
|
const (
|
||||||
|
augmentTestAgentID = "agent-1"
|
||||||
|
augmentPrivateKey = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc"
|
||||||
|
)
|
||||||
|
|
||||||
type augmentFixtureOptions struct {
|
type augmentFixtureOptions struct {
|
||||||
existingRaw *string
|
existingRaw *string
|
||||||
withoutGuard bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type augmentFixture struct {
|
type augmentFixture struct {
|
||||||
plugin *AugmentPlugin
|
plugin *AugmentPlugin
|
||||||
config InstallConfig
|
config InstallConfig
|
||||||
homeDir string
|
homeDir string
|
||||||
workspaceDir string
|
projectDir string
|
||||||
agentDir string
|
agentDir string
|
||||||
configPath string
|
configPath string
|
||||||
guardPath string
|
guardPath string
|
||||||
@@ -42,49 +44,34 @@ func prepareAugmentFixture(
|
|||||||
options augmentFixtureOptions,
|
options augmentFixtureOptions,
|
||||||
) *augmentFixture {
|
) *augmentFixture {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
homeDir := filepath.Join(t.TempDir(), "home with spaces and 'quote")
|
root := t.TempDir()
|
||||||
workspaceDir := filepath.Join(homeDir, "workspace")
|
homeDir := filepath.Join(root, "home with spaces and 'quote")
|
||||||
|
projectDir := filepath.Join(root, "project with spaces")
|
||||||
|
if err := os.MkdirAll(projectDir, 0700); err != nil {
|
||||||
|
t.Fatalf("create project directory: %v", err)
|
||||||
|
}
|
||||||
|
t.Setenv("HOME", homeDir)
|
||||||
|
t.Setenv("USERPROFILE", homeDir)
|
||||||
agentDir := filepath.Join(homeDir, ".elydora", augmentTestAgentID)
|
agentDir := filepath.Join(homeDir, ".elydora", augmentTestAgentID)
|
||||||
configPath := filepath.Join(homeDir, ".augment", "settings.json")
|
configPath := filepath.Join(homeDir, ".augment", "settings.json")
|
||||||
guardPath := filepath.Join(agentDir, augmentGuardScript)
|
|
||||||
if err := os.MkdirAll(workspaceDir, 0755); err != nil {
|
|
||||||
t.Fatalf("create workspace: %v", err)
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(agentDir, 0755); err != nil {
|
|
||||||
t.Fatalf("create agent directory: %v", err)
|
|
||||||
}
|
|
||||||
if !options.withoutGuard {
|
|
||||||
source := "process.stdin.resume(); process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n"
|
|
||||||
if err := os.WriteFile(guardPath, []byte(source), 0700); err != nil {
|
|
||||||
t.Fatalf("write guard runtime: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.existingRaw != nil {
|
if options.existingRaw != nil {
|
||||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
|
||||||
t.Fatalf("create Auggie config directory: %v", err)
|
t.Fatalf("create Auggie config directory: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(configPath, []byte(*options.existingRaw), 0600); err != nil {
|
if err := os.WriteFile(configPath, []byte(*options.existingRaw), 0600); err != nil {
|
||||||
t.Fatalf("write Auggie settings: %v", err)
|
t.Fatalf("write Auggie settings: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Setenv("HOME", homeDir)
|
guardPath := filepath.Join(agentDir, augmentGuardScript)
|
||||||
t.Setenv("USERPROFILE", homeDir)
|
|
||||||
return &augmentFixture{
|
return &augmentFixture{
|
||||||
plugin: &AugmentPlugin{},
|
plugin: &AugmentPlugin{},
|
||||||
homeDir: homeDir,
|
|
||||||
workspaceDir: workspaceDir,
|
|
||||||
agentDir: agentDir,
|
|
||||||
config: InstallConfig{
|
config: InstallConfig{
|
||||||
AgentName: augmentAgentKey,
|
AgentName: augmentAgentKey, OrgID: "org-1", AgentID: augmentTestAgentID,
|
||||||
OrgID: "org-1",
|
PrivateKey: augmentPrivateKey, KID: "kid-1", Token: "token-1",
|
||||||
AgentID: augmentTestAgentID,
|
BaseURL: "https://api.elydora.test", GuardScriptPath: guardPath,
|
||||||
PrivateKey: "test-key",
|
|
||||||
KID: "kid-1",
|
|
||||||
BaseURL: "https://api.elydora.test",
|
|
||||||
GuardScriptPath: guardPath,
|
|
||||||
},
|
},
|
||||||
configPath: configPath,
|
homeDir: homeDir, projectDir: projectDir, agentDir: agentDir,
|
||||||
guardPath: guardPath,
|
configPath: configPath, guardPath: guardPath,
|
||||||
hookPath: filepath.Join(agentDir, augmentAuditScript),
|
hookPath: filepath.Join(agentDir, augmentAuditScript),
|
||||||
guardWrapper: filepath.Join(agentDir, augmentGuardWrapperName()),
|
guardWrapper: filepath.Join(agentDir, augmentGuardWrapperName()),
|
||||||
auditWrapper: filepath.Join(agentDir, augmentAuditWrapperName()),
|
auditWrapper: filepath.Join(agentDir, augmentAuditWrapperName()),
|
||||||
@@ -108,19 +95,34 @@ func readAugmentTestObject(t *testing.T, path string) map[string]any {
|
|||||||
|
|
||||||
func writeAugmentTestObject(t *testing.T, path string, value map[string]any) {
|
func writeAugmentTestObject(t *testing.T, path string, value map[string]any) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
encoded, err := json.Marshal(value)
|
encoded, err := json.MarshalIndent(value, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal Auggie settings: %v", err)
|
t.Fatalf("marshal Auggie settings: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, encoded, 0600); err != nil {
|
encoded = append(encoded, '\n')
|
||||||
t.Fatalf("write Auggie settings: %v", err)
|
writeAugmentTestFile(t, path, encoded, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAugmentTestFile(
|
||||||
|
t *testing.T,
|
||||||
|
path string,
|
||||||
|
contents []byte,
|
||||||
|
mode os.FileMode,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||||
|
t.Fatalf("create parent directory for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, contents, mode); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func augmentTestManagedHandler(
|
func augmentTestManagedHandler(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
settings map[string]any,
|
settings map[string]any,
|
||||||
event, wrapperPath string,
|
event string,
|
||||||
|
wrapperPath string,
|
||||||
) map[string]any {
|
) map[string]any {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
expected := buildAugmentCommand(wrapperPath)
|
expected := buildAugmentCommand(wrapperPath)
|
||||||
@@ -140,8 +142,10 @@ func augmentTestManagedHandler(
|
|||||||
|
|
||||||
func runAugmentCommand(
|
func runAugmentCommand(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
command, homeDir, payload string,
|
command string,
|
||||||
) (int, string) {
|
fixture *augmentFixture,
|
||||||
|
payload []byte,
|
||||||
|
) (int, string, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var process *exec.Cmd
|
var process *exec.Cmd
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@@ -154,20 +158,67 @@ func runAugmentCommand(
|
|||||||
} else {
|
} else {
|
||||||
process = exec.Command("sh", "-c", command)
|
process = exec.Command("sh", "-c", command)
|
||||||
}
|
}
|
||||||
|
process.Dir = fixture.projectDir
|
||||||
process.Env = append(
|
process.Env = append(
|
||||||
os.Environ(), "HOME="+homeDir, "USERPROFILE="+homeDir,
|
os.Environ(),
|
||||||
|
"HOME="+fixture.homeDir,
|
||||||
|
"USERPROFILE="+fixture.homeDir,
|
||||||
)
|
)
|
||||||
process.Stdin = bytes.NewBufferString(payload)
|
process.Stdin = bytes.NewReader(payload)
|
||||||
var stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
|
process.Stdout = &stdout
|
||||||
process.Stderr = &stderr
|
process.Stderr = &stderr
|
||||||
err := process.Run()
|
err := process.Run()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return 0, stderr.String()
|
return 0, stdout.String(), stderr.String()
|
||||||
}
|
}
|
||||||
var exitError *exec.ExitError
|
var exitError *exec.ExitError
|
||||||
if errors.As(err, &exitError) {
|
if errors.As(err, &exitError) {
|
||||||
return exitError.ExitCode(), stderr.String()
|
return exitError.ExitCode(), stdout.String(), stderr.String()
|
||||||
}
|
}
|
||||||
t.Fatalf("run Auggie hook command: %v", err)
|
t.Fatalf("run Auggie hook command: %v", err)
|
||||||
return -1, stderr.String()
|
return -1, stdout.String(), stderr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNoAugmentRuntimeWrites(t *testing.T, fixture *augmentFixture) {
|
||||||
|
t.Helper()
|
||||||
|
for _, path := range []string{
|
||||||
|
fixture.runtimeConfig,
|
||||||
|
fixture.privateKey,
|
||||||
|
fixture.guardPath,
|
||||||
|
fixture.hookPath,
|
||||||
|
fixture.guardWrapper,
|
||||||
|
fixture.auditWrapper,
|
||||||
|
} {
|
||||||
|
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("runtime file exists at %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNoAugmentTransactionArtifacts(t *testing.T, root string) {
|
||||||
|
t.Helper()
|
||||||
|
if _, err := os.Lstat(root); errors.Is(err, os.ErrNotExist) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if filepath.Ext(name) == ".tmp" || filepath.Ext(name) == ".rollback" {
|
||||||
|
t.Errorf("transaction artifact remains at %s", path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("walk Auggie fixture: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentSymlinkOrSkip(t *testing.T, target, link string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.Symlink(target, link); err != nil {
|
||||||
|
t.Skipf("symbolic links unavailable: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareAugmentTestInstallation(
|
||||||
|
t *testing.T,
|
||||||
|
fixture *augmentFixture,
|
||||||
|
) ([]*fileChange, *augmentRuntimePaths, *augmentDocument) {
|
||||||
|
t.Helper()
|
||||||
|
document, err := readAugmentDocument()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read Auggie document: %v", err)
|
||||||
|
}
|
||||||
|
paths, nodePath, err := preflightAugmentInstallation(fixture.config, document)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("preflight Auggie installation: %v", err)
|
||||||
|
}
|
||||||
|
hooks, _ := removeManagedAugmentHooks(document.hooks, "", paths.runtimeRoot)
|
||||||
|
hooks["PreToolUse"] = append(
|
||||||
|
hooks["PreToolUse"],
|
||||||
|
buildAugmentGroup(buildAugmentHandler(paths.guardWrapperPath)),
|
||||||
|
)
|
||||||
|
hooks["PostToolUse"] = append(
|
||||||
|
hooks["PostToolUse"],
|
||||||
|
buildAugmentGroup(buildAugmentHandler(paths.auditWrapperPath)),
|
||||||
|
)
|
||||||
|
rendered, err := renderAugmentDocument(document, hooks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render Auggie document: %v", err)
|
||||||
|
}
|
||||||
|
changes, err := prepareAugmentInstallationChanges(
|
||||||
|
fixture.config,
|
||||||
|
paths,
|
||||||
|
nodePath,
|
||||||
|
rendered,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare Auggie installation: %v", err)
|
||||||
|
}
|
||||||
|
return changes, paths, document
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentInstallRollsBackAllSevenFiles(t *testing.T) {
|
||||||
|
original := "{\"telemetryEnabled\":true}\n"
|
||||||
|
fixture := prepareAugmentFixture(
|
||||||
|
t,
|
||||||
|
augmentFixtureOptions{existingRaw: &original},
|
||||||
|
)
|
||||||
|
failed := false
|
||||||
|
fixture.plugin.rename = func(source, destination string) error {
|
||||||
|
if sameAugmentPath(destination, fixture.configPath) &&
|
||||||
|
strings.HasSuffix(source, ".tmp") &&
|
||||||
|
!failed {
|
||||||
|
failed = true
|
||||||
|
return errors.New("injected Auggie settings failure")
|
||||||
|
}
|
||||||
|
return os.Rename(source, destination)
|
||||||
|
}
|
||||||
|
err := fixture.plugin.Install(fixture.config)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "injected Auggie settings failure") {
|
||||||
|
t.Fatalf("install error = %v", err)
|
||||||
|
}
|
||||||
|
raw, readErr := os.ReadFile(fixture.configPath)
|
||||||
|
if readErr != nil || string(raw) != original {
|
||||||
|
t.Fatalf("original settings changed: %q, %v", raw, readErr)
|
||||||
|
}
|
||||||
|
assertNoAugmentRuntimeWrites(t, fixture)
|
||||||
|
assertNoAugmentTransactionArtifacts(t, fixture.homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparedAugmentInstallRejectsConcurrentSettingsChange(t *testing.T) {
|
||||||
|
original := `{"telemetryEnabled":true}`
|
||||||
|
fixture := prepareAugmentFixture(
|
||||||
|
t,
|
||||||
|
augmentFixtureOptions{existingRaw: &original},
|
||||||
|
)
|
||||||
|
changes, paths, document := prepareAugmentTestInstallation(t, fixture)
|
||||||
|
concurrent := []byte(
|
||||||
|
"{\"telemetryEnabled\":false,\"hooks\":{\"Notification\":[]}}\n",
|
||||||
|
)
|
||||||
|
if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil {
|
||||||
|
t.Fatalf("write concurrent settings: %v", err)
|
||||||
|
}
|
||||||
|
err := writeAugmentChanges(
|
||||||
|
changes,
|
||||||
|
"Install Augment Code CLI hooks",
|
||||||
|
nil,
|
||||||
|
paths.runtimeRoot,
|
||||||
|
paths.agentDirectory,
|
||||||
|
filepath.Dir(document.configPath),
|
||||||
|
)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
|
||||||
|
t.Fatalf("prepared install error = %v", err)
|
||||||
|
}
|
||||||
|
actual, readErr := os.ReadFile(fixture.configPath)
|
||||||
|
if readErr != nil || string(actual) != string(concurrent) {
|
||||||
|
t.Fatalf("concurrent settings changed: %q, %v", actual, readErr)
|
||||||
|
}
|
||||||
|
assertNoAugmentRuntimeWrites(t, fixture)
|
||||||
|
assertNoAugmentTransactionArtifacts(t, fixture.homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentInstallDetectsConcurrentSettingsIdentityReplacement(t *testing.T) {
|
||||||
|
source := []byte(`{"owner":"original"}`)
|
||||||
|
fixture := prepareAugmentFixture(
|
||||||
|
t,
|
||||||
|
augmentFixtureOptions{existingRaw: augmentString(string(source))},
|
||||||
|
)
|
||||||
|
mutated := false
|
||||||
|
fixture.plugin.rename = func(stagedPath, destination string) error {
|
||||||
|
if !mutated && strings.HasSuffix(stagedPath, ".tmp") {
|
||||||
|
mutated = true
|
||||||
|
external := fixture.configPath + ".external"
|
||||||
|
if err := os.Rename(fixture.configPath, external); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(fixture.configPath, source, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.Rename(stagedPath, destination)
|
||||||
|
}
|
||||||
|
err := fixture.plugin.Install(fixture.config)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
|
||||||
|
t.Fatalf("concurrent identity error = %v", err)
|
||||||
|
}
|
||||||
|
actual, readErr := os.ReadFile(fixture.configPath)
|
||||||
|
if readErr != nil || string(actual) != string(source) {
|
||||||
|
t.Fatalf("replacement settings changed: %q, %v", actual, readErr)
|
||||||
|
}
|
||||||
|
assertNoAugmentRuntimeWrites(t, fixture)
|
||||||
|
assertNoAugmentTransactionArtifacts(t, fixture.homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparedAugmentInstallRejectsStaleSettingsBeforeStaging(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
document, err := readAugmentDocument()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read Auggie document: %v", err)
|
||||||
|
}
|
||||||
|
paths, nodePath, err := preflightAugmentInstallation(fixture.config, document)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("preflight Auggie installation: %v", err)
|
||||||
|
}
|
||||||
|
hooks, _ := removeManagedAugmentHooks(document.hooks, "", paths.runtimeRoot)
|
||||||
|
hooks["PreToolUse"] = append(
|
||||||
|
hooks["PreToolUse"],
|
||||||
|
buildAugmentGroup(buildAugmentHandler(paths.guardWrapperPath)),
|
||||||
|
)
|
||||||
|
hooks["PostToolUse"] = append(
|
||||||
|
hooks["PostToolUse"],
|
||||||
|
buildAugmentGroup(buildAugmentHandler(paths.auditWrapperPath)),
|
||||||
|
)
|
||||||
|
rendered, err := renderAugmentDocument(document, hooks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render Auggie document: %v", err)
|
||||||
|
}
|
||||||
|
writeAugmentTestFile(
|
||||||
|
t,
|
||||||
|
fixture.configPath,
|
||||||
|
[]byte("{\"hooks\":{\"Notification\":[]}}\n"),
|
||||||
|
0600,
|
||||||
|
)
|
||||||
|
_, err = prepareAugmentInstallationChanges(
|
||||||
|
fixture.config,
|
||||||
|
paths,
|
||||||
|
nodePath,
|
||||||
|
rendered,
|
||||||
|
)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "changed before update") {
|
||||||
|
t.Fatalf("stale settings error = %v", err)
|
||||||
|
}
|
||||||
|
assertNoAugmentRuntimeWrites(t, fixture)
|
||||||
|
assertNoAugmentTransactionArtifacts(t, fixture.homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentOrphanRuntimeArtifactsFailBeforeSettingsWrites(t *testing.T) {
|
||||||
|
for _, runtimeName := range []string{
|
||||||
|
"private.key",
|
||||||
|
augmentGuardScript,
|
||||||
|
augmentAuditScript,
|
||||||
|
augmentGuardWrapperName(),
|
||||||
|
augmentAuditWrapperName(),
|
||||||
|
"chain-state.json",
|
||||||
|
"status-cache.json",
|
||||||
|
"error.log",
|
||||||
|
} {
|
||||||
|
t.Run(runtimeName, func(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
artifact := filepath.Join(fixture.agentDir, runtimeName)
|
||||||
|
writeAugmentTestFile(t, artifact, []byte("orphan\n"), 0600)
|
||||||
|
err := fixture.plugin.Install(fixture.config)
|
||||||
|
if err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "identity cannot be verified") {
|
||||||
|
t.Fatalf("orphan runtime error = %v", err)
|
||||||
|
}
|
||||||
|
raw, readErr := os.ReadFile(artifact)
|
||||||
|
if readErr != nil || string(raw) != "orphan\n" {
|
||||||
|
t.Fatalf("orphan artifact changed: %q, %v", raw, readErr)
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("settings written after orphan error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentMismatchedRuntimeIdentityFailsBeforeWrites(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
writeAugmentTestObject(t, fixture.runtimeConfig, map[string]any{
|
||||||
|
"agent_id": "another-agent", "agent_name": augmentAgentKey,
|
||||||
|
})
|
||||||
|
original, err := os.ReadFile(fixture.runtimeConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read runtime config: %v", err)
|
||||||
|
}
|
||||||
|
err = fixture.plugin.Install(fixture.config)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "identity does not match") {
|
||||||
|
t.Fatalf("identity error = %v", err)
|
||||||
|
}
|
||||||
|
actual, readErr := os.ReadFile(fixture.runtimeConfig)
|
||||||
|
if readErr != nil || string(actual) != string(original) {
|
||||||
|
t.Fatalf("runtime config changed: %q, %v", actual, readErr)
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("settings written after identity error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentLinkedSettingsRuntimeAndWrappersAreRejected(t *testing.T) {
|
||||||
|
for _, kind := range []string{
|
||||||
|
"configuration",
|
||||||
|
"settings",
|
||||||
|
"runtime",
|
||||||
|
"agent-directory",
|
||||||
|
"runtime-config",
|
||||||
|
"wrapper",
|
||||||
|
} {
|
||||||
|
t.Run(kind, func(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
target := filepath.Join(t.TempDir(), kind+"-target")
|
||||||
|
switch kind {
|
||||||
|
case "settings":
|
||||||
|
writeAugmentTestFile(t, target, []byte("{}\n"), 0600)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fixture.configPath), 0700); err != nil {
|
||||||
|
t.Fatalf("create settings directory: %v", err)
|
||||||
|
}
|
||||||
|
augmentSymlinkOrSkip(t, target, fixture.configPath)
|
||||||
|
case "configuration":
|
||||||
|
if err := os.MkdirAll(target, 0700); err != nil {
|
||||||
|
t.Fatalf("create target directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(filepath.Dir(fixture.configPath)), 0700); err != nil {
|
||||||
|
t.Fatalf("create configuration parent: %v", err)
|
||||||
|
}
|
||||||
|
augmentSymlinkOrSkip(t, target, filepath.Dir(fixture.configPath))
|
||||||
|
case "runtime":
|
||||||
|
if err := os.MkdirAll(target, 0700); err != nil {
|
||||||
|
t.Fatalf("create target directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(filepath.Dir(fixture.agentDir)), 0700); err != nil {
|
||||||
|
t.Fatalf("create runtime parent: %v", err)
|
||||||
|
}
|
||||||
|
augmentSymlinkOrSkip(t, target, filepath.Dir(fixture.agentDir))
|
||||||
|
case "agent-directory":
|
||||||
|
if err := os.MkdirAll(target, 0700); err != nil {
|
||||||
|
t.Fatalf("create target directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fixture.agentDir), 0700); err != nil {
|
||||||
|
t.Fatalf("create runtime root: %v", err)
|
||||||
|
}
|
||||||
|
augmentSymlinkOrSkip(t, target, fixture.agentDir)
|
||||||
|
case "runtime-config":
|
||||||
|
writeAugmentTestFile(
|
||||||
|
t,
|
||||||
|
target,
|
||||||
|
[]byte(`{"agent_id":"agent-1","agent_name":"augment"}`),
|
||||||
|
0600,
|
||||||
|
)
|
||||||
|
if err := os.MkdirAll(fixture.agentDir, 0700); err != nil {
|
||||||
|
t.Fatalf("create agent directory: %v", err)
|
||||||
|
}
|
||||||
|
augmentSymlinkOrSkip(t, target, fixture.runtimeConfig)
|
||||||
|
case "wrapper":
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
writeAugmentTestFile(t, target, []byte("external wrapper\n"), 0600)
|
||||||
|
if err := os.Remove(fixture.guardWrapper); err != nil {
|
||||||
|
t.Fatalf("remove guard wrapper: %v", err)
|
||||||
|
}
|
||||||
|
augmentSymlinkOrSkip(t, target, fixture.guardWrapper)
|
||||||
|
}
|
||||||
|
err := fixture.plugin.Install(fixture.config)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "physical") {
|
||||||
|
t.Fatalf("linked %s error = %v", kind, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentInstallValidatesRuntimeInputsBeforeWrites(t *testing.T) {
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
name, field, value, want string
|
||||||
|
}{
|
||||||
|
{"agent-name", "agent_name", "codex", "requires agent name augment"},
|
||||||
|
{"agent-id", "agent_id", "../escape", "invalid agent ID"},
|
||||||
|
{"organization", "org_id", " ", "organization ID is required"},
|
||||||
|
{"key-id", "kid", " ", "key ID is required"},
|
||||||
|
{"private-key", "private_key", "invalid", "canonical 32-byte"},
|
||||||
|
{"token", "token", " ", "non-whitespace"},
|
||||||
|
{"base-url", "base_url", "https://api.elydora.com?q=1", "query parameters"},
|
||||||
|
{"guard-path", "guard_script_path", "outside", "managed agent directory"},
|
||||||
|
{"audit-path", "hook_script", "outside", "managed agent directory"},
|
||||||
|
} {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
switch testCase.field {
|
||||||
|
case "agent_name":
|
||||||
|
fixture.config.AgentName = testCase.value
|
||||||
|
case "agent_id":
|
||||||
|
fixture.config.AgentID = testCase.value
|
||||||
|
case "org_id":
|
||||||
|
fixture.config.OrgID = testCase.value
|
||||||
|
case "kid":
|
||||||
|
fixture.config.KID = testCase.value
|
||||||
|
case "private_key":
|
||||||
|
fixture.config.PrivateKey = testCase.value
|
||||||
|
case "token":
|
||||||
|
fixture.config.Token = testCase.value
|
||||||
|
case "base_url":
|
||||||
|
fixture.config.BaseURL = testCase.value
|
||||||
|
case "guard_script_path":
|
||||||
|
fixture.config.GuardScriptPath = testCase.value
|
||||||
|
case "hook_script":
|
||||||
|
fixture.config.HookScript = testCase.value
|
||||||
|
}
|
||||||
|
err := fixture.plugin.Install(fixture.config)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||||
|
t.Fatalf("validation error = %v, want %q", err, testCase.want)
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("settings written after validation error: %v", err)
|
||||||
|
}
|
||||||
|
assertNoAugmentRuntimeWrites(t, fixture)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentInstallLeavesNoTransactionArtifacts(t *testing.T) {
|
||||||
|
fixture := prepareAugmentFixture(t, augmentFixtureOptions{})
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
assertNoAugmentTransactionArtifacts(t, fixture.homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
|
||||||
|
original := `{"owner":"user"}`
|
||||||
|
fixture := prepareAugmentFixture(
|
||||||
|
t,
|
||||||
|
augmentFixtureOptions{existingRaw: &original},
|
||||||
|
)
|
||||||
|
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||||
|
t.Fatalf("install Auggie hooks: %v", err)
|
||||||
|
}
|
||||||
|
before, err := os.ReadFile(fixture.configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read installed settings: %v", err)
|
||||||
|
}
|
||||||
|
fixture.plugin.rename = func(source, destination string) error {
|
||||||
|
if sameAugmentPath(destination, fixture.configPath) &&
|
||||||
|
strings.HasSuffix(source, ".tmp") {
|
||||||
|
return errors.New("injected Auggie uninstall failure")
|
||||||
|
}
|
||||||
|
return os.Rename(source, destination)
|
||||||
|
}
|
||||||
|
err = fixture.plugin.Uninstall(augmentTestAgentID)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "injected Auggie uninstall failure") {
|
||||||
|
t.Fatalf("uninstall error = %v", err)
|
||||||
|
}
|
||||||
|
after, readErr := os.ReadFile(fixture.configPath)
|
||||||
|
if readErr != nil || string(after) != string(before) {
|
||||||
|
t.Fatalf("Auggie settings changed: %q, %v", after, readErr)
|
||||||
|
}
|
||||||
|
assertNoAugmentTransactionArtifacts(t, fixture.homeDir)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user