Files
Elydora-Go-SDK/cmd/elydora/plugins/copilot_io.go
T

310 lines
8.8 KiB
Go

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
}