package plugins import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "runtime" "strings" ) func copilotConfigPaths() (string, string, error) { home, err := os.UserHomeDir() if err != nil { return "", "", fmt.Errorf("resolve home directory: %w", err) } copilotHome := strings.TrimSpace(os.Getenv("COPILOT_HOME")) if copilotHome == "" { copilotHome = filepath.Join(home, ".copilot") } else if !filepath.IsAbs(copilotHome) { copilotHome, err = filepath.Abs(copilotHome) if err != nil { return "", "", fmt.Errorf("resolve COPILOT_HOME: %w", err) } } workingDirectory, err := os.Getwd() if err != nil { return "", "", fmt.Errorf("resolve working directory: %w", err) } return filepath.Join(copilotHome, "hooks", copilotConfigFile), filepath.Join(workingDirectory, ".github", "hooks", "hooks.json"), nil } func readCopilotHooks(value any, label string) (copilotHooks, error) { if value == nil { return nil, fmt.Errorf(`%s field "hooks" must be an object`, label) } object, ok := value.(map[string]any) if !ok { return nil, fmt.Errorf(`%s field "hooks" must be an object`, label) } hooks := make(copilotHooks, len(object)) for event, handlerValue := range object { values, ok := handlerValue.([]any) if !ok { return nil, fmt.Errorf(`%s field "hooks.%s" must be an array`, label, event) } handlers := make([]map[string]any, 0, len(values)) for index, value := range values { handler, ok := value.(map[string]any) if !ok || handler == nil { return nil, fmt.Errorf(`%s handler hooks.%s[%d] must be an object`, label, event, index) } handlers = append(handlers, handler) } hooks[event] = handlers } return hooks, nil } func parseCopilotDocument( filePath string, raw []byte, label string, ) (*copilotDocument, error) { var value any if err := json.Unmarshal(raw, &value); err != nil { return nil, fmt.Errorf("parse %s at %s: %w", label, filePath, err) } root, ok := value.(map[string]any) if !ok || root == nil { return nil, fmt.Errorf("%s at %s must contain a JSON object", label, filePath) } if root["version"] != float64(1) { return nil, fmt.Errorf("%s at %s must declare version 1", label, filePath) } hooks := copilotHooks{} if hooksValue, exists := root["hooks"]; exists { var err error hooks, err = readCopilotHooks(hooksValue, label) if err != nil { return nil, err } } disabled := false if value, exists := root["disableAllHooks"]; exists { var valid bool disabled, valid = value.(bool) if !valid { return nil, fmt.Errorf(`%s field "disableAllHooks" must be a boolean`, label) } } return &copilotDocument{ exists: true, filePath: filePath, root: root, hooks: hooks, raw: append([]byte(nil), raw...), disabled: disabled, }, nil } func readCopilotDocument(filePath, label string) (*copilotDocument, error) { info, err := os.Lstat(filePath) if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, fmt.Errorf("inspect %s at %s: %w", label, filePath, err) } if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { return nil, fmt.Errorf("%s path is not a physical file: %s", label, filePath) } raw, err := os.ReadFile(filePath) // #nosec G304 -- filePath is a validated provider configuration path. if err != nil { return nil, fmt.Errorf("read %s at %s: %w", label, filePath, err) } return parseCopilotDocument(filePath, raw, label) } func readCopilotSources() (*copilotSources, error) { userPath, legacyPath, err := copilotConfigPaths() if err != nil { return nil, err } user, err := readCopilotDocument(userPath, "GitHub Copilot user hooks") if err != nil { return nil, err } legacy, err := readCopilotDocument(legacyPath, "GitHub Copilot project hooks") if err != nil { return nil, err } if user == nil { user = &copilotDocument{ filePath: userPath, root: map[string]any{}, hooks: copilotHooks{}, } } return &copilotSources{user: user, legacy: legacy}, nil } func prepareRenderedCopilotChange( rendered *copilotRenderedDocument, ) (*fileChange, error) { if rendered == nil || !rendered.changed { return nil, nil } return prepareSourceChange( rendered.document.filePath, "GitHub Copilot hook source", rendered.document.raw, rendered.document.exists, rendered.next, 0600, rendered.remove, ) } func prepareCopilotInstallationChanges( config InstallConfig, agentDirectory string, auditPath string, rendered []*copilotRenderedDocument, ) ([]*fileChange, error) { baseURL := config.BaseURL if baseURL == "" { baseURL = "https://api.elydora.com" } runtimeConfig, err := json.MarshalIndent(agentRuntimeConfig{ OrgID: config.OrgID, AgentID: config.AgentID, KID: config.KID, BaseURL: baseURL, Token: config.Token, AgentName: copilotAgentKey, }, "", " ") if err != nil { return nil, fmt.Errorf("encode Elydora runtime config: %w", err) } runtimeConfig = append(runtimeConfig, '\n') if len(runtimeConfig) > maxRuntimeConfigBytes { return nil, fmt.Errorf( "Elydora runtime config exceeds %d bytes after JSON encoding", maxRuntimeConfigBytes, ) } items := []struct { path, label string content []byte mode os.FileMode }{ {filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600}, {filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600}, {auditPath, "Elydora audit runtime", []byte(buildHookScript(copilotAgentKey, config.AgentID)), 0700}, } changes := make([]*fileChange, 0, len(items)+len(rendered)) for _, item := range items { if err := validateRuntimeFileTarget(item.path, item.label); err != nil { return nil, err } change, err := prepareFileChange(item.path, item.label, item.content, item.mode) if err != nil { return nil, err } changes = append(changes, change) } for _, document := range rendered { change, err := prepareRenderedCopilotChange(document) if err != nil { return nil, err } changes = append(changes, change) } return changes, nil } func activeCopilotContracts( sources *copilotSources, runtimeRoot string, ) []copilotRuntimeContract { contracts := make([]copilotRuntimeContract, 0) for _, document := range []*copilotDocument{sources.user, sources.legacy} { if document == nil || document.disabled { continue } contracts = append(contracts, copilotRuntimeContracts(document.hooks, runtimeRoot)...) } unique := map[string]copilotRuntimeContract{} for _, contract := range contracts { key := contract.agentID if runtime.GOOS == "windows" { key = strings.ToLower(key) } unique[key] = contract } result := make([]copilotRuntimeContract, 0, len(unique)) for _, contract := range unique { result = append(result, contract) } return result } func configuredCopilotPath( sources *copilotSources, runtimeRoot string, ) string { if !sources.user.disabled && len(copilotRuntimeContracts(sources.user.hooks, runtimeRoot)) > 0 { return sources.user.filePath } if sources.legacy != nil && !sources.legacy.disabled && len(copilotRuntimeContracts(sources.legacy.hooks, runtimeRoot)) > 0 { return sources.legacy.filePath } return sources.user.filePath } func copilotPhysicalFileExists(path, label string) (bool, error) { info, err := os.Lstat(path) if errors.Is(err, os.ErrNotExist) { return false, nil } if err != nil { return false, fmt.Errorf("inspect %s at %s: %w", label, path, err) } if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { return false, fmt.Errorf("%s path is not a physical file: %s", label, path) } return true, nil } func copilotRuntimeFilesExist(contracts []copilotRuntimeContract) (bool, error) { for _, contract := range contracts { configPath := filepath.Join(filepath.Dir(contract.guardPath), "config.json") configExists, err := copilotPhysicalFileExists(configPath, "Elydora runtime config") if err != nil { return false, err } if !configExists { continue } raw, exists, err := readOptionalFile(configPath, "Elydora runtime config") if err != nil { return false, err } if !exists { continue } var value any if err := json.Unmarshal(raw, &value); err != nil { return false, fmt.Errorf("parse Elydora runtime config at %s: %w", configPath, err) } config, ok := value.(map[string]any) if !ok || config == nil { return false, fmt.Errorf("Elydora runtime config at %s must contain a JSON object", configPath) } agentID, ok := config["agent_id"].(string) if !ok || config["agent_name"] != copilotAgentKey || !sameCopilotAgentID(agentID, contract.agentID) { continue } guardExists, err := copilotPhysicalFileExists(contract.guardPath, "Elydora guard runtime") if err != nil { return false, err } auditExists, err := copilotPhysicalFileExists(contract.auditPath, "Elydora audit runtime") if err != nil { return false, err } if guardExists && auditExists { return true, nil } } return false, nil }