Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6764f51
docs: plan Cursor agent provider integration
nvdorman Aug 12, 2026
e78e413
docs: strengthen Cursor error test plan
nvdorman Aug 12, 2026
2d925cd
Add secret-safe Cursor metadata client
nvdorman Aug 12, 2026
370e564
Fix Cursor API error message truncation on rune boundaries
nvdorman Aug 12, 2026
1239c09
Add Cursor cloud agent lifecycle and streaming
nvdorman Aug 12, 2026
254709e
Document Cursor stream endpoint provenance and assert its path in tests
nvdorman Aug 12, 2026
ffc4d0b
Add Cursor agent provider capability
nvdorman Aug 12, 2026
41145d1
Guard Cursor agent provider activation
nvdorman Aug 12, 2026
7481cfc
Connect Cursor without changing the active LLM
nvdorman Aug 12, 2026
0f37d85
Fix round 1: close remaining Cursor model-boundary gaps
nvdorman Aug 12, 2026
55d0822
Add Cursor cloud agent delegation tools
nvdorman Aug 12, 2026
01972f3
Normalize and bound Cursor progress output
nvdorman Aug 12, 2026
ce386ea
Show Cursor as an agent integration
nvdorman Aug 12, 2026
b134936
Show Cursor model discovery errors
nvdorman Aug 12, 2026
b4c75ad
Document and verify Cursor agent integration
nvdorman Aug 12, 2026
d9735e8
Clarify Cursor agent integration behavior
nvdorman Aug 12, 2026
e95a8c9
Harden Cursor setup and stream recovery
nvdorman Aug 12, 2026
ced330f
Design DNS64-safe provider validation
nvdorman Aug 12, 2026
739cf55
Plan DNS64-safe provider validation
nvdorman Aug 12, 2026
f23b03c
Add standards-based DNS64 prefix discovery
nvdorman Aug 12, 2026
76d6b92
Fix provider connections on DNS64 networks
nvdorman Aug 12, 2026
117fd94
Reject ambiguous DNS64 prefixes during discovery
nvdorman Aug 12, 2026
3c1f8d7
Keep agent integrations out of the model command
nvdorman Aug 12, 2026
13da3d1
Survive dropped Cursor streams without losing results
nvdorman Aug 12, 2026
ca1c07b
Name the missing resource in Cursor 404s
nvdorman Aug 12, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ web/src/sample/
.mcp.json
opencode.json
AGENTS.md
.worktrees/
.superpowers/

# Release artifacts
dist/
Expand Down
25 changes: 23 additions & 2 deletions cmd/antares/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -763,10 +763,19 @@ func cmdModel(args []string) error {
return nil
}
prevProvider := cfg.Model.Provider
cfg.Model.Default = args[0]
resultProvider := prevProvider
if len(args) > 1 {
cfg.Model.Provider = args[1]
resultProvider = args[1]
}
// An agent integration can never become the active chat model — refused
// here before any mutation, exactly as /model, the API, and the TUI do.
if providers.CapabilityOf(cfg, resultProvider) == providers.CapabilityAgent {
return fmt.Errorf(
"%s is an agent integration, not a chat model provider; use the cursor_agent tool",
resultProvider)
}
cfg.Model.Default = args[0]
cfg.Model.Provider = resultProvider
if cfg.Model.Provider != prevProvider {
cfg.ClearInlineModelCredentials()
} else if p, ok := cfg.Providers[cfg.Model.Provider]; ok &&
Expand Down Expand Up @@ -821,6 +830,9 @@ func cmdProvider(args []string) error {
return fmt.Errorf("usage: antares provider use <id>")
}
id := args[1]
if providers.CapabilityOf(cfg, id) == providers.CapabilityAgent {
return fmt.Errorf("%s is an agent integration, not a chat model provider; use the cursor_agent tool", id)
}
if !providers.Connected(cfg, id) {
return fmt.Errorf("%s is not connected — run `antares provider add %s <api-key>`", id, id)
}
Expand All @@ -844,6 +856,15 @@ func cmdProvider(args []string) error {
if known && info.NeedsKey && key == "" && !providers.Connected(cfg, id) {
return fmt.Errorf("%s needs an API key: antares provider add %s <api-key>", info.Label, id)
}
if providers.CapabilityOf(cfg, id) == providers.CapabilityAgent {
providers.Connect(cfg, id, key)
if err := config.Save(cfg); err != nil {
return err
}
fmt.Printf("connected %s agent integration; active model remains %s (%s)\n",
id, cfg.Model.Default, cfg.Model.Provider)
return nil
}
providers.Activate(cfg, id, key)
if err := config.Save(cfg); err != nil {
return err
Expand Down
57 changes: 57 additions & 0 deletions cmd/antares/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"strings"
"testing"

"github.com/enowdev/antares/internal/config"
)

// `antares model <id> cursor` used to persist an agent integration as the
// active chat provider, which every other selector already refuses.
func TestModelCommandRejectsAgentProvider(t *testing.T) {
t.Setenv("ANTARES_HOME", t.TempDir())
cfg := config.Default()
beforeProvider, beforeModel := cfg.Model.Provider, cfg.Model.Default
if err := config.Save(cfg); err != nil {
t.Fatal(err)
}

err := cmdModel([]string{"claude-sonnet-5", "cursor"})
if err == nil || !strings.Contains(err.Error(), "cursor_agent") {
t.Fatalf("model set to cursor error = %v, want an agent-integration refusal", err)
}

after, err := config.Reload()
if err != nil {
t.Fatal(err)
}
if after.Model.Provider != beforeProvider || after.Model.Default != beforeModel {
t.Fatalf("active model changed to %s (%s), want %s (%s)",
after.Model.Default, after.Model.Provider, beforeModel, beforeProvider)
}
}

func TestModelCommandStillSwitchesLLMProvider(t *testing.T) {
t.Setenv("ANTARES_HOME", t.TempDir())
if err := config.Save(config.Default()); err != nil {
t.Fatal(err)
}

output := captureProviderStdout(t, func() {
if err := cmdModel([]string{"gpt-5", "openai"}); err != nil {
t.Fatal(err)
}
})
if !strings.Contains(output, "active model: gpt-5 (openai)") {
t.Fatalf("model set output = %q", output)
}

after, err := config.Reload()
if err != nil {
t.Fatal(err)
}
if after.Model.Provider != "openai" || after.Model.Default != "gpt-5" {
t.Fatalf("active model = %s (%s), want gpt-5 (openai)", after.Model.Default, after.Model.Provider)
}
}
70 changes: 70 additions & 0 deletions cmd/antares/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"io"
"os"
"strings"
"testing"

"github.com/enowdev/antares/internal/config"
)

func TestProviderAddAndUseCursorPreserveActiveModel(t *testing.T) {
t.Setenv("ANTARES_HOME", t.TempDir())
cfg := config.Default()
beforeProvider, beforeModel := cfg.Model.Provider, cfg.Model.Default
if err := config.Save(cfg); err != nil {
t.Fatal(err)
}

output := captureProviderStdout(t, func() {
if err := cmdProvider([]string{"add", "cursor", "synthetic-key"}); err != nil {
t.Fatal(err)
}
})
if !strings.Contains(output, "connected cursor agent integration") {
t.Fatalf("add output = %q", output)
}
connected, err := config.Reload()
if err != nil {
t.Fatal(err)
}
if connected.Model.Provider != beforeProvider || connected.Model.Default != beforeModel {
t.Fatalf("model changed to %s/%s", connected.Model.Provider, connected.Model.Default)
}
if p := connected.Providers["cursor"]; !p.Enabled || p.APIKey != "synthetic-key" || p.Kind != "cursor-agent" {
t.Fatalf("cursor provider = %+v", p)
}

err = cmdProvider([]string{"use", "cursor"})
if err == nil || !strings.Contains(err.Error(), "cursor_agent") {
t.Fatalf("use cursor error = %v", err)
}
afterUse, err := config.Reload()
if err != nil {
t.Fatal(err)
}
if afterUse.Model.Provider != beforeProvider || afterUse.Model.Default != beforeModel {
t.Fatalf("model changed to %s/%s", afterUse.Model.Provider, afterUse.Model.Default)
}
}

func captureProviderStdout(t *testing.T, f func()) string {
t.Helper()
old := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
t.Cleanup(func() { os.Stdout = old })
f()
if err := w.Close(); err != nil {
t.Fatal(err)
}
out, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
return string(out)
}
24 changes: 24 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ release. A model family Antares does not recognise defaults to the OpenAI
format, which is correct for the GLM, Kimi, DeepSeek, MiMo, GPT, Grok and
Hunyuan families it currently serves.

### Cursor Cloud Agents

Cursor is an agent integration, not a primary Antares model provider. Configure
the deployment-owned key through the environment:

```yaml
providers:
cursor:
kind: cursor-agent
base_url: https://api.cursor.com
api_key_env: CURSOR_API_KEY
enabled: true
timeout_seconds: 900
```

Cursor is a built-in cloud-only agent integration. Existing configuration and
providers need no migration and retain their current LLM behavior. The default
Cursor entry is enabled but disconnected; it becomes usable only when Antares
resolves its key. `CURSOR_API_KEY` works with that entry without writing a key
to YAML. One deployment key and its quota are shared by every user who can
invoke the Cursor tools. Repository-backed runs use the repository state
available to Cursor, so unpushed local changes are not included.

`model.auxiliary` is worth setting. Titles, compaction summaries, verification,
and goal judging all use it, and a small model does those as well as a large one
for a fraction of the cost.
Expand All @@ -100,6 +123,7 @@ OPENAI_API_KEY=…
OPENROUTER_API_KEY=…
OPENCODE_API_KEY=…
GEMINI_API_KEY=…
CURSOR_API_KEY=…
```

## Storage
Expand Down
Loading
Loading