fix(go): mirror transactional Auggie hooks
This commit is contained in:
1 parent
dccec10fea
commit
3ca5d701de
13 files changed
+1795
-510
No files matched your search
@@ -29,7 +29,7 @@ This repository publishes the open-source server, console, integration catalog,
|
||||
- Resolve Claude Code user hooks through `$CLAUDE_CONFIG_DIR/settings.json` with `~/.claude/settings.json` when the variable is absent. Match Claude Code's path resolution exactly: relative and empty values resolve from the current working directory, and literal tildes remain path segments. Keep project, local, managed, plugin, skill, and agent hook sources read-only. Validate the complete shipped handler schema, including command exec form, async rewake metadata, HTTP, MCP tool, prompt, and agent handlers. Register exact matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups with ten-second exec-form command handlers. Preserve the complete native snake_case payload and propagate freeze and revocation through exit code `2`. Commit each SDK's Claude Code user settings, generated runtimes, runtime config, and private key through one rollback-capable transaction. Reject installation while the user source sets `disableAllHooks`, then require `/hooks` and `claude doctor` verification for higher-scope policy effects.
|
||||
- Resolve Gemini CLI user hooks through `$GEMINI_CLI_HOME/.gemini/settings.json` with `~/.gemini/settings.json` when the variable is absent or empty. Preserve relative and literal-tilde home values as Gemini CLI does. Keep workspace, system defaults, system overrides, and extension hooks read-only. Preserve JSON comments while rejecting trailing commas and duplicate keys. Register exact matchless `BeforeTool` and `AfterTool` groups named `elydora-guard` and `elydora-audit` with ten-second command handlers. Preserve the complete native snake_case payload, emit valid JSON on exit code `0`, and propagate freeze and revocation through exit code `2`. Use encoded PowerShell commands on Windows. Commit each SDK's Gemini CLI user settings, generated runtimes, runtime config, and private key through one rollback-capable transaction. Respect `hooksConfig.enabled` and `hooksConfig.disabled`, then require `/hooks list` verification for higher-scope policy effects.
|
||||
- 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`.
|
||||
- The Node and Python SDKs commit Auggie settings, both wrappers, generated runtimes, runtime config, and private key through one rollback-capable transaction. Their status checks require exact runtime identity, canonical private keys, physical files, and exact wrapper sources, followed by effective configuration verification with `auggie tools list`. Go validates matcher syntax with its native regex engine and keeps status and uninstall available for recovery.
|
||||
- All three SDKs commit Auggie settings, both wrappers, generated runtimes, runtime config, and private key through one rollback-capable transaction. Their status checks require exact runtime identity, canonical private keys, physical files, and exact wrapper sources. Installation validates matcher syntax with JavaScript `RegExp`; status and uninstall stay available for offline recovery. Verify the effective configuration with `auggie tools list`.
|
||||
- Write Cline file hooks to `$CLINE_DIR/hooks` with `~/.cline/hooks` as the default; keep Documents and workspace hook roots read-only. Use supported event filenames, preserve official hook input byte-for-byte, use `process.execPath` in Node and Go wrappers, and resolve Python runtimes from absolute shebangs. Translate guard exit code `2` into JSON stdout with `cancel: true`. Treat hook errors, timeouts, and invalid control JSON as fail-open.
|
||||
- Resolve Codex user hooks through `$CODEX_HOME/hooks.json` with `~/.codex/hooks.json` as the default, matching Codex's existing-directory canonicalization rule. Model user JSON, inline TOML layers, project files, managed `requirements.toml`, and plugin-bundled hooks as additive stable sources. SDK adapters write only the user JSON source, register exact `PreToolUse` and `PostToolUse` match-all command groups, preserve the complete native payload, propagate freeze and revocation through exit code `2`, keep guard lookup and audit delivery fail-open with observable errors, commit user hooks and all four runtime artifacts through one rollback-capable transaction, and require `/hooks` trust review for both definition hashes.
|
||||
- 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.
|
||||
|
||||
+1
-1
@@ -94,7 +94,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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
if config.AgentID == "" {
|
||||
return fmt.Errorf("agent ID is required")
|
||||
}
|
||||
document, err := readAugmentConfig()
|
||||
document, err := readAugmentDocument()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.GuardScriptPath == "" {
|
||||
return fmt.Errorf("guard script path is required")
|
||||
}
|
||||
guardExists, err := regularFileExists(
|
||||
config.GuardScriptPath, "Elydora guard runtime",
|
||||
)
|
||||
paths, nodePath, err := preflightAugmentInstallation(config, document)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !guardExists {
|
||||
return fmt.Errorf(
|
||||
"Elydora guard runtime is missing: %s", config.GuardScriptPath,
|
||||
)
|
||||
}
|
||||
hookPath, err := hookScriptPath(config.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.HookScript != "" {
|
||||
hookPath = config.HookScript
|
||||
}
|
||||
nodePath, err := resolveNodeRuntime()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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, _ := removeManagedAugmentHooks(document.hooks, "", paths.runtimeRoot)
|
||||
hooks["PreToolUse"] = append(
|
||||
hooks["PreToolUse"],
|
||||
buildAugmentGroup(buildAugmentHandler(wrappers.guard)),
|
||||
buildAugmentGroup(buildAugmentHandler(paths.guardWrapperPath)),
|
||||
)
|
||||
hooks["PostToolUse"] = append(
|
||||
hooks["PostToolUse"],
|
||||
buildAugmentGroup(buildAugmentHandler(wrappers.audit)),
|
||||
buildAugmentGroup(buildAugmentHandler(paths.auditWrapperPath)),
|
||||
)
|
||||
next := cloneAugmentObject(document.root)
|
||||
next["hooks"] = renderAugmentHooks(hooks)
|
||||
|
||||
runtimeConfig := config
|
||||
runtimeConfig.AgentName = augmentAgentKey
|
||||
if err := GenerateHookScript(hookPath, runtimeConfig); err != nil {
|
||||
return fmt.Errorf("generate hook script: %w", err)
|
||||
rendered, err := renderAugmentDocument(document, hooks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("render Auggie user settings: %w", err)
|
||||
}
|
||||
if err := writeHookFileAtomic(
|
||||
wrappers.guard,
|
||||
buildAugmentWrapper(nodePath, config.GuardScriptPath),
|
||||
0700,
|
||||
changes, err := prepareAugmentInstallationChanges(
|
||||
config,
|
||||
paths,
|
||||
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 {
|
||||
return fmt.Errorf("write Auggie guard wrapper: %w", 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)
|
||||
return err
|
||||
}
|
||||
fmt.Println("Auggie: user-level PreToolUse and PostToolUse hooks installed.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AugmentPlugin) Uninstall(agentID string) error {
|
||||
document, err := readAugmentConfig()
|
||||
if err != nil || !document.exists {
|
||||
document, err := readAugmentDocument()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtimeRoot, err := augmentRuntimeRoot()
|
||||
if !document.exists {
|
||||
return nil
|
||||
}
|
||||
runtimeRoot, err := AgentRuntimeRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hooks, changed := removeManagedAugmentHooks(
|
||||
document.hooks, agentID, runtimeRoot,
|
||||
document.hooks,
|
||||
agentID,
|
||||
runtimeRoot,
|
||||
)
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
next := cloneAugmentObject(document.root)
|
||||
if len(hooks) == 0 {
|
||||
delete(next, "hooks")
|
||||
} else {
|
||||
next["hooks"] = renderAugmentHooks(hooks)
|
||||
rendered, err := renderAugmentDocument(document, hooks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("render Auggie user settings: %w", err)
|
||||
}
|
||||
if len(next) == 0 {
|
||||
return removeHookFile(document.configPath, "Auggie settings")
|
||||
change, err := prepareRenderedAugmentChange(rendered)
|
||||
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) {
|
||||
document, err := readAugmentConfig()
|
||||
configPath, pathErr := augmentConfigPath()
|
||||
entry := SupportedAgents[augmentAgentKey]
|
||||
status := PluginStatus{
|
||||
AgentName: augmentAgentKey,
|
||||
DisplayName: "Augment Code CLI",
|
||||
ConfigPath: document.configPath,
|
||||
AgentName: augmentAgentKey, DisplayName: entry.Name, ConfigPath: configPath,
|
||||
}
|
||||
if pathErr != nil {
|
||||
return status, pathErr
|
||||
}
|
||||
document, err := readAugmentDocument()
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
runtimeRoot, err := augmentRuntimeRoot()
|
||||
runtimeRoot, err := AgentRuntimeRoot()
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
contracts := augmentRuntimeContracts(document.hooks, runtimeRoot)
|
||||
if len(contracts) == 0 {
|
||||
status.HookConfigured = len(contracts) > 0
|
||||
if !status.HookConfigured {
|
||||
return status, nil
|
||||
}
|
||||
status.HookConfigured = true
|
||||
status.HookScriptExists, err = augmentRuntimeFilesExist(
|
||||
contracts, runtimeRoot,
|
||||
contracts,
|
||||
runtimeRoot,
|
||||
)
|
||||
if err != nil {
|
||||
return status, err
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
@@ -42,6 +45,14 @@ type augmentDocument struct {
|
||||
configPath string
|
||||
root map[string]any
|
||||
hooks augmentHooks
|
||||
raw []byte
|
||||
}
|
||||
|
||||
type augmentRenderedDocument struct {
|
||||
document *augmentDocument
|
||||
changed bool
|
||||
next []byte
|
||||
remove bool
|
||||
}
|
||||
|
||||
type augmentWrapperPaths struct {
|
||||
@@ -200,6 +211,71 @@ func readAugmentHooks(root map[string]any) (augmentHooks, error) {
|
||||
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 {
|
||||
result := make(map[string]any, len(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"
|
||||
}
|
||||
|
||||
func augmentHomeDirectory() (string, error) {
|
||||
func augmentConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func readAugmentConfig() (augmentDocument, error) {
|
||||
func readAugmentDocument() (*augmentDocument, error) {
|
||||
configPath, err := augmentConfigPath()
|
||||
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 {
|
||||
return augmentDocument{}, err
|
||||
return nil, err
|
||||
}
|
||||
hooks, err := readAugmentHooks(root)
|
||||
if err != nil {
|
||||
return augmentDocument{}, err
|
||||
if snapshot == nil {
|
||||
return createAugmentDocument(configPath), nil
|
||||
}
|
||||
return augmentDocument{
|
||||
exists: exists,
|
||||
configPath: configPath,
|
||||
root: root,
|
||||
hooks: hooks,
|
||||
}, nil
|
||||
return parseAugmentDocument(configPath, snapshot.contents)
|
||||
}
|
||||
|
||||
func prepareRenderedAugmentChange(
|
||||
rendered *augmentRenderedDocument,
|
||||
) (*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 {
|
||||
@@ -106,7 +148,9 @@ func validateAugmentMatchers(hooks augmentHooks, nodePath string) error {
|
||||
return fmt.Errorf(
|
||||
"Auggie settings group hooks.%s[%d] matcher "+
|
||||
"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 {
|
||||
agentDirectory := filepath.Join(runtimeRoot, agentID)
|
||||
func resolveAugmentWrapperPaths(agentDirectory string) augmentWrapperPaths {
|
||||
return augmentWrapperPaths{
|
||||
guard: filepath.Join(agentDirectory, augmentGuardWrapperName()),
|
||||
audit: filepath.Join(agentDirectory, augmentAuditWrapperName()),
|
||||
}
|
||||
}
|
||||
|
||||
func managedAugmentIDs(
|
||||
groups []augmentGroup,
|
||||
wrapperName, 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 {
|
||||
key := agentID
|
||||
if runtime.GOOS == "windows" {
|
||||
key = strings.ToLower(key)
|
||||
}
|
||||
result[key] = agentID
|
||||
}
|
||||
}
|
||||
func requireAugmentAbsoluteNode(nodePath string) error {
|
||||
if !filepath.IsAbs(nodePath) || !isClaudeNodeExecutable(nodePath) {
|
||||
return fmt.Errorf("Auggie hooks require an absolute Node.js executable path")
|
||||
}
|
||||
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
|
||||
return 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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -10,16 +10,20 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAugmentRegistryAndFactory(t *testing.T) {
|
||||
func TestAugmentRegistryFactoryAndGuardOwnership(t *testing.T) {
|
||||
entry := SupportedAgents[augmentAgentKey]
|
||||
if entry.Name != "Augment Code CLI" ||
|
||||
entry.ConfigDir != "~/.augment" ||
|
||||
entry.ConfigFile != "settings.json" {
|
||||
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))
|
||||
}
|
||||
if !plugin.ManagesGuardRuntime() {
|
||||
t.Fatal("Auggie plugin must own guard runtime generation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAugmentGeneratedArgumentParsers(t *testing.T) {
|
||||
@@ -46,7 +50,8 @@ func TestAugmentInstallPreservesSettingsAndIsIdempotent(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
fixture := prepareAugmentFixture(
|
||||
t, augmentFixtureOptions{existingRaw: &existing},
|
||||
t,
|
||||
augmentFixtureOptions{existingRaw: &existing},
|
||||
)
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Auggie hooks: %v", err)
|
||||
@@ -64,178 +69,77 @@ func TestAugmentInstallPreservesSettingsAndIsIdempotent(t *testing.T) {
|
||||
len(requireArray(t, hooks["PostToolUse"])) != 1 {
|
||||
t.Fatalf("unexpected hooks after repeat install: %#v", hooks)
|
||||
}
|
||||
preGroups := requireArray(t, hooks["PreToolUse"])
|
||||
managedGroup := requireObject(t, preGroups[len(preGroups)-1])
|
||||
managedGroup := requireObject(
|
||||
t,
|
||||
requireArray(t, hooks["PreToolUse"])[1],
|
||||
)
|
||||
if managedGroup["matcher"] != ".*" {
|
||||
t.Fatalf("managed matcher = %#v", managedGroup["matcher"])
|
||||
}
|
||||
guard := augmentTestManagedHandler(
|
||||
t, settings, "PreToolUse", fixture.guardWrapper,
|
||||
t,
|
||||
settings,
|
||||
"PreToolUse",
|
||||
fixture.guardWrapper,
|
||||
)
|
||||
audit := augmentTestManagedHandler(
|
||||
t, settings, "PostToolUse", fixture.auditWrapper,
|
||||
t,
|
||||
settings,
|
||||
"PostToolUse",
|
||||
fixture.auditWrapper,
|
||||
)
|
||||
for _, handler := range []map[string]any{guard, audit} {
|
||||
wantKeys := map[string]struct{}{"type": {}, "command": {}, "timeout": {}}
|
||||
if len(handler) != len(wantKeys) {
|
||||
t.Fatalf("handler has extra fields: %#v", handler)
|
||||
want := map[string]any{
|
||||
"type": "command", "command": handler["command"],
|
||||
"timeout": augmentHookTimeout,
|
||||
}
|
||||
for key := range handler {
|
||||
if _, exists := wantKeys[key]; !exists {
|
||||
t.Fatalf("handler has unexpected field %q", key)
|
||||
}
|
||||
}
|
||||
if handler["type"] != "command" || handler["timeout"] != augmentHookTimeout {
|
||||
t.Fatalf("unexpected handler: %#v", handler)
|
||||
if !reflect.DeepEqual(handler, want) {
|
||||
t.Fatalf("managed handler = %#v", handler)
|
||||
}
|
||||
}
|
||||
guardWrapper, err := os.ReadFile(fixture.guardWrapper)
|
||||
if err != nil || !strings.Contains(string(guardWrapper), augmentGuardScript) {
|
||||
t.Fatalf("guard wrapper = %q, %v", guardWrapper, err)
|
||||
nodePath, err := resolveNodeRuntime()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve Node.js runtime: %v", err)
|
||||
}
|
||||
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)
|
||||
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)
|
||||
}
|
||||
} 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)
|
||||
want := buildAugmentWrapper(nodePath, item.script)
|
||||
if string(actual) != string(want) {
|
||||
t.Fatalf("wrapper %s = %q, want %q", item.path, actual, want)
|
||||
}
|
||||
}
|
||||
runtimeConfig := readAugmentTestObject(t, fixture.runtimeConfig)
|
||||
if runtimeConfig["agent_name"] != augmentAgentKey {
|
||||
t.Fatalf("runtime agent name = %#v", runtimeConfig["agent_name"])
|
||||
if runtimeConfig["agent_name"] != augmentAgentKey ||
|
||||
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{
|
||||
filepath.Join(fixture.workspaceDir, ".augment", "settings.json"),
|
||||
filepath.Join(fixture.workspaceDir, ".augment", "settings.local.json"),
|
||||
filepath.Join(fixture.projectDir, ".augment", "settings.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
existing := `{"owner":"user","hooks":{"Notification":[]}}`
|
||||
fixture := prepareAugmentFixture(
|
||||
t, augmentFixtureOptions{existingRaw: &existing},
|
||||
t,
|
||||
augmentFixtureOptions{existingRaw: &existing},
|
||||
)
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Auggie hooks: %v", err)
|
||||
@@ -251,12 +155,15 @@ func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) {
|
||||
requireArray(t, managedGroup["hooks"]),
|
||||
map[string]any{"type": "command", "command": "user-command"},
|
||||
)
|
||||
preGroups = append(preGroups,
|
||||
preGroups = append(
|
||||
preGroups,
|
||||
map[string]any{"hooks": []any{buildAugmentHandler(
|
||||
fixture.guardWrapper + ".backup",
|
||||
)}},
|
||||
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
|
||||
@@ -274,7 +181,10 @@ func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) {
|
||||
len(requireArray(t, remainingHooks["PreToolUse"])) != 4 {
|
||||
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"])
|
||||
}
|
||||
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{})
|
||||
if err := fixture.plugin.Install(fixture.config); err != nil {
|
||||
t.Fatalf("install Auggie hooks: %v", err)
|
||||
@@ -305,7 +215,9 @@ func TestAugmentInstallReplacesStaleAndPreservesEmptyGroups(t *testing.T) {
|
||||
hooks[item.event] = append(
|
||||
requireArray(t, hooks[item.event]),
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAugmentMalformedSettingsPreventRuntimeWrites(t *testing.T) {
|
||||
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-hooks", `{"hooks":null}`, `field "hooks" must be an object`},
|
||||
{"unknown-event", `{"hooks":{"UnknownEvent":[]}}`, "unsupported event"},
|
||||
@@ -356,7 +271,8 @@ func TestAugmentMalformedSettingsPreventRuntimeWrites(t *testing.T) {
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
fixture := prepareAugmentFixture(
|
||||
t, augmentFixtureOptions{existingRaw: &testCase.raw},
|
||||
t,
|
||||
augmentFixtureOptions{existingRaw: &testCase.raw},
|
||||
)
|
||||
err := fixture.plugin.Install(fixture.config)
|
||||
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 {
|
||||
t.Fatalf("original settings changed: %q, %v", raw, readErr)
|
||||
}
|
||||
for _, path := range []string{
|
||||
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)
|
||||
}
|
||||
}
|
||||
assertNoAugmentRuntimeWrites(t, fixture)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
const augmentTestAgentID = "agent-1"
|
||||
const (
|
||||
augmentTestAgentID = "agent-1"
|
||||
augmentPrivateKey = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc"
|
||||
)
|
||||
|
||||
type augmentFixtureOptions struct {
|
||||
existingRaw *string
|
||||
withoutGuard bool
|
||||
existingRaw *string
|
||||
}
|
||||
|
||||
type augmentFixture struct {
|
||||
plugin *AugmentPlugin
|
||||
config InstallConfig
|
||||
homeDir string
|
||||
workspaceDir string
|
||||
projectDir string
|
||||
agentDir string
|
||||
configPath string
|
||||
guardPath string
|
||||
@@ -42,49 +44,34 @@ func prepareAugmentFixture(
|
||||
options augmentFixtureOptions,
|
||||
) *augmentFixture {
|
||||
t.Helper()
|
||||
homeDir := filepath.Join(t.TempDir(), "home with spaces and 'quote")
|
||||
workspaceDir := filepath.Join(homeDir, "workspace")
|
||||
root := t.TempDir()
|
||||
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)
|
||||
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 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)
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte(*options.existingRaw), 0600); err != nil {
|
||||
t.Fatalf("write Auggie settings: %v", err)
|
||||
}
|
||||
}
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
guardPath := filepath.Join(agentDir, augmentGuardScript)
|
||||
return &augmentFixture{
|
||||
plugin: &AugmentPlugin{},
|
||||
homeDir: homeDir,
|
||||
workspaceDir: workspaceDir,
|
||||
agentDir: agentDir,
|
||||
plugin: &AugmentPlugin{},
|
||||
config: InstallConfig{
|
||||
AgentName: augmentAgentKey,
|
||||
OrgID: "org-1",
|
||||
AgentID: augmentTestAgentID,
|
||||
PrivateKey: "test-key",
|
||||
KID: "kid-1",
|
||||
BaseURL: "https://api.elydora.test",
|
||||
GuardScriptPath: guardPath,
|
||||
AgentName: augmentAgentKey, OrgID: "org-1", AgentID: augmentTestAgentID,
|
||||
PrivateKey: augmentPrivateKey, KID: "kid-1", Token: "token-1",
|
||||
BaseURL: "https://api.elydora.test", GuardScriptPath: guardPath,
|
||||
},
|
||||
configPath: configPath,
|
||||
guardPath: guardPath,
|
||||
homeDir: homeDir, projectDir: projectDir, agentDir: agentDir,
|
||||
configPath: configPath, guardPath: guardPath,
|
||||
hookPath: filepath.Join(agentDir, augmentAuditScript),
|
||||
guardWrapper: filepath.Join(agentDir, augmentGuardWrapperName()),
|
||||
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) {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(value)
|
||||
encoded, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal Auggie settings: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path, encoded, 0600); err != nil {
|
||||
t.Fatalf("write Auggie settings: %v", err)
|
||||
encoded = append(encoded, '\n')
|
||||
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(
|
||||
t *testing.T,
|
||||
settings map[string]any,
|
||||
event, wrapperPath string,
|
||||
event string,
|
||||
wrapperPath string,
|
||||
) map[string]any {
|
||||
t.Helper()
|
||||
expected := buildAugmentCommand(wrapperPath)
|
||||
@@ -140,8 +142,10 @@ func augmentTestManagedHandler(
|
||||
|
||||
func runAugmentCommand(
|
||||
t *testing.T,
|
||||
command, homeDir, payload string,
|
||||
) (int, string) {
|
||||
command string,
|
||||
fixture *augmentFixture,
|
||||
payload []byte,
|
||||
) (int, string, string) {
|
||||
t.Helper()
|
||||
var process *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -154,20 +158,67 @@ func runAugmentCommand(
|
||||
} else {
|
||||
process = exec.Command("sh", "-c", command)
|
||||
}
|
||||
process.Dir = fixture.projectDir
|
||||
process.Env = append(
|
||||
os.Environ(), "HOME="+homeDir, "USERPROFILE="+homeDir,
|
||||
os.Environ(),
|
||||
"HOME="+fixture.homeDir,
|
||||
"USERPROFILE="+fixture.homeDir,
|
||||
)
|
||||
process.Stdin = bytes.NewBufferString(payload)
|
||||
var stderr bytes.Buffer
|
||||
process.Stdin = bytes.NewReader(payload)
|
||||
var stdout, stderr bytes.Buffer
|
||||
process.Stdout = &stdout
|
||||
process.Stderr = &stderr
|
||||
err := process.Run()
|
||||
if err == nil {
|
||||
return 0, stderr.String()
|
||||
return 0, stdout.String(), stderr.String()
|
||||
}
|
||||
var exitError *exec.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)
|
||||
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