Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a6efb5f
feat: add agent catalog API and integrate with project settings
nikhilachale Jun 15, 2026
19a0cc1
Implement agent authorization status checks and update frontend to re…
nikhilachale Jun 16, 2026
b44e388
fix: simplify orchestrator replacement retry flow
nikhilachale Jun 17, 2026
39a1d7b
refactor: cache agent catalog ,remove AgentCounts schema and related…
nikhilachale Jun 18, 2026
783dd0d
fix: clarify orchestrator replacement recovery state
nikhilachale Jun 19, 2026
c34fcd4
feat: enhance project settings and agent management
nikhilachale Jun 22, 2026
bf79fe1
chore: format with prettier [skip ci]
github-actions[bot] Jun 22, 2026
34122fb
feat: add AuthStatus method documentation and improve error handling …
nikhilachale Jun 22, 2026
2da9afc
feat: enhance agent authentication and configuration management
nikhilachale Jun 30, 2026
0e615a9
chore: format with prettier [skip ci]
github-actions[bot] Jun 30, 2026
3e9ff0d
test: fix session manager fake after rebase
nikhilachale Jun 30, 2026
da82acf
feat: enhance agent authentication status checks
nikhilachale Jun 30, 2026
ab26d92
feat: clear environment variables in auth status tests for Aider and …
nikhilachale Jun 30, 2026
a4fd851
feat: enhance error handling in authentication status functions and u…
nikhilachale Jun 30, 2026
c24a7a0
feat: integrate fallback agents in RequiredAgentField and streamline …
nikhilachale Jun 30, 2026
cdf4922
Merge branch 'main' into agent-switcher-updated
nikhilachale Jul 1, 2026
a14921a
fix: refactor Sidebar test imports and parameters for clarity
nikhilachale Jul 1, 2026
1a708c6
refactor: remove orchestrator retirement logic and related tests
nikhilachale Jul 1, 2026
65e5218
feat: enhance agent management and error handling
nikhilachale Jul 1, 2026
13a4af7
chore: format with prettier [skip ci]
github-actions[bot] Jul 1, 2026
d024d4d
fix: satisfy backend lint checks
nikhilachale Jul 1, 2026
9164e6e
Merge upstream/main into agent-switcher-updated
nikhilachale Jul 1, 2026
f5aede5
refactor: clean up authentication logic and improve error handling ac…
nikhilachale Jul 1, 2026
f11df8f
chore: format with prettier [skip ci]
github-actions[bot] Jul 1, 2026
abbd29c
Merge branch 'main' into agent-switcher-updated
nikhilachale Jul 2, 2026
3cde7f4
feat(authprobe): enhance status classification for authentication out…
nikhilachale Jul 2, 2026
74142f9
chore: format with prettier [skip ci]
github-actions[bot] Jul 2, 2026
8f7dea2
refactor: remove shell environment authentication logic and update re…
nikhilachale Jul 3, 2026
a1bc193
Merge remote-tracking branch 'upstream/main' into agent-switcher-updated
nikhilachale Jul 4, 2026
4c59192
feat(tests): integrate QueryClient for agent data in CreateProjectAge…
nikhilachale Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ For detailed architecture diagrams, data flows, and load-bearing rules, see [arc
| [Architecture](docs/architecture.md) | System architecture, data flows, and load-bearing rules |
| [Backend Code Structure](docs/backend-code-structure.md) | Package-by-package ownership and dependency rules |
| [AGENTS.md](AGENTS.md) | Contributor and worker-agent contract |
| [Agent Adapter Contract](docs/agent/README.md) | Agent adapter interface and hook behavior |

---

Expand Down
12 changes: 12 additions & 0 deletions backend/internal/adapters/agent/agy/agy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,15 @@ func TestHooksLifecycle(t *testing.T) {
t.Fatal("expected hooks to be uninstalled after UninstallHooks")
}
}

func TestAuthStatus(t *testing.T) {
plugin := &Plugin{resolvedBinary: "agy"}

status, err := plugin.AuthStatus(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != ports.AgentAuthStatusAuthorized {
t.Errorf("AuthStatus() = %v, want AgentAuthStatusAuthorized", status)
}
}
17 changes: 17 additions & 0 deletions backend/internal/adapters/agent/agy/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package agy

import (
"context"

"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)

var _ ports.AgentAuthChecker = (*Plugin)(nil)

// AuthStatus returns the plugin's local authentication status.
func (p *Plugin) AuthStatus(ctx context.Context) (ports.AgentAuthStatus, error) {
if _, err := p.ResolveBinary(ctx); err != nil {
return ports.AgentAuthStatusUnknown, err
}
return ports.AgentAuthStatusAuthorized, nil
}
8 changes: 8 additions & 0 deletions backend/internal/adapters/agent/agy/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package agy

import "context"

// ResolveBinary resolves the executable path for the plugin.
func (p *Plugin) ResolveBinary(ctx context.Context) (string, error) {
return p.agyBinary(ctx)
}
131 changes: 131 additions & 0 deletions backend/internal/adapters/agent/aider/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package aider

import (
"context"
"os"
"path/filepath"
"strings"

"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)

var _ ports.AgentAuthChecker = (*Plugin)(nil)

// AuthStatus returns the plugin's local authentication status.
func (p *Plugin) AuthStatus(ctx context.Context) (ports.AgentAuthStatus, error) {
if _, err := p.ResolveBinary(ctx); err != nil {
return ports.AgentAuthStatusUnknown, err
}
if status, ok, err := aiderLocalAuthStatus(ctx); err != nil {
return ports.AgentAuthStatusUnknown, err
} else if ok {
return status, nil
}
return ports.AgentAuthStatusUnknown, nil
}

var aiderAPIKeyEnvVars = []string{
"AIDER_API_KEY",
"AIDER_OPENAI_API_KEY",
"AIDER_ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"GEMINI_API_KEY",
"GOOGLE_API_KEY",
"OPENROUTER_API_KEY",
"DEEPSEEK_API_KEY",
"GROQ_API_KEY",
"XAI_API_KEY",
"MISTRAL_API_KEY",
"COHERE_API_KEY",
}

func aiderLocalAuthStatus(ctx context.Context) (ports.AgentAuthStatus, bool, error) {
if err := ctx.Err(); err != nil {
return ports.AgentAuthStatusUnknown, false, err
}
for _, name := range aiderAPIKeyEnvVars {
if strings.TrimSpace(os.Getenv(name)) != "" {
return ports.AgentAuthStatusAuthorized, true, nil
}
}

for _, path := range aiderConfigPaths() {
status, ok, err := aiderAuthStatusFromFile(path)
if err != nil {
return ports.AgentAuthStatusUnknown, false, err
}
if ok {
return status, true, nil
}
}
return ports.AgentAuthStatusUnknown, false, nil
}

func aiderConfigPaths() []string {
paths := []string{}
if cwd, err := os.Getwd(); err == nil && cwd != "" {
paths = append(paths,
filepath.Join(cwd, ".env"),
filepath.Join(cwd, ".aider.conf.yml"),
filepath.Join(cwd, ".aider.conf.yaml"),
)
}
if home, err := os.UserHomeDir(); err == nil && home != "" {
paths = append(paths,
filepath.Join(home, ".env"),
filepath.Join(home, ".aider.conf.yml"),
filepath.Join(home, ".aider.conf.yaml"),
)
}
return paths
}

func aiderAuthStatusFromFile(path string) (ports.AgentAuthStatus, bool, error) {
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return ports.AgentAuthStatusUnknown, false, nil
}
if err != nil {
return ports.AgentAuthStatusUnknown, false, err
}
text := strings.TrimSpace(string(data))
if text == "" {
return ports.AgentAuthStatusUnknown, false, nil
}
if fileContainsAPIKey(text) {
return ports.AgentAuthStatusAuthorized, true, nil
}
return ports.AgentAuthStatusUnknown, false, nil
}

func fileContainsAPIKey(text string) bool {
for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
lower := strings.ToLower(line)
if !strings.Contains(lower, "api") || !strings.Contains(lower, "key") {
continue
}
if valueAfterAssignment(line) != "" {
return true
}
}
return false
}

func valueAfterAssignment(line string) string {
for _, sep := range []string{"=", ":"} {
before, after, ok := strings.Cut(line, sep)
if !ok || !strings.Contains(strings.ToLower(before), "key") {
continue
}
value := strings.Trim(strings.TrimSpace(after), `"'`)
if value != "" && !strings.EqualFold(value, "null") && !strings.EqualFold(value, "none") {
return value
}
}
return ""
}
77 changes: 77 additions & 0 deletions backend/internal/adapters/agent/aider/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package aider

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)

func TestAiderLocalAuthStatusAuthorizedWithProviderEnv(t *testing.T) {
clearAiderAuthEnv(t)
t.Setenv("OPENAI_API_KEY", "sk-test")

status, ok, err := aiderLocalAuthStatus(context.Background())
if err != nil {
t.Fatal(err)
}
if !ok || status != ports.AgentAuthStatusAuthorized {
t.Fatalf("status = (%q, %v), want (%q, true)", status, ok, ports.AgentAuthStatusAuthorized)
}
}

func TestAiderLocalAuthStatusAuthorizedWithConfigFile(t *testing.T) {
clearAiderAuthEnv(t)
home := t.TempDir()
t.Setenv("HOME", home)
if err := os.WriteFile(filepath.Join(home, ".aider.conf.yml"), []byte("openai-api-key: sk-test\n"), 0o600); err != nil {
t.Fatal(err)
}

status, ok, err := aiderLocalAuthStatus(context.Background())
if err != nil {
t.Fatal(err)
}
if !ok || status != ports.AgentAuthStatusAuthorized {
t.Fatalf("status = (%q, %v), want (%q, true)", status, ok, ports.AgentAuthStatusAuthorized)
}
}

func TestAiderLocalAuthStatusAuthorizedWithDotEnv(t *testing.T) {
clearAiderAuthEnv(t)
home := t.TempDir()
t.Setenv("HOME", home)
if err := os.WriteFile(filepath.Join(home, ".env"), []byte("ANTHROPIC_API_KEY=sk-ant-test\n"), 0o600); err != nil {
t.Fatal(err)
}

status, ok, err := aiderLocalAuthStatus(context.Background())
if err != nil {
t.Fatal(err)
}
if !ok || status != ports.AgentAuthStatusAuthorized {
t.Fatalf("status = (%q, %v), want (%q, true)", status, ok, ports.AgentAuthStatusAuthorized)
}
}

func TestAiderLocalAuthStatusUnknownWhenMissing(t *testing.T) {
clearAiderAuthEnv(t)
t.Setenv("HOME", t.TempDir())

status, ok, err := aiderLocalAuthStatus(context.Background())
if err != nil {
t.Fatal(err)
}
if ok || status != ports.AgentAuthStatusUnknown {
t.Fatalf("status = (%q, %v), want (%q, false)", status, ok, ports.AgentAuthStatusUnknown)
}
}

func clearAiderAuthEnv(t *testing.T) {
t.Helper()
for _, name := range aiderAPIKeyEnvVars {
t.Setenv(name, "")
}
}
8 changes: 8 additions & 0 deletions backend/internal/adapters/agent/aider/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package aider

import "context"

// ResolveBinary resolves the executable path for the plugin.
func (p *Plugin) ResolveBinary(ctx context.Context) (string, error) {
return p.aiderBinary(ctx)
}
121 changes: 121 additions & 0 deletions backend/internal/adapters/agent/amp/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package amp

import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"time"

"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/authprobe"
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)

var _ ports.AgentAuthChecker = (*Plugin)(nil)

// AuthStatus returns the plugin's local authentication status.
func (p *Plugin) AuthStatus(ctx context.Context) (ports.AgentAuthStatus, error) {
binary, err := p.ResolveBinary(ctx)
if err != nil {
return ports.AgentAuthStatusUnknown, err
}
if status, ok, err := ampLocalAuthStatus(ctx); err != nil || ok {
return status, err
}
return ampUsageAuthStatus(ctx, binary)
}

func ampLocalAuthStatus(ctx context.Context) (ports.AgentAuthStatus, bool, error) {
if err := ctx.Err(); err != nil {
return ports.AgentAuthStatusUnknown, false, err
}
if strings.TrimSpace(os.Getenv("AMP_API_KEY")) != "" {
return ports.AgentAuthStatusAuthorized, true, nil
}
status, ok, err := ampSettingsAuthStatus(ampSettingsPath())
if err != nil {
return ports.AgentAuthStatusUnknown, false, err
}
return status, ok, nil
}

func ampSettingsPath() string {
if path := strings.TrimSpace(os.Getenv("AMP_SETTINGS_FILE")); path != "" {
return expandHome(path)
}
if home, err := os.UserHomeDir(); err == nil && home != "" {
return filepath.Join(home, ".config", "amp", "settings.json")
}
return ""
}

func ampSettingsAuthStatus(path string) (ports.AgentAuthStatus, bool, error) {
if path == "" {
return ports.AgentAuthStatusUnknown, false, nil
}
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return ports.AgentAuthStatusUnknown, false, nil
}
return ports.AgentAuthStatusUnknown, false, err
}
var settings map[string]any
if err := json.Unmarshal(data, &settings); err != nil {
return ports.AgentAuthStatusUnknown, false, err
}
for _, key := range []string{"amp.apiKey", "amp.api_key", "apiKey", "api_key"} {
if value, ok := settings[key]; ok {
if strings.TrimSpace(stringValue(value)) != "" {
return ports.AgentAuthStatusAuthorized, true, nil
}
return ports.AgentAuthStatusUnauthorized, true, nil
}
}
return ports.AgentAuthStatusUnknown, false, nil
}

func ampUsageAuthStatus(ctx context.Context, binary string) (ports.AgentAuthStatus, error) {
if binary == "" {
return ports.AgentAuthStatusUnknown, nil
}
probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

out, err := authprobe.CmdRunner(probeCtx, binary, "usage", "--no-color")
if probeCtx.Err() != nil {
return ports.AgentAuthStatusUnknown, probeCtx.Err()
}
if status := authprobe.StatusFromText(string(out)); status != ports.AgentAuthStatusUnknown {
return status, nil
}
if err == nil {
return ports.AgentAuthStatusAuthorized, nil
}
return ports.AgentAuthStatusUnknown, nil
}

func stringValue(value any) string {
switch v := value.(type) {
case string:
return v
default:
return ""
}
}

func expandHome(path string) string {
if path == "~" {
if home, err := os.UserHomeDir(); err == nil {
return home
}
}
if strings.HasPrefix(path, "~/") {
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, strings.TrimPrefix(path, "~/"))
}
}
return path
}
Loading
Loading