Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions backend/internal/adapters/agent/agy/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package agy

import (
"context"

"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) {
cmd, err := p.GetLaunchCommand(ctx, ports.LaunchConfig{})
if err != nil || len(cmd) == 0 {
return ports.AgentAuthStatusUnknown, err
}
return authprobe.CLIStatus(ctx, cmd[0], nil)
}
19 changes: 19 additions & 0 deletions backend/internal/adapters/agent/aider/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package aider

import (
"context"

"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) {
cmd, err := p.GetLaunchCommand(ctx, ports.LaunchConfig{})
if err != nil || len(cmd) == 0 {
return ports.AgentAuthStatusUnknown, err
}
return authprobe.CLIStatus(ctx, cmd[0], nil)
}
19 changes: 19 additions & 0 deletions backend/internal/adapters/agent/amp/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package amp

import (
"context"

"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) {
cmd, err := p.GetLaunchCommand(ctx, ports.LaunchConfig{})
if err != nil || len(cmd) == 0 {
return ports.AgentAuthStatusUnknown, err
}
return authprobe.CLIStatus(ctx, cmd[0], nil)
}
19 changes: 19 additions & 0 deletions backend/internal/adapters/agent/auggie/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package auggie

import (
"context"

"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) {
cmd, err := p.GetLaunchCommand(ctx, ports.LaunchConfig{})
if err != nil || len(cmd) == 0 {
return ports.AgentAuthStatusUnknown, err
}
return authprobe.CLIStatus(ctx, cmd[0], nil)
}
96 changes: 96 additions & 0 deletions backend/internal/adapters/agent/authprobe/authprobe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package authprobe

import (
"context"
"os/exec"
"strings"
"time"

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

// DefaultCommands are cheap local auth/status probes common across agent CLIs.
// Unsupported commands usually exit quickly with help text and are treated as
// unknown rather than unauthorized.
var DefaultCommands = [][]string{
{"auth", "status"},
{"login", "status"},
{"providers", "list"},
}

// CLIStatus runs bounded local CLI probes and classifies their output.
func CLIStatus(ctx context.Context, binary string, commands [][]string) (ports.AgentAuthStatus, error) {
if err := ctx.Err(); err != nil {
return ports.AgentAuthStatusUnknown, err
}
if binary == "" {
return ports.AgentAuthStatusUnknown, nil
}
if len(commands) == 0 {
commands = DefaultCommands
}
for _, args := range commands {
status, err := commandStatus(ctx, binary, args)
if err != nil {
return ports.AgentAuthStatusUnknown, err
}
if status != ports.AgentAuthStatusUnknown {
return status, nil
}
}
return ports.AgentAuthStatusUnknown, nil
}

func commandStatus(ctx context.Context, binary string, args []string) (ports.AgentAuthStatus, error) {
probeCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

out, err := exec.CommandContext(probeCtx, binary, args...).CombinedOutput()
if probeCtx.Err() != nil {
return ports.AgentAuthStatusUnknown, probeCtx.Err()
}
text := strings.ToLower(string(out))
if hasAny(text,
"not logged in",
"logged out",
"not authenticated",
"unauthenticated",
"authentication required",
"not authorized",
"unauthorized",
"login required",
"no credentials",
"0 credentials",
"no api key",
"no token",
`"loggedin": false`,
`"loggedin":false`,
) {
return ports.AgentAuthStatusUnauthorized, nil
}
if hasAny(text,
"logged in",
"authenticated",
"authorized",
"token valid",
"api key found",
"credentials found",
`"loggedin": true`,
`"loggedin":true`,
) {
return ports.AgentAuthStatusAuthorized, nil
}
if err != nil {
return ports.AgentAuthStatusUnknown, nil
}
return ports.AgentAuthStatusUnknown, nil
}

func hasAny(text string, needles ...string) bool {
for _, needle := range needles {
if strings.Contains(text, needle) {
return true
}
}
return false
}
19 changes: 19 additions & 0 deletions backend/internal/adapters/agent/autohand/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package autohand

import (
"context"

"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) {
cmd, err := p.GetLaunchCommand(ctx, ports.LaunchConfig{})
if err != nil || len(cmd) == 0 {
return ports.AgentAuthStatusUnknown, err
}
return authprobe.CLIStatus(ctx, cmd[0], nil)
}
31 changes: 31 additions & 0 deletions backend/internal/adapters/agent/claudecode/claudecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"runtime"
"strings"
"sync"
"time"

"github.com/google/uuid"

Expand Down Expand Up @@ -60,6 +61,7 @@ func New() *Plugin {

var _ adapters.Adapter = (*Plugin)(nil)
var _ ports.Agent = (*Plugin)(nil)
var _ ports.AgentAuthChecker = (*Plugin)(nil)

// Manifest returns the adapter's static self-description.
func (p *Plugin) Manifest() adapters.Manifest {
Expand Down Expand Up @@ -268,6 +270,35 @@ func (p *Plugin) SessionInfo(ctx context.Context, session ports.SessionRef) (por
return info, true, nil
}

// AuthStatus checks Claude Code's local authentication state without starting a
// session.
func (p *Plugin) AuthStatus(ctx context.Context) (ports.AgentAuthStatus, error) {
binary, err := p.claudeBinary(ctx)
if err != nil {
return ports.AgentAuthStatusUnknown, err
}
probeCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

out, err := exec.CommandContext(probeCtx, binary, "auth", "status").CombinedOutput()
if probeCtx.Err() != nil {
return ports.AgentAuthStatusUnknown, probeCtx.Err()
}
var status struct {
LoggedIn bool `json:"loggedIn"`
}
if json.Unmarshal(out, &status) == nil {
if status.LoggedIn {
return ports.AgentAuthStatusAuthorized, nil
}
return ports.AgentAuthStatusUnauthorized, nil
}
if err != nil {
return ports.AgentAuthStatusUnauthorized, nil
}
return ports.AgentAuthStatusUnknown, nil
}

// claudeSessionUUID maps an AO session id onto a stable Claude Code
// session UUID via UUIDv5 over a fixed namespace, so the same AO session
// always resolves to the same Claude session.
Expand Down
19 changes: 19 additions & 0 deletions backend/internal/adapters/agent/cline/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cline

import (
"context"

"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) {
cmd, err := p.GetLaunchCommand(ctx, ports.LaunchConfig{})
if err != nil || len(cmd) == 0 {
return ports.AgentAuthStatusUnknown, err
}
return authprobe.CLIStatus(ctx, cmd[0], nil)
}
43 changes: 41 additions & 2 deletions backend/internal/adapters/agent/codex/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import (
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"

"github.com/aoagents/agent-orchestrator/backend/internal/adapters"
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
Expand All @@ -34,6 +36,7 @@ func New() *Plugin {

var _ adapters.Adapter = (*Plugin)(nil)
var _ ports.Agent = (*Plugin)(nil)
var _ ports.AgentAuthChecker = (*Plugin)(nil)

// Manifest returns the adapter's static self-description.
func (p *Plugin) Manifest() adapters.Manifest {
Expand Down Expand Up @@ -146,10 +149,37 @@ func (p *Plugin) SessionInfo(ctx context.Context, session ports.SessionRef) (por
return info, true, nil
}

// AuthStatus checks Codex's local login state without making a model call.
func (p *Plugin) AuthStatus(ctx context.Context) (ports.AgentAuthStatus, error) {
binary, err := p.codexBinary(ctx)
if err != nil {
return ports.AgentAuthStatusUnknown, err
}
probeCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

out, err := exec.CommandContext(probeCtx, binary, "login", "status").CombinedOutput()
if probeCtx.Err() != nil {
return ports.AgentAuthStatusUnknown, probeCtx.Err()
}
text := strings.ToLower(string(out))
if strings.Contains(text, "not logged in") || strings.Contains(text, "logged out") {
return ports.AgentAuthStatusUnauthorized, nil
}
if strings.Contains(text, "logged in") {
return ports.AgentAuthStatusAuthorized, nil
}
if err != nil {
return ports.AgentAuthStatusUnauthorized, nil
}
return ports.AgentAuthStatusUnknown, nil
}

// ResolveCodexBinary returns the path to the codex binary on this machine,
// searching PATH then a handful of well-known install locations
// (Homebrew, Cargo, npm global). Returns "codex" as a last-ditch fallback
// so callers see a clear "command not found" rather than an empty argv.
// (Homebrew, Cargo, npm global, NVM). Returns "codex" as a last-ditch
// fallback so callers see a clear "command not found" rather than an empty
// argv.
func ResolveCodexBinary(ctx context.Context) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
Expand Down Expand Up @@ -203,6 +233,7 @@ func ResolveCodexBinary(ctx context.Context) (string, error) {
filepath.Join(home, ".cargo", "bin", "codex"),
filepath.Join(home, ".npm", "bin", "codex"),
)
candidates = append(candidates, nvmNodeBinCandidates(home, "codex")...)
}

for _, candidate := range candidates {
Expand All @@ -217,6 +248,14 @@ func ResolveCodexBinary(ctx context.Context) (string, error) {
return "", fmt.Errorf("codex: %w", ports.ErrAgentBinaryNotFound)
}

func nvmNodeBinCandidates(home, binary string) []string {
matches, err := filepath.Glob(filepath.Join(home, ".nvm", "versions", "node", "*", "bin", binary))
if err != nil || len(matches) == 0 {
return nil
}
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
return matches

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`nvmNodeBinCandidates` is missing its closing brace — the function body never terminates before `func resolveNativeWindowsCodex(...)` starts on the next line, which nests a named function declaration inside another function. This is not valid Go and the package will not compile as currently pushed:

```go
func nvmNodeBinCandidates(home, binary string) []string {
matches, err := filepath.Glob(filepath.Join(home, ".nvm", "versions", "node", "*", "bin", binary))
if err != nil || len(matches) == 0 {
return nil
}
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
return matches
func resolveNativeWindowsCodex(path string) string { // <-- missing } above


Needs a `}` after `return matches` to close `nvmNodeBinCandidates` before `resolveNativeWindowsCodex` starts. Given `TestResolveCodexBinaryFindsNVMInstallWhenPathIsSparse` was added in this same PR, this file couldn't have been built/tested in its current state — worth double-checking the push matches what was actually tested locally.

}
func resolveNativeWindowsCodex(path string) string {
if runtime.GOOS != "windows" || !strings.EqualFold(filepath.Ext(path), ".cmd") {
return path
Expand Down
22 changes: 22 additions & 0 deletions backend/internal/adapters/agent/codex/codex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ func TestGetLaunchCommandWithoutWorkspaceOmitsTrustFlag(t *testing.T) {
}
}

func TestResolveCodexBinaryFindsNVMInstallWhenPathIsSparse(t *testing.T) {
home := t.TempDir()
binDir := filepath.Join(home, ".nvm", "versions", "node", "v20.19.4", "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
t.Fatal(err)
}
want := filepath.Join(binDir, "codex")
if err := os.WriteFile(want, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("HOME", home)
t.Setenv("PATH", "")

got, err := ResolveCodexBinary(context.Background())
if err != nil {
t.Fatalf("ResolveCodexBinary: %v", err)
}
if got != want {
t.Fatalf("ResolveCodexBinary = %q, want %q", got, want)
}
}

func TestGetLaunchCommandMapsApprovalModes(t *testing.T) {
tests := []struct {
name string
Expand Down
19 changes: 19 additions & 0 deletions backend/internal/adapters/agent/continueagent/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package continueagent

import (
"context"

"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) {
cmd, err := p.GetLaunchCommand(ctx, ports.LaunchConfig{})
if err != nil || len(cmd) == 0 {
return ports.AgentAuthStatusUnknown, err
}
return authprobe.CLIStatus(ctx, cmd[0], nil)
}
Loading
Loading